diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..c3f502a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 디폴트 무시된 파일 +/shelf/ +/workspace.xml +# 에디터 기반 HTTP 클라이언트 요청 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..01ad0ce --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..df7b4e1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..df380f3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/presentRecommend-f.iml b/.idea/presentRecommend-f.iml new file mode 100644 index 0000000..00af9fc --- /dev/null +++ b/.idea/presentRecommend-f.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/analysis/page.tsx b/app/analysis/page.tsx index 45c2800..928681b 100644 --- a/app/analysis/page.tsx +++ b/app/analysis/page.tsx @@ -1,10 +1,13 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" +import { useSearchParams } from "next/navigation" import Link from "next/link" import { Gift, Home, BarChart2, MessageSquare, Heart, Star } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { analyzeConversation } from "@/lib/analyze" +import type { AnalysisResult } from "@/lib/analyze" import { BarChart, Bar, @@ -13,139 +16,163 @@ import { CartesianGrid, Tooltip, ResponsiveContainer, - PieChart, - Pie, - Cell, - Legend, LineChart, Line, Area, AreaChart, } from "recharts" +import { useAnalysis } from "@/context/analysis-context" -// Mock data for demonstration -const sentimentData = [ - { name: "긍정적", value: 65, color: "#22c55e" }, - { name: "중립적", value: 25, color: "#94a3b8" }, - { name: "부정적", value: 10, color: "#ef4444" }, -] +export default function AnalysisPage() { + const searchParams = useSearchParams() + const fileId = searchParams.get('fileId') + const { setAnalysisResult } = useAnalysis() + const [analysisResult, setAnalysisResultState] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(true) -const keywordData = [ - { name: "생일", value: 15 }, - { name: "선물", value: 12 }, - { name: "축하", value: 10 }, - { name: "파티", value: 8 }, - { name: "기념일", value: 6 }, -] + useEffect(() => { + async function fetchAnalysis() { + if (!fileId) { + setError('파일 ID가 없습니다.') + setLoading(false) + return + } -const relationshipData = [ - { date: "1월", intimacy: 65, trend: 65 }, - { date: "2월", intimacy: 75, trend: 70 }, - { date: "3월", intimacy: 60, trend: 68 }, - { date: "4월", intimacy: 85, trend: 72 }, - { date: "5월", intimacy: 70, trend: 75 }, - { date: "6월", intimacy: 90, trend: 80 }, - { date: "7월", intimacy: 75, trend: 82 }, - { date: "8월", intimacy: 95, trend: 85 }, - { date: "9월", intimacy: 80, trend: 87 }, - { date: "10월", intimacy: 88, trend: 88 }, - { date: "11월", intimacy: 92, trend: 89 }, - { date: "12월", intimacy: 95, trend: 90 }, -] + try { + const results = await analyzeConversation(fileId) + setAnalysisResultState(results) + setAnalysisResult(results) + } catch (err) { + console.error('Analysis error:', err) + setError(err instanceof Error ? err.message : '분석 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } -const messageStyle = { - tone: "친근한", - keywords: ["생일", "축하", "행복"], -} + fetchAnalysis() + }, [fileId, setAnalysisResult]) + + if (loading) { + return ( +
+
+
+

분석 중...

+
+
+ ) + } + + if (error) { + return ( +
+
+

{error}

+ +
+
+ ) + } + + if (!analysisResult) { + return null + } + + // keywords: 모든 AnalysisResult의 keywords를 평탄화하여 name별로 score를 합산 + const keywordMap: Record = {} + analysisResult.forEach(item => { + item.keywords.forEach(kw => { + if (keywordMap[kw.name]) { + keywordMap[kw.name] += kw.score + } else { + keywordMap[kw.name] = kw.score + } + }) + }) + // 상위 5개만 추출 + const keywords = Object.entries(keywordMap) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value) + .slice(0, 5) + + // relationship: 날짜별로 intimacy를 모아 그래프 데이터 생성 + const relationship = analysisResult.map(item => ({ + date: item.date, + intimacy: item.intimacy, + })) -export default function AnalysisPage() { return ( -
+

카카오톡 대화 분석 결과

-

대화 데이터를 다양한 시각화로 분석해드립니다.

+

대화 데이터를 기반으로 주요 키워드와 관계 친밀도를 분석해드립니다.

-
- {/* 감정 분석 */} - - - - - 감정 분석 - - - -
- - - - {sentimentData.map((entry, index) => ( - - ))} - - - - - -
-
-
- +
{/* 키워드 분석 */} - + - - + + 주요 키워드 -
+
- - - - - + + + + +
-
-
{/* 관계 변화 추적 */} - + - - + + 관계 친밀도 변화 -
+
- + - - - - + + + + -
@@ -167,44 +187,9 @@ export default function AnalysisPage() {
-
- {/* 메시지 스타일 분석 */} - - - - - 대화 스타일 분석 - - - -
-
-
-

말투 특성

-

{messageStyle.tone}

-
-
-

주요 키워드

-
- {messageStyle.keywords.map((keyword) => ( - - {keyword} - - ))} -
-
-
-
-
-
-
-
-
diff --git a/app/page.tsx b/app/page.tsx index d9a09a6..c8f3952 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation" import { Gift, Upload } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" +import { uploadFile } from "@/lib/analyze" function FileUpload() { const [file, setFile] = useState(null) @@ -41,23 +42,8 @@ function FileUpload() { setError("") try { - const formData = new FormData() - formData.append("file", file) - - const response = await fetch("/api/upload", { - method: "POST", - body: formData, - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(data.error || "파일 업로드에 실패했습니다.") - } - - if (data.redirect) { - router.push(data.redirect) - } + const result = await uploadFile(file) + router.push(`/analysis?fileId=${result.fileId}`) } catch (err) { console.error('Upload error:', err) setError(err instanceof Error ? err.message : "파일 업로드 중 오류가 발생했습니다.") diff --git a/app/recommendations/page.tsx b/app/recommendations/page.tsx index a88c182..eaaac24 100644 --- a/app/recommendations/page.tsx +++ b/app/recommendations/page.tsx @@ -1,52 +1,71 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, useContext } from "react" +import { useSearchParams } from "next/navigation" import Link from "next/link" import { Gift, Home, BarChart2, Coins, ShoppingBag } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Slider } from "@/components/ui/slider" +import { generateGiftRecommendations } from "@/lib/analyze" import type { GiftItem } from "@/lib/utils" +import type { RecommendationResult } from "@/lib/utils" +import { useAnalysis } from "@/context/analysis-context" export default function RecommendationsPage() { - const [budget, setBudget] = useState(200000) - const [gifts, setGifts] = useState([]) + const searchParams = useSearchParams() + // const fileId = searchParams.get('fileId')is() + const { analysisResult } = useAnalysis() + const [selectedIndex, setSelectedIndex] = useState(0) + const [showGifts, setShowGifts] = useState(false) + const [recommendations, setRecommendations] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const fileId = searchParams.get('fileId') const fetchRecommendations = async () => { + if (!fileId) { + setError('fileId가 없습니다.') + setLoading(false) + return + } + if (!analysisResult || !Array.isArray(analysisResult) || analysisResult.length === 0) { + setError('분석 결과가 없습니다.') + setLoading(false) + return + } + try { setLoading(true) setError(null) - const keywords = ["골프", "스포츠", "운동"] - const response = await fetch(`/api/gifts?budget=${budget}&keywords=${keywords.join(',')}`) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || "선물 추천을 가져오는데 실패했습니다.") - } + // const results: RecommendationResult[] = await generateGiftRecommendations(analysisResult, {}) + console.log("🚀 추천 요청 전에 fileId 확인:", fileId); + const results = await generateGiftRecommendations(fileId) - const data = await response.json() - if (!Array.isArray(data) || data.length === 0) { + if (!results || results.length === 0) { throw new Error("추천할 수 있는 선물이 없습니다.") } - setGifts(data) + + // 날짜별로 정렬 + const sortedResults = results.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + setRecommendations(sortedResults) + setSelectedIndex(sortedResults.length - 1) + setShowGifts(true) } catch (err) { console.error('Error fetching recommendations:', err) setError(err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다.") + setShowGifts(false) } finally { setLoading(false) } } useEffect(() => { - fetchRecommendations() - }, []) - - const handleBudgetChange = (value: number[]) => { - setBudget(value[0]) - } + if (analysisResult) { + fetchRecommendations() + } + }, [analysisResult]) return (
@@ -57,7 +76,9 @@ export default function RecommendationsPage() {
{error && (
- 오류가 발생했습니다: {error} +

오류가 발생했습니다:

+

{error}

+

서버가 실행 중인지 확인해주세요.

)} {loading ? ( @@ -65,91 +86,53 @@ export default function RecommendationsPage() {

선물을 찾고 있습니다...

- ) : gifts.length > 0 ? ( -
- {gifts.map((gift, index) => ( - - -
- {gift.name} -
-

{gift.name}

-

{gift.brand}

-

{gift.price.toLocaleString()}원

- - - 구매하기 - -
-
- ))} -
) : ( -
-

선물 추천을 시작하려면 아래 버튼을 클릭하세요.

-
- )} - -
- - - - - 예산 설정 - - - -
-
- 최대 예산 -
- - {budget.toLocaleString()}원 - + <> + {recommendations.length > 0 && ( + <> +
+ {showGifts && recommendations[selectedIndex]?.recommendations.slice(0, 3).map(gift => ( + + +
+ {gift.name} +
+

{gift.name}

+

{gift.category}

+

{gift.price}

+

{gift.description}

+
+
+ ))}
-
-
-
+
{ setSelectedIndex(v); setShowGifts(false); }} + className="w-full max-w-lg" /> -
- {budget.toLocaleString()}원 +
+ {recommendations[selectedIndex]?.date}
+
-
- 0원 - 200,000원 -
-
-
- - - + + )} + + )}
) diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx index c31c2b3..4a119a1 100644 --- a/components/ui/slider.tsx +++ b/components/ui/slider.tsx @@ -17,10 +17,10 @@ const Slider = React.forwardRef< )} {...props} > - - + + - + )) Slider.displayName = SliderPrimitive.Root.displayName diff --git a/context/analysis-context.tsx b/context/analysis-context.tsx index 57be0e7..e157392 100644 --- a/context/analysis-context.tsx +++ b/context/analysis-context.tsx @@ -2,19 +2,24 @@ import { createContext, useContext, useState, type ReactNode } from "react" import type { AnalysisResult } from "@/lib/analyze" +import {RecommendationResult} from "@/lib/utils"; interface AnalysisContextType { - analysisResult: AnalysisResult | null - setAnalysisResult: (result: AnalysisResult) => void + analysisResult: AnalysisResult[] | null + setAnalysisResult: (result: AnalysisResult[]) => void recommendations: any[] | null setRecommendations: (recommendations: any[]) => void + fileId: string | null + setFileId: (id: string | null) => void } const AnalysisContext = createContext(undefined) export function AnalysisProvider({ children }: { children: ReactNode }) { - const [analysisResult, setAnalysisResult] = useState(null) + const [analysisResult, setAnalysisResult] = useState(null) const [recommendations, setRecommendations] = useState(null) + const [fileId, setFileId] = useState(null) + // const [recommendations, setRecommendations] = useState(null) return ( {children} diff --git a/lib/analyze.ts b/lib/analyze.ts index dae1cd8..2807676 100644 --- a/lib/analyze.ts +++ b/lib/analyze.ts @@ -1,108 +1,136 @@ // This is a simplified mock implementation // In a real application, you would use NLP libraries or AI services -import { readGiftItems, filterGiftItems, type GiftItem } from './utils' +import { type GiftItem, type RecommendationResult } from './utils' export interface AnalysisResult { - sentiment: { - positive: number - neutral: number - negative: number - } + category: string + date: string + intimacy: number keywords: { name: string - value: number - }[] - relationship: { - date: string - intimacy: number + score: number }[] - messageStyle: { - tone: string - keywords: string[] - example: string - } + subject: string } -let cachedGiftItems: GiftItem[] | null = null - -async function getGiftItems(): Promise { - if (cachedGiftItems) { - return cachedGiftItems - } - - cachedGiftItems = await readGiftItems() - return cachedGiftItems +interface ApiResponse { + success: boolean + data: T + message: string | null + error: string | null } -export async function analyzeConversation(text: string): Promise { - // In a real application, this would be a call to an NLP service - // or use a library like natural, sentiment, etc. +// 파일 업로드 API 호출 +export async function uploadFile(file: File): Promise<{ fileId: string }> { + const formData = new FormData() + formData.append('file', file) - // Mock implementation for demonstration - return { - keywords: ["Birthday", "Congratulations", "Party", "Gift"], - sentiment: { - positive: 35, - neutral: 25, - negative: 15, - }, - relationship: { - intimacy: 0.75, - tone: "Friendly", - }, + const response = await fetch('http://210.125.91.91:5000/api/upload', { + method: 'POST', + body: formData + }) + + if (!response.ok) { + throw new Error('파일 업로드에 실패했습니다.') } -} -export async function generateGiftRecommendations( - analysis: AnalysisResult, - filters: { - occasion?: string - recipient?: string - budget?: number + const result = await response.json() as ApiResponse<{ fileId: string }> + if (!result.success) { + throw new Error(result.message || '파일 업로드에 실패했습니다.') } -): Promise { - const items = await getGiftItems() - const budget = filters.budget || 200000 // 기본 예산 20만원 - - // 분석 결과에서 키워드 추출 - const keywords = analysis.keywords || [] - - // 예산과 키워드 기반으로 상품 필터링 - const filteredItems = filterGiftItems(items, budget, keywords) - - // 상위 3개 상품 반환 - return filteredItems.slice(0, 3) + + return result.data } -export async function analyzeChat(text: string): Promise { - // 실제 구현에서는 여기서 텍스트 분석을 수행합니다 - // 현재는 목업 데이터를 반환합니다 - return { - sentiment: { - positive: 65, - neutral: 25, - negative: 10 +// 대화 분석 API 호출 +export async function analyzeConversation(fileId: string): Promise { + const response = await fetch('http://210.125.91.91:5000/api/analyze', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' }, - keywords: [ - { name: "생일", value: 15 }, - { name: "선물", value: 12 }, - { name: "축하", value: 10 }, - { name: "파티", value: 8 }, - { name: "기념일", value: 6 } - ], - relationship: [ - { date: "1월", intimacy: 65 }, - { date: "2월", intimacy: 70 }, - { date: "3월", intimacy: 75 }, - { date: "4월", intimacy: 80 }, - { date: "5월", intimacy: 85 }, - { date: "6월", intimacy: 90 } - ], - messageStyle: { - tone: "친근한", - keywords: ["생일", "축하", "행복"], - example: "생일 축하해! 너의 특별한 날을 함께 축하하고 싶어. 앞으로도 항상 행복하길 바랄게!" - } + body: JSON.stringify({ fileId }) + }) + + if (!response.ok) { + throw new Error('대화 분석에 실패했습니다.') } + + const result = await response.json() as ApiResponse + if (!result.success) { + throw new Error(result.message || '분석 중 오류가 발생했습니다.') + } + + return result.data +} + +// 선물 추천 API 호출 +// export async function generateGiftRecommendations( +// analysis: AnalysisResult[], +// filters: { +// category?: string +// maxPrice?: number +// page?: number +// limit?: number +// } +// ): Promise { +// try { +// console.log('API 요청 데이터:', { +// analysis, +// filters +// }); +// +// const response = await fetch('http://210.125.91.91:5000/api/recommendations', { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json', +// 'Accept': 'application/json', +// }, +// mode: 'cors', +// body: JSON.stringify({ +// analysis, +// filters +// }) +// }); +// +// console.log('API 응답 상태:', response.status, response.statusText); +// +// if (!response.ok) { +// const errorText = await response.text(); +// console.error('API 에러 응답:', errorText); +// throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); +// } +// +// const result = await response.json() as ApiResponse; +// console.log('API 성공 응답:', result); +// +// if (!result.success) { +// throw new Error(result.message || '추천 중 오류가 발생했습니다.'); +// } +// +// return result.data; +// } catch (error) { +// console.error('API 호출 중 상세 오류:', error); +// if (error instanceof TypeError && error.message === 'Failed to fetch') { +// throw new Error('서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요.'); +// } +// if (error instanceof Error) { +// throw new Error(`선물 추천에 실패했습니다: ${error.message}`); +// } +// throw new Error('선물 추천에 실패했습니다.'); +// } +// } +export async function generateGiftRecommendations( + fileId: string +): Promise { + const res = await fetch("http://210.125.91.91:5000/api/recommendations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileId }) + }); + + const json = await res.json(); + if (!json.success) throw new Error(json.error ?? "추천 실패"); + return json.data; } diff --git a/lib/session.ts b/lib/session.ts index d8a8bc3..66cdb80 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -3,7 +3,7 @@ import { cookies } from "next/headers" import type { AnalysisResult } from "./analyze" // 세션에 분석 결과 저장 -export async function saveAnalysisToSession(analysisResult: AnalysisResult): Promise { +export async function saveAnalysisToSession(analysisResult: AnalysisResult[]): Promise { const cookieStore = await cookies() await cookieStore.set("analysis", JSON.stringify(analysisResult), { maxAge: 60 * 60, // 1시간 @@ -12,7 +12,7 @@ export async function saveAnalysisToSession(analysisResult: AnalysisResult): Pro } // 세션에서 분석 결과 가져오기 -export async function getAnalysisFromSession(): Promise { +export async function getAnalysisFromSession(): Promise { const cookieStore = await cookies() const analysisCookie = cookieStore.get("analysis") @@ -21,7 +21,7 @@ export async function getAnalysisFromSession(): Promise { } try { - return JSON.parse(analysisCookie.value) as AnalysisResult + return JSON.parse(analysisCookie.value) as AnalysisResult[] } catch (error) { console.error("Error parsing analysis from session:", error) return null diff --git a/lib/utils.ts b/lib/utils.ts index 977ec12..3b18bc6 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -6,9 +6,15 @@ export function cn(...inputs: ClassValue[]) { } export interface GiftItem { + id: string name: string - brand: string - price: number - productUrl: string + price: string imageUrl: string + description: string + category: string +} + +export interface RecommendationResult { + date: string + recommendations: GiftItem[] } diff --git a/next.config.mjs b/next.config.mjs index f5cbc38..559e40d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -9,6 +9,9 @@ const nextConfig = { images: { unoptimized: true, }, + env: { + NEXT_PUBLIC_API_URL: 'http://210.125.91.91:5000', + }, } export default nextConfig