[FEAT] 학습 도메인 api 구현#45
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough학습 결과 저장 및 진행률 조회 API가 추가되었습니다. 요청·응답 DTO, 사용자 연관관계와 상태 계산, JPA 저장소, 서비스 로직, 예외 상태 및 컨트롤러가 구현되었습니다. Changes학습 결과 및 진행률
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant LearningController
participant LearningService
participant UserLearningProgressRepository
participant LearningStepRepository
Client->>LearningController: 학습 결과 저장 요청
LearningController->>LearningService: saveResult(...)
LearningService->>UserLearningProgressRepository: 진행 데이터 저장 또는 갱신
LearningService-->>LearningController: 저장 결과 반환
LearningController-->>Client: ApiResponse 응답
Client->>LearningController: 진행률 조회 요청
LearningController->>LearningService: getLearningProgress(...)
LearningService->>LearningStepRepository: 전체 단계 수 조회
LearningService->>UserLearningProgressRepository: 완료 단계 수 조회
LearningService-->>Client: 진행률 응답
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (2)
src/test/java/com/mr/global/client/ai/AiServerClientTest.java (1)
31-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win모든 테스트 뒤에 Mock 서버 기대값을 검증하세요.
현재는 등록한
expect(...)가 실제로 모두 소비됐는지 확인하지 않습니다.@AfterEach에서verify()를 호출하면 요청이 누락된 테스트도 실패합니다. Spring 문서도 테스트 종료 시verify()호출을 권장합니다. (docs.spring.io)수정 예시
+import org.junit.jupiter.api.AfterEach; + `@BeforeEach` void setUp() { // ... } + + `@AfterEach` + void verifyMockServer() { + mockServer.verify(); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/mr/global/client/ai/AiServerClientTest.java` around lines 31 - 43, AiServerClientTest의 MockRestServiceServer 기대값이 테스트 종료 후 검증되지 않습니다. 테스트 클래스에 `@AfterEach` 정리 메서드를 추가하고 mockServer.verify()를 호출하여 등록된 모든 expect(...)가 소비되었는지 확인하세요.src/main/java/com/mr/global/security/jwt/JwtProperties.java (1)
11-18: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winJWT Secret의 Base64 및 최소 길이를 바인딩 단계에서 검증하세요.
현재
@NotBlank는 값이 비어 있는지만 확인합니다.JwtTokenProvider는 이후Decoders.BASE64.decode(...)와 HMAC 키 생성을 수행하므로, 잘못된 Base64 형식이나 32바이트 미만의 키가 설정 바인딩을 통과한 뒤 애플리케이션 초기화에서 실패할 수 있습니다. Base64 디코딩 후 최소 32바이트인지 확인하는 커스텀 제약 조건 또는 명시적인 startup validation을 추가해 주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/mr/global/security/jwt/JwtProperties.java` around lines 11 - 18, Update the JWT properties binding around the secret field in JwtProperties so it validates that the configured value is valid Base64 and decodes to at least 32 bytes before startup proceeds. Add a custom constraint/validator or explicit startup validation integrated with this configuration, while preserving the existing non-blank and token-validity checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/com/mr/domain/auth/controller/AuthController.java`:
- Around line 17-20: AuthController의 `@Profile`({"local", "dev"}) 제한을 제거하거나 운영
프로필에서도 등록되도록 조정해 /api/auth/login/** 엔드포인트가 prod 및 프로필 미지정 환경에서 제공되게 하세요. Mock 전용
로그인 동작은 별도 프로필 설정 또는 구현으로 분리하고, 운영 환경에서는 실제 인증 구현이 등록되도록 구성하세요.
In `@src/main/java/com/mr/domain/auth/entity/enums/SocialType.java`:
- Around line 4-5: Update SocialType binding for /api/auth/login/{socialType} to
preserve compatibility with existing uppercase values KAKAO and GOOGLE while
continuing to accept the declared lowercase enum names. Register an explicit
case-insensitive Converter for the SocialType path variable, or otherwise retain
both representations through the enum binding configuration; ensure invalid
values still produce the existing conversion failure behavior.
In `@src/main/java/com/mr/domain/auth/service/AuthService.java`:
- Around line 19-32: Remove the mock authentication flow: in
src/main/java/com/mr/domain/auth/service/AuthService.java:19-32, validate the
social provider token, find or create the real user, and issue JWTs using that
user’s ID; in
src/main/java/com/mr/domain/auth/controller/AuthController.java:17-20, register
the production login endpoint and move mock behavior behind a profile; in
src/main/java/com/mr/global/security/principal/CustomUserDetailsService.java:12-23,
load the user and roles through UserRepository and reject missing users.
In
`@src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java`:
- Around line 5-9: Update the SaveResultDTO.score validation in
LearningResultSaveRequestDTO to enforce the valid 0–100 range by adding minimum
and maximum constraints alongside `@NotNull`, preserving the existing
required-field message.
- Around line 5-9: 학습 결과 저장의 식별 단위를 user·learning·learningStep으로 통일하세요.
src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java:5-9의
SaveResultDTO에 learningStepId를 추가하고,
src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java:77-81에서는
필수 learningStep 누락을 Custom Exception으로 거부하세요.
src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java:12-17의
조회·완료 집계를 user·learning·learningStep 기준으로 변경하고,
src/main/java/com/mr/domain/learning/service/LearningService.java:47-56에서 단계 존재
및 학습 소속을 검증한 뒤 단계 포함 조회·생성 경로를 사용하세요.
In `@src/main/java/com/mr/domain/learning/dto/res/LearningResultResponseDTO.java`:
- Around line 8-24: Update SaveResultResultDTO.from so completedAt is populated
only from a dedicated completion timestamp recorded when the learning status
transitions to completed; otherwise rename the response field to lastStudiedAt
or updatedAt and map progress.getUpdatedAt() to that field, avoiding use of
updatedAt as completedAt.
In
`@src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java`:
- Line 13: Update the completion-criteria comment in
UserLearningProgressRepository to state that completed stages require a score of
90 or higher, matching the query and getLearningStatus(). If practical within
the existing design, centralize the completion threshold so these checks reuse
one managed value instead of duplicating 90.
- Around line 12-16: User 식별자 경로가 엔티티의 실제 필드명과 일치하지 않습니다.
UserLearningProgressRepository의 findByUserIdAndLearningId 파생 메서드를 User.userId와
Learning.id를 명시하는 중첩 프로퍼티 경로로 변경하고, 같은 리포지토리의 `@Query에서도` ulp.user.id를
ulp.user.userId로 수정하세요. 기존 메서드 인자와 조회 동작은 유지하세요.
In `@src/main/java/com/mr/domain/learning/service/LearningService.java`:
- Around line 23-25: Update LearningService so the write method saveResult()
explicitly uses a read-write `@Transactional` configuration, preventing
inheritance of the class-level readOnly transaction. Keep
`@Transactional`(readOnly = true) on the class or apply it only to query methods,
while preserving read-only behavior for those queries.
In `@src/main/java/com/mr/domain/subscriptions/entity/Subscription.java`:
- Around line 61-64: Update Subscription.create and its validateDates flow to
reject null startDate or endDate with the appropriate dedicated
SubscriptionErrorStatus before checking date ordering. Preserve the existing
invalid-range validation for non-null dates, ensuring required subscription
periods fail as domain exceptions rather than reaching persistence or
isActive().
In `@src/main/java/com/mr/global/apipayload/domain/AiServerErrorStatus.java`:
- Line 12: Update INVALID_RESPONSE in AiServerErrorStatus to use
HttpStatus.BAD_GATEWAY instead of INTERNAL_SERVER_ERROR, and change its error
code from the AI_SERVER_500 family to the corresponding 502-series code. Leave
the message unchanged.
In `@src/main/java/com/mr/global/client/ai/AiServerClient.java`:
- Line 44: Update the error logging in AiServerClient to stop logging
e.getResponseBodyAsString() verbatim; retain the HTTP status code and log only a
safe tracing identifier, or sanitize and length-limit the body if it must be
included.
In `@src/main/java/com/mr/global/security/jwt/JwtAuthenticationFilter.java`:
- Around line 33-49: Update the logging in JwtAuthenticationFilter’s JWT
validation and authentication exception paths to never include the raw jwt
value. Log only the failure type, request path, and available correlation ID,
while preserving the existing context-clearing and request exception handling.
In
`@src/main/java/com/mr/global/security/principal/CustomUserDetailsService.java`:
- Around line 12-23: Update loadUserByUsername in CustomUserDetailsService to
inject and use UserRepository for looking up the parsed userId. Throw
UsernameNotFoundException when no user exists, and construct CustomUserDetails
with the retrieved user’s persisted role instead of always using
UserRole.ROLE_STUDENT; preserve the existing handling for invalid numeric
usernames.
---
Nitpick comments:
In `@src/main/java/com/mr/global/security/jwt/JwtProperties.java`:
- Around line 11-18: Update the JWT properties binding around the secret field
in JwtProperties so it validates that the configured value is valid Base64 and
decodes to at least 32 bytes before startup proceeds. Add a custom
constraint/validator or explicit startup validation integrated with this
configuration, while preserving the existing non-blank and token-validity
checks.
In `@src/test/java/com/mr/global/client/ai/AiServerClientTest.java`:
- Around line 31-43: AiServerClientTest의 MockRestServiceServer 기대값이 테스트 종료 후
검증되지 않습니다. 테스트 클래스에 `@AfterEach` 정리 메서드를 추가하고 mockServer.verify()를 호출하여 등록된 모든
expect(...)가 소비되었는지 확인하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 704e8f01-b8ce-4002-9d9b-56a763184dfe
📒 Files selected for processing (47)
.gitignoreMR_config/local/application.example.ymlsrc/main/java/com/mr/domain/auth/controller/AuthController.javasrc/main/java/com/mr/domain/auth/dto/AuthRequestDTO.javasrc/main/java/com/mr/domain/auth/dto/AuthResponseDTO.javasrc/main/java/com/mr/domain/auth/entity/SocialAuth.javasrc/main/java/com/mr/domain/auth/entity/enums/SocialType.javasrc/main/java/com/mr/domain/auth/service/AuthService.javasrc/main/java/com/mr/domain/learning/controller/LearningController.javasrc/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.javasrc/main/java/com/mr/domain/learning/dto/res/LearningProgressResponseDTO.javasrc/main/java/com/mr/domain/learning/dto/res/LearningResultResponseDTO.javasrc/main/java/com/mr/domain/learning/entity/UserLearningProgress.javasrc/main/java/com/mr/domain/learning/exception/LearningErrorStatus.javasrc/main/java/com/mr/domain/learning/repository/LearningRepository.javasrc/main/java/com/mr/domain/learning/repository/LearningStepRepository.javasrc/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.javasrc/main/java/com/mr/domain/learning/service/LearningService.javasrc/main/java/com/mr/domain/subscriptions/entity/Subscription.javasrc/main/java/com/mr/domain/subscriptions/exception/SubscriptionErrorStatus.javasrc/main/java/com/mr/domain/user/entity/StudentInstrument.javasrc/main/java/com/mr/domain/user/entity/User.javasrc/main/java/com/mr/domain/user/entity/enums/UserRole.javasrc/main/java/com/mr/domain/user/exception/UserErrorStatus.javasrc/main/java/com/mr/domain/user/repository/UserRepository.javasrc/main/java/com/mr/global/apipayload/code/CommonStatus.javasrc/main/java/com/mr/global/apipayload/domain/AiServerErrorStatus.javasrc/main/java/com/mr/global/client/ai/AiAnalysisRequest.javasrc/main/java/com/mr/global/client/ai/AiServerClient.javasrc/main/java/com/mr/global/config/AiServerProperties.javasrc/main/java/com/mr/global/config/AiServerRestClientConfig.javasrc/main/java/com/mr/global/config/SecurityConfig.javasrc/main/java/com/mr/global/config/SwaggerConfig.javasrc/main/java/com/mr/global/security/SecurityConfig.javasrc/main/java/com/mr/global/security/SecurityUtil.javasrc/main/java/com/mr/global/security/jwt/JwtAccessDeniedHandler.javasrc/main/java/com/mr/global/security/jwt/JwtAuthenticationEntryPoint.javasrc/main/java/com/mr/global/security/jwt/JwtAuthenticationFilter.javasrc/main/java/com/mr/global/security/jwt/JwtProperties.javasrc/main/java/com/mr/global/security/jwt/JwtTokenProvider.javasrc/main/java/com/mr/global/security/principal/CustomUserDetails.javasrc/main/java/com/mr/global/security/principal/CustomUserDetailsService.javasrc/main/resources/application.example.ymlsrc/main/resources/application.ymlsrc/test/java/com/mr/global/client/ai/AiAnalysisRequestSerializationTest.javasrc/test/java/com/mr/global/client/ai/AiServerClientTest.javasrc/test/java/com/mr/global/config/AiServerPropertiesBindingTest.java
💤 Files with no reviewable changes (1)
- src/main/java/com/mr/global/config/SecurityConfig.java
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/auth") | ||
| @Profile({"local", "dev"}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
운영 환경에서는 로그인 엔드포인트가 등록되지 않습니다.
prod 또는 프로필 미지정 환경에서는 이 컨트롤러가 생성되지 않아 /api/auth/login/**가 404가 됩니다. 운영용 구현을 등록하고, mock 전용 구성을 분리해 주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/domain/auth/controller/AuthController.java` around lines
17 - 20, AuthController의 `@Profile`({"local", "dev"}) 제한을 제거하거나 운영 프로필에서도 등록되도록
조정해 /api/auth/login/** 엔드포인트가 prod 및 프로필 미지정 환경에서 제공되게 하세요. Mock 전용 로그인 동작은 별도
프로필 설정 또는 구현으로 분리하고, 운영 환경에서는 실제 인증 구현이 등록되도록 구성하세요.
| kakao, | ||
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP 'SocialType\.(KAKAO|GOOGLE)|/api/auth/login/(KAKAO|GOOGLE)' \
-g '*.java' -g '*.kt' -g '*.md' -g '*.json' -g '*.yaml' -g '*.yml' .Repository: Musereview/BE
Length of output: 151
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- relevant files ---'
git ls-files | rg '(^|/)SocialType\.java$|SocialType|social|login|controller|path|enum|openapi|swagger|README|docs|api|auth' || true
echo
echo '--- SocialType.java ---'
if [ -f src/main/java/com/mr/domain/auth/entity/enums/SocialType.java ]; then
cat -n src/main/java/com/mr/domain/auth/entity/enums/SocialType.java
else
fd -a 'SocialType.java' . | while read -r f; do
echo "===== $f ====="
cat -n "$f"
done
fi
echo
echo '--- references to social type / login path (broader) ---'
rg -n -i 'SocialType|socialType|kakao|google|/login|login/(KKA|KAKAO|GOOGLE)|api/auth/login|/api/auth/login' \
-g '*.java' -g '*.kt' -g '*.md' -g '*.jsp' -g '*.html' -g '*.json' -g '*.yaml' -g '*.yml' -g '*.properties' -g '*.xml' . || trueRepository: Musereview/BE
Length of output: 6295
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- AuthController.java ---'
cat -n src/main/java/com/mr/domain/auth/controller/AuthController.java
echo
echo '--- AuthService.java relevant lines ---'
sed -n '1,80p' src/main/java/com/mr/domain/auth/service/AuthService.java | cat -n
echo
echo '--- build files Spring Boot versions (non-exec) ---'
for f in build.gradle build.gradle.kts pom.xml settings.gradle settings.gradle.kts; do
[ -f "$f" ] && { echo "===== $f ====="; sed -n '1,220p' "$f" | cat -n; }
done
echo
echo '--- config files with WebMvcConfigurer enum-related settings ---'
rg -n -i 'FormattingConversionService|EnumValueResolver|Formatter|WebMvcConfigurer|`@Configuration`|addFormatters|setFormatter' \
-g '*.java' -g '*.kt' -g '*.yml' -g '*.yaml' -g '*.properties' . || trueRepository: Musereview/BE
Length of output: 6950
🌐 Web query:
Spring 6 WebMVC default enum conversion from PathVariable toString case insensitive match
💡 Result:
In Spring 6 WebMVC, the default conversion of a PathVariable to an enum is case-sensitive [1][2]. Spring Framework's built-in mechanism uses Enum.valueOf, which requires the input string to exactly match the declared enum constant name [2]. Spring does not provide a built-in, default configuration for case-insensitive enum binding because it is considered an opinionated approach that may have security implications [1]. To achieve case-insensitive matching for PathVariables, you must implement and register a custom Converter [1][3][2]. To implement this: 1. Create a custom Converter: public class StringToEnumConverter implements Converter<String, YourEnum> { @Override public YourEnum convert(String source) { return YourEnum.valueOf(source.toUpperCase); } } 2. Register the converter in your WebMvcConfigurer configuration: @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToEnumConverter); } } By registering this converter, Spring will automatically apply it when binding a PathVariable to your enum type, allowing you to accept both lowercase and uppercase inputs [3][4][2].
Citations:
- 1: More flexible binding for enums spring-projects/spring-framework#23023
- 2: https://www.baeldung.com/spring-enum-request-param
- 3: https://stackoverflow.com/questions/77935282/can-an-enum-constant-be-specified-in-both-upper-and-lower-case-in-a-spring-endpo
- 4: https://www.baeldung.com/spring-mvc-custom-data-binder
大写 경로 호환성 마이그레이션을 추가해 주세요.
/api/auth/login/{socialType}에서 @PathVariable은 SocialType의 선언 이름과 일치해야 합니다. 기존 클라이언트에서 KAKAO, GOOGLE을 보내면 소문자로 바뀌어 컨버전 실패로 400 응답을 받을 수 있으므로 배포된 서비스라면 기존 값 유지 또는 대소문자 양쪽을 처리하도록 명시적 Converter를 등록해 주세요. Spring 공식 enum bind 참고: Mapping path variables to enumerations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/domain/auth/entity/enums/SocialType.java` around lines 4
- 5, Update SocialType binding for /api/auth/login/{socialType} to preserve
compatibility with existing uppercase values KAKAO and GOOGLE while continuing
to accept the declared lowercase enum names. Register an explicit
case-insensitive Converter for the SocialType path variable, or otherwise retain
both representations through the enum binding configuration; ensure invalid
values still produce the existing conversion failure behavior.
| @Transactional | ||
| public AuthResponseDTO.LoginResponse socialLogin(SocialType socialType, String accessToken) { | ||
| // 1. 외부 소셜 API (카카오/구글) 통신하여 유저 프로필(email, socialId) 파싱 | ||
| // SocialUserInfo userInfo = getSocialUserInfo(socialType, accessToken); | ||
|
|
||
| // 2. TODO: User 엔티티 연동 및 가입여부 검증 (Stub 구조) | ||
| // 만약 가입 안 되어있으면 DB User 생성 -> 저장 | ||
| Long mockUserId = 1L; | ||
| String mockEmail = "user@example.com"; | ||
| String mockNickname = "뮤즈유저"; | ||
| boolean isNewUser = false; | ||
|
|
||
| String appAccessToken = jwtTokenProvider.createAccessToken(mockUserId); | ||
| String appRefreshToken = jwtTokenProvider.createRefreshToken(mockUserId); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
mock 인증 흐름을 운영 경로에서 제거하세요.
현재 소셜 토큰 검증, 실제 사용자 식별, 저장된 역할 조회가 모두 생략되어 인증 우회와 운영 환경의 로그인 불가 문제가 함께 발생합니다.
src/main/java/com/mr/domain/auth/service/AuthService.java#L19-L32: 소셜 제공자 토큰을 검증하고 실제 사용자 조회·생성 후 해당 ID로 JWT를 발급하세요.src/main/java/com/mr/domain/auth/controller/AuthController.java#L17-L20: 운영용 로그인 컨트롤러를 등록하고 mock 구현만 프로필로 분리하세요.src/main/java/com/mr/global/security/principal/CustomUserDetailsService.java#L12-L23:UserRepository로 사용자와 역할을 조회하고, 없는 사용자는 인증을 거부하세요.
📍 Affects 3 files
src/main/java/com/mr/domain/auth/service/AuthService.java#L19-L32(this comment)src/main/java/com/mr/domain/auth/controller/AuthController.java#L17-L20src/main/java/com/mr/global/security/principal/CustomUserDetailsService.java#L12-L23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 19 -
32, Remove the mock authentication flow: in
src/main/java/com/mr/domain/auth/service/AuthService.java:19-32, validate the
social provider token, find or create the real user, and issue JWTs using that
user’s ID; in
src/main/java/com/mr/domain/auth/controller/AuthController.java:17-20, register
the production login endpoint and move mock behavior behind a profile; in
src/main/java/com/mr/global/security/principal/CustomUserDetailsService.java:12-23,
load the user and roles through UserRepository and reject missing users.
| public class LearningResultSaveRequestDTO { | ||
| public record SaveResultDTO( | ||
| @NotNull(message = "점수는 필수 입력값입니다.") | ||
| Integer score | ||
| ) {} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
점수 범위를 요청 단계에서 제한해 주세요.
현재 @NotNull만 있어 음수나 100 초과 점수가 저장될 수 있고, 이후 score >= 90 조건으로 완료 처리됩니다. 점수 범위가 0~100이라면 @Min(0)과 @Max(100)을 추가해 잘못된 학습 결과를 차단해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java`
around lines 5 - 9, Update the SaveResultDTO.score validation in
LearningResultSaveRequestDTO to enforce the valid 0–100 range by adding minimum
and maximum constraints alongside `@NotNull`, preserving the existing
required-field message.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
학습 결과 저장의 식별 단위를 단계별로 통일해야 합니다.
현재 UserLearningProgress는 (user, learningStep) 단위인데, 요청과 서비스는 (user, learning) 단위로 처리합니다. 이 불일치 때문에 신규 저장은 null 단계로 실패하고, 여러 단계 저장 시 단건 조회가 비유일해지며, 진행률 집계에서는 저장된 결과가 제외됩니다.
src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java#L5-L9:learningStepId를 요청에 추가하거나, 엔티티를 학습 단위 모델로 재설계하세요.src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java#L77-L81: 필수learningStep이 null이면 Custom Exception으로 즉시 거부하세요.src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java#L12-L17: 유저·학습·단계 기준으로 단건 조회하고, 동일한 단계 기준으로 완료 수를 집계하세요.src/main/java/com/mr/domain/learning/service/LearningService.java#L47-L56: 단계 존재 여부와 학습 소속을 검증한 뒤 단계가 포함된 조회/생성 경로를 사용하세요.
📍 Affects 4 files
src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java#L5-L9(this comment)src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java#L77-L81src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java#L12-L17src/main/java/com/mr/domain/learning/service/LearningService.java#L47-L56
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java`
around lines 5 - 9, 학습 결과 저장의 식별 단위를 user·learning·learningStep으로 통일하세요.
src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java:5-9의
SaveResultDTO에 learningStepId를 추가하고,
src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java:77-81에서는
필수 learningStep 누락을 Custom Exception으로 거부하세요.
src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java:12-17의
조회·완료 집계를 user·learning·learningStep 기준으로 변경하고,
src/main/java/com/mr/domain/learning/service/LearningService.java:47-56에서 단계 존재
및 학습 소속을 검증한 뒤 단계 포함 조회·생성 경로를 사용하세요.
| public record SaveResultResultDTO( | ||
| Long userLearningProgressId, | ||
| Long userId, | ||
| Long learningId, | ||
| String status, | ||
| Integer score, | ||
| LocalDateTime completedAt | ||
| ) { | ||
| public static SaveResultResultDTO from(UserLearningProgress progress) { | ||
| return new SaveResultResultDTO( | ||
| progress.getId(), | ||
| progress.getUser().getUserId(), | ||
| progress.getLearning().getId(), | ||
| progress.getLearningStatus(), | ||
| progress.getScore(), | ||
| progress.getUpdatedAt() | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
completedAt에 일반 수정 시각을 넣지 마세요.
IN_PROGRESS 결과를 저장해도 progress.getUpdatedAt()이 completedAt으로 반환됩니다. 실제 완료 시각을 제공하려면 완료 상태로 전환되는 순간만 별도 필드에 기록하고, 그렇지 않다면 응답 필드를 lastStudiedAt 또는 updatedAt으로 변경해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/domain/learning/dto/res/LearningResultResponseDTO.java`
around lines 8 - 24, Update SaveResultResultDTO.from so completedAt is populated
only from a dedicated completion timestamp recorded when the learning status
transitions to completed; otherwise rename the response field to lastStudiedAt
or updatedAt and map progress.getUpdatedAt() to that field, avoiding use of
updatedAt as completedAt.
| public static Subscription create(User user, String tier, LocalDateTime startDate, LocalDateTime endDate) { | ||
| validateUser(user); | ||
| validateTier(tier); | ||
| validateDates(startDate, endDate); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
필수 구독 기간도 도메인 예외로 검증해 주세요.
validateDates는 두 날짜가 모두 있을 때만 순서를 검사하므로 null 시작일/종료일이 생성됩니다. 이후 영속화 시 DB/JPA 예외로 바뀌거나 isActive()에서 NPE가 발생할 수 있어, null도 전용 SubscriptionErrorStatus로 거절해야 합니다. 도메인 불변식은 DB NOT NULL 제약 전에 검증하는 편이 API 예외 계약을 지킬 수 있습니다.
수정 예시
private static void validateDates(LocalDateTime startDate, LocalDateTime endDate) {
- if (startDate != null && endDate != null && endDate.isBefore(startDate)) {
+ if (startDate == null || endDate == null) {
+ throw new GeneralException(SubscriptionErrorStatus.SUBSCRIPTION_PERIOD_REQUIRED);
+ }
+ if (endDate.isBefore(startDate)) {
throw new GeneralException(SubscriptionErrorStatus.INVALID_SUBSCRIPTION_DATE);
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/domain/subscriptions/entity/Subscription.java` around
lines 61 - 64, Update Subscription.create and its validateDates flow to reject
null startDate or endDate with the appropriate dedicated SubscriptionErrorStatus
before checking date ordering. Preserve the existing invalid-range validation
for non-null dates, ensuring required subscription periods fail as domain
exceptions rather than reaching persistence or isActive().
| @AllArgsConstructor | ||
| public enum AiServerErrorStatus implements BaseCode { | ||
|
|
||
| INVALID_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "AI_SERVER_500_01", "AI 서버 응답을 처리하지 못했습니다."), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
업스트림의 잘못된 응답은 502로 분류하세요.
INVALID_RESPONSE는 클라이언트에서 AI 서버의 빈·해석 불가 응답을 뜻하므로, 애플리케이션 내부 오류인 500보다 502가 맞습니다. 상태 변경 시 오류 코드도 502 계열로 함께 변경하세요. 502는 게이트웨이가 업스트림에서 잘못된 응답을 받은 경우를 의미합니다. (datatracker.ietf.org)
수정 예시
- INVALID_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "AI_SERVER_500_01", "AI 서버 응답을 처리하지 못했습니다."),
+ INVALID_RESPONSE(HttpStatus.BAD_GATEWAY, "AI_SERVER_502_02", "AI 서버 응답을 처리하지 못했습니다."),📝 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.
| INVALID_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "AI_SERVER_500_01", "AI 서버 응답을 처리하지 못했습니다."), | |
| INVALID_RESPONSE(HttpStatus.BAD_GATEWAY, "AI_SERVER_502_02", "AI 서버 응답을 처리하지 못했습니다."), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/global/apipayload/domain/AiServerErrorStatus.java` at
line 12, Update INVALID_RESPONSE in AiServerErrorStatus to use
HttpStatus.BAD_GATEWAY instead of INTERNAL_SERVER_ERROR, and change its error
code from the AI_SERVER_500 family to the corresponding 502-series code. Leave
the message unchanged.
| } catch (ResourceAccessException e) { | ||
| throw new GeneralException(hasTimeoutCause(e) ? AiServerErrorStatus.TIMEOUT : AiServerErrorStatus.CONNECTION_FAILED); | ||
| } catch (HttpStatusCodeException e) { | ||
| log.warn("AI 서버가 오류 응답을 반환했습니다. status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString()); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
업스트림 오류 본문을 원문 그대로 로그에 남기지 마세요.
AI 서버가 반환한 오류 본문에는 요청 데이터, 내부 스택 정보 또는 민감 정보가 포함될 수 있습니다. 상태 코드와 안전한 추적 식별자만 기록하고, 본문 로깅이 꼭 필요하면 길이 제한·마스킹을 적용하세요.
수정 예시
- log.warn("AI 서버가 오류 응답을 반환했습니다. status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString());
+ log.warn("AI 서버가 오류 응답을 반환했습니다. status={}", e.getStatusCode());📝 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.
| log.warn("AI 서버가 오류 응답을 반환했습니다. status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString()); | |
| log.warn("AI 서버가 오류 응답을 반환했습니다. status={}", e.getStatusCode()); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/global/client/ai/AiServerClient.java` at line 44, Update
the error logging in AiServerClient to stop logging e.getResponseBodyAsString()
verbatim; retain the HTTP status code and log only a safe tracing identifier, or
sanitize and length-limit the body if it must be included.
| if (StringUtils.hasText(jwt)) { | ||
| if (!tokenProvider.validateAccessToken(jwt)) { | ||
| log.warn("유효하지 않은 JWT 토큰입니다. Token: {}", jwt); | ||
| SecurityContextHolder.clearContext(); | ||
| request.setAttribute("exception", new JwtException("유효하지 않거나 만료된 토큰입니다.")); | ||
| } else { | ||
| try { | ||
| Authentication authentication = tokenProvider.getAuthentication(jwt); | ||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
| } catch (UsernameNotFoundException | NumberFormatException e) { | ||
| log.error("Security Context에 인증 정보를 저장할 수 없습니다. Token: {}, Error: {}", jwt, e.getMessage()); | ||
| SecurityContextHolder.clearContext(); | ||
| request.setAttribute("exception", e); | ||
| } catch (Exception e) { | ||
| log.error("JWT 인증 처리 중 알 수 없는 에러 발생: {}", e.getMessage()); | ||
| SecurityContextHolder.clearContext(); | ||
| request.setAttribute("exception", e); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
JWT 원문을 로그에 남기지 마세요.
만료되지 않은 토큰이 사용자 조회 실패 경로에 들어가면 원문이 로그·SIEM으로 전파될 수 있습니다. 토큰 값 대신 실패 유형, 요청 경로, 상관관계 ID만 기록해 주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/global/security/jwt/JwtAuthenticationFilter.java` around
lines 33 - 49, Update the logging in JwtAuthenticationFilter’s JWT validation
and authentication exception paths to never include the raw jwt value. Log only
the failure type, request path, and available correlation ID, while preserving
the existing context-clearing and request exception handling.
| // TODO: 추후 User 엔티티 및 UserRepository 완성 시 주입 | ||
| // private final UserRepository userRepository; | ||
|
|
||
| @Override | ||
| public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | ||
| try { | ||
| Long userId = Long.parseLong(username); | ||
|
|
||
| // TODO: User user = userRepository.findById(userId) | ||
| // .orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 사용자입니다. ID: " + userId)); | ||
|
|
||
| return new CustomUserDetails(userId, UserRole.ROLE_STUDENT); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
실제 사용자 존재 여부와 역할을 확인하지 않습니다.
유효하게 서명된 모든 숫자 subject가 존재하지 않아도 학생 권한으로 인증됩니다. UserRepository에서 사용자를 조회하고, 없으면 UsernameNotFoundException을 던지며 저장된 역할을 사용해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/mr/global/security/principal/CustomUserDetailsService.java`
around lines 12 - 23, Update loadUserByUsername in CustomUserDetailsService to
inject and use UserRepository for looking up the parsed userId. Throw
UsernameNotFoundException when no user exists, and construct CustomUserDetails
with the retrieved user’s persisted role instead of always using
UserRole.ROLE_STUDENT; preserve the existing handling for invalid numeric
usernames.
📍 개요
⛓️💥 관련 이슈
🛠️ 작업 내용
🔥 리뷰 요청 사항
✅ 체크리스트
📎 참고 사항
Summary by CodeRabbit