diff --git a/src/app/(main)/events/page.tsx b/src/app/(main)/events/page.tsx
index bf80422..40bfc67 100644
--- a/src/app/(main)/events/page.tsx
+++ b/src/app/(main)/events/page.tsx
@@ -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 && (
+
+
+
+ 이벤트 목록을 불러오는 중...
+
+
+ )}
+
+ {/* Error State */}
+ {error && (
+
+
+
+
+ 이벤트 목록을 불러오는데 실패했습니다
+
+
+ {error.message}
+
+ 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' },
+ }}
+ >
+ 다시 시도
+
+
+
+ )}
+
{/* Summary Statistics */}
diff --git a/src/entities/event/api/eventApi.ts b/src/entities/event/api/eventApi.ts
new file mode 100644
index 0000000..929eefe
--- /dev/null
+++ b/src/entities/event/api/eventApi.ts
@@ -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 => {
+ console.log('📞 eventApi.getEvents 호출', params);
+ const response = await eventApiClient.get(EVENT_API_BASE, {
+ params,
+ });
+ return response.data;
+ },
+
+ /**
+ * 이벤트 상세 조회
+ */
+ getEvent: async (eventId: string): Promise> => {
+ console.log('📞 eventApi.getEvent 호출', eventId);
+ const response = await eventApiClient.get>(
+ `${EVENT_API_BASE}/${eventId}`
+ );
+ return response.data;
+ },
+
+ /**
+ * 이벤트 생성 (목적 선택)
+ */
+ createEvent: async (
+ data: SelectObjectiveRequest
+ ): Promise> => {
+ console.log('📞 eventApi.createEvent 호출', data);
+ const response = await eventApiClient.post>(
+ `${EVENT_API_BASE}/objectives`,
+ data
+ );
+ return response.data;
+ },
+
+ /**
+ * 이벤트 삭제
+ */
+ deleteEvent: async (eventId: string): Promise> => {
+ console.log('📞 eventApi.deleteEvent 호출', eventId);
+ const response = await eventApiClient.delete>(
+ `${EVENT_API_BASE}/${eventId}`
+ );
+ return response.data;
+ },
+
+ /**
+ * 이벤트 배포
+ */
+ publishEvent: async (eventId: string): Promise> => {
+ console.log('📞 eventApi.publishEvent 호출', eventId);
+ const response = await eventApiClient.post>(
+ `${EVENT_API_BASE}/${eventId}/publish`
+ );
+ return response.data;
+ },
+
+ /**
+ * 이벤트 종료
+ */
+ endEvent: async (eventId: string): Promise> => {
+ console.log('📞 eventApi.endEvent 호출', eventId);
+ const response = await eventApiClient.post>(
+ `${EVENT_API_BASE}/${eventId}/end`
+ );
+ return response.data;
+ },
+
+ /**
+ * AI 추천 요청
+ */
+ requestAiRecommendations: async (
+ eventId: string,
+ data: AiRecommendationRequest
+ ): Promise> => {
+ console.log('📞 eventApi.requestAiRecommendations 호출', eventId, data);
+ const response = await eventApiClient.post>(
+ `${EVENT_API_BASE}/${eventId}/ai-recommendations`,
+ data
+ );
+ return response.data;
+ },
+
+ /**
+ * 이미지 생성 요청
+ */
+ requestImageGeneration: async (
+ eventId: string,
+ data: ImageGenerationRequest
+ ): Promise> => {
+ console.log('📞 eventApi.requestImageGeneration 호출', eventId, data);
+ const response = await eventApiClient.post>(
+ `${EVENT_API_BASE}/${eventId}/images`,
+ data
+ );
+ return response.data;
+ },
+};
+
+export default eventApi;
diff --git a/src/entities/event/model/types.ts b/src/entities/event/model/types.ts
new file mode 100644
index 0000000..b3de860
--- /dev/null
+++ b/src/entities/event/model/types.ts
@@ -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 {
+ content: T[];
+ page: number;
+ size: number;
+ totalElements: number;
+ totalPages: number;
+ first: boolean;
+ last: boolean;
+}
+
+/**
+ * API 표준 응답
+ */
+export interface ApiResponse {
+ 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>;
+
+/**
+ * 이벤트 목적 선택 요청
+ */
+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;
+}
diff --git a/src/entities/event/model/useEvents.ts b/src/entities/event/model/useEvents.ts
new file mode 100644
index 0000000..7eeb1d7
--- /dev/null
+++ b/src/entities/event/model/useEvents.ts
@@ -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([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [pageInfo, setPageInfo] = useState, '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(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(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(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,
+ };
+}