Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/analysis/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default function AnalysisPage() {
<section className="flex flex-col items-center justify-center min-h-screen py-12 bg-gradient-to-b from-white to-pink-50">
<div className="mb-10 text-center">
<h1 className="text-4xl md:text-5xl font-extrabold mb-4 text-pink-500 drop-shadow-sm">카카오톡 대화 분석 결과</h1>
<p className="text-lg md:text-xl text-gray-600 font-medium max-w-xl mx-auto">대화 데이터를 기반으로 주요 키워드와 관계 친밀도를 분석해드립니다.</p>
<p className="text-lg md:text-xl text-gray-600 font-medium max-w-xl mx-auto">대화 데이터를 기반으로 주요 키워드와 관계 친밀도를 분석해드려요.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-7xl mb-8 px-4">
{/* 키워드 분석 */}
Expand Down
6 changes: 3 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AnalysisProvider } from "@/context/analysis-context"
const inter = Inter({ subsets: ["latin"] })

export const metadata = {
title: "선물 주니",
title: "ChatPick",
description: "카카오톡 대화 기반 맞춤형 선물 추천 서비스",
}

Expand All @@ -19,7 +19,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<div className="container mx-auto flex items-center justify-between py-4 px-4 md:px-0">
<Link href="/" className="flex items-center text-2xl font-extrabold text-primary gap-2">
<Gift className="text-pink-500 w-8 h-8" />
선물 주니
ChatPick
</Link>
<nav>
<ul className="flex gap-6 text-base font-semibold">
Expand All @@ -35,7 +35,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</main>
<footer className="bg-white/80 border-t border-gray-100 text-center py-6 text-gray-500 text-sm mt-12 shadow-inner">
<div className="container mx-auto">
© 2025 선물 주니. All rights reserved.
© 2025 ChatPick. All rights reserved.
</div>
</footer>
</body>
Expand Down
145 changes: 79 additions & 66 deletions app/recommendations/page.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,76 @@
"use client"
"use client";

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"
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 searchParams = useSearchParams()
const fileId = searchParams.get('fileId')
const { analysisResult } = useAnalysis()
const [selectedIndex, setSelectedIndex] = useState(0)
const [showGifts, setShowGifts] = useState(false)
const [recommendations, setRecommendations] = useState<RecommendationResult[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const searchParams = useSearchParams();
const fileId = searchParams.get("fileId");
const { analysisResult } = useAnalysis();
const [selectedIndex, setSelectedIndex] = useState(0);
const [recommendations, setRecommendations] = useState<RecommendationResult[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const fetchRecommendations = async () => {
if (!analysisResult || !Array.isArray(analysisResult) || analysisResult.length === 0) {
setError('분석 결과가 없습니다.')
setLoading(false)
return
setError("분석 결과가 없습니다.");
setLoading(false);
return;
}

try {
setLoading(true)
setError(null)
setLoading(true);
setError(null);

const results: RecommendationResult[] = await generateGiftRecommendations(analysisResult, {})
const results: RecommendationResult[] = await generateGiftRecommendations(
fileId as string,
analysisResult,
{}
);

if (!results || results.length === 0) {
throw new Error("추천할 수 있는 선물이 없습니다.")
throw new Error("추천할 수 있는 선물이 없습니다.");
}

// 날짜별로 정렬
const sortedResults = results.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
setRecommendations(sortedResults)
setSelectedIndex(sortedResults.length - 1)
setShowGifts(true)
const sortedResults = results.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
setRecommendations(sortedResults);
setSelectedIndex(sortedResults.length - 1);
} catch (err) {
console.error('Error fetching recommendations:', err)
setError(err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다.")
setShowGifts(false)
console.error("Error fetching recommendations:", err);
setError(err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다.");
} finally {
setLoading(false)
setLoading(false);
}
}
};

useEffect(() => {
if (analysisResult) {
fetchRecommendations()
fetchRecommendations();
}
}, [analysisResult])
}, [analysisResult]);

return (
<section className="flex flex-col items-center justify-center min-h-[70vh] py-12">
<div className="mb-10 text-center">
<h1 className="text-4xl md:text-5xl font-extrabold mb-4 text-pink-500 drop-shadow-sm">맞춤형 선물 추천</h1>
<p className="text-lg md:text-xl text-gray-600 font-medium max-w-xl mx-auto">분석 결과를 바탕으로 예산에 맞는 선물을 추천해드려요.</p>
<h1 className="text-4xl md:text-5xl font-extrabold mb-4 text-pink-500 drop-shadow-sm">
맞춤형 선물 추천
</h1>
<p className="text-lg md:text-xl text-gray-600 font-medium max-w-xl mx-auto">
분석 결과를 바탕으로 선물을 추천해드려요.
</p>
</div>
<div className="w-full max-w-5xl">
{error && (
Expand All @@ -83,49 +90,55 @@ export default function RecommendationsPage() {
{recommendations.length > 0 && (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
{showGifts && recommendations[selectedIndex]?.recommendations.slice(0, 3).map(gift => (
<Card key={gift.id} className="overflow-hidden rounded-2xl shadow-md border-0 bg-white/95 hover:shadow-xl transition-shadow duration-300">
<CardContent className="p-6 flex flex-col items-center">
<div className="aspect-square w-full mb-4">
<img
src={gift.imageUrl}
alt={gift.name}
className="w-full h-full object-cover rounded-xl"
/>
</div>
<h3 className="text-lg font-semibold mb-2 text-gray-900 text-center">{gift.name}</h3>
<p className="text-gray-500 text-sm mb-2 text-center">{gift.category}</p>
<p className="text-xl font-bold mb-4 text-pink-500 text-center">{gift.price}</p>
<p className="text-sm text-gray-600 mb-4 text-center">{gift.description}</p>
</CardContent>
</Card>
{recommendations[selectedIndex]?.recommendations.slice(0, 3).map((gift) => (
<a
key={gift.id}
href={gift.description}
target="_blank"
rel="noopener noreferrer"
className="block h-full"
>
<Card className="overflow-hidden rounded-2xl shadow-md border-0 bg-white/95 hover:shadow-xl transition-shadow duration-300 cursor-pointer h-full">
<CardContent className="p-6 flex flex-col items-center h-full">
<div className="aspect-square w-full mb-4">
<img
src={gift.imageUrl}
alt={gift.name}
className="w-full h-full object-cover rounded-xl"
/>
</div>
<h3 className="text-lg font-semibold mb-2 text-gray-900 text-center line-clamp-2">
{gift.name}
</h3>
<p className="text-gray-500 text-sm mb-2 text-center">
{gift.category}
</p>
<p className="text-xl font-bold mb-4 text-pink-500 text-center">
{gift.price}
</p>
</CardContent>
</Card>
</a>
))}
</div>
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 w-full max-w-lg flex flex-col items-center z-50">
<div className="sticky bottom-10 left-1/2 -translate-x-1/2 w-full max-w-lg flex flex-col items-center z-50 bg-white/80 backdrop-blur-sm py-4 rounded-lg">
<Slider
min={0}
max={recommendations.length - 1}
step={1}
value={[selectedIndex]}
onValueChange={([v]) => { setSelectedIndex(v); setShowGifts(false); }}
onValueChange={([v]) => setSelectedIndex(v)}
className="w-full max-w-lg"
/>
<div className="text-lg font-bold text-pink-700 mt-2">
{recommendations[selectedIndex]?.date}
</div>
<Button
className="bg-pink-500 text-white rounded-full px-8 py-2 text-lg font-bold shadow-md hover:bg-pink-600 transition-all duration-300 hover:scale-105 mt-2"
onClick={() => setShowGifts(true)}
disabled={recommendations.length === 0}
>
추천받기
</Button>
</div>
</>
)}
</>
)}
</div>
</section>
)
);
}
54 changes: 15 additions & 39 deletions lib/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,66 +67,42 @@ export async function analyzeConversation(
return result.data;
}

// 선물 추천 API 호출
//선물 추천 API
export async function generateGiftRecommendations(
fileId: string, // ← 추가됨
analysis: AnalysisResult[],
filters: {
fileId: string,
_analysis: AnalysisResult[],
_filters: {
category?: string;
maxPrice?: number;
page?: number;
limit?: number;
}
): Promise<RecommendationResult[]> {
try {
console.log("API 요청 데이터:", {
fileId, // ← 추가됨
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({ fileId }), // ✅ 핵심
});

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}`
);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}

const result = (await response.json()) as ApiResponse<
RecommendationResult[]
>;
console.log("API 성공 응답:", result);

const result = (await response.json()) as ApiResponse<RecommendationResult[]>;
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(
"서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요."
);
throw new Error("서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요.");
}
if (error instanceof Error) {
throw new Error(`선물 추천에 실패했습니다: ${error.message}`);
Expand Down
2 changes: 2 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface GiftItem {
imageUrl: string
description: string
category: string
url: string
link: string
}

export interface RecommendationResult {
Expand Down
Loading