-
Notifications
You must be signed in to change notification settings - Fork 2
[FEAT] 학습 도메인 api 구현 #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7cedf61
647cb26
e8dda65
74f7103
7fc950d
04faef4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package com.mr.domain.learning.controller; | ||
|
|
||
| import com.mr.domain.learning.dto.req.LearningResultSaveRequestDTO; | ||
| import com.mr.domain.learning.dto.res.LearningProgressResponseDTO; | ||
| import com.mr.domain.learning.dto.res.LearningResultResponseDTO; | ||
| import com.mr.domain.learning.service.LearningService; | ||
| import com.mr.global.apipayload.ApiResponse; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/learnings") | ||
| public class LearningController { | ||
|
|
||
| private final LearningService learningService; | ||
|
|
||
| // 학습 결과 저장 | ||
| @PostMapping("/{learningId}/result") | ||
| public ApiResponse<LearningResultResponseDTO.SaveResultResultDTO> saveLearningResult( | ||
| @PathVariable Long learningId, // 임시 | ||
| @Valid @RequestBody LearningResultSaveRequestDTO.SaveResultDTO request | ||
| ){ | ||
| LearningResultResponseDTO.SaveResultResultDTO response = | ||
| learningService.saveResult(learningId, request); | ||
|
|
||
| return ApiResponse.onSuccess(response); | ||
| } | ||
|
|
||
| // 학습 진행률 조회 | ||
| @GetMapping("/{learningId}/progress") | ||
| public ApiResponse<LearningProgressResponseDTO.ProgressResultDTO> getLearningProgress( | ||
| @PathVariable Long learningId | ||
| ) { | ||
| LearningProgressResponseDTO.ProgressResultDTO result = learningService.getLearningProgress(learningId); | ||
| return ApiResponse.onSuccess(result); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.mr.domain.learning.dto.req; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| public class LearningResultSaveRequestDTO { | ||
| public record SaveResultDTO( | ||
| @NotNull(message = "점수는 필수 입력값입니다.") | ||
| Integer score | ||
| ) {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.mr.domain.learning.dto.res; | ||
|
|
||
| public class LearningProgressResponseDTO { | ||
|
|
||
| public record ProgressResultDTO( | ||
| Long learningId, | ||
| Integer progressRate | ||
| ){ | ||
| public static ProgressResultDTO of(Long learningId, Integer progressRate){ | ||
| return new ProgressResultDTO(learningId, progressRate); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.mr.domain.learning.dto.res; | ||
|
|
||
| import com.mr.domain.learning.entity.UserLearningProgress; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public class LearningResultResponseDTO { | ||
| 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() | ||
| ); | ||
|
Comment on lines
+8
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.mr.domain.learning.repository; | ||
|
|
||
| import com.mr.domain.learning.entity.Learning; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface LearningRepository extends JpaRepository<Learning, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.mr.domain.learning.repository; | ||
|
|
||
| import com.mr.domain.learning.entity.LearningStep; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface LearningStepRepository extends JpaRepository<LearningStep, Long> { | ||
| // 특정 학습에 속한 전체 단계 수 조회 | ||
| long countByLearningId(Long learningId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.mr.domain.learning.repository; | ||
|
|
||
| import com.mr.domain.learning.entity.UserLearningProgress; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface UserLearningProgressRepository extends JpaRepository<UserLearningProgress, Long> { | ||
|
|
||
| Optional<UserLearningProgress> findByUserIdAndLearningId(Long userId, Long learningId); | ||
| // 유저가 완료한(점수 80점 이상) 학습 단계 수 조회 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 완료 기준 주석을 실제 기준과 통일하세요. 주석은 80점 이상이라고 설명하지만 쿼리와 🤖 Prompt for AI Agents |
||
| @Query("SELECT COUNT(ulp) FROM UserLearningProgress ulp " + | ||
| "JOIN ulp.learningStep ls " + | ||
| "WHERE ulp.user.id = :userId AND ls.learning.id = :learningId AND ulp.score >= 90") | ||
|
Comment on lines
+12
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
USER_FILE=$(fd -a '^User\.java$' src/main/java | head -n 1)
rg -n '`@Id`|class User|private .* (id|userId)' "$USER_FILE"
rg -n 'findByUserIdAndLearningId|ulp\.user\.id' src/main/javaRepository: Musereview/BE Length of output: 736 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== repository target =="
cat -n src/main/java/com/mr/domain/learning/repository/UserLearningProgressRepository.java | sed -n '1,80p'
user_file="$(fd -a '^User\.java$' src/main/java | head -n 1)"
echo "== user model =="
cat -n "$user_file" | sed -n '1,220p'
echo "== progress model =="
progress_file="$(fd -a '^UserLearningProgress\.java$' src/main/java | head -n 1)"
cat -n "$progress_file" | sed -n '1,220p'
echo "== service usage =="
cat -n src/main/java/com/mr/domain/learning/service/LearningService.java | sed -n '1,120p'
echo "== JPA method resolution semantics evidence in code =="
python3 - <<'PY'
from pathlib import Path
import re
user_file = next(Path('src/main/java').rglob('User.java'))
progress_file = next(Path('src/main/java').rglob('UserLearningProgressRepository.java'))
for path in [user_file, progress_file]:
text = path.read_text()
print(f"-- {path} --")
for name in ['userId', 'user.setId', '.setId', 'setUserId']:
print(name, '=>' , 'YES' if name in text else 'NO')
PYRepository: Musereview/BE Length of output: 13369 🌐 Web query:
💡 Result: In Spring Data JPA, derived query methods map directly to the property names defined in your entity class [1]. The discrepancy between User.id and User.userId often arises from a misunderstanding of how the framework parses these names [2][1]. 1. Mapping Rules: When you use a method like findByUserId, Spring Data JPA parses the string after By and looks for an exact matching property name (e.g., userId) within your User entity [1]. If your entity has a field named id but you attempt to use findByUserId, the framework will throw a PropertyReferenceException because it cannot find a property named userId [2]. 2. Identifier Property (id): The property marked with Citations:
🌐 Web query:
💡 Result: In Spring Data Commons, the term "id" does not function as a shorthand for all identifier properties in property paths, nor is it a globally reserved word in the context of JDOQL (JDO Query Language) [1][2][3]. Spring Data Commons In Spring Data repositories, repository methods like findById are reserved keywords specifically bound to the entity's identifier property (defined by Citations:
🌐 Web query:
💡 Result: Based on a comprehensive review of the Spring Framework, JDOQL, and DataNucleus documentation, there is no public API, constant, or identifier known as ID_IS_RESERVED_FOR_PROPERTY_PATH_SHORTHAND within the Spring Framework or the official JDO/JDOQL specifications [1][2][3][4]. While the Spring Framework extensively uses terms like "property path" (e.g., in Citations:
🌐 Web query:
💡 Result: The identifier ID_IS_RESERVED_FOR_PROPERTY_PATH_SHORTHAND does not appear in the provided search results for the spring-projects/spring-data-commons repository. However, the provided documentation confirms that Spring Data repositories use reserved method names that target the identifier property (the field annotated with Citations:
🌐 Web query:
💡 Result: Based on the provided search results, there is no evidence of a class named PathExpressionResolver or a constant named ID_IS_RESERVED_FOR_PROPERTY_PATH_SHORTHAND within the spring-projects/spring-data-commons repository. The search results show that while Spring Framework contains classes like PathResourceResolver [1][2] for handling resource paths, and Symfony's PropertyAccess component utilizes a PropertyPath class [3] for path manipulation, the specific identifier and class name provided do not appear in official Spring Data Commons documentation or codebase references found in the search. Citations:
🌐 Web query:
💡 Result: The term ID_IS_RESERVED_FOR_PROPERTY_PATH_SHORTHAND does not appear to be a standard, universally recognized programming term, language specification, or error message across major software ecosystems. Research indicates that it is likely an internal identifier, constant, or diagnostic message specific to a particular compiler, transpiler, or data processing library—most likely one involved in handling JavaScript/TypeScript object property shorthand syntax [1][2] or complex property path resolution [3][4]. In the context of JavaScript tooling (such as Babel or Next.js compilers), internal logic often distinguishes between standard object properties and "shorthand" properties (e.g., { id } vs { id: id }) [1][2]. Compilers and transpilers may use internal flags or restricted identifiers to ensure these properties are processed correctly, especially when performing code transformations, scope renaming, or compiling server-side actions [1][5][2]. If you are encountering this string in an error log or code base: 1. It is likely a diagnostic artifact from a build tool or compiler plugin. 2. It suggests that a variable name or property key named id is colliding with an internal mechanism reserved for processing property path shortcuts or shorthand object syntax [1][5][6]. 3. To resolve such errors, you may need to rename the property (e.g., change id to entityId) to avoid conflicts with the framework's internal reserved namespaces [5][7][8]. For authoritative guidance, search the specific GitHub repository or documentation for the software framework (e.g., Next.js, Babel, or a specific database client) where this string appears, as it is almost certainly implementation-specific [1][5][2][8]. Citations:
사용자 연관관계의 실제 식별자 경로를 맞춰 주세요.
🤖 Prompt for AI Agents |
||
| long countCompletedStepsByUserIdAndLearningId(@Param("userId") Long userId, @Param("learningId") Long learningId); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package com.mr.domain.learning.service; | ||
|
|
||
| import com.mr.domain.learning.dto.req.LearningResultSaveRequestDTO; | ||
| import com.mr.domain.learning.dto.res.LearningProgressResponseDTO; | ||
| import com.mr.domain.learning.dto.res.LearningResultResponseDTO; | ||
| import com.mr.domain.learning.entity.Learning; | ||
| import com.mr.domain.learning.entity.UserLearningProgress; | ||
| import com.mr.domain.learning.exception.LearningErrorStatus; | ||
| import com.mr.domain.learning.repository.LearningRepository; | ||
| import com.mr.domain.learning.repository.LearningStepRepository; | ||
| import com.mr.domain.learning.repository.UserLearningProgressRepository; | ||
| import com.mr.domain.user.entity.User; | ||
| import com.mr.domain.user.exception.UserErrorStatus; | ||
| import com.mr.domain.user.repository.UserRepository; | ||
| import com.mr.global.apipayload.exception.GeneralException; | ||
| import com.mr.global.security.SecurityUtil; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
|
Comment on lines
+23
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== candidate file =="
fd -a 'LearningService.java' . || true
echo "== file outline/stat =="
if [ -f src/main/java/com/mr/domain/learning/service/LearningService.java ]; then
wc -l src/main/java/com/mr/domain/learning/service/LearningService.java
ast-grep outline src/main/java/com/mr/domain/learning/service/LearningService.cpp --view compact 2>/dev/null || true
ast-grep outline src/main/java/com/mr/domain/learning/service/LearningService.java --view expanded || true
fi
echo "== relevant file sections =="
sed -n '1,220p' src/main/java/com/mr/domain/learning/service/LearningService.java | cat -n
echo "== related persistence classes references =="
rg -n "class .*Progress|interface .*Progress|create\\(|saveResult|readOnly|Transactional" src/main/java/com/mr -S || trueRepository: Musereview/BE Length of output: 8805 쓰기 메서드에서 클래스 수준 권장 수정+ `@Transactional`
public LearningResultResponseDTO.SaveResultResultDTO saveResult(🤖 Prompt for AI Agents |
||
| public class LearningService { | ||
|
|
||
| private final UserLearningProgressRepository userLearningProgressRepository; | ||
| private final LearningStepRepository learningStepRepository; | ||
| private final LearningRepository learningRepository; | ||
| // 임시 작명 | ||
| private final UserRepository userRepository; | ||
|
|
||
| // 학습 결과 저장 | ||
| public LearningResultResponseDTO.SaveResultResultDTO saveResult( | ||
| Long learningId, | ||
| LearningResultSaveRequestDTO.SaveResultDTO request | ||
| ){ | ||
| Long userId = SecurityUtil.getCurrentUserId(); | ||
|
|
||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(()-> new GeneralException(UserErrorStatus.USER_NOT_FOUND)); | ||
|
|
||
| Learning learning = learningRepository.findById(learningId) | ||
| .orElseThrow(() -> new GeneralException(LearningErrorStatus.LEARNING_NOT_FOUND)); | ||
|
|
||
| UserLearningProgress progress = userLearningProgressRepository | ||
| .findByUserIdAndLearningId(userId, learning.getId()) | ||
| .map(p -> { | ||
| p.updateProgress(request.score(), LocalDateTime.now()); | ||
| return p; | ||
| }) | ||
| .orElseGet(() -> { | ||
| UserLearningProgress newProgress = UserLearningProgress.create(user, learning, null); | ||
| newProgress.updateProgress(request.score(), LocalDateTime.now()); | ||
| return userLearningProgressRepository.save(newProgress); | ||
| }); | ||
| return LearningResultResponseDTO.SaveResultResultDTO.from(progress); | ||
| } | ||
|
|
||
| // 학습 진행률 조회 로직 | ||
| public LearningProgressResponseDTO.ProgressResultDTO getLearningProgress(Long learningId) { | ||
| // 학습 존재 여부 확인 | ||
| if (!learningRepository.existsById(learningId)) { | ||
| throw new GeneralException(LearningErrorStatus.LEARNING_NOT_FOUND); | ||
| } | ||
|
|
||
| // 현재 로그인한 유저 ID 획득 (SecurityUtil 활용) | ||
| Long userId = SecurityUtil.getCurrentUserId(); | ||
|
|
||
| // 전체 학습 단계 수 조회 | ||
| long totalStepCount = learningStepRepository.countByLearningId(learningId); | ||
|
|
||
| // 완료한 학습 단계 수 조회 | ||
| long completedStepCount = userLearningProgressRepository.countCompletedStepsByUserIdAndLearningId(userId, learningId); | ||
|
|
||
| // 진행률 계산 (0으로 나누기 예외 방지) | ||
| int progressRate = totalStepCount == 0 ? 0 : (int) Math.round((double) completedStepCount / totalStepCount * 100); | ||
|
|
||
| return LearningProgressResponseDTO.ProgressResultDTO.of(learningId, progressRate); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.mr.domain.user.repository; | ||
|
|
||
| import com.mr.domain.user.entity.User; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface UserRepository extends JpaRepository<User, Long> { | ||
| } |
There was a problem hiding this comment.
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
🗄️ 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