mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2026-01-21 13:36:24 +00:00
이벤트 엔티티 및 페이지 기능 추가
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4511957ff6
commit
78cc41b453
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -37,78 +37,12 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import Header from '@/shared/ui/Header';
|
import Header from '@/shared/ui/Header';
|
||||||
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
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 데이터
|
// ==================== API 연동 ====================
|
||||||
const mockEvents = [
|
// Mock 데이터를 실제 API 호출로 교체
|
||||||
{
|
// 백업 파일: page.tsx.backup
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type EventStatus = 'all' | 'active' | 'scheduled' | 'ended';
|
type EventStatus = 'all' | 'active' | 'scheduled' | 'ended';
|
||||||
type Period = '1month' | '3months' | '6months' | '1year' | 'all';
|
type Period = '1month' | '3months' | '6months' | '1year' | 'all';
|
||||||
@ -123,8 +57,57 @@ export default function EventsPage() {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const itemsPerPage = 20;
|
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) => {
|
.filter((event) => {
|
||||||
const matchesSearch = event.title.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = event.title.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
const matchesStatus = statusFilter === 'all' || event.status === statusFilter;
|
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;
|
if (event.status !== 'active') return 0;
|
||||||
const total = new Date(event.endDate).getTime() - new Date(event.startDate).getTime();
|
const startTime = new Date(event.startDate).getTime();
|
||||||
const elapsed = Date.now() - 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);
|
return Math.min(Math.max((elapsed / total) * 100, 0), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 통계 계산
|
// 통계 계산
|
||||||
const stats = {
|
const stats = {
|
||||||
total: mockEvents.length,
|
total: transformedEvents.length,
|
||||||
active: mockEvents.filter((e) => e.status === 'active').length,
|
active: transformedEvents.filter((e) => e.status === 'active').length,
|
||||||
totalParticipants: mockEvents.reduce((sum, e) => sum + e.participants, 0),
|
totalParticipants: transformedEvents.reduce((sum, e) => sum + e.participants, 0),
|
||||||
avgROI: Math.round(
|
avgROI: transformedEvents.filter((e) => e.roi > 0).length > 0
|
||||||
mockEvents.filter((e) => e.roi > 0).reduce((sum, e) => sum + e.roi, 0) /
|
? Math.round(
|
||||||
mockEvents.filter((e) => e.roi > 0).length
|
transformedEvents.filter((e) => e.roi > 0).reduce((sum, e) => sum + e.roi, 0) /
|
||||||
),
|
transformedEvents.filter((e) => e.roi > 0).length
|
||||||
|
)
|
||||||
|
: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -237,6 +224,59 @@ export default function EventsPage() {
|
|||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 6 }, px: { xs: 3, sm: 6, md: 10 } }}
|
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 */}
|
{/* Summary Statistics */}
|
||||||
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mb: { xs: 4, sm: 8 } }}>
|
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mb: { xs: 4, sm: 8 } }}>
|
||||||
<Grid item xs={6} sm={3}>
|
<Grid item xs={6} sm={3}>
|
||||||
|
|||||||
198
src/entities/event/api/eventApi.ts
Normal file
198
src/entities/event/api/eventApi.ts
Normal 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;
|
||||||
173
src/entities/event/model/types.ts
Normal file
173
src/entities/event/model/types.ts
Normal 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;
|
||||||
|
}
|
||||||
200
src/entities/event/model/useEvents.ts
Normal file
200
src/entities/event/model/useEvents.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user