Skip to content

Commit fcec8cd

Browse files
committed
2 parents 68266d5 + c9be294 commit fcec8cd

File tree

20 files changed

+319
-193
lines changed

20 files changed

+319
-193
lines changed

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

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import java.util.ArrayList;
2626
import java.util.List;
27+
import java.util.Objects;
2728

2829
@Slf4j
2930
@RestController
@@ -53,19 +54,18 @@ public ResponseEntity<ApiResponse<AnalysisStartResponse>> analyzeRepository(
5354
}
5455

5556
// GET: 사용자의 모든 Repository 목록 조회
56-
@GetMapping("/{userId}/repositories")
57+
@GetMapping("/repositories")
5758
@Transactional(readOnly = true)
5859
public ResponseEntity<ApiResponse<List<RepositoryResponse>>> getMemberHistory(
59-
@PathVariable Long userId,
6060
HttpServletRequest httpRequest
6161
){
6262
Long jwtUserId = jwtUtil.getUserId(httpRequest);
63-
if (!jwtUserId.equals(userId)) {
64-
throw new BusinessException(ErrorCode.FORBIDDEN);
63+
if (jwtUserId == null) {
64+
throw new BusinessException(ErrorCode.UNAUTHORIZED);
6565
}
6666

6767
List<RepositoryResponse> repositories = repositoryService
68-
.findRepositoryByUser(userId)
68+
.findRepositoryByUser(jwtUserId)
6969
.stream()
7070
.map(RepositoryResponse::new)
7171
.toList();
@@ -74,20 +74,18 @@ public ResponseEntity<ApiResponse<List<RepositoryResponse>>> getMemberHistory(
7474
}
7575

7676
// GET: 특정 Repository의 분석 히스토리 조회, 모든 분석 결과 조회
77-
@GetMapping("/{userId}/repositories/{repositoriesId}")
77+
@GetMapping("/repositories/{repositoriesId}")
7878
@Transactional(readOnly = true)
7979
public ResponseEntity<ApiResponse<HistoryResponseDto>> getAnalysisByRepositoriesId(
80-
@PathVariable("userId") Long userId,
8180
@PathVariable("repositoriesId") Long repoId,
8281
HttpServletRequest httpRequest
8382
){
84-
8583
// 1. Repository 정보 조회
8684
Repositories repository = repositoryService.findById(repoId)
8785
.orElseThrow(() -> new BusinessException(ErrorCode.GITHUB_REPO_NOT_FOUND));
8886

8987
// 권한 검증
90-
validateAccess(httpRequest, userId, repository);
88+
validateAccess(httpRequest, repository);
9189

9290
// 2. 분석 결과 목록 조회 (최신순 정렬)
9391
List<AnalysisResult> analysisResults =
@@ -110,10 +108,9 @@ public ResponseEntity<ApiResponse<HistoryResponseDto>> getAnalysisByRepositories
110108
}
111109

112110
// GET: 특정 분석 결과 상세 조회
113-
@GetMapping("/{userId}/repositories/{repositoryId}/results/{analysisId}")
111+
@GetMapping("/repositories/{repositoryId}/results/{analysisId}")
114112
@Transactional(readOnly = true)
115113
public ResponseEntity<ApiResponse<AnalysisResultResponseDto>> getAnalysisDetail(
116-
@PathVariable Long userId,
117114
@PathVariable Long repositoryId,
118115
@PathVariable Long analysisId,
119116
HttpServletRequest httpRequest
@@ -126,7 +123,7 @@ public ResponseEntity<ApiResponse<AnalysisResultResponseDto>> getAnalysisDetail(
126123
}
127124

128125
// 권한 검증
129-
validateAccess(httpRequest, userId, analysisResult.getRepositories());
126+
validateAccess(httpRequest, analysisResult.getRepositories());
130127

131128
AnalysisResultResponseDto response =
132129
new AnalysisResultResponseDto(analysisResult, analysisResult.getScore());
@@ -197,34 +194,28 @@ public SseEmitter stream(@PathVariable Long userId,
197194
* - 공개 리포지토리: 누구나 접근 가능 (비로그인 포함)
198195
* - 비공개 리포지토리: 소유자만 접근 가능
199196
*/
200-
201-
private void validateAccess(HttpServletRequest requeset, Long pathUserId, Repositories repository) {
197+
private void validateAccess(HttpServletRequest requeset, Repositories repository) {
202198
Long jwtUserId = jwtUtil.getUserId(requeset);
199+
Long ownerId = repository.getUser().getId();
203200

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-
}
201+
// 1. 공개 리포지토리는 누구나 접근 가능
202+
if (repository.isPublicRepository()) {
203+
log.info("공개 리포지토리 접근: repoId={}, jwtUserId={}",
204+
repository.getId(), jwtUserId);
214205
return;
215206
}
216207

217-
// 2. 비공개 리포지토리일 경우
218-
// 2-1. 비로그인
208+
// 2. 비공개 리포지토리는 로그인 필수
219209
if (jwtUserId == null) {
220-
log.warn("비로그인 사용자의 비공개 리포지토리 접근 시도: repoId={}", repository.getId());
221-
throw new BusinessException(ErrorCode.FORBIDDEN);
210+
log.warn("비로그인 사용자의 비공개 리포지토리 접근 시도: repoId={}",
211+
repository.getId());
212+
throw new BusinessException(ErrorCode.UNAUTHORIZED);
222213
}
223214

224-
// 2-2. 다른 사용자
225-
if (!jwtUserId.equals(pathUserId)) {
226-
log.warn("권한 없는 접근 시도: jwtUserId={}, pathUserId={}, repoId={}",
227-
jwtUserId, pathUserId, repository.getId());
215+
// 3. 비공개 리포지토리는 소유자만 접근 가능
216+
if (!Objects.equals(jwtUserId, ownerId)) {
217+
log.warn("권한 없는 사용자의 비공개 리포지토리 접근: jwtUserId={}, ownerId={}, repoId={}",
218+
jwtUserId, ownerId, repository.getId());
228219
throw new BusinessException(ErrorCode.FORBIDDEN);
229220
}
230221
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.backend.domain.analysis.lock;
2+
3+
import lombok.Getter;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.scheduling.annotation.Scheduled;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.util.concurrent.ConcurrentHashMap;
9+
10+
@Slf4j
11+
@Component
12+
public class InMemoryLockManager {
13+
private static final long LOCK_TIMEOUT_MILLIS = 5 * 60 * 1000; // 락 유지 시간 5분
14+
15+
// 현재 진행 중인 분석 정보
16+
private final ConcurrentHashMap<String, LockInfo> processingRepositories = new ConcurrentHashMap<>();
17+
18+
// 만료된 락 정리
19+
@Scheduled(fixedDelay = 60000)
20+
public void cleanExpiredLocks() {
21+
processingRepositories.entrySet().removeIf(entry ->
22+
entry.getValue().isExpired(LOCK_TIMEOUT_MILLIS)); // 만료된 락만 정리
23+
}
24+
25+
// 락 획득
26+
public boolean tryLock(String key) {
27+
LockInfo newLock = new LockInfo(System.currentTimeMillis(), Thread.currentThread().getName());
28+
LockInfo existing = processingRepositories.putIfAbsent(key, newLock);
29+
30+
if (existing != null && !existing.isExpired(LOCK_TIMEOUT_MILLIS)) {
31+
log.warn("중복 분석 요청 차단: {}", key);
32+
return false;
33+
}
34+
35+
if (existing != null && existing.isExpired(LOCK_TIMEOUT_MILLIS)) {
36+
processingRepositories.put(key, newLock);
37+
log.info("만료된 락 재획득: {}", key);
38+
}
39+
40+
log.debug("락 획득 성공: {}", key);
41+
return true;
42+
}
43+
44+
// 락 타임스탬프 갱신
45+
public void refreshLock(String key) {
46+
processingRepositories.computeIfPresent(key, (k, old) ->
47+
new LockInfo(System.currentTimeMillis(), old.getOwner())
48+
);
49+
log.debug("락 타임스탬프 갱신: {}", key);
50+
}
51+
52+
// 락 해제
53+
public void releaseLock(String key) {
54+
processingRepositories.remove(key);
55+
log.debug("락 해제: {}", key);
56+
}
57+
58+
@Getter
59+
private static class LockInfo {
60+
private final long timestamp;
61+
private final String owner;
62+
63+
public LockInfo(long timestamp, String owner) {
64+
this.timestamp = timestamp;
65+
this.owner = owner;
66+
}
67+
68+
public boolean isExpired(long timeoutMillis) {
69+
return System.currentTimeMillis() - timestamp > timeoutMillis;
70+
}
71+
}
72+
}

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

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

33
import com.backend.domain.analysis.entity.AnalysisResult;
4+
import com.backend.domain.analysis.lock.InMemoryLockManager;
45
import com.backend.domain.analysis.repository.AnalysisResultRepository;
56
import com.backend.domain.evaluation.service.EvaluationService;
67
import com.backend.domain.repository.dto.response.RepositoryData;
@@ -15,7 +16,6 @@
1516
import org.springframework.transaction.annotation.Transactional;
1617

1718
import java.util.List;
18-
import java.util.concurrent.ConcurrentHashMap;
1919

2020
@Slf4j
2121
@Service
@@ -26,9 +26,7 @@ public class AnalysisService {
2626
private final EvaluationService evaluationService;
2727
private final RepositoryJpaRepository repositoryJpaRepository;
2828
private final SseProgressNotifier sseProgressNotifier;
29-
30-
// 인메모리 락
31-
private final ConcurrentHashMap<String, Boolean> processingRepositories = new ConcurrentHashMap<>();
29+
private final InMemoryLockManager lockManager;
3230

3331
/* Analysis 분석 프로세스 오케스트레이션 담당
3432
* 1. GitHub URL 파싱 및 검증
@@ -44,9 +42,7 @@ public Long analyze(String githubUrl, Long userId) {
4442

4543
String cacheKey = userId + ":" + githubUrl;
4644

47-
// 현재 처리 중인지 체크
48-
if (processingRepositories.putIfAbsent(cacheKey, true) != null) {
49-
log.warn("⚠중복 분석 요청 차단: userId={}, url={}", userId, githubUrl);
45+
if (!lockManager.tryLock(cacheKey)) {
5046
throw new BusinessException(ErrorCode.ANALYSIS_IN_PROGRESS);
5147
}
5248

@@ -58,6 +54,7 @@ public Long analyze(String githubUrl, Long userId) {
5854

5955
try {
6056
repositoryData = repositoryService.fetchAndSaveRepository(owner, repo, userId);
57+
lockManager.refreshLock(cacheKey);
6158
log.info("🫠 Repository Data 수집 완료: {}", repositoryData);
6259
} catch (BusinessException e) {
6360
log.error("Repository 데이터 수집 실패: {}/{}", owner, repo, e);
@@ -73,6 +70,7 @@ public Long analyze(String githubUrl, Long userId) {
7370
// OpenAI API 데이터 분석 및 저장
7471
try {
7572
evaluationService.evaluateAndSave(repositoryData);
73+
lockManager.refreshLock(cacheKey);
7674
} catch (BusinessException e) {
7775
sseProgressNotifier.notify(userId, "error", "AI 평가 실패: " + e.getMessage());
7876
throw e;
@@ -82,7 +80,7 @@ public Long analyze(String githubUrl, Long userId) {
8280
return repositoryId;
8381
} finally {
8482
// 락 해제
85-
processingRepositories.remove(cacheKey);
83+
lockManager.releaseLock(cacheKey);
8684
log.info("분석 락 해제: cacheKey={}", cacheKey);
8785
}
8886
}

backend/src/main/java/com/backend/domain/user/controller/AuthController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public ApiResponse<String> verifyAuthCode(@RequestBody VerifyRequest request)
7070
//인증 성공시
7171
return ApiResponse.success("이메일 인증 성공");
7272
}else{
73-
return ApiResponse.error(ErrorCode.Email_verify_Failed);
73+
return ApiResponse.error(ErrorCode.EMAIL_VERIFY_FAILED);
7474
}
7575

7676
}
@@ -117,7 +117,7 @@ public ApiResponse<LoginResponse> login(
117117

118118
return ApiResponse.success(new LoginResponse(new UserDto(user)));
119119
}else{
120-
return ApiResponse.error(ErrorCode.Login_Failed);
120+
return ApiResponse.error(ErrorCode.LOGIN_FAILED);
121121
}
122122
}
123123

@@ -151,7 +151,7 @@ public ApiResponse<String> logout(
151151
}
152152

153153

154-
return ApiResponse.success("success");
154+
return ApiResponse.success("로그아웃 되었습니다.");
155155
}
156156

157157

backend/src/main/java/com/backend/domain/user/repository/UserRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository<User,Long> {
1212

1313
@Query("SELECT u FROM User u WHERE u.email = :email AND u.deleted = true")
1414
Optional<User> findByEmailIncludeDeleted(@Param("email") String email);
15+
16+
boolean existsByEmail(String email);
1517
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
@RequiredArgsConstructor
1919
@Transactional
2020
public class EmailService {
21+
private final UserRepository userRepository;
2122
private final JavaMailSender javaMailSender;
2223
private final RedisUtil redisUtil;
2324

@@ -38,6 +39,13 @@ private String createCode(){
3839
* @param email
3940
*/
4041
public void sendEmail(String email) throws MessagingException {
42+
//이미 회원가입된 email이면 예외 발생
43+
boolean existsByEmail = userRepository.existsByEmail(email);
44+
if(existsByEmail){
45+
System.out.println("이미 회원가입된 이메일 입니다.");
46+
throw new BusinessException(ErrorCode.ALREADY_REGISTERED_EMAIL);
47+
}
48+
4149
String authCode = createCode();
4250
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
4351
// true: 멀티파트 메시지(HTML 등) 활성화, "utf-8": 인코딩 설정

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public String login(@NotBlank(message = "이메일은 필수 입력값 입니다
2929
//email에 대응하는 비밀번호가 맞다면 jwt토큰 발급
3030
return jwtUtil.createToken(user.getEmail(), user.getName(), user.getId());
3131
}else{
32-
throw new BusinessException(ErrorCode.Login_Failed);
32+
throw new BusinessException(ErrorCode.LOGIN_FAILED);
3333
}
3434
}
3535

backend/src/main/java/com/backend/global/exception/ErrorCode.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ public enum ErrorCode {
1212
INVALID_INPUT_VALUE("CMN003", HttpStatus.BAD_REQUEST, "잘못된 입력값입니다."),
1313
INVALID_TYPE_VALUE("CMN004", HttpStatus.BAD_REQUEST, "잘못된 타입의 값입니다."),
1414
MISSING_REQUEST_PARAMETER("CMN005", HttpStatus.BAD_REQUEST, "필수 요청 파라미터가 누락되었습니다."),
15+
UNAUTHORIZED("CMN006", HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
1516

1617
// ========== user 도메인 에러 ==========
17-
Login_Failed("U001", HttpStatus.BAD_REQUEST, "로그인에 실패했습니다."),
18-
Email_verify_Failed("U002", HttpStatus.BAD_REQUEST, "이메일 인증코드가 일치하지 않습니다"),
18+
LOGIN_FAILED("U001", HttpStatus.BAD_REQUEST, "로그인에 실패했습니다."),
19+
EMAIL_VERIFY_FAILED("U002", HttpStatus.BAD_REQUEST, "이메일 인증코드가 일치하지 않습니다"),
1920
NAME_NOT_FOUND("U003", HttpStatus.NOT_FOUND, "이름이 입력되지 않았습니다."),
2021
PASSWORD_NOT_FOUND("U004", HttpStatus.NOT_FOUND, "비밀번호가 입력되지 않았습니다."),
2122
PASSWORD_NOT_EQUAL("U005", HttpStatus.BAD_REQUEST, "비밀번호 확인이 일치하지 않습니다."),
2223
EMAIL_NOT_FOUND("U006", HttpStatus.NOT_FOUND, "해당 이메일은 없는 계정입니다.") ,
24+
ALREADY_REGISTERED_EMAIL("U007", HttpStatus.BAD_REQUEST, "이미 회원가입된 이메일입니다."),
2325

2426
// ========== analysis 도메인 에러 ==========
2527
INVALID_GITHUB_URL("A001", HttpStatus.BAD_REQUEST, "올바른 GitHub 저장소 URL이 아닙니다."),

0 commit comments

Comments
 (0)