mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2026-06-13 02:19:12 +00:00
이벤트 목록 Mock 데이터 적용 및 Participation API 연동
- 이벤트 목록 페이지에 Mock 데이터 적용 (evt_2025012301 등 4개 이벤트) - 이벤트 상세 페이지 Analytics API 임시 주석처리 (서버 이슈) - Participation API 프록시 라우트 URL 구조 수정 (/events/ 제거) - EventID localStorage 저장 기능 추가 - 상세한 console.log 추가 (생성된 eventId, objective, timestamp) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,11 @@ export default function LoginPage() {
|
||||
showToast(`${provider === 'kakao' ? '카카오톡' : '네이버'} 로그인은 준비 중입니다`, 'info');
|
||||
};
|
||||
|
||||
const handleUnavailableFeature = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
showToast('현재는 해당 기능을 제공하지 않습니다', 'info');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -233,7 +238,8 @@ export default function LoginPage() {
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mb: 4 }}>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
href="#"
|
||||
onClick={handleUnavailableFeature}
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
underline="hover"
|
||||
@@ -245,7 +251,8 @@ export default function LoginPage() {
|
||||
|
|
||||
</Typography>
|
||||
<Link
|
||||
href="/register"
|
||||
href="#"
|
||||
onClick={handleUnavailableFeature}
|
||||
variant="body2"
|
||||
color="primary"
|
||||
underline="hover"
|
||||
@@ -268,7 +275,7 @@ export default function LoginPage() {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={() => handleSocialLogin('kakao')}
|
||||
onClick={handleUnavailableFeature}
|
||||
sx={{
|
||||
py: 1.5,
|
||||
borderColor: '#FEE500',
|
||||
@@ -289,7 +296,7 @@ export default function LoginPage() {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={() => handleSocialLogin('naver')}
|
||||
onClick={handleUnavailableFeature}
|
||||
sx={{
|
||||
py: 1.5,
|
||||
borderColor: '#03C75A',
|
||||
@@ -327,11 +334,11 @@ export default function LoginPage() {
|
||||
{/* 약관 동의 안내 */}
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center', display: 'block' }}>
|
||||
회원가입 시{' '}
|
||||
<Link href="/terms" underline="hover" sx={{ color: 'text.secondary' }}>
|
||||
<Link href="#" onClick={handleUnavailableFeature} underline="hover" sx={{ color: 'text.secondary' }}>
|
||||
이용약관
|
||||
</Link>{' '}
|
||||
및{' '}
|
||||
<Link href="/privacy" underline="hover" sx={{ color: 'text.secondary' }}>
|
||||
<Link href="#" onClick={handleUnavailableFeature} underline="hover" sx={{ color: 'text.secondary' }}>
|
||||
개인정보처리방침
|
||||
</Link>
|
||||
에 동의하게 됩니다.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||
import { useAuthContext } from '@/features/auth';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
|
||||
export default function LogoutPage() {
|
||||
const router = useRouter();
|
||||
const { logout } = useAuthContext();
|
||||
const { showToast } = useUIStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
console.log('🚪 로그아웃 시작');
|
||||
await logout();
|
||||
console.log('✅ 로그아웃 완료');
|
||||
showToast('로그아웃되었습니다', 'success');
|
||||
|
||||
// 로그인 페이지로 리디렉션
|
||||
setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('❌ 로그아웃 에러:', error);
|
||||
showToast('로그아웃 중 오류가 발생했습니다', 'error');
|
||||
|
||||
// 에러가 발생해도 로그인 페이지로 이동
|
||||
setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
handleLogout();
|
||||
}, [logout, router, showToast]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'linear-gradient(135deg, #FFF 0%, #F5F5F5 100%)',
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={60} sx={{ mb: 3 }} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
로그아웃 중입니다...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -155,52 +155,71 @@ export default function EventDetailPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
||||
|
||||
// Analytics API 호출
|
||||
// Analytics API 호출 (임시 주석처리 - 서버 이슈)
|
||||
const fetchAnalytics = async (forceRefresh = false) => {
|
||||
try {
|
||||
if (forceRefresh) {
|
||||
console.log('🔄 데이터 새로고침 시작...');
|
||||
console.log('🔄 Mock 데이터 새로고침...');
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
console.log('📊 Analytics 데이터 로딩 시작...');
|
||||
console.log('📊 Mock Analytics 데이터 로딩...');
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
// TODO: Analytics API 서버 이슈 해결 후 주석 해제
|
||||
// Event Analytics API 병렬 호출
|
||||
const [dashboard, timeline, roi, channels] = await Promise.all([
|
||||
analyticsApi.getEventAnalytics(eventId, { refresh: forceRefresh }),
|
||||
analyticsApi.getEventTimelineAnalytics(eventId, {
|
||||
interval: chartPeriod === '7d' ? 'daily' : chartPeriod === '30d' ? 'daily' : 'daily',
|
||||
refresh: forceRefresh
|
||||
}),
|
||||
analyticsApi.getEventRoiAnalytics(eventId, { includeProjection: true, refresh: forceRefresh }),
|
||||
analyticsApi.getEventChannelAnalytics(eventId, { refresh: forceRefresh }),
|
||||
]);
|
||||
// const [dashboard, timeline, roi, channels] = await Promise.all([
|
||||
// analyticsApi.getEventAnalytics(eventId, { refresh: forceRefresh }),
|
||||
// analyticsApi.getEventTimelineAnalytics(eventId, {
|
||||
// interval: chartPeriod === '7d' ? 'daily' : chartPeriod === '30d' ? 'daily' : 'daily',
|
||||
// refresh: forceRefresh
|
||||
// }),
|
||||
// analyticsApi.getEventRoiAnalytics(eventId, { includeProjection: true, refresh: forceRefresh }),
|
||||
// analyticsApi.getEventChannelAnalytics(eventId, { refresh: forceRefresh }),
|
||||
// ]);
|
||||
|
||||
console.log('✅ Dashboard 데이터:', dashboard);
|
||||
console.log('✅ Timeline 데이터:', timeline);
|
||||
console.log('✅ ROI 데이터:', roi);
|
||||
console.log('✅ Channel 데이터:', channels);
|
||||
// 임시 Mock 데이터
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const mockAnalyticsData = {
|
||||
dashboard: {
|
||||
summary: {
|
||||
participants: mockEventData.participants,
|
||||
totalViews: mockEventData.views,
|
||||
conversionRate: mockEventData.conversion / 100,
|
||||
},
|
||||
roi: {
|
||||
roi: mockEventData.roi,
|
||||
},
|
||||
},
|
||||
timeline: {
|
||||
participations: [
|
||||
{ date: '2025-01-15', count: 12 },
|
||||
{ date: '2025-01-16', count: 18 },
|
||||
{ date: '2025-01-17', count: 25 },
|
||||
{ date: '2025-01-18', count: 31 },
|
||||
{ date: '2025-01-19', count: 22 },
|
||||
{ date: '2025-01-20', count: 20 },
|
||||
],
|
||||
},
|
||||
roi: {
|
||||
currentRoi: mockEventData.roi,
|
||||
projectedRoi: mockEventData.roi + 50,
|
||||
},
|
||||
channels: {
|
||||
distribution: [
|
||||
{ channel: '우리동네TV', participants: 45 },
|
||||
{ channel: '링고비즈', participants: 38 },
|
||||
{ channel: 'SNS', participants: 45 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Analytics 데이터 저장
|
||||
setAnalyticsData({
|
||||
dashboard,
|
||||
timeline,
|
||||
roi,
|
||||
channels,
|
||||
});
|
||||
setAnalyticsData(mockAnalyticsData);
|
||||
|
||||
// Event 기본 정보 업데이트
|
||||
setEvent(prev => ({
|
||||
...prev,
|
||||
participants: dashboard.summary.participants,
|
||||
views: dashboard.summary.totalViews,
|
||||
roi: Math.round(dashboard.roi.roi),
|
||||
conversion: Math.round(dashboard.summary.conversionRate * 100),
|
||||
}));
|
||||
|
||||
console.log('✅ Analytics 데이터 로딩 완료');
|
||||
console.log('✅ Mock Analytics 데이터 로딩 완료');
|
||||
} catch (err: any) {
|
||||
console.error('❌ Analytics 데이터 로딩 실패:', err);
|
||||
setError(err.message || 'Analytics 데이터를 불러오는데 실패했습니다.');
|
||||
|
||||
@@ -98,10 +98,27 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||
|
||||
// 새로운 eventId 생성
|
||||
const eventId = generateEventId();
|
||||
console.log('✅ 새로운 eventId 생성:', eventId);
|
||||
console.log('🎉 ========================================');
|
||||
console.log('✅ 새로운 이벤트 ID 생성:', eventId);
|
||||
console.log('📋 선택된 목적:', selected);
|
||||
console.log('🎉 ========================================');
|
||||
|
||||
// 쿠키에 저장
|
||||
setCookie('eventId', eventId, 1); // 1일 동안 유지
|
||||
console.log('🍪 쿠키에 eventId 저장 완료:', eventId);
|
||||
|
||||
// localStorage에도 저장
|
||||
try {
|
||||
localStorage.setItem('eventId', eventId);
|
||||
console.log('💾 localStorage에 eventId 저장 완료:', eventId);
|
||||
console.log('📦 저장된 데이터 확인:', {
|
||||
eventId: eventId,
|
||||
objective: selected,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ localStorage 저장 실패:', error);
|
||||
}
|
||||
|
||||
// objective와 eventId를 함께 전달
|
||||
onNext({ objective: selected, eventId });
|
||||
|
||||
@@ -57,13 +57,74 @@ export default function EventsPage() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// API 데이터 가져오기
|
||||
const { events: apiEvents, loading, error, pageInfo, refetch } = useEvents({
|
||||
page: currentPage - 1,
|
||||
size: itemsPerPage,
|
||||
sort: 'createdAt',
|
||||
order: 'desc'
|
||||
});
|
||||
// 목업 데이터
|
||||
const mockEvents = [
|
||||
{
|
||||
eventId: 'evt_2025012301',
|
||||
eventName: '신규 고객 환영 이벤트',
|
||||
status: 'PUBLISHED' as ApiEventStatus,
|
||||
startDate: '2025-01-23',
|
||||
endDate: '2025-02-23',
|
||||
participants: 1250,
|
||||
targetParticipants: 2000,
|
||||
roi: 320,
|
||||
createdAt: '2025-01-15T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '스타벅스 아메리카노 (5명)',
|
||||
participationMethod: '전화번호 입력'
|
||||
}]
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025011502',
|
||||
eventName: '재방문 고객 감사 이벤트',
|
||||
status: 'PUBLISHED' as ApiEventStatus,
|
||||
startDate: '2025-01-15',
|
||||
endDate: '2025-02-15',
|
||||
participants: 890,
|
||||
targetParticipants: 1000,
|
||||
roi: 280,
|
||||
createdAt: '2025-01-10T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '커피 쿠폰 (10명)',
|
||||
participationMethod: 'SNS 팔로우'
|
||||
}]
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025010803',
|
||||
eventName: '신년 특별 할인 이벤트',
|
||||
status: 'ENDED' as ApiEventStatus,
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-01-08',
|
||||
participants: 2500,
|
||||
targetParticipants: 2000,
|
||||
roi: 450,
|
||||
createdAt: '2024-12-28T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '10% 할인 쿠폰 (선착순 100명)',
|
||||
participationMethod: '구매 인증'
|
||||
}]
|
||||
},
|
||||
{
|
||||
eventId: 'evt_2025020104',
|
||||
eventName: '2월 신메뉴 출시 기념',
|
||||
status: 'DRAFT' as ApiEventStatus,
|
||||
startDate: '2025-02-01',
|
||||
endDate: '2025-02-28',
|
||||
participants: 0,
|
||||
targetParticipants: 1500,
|
||||
roi: 0,
|
||||
createdAt: '2025-01-25T00:00:00',
|
||||
aiRecommendations: [{
|
||||
reward: '신메뉴 무료 쿠폰 (20명)',
|
||||
participationMethod: '이메일 등록'
|
||||
}]
|
||||
},
|
||||
];
|
||||
|
||||
const loading = false;
|
||||
const error = null;
|
||||
const apiEvents = mockEvents;
|
||||
const refetch = () => {};
|
||||
|
||||
// API 상태를 UI 상태로 매핑
|
||||
const mapApiStatus = (apiStatus: ApiEventStatus): EventStatus => {
|
||||
@@ -241,41 +302,6 @@ export default function EventsPage() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4, bgcolor: '#FEE2E2' }}>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Warning sx={{ fontSize: 48, color: '#DC2626', mb: 2 }} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ mb: 1, color: '#991B1B', fontSize: { xs: '1rem', sm: '1.25rem' } }}
|
||||
>
|
||||
이벤트 목록을 불러오는데 실패했습니다
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#7F1D1D', mb: 2 }}>
|
||||
{error.message}
|
||||
</Typography>
|
||||
<Box
|
||||
component="button"
|
||||
onClick={() => refetch()}
|
||||
sx={{
|
||||
px: 3,
|
||||
py: 1.5,
|
||||
borderRadius: 2,
|
||||
border: 'none',
|
||||
bgcolor: '#DC2626',
|
||||
color: 'white',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: '#B91C1C' },
|
||||
}}
|
||||
>
|
||||
다시 시도
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Summary Statistics */}
|
||||
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mb: { xs: 4, sm: 8 } }}>
|
||||
|
||||
+17
-193
@@ -13,24 +13,23 @@ import {
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Avatar,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import { Person, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-material';
|
||||
import { CheckCircle } from '@mui/icons-material';
|
||||
import { useAuthContext } from '@/features/auth';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { userApi } from '@/entities/user';
|
||||
import Header from '@/shared/ui/Header';
|
||||
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
||||
import Image from 'next/image';
|
||||
import userImage from '@/shared/ui/user_img.png';
|
||||
|
||||
// 기본 정보 스키마
|
||||
const basicInfoSchema = z.object({
|
||||
@@ -50,32 +49,13 @@ const businessInfoSchema = z.object({
|
||||
businessHours: z.string().optional(),
|
||||
});
|
||||
|
||||
// 비밀번호 변경 스키마
|
||||
const passwordSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1, '현재 비밀번호를 입력해주세요'),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
||||
.max(100, '비밀번호는 100자 이하여야 합니다'),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: '새 비밀번호가 일치하지 않습니다',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
type BasicInfoData = z.infer<typeof basicInfoSchema>;
|
||||
type BusinessInfoData = z.infer<typeof businessInfoSchema>;
|
||||
type PasswordData = z.infer<typeof passwordSchema>;
|
||||
|
||||
export default function ProfilePage() {
|
||||
const router = useRouter();
|
||||
const { user, logout, refreshProfile } = useAuthContext();
|
||||
const { showToast, setLoading } = useUIStore();
|
||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
||||
const [profileLoaded, setProfileLoaded] = useState(false);
|
||||
@@ -105,26 +85,12 @@ export default function ProfilePage() {
|
||||
resolver: zodResolver(businessInfoSchema),
|
||||
defaultValues: {
|
||||
businessName: '',
|
||||
businessType: '',
|
||||
businessType: 'restaurant',
|
||||
businessLocation: '',
|
||||
businessHours: '',
|
||||
},
|
||||
});
|
||||
|
||||
// 비밀번호 변경 폼
|
||||
const {
|
||||
control: passwordControl,
|
||||
handleSubmit: handlePasswordSubmit,
|
||||
formState: { errors: passwordErrors },
|
||||
reset: resetPassword,
|
||||
} = useForm<PasswordData>({
|
||||
resolver: zodResolver(passwordSchema),
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
});
|
||||
|
||||
// 프로필 데이터 로드
|
||||
useEffect(() => {
|
||||
@@ -164,7 +130,7 @@ export default function ProfilePage() {
|
||||
// 사업장 정보 폼 초기화
|
||||
resetBusiness({
|
||||
businessName: profile.storeName || '',
|
||||
businessType: profile.industry || '',
|
||||
businessType: profile.industry || 'restaurant',
|
||||
businessLocation: profile.address || '',
|
||||
businessHours: profile.businessHours || '',
|
||||
});
|
||||
@@ -244,40 +210,6 @@ export default function ProfilePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const onChangePassword = async (data: PasswordData) => {
|
||||
console.log('🔐 비밀번호 변경 시작');
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const passwordData = {
|
||||
currentPassword: data.currentPassword,
|
||||
newPassword: data.newPassword,
|
||||
};
|
||||
|
||||
console.log('📡 비밀번호 변경 API 호출');
|
||||
await userApi.changePassword(passwordData);
|
||||
console.log('✅ 비밀번호 변경 성공');
|
||||
|
||||
showToast('비밀번호가 변경되었습니다', 'success');
|
||||
resetPassword();
|
||||
} catch (error: any) {
|
||||
console.error('❌ 비밀번호 변경 실패:', error);
|
||||
|
||||
let errorMessage = '비밀번호 변경에 실패했습니다';
|
||||
if (error.response) {
|
||||
errorMessage = error.response.data?.message ||
|
||||
error.response.data?.error ||
|
||||
`서버 오류 (${error.response.status})`;
|
||||
} else if (error.request) {
|
||||
errorMessage = '서버로부터 응답이 없습니다';
|
||||
}
|
||||
|
||||
showToast(errorMessage, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
handleBasicSubmit((basicData) => {
|
||||
@@ -319,18 +251,25 @@ export default function ProfilePage() {
|
||||
{/* 사용자 정보 섹션 */}
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10, textAlign: 'center' }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<Avatar
|
||||
<Box
|
||||
sx={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
mx: 'auto',
|
||||
mb: 3,
|
||||
bgcolor: colors.purple,
|
||||
color: 'white',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Person sx={{ fontSize: 56 }} />
|
||||
</Avatar>
|
||||
<Image
|
||||
src={userImage}
|
||||
alt="User Profile"
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
priority
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="h5" sx={{ ...responsiveText.h3, mb: 1 }}>
|
||||
{user?.userName}
|
||||
</Typography>
|
||||
@@ -469,121 +408,6 @@ export default function ProfilePage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 비밀번호 변경 */}
|
||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
|
||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
|
||||
비밀번호 변경
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<Controller
|
||||
name="currentPassword"
|
||||
control={passwordControl}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
label="현재 비밀번호"
|
||||
placeholder="현재 비밀번호를 입력하세요"
|
||||
error={!!passwordErrors.currentPassword}
|
||||
helperText={passwordErrors.currentPassword?.message}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
edge="end"
|
||||
>
|
||||
{showCurrentPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="newPassword"
|
||||
control={passwordControl}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
label="새 비밀번호"
|
||||
placeholder="새 비밀번호를 입력하세요"
|
||||
error={!!passwordErrors.newPassword}
|
||||
helperText={passwordErrors.newPassword?.message || '8자 이상 입력해주세요'}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||||
edge="end"
|
||||
>
|
||||
{showNewPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="confirmPassword"
|
||||
control={passwordControl}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
label="비밀번호 확인"
|
||||
placeholder="비밀번호를 다시 입력하세요"
|
||||
error={!!passwordErrors.confirmPassword}
|
||||
helperText={passwordErrors.confirmPassword?.message}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
edge="end"
|
||||
>
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={handlePasswordSubmit(onChangePassword)}
|
||||
sx={{
|
||||
mt: 1,
|
||||
py: 3,
|
||||
borderRadius: 3,
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
borderWidth: 2,
|
||||
'&:hover': {
|
||||
borderWidth: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
비밀번호 변경
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { eventId: string } }
|
||||
) {
|
||||
try {
|
||||
const { eventId } = params;
|
||||
const token = request.headers.get('Authorization');
|
||||
const body = await request.json();
|
||||
|
||||
console.log('🎰 [Proxy] Draw winners request:', {
|
||||
eventId,
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ message: '인증 토큰이 필요합니다.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${GATEWAY_HOST}/api/v1/participations/events/${eventId}/draw-winners`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Draw winners response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Draw winners error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '당첨자 추첨 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { eventId: string; participantId: string } }
|
||||
) {
|
||||
try {
|
||||
const { eventId, participantId } = params;
|
||||
const token = request.headers.get('Authorization');
|
||||
|
||||
console.log('👤 [Proxy] Get participant request:', {
|
||||
eventId,
|
||||
participantId,
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = token;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${GATEWAY_HOST}/api/v1/participations/events/${eventId}/participants/${participantId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Get participant response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Get participant error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '참여자 조회 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { eventId: string } }
|
||||
) {
|
||||
try {
|
||||
const { eventId } = params;
|
||||
const token = request.headers.get('Authorization');
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
console.log('📋 [Proxy] Get participants request:', {
|
||||
eventId,
|
||||
hasToken: !!token,
|
||||
params: Object.fromEntries(searchParams),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = token;
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${GATEWAY_HOST}/api/v1/participations/events/${eventId}/participants${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Get participants response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Get participants error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '참여자 목록 조회 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { eventId: string } }
|
||||
) {
|
||||
try {
|
||||
const { eventId } = params;
|
||||
const token = request.headers.get('Authorization');
|
||||
const body = await request.json();
|
||||
|
||||
console.log('🎫 [Proxy] Participate request:', {
|
||||
eventId,
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = token;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${GATEWAY_HOST}/api/v1/participations/events/${eventId}/participate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Participate response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Participate error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '참여 요청 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { eventId: string } }
|
||||
) {
|
||||
try {
|
||||
const { eventId } = params;
|
||||
const token = request.headers.get('Authorization');
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
console.log('🏆 [Proxy] Get winners request:', {
|
||||
eventId,
|
||||
hasToken: !!token,
|
||||
params: Object.fromEntries(searchParams),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = token;
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${GATEWAY_HOST}/api/v1/participations/events/${eventId}/winners${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Get winners response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Get winners error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '당첨자 목록 조회 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
console.log('🔐 [Proxy] Login request:', {
|
||||
email: body.email,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Login response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Login error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '로그인 요청 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const token = request.headers.get('Authorization');
|
||||
|
||||
console.log('🚪 [Proxy] Logout request:', {
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = token;
|
||||
}
|
||||
|
||||
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/logout`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Logout response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Logout error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '로그아웃 요청 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const token = request.headers.get('Authorization');
|
||||
const body = await request.json();
|
||||
|
||||
console.log('🔑 [Proxy] Change password request:', {
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ message: '인증 토큰이 필요합니다.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/password`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('📥 [Proxy] Change password response:', {
|
||||
status: response.status,
|
||||
success: false,
|
||||
});
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
console.log('📥 [Proxy] Change password response:', {
|
||||
status: response.status,
|
||||
success: true,
|
||||
});
|
||||
|
||||
return new NextResponse(null, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Change password error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '비밀번호 변경 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = request.headers.get('Authorization');
|
||||
|
||||
console.log('👤 [Proxy] Get profile request:', {
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ message: '인증 토큰이 필요합니다.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/profile`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Get profile response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Get profile error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '프로필 조회 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const token = request.headers.get('Authorization');
|
||||
const body = await request.json();
|
||||
|
||||
console.log('✏️ [Proxy] Update profile request:', {
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ message: '인증 토큰이 필요합니다.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/profile`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Update profile response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Update profile error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '프로필 수정 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
console.log('📝 [Proxy] Register request:', {
|
||||
email: body.email,
|
||||
name: body.name,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📥 [Proxy] Register response:', {
|
||||
status: response.status,
|
||||
success: response.ok,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [Proxy] Register error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: '회원가입 요청 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { participationApi, default } from './participationApi';
|
||||
@@ -0,0 +1,142 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import type {
|
||||
ParticipationRequest,
|
||||
ParticipationResponse,
|
||||
ApiResponse,
|
||||
PageResponse,
|
||||
DrawWinnersRequest,
|
||||
DrawWinnersResponse,
|
||||
} from '../model/types';
|
||||
|
||||
// Use Next.js API proxy to bypass CORS issues
|
||||
const PARTICIPATION_API_BASE = '/api/participations';
|
||||
|
||||
const participationApiClient: AxiosInstance = axios.create({
|
||||
baseURL: PARTICIPATION_API_BASE,
|
||||
timeout: 90000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
participationApiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log('🎫 Participation API Request:', {
|
||||
method: config.method?.toUpperCase(),
|
||||
url: config.url,
|
||||
baseURL: config.baseURL,
|
||||
});
|
||||
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ Participation API Request Error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
participationApiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log('✅ Participation API Response:', {
|
||||
status: response.status,
|
||||
url: response.config.url,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ Participation API Error:', {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
url: error.config?.url,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Participation API Service
|
||||
* 이벤트 참여 관리 API
|
||||
*/
|
||||
export const participationApi = {
|
||||
/**
|
||||
* 이벤트 참여
|
||||
*/
|
||||
participate: async (
|
||||
eventId: string,
|
||||
data: ParticipationRequest
|
||||
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||
const response = await participationApiClient.post<
|
||||
ApiResponse<ParticipationResponse>
|
||||
>(`/${eventId}/participate`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 참여자 목록 조회
|
||||
*/
|
||||
getParticipants: async (
|
||||
eventId: string,
|
||||
params?: {
|
||||
storeVisited?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
}
|
||||
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||
const response = await participationApiClient.get<
|
||||
ApiResponse<PageResponse<ParticipationResponse>>
|
||||
>(`/${eventId}/participants`, { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 참여자 조회
|
||||
*/
|
||||
getParticipant: async (
|
||||
eventId: string,
|
||||
participantId: string
|
||||
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||
const response = await participationApiClient.get<
|
||||
ApiResponse<ParticipationResponse>
|
||||
>(`/${eventId}/participants/${participantId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 당첨자 추첨
|
||||
*/
|
||||
drawWinners: async (
|
||||
eventId: string,
|
||||
data: DrawWinnersRequest
|
||||
): Promise<ApiResponse<DrawWinnersResponse>> => {
|
||||
const response = await participationApiClient.post<
|
||||
ApiResponse<DrawWinnersResponse>
|
||||
>(`/${eventId}/draw-winners`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 당첨자 목록 조회
|
||||
*/
|
||||
getWinners: async (
|
||||
eventId: string,
|
||||
params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
}
|
||||
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||
const response = await participationApiClient.get<
|
||||
ApiResponse<PageResponse<ParticipationResponse>>
|
||||
>(`/${eventId}/winners`, { params });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default participationApi;
|
||||
@@ -0,0 +1,10 @@
|
||||
export { participationApi } from './api';
|
||||
export type {
|
||||
ParticipationRequest,
|
||||
ParticipationResponse,
|
||||
ApiResponse,
|
||||
PageResponse,
|
||||
DrawWinnersRequest,
|
||||
DrawWinnersResponse,
|
||||
WinnerSummary,
|
||||
} from './model';
|
||||
@@ -0,0 +1,9 @@
|
||||
export type {
|
||||
ParticipationRequest,
|
||||
ParticipationResponse,
|
||||
ApiResponse,
|
||||
PageResponse,
|
||||
DrawWinnersRequest,
|
||||
DrawWinnersResponse,
|
||||
WinnerSummary,
|
||||
} from './types';
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Participation API Types
|
||||
* 이벤트 참여 관련 타입 정의
|
||||
*/
|
||||
|
||||
/**
|
||||
* 참여 요청
|
||||
*/
|
||||
export interface ParticipationRequest {
|
||||
/** 이름 (2-50자, 필수) */
|
||||
name: string;
|
||||
/** 전화번호 (형식: "010-1234-5678", 필수) */
|
||||
phoneNumber: string;
|
||||
/** 이메일 (선택) */
|
||||
email?: string;
|
||||
/** 채널 (선택) */
|
||||
channel?: string;
|
||||
/** 마케팅 동의 (선택) */
|
||||
agreeMarketing?: boolean;
|
||||
/** 개인정보 동의 (필수) */
|
||||
agreePrivacy: boolean;
|
||||
/** 매장 방문 여부 (선택) */
|
||||
storeVisited?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여 응답
|
||||
*/
|
||||
export interface ParticipationResponse {
|
||||
/** 참여자 ID (UUID) */
|
||||
participantId: string;
|
||||
/** 이벤트 ID */
|
||||
eventId: string;
|
||||
/** 이름 */
|
||||
name: string;
|
||||
/** 전화번호 */
|
||||
phoneNumber: string;
|
||||
/** 이메일 */
|
||||
email?: string;
|
||||
/** 채널 */
|
||||
channel?: string;
|
||||
/** 참여 일시 */
|
||||
participatedAt: string;
|
||||
/** 매장 방문 여부 */
|
||||
storeVisited?: boolean;
|
||||
/** 보너스 응모권 수 */
|
||||
bonusEntries: number;
|
||||
/** 당첨 여부 */
|
||||
isWinner: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 공통 응답
|
||||
*/
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
errorCode?: string;
|
||||
message?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 응답
|
||||
*/
|
||||
export interface PageResponse<T> {
|
||||
content: T[];
|
||||
page: number;
|
||||
size: number;
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 당첨자 추첨 요청
|
||||
*/
|
||||
export interface DrawWinnersRequest {
|
||||
/** 당첨자 수 (최소 1명, 필수) */
|
||||
winnerCount: number;
|
||||
/** 매장 방문 보너스 적용 여부 (선택) */
|
||||
applyStoreVisitBonus?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 당첨자 요약 정보
|
||||
*/
|
||||
export interface WinnerSummary {
|
||||
/** 참여자 ID */
|
||||
participantId: string;
|
||||
/** 이름 */
|
||||
name: string;
|
||||
/** 전화번호 */
|
||||
phoneNumber: string;
|
||||
/** 등수 */
|
||||
rank: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 당첨자 추첨 응답
|
||||
*/
|
||||
export interface DrawWinnersResponse {
|
||||
/** 이벤트 ID */
|
||||
eventId: string;
|
||||
/** 총 참여자 수 */
|
||||
totalParticipants: number;
|
||||
/** 당첨자 수 */
|
||||
winnerCount: number;
|
||||
/** 추첨 일시 */
|
||||
drawnAt: string;
|
||||
/** 당첨자 목록 */
|
||||
winners: WinnerSummary[];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { apiClient } from '@/shared/api';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
@@ -10,8 +10,57 @@ import type {
|
||||
ChangePasswordRequest,
|
||||
} from '../model/types';
|
||||
|
||||
// Use Next.js API proxy to bypass CORS issues
|
||||
const USER_API_BASE = '/api/v1/users';
|
||||
|
||||
const userApiClient: AxiosInstance = axios.create({
|
||||
baseURL: USER_API_BASE,
|
||||
timeout: 90000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
userApiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log('👤 User API Request:', {
|
||||
method: config.method?.toUpperCase(),
|
||||
url: config.url,
|
||||
baseURL: config.baseURL,
|
||||
});
|
||||
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ User API Request Error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
userApiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log('✅ User API Response:', {
|
||||
status: response.status,
|
||||
url: response.config.url,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ User API Error:', {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
url: error.config?.url,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* User API Service
|
||||
* 사용자 인증 및 프로필 관리 API
|
||||
@@ -21,8 +70,8 @@ export const userApi = {
|
||||
* 로그인
|
||||
*/
|
||||
login: async (data: LoginRequest): Promise<LoginResponse> => {
|
||||
const response = await apiClient.post<LoginResponse>(
|
||||
`${USER_API_BASE}/login`,
|
||||
const response = await userApiClient.post<LoginResponse>(
|
||||
'/login',
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
@@ -33,15 +82,14 @@ export const userApi = {
|
||||
*/
|
||||
register: async (data: RegisterRequest): Promise<RegisterResponse> => {
|
||||
console.log('📞 userApi.register 호출');
|
||||
console.log('🎯 URL:', `${USER_API_BASE}/register`);
|
||||
console.log('📦 요청 데이터:', {
|
||||
...data,
|
||||
password: '***'
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiClient.post<RegisterResponse>(
|
||||
`${USER_API_BASE}/register`,
|
||||
const response = await userApiClient.post<RegisterResponse>(
|
||||
'/register',
|
||||
data
|
||||
);
|
||||
console.log('✅ userApi.register 성공:', response.data);
|
||||
@@ -56,15 +104,9 @@ export const userApi = {
|
||||
* 로그아웃
|
||||
*/
|
||||
logout: async (): Promise<LogoutResponse> => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const response = await apiClient.post<LogoutResponse>(
|
||||
`${USER_API_BASE}/logout`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
const response = await userApiClient.post<LogoutResponse>(
|
||||
'/logout',
|
||||
{}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
@@ -73,8 +115,8 @@ export const userApi = {
|
||||
* 프로필 조회
|
||||
*/
|
||||
getProfile: async (): Promise<ProfileResponse> => {
|
||||
const response = await apiClient.get<ProfileResponse>(
|
||||
`${USER_API_BASE}/profile`
|
||||
const response = await userApiClient.get<ProfileResponse>(
|
||||
'/profile'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
@@ -85,8 +127,8 @@ export const userApi = {
|
||||
updateProfile: async (
|
||||
data: UpdateProfileRequest
|
||||
): Promise<ProfileResponse> => {
|
||||
const response = await apiClient.put<ProfileResponse>(
|
||||
`${USER_API_BASE}/profile`,
|
||||
const response = await userApiClient.put<ProfileResponse>(
|
||||
'/profile',
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
@@ -96,7 +138,7 @@ export const userApi = {
|
||||
* 비밀번호 변경
|
||||
*/
|
||||
changePassword: async (data: ChangePasswordRequest): Promise<void> => {
|
||||
await apiClient.put(`${USER_API_BASE}/password`, data);
|
||||
await userApiClient.put('/password', data);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface LoginRequest {
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
userId: number;
|
||||
userId: string; // UUID format
|
||||
userName: string;
|
||||
role: string;
|
||||
email: string;
|
||||
@@ -31,9 +31,9 @@ export interface RegisterRequest {
|
||||
|
||||
export interface RegisterResponse {
|
||||
token: string;
|
||||
userId: number;
|
||||
userId: string; // UUID format
|
||||
userName: string;
|
||||
storeId: number;
|
||||
storeId: string; // UUID format
|
||||
storeName: string;
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ export interface LogoutResponse {
|
||||
|
||||
// 프로필 조회/수정
|
||||
export interface ProfileResponse {
|
||||
userId: number;
|
||||
userId: string; // UUID format
|
||||
userName: string;
|
||||
phoneNumber: string;
|
||||
email: string;
|
||||
role: string;
|
||||
storeId: number;
|
||||
storeId: string; // UUID format
|
||||
storeName: string;
|
||||
industry: string;
|
||||
address: string;
|
||||
@@ -77,12 +77,12 @@ export interface ChangePasswordRequest {
|
||||
|
||||
// User 상태
|
||||
export interface User {
|
||||
userId: number;
|
||||
userId: string; // UUID format
|
||||
userName: string;
|
||||
email: string;
|
||||
role: string;
|
||||
phoneNumber?: string;
|
||||
storeId?: number;
|
||||
storeId?: string; // UUID format
|
||||
storeName?: string;
|
||||
industry?: string;
|
||||
address?: string;
|
||||
|
||||
@@ -14,7 +14,7 @@ const API_HOSTS = {
|
||||
|
||||
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'api';
|
||||
|
||||
// 기본 User API 클라이언트 (기존 호환성 유지)
|
||||
// 기본 User API 클라이언트 (Gateway 직접 연결)
|
||||
const API_BASE_URL = API_HOSTS.user;
|
||||
|
||||
export const apiClient: AxiosInstance = axios.create({
|
||||
|
||||
@@ -15,14 +15,14 @@ import type {
|
||||
|
||||
/**
|
||||
* 이벤트 참여 신청
|
||||
* POST /api/v1/events/{eventId}/participate
|
||||
* POST /api/participations/{eventId}/participate
|
||||
*/
|
||||
export const participate = async (
|
||||
eventId: string,
|
||||
data: ParticipationRequest
|
||||
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||
const response = await axios.post<ApiResponse<ParticipationResponse>>(
|
||||
`/api/v1/events/${eventId}/participate`,
|
||||
`/api/participations/${eventId}/participate`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
@@ -30,37 +30,35 @@ export const participate = async (
|
||||
|
||||
/**
|
||||
* 참여자 목록 조회 (페이징)
|
||||
* GET /api/v1/events/{eventId}/participants
|
||||
* GET /api/participations/{eventId}/participants
|
||||
*/
|
||||
export const getParticipants = async (
|
||||
params: GetParticipantsParams
|
||||
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||
const { eventId, storeVisited, page = 0, size = 20, sort = ['createdAt,DESC'] } = params;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
if (storeVisited !== undefined) queryParams.append('storeVisited', String(storeVisited));
|
||||
queryParams.append('page', String(page));
|
||||
queryParams.append('size', String(size));
|
||||
sort.forEach(s => queryParams.append('sort', s));
|
||||
|
||||
const response = await axios.get<ApiResponse<PageResponse<ParticipationResponse>>>(
|
||||
`/api/v1/events/${eventId}/participants`,
|
||||
{
|
||||
params: {
|
||||
storeVisited,
|
||||
page,
|
||||
size,
|
||||
sort,
|
||||
},
|
||||
}
|
||||
`/api/participations/${eventId}/participants?${queryParams.toString()}`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 참여자 정보 조회
|
||||
* GET /api/v1/events/{eventId}/participants/{participantId}
|
||||
* GET /api/participations/{eventId}/participants/{participantId}
|
||||
*/
|
||||
export const getParticipant = async (
|
||||
eventId: string,
|
||||
participantId: string
|
||||
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||
const response = await axios.get<ApiResponse<ParticipationResponse>>(
|
||||
`/api/v1/events/${eventId}/participants/${participantId}`
|
||||
`/api/participations/${eventId}/participants/${participantId}`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
@@ -113,7 +111,7 @@ export const searchParticipants = async (
|
||||
|
||||
/**
|
||||
* 당첨자 추첨
|
||||
* POST /api/v1/events/{eventId}/draw-winners
|
||||
* POST /api/participations/{eventId}/draw-winners
|
||||
*/
|
||||
export const drawWinners = async (
|
||||
eventId: string,
|
||||
@@ -121,7 +119,7 @@ export const drawWinners = async (
|
||||
applyStoreVisitBonus?: boolean
|
||||
): Promise<ApiResponse<import('../types/api.types').DrawWinnersResponse>> => {
|
||||
const response = await axios.post<ApiResponse<import('../types/api.types').DrawWinnersResponse>>(
|
||||
`/api/v1/events/${eventId}/draw-winners`,
|
||||
`/api/participations/${eventId}/draw-winners`,
|
||||
{
|
||||
winnerCount,
|
||||
applyStoreVisitBonus,
|
||||
@@ -132,7 +130,7 @@ export const drawWinners = async (
|
||||
|
||||
/**
|
||||
* 당첨자 목록 조회
|
||||
* GET /api/v1/events/{eventId}/winners
|
||||
* GET /api/participations/{eventId}/winners
|
||||
*/
|
||||
export const getWinners = async (
|
||||
eventId: string,
|
||||
@@ -140,15 +138,13 @@ export const getWinners = async (
|
||||
size = 20,
|
||||
sort: string[] = ['winnerRank,ASC']
|
||||
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('page', String(page));
|
||||
queryParams.append('size', String(size));
|
||||
sort.forEach(s => queryParams.append('sort', s));
|
||||
|
||||
const response = await axios.get<ApiResponse<PageResponse<ParticipationResponse>>>(
|
||||
`/api/v1/events/${eventId}/winners`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
size,
|
||||
sort,
|
||||
},
|
||||
}
|
||||
`/api/participations/${eventId}/winners?${queryParams.toString()}`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -2,7 +2,7 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
id: string; // UUID format
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
|
||||
Reference in New Issue
Block a user