이벤트 목록 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:
cherry2250
2025-10-30 20:17:09 +09:00
parent 86ae038a31
commit 974961e1bd
29 changed files with 2105 additions and 328 deletions
+13 -6
View File
@@ -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>
.
+57
View File
@@ -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>
);
}
+51 -32
View File
@@ -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 });
+68 -42
View File
@@ -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
View File
@@ -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 }
);
}
}
+41
View File
@@ -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 }
);
}
}
+47
View File
@@ -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 }
);
}
}
+53
View File
@@ -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 }
);
}
}
+95
View File
@@ -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 }
);
}
}
+42
View File
@@ -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 }
);
}
}
+1
View File
@@ -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;
+10
View File
@@ -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';
+114
View File
@@ -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[];
}
+62 -20
View File
@@ -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);
},
};
+7 -7
View File
@@ -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;
+1 -1
View File
@@ -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({
+21 -25
View File
@@ -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

+1 -1
View File
@@ -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;