From 01d91e194a57da03d96d0c378ab48637811d8877 Mon Sep 17 00:00:00 2001 From: cherry2250 Date: Fri, 24 Oct 2025 15:08:50 +0900 Subject: [PATCH] =?UTF-8?q?event=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/register/page.tsx | 780 +++++++++++------- src/app/(main)/events/create/page.tsx | 100 +++ .../events/create/steps/ApprovalStep.tsx | 347 ++++++++ .../events/create/steps/ChannelStep.tsx | 413 ++++++++++ .../events/create/steps/ContentEditStep.tsx | 159 ++++ .../create/steps/ContentPreviewStep.tsx | 368 +++++++++ .../events/create/steps/ObjectiveStep.tsx | 157 ++++ .../create/steps/RecommendationStep.tsx | 330 ++++++++ src/app/(main)/profile/page.tsx | 3 +- src/app/page.tsx | 469 ++++++++++- 10 files changed, 2792 insertions(+), 334 deletions(-) create mode 100644 src/app/(main)/events/create/page.tsx create mode 100644 src/app/(main)/events/create/steps/ApprovalStep.tsx create mode 100644 src/app/(main)/events/create/steps/ChannelStep.tsx create mode 100644 src/app/(main)/events/create/steps/ContentEditStep.tsx create mode 100644 src/app/(main)/events/create/steps/ContentPreviewStep.tsx create mode 100644 src/app/(main)/events/create/steps/ObjectiveStep.tsx create mode 100644 src/app/(main)/events/create/steps/RecommendationStep.tsx diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index d385a7b..8dc959c 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { z } from 'zod'; import { Box, @@ -18,24 +18,27 @@ import { Checkbox, FormControlLabel, FormGroup, + Link, } from '@mui/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 { useAuthStore } from '@/stores/authStore'; // 각 단계별 유효성 검사 스키마 -const step1Schema = z.object({ - email: z.string().email('올바른 이메일 형식이 아닙니다'), - password: z - .string() - .min(8, '비밀번호는 8자 이상이어야 합니다') - .regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'), - confirmPassword: z.string(), -}).refine((data) => data.password === data.confirmPassword, { - message: '비밀번호가 일치하지 않습니다', - path: ['confirmPassword'], -}); +const step1Schema = z + .object({ + email: z.string().email('올바른 이메일 형식이 아닙니다'), + password: z + .string() + .min(8, '비밀번호는 8자 이상이어야 합니다') + .regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + message: '비밀번호가 일치하지 않습니다', + path: ['confirmPassword'], + }); const step2Schema = z.object({ name: z.string().min(2, '이름은 2자 이상이어야 합니다'), @@ -47,6 +50,10 @@ const step2Schema = z.object({ const step3Schema = z.object({ 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, '업종을 선택해주세요'), businessLocation: z.string().optional(), agreeTerms: z.boolean().refine((val) => val === true, { @@ -64,20 +71,34 @@ type Step3Data = z.infer; type RegisterData = Step1Data & Step2Data & Step3Data; -type Step = '계정정보' | '개인정보' | '사업장정보'; - -export default function RegisterPage() { +function RegisterForm() { const router = useRouter(); + const searchParams = useSearchParams(); const { showToast, setLoading } = useUIStore(); const { login } = useAuthStore(); - const [currentStep, setCurrentStep] = useState('계정정보'); + // URL 쿼리에서 step 파라미터 읽기 (기본값: 1) + const stepParam = searchParams.get('step'); + const initialStep = stepParam ? parseInt(stepParam) : 1; + + const [currentStep, setCurrentStep] = useState(initialStep); const [formData, setFormData] = useState>({ agreeMarketing: false, }); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [errors, setErrors] = useState>({}); + 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 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)}`; }; - const steps: Step[] = ['계정정보', '개인정보', '사업장정보']; - const stepIndex = steps.indexOf(currentStep) + 1; - const progress = (stepIndex / steps.length) * 100; + const formatBusinessNumber = (value: string) => { + const numbers = value.replace(/[^\d]/g, ''); + 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({}); try { - if (step === '계정정보') { + if (step === 1) { step1Schema.parse(formData); - } else if (step === '개인정보') { + } else if (step === 2) { step2Schema.parse(formData); - } else if (step === '사업장정보') { + } else if (step === 3) { step3Schema.parse(formData); } return true; @@ -111,6 +169,12 @@ export default function RegisterPage() { } }); setErrors(newErrors); + + // 첫 번째 에러 메시지를 Toast로 표시 + const firstError = error.errors[0]; + if (firstError) { + showToast(firstError.message, 'error'); + } } return false; } @@ -121,27 +185,23 @@ export default function RegisterPage() { return; } - if (currentStep === '계정정보') { - setCurrentStep('개인정보'); - } else if (currentStep === '개인정보') { - setCurrentStep('사업장정보'); - } else if (currentStep === '사업장정보') { + if (currentStep < 3) { + setCurrentStep(currentStep + 1); + } else { handleSubmit(); } }; const handleBack = () => { - if (currentStep === '개인정보') { - setCurrentStep('계정정보'); - } else if (currentStep === '사업장정보') { - setCurrentStep('개인정보'); + if (currentStep > 1) { + setCurrentStep(currentStep - 1); } else { router.back(); } }; const handleSubmit = async () => { - if (!validateStep('사업장정보')) { + if (!validateStep(3)) { return; } @@ -152,7 +212,7 @@ export default function RegisterPage() { // 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 = { id: '1', @@ -179,326 +239,424 @@ export default function RegisterPage() { minHeight: '100vh', display: 'flex', flexDirection: 'column', - bgcolor: 'background.default', + justifyContent: 'center', + alignItems: 'center', + px: 3, + py: 8, + background: 'linear-gradient(135deg, #FFF 0%, #F5F5F5 100%)', }} > - {/* Header */} - - - - - - 회원가입 - - + {/* Header with back button */} + + + + + + 회원가입 + + - {/* Progress Bar */} - - - - {stepIndex}/3 단계 - - + {/* Progress Bar */} + + + + {currentStep}/3 단계 + + - {/* Funnel Content */} - - {currentStep === '계정정보' && ( - - - 계정 정보를 입력해주세요 - - - 로그인에 사용할 이메일과 비밀번호를 설정합니다 - + {/* Step 1: 계정정보 */} + {currentStep === 1 && ( + + + 계정 정보를 입력해주세요 + + + 로그인에 사용할 이메일과 비밀번호를 설정합니다 + - - setFormData({ ...formData, email: e.target.value })} - error={!!errors.email} - helperText={errors.email || '이메일 주소로 로그인합니다'} - required - /> + + setFormData({ ...formData, email: e.target.value })} + error={!!errors.email} + helperText={errors.email || '이메일 주소로 로그인합니다'} + required + /> - setFormData({ ...formData, password: e.target.value })} - error={!!errors.password} - helperText={errors.password} - required - InputProps={{ - endAdornment: ( - - setShowPassword(!showPassword)} edge="end"> - {showPassword ? : } - - - ), - }} - /> + setFormData({ ...formData, password: e.target.value })} + error={!!errors.password} + helperText={errors.password} + required + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} edge="end"> + {showPassword ? : } + + + ), + }} + /> - setFormData({ ...formData, confirmPassword: e.target.value })} - error={!!errors.confirmPassword} - helperText={errors.confirmPassword} - required - InputProps={{ - endAdornment: ( - - setShowConfirmPassword(!showConfirmPassword)} edge="end"> - {showConfirmPassword ? : } - - - ), - }} - /> + setFormData({ ...formData, confirmPassword: e.target.value })} + error={!!errors.confirmPassword} + helperText={errors.confirmPassword} + required + InputProps={{ + endAdornment: ( + + setShowConfirmPassword(!showConfirmPassword)} + edge="end" + > + {showConfirmPassword ? : } + + + ), + }} + /> + + + + + 이미 계정이 있으신가요?{' '} + + 로그인 + + + + + + )} + + {/* Step 2: 개인정보 */} + {currentStep === 2 && ( + + + 개인 정보를 입력해주세요 + + + 서비스 이용을 위한 기본 정보입니다 + + + + setFormData({ ...formData, name: e.target.value })} + error={!!errors.name} + helperText={errors.name} + required + /> + + { + const formatted = formatPhoneNumber(e.target.value); + setFormData({ ...formData, phone: formatted }); + }} + error={!!errors.phone} + helperText={errors.phone} + required + /> + + + - + + )} - {currentStep === '개인정보' && ( - - - 개인 정보를 입력해주세요 - - - 서비스 이용을 위한 기본 정보입니다 - + {/* Step 3: 사업장정보 */} + {currentStep === 3 && ( + + + 사업장 정보를 입력해주세요 + + + 맞춤형 이벤트 추천을 위한 정보입니다 + - + + setFormData({ ...formData, businessName: e.target.value })} + error={!!errors.businessName} + helperText={errors.businessName} + required + /> + + setFormData({ ...formData, name: e.target.value })} - error={!!errors.name} - helperText={errors.name} - required - /> - - { - const formatted = formatPhoneNumber(e.target.value); - setFormData({ ...formData, phone: formatted }); + const formatted = formatBusinessNumber(e.target.value); + setFormData({ ...formData, businessNumber: formatted }); + setIsBusinessNumberVerified(false); }} - error={!!errors.phone} - helperText={errors.phone} + error={!!errors.businessNumber} + helperText={ + errors.businessNumber || + (isBusinessNumberVerified + ? '✓ 인증 완료' + : '사업자 번호를 입력하고 인증해주세요') + } required + sx={{ + '& .MuiFormHelperText-root': { + color: isBusinessNumberVerified ? 'success.main' : undefined, + }, + }} /> - - - - - + - - )} - {currentStep === '사업장정보' && ( - - - 사업장 정보를 입력해주세요 - - - 맞춤형 이벤트 추천을 위한 정보입니다 - + + 업종 + + {errors.businessType && ( + + {errors.businessType} + + )} + - - setFormData({ ...formData, businessName: e.target.value })} - error={!!errors.businessName} - helperText={errors.businessName} - required - /> + setFormData({ ...formData, businessLocation: e.target.value })} + helperText="선택 사항입니다" + /> - - 업종 - - {errors.businessType && ( - - {errors.businessType} - - )} - - - setFormData({ ...formData, businessLocation: e.target.value })} - helperText="선택 사항입니다" - /> - - - + + + { + const checked = e.target.checked; + setFormData({ + ...formData, + agreeTerms: checked, + agreePrivacy: checked, + agreeMarketing: checked, + }); + }} + /> + } + label={ + + 전체 동의 + + } + /> + { - const checked = e.target.checked; - setFormData({ - ...formData, - agreeTerms: checked, - agreePrivacy: checked, - agreeMarketing: checked, - }); - }} + checked={formData.agreeTerms || false} + onChange={(e) => + setFormData({ ...formData, agreeTerms: e.target.checked }) + } /> } label={ - - 전체 동의 + + [필수] 이용약관 동의 } /> - - - setFormData({ ...formData, agreeTerms: e.target.checked }) - } - /> - } - label={ - - [필수] 이용약관 동의 - - } - /> - - setFormData({ ...formData, agreePrivacy: e.target.checked }) - } - /> - } - label={ - - [필수] 개인정보 처리방침 동의 - - } - /> - - setFormData({ ...formData, agreeMarketing: e.target.checked }) - } - /> - } - label={ - - [선택] 마케팅 정보 수신 동의 - - } - /> - - - {(errors.agreeTerms || errors.agreePrivacy) && ( - - 필수 약관에 동의해주세요 - - )} - - - - - - + 필수 약관에 동의해주세요 + + )} - + + + + + + + )} - + ); } + +export default function RegisterPage() { + return ( + + Loading... + + } + > + + + ); +} diff --git a/src/app/(main)/events/create/page.tsx b/src/app/(main)/events/create/page.tsx new file mode 100644 index 0000000..24991c8 --- /dev/null +++ b/src/app/(main)/events/create/page.tsx @@ -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 ( + ( + { + history.push('recommendation', { objective }); + }} + /> + )} + recommendation={({ context, history }) => ( + { + history.push('channel', { ...context, recommendation }); + }} + onBack={() => { + history.go(-1); + }} + /> + )} + channel={({ context, history }) => ( + { + history.push('approval', { ...context, channels }); + }} + onBack={() => { + history.go(-1); + }} + /> + )} + approval={({ context, history }) => ( + { + history.go(-1); + }} + /> + )} + /> + ); +} diff --git a/src/app/(main)/events/create/steps/ApprovalStep.tsx b/src/app/(main)/events/create/steps/ApprovalStep.tsx new file mode 100644 index 0000000..4729545 --- /dev/null +++ b/src/app/(main)/events/create/steps/ApprovalStep.tsx @@ -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 = { + uriTV: '우리동네TV', + ringoBiz: '링고비즈', + genieTV: '지니TV', + sns: 'SNS', + }; + + return channels?.map((ch) => channelMap[ch] || ch) || []; + }; + + return ( + + + {/* Header */} + + + + + + 최종 승인 + + + + {/* Title Section */} + + + + 이벤트를 확인해주세요 + + + 모든 정보를 검토한 후 배포하세요 + + + + {/* Event Summary Card */} + + + + {eventData.recommendation?.title || '이벤트 제목'} + + + + + + + + + + + 이벤트 기간 + + + 2025.02.01 ~ 2025.02.28 + + + + + 목표 참여자 + + + {eventData.recommendation?.expectedParticipants || 0}명 + + + + + 예상 비용 + + + {eventData.recommendation?.estimatedCost.toLocaleString() || 0}원 + + + + + 예상 ROI + + + {eventData.recommendation?.roi || 0}% + + + + + + + {/* Event Details */} + + 이벤트 상세 + + + + + + + + 이벤트 제목 + + + {eventData.recommendation?.title} + + + + + + + + + + + + + + + 경품 + + + {eventData.recommendation?.prize} + + + + + + + + + + + + + + + 참여 방법 + + + {eventData.recommendation?.participationMethod} + + + + + + + {/* Distribution Channels */} + + 배포 채널 + + + + + + {getChannelNames(eventData.channels).map((channel) => ( + + ))} + + + + + + {/* Terms Agreement */} + + + setAgreeTerms(e.target.checked)} + /> + } + label={ + + 이벤트 약관 및 개인정보 처리방침에 동의합니다{' '} + (필수) + + } + /> + setTermsDialogOpen(true)} + sx={{ color: 'error.main', ml: 4, mt: 1 }} + > + 약관 보기 + + + + + {/* Action Buttons */} + + + + + + + {/* Terms Dialog */} + setTermsDialogOpen(false)} + maxWidth="sm" + fullWidth + > + 이벤트 약관 + + + 제1조 (목적) + + + 본 약관은 KT AI 이벤트 마케팅 서비스를 통해 진행되는 이벤트의 참여 및 개인정보 처리에 관한 + 사항을 규정합니다. + + + + 제2조 (개인정보 수집 및 이용) + + + 수집 항목: 이름, 전화번호, 이메일 + + + 이용 목적: 이벤트 참여 확인 및 경품 제공 + + + 보유 기간: 이벤트 종료 후 6개월 + + + + 제3조 (당첨자 발표) + + + 당첨자는 이벤트 종료 후 7일 이내 개별 연락 드립니다. + + + + + + + + {/* Success Dialog */} + { + setSuccessDialogOpen(false); + onApprove(); + }} + > + + + + 배포 완료! + + + 이벤트가 성공적으로 배포되었습니다. +
+ 실시간으로 참여자를 확인할 수 있습니다. +
+ + + +
+
+
+ ); +} diff --git a/src/app/(main)/events/create/steps/ChannelStep.tsx b/src/app/(main)/events/create/steps/ChannelStep.tsx new file mode 100644 index 0000000..ec7bee7 --- /dev/null +++ b/src/app/(main)/events/create/steps/ChannelStep.tsx @@ -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; +} + +interface ChannelStepProps { + onNext: (channels: string[]) => void; + onBack: () => void; +} + +export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { + const [channels, setChannels] = useState([ + { 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 ( + + + {/* Header */} + + + + + + 배포 채널 선택 + + + + + (최소 1개 이상) + + + {/* 우리동네TV */} + + + handleChannelToggle('uriTV')} + /> + } + label="우리동네TV" + sx={{ mb: channels[0].selected ? 2 : 0 }} + /> + + {channels[0].selected && ( + + + 반경 + + + + + 노출 시간대 + + + + + 예상 노출: 5만명 + + + 비용: 8만원 + + + )} + + + + {/* 링고비즈 */} + + + handleChannelToggle('ringoBiz')} + /> + } + label="링고비즈" + sx={{ mb: channels[1].selected ? 2 : 0 }} + /> + + {channels[1].selected && ( + + + + + 연결음 자동 업데이트 + + + 예상 노출: 3만명 + + + 비용: 무료 + + + )} + + + + {/* 지니TV 광고 */} + + + handleChannelToggle('genieTV')} + /> + } + label="지니TV 광고" + sx={{ mb: channels[2].selected ? 2 : 0 }} + /> + + {channels[2].selected && ( + + + 지역 + + + + + 노출 시간대 + + + + handleConfigChange('genieTV', 'budget', e.target.value)} + InputProps={{ inputProps: { min: 0, step: 10000 } }} + sx={{ mb: 2 }} + /> + + + 예상 노출:{' '} + + {getChannelConfig('genieTV', 'budget') + ? `${(Math.floor(parseInt(getChannelConfig('genieTV', 'budget')) / 100) * 1000 / 10000).toFixed(1)}만명` + : '계산중...'} + + + + )} + + + + {/* SNS */} + + + handleChannelToggle('sns')} + /> + } + label="SNS" + sx={{ mb: channels[3].selected ? 2 : 0 }} + /> + + {channels[3].selected && ( + + + 플랫폼 선택 + + + handleConfigChange('sns', 'instagram', e.target.checked.toString()) + } + /> + } + label="Instagram" + sx={{ display: 'block' }} + /> + + handleConfigChange('sns', 'naver', e.target.checked.toString()) + } + /> + } + label="Naver Blog" + sx={{ display: 'block' }} + /> + + handleConfigChange('sns', 'kakao', e.target.checked.toString()) + } + /> + } + label="Kakao Channel" + sx={{ display: 'block', mb: 2 }} + /> + + + 예약 게시 + + + + + 예상 노출: - + + + 비용: 무료 + + + )} + + + + {/* Summary */} + + + + 총 예상 비용 + + {totalCost.toLocaleString()}원 + + + + 총 예상 노출 + + {totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'} + + + + + + {/* Action Buttons */} + + + + + + + ); +} diff --git a/src/app/(main)/events/create/steps/ContentEditStep.tsx b/src/app/(main)/events/create/steps/ContentEditStep.tsx new file mode 100644 index 0000000..cb037c5 --- /dev/null +++ b/src/app/(main)/events/create/steps/ContentEditStep.tsx @@ -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 ( + + + {/* Header */} + + + + + + 콘텐츠 편집 + + + + + {/* Preview Section */} + + + 미리보기 + + + + + celebration + + + {title || '제목을 입력하세요'} + + + {prize || '경품을 입력하세요'} + + + {guide || '참여 안내를 입력하세요'} + + + + + + {/* Edit Section */} + + + 편집 + + + + + + + + 텍스트 편집 + + + + + + setTitle(e.target.value)} + inputProps={{ maxLength: 50 }} + helperText={`${title.length}/50자`} + /> + + + + setPrize(e.target.value)} + inputProps={{ maxLength: 30 }} + helperText={`${prize.length}/30자`} + /> + + + + setGuide(e.target.value)} + multiline + rows={3} + inputProps={{ maxLength: 100 }} + helperText={`${guide.length}/100자`} + /> + + + + + + + + {/* Action Buttons */} + + + + + + + ); +} diff --git a/src/app/(main)/events/create/steps/ContentPreviewStep.tsx b/src/app/(main)/events/create/steps/ContentPreviewStep.tsx new file mode 100644 index 0000000..8186c53 --- /dev/null +++ b/src/app/(main)/events/create/steps/ContentPreviewStep.tsx @@ -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(null); + const [fullscreenOpen, setFullscreenOpen] = useState(false); + const [fullscreenStyle, setFullscreenStyle] = useState(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 ( + + + + + + + + SNS 이미지 생성 + + + + + + + AI 이미지 생성 중 + + + 딥러닝 모델이 이벤트에 어울리는 +
+ 이미지를 생성하고 있어요... +
+ + 예상 시간: 5초 + +
+
+
+ ); + } + + return ( + + + {/* Header */} + + + + + + SNS 이미지 생성 + + + + handleStyleSelect(e.target.value)}> + {imageStyles.map((style) => ( + + + {style.name} + + + handleStyleSelect(style.id)} + > + {selectedStyle === style.id && ( + + + check + + + )} + + + + + {style.icon} + + + {title} + + + {prize} + + + + + + } + label="" + sx={{ display: 'none' }} + /> + + + + + ))} + + + {/* Action Buttons */} + + + + + + + {/* Fullscreen Dialog */} + setFullscreenOpen(false)} + maxWidth={false} + PaperProps={{ + sx: { + bgcolor: 'rgba(0, 0, 0, 0.95)', + boxShadow: 'none', + maxWidth: '90vw', + maxHeight: '90vh', + }, + }} + > + + setFullscreenOpen(false)} + sx={{ + position: 'absolute', + top: 16, + right: 16, + bgcolor: 'rgba(255, 255, 255, 0.9)', + '&:hover': { bgcolor: 'white' }, + }} + > + close + + + {fullscreenStyle && ( + + + {fullscreenStyle.icon} + + + {title} + + + {prize} + + + )} + + + + ); +} diff --git a/src/app/(main)/events/create/steps/ObjectiveStep.tsx b/src/app/(main)/events/create/steps/ObjectiveStep.tsx new file mode 100644 index 0000000..a58e478 --- /dev/null +++ b/src/app/(main)/events/create/steps/ObjectiveStep.tsx @@ -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: , + title: '신규 고객 유치', + description: '새로운 고객을 확보하여 매장을 성장시키고 싶어요', + }, + { + id: 'revisit', + icon: , + title: '재방문 유도', + description: '기존 고객의 재방문을 촉진하고 싶어요', + }, + { + id: 'sales', + icon: , + title: '매출 증대', + description: '단기간에 매출을 높이고 싶어요', + }, + { + id: 'awareness', + icon: , + title: '인지도 향상', + description: '브랜드나 매장 인지도를 높이고 싶어요', + }, +]; + +interface ObjectiveStepProps { + onNext: (objective: EventObjective) => void; +} + +export default function ObjectiveStep({ onNext }: ObjectiveStepProps) { + const [selected, setSelected] = useState(null); + + const handleNext = () => { + if (selected) { + onNext(selected); + } + }; + + return ( + + + {/* Title Section */} + + + + 이벤트 목적을 선택해주세요 + + + AI가 목적에 맞는 최적의 이벤트를 추천해드립니다 + + + + {/* Purpose Options */} + setSelected(e.target.value as EventObjective)}> + + {objectives.map((objective) => ( + + setSelected(objective.id)} + > + + + {objective.icon} + + + {objective.title} + + + {objective.description} + + + } + label="" + sx={{ m: 0 }} + /> + + + + + ))} + + + + {/* Info Box */} + + + + + 선택하신 목적에 따라 AI가 업종, 지역, 계절 트렌드를 분석하여 가장 효과적인 이벤트를 추천합니다. + + + + + {/* Action Buttons */} + + + + + + ); +} diff --git a/src/app/(main)/events/create/steps/RecommendationStep.tsx b/src/app/(main)/events/create/steps/RecommendationStep.tsx new file mode 100644 index 0000000..f4245e1 --- /dev/null +++ b/src/app/(main)/events/create/steps/RecommendationStep.tsx @@ -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('low'); + const [selected, setSelected] = useState(null); + const [editedData, setEditedData] = useState>({}); + + 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 ( + + + {/* Header */} + + + + + + AI 이벤트 추천 + + + + {/* Trends Analysis */} + + + + + + AI 트렌드 분석 + + + + + + 📍 업종 트렌드 + + + 음식점업 신년 프로모션 트렌드 + + + + + 🗺️ 지역 트렌드 + + + 강남구 음식점 할인 이벤트 증가 + + + + + ☀️ 시즌 트렌드 + + + 설 연휴 특수 대비 고객 유치 전략 + + + + + + + {/* Budget Selection */} + + + 예산별 추천 이벤트 + + + 각 예산별 2가지 방식 (온라인 1개, 오프라인 1개)을 추천합니다 + + setSelectedBudget(value)} + variant="fullWidth" + sx={{ mb: 4 }} + > + + + + + + + {/* Recommendations */} + setSelected(e.target.value)}> + + {budgetRecommendations.map((rec) => ( + + setSelected(rec.id)} + > + + + + } label="" sx={{ m: 0 }} /> + + + handleEditTitle(rec.id, e.target.value)} + onClick={(e) => e.stopPropagation()} + sx={{ mb: 2 }} + InputProps={{ + endAdornment: , + }} + /> + + + + 경품 + + handleEditPrize(rec.id, e.target.value)} + onClick={(e) => e.stopPropagation()} + InputProps={{ + endAdornment: , + }} + /> + + + + + + 참여 방법 + + + {rec.participationMethod} + + + + + 예상 참여 + + + {rec.expectedParticipants}명 + + + + + 예상 비용 + + + {(rec.estimatedCost / 10000).toFixed(0)}만원 + + + + + 투자대비수익률 + + + {rec.roi}% + + + + + + + ))} + + + + {/* Action Buttons */} + + + + + + + ); +} diff --git a/src/app/(main)/profile/page.tsx b/src/app/(main)/profile/page.tsx index f131d0b..0b3f4b9 100644 --- a/src/app/(main)/profile/page.tsx +++ b/src/app/(main)/profile/page.tsx @@ -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 { setLoading(true); diff --git a/src/app/page.tsx b/src/app/page.tsx index bd316e4..702266a 100644 --- a/src/app/page.tsx +++ b/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() { + 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 ( - - + + {/* Welcome Section */} + + + 안녕하세요, {mockUser.name}님! + + + 오늘도 성공적인 이벤트를 준비해보세요 ✨ + + + + {/* KPI Cards */} + + + + + + + 진행 중 + + + {activeEvents.length}개 + + + + + + + + + + 총 참여자 + + + {totalParticipants.toLocaleString()}명 + + + + + + + + + + 평균 ROI + + + {avgROI}% + + + + + + + {/* Quick Actions */} + + + 빠른 시작 + + + + + + + + + + 새 이벤트 + + + + + + + + + + + + 분석 + + + + + + + + {/* Active Events */} + + + + 진행 중인 이벤트 + + + + + {activeEvents.length === 0 ? ( + + + + + event_busy + + + + 진행 중인 이벤트가 없습니다 + + + + + ) : ( + + {activeEvents.map((event) => ( + handleEventClick(event.id)} + > + + + + {event.title} + + + {event.status} + + + + 📅 {event.startDate} ~ {event.endDate} + + + + + 참여자 + + + {event.participants.toLocaleString()}명 + + + + + ROI + + + {event.roi}% + + + + + + ))} + + )} + + + {/* Recent Activity */} + + + 최근 활동 + + + + {mockActivities.map((activity, index) => ( + 0 ? 3 : 0, + mt: index > 0 ? 3 : 0, + borderTop: index > 0 ? 1 : 0, + borderColor: 'divider', + }} + > + + + + + + {activity.text} + + + {activity.time} + + + + ))} + + + + + + {/* Floating Action Button */} + - - KT AI 이벤트 마케팅 - - - 소상공인을 위한 스마트 마케팅 - - - 프로젝트 초기 설정이 완료되었습니다. -
- 화면 개발은 사용자와 함께 진행됩니다. -
-
-
+ + + ); }