Skip to content

Commit 0226cf4

Browse files
committed
feat(fe/analysis): 결과 조회
1 parent 8a00f34 commit 0226cf4

20 files changed

+597
-92
lines changed

front/next.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ const nextConfig = {
99
if (process.env.NEXT_PUBLIC_DEV_PROXY === 'true') {
1010
return [
1111
{
12-
source: "/:path*",
13-
destination: `${process.env.NEXT_PUBLIC_BACKEND_URL}/:path*`,
12+
source: "/api/:path*",
13+
destination: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/:path*`,
1414
},
1515
];
1616
}
Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,53 @@
1-
// 분석 결과 페이지
21
"use client"
32

4-
export default function AnalyzeResultPage() {
5-
return
6-
}
3+
import { useParams } from "next/navigation"
4+
import { useAnalysisResult } from "@/hooks/analysis/useAnalysisResult"
5+
import { AnalysisHeader } from "@/components/analysis/AnalysisHeader"
6+
import { AnalysisRadarCard } from "@/components/analysis/AnalysisRadarCard"
7+
import { AnalysisResultTabs } from "@/components/analysis/AnalysisResultTabs"
8+
import { AnalysisSummaryCard } from "@/components/analysis/AnalysisSummaryCard"
9+
import { RepositoryPublicSection } from "@/components/analysis/RepositoryPublicSection"
10+
11+
export default function ResultsPage() {
12+
const params = useParams()
13+
const repoId = Number(params.id)
14+
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || "{}") : null
15+
const userId = user?.id
16+
17+
const { history, result, loading, selectedId, setSelectedId } = useAnalysisResult(userId, repoId)
18+
19+
if (loading)
20+
return <div className="p-8 text-center text-muted-foreground">🕓 분석 결과를 불러오는 중...</div>
21+
22+
if (!history || !result)
23+
return <div className="p-8 text-center text-muted-foreground">⚠️ 분석 데이터를 찾을 수 없습니다.</div>
24+
25+
const radarData = [
26+
{ category: "README", score: (result.readmeScore / 30) * 100 },
27+
{ category: "TEST", score: (result.testScore / 30) * 100 },
28+
{ category: "COMMIT", score: (result.commitScore / 25) * 100 },
29+
{ category: "CI/CD", score: (result.cicdScore / 15) * 100 },
30+
]
31+
32+
return (
33+
<div className="flex justify-center">
34+
<div className="w-full max-w-5xl px-6 sm:px-8 lg:px-12 py-10">
35+
<AnalysisHeader history={history} selectedId={selectedId} onSelect={setSelectedId} />
36+
37+
<AnalysisSummaryCard totalScore={result.totalScore} summary={result.summary} />
38+
39+
<div className="grid gap-6 lg:grid-cols-2 items-stretch mb-8">
40+
<AnalysisRadarCard data={radarData} />
41+
<AnalysisResultTabs strengths={result.strengths} improvements={result.improvements} />
42+
</div>
43+
44+
{/* 🌐 공개 설정 및 커뮤니티 섹션 */}
45+
<RepositoryPublicSection
46+
userId={userId}
47+
repoId={repoId}
48+
initialPublic={history.repository.publicRepository}
49+
/>
50+
</div>
51+
</div>
52+
)
53+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client"
2+
3+
import { Clock } from "lucide-react"
4+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
5+
import type { HistoryResponseDto } from "@/types/analysis"
6+
7+
interface Props {
8+
history: HistoryResponseDto
9+
selectedId: number | null
10+
onSelect: (val: number) => void
11+
}
12+
13+
export function AnalysisHeader({ history, selectedId, onSelect }: Props) {
14+
return (
15+
<div className="mb-8">
16+
<h1 className="text-2xl font-bold mb-2">{history.repository.name}</h1>
17+
<p className="text-muted-foreground mb-4">{history.repository.description}</p>
18+
19+
<Select value={selectedId?.toString() || ""} onValueChange={(val) => onSelect(Number(val))}>
20+
<SelectTrigger className="w-[260px]">
21+
<SelectValue placeholder="분석 버전 선택" />
22+
</SelectTrigger>
23+
<SelectContent>
24+
{history.analysisVersions.map((ver) => (
25+
<SelectItem key={ver.analysisId} value={ver.analysisId.toString()}>
26+
<div className="flex items-center gap-2">
27+
<Clock className="h-3 w-3" />
28+
<span>{ver.versionLabel}</span>
29+
</div>
30+
</SelectItem>
31+
))}
32+
</SelectContent>
33+
</Select>
34+
</div>
35+
)
36+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Card } from "@/components/ui/card"
2+
import { RadarChartComponent } from "@/components/ui/radar-chart"
3+
4+
export function AnalysisRadarCard({ data }: { data: any[] }) {
5+
return (
6+
<Card className="p-6 flex items-center justify-center h-full">
7+
<RadarChartComponent data={data} />
8+
</Card>
9+
)
10+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Card } from "@/components/ui/card"
2+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
3+
import { CheckCircle2, AlertCircle } from "lucide-react"
4+
5+
export function AnalysisResultTabs({ strengths, improvements }: { strengths: string; improvements: string }) {
6+
const renderList = (text: string, Icon: any, color: string) =>
7+
text.split("\n").map((s, i) => {
8+
const clean = s.trim().replace(/^-+\s*/, "")
9+
return (
10+
<div key={i} className="flex items-start gap-2">
11+
<Icon className={`h-4 w-4 mt-[2px] ${color} shrink-0`} />
12+
<p className="text-muted-foreground text-sm leading-relaxed">{clean}</p>
13+
</div>
14+
)
15+
})
16+
17+
return (
18+
<Card className="p-6 h-full">
19+
<Tabs defaultValue="strengths" className="h-full">
20+
<TabsList className="grid grid-cols-2 w-full">
21+
<TabsTrigger value="strengths">강점</TabsTrigger>
22+
<TabsTrigger value="improvements">개선사항</TabsTrigger>
23+
</TabsList>
24+
25+
<TabsContent value="strengths" className="mt-6 h-full">
26+
<div className="space-y-3">{renderList(strengths, CheckCircle2, "text-green-500")}</div>
27+
</TabsContent>
28+
29+
<TabsContent value="improvements" className="mt-6 h-full">
30+
<div className="space-y-3">{renderList(improvements, AlertCircle, "text-yellow-500")}</div>
31+
</TabsContent>
32+
</Tabs>
33+
</Card>
34+
)
35+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client"
2+
3+
import { Card } from "@/components/ui/card"
4+
import { ScoreBadge } from "@/components/ui/score-badge"
5+
6+
interface AnalysisSummaryCardProps {
7+
totalScore: number
8+
summary: string
9+
}
10+
11+
export function AnalysisSummaryCard({ totalScore, summary }: AnalysisSummaryCardProps) {
12+
return (
13+
<Card className="mb-8 p-8">
14+
<div className="flex flex-col md:flex-row justify-between items-center mb-6">
15+
<h2 className="text-xl font-semibold">종합 점수</h2>
16+
<ScoreBadge score={totalScore} size="lg" showLabel />
17+
</div>
18+
<p className="text-muted-foreground leading-relaxed">{summary}</p>
19+
</Card>
20+
)
21+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"use client"
2+
3+
import { Card } from "@/components/ui/card"
4+
import { Button } from "@/components/ui/Button"
5+
import { Switch } from "@/components/ui/switch"
6+
import { ShareButton } from "@/components/analysis/ShareButton"
7+
import { Globe, Lock, MessageSquare, Share2, ThumbsUp } from "lucide-react"
8+
import { useRepositoryPublic } from "@/hooks/analysis/useRepositoryPublic"
9+
import { useState } from "react"
10+
11+
interface Props {
12+
userId: number
13+
repoId: number
14+
initialPublic: boolean
15+
}
16+
17+
export function RepositoryPublicSection({ userId, repoId, initialPublic }: Props) {
18+
const { isPublic, togglePublic } = useRepositoryPublic(initialPublic, userId, repoId)
19+
const [liked, setLiked] = useState(false)
20+
const [likeCount, setLikeCount] = useState(42)
21+
22+
const handleLike = () => {
23+
setLiked((prev) => !prev)
24+
setLikeCount((prev) => (liked ? prev - 1 : prev + 1))
25+
}
26+
27+
return (
28+
<>
29+
{/* 🌐 공개 설정 */}
30+
<Card className="mb-8 p-6">
31+
<div className="flex items-center justify-between">
32+
<div className="flex items-center gap-3">
33+
{isPublic ? (
34+
<Globe className="h-5 w-5 text-green-500" />
35+
) : (
36+
<Lock className="h-5 w-5 text-muted-foreground" />
37+
)}
38+
<div>
39+
<h3 className="font-semibold">리포지토리 공개 설정</h3>
40+
<p className="text-sm text-muted-foreground">
41+
{isPublic
42+
? "이 리포지토리의 분석 결과가 커뮤니티에 공개됩니다."
43+
: "이 리포지토리의 분석 결과는 비공개 상태입니다."}
44+
</p>
45+
</div>
46+
</div>
47+
48+
<div className="flex items-center gap-2">
49+
<span className="text-sm text-muted-foreground">{isPublic ? "공개" : "비공개"}</span>
50+
<Switch checked={isPublic} onCheckedChange={togglePublic} />
51+
</div>
52+
</div>
53+
</Card>
54+
55+
{/* 💬 커뮤니티 섹션 */}
56+
{isPublic ? (
57+
<>
58+
<Card className="p-6 mb-8">
59+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
60+
<div>
61+
<h3 className="mb-1 font-semibold">커뮤니티 반응</h3>
62+
<p className="text-sm text-muted-foreground">다른 개발자들과 소통하세요.</p>
63+
</div>
64+
<div className="flex gap-2">
65+
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
66+
<MessageSquare className="h-4 w-4" />
67+
댓글 (n)
68+
</Button>
69+
<ShareButton />
70+
</div>
71+
</div>
72+
</Card>
73+
74+
{/* ⚠️ TODO: 댓글 컴포넌트 연결 위치 */}
75+
{/*
76+
<CommentSection
77+
repoId={repoId}
78+
userId={userId}
79+
/>
80+
*/}
81+
</>
82+
) : (
83+
<Card className="p-6 text-center text-muted-foreground">
84+
🔒 이 리포지토리는 현재 비공개 상태입니다.
85+
</Card>
86+
)}
87+
</>
88+
)
89+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Button } from "@/components/ui/Button"
2+
import { Share2 } from "lucide-react"
3+
import { useState } from "react"
4+
5+
export function ShareButton() {
6+
const [copied, setCopied] = useState(false)
7+
8+
const handleShare = async () => {
9+
try {
10+
await navigator.clipboard.writeText(window.location.href)
11+
setCopied(true)
12+
// ✅ 2초 뒤 “복사됨” 상태 초기화
13+
setTimeout(() => setCopied(false), 2000)
14+
} catch (err) {
15+
console.error("❌ URL 복사 실패:", err)
16+
alert("URL 복사 중 오류가 발생했습니다.")
17+
}
18+
}
19+
20+
return (
21+
<Button
22+
variant="outline"
23+
size="sm"
24+
onClick={handleShare}
25+
className={`gap-2 bg-transparent transition ${
26+
copied ? "border-green-500 text-green-500" : ""
27+
}`}
28+
>
29+
<Share2 className="h-4 w-4" />
30+
{copied ? "복사됨!" : "공유"}
31+
</Button>
32+
)
33+
}

front/src/components/ui/avatar.tsx

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)