Repository navigation
[FEAT] 사용자 프로필 조회 및 관리 API 구현 #47
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
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
83602b3
feat: 프로필 Repository 6개 추가 (#39)
p1001q a2c3f0d
feat: 프로필 에러 코드 4개 추가 (#39)
p1001q 04ffe98
feat: PIANO 악기 시드 데이터 추가 (#39)
p1001q e99d1f1
feat: 프로필 조회 API 구현 (#39)
p1001q fe98864
feat: 프로필 최초 등록 API 구현 (#39)
p1001q 5d3a2c1
feat: 프로필 수정 API 구현 (#39)
p1001q e8c44c1
test: 프로필 API 컨트롤러 테스트 추가 (#39)
p1001q 4a8642a
fix: 프로필 조회 시 활성 구독만 조회하도록 수정 (#39)
p1001q 73b8684
fix: 시드 동시 실행 시 중복 저장 안전 처리 (#39)
p1001q da3f6d2
fix: 닉네임 중복 검사를 엔티티 변경 전으로 이동 (#39)
p1001q 8403213
fix: 온보딩 동시 요청 시 중복 생성 방지 (#39)
p1001q 8fdd0e3
fix: 구독등급 타입을 String으로 통일하고 값 검증 추가 (#39)
p1001q 3eca142
fix: IllegalStateException을 GeneralException으로 교체 (#39)
p1001q e445c83
fix: 시드 실행에서 트랜잭션 오염 문제 해결 (#39)
p1001q 825a92c
fix: 시드 실행에서 트랜잭션 오염 문제 해결 (#39)
p1001q 32daad5
fix: 시드 중복키 외 무결성 오류는 그대로 전파 (#39)
p1001q da5c092
fix: 구독등급 검증 순서 및 Student 저장 방식 개선 (#39)
p1001q a61f637
docs: 프로필 API 스웨거 설명 추가 (#39)
p1001q 47c6fbf
Merge remote-tracking branch 'origin/develop' into feat/#39-user-prof…
p1001q ef97044
fix: 닉네임 trim을 한 곳에서 처리하도록 정리 (#39)
p1001q 67ba529
refactor: DTO를 req/res 패키지로 분리 (#39)
p1001q 600940b
fix: 온보딩 구독 등급을 요청값 대신 서버에서 PRO로 고정 (#39)
p1001q ad9a833
fix: 닉네임/온보딩 동시성 예외를 SQLState로 구분해 처리 (#39)
p1001q f824f11
refactor: 동시성 예외 판별 분기를 긍정문으로 정리 (#39)
p1001q 7c17409
feat: completedLearningCount를 실제 집계값으로 연동 (#39)
p1001q 9a53f66
fix: 프로필 수정 API를 부분 수정 방식으로 변경 (#39)
p1001q 7d8ab2f
Merge branch 'develop' into feat/#39-user-profile-api
p1001q File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
src/main/java/com/mr/domain/statistics/repository/UserStatisticsRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.mr.domain.statistics.repository; | ||
|
|
||
| import com.mr.domain.statistics.entity.UserStatistics; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface UserStatisticsRepository extends JpaRepository<UserStatistics, Long> { | ||
|
|
||
| Optional<UserStatistics> findByUserId(Long userId); | ||
| } |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/mr/domain/subscriptions/entity/enums/SubscriptionTier.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.mr.domain.subscriptions.entity.enums; | ||
|
|
||
| public enum SubscriptionTier { | ||
| FREE, | ||
| STANDARD, | ||
| PRO | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
src/main/java/com/mr/domain/subscriptions/repository/SubscriptionRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.mr.domain.subscriptions.repository; | ||
|
|
||
| import com.mr.domain.subscriptions.entity.Subscription; | ||
| import com.mr.domain.user.entity.User; | ||
| import java.time.LocalDateTime; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface SubscriptionRepository extends JpaRepository<Subscription, Long> { | ||
|
|
||
| Optional<Subscription> findFirstByUserAndStartDateLessThanEqualAndEndDateGreaterThanEqualOrderByStartDateDesc( | ||
| User user, LocalDateTime startDateInclusive, LocalDateTime endDateInclusive); | ||
| } |
50 changes: 50 additions & 0 deletions
50
src/main/java/com/mr/domain/user/config/InstrumentSeeder.java
|
p1001q marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.mr.domain.user.config; | ||
|
|
||
| import com.mr.domain.user.entity.Instrument; | ||
| import com.mr.domain.user.repository.InstrumentRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.hibernate.exception.ConstraintViolationException; | ||
| import org.springframework.boot.ApplicationArguments; | ||
| import org.springframework.boot.ApplicationRunner; | ||
| import org.springframework.dao.DataIntegrityViolationException; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class InstrumentSeeder implements ApplicationRunner { | ||
|
|
||
| private static final String PIANO_CODE = "PIANO"; | ||
| private static final String PIANO_NAME = "피아노"; | ||
| private static final String POSTGRES_UNIQUE_VIOLATION_SQL_STATE = "23505"; | ||
|
|
||
| private final InstrumentRepository instrumentRepository; | ||
|
|
||
| // NOTE: run() 자체엔 @Transactional을 걸지 않음 — PostgreSQL은 트랜잭션 내 한 문장이라도 | ||
| // 제약 위반으로 실패하면 그 트랜잭션 전체가 abort 상태가 되어, 이후 catch로 예외를 소비해도 | ||
| // 커밋 시점에 다시 실패함. saveAndFlush() 각각이 Spring Data JPA의 자체 트랜잭션으로 독립 처리되도록 | ||
| // run() 레벨 트랜잭션을 없애서, 중복 저장 실패가 그 saveAndFlush() 호출 안에서 깔끔하게 끝나게 함. | ||
| @Override | ||
| public void run(ApplicationArguments args) { | ||
| if (instrumentRepository.findByCode(PIANO_CODE).isPresent()) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| instrumentRepository.saveAndFlush(Instrument.create(PIANO_CODE, PIANO_NAME)); | ||
| } catch (DataIntegrityViolationException e) { | ||
| if (!isDuplicateKeyViolation(e)) { | ||
| throw e; | ||
| } | ||
| // 여러 인스턴스가 동시에 기동해 경쟁 상태로 중복 저장을 시도한 경우 | ||
| // code unique 제약으로 한쪽만 성공하므로, 나머지는 무시하고 넘어감 | ||
| log.info("PIANO 악기 시드 데이터가 다른 인스턴스에 의해 이미 생성됨"); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private boolean isDuplicateKeyViolation(DataIntegrityViolationException e) { | ||
| return e.getCause() instanceof ConstraintViolationException constraintViolationException | ||
| && POSTGRES_UNIQUE_VIOLATION_SQL_STATE.equals(constraintViolationException.getSQLState()); | ||
| } | ||
| } | ||
61 changes: 61 additions & 0 deletions
61
src/main/java/com/mr/domain/user/controller/UserProfileController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.mr.domain.user.controller; | ||
|
|
||
| import com.mr.domain.user.dto.req.UserProfileRequestDTO; | ||
| import com.mr.domain.user.dto.res.UserProfileResponseDTO; | ||
| import com.mr.domain.user.service.UserProfileService; | ||
| import com.mr.global.apipayload.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PatchMapping; | ||
| 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.ResponseStatus; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/users/me/profile") | ||
| @Tag(name = "사용자 프로필", description = "프로필 조회/최초 등록(온보딩)/수정 API") | ||
| public class UserProfileController { | ||
|
|
||
| private final UserProfileService userProfileService; | ||
|
|
||
| @Operation( | ||
| summary = "프로필 조회", | ||
| description = "현재 로그인한 사용자의 닉네임, 프로필 이미지, 대표 악기, 화성학 숙련도, 구독 등급, 누적 학습 통계를 조회합니다." | ||
| ) | ||
| @GetMapping | ||
| public ApiResponse<UserProfileResponseDTO.ProfileResponse> getMyProfile() { | ||
| return ApiResponse.onSuccess(userProfileService.getMyProfile()); | ||
| } | ||
|
|
||
| @Operation( | ||
| summary = "프로필 최초 등록 (온보딩)", | ||
| description = "닉네임, 화성학 숙련도를 입력받아 Student/StudentInstrument(대표 악기 PIANO)/Subscription을 하나의 트랜잭션으로 생성합니다. " | ||
| + "구독 등급은 MVP 기간 동안 결제 없이 항상 PRO로 서버에서 고정됩니다. " | ||
| + "이미 온보딩을 완료한 사용자가 다시 호출하면 409(USER_409_02)가 반환됩니다." | ||
| ) | ||
| @PostMapping | ||
| @ResponseStatus(HttpStatus.CREATED) | ||
| public ApiResponse<UserProfileResponseDTO.OnboardingResponse> registerProfile( | ||
| @RequestBody UserProfileRequestDTO.OnboardingRequest request | ||
|
p1001q marked this conversation as resolved.
|
||
| ) { | ||
| return ApiResponse.onSuccess(userProfileService.registerProfile(request)); | ||
| } | ||
|
|
||
| @Operation( | ||
| summary = "프로필 수정", | ||
| description = "닉네임과 화성학 숙련도를 부분 수정합니다. 요청에 생략하거나 null로 보낸 필드는 기존 값을 유지합니다. " | ||
| + "대표 악기, 프로필 이미지, 구독 등급은 이 API에서 변경되지 않습니다." | ||
| ) | ||
| @PatchMapping | ||
| public ApiResponse<UserProfileResponseDTO.UpdateResponse> updateProfile( | ||
| @RequestBody UserProfileRequestDTO.UpdateRequest request | ||
| ) { | ||
| return ApiResponse.onSuccess(userProfileService.updateProfile(request)); | ||
| } | ||
| } | ||
|
p1001q marked this conversation as resolved.
|
||
25 changes: 25 additions & 0 deletions
25
src/main/java/com/mr/domain/user/dto/req/UserProfileRequestDTO.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.mr.domain.user.dto.req; | ||
|
|
||
| import com.mr.domain.user.entity.enums.TheoryLevel; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public class UserProfileRequestDTO { | ||
|
p1001q marked this conversation as resolved.
|
||
|
|
||
| @Schema(description = "프로필 최초 등록(온보딩) 요청") | ||
| public record OnboardingRequest( | ||
| @Schema(description = "닉네임 (한글/영문/숫자 2~10자)", example = "김뮤즈") | ||
| String nickname, | ||
|
|
||
| @Schema(description = "화성학 숙련도", example = "INTERMEDIATE") | ||
| TheoryLevel skillLevel | ||
| ) {} | ||
|
|
||
| @Schema(description = "프로필 수정 요청 (부분 수정 — 생략하거나 null인 필드는 기존 값 유지)") | ||
| public record UpdateRequest( | ||
| @Schema(description = "변경할 닉네임 (한글/영문/숫자 2~10자). 생략 시 기존 닉네임 유지", example = "새로운뮤즈") | ||
| String nickname, | ||
|
|
||
| @Schema(description = "변경할 화성학 숙련도. 생략 시 기존 숙련도 유지", example = "ADVANCED") | ||
| TheoryLevel skillLevel | ||
| ) {} | ||
| } | ||
|
p1001q marked this conversation as resolved.
|
||
81 changes: 81 additions & 0 deletions
81
src/main/java/com/mr/domain/user/dto/res/UserProfileResponseDTO.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package com.mr.domain.user.dto.res; | ||
|
|
||
| import com.mr.domain.user.entity.enums.TheoryLevel; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import java.time.LocalDateTime; | ||
| import lombok.Builder; | ||
|
|
||
| public class UserProfileResponseDTO { | ||
|
|
||
| @Builder | ||
| @Schema(description = "프로필 조회 응답") | ||
| public record ProfileResponse( | ||
| @Schema(description = "닉네임", example = "김뮤즈") | ||
| String nickname, | ||
|
|
||
| @Schema(description = "프로필 이미지 URL", example = "https://musereview-storage-526426842030-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/profile/default-profile.png") | ||
| String profileImgUrl, | ||
|
|
||
| @Schema(description = "대표 악기 (MVP는 PIANO 고정)", example = "PIANO") | ||
| String instrumentType, | ||
|
|
||
| @Schema(description = "화성학 숙련도", example = "INTERMEDIATE") | ||
| TheoryLevel skillLevel, | ||
|
|
||
| @Schema(description = "구독 등급", example = "PRO") | ||
| String subscriptionTier, | ||
|
|
||
| StatisticsResponse statistics | ||
| ) {} | ||
|
|
||
| @Builder | ||
| @Schema(description = "누적 학습 통계") | ||
| public record StatisticsResponse( | ||
| @Schema(description = "누적 연주 세션 수", example = "24") | ||
| Long practiceSessionCount, | ||
|
|
||
| @Schema(description = "누적 연습 시간(분)", example = "350") | ||
| Long totalPracticeMinutes, | ||
|
|
||
| @Schema(description = "완료한 학습 개수 (score 90점 이상)", example = "8") | ||
| Long completedLearningCount | ||
| ) {} | ||
|
|
||
| @Builder | ||
| @Schema(description = "프로필 최초 등록(온보딩) 응답") | ||
| public record OnboardingResponse( | ||
| @Schema(description = "사용자 ID", example = "1") | ||
| Long userId, | ||
|
|
||
| @Schema(description = "닉네임", example = "김뮤즈") | ||
| String nickname, | ||
|
|
||
| @Schema(description = "화성학 숙련도", example = "INTERMEDIATE") | ||
| TheoryLevel skillLevel, | ||
|
|
||
| @Schema(description = "대표 악기", example = "PIANO") | ||
| String instrumentType, | ||
|
|
||
| @Schema(description = "구독 등급", example = "PRO") | ||
| String subscriptionTier, | ||
|
|
||
| @Schema(description = "프로필 이미지 URL", example = "https://musereview-storage-526426842030-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/profile/default-profile.png") | ||
| String profileImgUrl | ||
| ) {} | ||
|
|
||
| @Builder | ||
| @Schema(description = "프로필 수정 응답") | ||
| public record UpdateResponse( | ||
| @Schema(description = "사용자 ID", example = "1") | ||
| Long userId, | ||
|
|
||
| @Schema(description = "수정된 닉네임", example = "새로운뮤즈") | ||
| String nickname, | ||
|
|
||
| @Schema(description = "수정된 화성학 숙련도", example = "ADVANCED") | ||
| TheoryLevel skillLevel, | ||
|
|
||
| @Schema(description = "처리 완료 시각", example = "2026-07-24T01:30:00") | ||
| LocalDateTime updatedAt | ||
| ) {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
src/main/java/com/mr/domain/user/repository/InstrumentRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.mr.domain.user.repository; | ||
|
|
||
| import com.mr.domain.user.entity.Instrument; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface InstrumentRepository extends JpaRepository<Instrument, Long> { | ||
|
|
||
| Optional<Instrument> findByCode(String code); | ||
| } |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/mr/domain/user/repository/StudentInstrumentRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.mr.domain.user.repository; | ||
|
|
||
| import com.mr.domain.user.entity.Student; | ||
| import com.mr.domain.user.entity.StudentInstrument; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface StudentInstrumentRepository extends JpaRepository<StudentInstrument, Long> { | ||
|
|
||
| Optional<StudentInstrument> findByStudentAndPrimaryTrue(Student student); | ||
| } |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/mr/domain/user/repository/StudentRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.mr.domain.user.repository; | ||
|
|
||
| import com.mr.domain.user.entity.Student; | ||
| import com.mr.domain.user.entity.User; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface StudentRepository extends JpaRepository<Student, Long> { | ||
|
|
||
| Optional<Student> findByUser(User user); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.