Skip to content

Commit ce79823

Browse files
authored
feat : 코드 리뷰 락 AOP 적용 및 주간 토큰 지급 로직 구현 (#75)
* refactor : User 패키지 구조 변경 및 코드 리뷰 요청을 위한 review_token 관련 로직 구현 * feat : review_token 관련 DTO 추가 * feat : 중복 리뷰 요청을 방지하기 위한 AOP 구현 * refactor : 공통 락을 사용하기 위한 prefix 추가 * refactor : 가독성을 위한 클래스 분리 및 롤백 추가 * refactor : AOP 사용을 위해 어노테이션 추가 * feat : 제출내역 조회 QueryDSL 구현 * feat : 매주 월요일 00시에 리뷰 토큰 초기화하는 스케줄러 구현 * refactor : 사용하지 않는 import문, 메서드 제거 * refactor : 로깅 및 예외 처리 강화
1 parent ae17114 commit ce79823

36 files changed

+510
-108
lines changed

src/main/java/org/ezcode/codetest/CodetestApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.context.annotation.EnableAspectJAutoProxy;
56
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
67
import org.springframework.scheduling.annotation.EnableScheduling;
78

89
@SpringBootApplication
910
@EnableJpaAuditing
1011
@EnableScheduling
12+
@EnableAspectJAutoProxy(proxyTargetClass = true)
1113
public class CodetestApplication {
1214

1315
public static void main(String[] args) {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.ezcode.codetest.application.submission.aop;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
9+
@Target(ElementType.METHOD)
10+
@Retention(RetentionPolicy.RUNTIME)
11+
@Documented
12+
public @interface CodeReviewLock {
13+
String prefix();
14+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.ezcode.codetest.application.submission.aop;
2+
3+
import org.aspectj.lang.ProceedingJoinPoint;
4+
import org.aspectj.lang.annotation.Around;
5+
import org.aspectj.lang.annotation.Aspect;
6+
import org.aspectj.lang.reflect.MethodSignature;
7+
import org.ezcode.codetest.application.submission.port.LockManager;
8+
import org.ezcode.codetest.domain.submission.exception.CodeReviewException;
9+
import org.ezcode.codetest.domain.submission.exception.code.CodeReviewExceptionCode;
10+
import org.ezcode.codetest.domain.user.model.entity.AuthUser;
11+
import org.springframework.core.Ordered;
12+
import org.springframework.core.annotation.Order;
13+
import org.springframework.stereotype.Component;
14+
15+
import lombok.RequiredArgsConstructor;
16+
17+
@Aspect
18+
@Component
19+
@RequiredArgsConstructor
20+
@Order(Ordered.HIGHEST_PRECEDENCE)
21+
public class CodeReviewLockAspect {
22+
23+
private final LockManager lockManager;
24+
25+
@Around("@annotation(org.ezcode.codetest.application.submission.aop.CodeReviewLock)")
26+
public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
27+
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
28+
String prefix = signature.getMethod().getAnnotation(CodeReviewLock.class).prefix();
29+
Object[] args = joinPoint.getArgs();
30+
31+
Long problemId = null;
32+
Long userId = null;
33+
34+
for (Object arg : args) {
35+
if (arg instanceof Long) {
36+
problemId = (Long) arg;
37+
} else if (arg instanceof AuthUser) {
38+
userId = ((AuthUser)arg).getId();
39+
}
40+
}
41+
42+
if (problemId == null || userId == null) {
43+
throw new CodeReviewException(CodeReviewExceptionCode.REQUIRED_ARGS_NOT_FOUND);
44+
}
45+
46+
boolean locked = lockManager.tryLock(prefix, userId, problemId);
47+
if (!locked) {
48+
throw new CodeReviewException(CodeReviewExceptionCode.ALREADY_REVIEWING);
49+
}
50+
51+
try {
52+
return joinPoint.proceed();
53+
} finally {
54+
lockManager.releaseLock(prefix, userId, problemId);
55+
}
56+
}
57+
}

src/main/java/org/ezcode/codetest/application/submission/model/OpenAIResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public record Message(String role, String content) {}
1515
public String getReviewContent() {
1616
return Optional.ofNullable(choices)
1717
.flatMap(list -> list.stream().findFirst())
18-
.map(choice -> choice.message.content)
18+
.map(choice -> choice.message().content())
1919
.orElse("");
2020
}
2121
}

src/main/java/org/ezcode/codetest/application/submission/port/LockManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
public interface LockManager {
44

5-
boolean tryLock(Long userId, Long problemId);
5+
boolean tryLock(String prefix, Long userId, Long problemId);
66

7-
void releaseLock(Long userId, Long problemId);
7+
void releaseLock(String prefix, Long userId, Long problemId);
88

99
}

src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.concurrent.Executor;
88
import java.util.concurrent.TimeUnit;
99

10+
import org.ezcode.codetest.application.submission.aop.CodeReviewLock;
1011
import org.ezcode.codetest.application.submission.dto.request.review.CodeReviewRequest;
1112
import org.ezcode.codetest.application.submission.dto.request.review.ReviewPayload;
1213
import org.ezcode.codetest.application.submission.dto.response.review.CodeReviewResponse;
@@ -67,7 +68,7 @@ public class SubmissionService {
6768

6869
public SseEmitter enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) {
6970

70-
boolean acquired = lockManager.tryLock(authUser.getId(), problemId);
71+
boolean acquired = lockManager.tryLock("submission", authUser.getId(), problemId);
7172

7273
if (!acquired) {
7374
throw new SubmissionException(SubmissionExceptionCode.ALREADY_JUDGING);
@@ -126,7 +127,7 @@ public void submitCodeStream(SubmissionMessage msg) {
126127
exceptionNotificationHelper(e);
127128
} finally {
128129
emitterStore.remove(msg.emitterKey());
129-
lockManager.releaseLock(msg.userId(), msg.problemId());
130+
lockManager.releaseLock("submission", msg.userId(), msg.problemId());
130131
}
131132
}
132133

@@ -138,8 +139,13 @@ public List<GroupedSubmissionResponse> getSubmissions(AuthUser authUser) {
138139
return GroupedSubmissionResponse.groupByProblem(submissions);
139140
}
140141

141-
public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request) {
142-
Problem problem = problemDomainService.getProblem(problemId);
142+
@Transactional
143+
@CodeReviewLock(prefix = "review")
144+
public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request, AuthUser authUser) {
145+
User user = userDomainService.getUserById(authUser.getId());
146+
userDomainService.decreaseReviewToken(user);
147+
148+
Problem problem = problemDomainService.getProblem(problemId);
143149
Language language = languageDomainService.getLanguage(request.languageId());
144150

145151
ReviewResult reviewResult = reviewClient.requestReview(ReviewPayload.of(problem, language, request));

src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SignupResponse;
1111
import org.ezcode.codetest.application.usermanagement.user.dto.response.LogoutResponse;
1212
import org.ezcode.codetest.domain.user.exception.AuthException;
13-
import org.ezcode.codetest.domain.user.exception.AuthExceptionCode;
13+
import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode;
1414
import org.ezcode.codetest.domain.user.model.entity.User;
1515
import org.ezcode.codetest.domain.user.model.entity.UserAuthType;
1616
import org.ezcode.codetest.domain.user.model.enums.AuthType;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.ezcode.codetest.application.usermanagement.user.model;
2+
3+
import java.util.List;
4+
5+
import org.ezcode.codetest.domain.submission.dto.WeeklySolveCount;
6+
7+
public record UsersByWeek(
8+
9+
List<Long> fullWeek,
10+
11+
List<Long> partialWeek
12+
13+
) {
14+
public static UsersByWeek from(List<WeeklySolveCount> counts, long weekLength) {
15+
List<Long> fullWeek = counts.stream()
16+
.filter(c -> c.solveDayCount() == weekLength)
17+
.map(WeeklySolveCount::userId)
18+
.toList();
19+
List<Long> partialWeek = counts.stream()
20+
.filter(c -> c.solveDayCount() != weekLength)
21+
.map(WeeklySolveCount::userId)
22+
.toList();
23+
return new UsersByWeek(fullWeek, partialWeek);
24+
}
25+
}

src/main/java/org/ezcode/codetest/application/usermanagement/user/service/.gitkeep

Whitespace-only changes.

src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package org.ezcode.codetest.application.usermanagement.user.service;
22

3+
import java.time.LocalDateTime;
4+
import java.time.temporal.ChronoUnit;
5+
import java.util.List;
6+
7+
import org.ezcode.codetest.application.usermanagement.user.model.UsersByWeek;
8+
import org.ezcode.codetest.domain.submission.dto.WeeklySolveCount;
39
import org.ezcode.codetest.application.usermanagement.user.dto.request.ChangeUserPasswordRequest;
410
import org.ezcode.codetest.application.usermanagement.user.dto.request.ModifyUserInfoRequest;
511
import org.ezcode.codetest.application.usermanagement.user.dto.response.ChangeUserPasswordResponse;
612
import org.ezcode.codetest.application.usermanagement.user.dto.response.UserInfoResponse;
713
import org.ezcode.codetest.application.usermanagement.user.dto.response.WithdrawUserResponse;
14+
import org.ezcode.codetest.domain.submission.service.SubmissionDomainService;
815
import org.ezcode.codetest.domain.user.exception.AuthException;
9-
import org.ezcode.codetest.domain.user.exception.AuthExceptionCode;
16+
import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode;
1017
import org.ezcode.codetest.domain.user.model.entity.AuthUser;
1118
import org.ezcode.codetest.domain.user.model.entity.User;
1219
import org.ezcode.codetest.domain.user.model.enums.AuthType;
@@ -16,7 +23,6 @@
1623

1724
import org.springframework.transaction.annotation.Transactional;
1825

19-
import jakarta.servlet.http.HttpServletRequest;
2026
import lombok.RequiredArgsConstructor;
2127
import lombok.extern.slf4j.Slf4j;
2228

@@ -26,6 +32,7 @@
2632
public class UserService {
2733

2834
private final UserDomainService userDomainService;
35+
private final SubmissionDomainService submissionDomainService;
2936
private final RedisTemplate<String, String> redisTemplate;
3037

3138
@Transactional(readOnly = true)
@@ -59,7 +66,6 @@ public UserInfoResponse modifyUserInfo(AuthUser authUser, ModifyUserInfoRequest
5966
modifyUserInfoRequest.introduction(),
6067
modifyUserInfoRequest.age());
6168

62-
6369
return UserInfoResponse.builder()
6470
.username(user.getUsername())
6571
.age(user.getAge())
@@ -75,7 +81,8 @@ public UserInfoResponse modifyUserInfo(AuthUser authUser, ModifyUserInfoRequest
7581
}
7682

7783
@Transactional
78-
public ChangeUserPasswordResponse modifyUserPassword(AuthUser authUser, ChangeUserPasswordRequest changeUserPasswordRequest) {
84+
public ChangeUserPasswordResponse modifyUserPassword(AuthUser authUser,
85+
ChangeUserPasswordRequest changeUserPasswordRequest) {
7986
User user = userDomainService.getUserById(authUser.getId());
8087

8188
//소셜로그인 회원은 변경 불가
@@ -95,7 +102,6 @@ public ChangeUserPasswordResponse modifyUserPassword(AuthUser authUser, ChangeUs
95102
return new ChangeUserPasswordResponse("비밀번호를 성공적으로 변경했습니다");
96103
}
97104

98-
99105
@Transactional
100106
public WithdrawUserResponse withdrawUser(AuthUser authUser) {
101107
User user = userDomainService.getUserById(authUser.getId());
@@ -104,8 +110,17 @@ public WithdrawUserResponse withdrawUser(AuthUser authUser) {
104110

105111
user.setDeleted();
106112

107-
redisTemplate.delete("RefreshToken:"+authUser.getId());
113+
redisTemplate.delete("RefreshToken:" + authUser.getId());
108114

109115
return new WithdrawUserResponse("탈퇴가 완료되었습니다");
110116
}
117+
118+
@Transactional
119+
public void resetAllUsersTokensWeekly(LocalDateTime startDateTime, LocalDateTime endDateTime) {
120+
121+
List<WeeklySolveCount> counts = submissionDomainService.getWeeklySolveCounts(startDateTime, endDateTime);
122+
long weekLength = ChronoUnit.DAYS.between(startDateTime, endDateTime);
123+
124+
userDomainService.resetReviewTokensForUsers(UsersByWeek.from(counts, weekLength));
125+
}
111126
}

0 commit comments

Comments
 (0)