Skip to content

[FEAT] 학습 도메인 api 구현#45

Closed
rkdehdrbs7885-oss wants to merge 6 commits into
developfrom
feat/#32-learning-status-api
Closed

[FEAT] 학습 도메인 api 구현#45
rkdehdrbs7885-oss wants to merge 6 commits into
developfrom
feat/#32-learning-status-api

Conversation

@rkdehdrbs7885-oss

@rkdehdrbs7885-oss rkdehdrbs7885-oss commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

📍 개요

학습 도메인 api 구현

⛓️‍💥 관련 이슈


🛠️ 작업 내용

  • 학습 도메인 api 구현

🔥 리뷰 요청 사항

리뷰어가 중점적으로 확인해주었으면 하는 내용을 작성해주세요.

  • 예외 코드 처리 방식

✅ 체크리스트

  • 코드 컨벤션을 준수했습니다.
  • 불필요한 코드 및 import를 제거했습니다.
  • 예외 처리를 적용했습니다.
  • 테스트를 완료했습니다.
  • 관련 Issue를 연결했습니다.

📎 참고 사항

Summary by CodeRabbit

  • 새 기능
    • 학습 결과를 저장하고 점수와 완료 상태를 확인할 수 있습니다.
    • 학습별 진행률을 조회할 수 있습니다.
    • 점수 입력 누락 시 안내 메시지가 표시됩니다.
    • 학습 및 사용자 미존재 상황에 대한 오류 안내를 추가했습니다.
    • 점수에 따라 학습 상태가 시작 전, 재시도, 완료로 구분됩니다.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e86cd2a3-6b83-450e-bb6c-47fd3fbe2eb1

📥 Commits

Reviewing files that changed from the base of the PR and between 74f7103 and 04faef4.

📒 Files selected for processing (2)
  • src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java
  • src/main/java/com/mr/domain/user/exception/UserErrorStatus.java
💤 Files with no reviewable changes (1)
  • src/main/java/com/mr/domain/user/exception/UserErrorStatus.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java

📝 Walkthrough

Walkthrough

학습 결과 저장 및 진행률 조회 API가 추가되었습니다. 요청·응답 DTO, 사용자 연관관계와 상태 계산, JPA 저장소, 서비스 로직, 예외 상태 및 컨트롤러가 구현되었습니다.

Changes

학습 결과 및 진행률

Layer / File(s) Summary
학습 계약과 상태 모델
src/main/java/com/mr/domain/learning/dto/..., src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java, src/main/java/com/mr/domain/learning/exception/LearningErrorStatus.java, src/main/java/com/mr/domain/user/exception/UserErrorStatus.java
점수 요청 검증 및 결과·진행률 응답 DTO가 추가되었습니다. UserLearningProgressUser 연관관계를 사용하고 점수에 따라 NOT_STARTED, COMPLETED, RETRY 상태를 반환합니다.
학습 저장소와 서비스 로직
src/main/java/com/mr/domain/learning/repository/*, src/main/java/com/mr/domain/learning/service/LearningService.java, src/main/java/com/mr/domain/user/repository/UserRepository.java
학습·단계·사용자·진행 데이터 저장소가 추가되었습니다. 결과 저장 시 기존 진행 데이터를 갱신하거나 새로 저장하며, 전체 단계 수와 완료 단계 수를 기준으로 진행률을 계산합니다.
학습 API 진입점
src/main/java/com/mr/domain/learning/controller/LearningController.java
POST /api/learnings/{learningId}/resultGET /api/learnings/{learningId}/progress 엔드포인트가 추가되고 ApiResponse 형식으로 응답합니다.

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: 진행률 응답
Loading

Possibly related PRs

  • Musereview/BE#18: 동일한 UserErrorStatus 열거형의 사용자 오류 코드 변경과 관련됩니다.
  • Musereview/BE#24: UserLearningProgress의 사용자 식별자와 진행 상태 관련 설계가 겹칩니다.
  • Musereview/BE#31: UserLearningProgress.create(...) 검증과 LearningErrorStatus 변경 지점이 연결됩니다.

Poem

점수 하나 저장하고,
진행률은 둥글게 계산해요.
멈추면 시작, 아홉십이면 완료,
조금 모자라면 다시 도전!
학습의 길에 API가 켜졌네 ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 직접 이슈는 /api/learnings/{learningId}/progressPATCH 구현을 요구하지만, PR은 GET /progress로 추가해 요구사항과 불일치합니다. 진행률 API의 HTTP 메서드와 경로를 이슈 요구사항에 맞게 PATCH /api/learnings/{learningId}/progress로 수정하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 학습 도메인 API 구현이라는 핵심 변경을 간결하게 잘 요약합니다.
Out of Scope Changes check ✅ Passed 요청된 학습 결과/진행률 API, DTO, 서비스·리포지토리, 예외 처리 범위 내 변경만 보입니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#32-learning-status-api

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

JWT 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

📥 Commits

Reviewing files that changed from the base of the PR and between 274468e and 74f7103.

📒 Files selected for processing (47)
  • .gitignore
  • MR_config/local/application.example.yml
  • src/main/java/com/mr/domain/auth/controller/AuthController.java
  • src/main/java/com/mr/domain/auth/dto/AuthRequestDTO.java
  • src/main/java/com/mr/domain/auth/dto/AuthResponseDTO.java
  • src/main/java/com/mr/domain/auth/entity/SocialAuth.java
  • src/main/java/com/mr/domain/auth/entity/enums/SocialType.java
  • src/main/java/com/mr/domain/auth/service/AuthService.java
  • src/main/java/com/mr/domain/learning/controller/LearningController.java
  • src/main/java/com/mr/domain/learning/dto/req/LearningResultSaveRequestDTO.java
  • src/main/java/com/mr/domain/learning/dto/res/LearningProgressResponseDTO.java
  • src/main/java/com/mr/domain/learning/dto/res/LearningResultResponseDTO.java
  • src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java
  • src/main/java/com/mr/domain/learning/exception/LearningErrorStatus.java
  • src/main/java/com/mr/domain/learning/repository/LearningRepository.java
  • src/main/java/com/mr/domain/learning/repository/LearningStepRepository.java
  • src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java
  • src/main/java/com/mr/domain/learning/service/LearningService.java
  • src/main/java/com/mr/domain/subscriptions/entity/Subscription.java
  • src/main/java/com/mr/domain/subscriptions/exception/SubscriptionErrorStatus.java
  • src/main/java/com/mr/domain/user/entity/StudentInstrument.java
  • src/main/java/com/mr/domain/user/entity/User.java
  • src/main/java/com/mr/domain/user/entity/enums/UserRole.java
  • src/main/java/com/mr/domain/user/exception/UserErrorStatus.java
  • src/main/java/com/mr/domain/user/repository/UserRepository.java
  • src/main/java/com/mr/global/apipayload/code/CommonStatus.java
  • src/main/java/com/mr/global/apipayload/domain/AiServerErrorStatus.java
  • src/main/java/com/mr/global/client/ai/AiAnalysisRequest.java
  • src/main/java/com/mr/global/client/ai/AiServerClient.java
  • src/main/java/com/mr/global/config/AiServerProperties.java
  • src/main/java/com/mr/global/config/AiServerRestClientConfig.java
  • src/main/java/com/mr/global/config/SecurityConfig.java
  • src/main/java/com/mr/global/config/SwaggerConfig.java
  • src/main/java/com/mr/global/security/SecurityConfig.java
  • src/main/java/com/mr/global/security/SecurityUtil.java
  • src/main/java/com/mr/global/security/jwt/JwtAccessDeniedHandler.java
  • src/main/java/com/mr/global/security/jwt/JwtAuthenticationEntryPoint.java
  • src/main/java/com/mr/global/security/jwt/JwtAuthenticationFilter.java
  • src/main/java/com/mr/global/security/jwt/JwtProperties.java
  • src/main/java/com/mr/global/security/jwt/JwtTokenProvider.java
  • src/main/java/com/mr/global/security/principal/CustomUserDetails.java
  • src/main/java/com/mr/global/security/principal/CustomUserDetailsService.java
  • src/main/resources/application.example.yml
  • src/main/resources/application.yml
  • src/test/java/com/mr/global/client/ai/AiAnalysisRequestSerializationTest.java
  • src/test/java/com/mr/global/client/ai/AiServerClientTest.java
  • src/test/java/com/mr/global/config/AiServerPropertiesBindingTest.java
💤 Files with no reviewable changes (1)
  • src/main/java/com/mr/global/config/SecurityConfig.java

Comment on lines +17 to +20
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
@Profile({"local", "dev"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 전용 로그인 동작은 별도
프로필 설정 또는 구현으로 분리하고, 운영 환경에서는 실제 인증 구현이 등록되도록 구성하세요.

Comment on lines +4 to +5
kakao,
google

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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' . || true

Repository: 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' . || true

Repository: 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:


大写 경로 호환성 마이그레이션을 추가해 주세요.

/api/auth/login/{socialType}에서 @PathVariableSocialType의 선언 이름과 일치해야 합니다. 기존 클라이언트에서 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.

Comment on lines +19 to +32
@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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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-L20
  • src/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.

Comment on lines +5 to +9
public class LearningResultSaveRequestDTO {
public record SaveResultDTO(
@NotNull(message = "점수는 필수 입력값입니다.")
Integer score
) {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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-L81
  • src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java#L12-L17
  • src/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에서 단계 존재
및 학습 소속을 검증한 뒤 단계 포함 조회·생성 경로를 사용하세요.

Comment on lines +8 to +24
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()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Comment on lines +61 to 64
public static Subscription create(User user, String tier, LocalDateTime startDate, LocalDateTime endDate) {
validateUser(user);
validateTier(tier);
validateDates(startDate, endDate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 서버 응답을 처리하지 못했습니다."),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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.

Suggested change
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.

Comment on lines +33 to +49
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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.

Comment on lines +12 to +23
// 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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.

@rkdehdrbs7885-oss rkdehdrbs7885-oss changed the title Feat/#32 학습 도메인 api 구현 [Feat] 학습 도메인 api 구현 Jul 24, 2026
@ownue ownue changed the title [Feat] 학습 도메인 api 구현 [FEAT] 학습 도메인 api 구현 Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Feature - 학습 진행 상태 및 결과 저장 API 구현

1 participant