From 90b10a3734df36f05a96df2f0f16d5760ee9b41c Mon Sep 17 00:00:00 2001 From: SeungWoo Ryu Date: Sat, 20 Dec 2025 21:24:25 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=A7=81=ED=9B=84=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=86=A0=ED=81=B0=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문제점: - 이메일 인증 완료 후 verified=true가 되어도 리뷰 토큰이 없음 - 토큰 값이 하드코딩되어 유지보수 어려움 - setReviewToken(int)로 임의 값 설정 가능하여 오용 위험 변경사항: - AuthService.verifyEmailCode()에서 인증 완료 시 기본 토큰 발급 - User 엔티티에 토큰 상수 추가 (DEFAULT_REVIEW_TOKEN=20, FULL_WEEK_REVIEW_TOKEN=40) - setReviewToken(int) 제거하고 의미 있는 메서드로 분리 - setDefaultReviewToken(): 기본 토큰 설정 (이메일 인증, OAuth 가입 시) - setFullWeekReviewToken(): 주간 풀 참여 토큰 설정 - CustomOAuth2UserService에서도 새로운 메서드 사용 --- .../auth/service/AuthService.java | 1 + .../domain/user/model/entity/User.java | 21 +++++++++++++++---- .../user/service/CustomOAuth2UserService.java | 4 ++-- .../user/service/UserDomainService.java | 4 ++-- 4 files changed, 22 insertions(+), 8 deletions(-) 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/domain/user/model/entity/User.java b/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java index e03101b8..25ef6515 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 @@ -188,10 +188,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 +215,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() { From c300273f54696512b37425d27e1e0c991f18f1ce Mon Sep 17 00:00:00 2001 From: SeungWoo Ryu Date: Sat, 20 Dec 2025 21:42:50 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=9D=B4=20=EC=9E=AC=EA=B0=80=EC=9E=85=20=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=20=EC=8B=9C,=20=EC=83=88=EB=A1=9C=EC=9A=B4=20id?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=9C=EA=B8=89=ED=95=98=EA=B3=A0=20=EC=99=84?= =?UTF-8?q?=EC=A0=84=ED=9E=88=20=EA=B9=A8=EB=81=97=ED=95=9C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A1=9C=20=EC=8B=9C=EC=9E=91=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원 탈퇴 시 soft delete 처리를 하고 있는데, 추후 같은 이메일로 재가입 시도 시 정상적으로 가입할 수 있도록 함 - 이를 위해 탈퇴한 계정의 이메일 값을 변경함 - ex) user@example.com -> user@example.com__deleted_20231220 - `setDeleted` 메서드 이름도 `markAsDeleted`로 변경 --- .../usermanagement/user/service/UserService.java | 2 +- .../codetest/domain/user/model/entity/User.java | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) 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 25ef6515..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) { From 0ba09d6e9ec93d544194d6073abbbcca83efcd53 Mon Sep 17 00:00:00 2001 From: SeungWoo Ryu Date: Sun, 21 Dec 2025 01:37:33 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20OpenAIReviewClient=EC=97=90=EC=84=9C?= =?UTF-8?q?=20open=20ai=EC=9D=98=20url=EA=B3=BC=20key=20=EA=B0=92=EC=9D=84?= =?UTF-8?q?=20=EC=9D=BD=EC=A7=80=20=EB=AA=BB=ED=95=98=EA=B3=A0=20=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개발 중 IntelliJ 환경변수 사용하던 값을 수정하지 않은 것으로 보임 - application.properties 파일에서 해당 값을 정상적으로 읽어올 수 있도록 수정함 - 그리고 open ai 계정에 API key가 발급되있지 않은 상태였음. 지금까지는 어떻게 동작했는지는 의문... --- .../openai/OpenAIReviewClient.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) 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();