Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public WithdrawUserResponse withdrawUser(AuthUser authUser) {

userDomainService.isDeletedUser(user);

user.setDeleted();
user.markAsDeleted();

redisTemplate.delete("RefreshToken:" + authUser.getId());

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
}
}
Comment on lines +176 to 188
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

동일 이메일 재가입 후 같은 날 탈퇴 시 unique constraint 충돌 가능성

날짜 기반 suffix만 사용하면 같은 날 동일 이메일로 재가입 후 탈퇴하는 경우 unique key 충돌이 발생할 수 있습니다:

  1. user@example.com → 탈퇴 → user@example.com__deleted_20231220
  2. 같은 이메일로 재가입 → user@example.com
  3. 같은 날 탈퇴 시도 → user@example.com__deleted_20231220 (충돌!)

또한 markAsDeleted()가 중복 호출되면 suffix가 계속 추가됩니다.

🔎 UUID 또는 고유 식별자 추가를 권장합니다
 public void markAsDeleted() {
     this.isDeleted = true;
-    if (this.email != null && !this.email.isBlank()) {
+    if (this.email != null && !this.email.isBlank() && !this.email.contains("__deleted_")) {
         String deletedDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
-        this.email = this.email + "__deleted_" + deletedDate;
+        String uniqueSuffix = java.util.UUID.randomUUID().toString().substring(0, 8);
+        this.email = this.email + "__deleted_" + deletedDate + "_" + uniqueSuffix;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 회원 탈퇴 호출되는 메서드
* - 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;
}
}
/**
* 회원 탈퇴 호출되는 메서드
* - 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() && !this.email.contains("__deleted_")) {
String deletedDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String uniqueSuffix = java.util.UUID.randomUUID().toString().substring(0, 8);
this.email = this.email + "__deleted_" + deletedDate + "_" + uniqueSuffix;
}
}
🤖 Prompt for AI Agents
In src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java around
lines 176 to 188, the current markAsDeleted() appends a date-only suffix which
can collide if a user re-registers and is deleted the same day and also keeps
appending on repeated calls; change the suffix to include a unique component
(e.g., a short UUID or nano timestamp) so each deleted email is globally unique,
and make the method idempotent by first checking if isDeleted is already true or
if the email already contains your deleted-marker (skip modification if so).
Ensure the generated suffix format remains stable and human-readable (e.g.,
"__deleted_yyyyMMdd_<uuid>") and only modify the email when email is non-empty
and the account is not already marked deleted.


public boolean shouldSkipNotification(User recipient) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -77,7 +78,6 @@ public ReviewResult requestReview(ReviewPayload reviewPayload) {
}

private String callChatApi(Map<String, Object> requestBody) {

OpenAIResponse response = webClient.post()
.uri("/v1/chat/completions")
.bodyValue(requestBody)
Expand All @@ -87,14 +87,29 @@ private String callChatApi(Map<String, Object> 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();
Comment on lines +103 to 113
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

네트워크 연결 오류 처리 누락

WebClientResponseExceptionTimeoutException 외의 네트워크 연결 오류(예: ConnectException, UnknownHostException 등)가 발생할 경우 처리되지 않고 상위로 전파될 가능성이 있습니다. 이러한 예외들을 처리하기 위해 추가 onErrorMap() 핸들러를 추가하거나 .block() 호출을 try-catch로 감싸는 것을 고려하세요.

🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java
around lines 103 to 113, the reactive chain only maps WebClientResponseException
and TimeoutException but will let other network errors (e.g., ConnectException,
UnknownHostException, generic IOException) propagate; add an additional
onErrorMap handler (or wrap block() in try-catch) to catch these
connection-related exceptions and map them to an appropriate CodeReviewException
(e.g., REVIEW_NETWORK_ERROR) with a clear log entry including the exception;
specifically, add an onErrorMap for
ConnectException/UnknownHostException/IOException (or a broad Throwable fallback
after the specific handlers) that logs the error and returns the
CodeReviewException so network errors are standardized and don’t leak up
unhandled.


return Objects.requireNonNull(response).getReviewContent();
Expand Down