Skip to content

Commit 3332eef

Browse files
authored
feat, fix(analysis): 백엔드 로직(검증/락 등) 개선, 프론트 UI 개선
feat, fix(analysis): 백엔드 로직(검증/락 등) 개선, 프론트 UI 개선
2 parents 5c8e91c + ae42719 commit 3332eef

File tree

30 files changed

+656
-324
lines changed

30 files changed

+656
-324
lines changed

backend/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ dependencies {
4343
testImplementation("org.mockito:mockito-junit-jupiter")
4444
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
4545
implementation("org.springframework.boot:spring-boot-starter-webflux")
46-
4746
implementation("org.springframework.boot:spring-boot-starter-validation")
4847
implementation("org.springframework.boot:spring-boot-starter-mail")
4948
implementation("org.springframework.boot:spring-boot-starter-data-redis")
50-
5149
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
5250
implementation("io.jsonwebtoken:jjwt-impl:0.13.0")
5351
implementation("io.jsonwebtoken:jjwt-jackson:0.13.0")
5452
implementation("org.springframework.boot:spring-boot-starter-security")
53+
implementation("org.springframework.retry:spring-retry")
54+
implementation("org.springframework.boot:spring-boot-starter-aop")
5555
}
5656

5757
tasks.withType<Test> {

backend/src/main/java/com/backend/BackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6+
import org.springframework.retry.annotation.EnableRetry;
67

78
@SpringBootApplication
89
@EnableJpaAuditing
10+
@EnableRetry
911
public class BackendApplication {
1012

1113
public static void main(String[] args) {

backend/src/main/java/com/backend/domain/analysis/controller/AnalysisController.java

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.backend.global.response.ApiResponse;
1717
import jakarta.servlet.http.HttpServletRequest;
1818
import lombok.RequiredArgsConstructor;
19+
import lombok.extern.slf4j.Slf4j;
1920
import org.springframework.http.ResponseEntity;
2021
import org.springframework.transaction.annotation.Transactional;
2122
import org.springframework.web.bind.annotation.*;
@@ -24,6 +25,7 @@
2425
import java.util.ArrayList;
2526
import java.util.List;
2627

28+
@Slf4j
2729
@RestController
2830
@RequiredArgsConstructor
2931
@RequestMapping("/api/analysis")
@@ -79,16 +81,13 @@ public ResponseEntity<ApiResponse<HistoryResponseDto>> getAnalysisByRepositories
7981
@PathVariable("repositoriesId") Long repoId,
8082
HttpServletRequest httpRequest
8183
){
82-
Long jwtUserId = jwtUtil.getUserId(httpRequest);
83-
if (!jwtUserId.equals(userId)) {
84-
throw new BusinessException(ErrorCode.FORBIDDEN);
85-
}
8684

8785
// 1. Repository 정보 조회
8886
Repositories repository = repositoryService.findById(repoId)
8987
.orElseThrow(() -> new BusinessException(ErrorCode.GITHUB_REPO_NOT_FOUND));
9088

91-
RepositoryResponse repositoryResponse = new RepositoryResponse(repository);
89+
// 권한 검증
90+
validateAccess(httpRequest, userId, repository);
9291

9392
// 2. 분석 결과 목록 조회 (최신순 정렬)
9493
List<AnalysisResult> analysisResults =
@@ -104,6 +103,7 @@ public ResponseEntity<ApiResponse<HistoryResponseDto>> getAnalysisByRepositories
104103
}
105104

106105
// 4. 응답 조합
106+
RepositoryResponse repositoryResponse = new RepositoryResponse(repository);
107107
HistoryResponseDto response = HistoryResponseDto.of(repositoryResponse, versions);
108108

109109
return ResponseEntity.ok(ApiResponse.success(response));
@@ -118,17 +118,15 @@ public ResponseEntity<ApiResponse<AnalysisResultResponseDto>> getAnalysisDetail(
118118
@PathVariable Long analysisId,
119119
HttpServletRequest httpRequest
120120
) {
121-
Long jwtUserId = jwtUtil.getUserId(httpRequest);
122-
if (!jwtUserId.equals(userId)) {
123-
throw new BusinessException(ErrorCode.FORBIDDEN);
124-
}
125-
126121
// 분석 결과 조회
127122
AnalysisResult analysisResult = analysisService.getAnalysisById(analysisId);
128123

129124
if (!analysisResult.getRepositories().getId().equals(repositoryId)) {
130125
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
131126
}
127+
128+
// 권한 검증
129+
validateAccess(httpRequest, userId, analysisResult.getRepositories());
132130

133131
AnalysisResultResponseDto response =
134132
new AnalysisResultResponseDto(analysisResult, analysisResult.getScore());
@@ -163,7 +161,7 @@ public ResponseEntity<ApiResponse<Void>> deleteAnalysisResult(
163161
throw new BusinessException(ErrorCode.FORBIDDEN);
164162
}
165163

166-
analysisService.deleteAnalysisResult(analysisId, userId);
164+
analysisService.deleteAnalysisResult(analysisId, repositoryId, userId);
167165
return ResponseEntity.ok(ApiResponse.success());
168166
}
169167

@@ -193,4 +191,41 @@ public SseEmitter stream(@PathVariable Long userId,
193191

194192
return analysisProgressService.connect(userId);
195193
}
194+
195+
/**
196+
* 리포지토리 접근 권한 검증
197+
* - 공개 리포지토리: 누구나 접근 가능 (비로그인 포함)
198+
* - 비공개 리포지토리: 소유자만 접근 가능
199+
*/
200+
201+
private void validateAccess(HttpServletRequest requeset, Long pathUserId, Repositories repository) {
202+
Long jwtUserId = jwtUtil.getUserId(requeset);
203+
204+
// 1. 공개 리포지토리일 경우
205+
if(repository.isPublicRepository()) {
206+
if (jwtUserId == null) {
207+
log.warn("비로그인 사용자의 공개 리포지토리 접근 시도: repoId={}", repository.getId());
208+
}
209+
210+
if (!jwtUserId.equals(pathUserId)) {
211+
log.warn("다른 사용자의 공개 리포지토리 접근 시도: jwtUserId={}, pathUserId={}, repoId={}",
212+
jwtUserId, pathUserId, repository.getId());
213+
}
214+
return;
215+
}
216+
217+
// 2. 비공개 리포지토리일 경우
218+
// 2-1. 비로그인
219+
if (jwtUserId == null) {
220+
log.warn("비로그인 사용자의 비공개 리포지토리 접근 시도: repoId={}", repository.getId());
221+
throw new BusinessException(ErrorCode.FORBIDDEN);
222+
}
223+
224+
// 2-2. 다른 사용자
225+
if (!jwtUserId.equals(pathUserId)) {
226+
log.warn("권한 없는 접근 시도: jwtUserId={}, pathUserId={}, repoId={}",
227+
jwtUserId, pathUserId, repository.getId());
228+
throw new BusinessException(ErrorCode.FORBIDDEN);
229+
}
230+
}
196231
}

backend/src/main/java/com/backend/domain/analysis/service/AnalysisService.java

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.springframework.transaction.annotation.Transactional;
1616

1717
import java.util.List;
18+
import java.util.concurrent.ConcurrentHashMap;
1819

1920
@Slf4j
2021
@Service
@@ -26,6 +27,9 @@ public class AnalysisService {
2627
private final RepositoryJpaRepository repositoryJpaRepository;
2728
private final SseProgressNotifier sseProgressNotifier;
2829

30+
// 인메모리 락
31+
private final ConcurrentHashMap<String, Boolean> processingRepositories = new ConcurrentHashMap<>();
32+
2933
/* Analysis 분석 프로세스 오케스트레이션 담당
3034
* 1. GitHub URL 파싱 및 검증
3135
* 2. Repository 도메인을 통한 데이터 수집
@@ -38,35 +42,49 @@ public Long analyze(String githubUrl, Long userId) {
3842
String owner = repoInfo[0];
3943
String repo = repoInfo[1];
4044

41-
sseProgressNotifier.notify(userId, "status", "분석 시작");
45+
String cacheKey = userId + ":" + githubUrl;
4246

43-
// Repository 데이터 수집
44-
RepositoryData repositoryData;
47+
// 현재 처리 중인지 체크
48+
if (processingRepositories.putIfAbsent(cacheKey, true) != null) {
49+
log.warn("⚠중복 분석 요청 차단: userId={}, url={}", userId, githubUrl);
50+
throw new BusinessException(ErrorCode.ANALYSIS_IN_PROGRESS);
51+
}
4552

4653
try {
47-
repositoryData = repositoryService.fetchAndSaveRepository(owner, repo, userId);
48-
log.info("🫠 Repository Data 수집 완료: {}", repositoryData);
49-
} catch (BusinessException e) {
50-
log.error("Repository 데이터 수집 실패: {}/{}", owner, repo, e);
51-
throw handleRepositoryFetchError(e, owner, repo);
52-
}
54+
sseProgressNotifier.notify(userId, "status", "분석 시작");
5355

54-
Repositories savedRepository = repositoryJpaRepository
55-
.findByHtmlUrl(repositoryData.getRepositoryUrl())
56-
.orElseThrow(() -> new BusinessException(ErrorCode.GITHUB_REPO_NOT_FOUND));
56+
// Repository 데이터 수집
57+
RepositoryData repositoryData;
5758

58-
Long repositoryId = savedRepository.getId();
59+
try {
60+
repositoryData = repositoryService.fetchAndSaveRepository(owner, repo, userId);
61+
log.info("🫠 Repository Data 수집 완료: {}", repositoryData);
62+
} catch (BusinessException e) {
63+
log.error("Repository 데이터 수집 실패: {}/{}", owner, repo, e);
64+
throw handleRepositoryFetchError(e, owner, repo);
65+
}
5966

60-
// OpenAI API 데이터 분석 및 저장
61-
try {
62-
evaluationService.evaluateAndSave(repositoryData);
63-
} catch (BusinessException e) {
64-
sseProgressNotifier.notify(userId, "error", "AI 평가 실패: " + e.getMessage());
65-
throw e;
66-
}
67+
Repositories savedRepository = repositoryJpaRepository
68+
.findByHtmlUrl(repositoryData.getRepositoryUrl())
69+
.orElseThrow(() -> new BusinessException(ErrorCode.GITHUB_REPO_NOT_FOUND));
70+
71+
Long repositoryId = savedRepository.getId();
6772

68-
sseProgressNotifier.notify(userId, "complete", "최종 리포트 생성");
69-
return repositoryId;
73+
// OpenAI API 데이터 분석 및 저장
74+
try {
75+
evaluationService.evaluateAndSave(repositoryData);
76+
} catch (BusinessException e) {
77+
sseProgressNotifier.notify(userId, "error", "AI 평가 실패: " + e.getMessage());
78+
throw e;
79+
}
80+
81+
sseProgressNotifier.notify(userId, "complete", "최종 리포트 생성");
82+
return repositoryId;
83+
} finally {
84+
// 락 해제
85+
processingRepositories.remove(cacheKey);
86+
log.info("분석 락 해제: cacheKey={}", cacheKey);
87+
}
7088
}
7189

7290
private String[] parseGitHubUrl(String githubUrl) {
@@ -135,15 +153,15 @@ public void delete(Long repositoriesId, Long userId){
135153

136154
// 특정 분석 결과 삭제
137155
@Transactional
138-
public void deleteAnalysisResult(Long analysisResultId, Long memberId) {
156+
public void deleteAnalysisResult(Long analysisResultId, Long repositoryId, Long memberId) {
139157
if (analysisResultId == null) {
140158
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
141159
}
142160

143161
AnalysisResult analysisResult = analysisResultRepository.findById(analysisResultId)
144162
.orElseThrow(() -> new BusinessException(ErrorCode.ANALYSIS_NOT_FOUND));
145163

146-
if (!analysisResult.getRepositories().getId().equals(analysisResultId)) {
164+
if (!analysisResult.getRepositories().getId().equals(repositoryId)) {
147165
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
148166
}
149167

backend/src/main/java/com/backend/domain/community/dto/response/CommunityResponseDto.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ public record CommunityResponseDto(
1818
List<String> language,
1919
int totalScore,
2020
LocalDateTime createDate,
21-
boolean vewingStatus
21+
boolean vewingStatus,
22+
String htmlUrl
2223
) {
2324
public CommunityResponseDto(Repositories repositories, AnalysisResult analysis, Score score) {
2425
this(
@@ -33,7 +34,8 @@ public CommunityResponseDto(Repositories repositories, AnalysisResult analysis,
3334
.collect(Collectors.toList()),
3435
score.getTotalScore(),
3536
analysis.getCreateDate(),
36-
repositories.isPublicRepository()
37+
repositories.isPublicRepository(),
38+
repositories.getHtmlUrl()
3739
);
3840
}
3941
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ public record RepositoryResponse(
1818
boolean publicRepository,
1919
String mainBranch,
2020
List<String> languages,
21-
LocalDateTime createDate
21+
LocalDateTime createDate,
22+
Long ownerId
2223
) {
2324
public RepositoryResponse(Repositories repositories) {
2425
this(
@@ -31,7 +32,8 @@ public RepositoryResponse(Repositories repositories) {
3132
repositories.getLanguages().stream()
3233
.map(lang -> lang.getLanguage().name())
3334
.collect(Collectors.toList()),
34-
repositories.getCreateDate()
35+
repositories.getCreateDate(),
36+
repositories.getUser().getId()
3537
);
3638
}
3739
}

0 commit comments

Comments
 (0)