diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java index dd17310f..1c88b082 100644 --- a/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java @@ -128,6 +128,7 @@ public VerifyEmailCodeResponse verifyEmailCode(String email, String key) { if (isMatch){ user.setVerified(); + user.setDefaultReviewToken(); return VerifyEmailCodeResponse.from("인증되었습니다", isMatch); } else { throw new UserException(UserExceptionCode.NOT_MATCH_CODE); diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java index 32ff7efa..5dd29dcc 100644 --- a/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java @@ -156,7 +156,7 @@ public WithdrawUserResponse withdrawUser(AuthUser authUser) { userDomainService.isDeletedUser(user); - user.setDeleted(); + user.markAsDeleted(); redisTemplate.delete("RefreshToken:" + authUser.getId()); diff --git a/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java b/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java index e03101b8..a721793c 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java +++ b/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java @@ -1,5 +1,7 @@ package org.ezcode.codetest.domain.user.model.entity; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @@ -171,8 +173,18 @@ public void modifyUserInfo(String nickname, String githubUrl, String blogUrl, St } - public void setDeleted() { + /** + * 회원 탈퇴 시 호출되는 메서드 + * - isDeleted 플래그를 true로 설정 + * - 이메일을 변경하여 동일 이메일 재가입 시 unique key 충돌 방지 + * (예: user@example.com -> user@example.com__deleted_20231220) + */ + public void markAsDeleted() { this.isDeleted = true; + if (this.email != null && !this.email.isBlank()) { + String deletedDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + this.email = this.email + "__deleted_" + deletedDate; + } } public boolean shouldSkipNotification(User recipient) { @@ -188,10 +200,6 @@ public void setVerified(){ this.verified = true; } - public void setReviewToken(int reviewToken){ - this.reviewToken = reviewToken; - } - public void setGithubUrl(String githubUrl){ this.githubUrl = githubUrl; } @@ -219,4 +227,21 @@ public void modifyUserRole(UserRole userRole) { public void setLanguage(Language userLanguage) { this.language = userLanguage; } + + public static final int DEFAULT_REVIEW_TOKEN = 20; + public static final int FULL_WEEK_REVIEW_TOKEN = 40; + + /** + * 기본 리뷰 토큰을 설정 이메일 인증 직후, OAuth 가입 시 사용) + */ + public void setDefaultReviewToken() { + this.reviewToken = DEFAULT_REVIEW_TOKEN; + } + + /** + * 주간 풀 참여 리뷰 토큰을 설정 (주간 스케줄러에서 사용) + */ + public void setFullWeekReviewToken() { + this.reviewToken = FULL_WEEK_REVIEW_TOKEN; + } } diff --git a/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java b/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java index f6378add..c11d9c05 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java +++ b/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java @@ -88,7 +88,7 @@ private void createNewUser(OAuth2Response response, AuthType authType, String pr Language language = languageDomainService.getLanguage(1L); //기본적으로 1번 언어로 가입 시 세팅 User newUser = UserFactory.createSocialUser(response, nickname, provider, language); newUser.setVerified(); - newUser.setReviewToken(20); + newUser.setDefaultReviewToken(); userRepository.createUser(newUser); userAuthTypeRepository.createUserAuthType(new UserAuthType(newUser, authType)); updateGithubUrl(newUser, response, provider); @@ -98,7 +98,7 @@ private void updateExistingUser(User user, OAuth2Response response, AuthType aut if (!userDomainService.getUserAuthTypes(user).contains(authType)) { if (!user.isVerified()) { user.setVerified(); - user.setReviewToken(20); + user.setDefaultReviewToken(); } userAuthTypeRepository.createUserAuthType(new UserAuthType(user, authType)); updateGithubUrl(user, response, provider); diff --git a/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java b/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java index e01ce696..bad8def9 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java @@ -113,8 +113,8 @@ public void decreaseReviewToken(User user) { } public void resetReviewTokensForUsers(UsersByWeek users) { - userRepository.updateReviewTokens(users.fullWeek(), 40); - userRepository.updateReviewTokens(users.partialWeek(), 20); + userRepository.updateReviewTokens(users.fullWeek(), User.FULL_WEEK_REVIEW_TOKEN); + userRepository.updateReviewTokens(users.partialWeek(), User.DEFAULT_REVIEW_TOKEN); } private static String generateRandomNickname() { diff --git a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java index e19024f4..72dd2c40 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java @@ -14,6 +14,7 @@ import org.ezcode.codetest.domain.submission.exception.code.CodeReviewExceptionCode; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.transaction.interceptor.TransactionAspectSupport; @@ -30,10 +31,10 @@ @RequiredArgsConstructor public class OpenAIReviewClient implements ReviewClient { - @Value("${OPEN_API_URL}") + @Value("${openai.api.url}") private String openApiUrl; - @Value("${OPEN_API_KEY}") + @Value("${openai.api.key}") private String openApiKey; private WebClient webClient; private final OpenAIMessageBuilder openAiMessageBuilder; @@ -77,7 +78,6 @@ public ReviewResult requestReview(ReviewPayload reviewPayload) { } private String callChatApi(Map requestBody) { - OpenAIResponse response = webClient.post() .uri("/v1/chat/completions") .bodyValue(requestBody) @@ -87,14 +87,29 @@ private String callChatApi(Map requestBody) { .retryWhen( Retry.backoff(3, Duration.ofSeconds(1)) .maxBackoff(Duration.ofSeconds(5)) - .filter(ex -> ex instanceof WebClientResponseException - || ex instanceof TimeoutException) + .filter(ex -> { + if (ex instanceof TimeoutException) { + return true; + } + if (ex instanceof WebClientResponseException) { + HttpStatusCode statusCode = ((WebClientResponseException) ex).getStatusCode(); + // 5xx 서버 오류만 재시도 (502, 503 등) + return statusCode.is5xxServerError(); + } + return false; + }) .onRetryExhaustedThrow((spec, signal) -> signal.failure()) ) - .onErrorMap(WebClientResponseException.class, - ex -> new CodeReviewException(CodeReviewExceptionCode.REVIEW_SERVER_ERROR)) - .onErrorMap(TimeoutException.class, - ex -> new CodeReviewException(CodeReviewExceptionCode.REVIEW_TIMEOUT)) + .onErrorMap(WebClientResponseException.class, ex -> { + HttpStatusCode statusCode = ex.getStatusCode(); + String responseBody = ex.getResponseBodyAsString(); + log.error("OpenAI API 호출 실패 - Status: {}, Response Body: {}", statusCode, responseBody, ex); + return new CodeReviewException(CodeReviewExceptionCode.REVIEW_SERVER_ERROR); + }) + .onErrorMap(TimeoutException.class, ex -> { + log.error("OpenAI API 호출 타임아웃", ex); + return new CodeReviewException(CodeReviewExceptionCode.REVIEW_TIMEOUT); + }) .block(); return Objects.requireNonNull(response).getReviewContent();