Skip to content

Commit 26e69e3

Browse files
authored
fix, feat(repository): README 없을 경우 에러 반환 수정, 분석 실패 시 프론트 수정
fix, feat(repository): README 없을 경우 에러 반환 수정, 분석 실패 시 프론트 수정
2 parents d0b53d8 + 4bd1ea4 commit 26e69e3

File tree

8 files changed

+203
-77
lines changed

8 files changed

+203
-77
lines changed

backend/src/main/java/com/backend/domain/repository/service/RepositoryService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ public RepositoryData fetchCompleteRepositoryData(String owner, String repo, Lon
8484

8585
// 3. README 데이터 수집 및 매핑
8686
sseProgressNotifier.notify(userId, "status", "문서화 품질 분석");
87-
String readme = gitHubDataFetcher.fetchReadmeContent(owner, repo);
87+
String readme = gitHubDataFetcher.fetchReadmeContent(owner, repo).orElse("");
8888
readmeInfoMapper.mapReadmeInfo(data, readme);
8989

9090
// 4. 보안 관리 데이터 수집 및 매핑
9191
sseProgressNotifier.notify(userId, "status", "보안 구성 분석");
92-
TreeResponse tree = gitHubDataFetcher.fetchRepositoryTreeInfo(owner, repo, repoInfo.defaultBranch());
92+
TreeResponse tree = gitHubDataFetcher.fetchRepositoryTreeInfo(owner, repo, repoInfo.defaultBranch()).orElse(null);
9393
securityInfoMapper.mapSecurityInfo(data, tree);
9494

9595
// 5. 테스트 데이터 수집 및 매핑
@@ -111,6 +111,10 @@ public RepositoryData fetchCompleteRepositoryData(String owner, String repo, Lon
111111
saveOrUpdateRepository(repoInfo, owner, repo, userId);
112112

113113
return data;
114+
} catch (BusinessException e) {
115+
sseProgressNotifier.notify(userId, "error", "❌ " + e.getErrorCode().getMessage());
116+
throw e;
117+
114118
} catch (Exception e) {
115119
sseProgressNotifier.notify(userId, "error", "❌ Repository 데이터 수집 실패: " + e.getMessage());
116120
throw e;

backend/src/main/java/com/backend/domain/repository/service/fetcher/GitHubDataFetcher.java

Lines changed: 59 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
import lombok.RequiredArgsConstructor;
88
import lombok.extern.slf4j.Slf4j;
99
import org.springframework.retry.annotation.Backoff;
10-
import org.springframework.retry.annotation.Recover;
1110
import org.springframework.retry.annotation.Retryable;
1211
import org.springframework.stereotype.Component;
1312
import org.springframework.web.reactive.function.client.WebClientRequestException;
1413
import org.springframework.web.reactive.function.client.WebClientResponseException;
1514

1615
import java.time.LocalDateTime;
1716
import java.time.format.DateTimeFormatter;
17+
import java.util.Collections;
1818
import java.util.List;
1919
import java.util.Map;
20+
import java.util.Optional;
2021
import java.util.stream.Collectors;
2122

2223
@Slf4j
@@ -30,8 +31,6 @@ public class GitHubDataFetcher {
3031
retryFor = {WebClientResponseException.ServiceUnavailable.class,
3132
WebClientResponseException.InternalServerError.class,
3233
WebClientRequestException.class}, // 네트워크 타임아웃
33-
noRetryFor = {WebClientResponseException.NotFound.class, // 404, 401 에러는 재시도 X
34-
WebClientResponseException.Unauthorized.class},
3534
maxAttempts = 2, // 최대 2회 시도 (원본 1회 + 재시도 1회)
3635
backoff = @Backoff(delay = 1000) // 재시도 전 1초 대기
3736
)
@@ -43,21 +42,25 @@ public RepoResponse fetchRepositoryInfo(String owner, String repoName) {
4342
retryFor = {WebClientResponseException.ServiceUnavailable.class,
4443
WebClientResponseException.InternalServerError.class,
4544
WebClientRequestException.class},
46-
noRetryFor = {WebClientResponseException.NotFound.class,
47-
WebClientResponseException.Unauthorized.class},
4845
maxAttempts = 2,
4946
backoff = @Backoff(delay = 1000)
5047
)
51-
public String fetchReadmeContent(String owner, String repoName) {
52-
return gitHubApiClient.getRaw("/repos/{owner}/{repo}/readme", owner, repoName);
48+
public Optional<String> fetchReadmeContent(String owner, String repoName) {
49+
try {
50+
String content = gitHubApiClient.getRaw("/repos/{owner}/{repo}/readme", owner, repoName);
51+
return Optional.ofNullable(content);
52+
} catch (BusinessException e) {
53+
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
54+
return Optional.empty();
55+
}
56+
throw e;
57+
}
5358
}
5459

5560
@Retryable(
5661
retryFor = {WebClientResponseException.ServiceUnavailable.class,
5762
WebClientResponseException.InternalServerError.class,
5863
WebClientRequestException.class},
59-
noRetryFor = {WebClientResponseException.NotFound.class,
60-
WebClientResponseException.Unauthorized.class},
6164
maxAttempts = 2,
6265
backoff = @Backoff(delay = 1000)
6366
)
@@ -71,62 +74,83 @@ public List<CommitResponse> fetchCommitInfo(String owner, String repoName, Strin
7174
retryFor = {WebClientResponseException.ServiceUnavailable.class,
7275
WebClientResponseException.InternalServerError.class,
7376
WebClientRequestException.class},
74-
noRetryFor = {WebClientResponseException.NotFound.class,
75-
WebClientResponseException.Unauthorized.class},
7677
maxAttempts = 2,
7778
backoff = @Backoff(delay = 1000)
7879
)
79-
public TreeResponse fetchRepositoryTreeInfo(String owner, String repoName, String defaultBranch) {
80-
return gitHubApiClient.get(
81-
"/repos/{owner}/{repo}/git/trees/{sha}?recursive=1", TreeResponse.class, owner, repoName, defaultBranch
82-
);
80+
public Optional<TreeResponse> fetchRepositoryTreeInfo(String owner, String repoName, String defaultBranch) {
81+
try {
82+
TreeResponse tree = gitHubApiClient.get(
83+
"/repos/{owner}/{repo}/git/trees/{sha}?recursive=1",
84+
TreeResponse.class, owner, repoName, defaultBranch
85+
);
86+
return Optional.ofNullable(tree);
87+
} catch (BusinessException e) {
88+
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
89+
return Optional.empty();
90+
}
91+
throw e;
92+
}
8393
}
8494

8595
@Retryable(
8696
retryFor = {WebClientResponseException.ServiceUnavailable.class,
8797
WebClientResponseException.InternalServerError.class,
8898
WebClientRequestException.class},
89-
noRetryFor = {WebClientResponseException.NotFound.class,
90-
WebClientResponseException.Unauthorized.class},
9199
maxAttempts = 2,
92100
backoff = @Backoff(delay = 1000)
93101
)
94102
public List<IssueResponse> fetchIssueInfo(String owner, String repoName) {
95-
List<IssueResponse> allIssues = gitHubApiClient.getList(
96-
"/repos/{owner}/{repo}/issues?state=all&per_page=100", IssueResponse.class, owner, repoName);
97-
98-
LocalDateTime sixMonthsAgo = getSixMonthsAgo();
99-
return allIssues.stream()
100-
.filter(IssueResponse::isPureIssue)
101-
.filter(issue -> parseGitHubDate(issue.created_at()).isAfter(sixMonthsAgo))
102-
.collect(Collectors.toList());
103+
try {
104+
List<IssueResponse> allIssues = gitHubApiClient.getList(
105+
"/repos/{owner}/{repo}/issues?state=all&per_page=100",
106+
IssueResponse.class, owner, repoName
107+
);
108+
109+
LocalDateTime sixMonthsAgo = getSixMonthsAgo();
110+
return allIssues.stream()
111+
.filter(IssueResponse::isPureIssue)
112+
.filter(issue -> parseGitHubDate(issue.created_at()).isAfter(sixMonthsAgo))
113+
.collect(Collectors.toList());
114+
115+
} catch (BusinessException e) {
116+
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
117+
return Collections.emptyList();
118+
}
119+
throw e;
120+
}
103121
}
104122

105123
@Retryable(
106124
retryFor = {WebClientResponseException.ServiceUnavailable.class,
107125
WebClientResponseException.InternalServerError.class,
108126
WebClientRequestException.class},
109-
noRetryFor = {WebClientResponseException.NotFound.class,
110-
WebClientResponseException.Unauthorized.class},
111127
maxAttempts = 2,
112128
backoff = @Backoff(delay = 1000)
113129
)
114130
public List<PullRequestResponse> fetchPullRequestInfo(String owner, String repoName) {
115-
List<PullRequestResponse> allPullRequests = gitHubApiClient.getList(
116-
"/repos/{owner}/{repo}/pulls?state=all&per_page=100", PullRequestResponse.class, owner, repoName);
131+
try {
132+
List<PullRequestResponse> allPullRequests = gitHubApiClient.getList(
133+
"/repos/{owner}/{repo}/pulls?state=all&per_page=100",
134+
PullRequestResponse.class, owner, repoName
135+
);
117136

118-
LocalDateTime sixMonthsAgo = getSixMonthsAgo();
119-
return allPullRequests.stream()
120-
.filter(pr -> parseGitHubDate(pr.created_at()).isAfter(sixMonthsAgo))
121-
.collect(Collectors.toList());
137+
LocalDateTime sixMonthsAgo = getSixMonthsAgo();
138+
return allPullRequests.stream()
139+
.filter(pr -> parseGitHubDate(pr.created_at()).isAfter(sixMonthsAgo))
140+
.collect(Collectors.toList());
141+
142+
} catch (BusinessException e) {
143+
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
144+
return Collections.emptyList();
145+
}
146+
throw e;
147+
}
122148
}
123149

124150
@Retryable(
125151
retryFor = {WebClientResponseException.ServiceUnavailable.class,
126152
WebClientResponseException.InternalServerError.class,
127153
WebClientRequestException.class},
128-
noRetryFor = {WebClientResponseException.NotFound.class,
129-
WebClientResponseException.Unauthorized.class},
130154
maxAttempts = 2,
131155
backoff = @Backoff(delay = 1000)
132156
)
@@ -141,10 +165,4 @@ private LocalDateTime getSixMonthsAgo() {
141165
private LocalDateTime parseGitHubDate(String dateString) {
142166
return LocalDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
143167
}
144-
145-
@Recover // 재시도 실패 시 호출되는 메서드
146-
public RepoResponse recover(WebClientResponseException e, String owner, String repoName) {
147-
log.error("GitHub API 재시도 실패: {}/{}", owner, repoName, e);
148-
throw new BusinessException(ErrorCode.GITHUB_API_FAILED);
149-
}
150168
}

backend/src/main/java/com/backend/domain/repository/service/mapper/IssueInfoMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ public void mapIssueInfo(RepositoryData data, List<IssueResponse> response) {
3737
}
3838

3939
private void setEmptyIssue(RepositoryData data) {
40-
data.setPullRequestCountLast6Months(0);
41-
data.setMergedPullRequestCountLast6Months(0);
42-
data.setRecentPullRequests(Collections.emptyList());
40+
data.setIssueCountLast6Months(0);
41+
data.setClosedIssueCountLast6Months(0);
42+
data.setRecentIssues(Collections.emptyList());
4343
}
4444

4545
private RepositoryData.IssueInfo convertToIssueInfo(IssueResponse issue) {

backend/src/main/java/com/backend/domain/repository/service/mapper/PullRequestInfoMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ public void mapPullRequestInfo(RepositoryData data, List<PullRequestResponse> re
3737
}
3838

3939
private void setEmptyPullRequest(RepositoryData data) {
40-
data.setIssueCountLast6Months(0);
41-
data.setClosedIssueCountLast6Months(0);
42-
data.setRecentIssues(Collections.emptyList());
40+
data.setPullRequestCountLast6Months(0);
41+
data.setMergedPullRequestCountLast6Months(0);
42+
data.setRecentPullRequests(Collections.emptyList());
4343
}
4444

4545
private RepositoryData.PullRequestInfo convertToPullRequestInfo(PullRequestResponse pr) {

backend/src/main/java/com/backend/domain/user/service/EmailService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.backend.domain.user.service;
22

33
import com.backend.domain.user.repository.UserRepository;
4-
54
import com.backend.domain.user.util.RedisUtil;
65
import com.backend.global.exception.BusinessException;
76
import com.backend.global.exception.ErrorCode;

backend/src/main/java/com/backend/global/github/GitHubApiClient.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import lombok.RequiredArgsConstructor;
66
import lombok.extern.slf4j.Slf4j;
77
import org.springframework.http.HttpHeaders;
8+
import org.springframework.http.HttpStatus;
89
import org.springframework.stereotype.Component;
910
import org.springframework.web.reactive.function.client.WebClient;
1011
import org.springframework.web.reactive.function.client.WebClientResponseException;
@@ -93,6 +94,30 @@ private <T> Mono<T> handleWebClientError(WebClientResponseException ex) {
9394
log.error("GitHub API 호출 실패: {}", ex.getMessage());
9495

9596
if (ex.getStatusCode().is4xxClientError()) {
97+
HttpStatus status = (HttpStatus) ex.getStatusCode();
98+
99+
if (status == HttpStatus.BAD_REQUEST) {
100+
return Mono.error(new BusinessException(ErrorCode.GITHUB_API_FAILED));
101+
}
102+
if (status == HttpStatus.UNAUTHORIZED) {
103+
return Mono.error(new BusinessException(ErrorCode.GITHUB_INVALID_TOKEN));
104+
}
105+
if (status == HttpStatus.FORBIDDEN) {
106+
return Mono.error(new BusinessException(ErrorCode.FORBIDDEN));
107+
}
108+
if (status == HttpStatus.NOT_FOUND) {
109+
return Mono.error(new BusinessException(ErrorCode.GITHUB_REPO_NOT_FOUND));
110+
}
111+
if (status == HttpStatus.GONE) {
112+
return Mono.error(new BusinessException(ErrorCode.GITHUB_API_FAILED));
113+
}
114+
if (status == HttpStatus.UNPROCESSABLE_ENTITY) {
115+
return Mono.error(new BusinessException(ErrorCode.GITHUB_API_FAILED));
116+
}
117+
if (status == HttpStatus.TOO_MANY_REQUESTS) {
118+
return Mono.error(new BusinessException(ErrorCode.GITHUB_RATE_LIMIT_EXCEEDED));
119+
}
120+
96121
return Mono.error(new BusinessException(ErrorCode.GITHUB_REPO_NOT_FOUND));
97122
}
98123
if (ex.getStatusCode().is5xxServerError()) {

front/src/app/(dashboard)/analysis/page.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"use client"
44

55
import type React from "react"
6-
import { useState, useEffect } from "react"
6+
import { useState, useEffect, useMemo } from "react"
77
import { useRouter } from "next/navigation"
88
import { Button } from "@/components/ui/Button"
99
import { Input } from "@/components/ui/input"
@@ -14,10 +14,14 @@ import { isValidGitHubUrl } from "@/lib/utils/validation"
1414
import { useAnalysis } from "@/hooks/analysis/useAnalysis"
1515
import { useRequireAuth } from "@/hooks/auth/useRequireAuth"
1616

17+
type AnalysisErrorKind = "repo" | "auth" | "rate" | "duplicate" | "server" | "network";
18+
1719
export default function AnalyzePage() {
1820
const [repoUrl, setRepoUrl] = useState("")
1921
const [isValidUrl, setIsValidUrl] = useState(true)
20-
const [isSubmitting, setIsSubmitting] = useState(false)
22+
const [isSubmitting, setIsSubmitting] = useState(false)
23+
const [analysisErrorMessage, setAnalysisErrorMessage] = useState<string | null>(null)
24+
const [analysisErrorType, setAnalysisErrorType] = useState<AnalysisErrorKind | null>(null)
2125
const router = useRouter()
2226
const { error } = useAnalysis()
2327
const { user } = useRequireAuth()
@@ -34,6 +38,37 @@ export default function AnalyzePage() {
3438
return () => document.removeEventListener('visibilitychange', handleVisibilityChange)
3539
}, [])
3640

41+
useEffect(() => {
42+
const stored = sessionStorage.getItem("analysisError");
43+
if (!stored) return;
44+
45+
try {
46+
const parsed = JSON.parse(stored) as { type?: AnalysisErrorKind; message?: string };
47+
setAnalysisErrorType(parsed.type ?? "server");
48+
setAnalysisErrorMessage(parsed.message ?? "분석 처리 중 문제가 발생했어요.");
49+
} catch {
50+
setAnalysisErrorType("server");
51+
setAnalysisErrorMessage(stored);
52+
} finally {
53+
sessionStorage.removeItem("analysisError");
54+
}
55+
}, []);
56+
57+
const analysisErrorClass = useMemo(() => {
58+
switch (analysisErrorType) {
59+
case "auth":
60+
return "text-amber-600";
61+
case "rate":
62+
return "text-orange-600";
63+
case "duplicate":
64+
return "text-blue-600";
65+
case "repo":
66+
case "server":
67+
case "network":
68+
default:
69+
return "text-destructive/80";
70+
}
71+
}, [analysisErrorType]);
3772

3873
if (!user) return null
3974

@@ -94,11 +129,16 @@ export default function AnalyzePage() {
94129
required
95130
/>
96131
</div>
132+
{analysisErrorMessage && (
133+
<p className={`text-sm font-medium ${analysisErrorClass}`}>
134+
{analysisErrorMessage}
135+
</p>
136+
)}
97137
{!isValidUrl && (
98-
<p className="text-sm text-destructive">올바른 GitHub 리포지토리 URL을 입력하세요</p>
138+
<p className="text-sm text-destructive/80">올바른 GitHub 리포지토리 URL을 입력해 주세요.</p>
99139
)}
100140
{error && (
101-
<p className="text-sm text-destructive">{error.message}</p>
141+
<p className="text-sm text-destructive/80">{error.message}</p>
102142
)}
103143
</div>
104144

0 commit comments

Comments
 (0)