diff --git a/src/app/(main)/events/[eventId]/page.tsx b/src/app/(main)/events/[eventId]/page.tsx index 412f739..dfcf8e9 100644 --- a/src/app/(main)/events/[eventId]/page.tsx +++ b/src/app/(main)/events/[eventId]/page.tsx @@ -687,8 +687,8 @@ export default function EventDetailPage() { - {/* Chart Section - 참여 추이 */} - + {/* Chart Section - 참여 추이 - 임시 주석처리 */} + {/* 📈 참여 추이 @@ -757,7 +757,7 @@ export default function EventDetailPage() { - + */} {/* Chart Section - 채널별 성과 & ROI 추이 */} diff --git a/src/app/(main)/events/create/page.tsx b/src/app/(main)/events/create/page.tsx index 59ccec7..97470b7 100644 --- a/src/app/(main)/events/create/page.tsx +++ b/src/app/(main)/events/create/page.tsx @@ -3,11 +3,13 @@ import { useFunnel } from '@use-funnel/browser'; import { useRouter } from 'next/navigation'; import ObjectiveStep from './steps/ObjectiveStep'; +import LoadingStep from './steps/LoadingStep'; import RecommendationStep from './steps/RecommendationStep'; import ContentPreviewStep from './steps/ContentPreviewStep'; import ContentEditStep from './steps/ContentEditStep'; import ChannelStep from './steps/ChannelStep'; import ApprovalStep from './steps/ApprovalStep'; +import type { AiRecommendationResult } from '@/shared/api/eventApi'; // 이벤트 생성 데이터 타입 export type EventObjective = 'new_customer' | 'revisit' | 'sales' | 'awareness'; @@ -18,6 +20,7 @@ export interface EventData { eventDraftId?: number; eventId?: string; objective?: EventObjective; + aiResult?: AiRecommendationResult; recommendation?: { recommendation: { optionNumber: number; @@ -74,6 +77,7 @@ export default function EventCreatePage() { const funnel = useFunnel<{ objective: EventData; + loading: EventData; recommendation: EventData; contentPreview: EventData; contentEdit: EventData; @@ -97,13 +101,27 @@ export default function EventCreatePage() { objective={({ history }) => ( { - history.push('recommendation', { objective, eventId }); + history.push('loading', { objective, eventId }); + }} + /> + )} + loading={({ context, history }) => ( + { + history.push('recommendation', { ...context, aiResult }); + }} + onError={(error) => { + console.error('❌ AI 추천 생성 실패:', error); + alert(error); + history.go(-1); // ObjectiveStep으로 돌아가기 }} /> )} recommendation={({ context, history }) => ( { history.push('channel', { ...context, recommendation }); }} @@ -122,13 +140,18 @@ export default function EventCreatePage() { if (needsContent) { // localStorage에 이벤트 정보 저장 + const baseTrends = context.recommendation?.recommendation.promotionChannels || []; + const requiredTrends = ['Samgyupsal', '삼겹살', 'Korean Pork BBQ']; + // 중복 제거하면서 필수 trends 추가 + const allTrends = [...new Set([...requiredTrends, ...baseTrends])]; + const eventData = { eventDraftId: context.recommendation?.eventId || String(Date.now()), // eventId 사용 eventTitle: context.recommendation?.recommendation.title || '', eventDescription: context.recommendation?.recommendation.description || '', industry: '', location: '', - trends: context.recommendation?.recommendation.promotionChannels || [], + trends: allTrends, prize: '', }; localStorage.setItem('eventCreationData', JSON.stringify(eventData)); diff --git a/src/app/(main)/events/create/steps/ApprovalStep.tsx b/src/app/(main)/events/create/steps/ApprovalStep.tsx index 2937336..c119a8a 100644 --- a/src/app/(main)/events/create/steps/ApprovalStep.tsx +++ b/src/app/(main)/events/create/steps/ApprovalStep.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Box, Container, @@ -17,12 +17,29 @@ import { DialogActions, Link, } from '@mui/material'; -import { ArrowBack, CheckCircle, Edit, RocketLaunch, Save, People, AttachMoney, TrendingUp } from '@mui/icons-material'; +import { + ArrowBack, + CheckCircle, + RocketLaunch, + Save, + People, + AttachMoney, + TrendingUp, +} from '@mui/icons-material'; import { EventData } from '../page'; import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles'; import { eventApi } from '@/entities/event/api/eventApi'; import type { EventObjective } from '@/entities/event/model/types'; +interface EventCreationData { + eventDraftId: string; + eventTitle: string; + eventDescription: string; + industry: string; + location: string; + trends: string[]; + prize: string; +} interface ApprovalStepProps { eventData: EventData; @@ -35,7 +52,17 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS const [termsDialogOpen, setTermsDialogOpen] = useState(false); const [successDialogOpen, setSuccessDialogOpen] = useState(false); const [isDeploying, setIsDeploying] = useState(false); - const DISTRIBUTION_API_BASE_URL = process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + const [localStorageData, setLocalStorageData] = useState(null); + const DISTRIBUTION_API_BASE_URL = + process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || + 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + + useEffect(() => { + const storedData = localStorage.getItem('eventCreationData'); + if (storedData) { + setLocalStorageData(JSON.parse(storedData)); + } + }, []); const handleApprove = async () => { if (!agreeTerms) return; @@ -48,13 +75,15 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS // objective 매핑 (Frontend → Backend) const objectiveMap: Record = { - 'new_customer': 'CUSTOMER_ACQUISITION', - 'revisit': 'Customer Retention', - 'sales': 'Sales Promotion', - 'awareness': 'awareness', + new_customer: 'CUSTOMER_ACQUISITION', + revisit: 'Customer Retention', + sales: 'Sales Promotion', + awareness: 'awareness', }; - const backendObjective: EventObjective = (objectiveMap[eventData.objective || 'new_customer'] || 'CUSTOMER_ACQUISITION') as EventObjective; + const backendObjective: EventObjective = (objectiveMap[ + eventData.objective || 'new_customer' + ] || 'CUSTOMER_ACQUISITION') as EventObjective; const createResponse = await eventApi.createEvent({ objective: backendObjective, @@ -70,7 +99,10 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS console.log('📞 Updating event details:', eventId); // 이벤트명 가져오기 (contentEdit.title 또는 recommendation.title) - const eventName = eventData.contentEdit?.title || eventData.recommendation?.recommendation?.title || '이벤트'; + const eventName = + eventData.contentEdit?.title || + eventData.recommendation?.recommendation?.title || + '이벤트'; // 날짜 설정 (오늘부터 30일간) const today = new Date(); @@ -82,7 +114,10 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS await eventApi.updateEvent(eventId, { eventName: eventName, - description: eventData.contentEdit?.guide || eventData.recommendation?.recommendation?.description || '', + description: + eventData.contentEdit?.guide || + eventData.recommendation?.recommendation?.description || + '', startDate: startDateStr, endDate: endDateStr, }); @@ -96,12 +131,15 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS sns: ['INSTAGRAM', 'NAVER', 'KAKAO'], }; - const apiChannels = eventData.channels?.flatMap(ch => channelMap[ch] || []) || []; + const apiChannels = eventData.channels?.flatMap((ch) => channelMap[ch] || []) || []; const distributionRequest = { eventId: eventId, title: eventName, - description: eventData.contentEdit?.guide || eventData.recommendation?.recommendation?.description || '', + description: + eventData.contentEdit?.guide || + eventData.recommendation?.recommendation?.description || + '', imageUrl: '', // TODO: 이미지 URL 연동 필요 channels: apiChannels, channelSettings: {}, @@ -109,13 +147,16 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS console.log('🚀 Distributing event:', distributionRequest); - const response = await fetch(`${DISTRIBUTION_API_BASE_URL}/api/v1/distribution/distribute`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(distributionRequest), - }); + const response = await fetch( + `${DISTRIBUTION_API_BASE_URL}/api/v1/distribution/distribute`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(distributionRequest), + } + ); if (!response.ok) { const errorData = await response.json(); @@ -127,7 +168,6 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS setIsDeploying(false); setSuccessDialogOpen(true); - } else { throw new Error('Event creation failed: No event ID returned'); } @@ -138,7 +178,6 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS } }; - const handleSaveDraft = () => { // TODO: 임시저장 API 연동 alert('임시저장되었습니다'); @@ -156,10 +195,27 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS }; return ( - - + + {/* Header */} - + @@ -180,7 +236,7 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS {/* Event Summary Statistics */} - + - - - + + + 이벤트 제목 - {eventData.recommendation?.recommendation.title || '이벤트 제목'} + {localStorageData?.eventTitle || + eventData.recommendation?.recommendation.title || + '이벤트 제목'} @@ -228,19 +292,24 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS borderColor: 'transparent', }} > - - - + + + 목표 참여자 {eventData.recommendation?.recommendation.expectedMetrics.newCustomers.max || 0} - + @@ -274,19 +346,24 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS borderColor: 'transparent', }} > - - - + + + 예상 비용 - {((eventData.recommendation?.recommendation.estimatedCost.max || 0) / 10000).toFixed(0)} - + {( + (eventData.recommendation?.recommendation.estimatedCost.max || 0) / 10000 + ).toFixed(0)} + 만원 @@ -320,19 +402,24 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS borderColor: 'transparent', }} > - - - + + + 예상 ROI @@ -357,53 +444,62 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS - - - - - 이벤트 제목 - - - {eventData.recommendation?.recommendation.title} - - - - - - + + + 이벤트 제목 + + + {localStorageData?.eventTitle || + eventData.recommendation?.recommendation.title || + '이벤트 제목'} + - - - - - 경품 - - - {eventData.recommendation?.recommendation.mechanics.details || ''} - - - - - - + + + 경품 + + + {localStorageData?.prize || + eventData.recommendation?.recommendation.mechanics.details || + ''} + - - - - - 참여 방법 - - - {eventData.recommendation?.recommendation.mechanics.details || ''} - - - + + + 참여 방법 + + + {localStorageData?.eventDescription || + eventData.recommendation?.recommendation.mechanics.details || + ''} + @@ -413,8 +509,8 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS - - + + {getChannelNames(eventData.channels).map((channel) => ( ))} - {/* Terms Agreement */} - - + + } label={ - + 이벤트 약관 및 개인정보 처리방침에 동의합니다{' '} - + (필수) @@ -551,21 +639,33 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS 제1조 (목적) - - 본 약관은 KT AI 이벤트 마케팅 서비스를 통해 진행되는 이벤트의 참여 및 개인정보 처리에 관한 - 사항을 규정합니다. + + 본 약관은 KT AI 이벤트 마케팅 서비스를 통해 진행되는 이벤트의 참여 및 개인정보 처리에 + 관한 사항을 규정합니다. 제2조 (개인정보 수집 및 이용) - + 수집 항목: 이름, 전화번호, 이메일 - + 이용 목적: 이벤트 참여 확인 및 경품 제공 - + 보유 기간: 이벤트 종료 후 6개월 @@ -616,7 +716,11 @@ export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalS 배포 완료! - + 이벤트가 성공적으로 배포되었습니다.
실시간으로 참여자를 확인할 수 있습니다. diff --git a/src/app/(main)/events/create/steps/ChannelStep.tsx b/src/app/(main)/events/create/steps/ChannelStep.tsx index aa4df36..c2cac05 100644 --- a/src/app/(main)/events/create/steps/ChannelStep.tsx +++ b/src/app/(main)/events/create/steps/ChannelStep.tsx @@ -14,6 +14,8 @@ import { FormControl, InputLabel, IconButton, + Radio, + RadioGroup, } from '@mui/material'; import { ArrowBack } from '@mui/icons-material'; @@ -108,18 +110,18 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { return ( - + {/* Header */} - - - + + + - + 배포 채널 선택 - + (최소 1개 이상) @@ -127,8 +129,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { - + handleChannelToggle('uriTV')} sx={{ + p: { xs: 0.5, sm: 1 }, color: colors.purple, '&.Mui-checked': { color: colors.purple, @@ -151,46 +154,48 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { /> } label={ - + 우리동네TV } - sx={{ mb: channels[0].selected ? 2 : 0 }} + sx={{ mb: channels[0].selected ? { xs: 1.5, sm: 2 } : 0 }} /> {channels[0].selected && ( - - - 반경 + + + 반경 - - 노출 시간대 + + 노출 시간대 - + 예상 노출: 5만명 - + 비용: 8만원 @@ -202,8 +207,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { - + handleChannelToggle('ringoBiz')} sx={{ + p: { xs: 0.5, sm: 1 }, color: colors.purple, '&.Mui-checked': { color: colors.purple, @@ -226,30 +232,32 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { /> } label={ - + 링고비즈 } - sx={{ mb: channels[1].selected ? 2 : 0 }} + sx={{ mb: channels[1].selected ? { xs: 1.5, sm: 2 } : 0 }} /> {channels[1].selected && ( - + - + 연결음 자동 업데이트 - + 예상 노출: 3만명 - + 비용: 무료 @@ -261,8 +269,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { - + handleChannelToggle('genieTV')} sx={{ + p: { xs: 0.5, sm: 1 }, color: colors.purple, '&.Mui-checked': { color: colors.purple, @@ -285,37 +294,39 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { /> } label={ - + 지니TV 광고 } - sx={{ mb: channels[2].selected ? 2 : 0 }} + sx={{ mb: channels[2].selected ? { xs: 1.5, sm: 2 } : 0 }} /> {channels[2].selected && ( - - - 지역 + + + 지역 - - 노출 시간대 + + 노출 시간대 @@ -327,10 +338,12 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { value={getChannelConfig('genieTV', 'budget')} onChange={(e) => handleConfigChange('genieTV', 'budget', e.target.value)} InputProps={{ inputProps: { min: 0, step: 10000 } }} - sx={{ mb: 2 }} + sx={{ mb: { xs: 1.5, sm: 2 } }} + InputLabelProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }} + inputProps={{ sx: { fontSize: { xs: '0.875rem', sm: '1rem' } } }} /> - + 예상 노출:{' '} {getChannelConfig('genieTV', 'budget') @@ -347,8 +360,8 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { - + handleChannelToggle('sns')} sx={{ + p: { xs: 0.5, sm: 1 }, color: colors.purple, '&.Mui-checked': { color: colors.purple, @@ -371,16 +385,16 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { /> } label={ - + SNS } - sx={{ mb: channels[3].selected ? 2 : 0 }} + sx={{ mb: channels[3].selected ? { xs: 1.5, sm: 2 } : 0 }} /> {channels[3].selected && ( - - + + 플랫폼 선택 } - label="Instagram" + label={Instagram} sx={{ display: 'block' }} /> } - label="Naver Blog" + label={Naver Blog} sx={{ display: 'block' }} /> } - label="Kakao Channel" - sx={{ display: 'block', mb: 2 }} + label={Kakao Channel} + sx={{ display: 'block', mb: { xs: 1.5, sm: 2 } }} /> - - 예약 게시 + + 예약 게시 - + 예상 노출: - - + 비용: 무료 @@ -465,26 +483,26 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { - - - + + + 총 예상 비용 - + {totalCost.toLocaleString()}원 - + 총 예상 노출 - + {totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'} @@ -492,16 +510,16 @@ export default function ChannelStep({ onNext, onBack }: ChannelStepProps) { {/* Action Buttons */} - + diff --git a/src/app/(main)/events/create/steps/RecommendationStep.tsx b/src/app/(main)/events/create/steps/RecommendationStep.tsx index 4aabc27..5de6a44 100644 --- a/src/app/(main)/events/create/steps/RecommendationStep.tsx +++ b/src/app/(main)/events/create/steps/RecommendationStep.tsx @@ -42,6 +42,7 @@ const colors = { interface RecommendationStepProps { eventId?: string; // 이전 단계에서 생성된 eventId + aiResult?: AiRecommendationResult; // LoadingStep에서 전달받은 AI 추천 결과 onNext: (data: { recommendation: EventRecommendation; eventId: string }) => void; onBack: () => void; } @@ -60,6 +61,7 @@ const getCookie = (name: string): string | null => { export default function RecommendationStep({ eventId: initialEventId, + aiResult: initialAiResult, onNext, onBack, }: RecommendationStepProps) { @@ -67,7 +69,7 @@ export default function RecommendationStep({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [aiResult, setAiResult] = useState(null); + const [aiResult, setAiResult] = useState(initialAiResult || null); const [selected, setSelected] = useState(null); const [editedData, setEditedData] = useState< Record @@ -78,7 +80,15 @@ export default function RecommendationStep({ // 컴포넌트 마운트 시 AI 추천 결과 조회 useEffect(() => { - // props에서만 eventId를 받음 + // LoadingStep에서 이미 데이터를 전달받은 경우 API 호출 생략 + if (initialAiResult) { + console.log('✅ LoadingStep에서 전달받은 AI 추천 결과 사용:', initialAiResult); + setAiResult(initialAiResult); + setEventId(initialEventId || null); + return; + } + + // props에서만 eventId를 받음 (하위 호환성) if (initialEventId) { // 이미 요청한 eventId면 중복 요청하지 않음 if (requestedEventIdRef.current === initialEventId) { @@ -95,28 +105,29 @@ export default function RecommendationStep({ console.error('❌ eventId가 없습니다. ObjectiveStep으로 돌아가세요.'); setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.'); } - }, [initialEventId]); + }, [initialEventId, initialAiResult]); const fetchAIRecommendations = async (evtId: string) => { try { setLoading(true); setError(null); - console.log('📡 AI 추천 요청 시작, eventId:', evtId); + console.log('📡 AI 추천 결과 조회 시작, eventId:', evtId); - // POST /events/{eventId}/ai-recommendations 엔드포인트로 AI 추천 요청 - const recommendations = await eventApi.requestAiRecommendations(evtId); + // GET /events/{eventId}/ai-recommendations 엔드포인트로 AI 추천 결과 조회 + // ObjectiveStep에서 이미 POST 요청으로 생성했으므로 GET으로 조회 + const recommendations = await eventApi.getAiRecommendations(evtId); - console.log('✅ AI 추천 요청 성공:', recommendations); + console.log('✅ AI 추천 결과 조회 성공:', recommendations); setAiResult(recommendations); setLoading(false); } catch (err: any) { - console.error('❌ AI 추천 요청 실패:', err); + console.error('❌ AI 추천 결과 조회 실패:', err); const errorMessage = err.response?.data?.message || err.response?.data?.error || - 'AI 추천을 생성하는데 실패했습니다'; + 'AI 추천 결과를 조회하는데 실패했습니다'; setError(errorMessage); setLoading(false); @@ -132,14 +143,25 @@ export default function RecommendationStep({ try { setLoading(true); - // AI 추천 선택 API 호출 - await eventApi.selectRecommendation(eventId, { - recommendationId: `${eventId}-opt${selected}`, - customizations: { - eventName: edited?.title || selectedRec.title, + // localStorage에 선택한 추천 정보 저장 + const selectedRecommendationData = { + eventId, + selectedOptionNumber: selected, + recommendation: { + ...selectedRec, + title: edited?.title || selectedRec.title, description: edited?.description || selectedRec.description, }, - }); + trendAnalysis: aiResult.trendAnalysis, + timestamp: new Date().toISOString(), + }; + + try { + localStorage.setItem('selectedRecommendation', JSON.stringify(selectedRecommendationData)); + console.log('💾 localStorage에 선택한 추천 정보 저장 완료:', selectedRecommendationData); + } catch (error) { + console.error('❌ localStorage 저장 실패:', error); + } // 다음 단계로 이동 onNext({ @@ -182,24 +204,24 @@ export default function RecommendationStep({ if (loading) { return ( - - - - + + + + - + AI 이벤트 추천 - - + + AI가 최적의 이벤트를 생성하고 있습니다... - + 업종, 지역, 시즌 트렌드를 분석하여 맞춤형 이벤트를 추천합니다 @@ -212,30 +234,30 @@ export default function RecommendationStep({ if (error) { return ( - - - - + + + + - + AI 이벤트 추천 - + {error} - + diff --git a/src/app/api/content/events/[eventId]/images/route.ts b/src/app/api/content/events/[eventId]/images/route.ts new file mode 100644 index 0000000..3cd48b2 --- /dev/null +++ b/src/app/api/content/events/[eventId]/images/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * Content API Proxy: 이미지 조회 + * GET /api/content/events/{eventId}/images + */ +export async function GET( + request: NextRequest, + { params }: { params: { eventId: string } } +) { + try { + const { eventId } = params; + const { searchParams } = new URL(request.url); + + const CONTENT_API_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + const queryString = searchParams.toString(); + const backendUrl = `${CONTENT_API_HOST}/api/v1/content/events/${eventId}/images${queryString ? `?${queryString}` : ''}`; + + console.log('🎨 Proxying to Content API:', backendUrl); + + const backendResponse = await fetch(backendUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': request.headers.get('Authorization') || '', + }, + }); + + const data = await backendResponse.json(); + + console.log('✅ Content API Response:', { + status: backendResponse.status, + imageCount: Array.isArray(data) ? data.length : 0, + }); + + return NextResponse.json(data, { status: backendResponse.status }); + } catch (error) { + console.error('❌ Content API Proxy Error:', error); + + return NextResponse.json( + { + success: false, + errorCode: 'PROXY_ERROR', + message: 'Content API 호출 중 오류가 발생했습니다', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/content/images/generate/route.ts b/src/app/api/content/images/generate/route.ts new file mode 100644 index 0000000..3bc30fc --- /dev/null +++ b/src/app/api/content/images/generate/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * Content API Proxy: 이미지 생성 + * POST /api/content/images/generate + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + const CONTENT_API_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + const backendUrl = `${CONTENT_API_HOST}/api/v1/content/images/generate`; + + console.log('🎨 Proxying to Content API:', backendUrl); + console.log('📦 Request body:', body); + + const backendResponse = await fetch(backendUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': request.headers.get('Authorization') || '', + }, + body: JSON.stringify(body), + }); + + const data = await backendResponse.json(); + + console.log('✅ Content API Response:', { + status: backendResponse.status, + data, + }); + + return NextResponse.json(data, { status: backendResponse.status }); + } catch (error) { + console.error('❌ Content API Proxy Error:', error); + + return NextResponse.json( + { + success: false, + errorCode: 'PROXY_ERROR', + message: 'Content API 호출 중 오류가 발생했습니다', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/content/images/jobs/[jobId]/route.ts b/src/app/api/content/images/jobs/[jobId]/route.ts new file mode 100644 index 0000000..c5edb61 --- /dev/null +++ b/src/app/api/content/images/jobs/[jobId]/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * Content API Proxy: Job 상태 조회 + * GET /api/content/images/jobs/{jobId} + */ +export async function GET( + request: NextRequest, + { params }: { params: { jobId: string } } +) { + try { + const { jobId } = params; + + const CONTENT_API_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io'; + const backendUrl = `${CONTENT_API_HOST}/api/v1/content/images/jobs/${jobId}`; + + console.log('🎨 Proxying to Content API:', backendUrl); + + const backendResponse = await fetch(backendUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': request.headers.get('Authorization') || '', + }, + }); + + const data = await backendResponse.json(); + + console.log('✅ Content API Response:', { + status: backendResponse.status, + jobStatus: data.status, + progress: data.progress, + }); + + return NextResponse.json(data, { status: backendResponse.status }); + } catch (error) { + console.error('❌ Content API Proxy Error:', error); + + return NextResponse.json( + { + success: false, + errorCode: 'PROXY_ERROR', + message: 'Content API 호출 중 오류가 발생했습니다', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/src/shared/api/eventApi.ts b/src/shared/api/eventApi.ts index dc2e759..7736fce 100644 --- a/src/shared/api/eventApi.ts +++ b/src/shared/api/eventApi.ts @@ -92,6 +92,9 @@ export interface AiRecommendationRequest { category: string; description?: string; }; + region?: string; + targetAudience?: string; + budget?: number; } export interface JobAcceptedResponse { @@ -306,9 +309,13 @@ export const eventApi = { }, // Step 2: AI 추천 요청 (POST) - requestAiRecommendations: async (eventId: string): Promise => { - const response = await eventApiClient.post( - `/events/${eventId}/ai-recommendations` + requestAiRecommendations: async ( + eventId: string, + request: AiRecommendationRequest + ): Promise => { + const response = await eventApiClient.post( + `/events/${eventId}/ai-recommendations`, + request ); console.log('✅ AI 추천 요청 성공:', response.data); return response.data; @@ -316,11 +323,12 @@ export const eventApi = { // AI 추천 결과 조회 (GET) getAiRecommendations: async (eventId: string): Promise => { - const response = await eventApiClient.get( + const response = await eventApiClient.get<{ success: boolean; data: AiRecommendationResult; timestamp: string }>( `/events/${eventId}/ai-recommendations` ); - console.log('✅ AI 추천 결과 조회:', response.data); - return response.data; + console.log('✅ AI 추천 결과 조회 (전체 응답):', response.data); + console.log('✅ AI 추천 데이터:', response.data.data); + return response.data.data; // 래퍼에서 실제 데이터 추출 }, // AI 추천 선택