이벤트 엔티티 및 페이지 기능 추가

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
merrycoral 2025-10-29 13:22:26 +09:00
parent 4511957ff6
commit 78cc41b453
4 changed files with 694 additions and 83 deletions

View File

@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import {
Box,
@ -37,78 +37,12 @@ import {
} from '@mui/icons-material';
import Header from '@/shared/ui/Header';
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
import { useEvents } from '@/entities/event/model/useEvents';
import type { EventStatus as ApiEventStatus } from '@/entities/event/model/types';
// Mock 데이터
const mockEvents = [
{
id: '1',
title: '신규고객 유치 이벤트',
status: 'active' as const,
daysLeft: 5,
participants: 128,
targetParticipants: 200,
roi: 450,
startDate: '2025-11-01',
endDate: '2025-11-15',
prize: '커피 쿠폰',
method: '전화번호 입력',
isUrgent: true,
isPopular: false,
isHighROI: true,
isNew: false,
},
{
id: '2',
title: '재방문 유도 이벤트',
status: 'active' as const,
daysLeft: 12,
participants: 56,
targetParticipants: 100,
roi: 320,
startDate: '2025-11-05',
endDate: '2025-11-20',
prize: '할인 쿠폰',
method: 'SNS 팔로우',
isUrgent: false,
isPopular: false,
isHighROI: false,
isNew: false,
},
{
id: '3',
title: '매출증대 프로모션',
status: 'ended' as const,
daysLeft: 0,
participants: 234,
targetParticipants: 150,
roi: 580,
startDate: '2025-10-15',
endDate: '2025-10-31',
prize: '상품권',
method: '구매 인증',
isUrgent: false,
isPopular: true,
isHighROI: true,
isNew: false,
},
{
id: '4',
title: '봄맞이 특별 이벤트',
status: 'scheduled' as const,
daysLeft: 30,
participants: 0,
targetParticipants: 300,
roi: 0,
startDate: '2025-12-01',
endDate: '2025-12-15',
prize: '체험권',
method: '이메일 등록',
isUrgent: false,
isPopular: false,
isHighROI: false,
isNew: true,
},
];
// ==================== API 연동 ====================
// Mock 데이터를 실제 API 호출로 교체
// 백업 파일: page.tsx.backup
type EventStatus = 'all' | 'active' | 'scheduled' | 'ended';
type Period = '1month' | '3months' | '6months' | '1year' | 'all';
@ -123,8 +57,57 @@ 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'
});
// API 상태를 UI 상태로 매핑
const mapApiStatus = (apiStatus: ApiEventStatus): EventStatus => {
switch (apiStatus) {
case 'PUBLISHED':
return 'active';
case 'DRAFT':
return 'scheduled';
case 'ENDED':
return 'ended';
default:
return 'all';
}
};
// API 이벤트를 UI 형식으로 변환
const transformedEvents = apiEvents.map(event => ({
id: event.eventId,
title: event.eventName || '제목 없음',
status: mapApiStatus(event.status),
startDate: event.startDate ? new Date(event.startDate).toLocaleDateString('ko-KR') : '-',
endDate: event.endDate ? new Date(event.endDate).toLocaleDateString('ko-KR') : '-',
prize: event.aiRecommendations[0]?.reward || '경품 정보 없음',
method: event.aiRecommendations[0]?.participationMethod || '참여 방법 없음',
participants: event.participants || 0,
targetParticipants: event.targetParticipants || 0,
roi: event.roi || 0,
daysLeft: event.endDate
? Math.ceil((new Date(event.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
: 0,
isUrgent: event.endDate
? Math.ceil((new Date(event.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) <= 3
: false,
isPopular: event.participants && event.targetParticipants
? (event.participants / event.targetParticipants) >= 0.8
: false,
isHighROI: event.roi ? event.roi >= 300 : false,
isNew: event.createdAt
? (Date.now() - new Date(event.createdAt).getTime()) < (7 * 24 * 60 * 60 * 1000)
: false,
}));
// 필터링 및 정렬
const filteredEvents = mockEvents
const filteredEvents = transformedEvents
.filter((event) => {
const matchesSearch = event.title.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || event.status === statusFilter;
@ -204,22 +187,26 @@ export default function EventsPage() {
}
};
const calculateProgress = (event: (typeof mockEvents)[0]) => {
const calculateProgress = (event: typeof transformedEvents[0]) => {
if (event.status !== 'active') return 0;
const total = new Date(event.endDate).getTime() - new Date(event.startDate).getTime();
const elapsed = Date.now() - new Date(event.startDate).getTime();
const startTime = new Date(event.startDate).getTime();
const endTime = new Date(event.endDate).getTime();
const total = endTime - startTime;
const elapsed = Date.now() - startTime;
return Math.min(Math.max((elapsed / total) * 100, 0), 100);
};
// 통계 계산
const stats = {
total: mockEvents.length,
active: mockEvents.filter((e) => e.status === 'active').length,
totalParticipants: mockEvents.reduce((sum, e) => sum + e.participants, 0),
avgROI: Math.round(
mockEvents.filter((e) => e.roi > 0).reduce((sum, e) => sum + e.roi, 0) /
mockEvents.filter((e) => e.roi > 0).length
),
total: transformedEvents.length,
active: transformedEvents.filter((e) => e.status === 'active').length,
totalParticipants: transformedEvents.reduce((sum, e) => sum + e.participants, 0),
avgROI: transformedEvents.filter((e) => e.roi > 0).length > 0
? Math.round(
transformedEvents.filter((e) => e.roi > 0).reduce((sum, e) => sum + e.roi, 0) /
transformedEvents.filter((e) => e.roi > 0).length
)
: 0,
};
return (
@ -237,6 +224,59 @@ export default function EventsPage() {
maxWidth="lg"
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 6 }, px: { xs: 3, sm: 6, md: 10 } }}
>
{/* Loading State */}
{loading && (
<Box sx={{ mb: 4 }}>
<LinearProgress sx={{ borderRadius: 1 }} />
<Typography
sx={{
mt: 2,
textAlign: 'center',
color: colors.gray[600],
fontSize: { xs: '0.875rem', sm: '1rem' },
}}
>
...
</Typography>
</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 } }}>
<Grid item xs={6} sm={3}>

View File

@ -0,0 +1,198 @@
import { apiClient } from '@/shared/api';
import type {
GetEventsRequest,
GetEventsResponse,
EventDetail,
ApiResponse,
SelectObjectiveRequest,
EventCreatedResponse,
AiRecommendationRequest,
JobAcceptedResponse,
ImageGenerationRequest,
ImageGenerationResponse,
} from '../model/types';
/**
* Event API
*
* 참고: apiClient는 user-service(8081)
* event API .
*
* apiClient를 , baseURL을 .
*/
const EVENT_API_BASE = '/api/v1/events';
const EVENT_HOST = process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080';
/**
* Event Service용 API
* Event Service는 (8080)
*/
import axios from 'axios';
const eventApiClient = axios.create({
baseURL: EVENT_HOST,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - JWT 토큰 추가
eventApiClient.interceptors.request.use(
(config) => {
console.log('🚀 Event API Request:', {
method: config.method?.toUpperCase(),
url: config.url,
baseURL: config.baseURL,
params: config.params,
});
const token = localStorage.getItem('accessToken');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
console.log('🔑 Token added to Event API request');
}
return config;
},
(error) => {
console.error('❌ Event API Request Error:', error);
return Promise.reject(error);
}
);
// Response interceptor - 에러 처리
eventApiClient.interceptors.response.use(
(response) => {
console.log('✅ Event API Response:', {
status: response.status,
url: response.config.url,
data: response.data,
});
return response;
},
(error) => {
console.error('❌ Event API Error:', {
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
data: error.response?.data,
});
if (error.response?.status === 401) {
console.warn('🔒 401 Unauthorized - Redirecting to login');
localStorage.removeItem('accessToken');
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
/**
* Event API Service
* API
*/
export const eventApi = {
/**
*
*/
getEvents: async (params?: GetEventsRequest): Promise<GetEventsResponse> => {
console.log('📞 eventApi.getEvents 호출', params);
const response = await eventApiClient.get<GetEventsResponse>(EVENT_API_BASE, {
params,
});
return response.data;
},
/**
*
*/
getEvent: async (eventId: string): Promise<ApiResponse<EventDetail>> => {
console.log('📞 eventApi.getEvent 호출', eventId);
const response = await eventApiClient.get<ApiResponse<EventDetail>>(
`${EVENT_API_BASE}/${eventId}`
);
return response.data;
},
/**
* ( )
*/
createEvent: async (
data: SelectObjectiveRequest
): Promise<ApiResponse<EventCreatedResponse>> => {
console.log('📞 eventApi.createEvent 호출', data);
const response = await eventApiClient.post<ApiResponse<EventCreatedResponse>>(
`${EVENT_API_BASE}/objectives`,
data
);
return response.data;
},
/**
*
*/
deleteEvent: async (eventId: string): Promise<ApiResponse<void>> => {
console.log('📞 eventApi.deleteEvent 호출', eventId);
const response = await eventApiClient.delete<ApiResponse<void>>(
`${EVENT_API_BASE}/${eventId}`
);
return response.data;
},
/**
*
*/
publishEvent: async (eventId: string): Promise<ApiResponse<void>> => {
console.log('📞 eventApi.publishEvent 호출', eventId);
const response = await eventApiClient.post<ApiResponse<void>>(
`${EVENT_API_BASE}/${eventId}/publish`
);
return response.data;
},
/**
*
*/
endEvent: async (eventId: string): Promise<ApiResponse<void>> => {
console.log('📞 eventApi.endEvent 호출', eventId);
const response = await eventApiClient.post<ApiResponse<void>>(
`${EVENT_API_BASE}/${eventId}/end`
);
return response.data;
},
/**
* AI
*/
requestAiRecommendations: async (
eventId: string,
data: AiRecommendationRequest
): Promise<ApiResponse<JobAcceptedResponse>> => {
console.log('📞 eventApi.requestAiRecommendations 호출', eventId, data);
const response = await eventApiClient.post<ApiResponse<JobAcceptedResponse>>(
`${EVENT_API_BASE}/${eventId}/ai-recommendations`,
data
);
return response.data;
},
/**
*
*/
requestImageGeneration: async (
eventId: string,
data: ImageGenerationRequest
): Promise<ApiResponse<ImageGenerationResponse>> => {
console.log('📞 eventApi.requestImageGeneration 호출', eventId, data);
const response = await eventApiClient.post<ApiResponse<ImageGenerationResponse>>(
`${EVENT_API_BASE}/${eventId}/images`,
data
);
return response.data;
},
};
export default eventApi;

View File

@ -0,0 +1,173 @@
/**
* Event
* Event Service API
*/
/**
*
*/
export type EventStatus = 'DRAFT' | 'PUBLISHED' | 'ENDED';
/**
*
*/
export type EventObjective =
| 'CUSTOMER_ACQUISITION'
| 'Sales Promotion'
| 'Customer Retention'
| 'New Customer Acquisition'
| 'awareness'
| 'sales'
| 'new_customer';
/**
*
*/
export type DistributionChannel = 'SMS' | 'EMAIL' | 'KAKAO' | 'PUSH';
/**
*
*/
export interface EventImage {
imageId: string;
imageUrl: string;
prompt?: string;
isSelected: boolean;
createdAt: string;
}
/**
* AI
*/
export interface AiRecommendation {
recommendationId: string;
eventName: string;
description: string;
reward: string;
participationMethod: string;
startDate: string;
endDate: string;
targetParticipants: number;
isSelected: boolean;
createdAt: string;
}
/**
*
*/
export interface EventDetail {
eventId: string;
userId: string;
storeId: string;
eventName: string;
description: string | null;
objective: EventObjective;
startDate: string | null;
endDate: string | null;
status: EventStatus;
selectedImageId: string | null;
selectedImageUrl: string | null;
participants: number | null;
targetParticipants: number | null;
roi: number | null;
generatedImages: EventImage[];
aiRecommendations: AiRecommendation[];
channels: DistributionChannel[];
createdAt: string;
updatedAt: string;
}
/**
*
*/
export interface PageResponse<T> {
content: T[];
page: number;
size: number;
totalElements: number;
totalPages: number;
first: boolean;
last: boolean;
}
/**
* API
*/
export interface ApiResponse<T> {
success: boolean;
data: T;
timestamp: string;
}
/**
*
*/
export interface GetEventsRequest {
status?: EventStatus;
search?: string;
objective?: string;
page?: number;
size?: number;
sort?: string;
order?: 'asc' | 'desc';
}
/**
*
*/
export type GetEventsResponse = ApiResponse<PageResponse<EventDetail>>;
/**
*
*/
export interface SelectObjectiveRequest {
objective: EventObjective;
}
/**
*
*/
export interface EventCreatedResponse {
eventId: string;
objective: EventObjective;
status: EventStatus;
createdAt: string;
}
/**
* AI
*/
export interface AiRecommendationRequest {
storeCategory?: string;
targetAudience?: string;
budget?: number;
additionalInfo?: string;
}
/**
* Job
*/
export interface JobAcceptedResponse {
jobId: string;
eventId: string;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
estimatedCompletionTime?: string;
}
/**
*
*/
export interface ImageGenerationRequest {
prompt: string;
numberOfImages?: number;
style?: string;
}
/**
*
*/
export interface ImageGenerationResponse {
jobId: string;
eventId: string;
status: string;
}

View File

@ -0,0 +1,200 @@
import { useState, useEffect } from 'react';
import { eventApi } from '../api/eventApi';
import type {
EventDetail,
GetEventsRequest,
EventStatus,
PageResponse,
} from './types';
/**
* useEvents Hook
*
*/
export function useEvents(initialParams?: GetEventsRequest) {
const [events, setEvents] = useState<EventDetail[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [pageInfo, setPageInfo] = useState<Omit<PageResponse<EventDetail>, 'content'>>({
page: 0,
size: 20,
totalElements: 0,
totalPages: 0,
first: true,
last: true,
});
const fetchEvents = async (params?: GetEventsRequest) => {
try {
setLoading(true);
setError(null);
console.log('🔄 Fetching events with params:', params);
const response = await eventApi.getEvents(params);
console.log('✅ Events fetched:', response);
if (response.success && response.data) {
setEvents(response.data.content);
setPageInfo({
page: response.data.page,
size: response.data.size,
totalElements: response.data.totalElements,
totalPages: response.data.totalPages,
first: response.data.first,
last: response.data.last,
});
}
} catch (err) {
console.error('❌ Error fetching events:', err);
setError(err as Error);
} finally {
setLoading(false);
}
};
// 초기 로드
useEffect(() => {
fetchEvents(initialParams);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
events,
loading,
error,
pageInfo,
refetch: fetchEvents,
};
}
/**
* useEvent Hook
*
*/
export function useEvent(eventId: string) {
const [event, setEvent] = useState<EventDetail | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchEvent = async () => {
if (!eventId) return;
try {
setLoading(true);
setError(null);
console.log('🔄 Fetching event:', eventId);
const response = await eventApi.getEvent(eventId);
console.log('✅ Event fetched:', response);
if (response.success && response.data) {
setEvent(response.data);
}
} catch (err) {
console.error('❌ Error fetching event:', err);
setError(err as Error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchEvent();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventId]);
return {
event,
loading,
error,
refetch: fetchEvent,
};
}
/**
* useEventActions Hook
* , ,
*/
export function useEventActions() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const createEvent = async (objective: string) => {
try {
setLoading(true);
setError(null);
console.log('🔄 Creating event with objective:', objective);
const response = await eventApi.createEvent({ objective: objective as any });
console.log('✅ Event created:', response);
return response.data;
} catch (err) {
console.error('❌ Error creating event:', err);
setError(err as Error);
throw err;
} finally {
setLoading(false);
}
};
const deleteEvent = async (eventId: string) => {
try {
setLoading(true);
setError(null);
console.log('🔄 Deleting event:', eventId);
await eventApi.deleteEvent(eventId);
console.log('✅ Event deleted');
} catch (err) {
console.error('❌ Error deleting event:', err);
setError(err as Error);
throw err;
} finally {
setLoading(false);
}
};
const publishEvent = async (eventId: string) => {
try {
setLoading(true);
setError(null);
console.log('🔄 Publishing event:', eventId);
await eventApi.publishEvent(eventId);
console.log('✅ Event published');
} catch (err) {
console.error('❌ Error publishing event:', err);
setError(err as Error);
throw err;
} finally {
setLoading(false);
}
};
const endEvent = async (eventId: string) => {
try {
setLoading(true);
setError(null);
console.log('🔄 Ending event:', eventId);
await eventApi.endEvent(eventId);
console.log('✅ Event ended');
} catch (err) {
console.error('❌ Error ending event:', err);
setError(err as Error);
throw err;
} finally {
setLoading(false);
}
};
return {
createEvent,
deleteEvent,
publishEvent,
endEvent,
loading,
error,
};
}