Skip to content

Commit 9bac094

Browse files
authored
feat(analysis): jwt 만료 시간 수정, 프론트엔드 히스토리 부분 UI 개선 및 정렬 추가
feat(analysis): jwt 만료 시간 수정, 프론트엔드 히스토리 부분 UI 개선 및 정렬 추가
2 parents 4cfd68a + 83b246e commit 9bac094

File tree

14 files changed

+287
-34
lines changed

14 files changed

+287
-34
lines changed

backend/src/main/java/com/backend/domain/analysis/dto/response/AnalysisResultResponseDto.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.backend.domain.analysis.entity.AnalysisResult;
44
import com.backend.domain.analysis.entity.Score;
55

6+
import java.time.LocalDateTime;
7+
68
/**
79
* 특정 분석 결과의 상세 정보 응답 DTO
810
* 분석 점수, 피드백 등을 포함
@@ -15,7 +17,8 @@ public record AnalysisResultResponseDto(
1517
int cicdScore,
1618
String summary,
1719
String strengths,
18-
String improvements
20+
String improvements,
21+
LocalDateTime createDate
1922
) {
2023
public AnalysisResultResponseDto(AnalysisResult analysisResult, Score score){
2124
this(
@@ -26,7 +29,8 @@ public AnalysisResultResponseDto(AnalysisResult analysisResult, Score score){
2629
score.getCicdScore(),
2730
analysisResult.getSummary(),
2831
analysisResult.getStrengths(),
29-
analysisResult.getImprovements()
32+
analysisResult.getImprovements(),
33+
analysisResult.getCreateDate()
3034
);
3135
}
3236
}

backend/src/main/java/com/backend/domain/repository/dto/response/RepositoryResponse.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.backend.domain.repository.entity.Repositories;
44

5+
import java.time.LocalDateTime;
56
import java.util.List;
67
import java.util.stream.Collectors;
78

@@ -16,7 +17,8 @@ public record RepositoryResponse(
1617
String htmlUrl,
1718
boolean publicRepository,
1819
String mainBranch,
19-
List<String> languages
20+
List<String> languages,
21+
LocalDateTime createDate
2022
) {
2123
public RepositoryResponse(Repositories repositories) {
2224
this(
@@ -28,7 +30,8 @@ public RepositoryResponse(Repositories repositories) {
2830
repositories.getMainBranch(),
2931
repositories.getLanguages().stream()
3032
.map(lang -> lang.getLanguage().name())
31-
.collect(Collectors.toList())
33+
.collect(Collectors.toList()),
34+
repositories.getCreateDate()
3235
);
3336
}
3437
}

backend/src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ github:
4343

4444
jwt:
4545
secret: ${SECRET_KEY}
46-
access-token-expiration-in-milliseconds: 600000 # 600 * 1000
46+
access-token-expiration-in-milliseconds: 7200000 # 두 시간

front/src/app/community/[id]/page.tsx

Whitespace-only changes.

front/src/components/history/HistoryContent.tsx

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
"use client"
22

3+
import { useEffect } from "react"
34
import { useHistory } from "@/hooks/history/useHistory"
5+
import { HistoryStats } from "@/components/history/HistoryStatsProps"
46
import { Card } from "@/components/ui/card"
57
import { Badge } from "@/components/ui/badge"
68
import { Button } from "@/components/ui/Button"
9+
import { ScoreBadge } from "@/components/history/ScoreBadge"
10+
import { formatRelativeTimeKST } from "@/lib/utils/formatDate"
711
import { Github, ExternalLink, Trash2, Calendar } from "lucide-react"
812
import { useRouter } from "next/navigation"
913

@@ -13,25 +17,62 @@ interface HistoryContentProps {
1317
}
1418

1519
export default function HistoryContent({ memberId, name }: HistoryContentProps) {
16-
const { repositories, loading, error } = useHistory(memberId)
20+
const { repositories, loading, error, handleDelete, sortType, setSortType } = useHistory(memberId)
1721
const router = useRouter()
1822

1923
if (loading) return <p className="p-8 text-center">히스토리 불러오는 중...</p>
2024
if (error) return <p className="p-8 text-center text-red-500">{error}</p>
2125

2226
return (
23-
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
27+
<div className="max-w-3xl mx-auto p-6">
2428
<div className="mb-8">
2529
<h1 className="mb-2 text-3xl font-bold">분석 히스토리</h1>
2630
<p className="text-muted-foreground">시간에 따른 리포지토리 개선 사항을 추적하세요</p>
2731
</div>
2832

33+
{/* 통계 카드 컴포넌트 */}
34+
<HistoryStats repositories={repositories} />
35+
36+
{/* ✅ 정렬 버튼 */}
37+
<div className="flex justify-end gap-2 mb-4">
38+
<Button
39+
variant={sortType === "latest" ? "default" : "outline"}
40+
size="sm"
41+
onClick={() => setSortType("latest")}
42+
>
43+
최신순
44+
</Button>
45+
<Button
46+
variant={sortType === "score" ? "default" : "outline"}
47+
size="sm"
48+
onClick={() => setSortType("score")}
49+
>
50+
점수순
51+
</Button>
52+
</div>
53+
2954
<div className="w-full max-w-3xl space-y-4">
30-
{repositories.map((repo) => (
55+
{repositories.length === 0 ? (
56+
// ✅ 분석 결과 없음 안내 카드
57+
<Card className="p-10 text-center bg-muted/30 border-dashed border-2 border-muted-foreground/20 rounded-2xl shadow-sm hover:shadow-md transition-all">
58+
<p className="text-lg mb-6 text-muted-foreground">
59+
아직 분석 결과가 없습니다. 지금 바로{" "}
60+
<span className="font-semibold text-primary">새 분석</span>을 시작해 보세요!
61+
</p>
62+
<Button
63+
size="lg"
64+
onClick={() => router.push("/analysis")}
65+
className="px-8"
66+
>
67+
🚀 새 분석 시작하기
68+
</Button>
69+
</Card>
70+
) : (
71+
repositories.map((repo) => (
3172
<Card
3273
key={repo.id}
3374
className="p-6 transition-all hover:border-primary/50 cursor-pointer"
34-
onClick={() => router.push(`/history/${repo.id}`)}
75+
onClick={() => router.push(`/analysis/${repo.id}`)}
3576
>
3677
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
3778
{/* 왼쪽 정보 */}
@@ -76,25 +117,36 @@ export default function HistoryContent({ memberId, name }: HistoryContentProps)
76117

77118
<div className="flex items-center gap-2 text-sm text-muted-foreground">
78119
<Calendar className="h-4 w-4" />
79-
<span>브랜치: {repo.mainBranch}</span>
120+
<span>{formatRelativeTimeKST(repo.createDate)}</span>
80121
</div>
81122
</div>
82123

83124
<div className="flex items-center gap-6">
125+
{repo.latestScore !== undefined && repo.latestScore !== null ? (
126+
<div className="text-center">
127+
<div className="mb-1 text-sm text-muted-foreground">점수</div>
128+
<ScoreBadge score={repo.latestScore} size="sm" />
129+
</div>
130+
) : (
131+
<div className="text-sm text-muted-foreground">점수 없음</div>
132+
)}
84133
<Button
85134
variant="ghost"
86135
size="sm"
87136
onClick={(e) => {
88137
e.stopPropagation()
89-
console.log("삭제 요청:", repo.id)
138+
if (confirm("정말 이 리포지토리를 삭제하시겠습니까?")) {
139+
handleDelete(repo.id)
140+
}
90141
}}
91-
>
142+
>
92143
<Trash2 className="h-4 w-4 text-destructive" />
93144
</Button>
94145
</div>
95146
</div>
96147
</Card>
97-
))}
148+
))
149+
)}
98150
</div>
99151
</div>
100152
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use client"
2+
3+
import { Card } from "@/components/ui/card"
4+
5+
interface HistoryStatsProps {
6+
repositories: { latestScore?: number | null }[]
7+
}
8+
9+
/** ✅ 리포지토리 통계 카드 */
10+
export function HistoryStats({ repositories }: HistoryStatsProps) {
11+
const total = repositories.length
12+
const validScores = repositories
13+
.map((r) => r.latestScore ?? 0)
14+
.filter((s) => s > 0)
15+
16+
const average =
17+
validScores.length > 0
18+
? (validScores.reduce((a, b) => a + b, 0) / validScores.length).toFixed(1)
19+
: "0.0"
20+
21+
const max = validScores.length > 0 ? Math.max(...validScores) : 0
22+
23+
return (
24+
<div className="mb-8 grid gap-4 sm:grid-cols-3">
25+
<Card className="p-6">
26+
<div className="mb-2 text-sm text-muted-foreground">총 리포지토리 수</div>
27+
<div className="text-3xl font-bold">{total}</div>
28+
</Card>
29+
30+
<Card className="p-6">
31+
<div className="mb-2 text-sm text-muted-foreground">평균 점수</div>
32+
<div className="text-3xl font-bold text-score-good">{average}</div>
33+
</Card>
34+
35+
<Card className="p-6">
36+
<div className="mb-2 text-sm text-muted-foreground">최고 점수</div>
37+
<div className="text-3xl font-bold text-score-excellent">{max}</div>
38+
</Card>
39+
</div>
40+
)
41+
}

front/src/hooks/analysis/useAnalysisResult.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect, useState } from "react"
44
import { analysisApi } from "@/lib/api/analysis"
55
import type { HistoryResponseDto, AnalysisResultResponseDto } from "@/types/analysis"
6-
import { formatDate } from "@/lib/utils/formatDate"
6+
import { formatDateTimeKST } from "@/lib/utils/formatDate"
77

88
export function useAnalysisResult(userId?: number, repoId?: number) {
99
const [history, setHistory] = useState<HistoryResponseDto | null>(null)
@@ -23,7 +23,7 @@ export function useAnalysisResult(userId?: number, repoId?: number) {
2323

2424
const relabeled = sorted.map((ver, idx) => ({
2525
...ver,
26-
versionLabel: `v${sorted.length - idx} (${formatDate(ver.analysisDate)})`,
26+
versionLabel: `v${sorted.length - idx} (${formatDateTimeKST(ver.analysisDate)})`,
2727
}))
2828

2929
setHistory({ ...data, analysisVersions: relabeled })

front/src/hooks/auth/useAuth.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ export function useAuth() {
5252
}
5353
}, []);
5454

55+
// ✅ 로그인된 상태일 때 자동 로그아웃 타이머 (2시간 후)
56+
useEffect(() => {
57+
if (!token) return; // 로그인 안 되어 있으면 실행 안 함
58+
59+
console.log('⏰ 2시간 자동 로그아웃 타이머 시작');
60+
const logoutTimer = setTimeout(() => {
61+
console.warn('🔒 토큰 만료 — 자동 로그아웃 실행');
62+
localStorage.removeItem('accessToken');
63+
localStorage.removeItem('user');
64+
setToken(null);
65+
setUser(null);
66+
toast.push('세션이 만료되어 로그아웃되었습니다.');
67+
window.location.reload();
68+
}, 2 * 60 * 60 * 1000); // ✅ 2시간 (7200000ms)
69+
70+
return () => clearTimeout(logoutTimer);
71+
}, [token]); // token이 새로 설정될 때마다 타이머 재설정
72+
5573
const isAuthed = useMemo(() => !!token, [token]);
5674

5775
function loginWithToken(userData: GetUserResponse) {
Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,90 @@
11
"use client"
22

3-
import { useEffect, useState } from "react"
3+
import { useEffect, useState, useMemo } from "react"
44
import { fetchHistory } from "@/lib/api/history"
5-
import type { RepositoryResponse } from "@/types/history"
5+
import { analysisApi } from "@/lib/api/analysis"
6+
import type { RepositoryResponse as RepoBaseResponse } from "@/types/history"
7+
import type { HistoryResponseDto } from "@/types/analysis"
68

79
export function useHistory(memberId: number) {
8-
const [repositories, setRepositories] = useState<RepositoryResponse[]>([])
10+
const [repositories, setRepositories] = useState<RepoBaseResponse[]>([])
911
const [loading, setLoading] = useState(true)
1012
const [error, setError] = useState<string | null>(null)
13+
const [sortType, setSortType] = useState<"latest" | "score">("latest")
1114

1215
useEffect(() => {
13-
async function load() {
14-
try {
15-
const result = await fetchHistory(memberId)
16-
setRepositories(result)
17-
} catch (err) {
18-
setError((err as Error).message)
19-
} finally {
20-
setLoading(false)
21-
}
16+
console.log("🧾 repositories:", repositories.map(r => ({
17+
id: r.id,
18+
createDate: r.createDate,
19+
latestScore: r.latestScore
20+
})))
21+
}, [repositories])
22+
23+
24+
async function load() {
25+
try {
26+
setLoading(true)
27+
const baseRepos = await fetchHistory(memberId)
28+
29+
const enrichedRepos: RepoBaseResponse[] = await Promise.all(
30+
baseRepos.map(async (repo): Promise<RepoBaseResponse> => {
31+
try {
32+
const historyData: HistoryResponseDto = await analysisApi.getRepositoryHistory(memberId, repo.id)
33+
const versions = historyData.analysisVersions
34+
const latest = versions.length > 0 ? versions[0] : null
35+
36+
return {
37+
...repo,
38+
latestScore: latest?.totalScore ?? null,
39+
latestAnalysisDate: latest?.analysisDate ?? null,
40+
}
41+
} catch (err) {
42+
console.error(`❌ 점수 불러오기 실패 (repoId: ${repo.id})`, err)
43+
return repo
44+
}
45+
})
46+
)
47+
48+
setRepositories(enrichedRepos)
49+
} catch (err) {
50+
setError((err as Error).message)
51+
} finally {
52+
setLoading(false)
2253
}
54+
}
55+
56+
useEffect(() => {
2357
load()
2458
}, [memberId])
2559

26-
return { repositories, loading, error }
60+
const sortedRepositories = useMemo(() => {
61+
if (sortType === "score") {
62+
return repositories
63+
.slice()
64+
.sort((a, b) => (b.latestScore ?? 0) - (a.latestScore ?? 0))
65+
}
66+
67+
// ✅ microseconds 제거 + UTC 보정
68+
const parseDate = (d?: string) => {
69+
if (!d) return 0
70+
return Date.parse(d.split(".")[0] + "Z")
71+
}
72+
73+
return repositories
74+
.slice()
75+
.sort((a, b) => parseDate(b.createDate) - parseDate(a.createDate))
76+
}, [repositories, sortType])
77+
78+
79+
async function handleDelete(repoId: number) {
80+
try {
81+
await analysisApi.deleteRepository(memberId, repoId)
82+
setRepositories((prev) => prev.filter((repo) => repo.id !== repoId))
83+
} catch (err) {
84+
console.error("삭제 실패:", err)
85+
alert("삭제 중 오류가 발생했습니다.")
86+
}
87+
}
88+
89+
return { repositories: sortedRepositories, loading, error, handleDelete, sortType, setSortType }
2790
}

front/src/lib/api/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ export async function api<T = unknown>(
5252
const text = await res.text();
5353
const responseData = text ? JSON.parse(text) : null;
5454

55+
if (res.status === 401) {
56+
console.warn("🔐 토큰 만료 — 로그인 페이지로 이동합니다.");
57+
if (typeof window !== "undefined") {
58+
localStorage.removeItem("accessToken");
59+
localStorage.removeItem("user");
60+
window.location.href = "/login";
61+
}
62+
// 즉시 리턴해서 아래 로직 수행 안 함
63+
throw new Error("Unauthorized");
64+
}
65+
5566
if (!res.ok) {
5667
// 🔥 에러 응답 데이터를 함께 전달
5768
const errorResponse = responseData as ErrorResponse;

0 commit comments

Comments
 (0)