Conversation
- 마이페이지 기능을 구현했습니다. - 유저 닉네임, 최고 점수, 최고 등수를 확인할 수 있습니다. - 유저의 현재 타이핑 이력을 확인할 수 있습니다. - 유저의 주간 활동 즉, 주간 최고 점수들로 이루어진 꺾은 그래프를 확인할 수 있습니다. issue #51
- 마이 페이지에 사용되는 데이터를 정확하게 불러오는지 테스트합니다. - 타이핑 이력이 최신순으로 정렬되어있는지 테스트합니다. - 주간 최고 점수 데이터들로만 반환하는지 테스트합니다. issue #51
- 주간 최고 기록의 의미가 다소 명확하지 않아 일별 최고 기록으로 내용을 변경했습니다. - 이에 대한 서비스 로직 및 조회 쿼리도 변경했습니다. issue #51
There was a problem hiding this comment.
Pull request overview
마이페이지 API를 추가해 사용자 기본 정보(닉네임/총 타이핑 수/최고점/현재 랭킹), 타이핑 이력, 그리고 최근 90일 일별 최고점(daily scores) 데이터를 조회할 수 있도록 구성한 PR입니다. (이슈 #51의 Member 도메인 “마이페이지 기능” 항목에 해당)
Changes:
/api/v1/mypage엔드포인트 및MyPageService조회 로직 추가- 타이핑 랭킹 산정용
TypingRepository.findRanking네이티브 쿼리 변경(스코어 기준) - 마이페이지 응답 DTO 및 서비스 테스트 추가
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/dasi/typing/api/controller/mypage/MyPageController.java | 마이페이지 조회 API 엔드포인트 추가 |
| src/main/java/dasi/typing/api/service/mypage/MyPageService.java | 마이페이지 집계 로직(기본정보/이력/일별 최고점/랭킹 fallback) 구현 |
| src/main/java/dasi/typing/api/controller/mypage/response/MyPageResponse.java | 마이페이지 응답 스키마(typingHistories, dailyScores 포함) 추가 |
| src/main/java/dasi/typing/api/controller/mypage/response/TypingHistoryResponse.java | 타이핑 이력 응답 DTO 추가 |
| src/main/java/dasi/typing/api/controller/mypage/response/DailyScoreResponse.java | 일별 최고점 응답 DTO 추가 |
| src/main/java/dasi/typing/api/controller/mypage/response/WeeklyScoreResponse.java | 주간 최고점 DTO 추가(현재 코드에서는 미사용) |
| src/main/java/dasi/typing/domain/typing/TypingRepository.java | 랭킹 계산 쿼리 수정 |
| src/test/java/dasi/typing/api/service/mypage/MyPageServiceTest.java | 마이페이지 서비스 통합 테스트 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| @JsonFormat(pattern = "yyyy-MM-dd") | ||
| private final LocalDateTime createdDate; |
There was a problem hiding this comment.
createdDate 타입이 LocalDateTime인데 @JsonFormat(pattern = "yyyy-MM-dd")로 날짜만 직렬화하도록 되어 있어 시간 정보가 손실됩니다. 의도가 날짜만 노출이라면 필드를 LocalDate로 바꾸고, 시간까지 필요하다면 패턴을 date-time 형식으로 수정해 주세요.
| public record WeeklyScoreResponse( | ||
| @JsonFormat(pattern = "yyyy-MM-dd") | ||
| LocalDate weekStartDate, | ||
| Integer highestScore | ||
| ) { | ||
|
|
||
| @Builder | ||
| public WeeklyScoreResponse(LocalDate weekStartDate, Integer highestScore) { | ||
| this.weekStartDate = weekStartDate; | ||
| this.highestScore = highestScore; | ||
| } | ||
| } |
There was a problem hiding this comment.
WeeklyScoreResponse가 추가됐지만 현재 main 코드에서 참조되는 곳이 없어(dead code) DailyScoreResponse/dailyScores와 혼재되어 혼란을 줄 수 있습니다. 주간 API를 제공하지 않는 방향이라면 이 DTO를 제거하고, 제공할 계획이라면 MyPageResponse/서비스 로직과 함께 실제로 사용되도록 연결해 주세요.
| public record WeeklyScoreResponse( | |
| @JsonFormat(pattern = "yyyy-MM-dd") | |
| LocalDate weekStartDate, | |
| Integer highestScore | |
| ) { | |
| @Builder | |
| public WeeklyScoreResponse(LocalDate weekStartDate, Integer highestScore) { | |
| this.weekStartDate = weekStartDate; | |
| this.highestScore = highestScore; | |
| } | |
| } | |
| // WeeklyScoreResponse DTO was removed because it was not referenced anywhere in the main codebase | |
| // and could cause confusion alongside DailyScoreResponse/dailyScores. | |
| // If a weekly API is introduced in the future, reintroduce an appropriate DTO and wire it into | |
| // MyPageResponse and the corresponding service logic. |
| // then | ||
| assertThat(response.nickname()).isEqualTo("빈데이터유저"); | ||
| assertThat(response.totalTypingCount()).isZero(); | ||
| assertThat(response.highestScore()).isNull(); | ||
| assertThat(response.currentRanking()).isNull(); | ||
| assertThat(response.typingHistories()).isEmpty(); | ||
| assertThat(response.weeklyScores()).isEmpty(); | ||
| } |
There was a problem hiding this comment.
MyPageResponse는 dailyScores()를 제공하는데, 테스트에서는 weeklyScores()를 호출하고 있어 컴파일이 깨집니다. 서비스/응답 DTO에 맞춰 테스트를 dailyScores() 기반으로 수정하거나, 응답 DTO를 weeklyScores로 유지하려면 MyPageResponse/MyPageService 쪽을 함께 변경해 일관성을 맞춰주세요.
|
|
||
| // then | ||
| assertThat(weeklyScores).isNotEmpty(); | ||
| assertThat(weeklyScores).extracting(WeeklyScoreResponse::getHighestScore) |
There was a problem hiding this comment.
WeeklyScoreResponse는 record라서 접근자가 highestScore() 형태인데, 테스트에서 WeeklyScoreResponse::getHighestScore를 참조하고 있어 컴파일이 깨집니다. record 접근자에 맞게 수정하거나 DTO를 class(+getter)로 변경해 주세요.
| assertThat(weeklyScores).extracting(WeeklyScoreResponse::getHighestScore) | |
| assertThat(weeklyScores).extracting(WeeklyScoreResponse::highestScore) |
| @Test | ||
| @DisplayName("주간 최고 점수 데이터가 반환된다.") | ||
| void getMyPageWeeklyScoresTest() { | ||
| // given | ||
| Member member = memberRepository.save(new Member("kakao_weekly", "주간테스트")); | ||
| Phrase phrase = phraseRepository.save(createPhrase("주간 점수 테스트 문장")); | ||
|
|
||
| typingRepository.save(createTyping(100, 0.90, 20, 120, member, phrase)); | ||
| typingRepository.save(createTyping(300, 0.95, 60, 320, member, phrase)); | ||
| typingRepository.save(createTyping(200, 0.88, 40, 220, member, phrase)); | ||
|
|
||
| // when | ||
| MyPageResponse response = myPageService.getMyPage("kakao_weekly"); | ||
| List<WeeklyScoreResponse> weeklyScores = response.weeklyScores(); | ||
|
|
||
| // then | ||
| assertThat(weeklyScores).isNotEmpty(); | ||
| assertThat(weeklyScores).extracting(WeeklyScoreResponse::getHighestScore) | ||
| .allMatch(score -> score > 0); | ||
| } |
There was a problem hiding this comment.
현재 서비스는 일별 최고점(dailyScores)을 반환하는데 테스트 케이스는 ‘주간’ 최고점/WeeklyScoreResponse를 가정하고 있습니다. 리팩터링 의도(주간→일별)에 맞춰 테스트명/검증 대상 DTO를 DailyScoreResponse로 변경하고, 반환 리스트가 날짜 오름차순/일별 1개로 집계되는지도 함께 검증하는 편이 좋습니다.
| SELECT COUNT(DISTINCT t.score) + 1 AS ranking | ||
| FROM typing t | ||
| WHERE t.score > :targetScore | ||
| """, nativeQuery = true) | ||
| Long findRanking(@Param("targetScore") int targetScore); |
There was a problem hiding this comment.
findRanking이 이제 점수만으로 랭킹을 계산(COUNT(DISTINCT t.score))해서 동점(같은 score)에서 max_cpm/acc tie-break가 사라졌습니다. Redis/DB의 다른 랭킹 로직은 (score, maxCpm, acc)로 정렬하고 있어 결과가 불일치할 수 있으니, 메서드 시그니처에 targetMaxCpm, targetAcc를 추가해 튜플 비교로 계산하거나(이전 의도 유지), 아니면 메서드/응답 필드명을 ‘scoreRank’처럼 명확히 바꿔 의미를 고정해 주세요.
| SELECT COUNT(DISTINCT t.score) + 1 AS ranking | |
| FROM typing t | |
| WHERE t.score > :targetScore | |
| """, nativeQuery = true) | |
| Long findRanking(@Param("targetScore") int targetScore); | |
| SELECT COUNT(DISTINCT t.score, t.max_cpm, t.acc) + 1 AS ranking | |
| FROM typing t | |
| WHERE t.score > :targetScore | |
| OR (t.score = :targetScore AND t.max_cpm > :targetMaxCpm) | |
| OR (t.score = :targetScore AND t.max_cpm = :targetMaxCpm AND t.acc > :targetAcc) | |
| """, nativeQuery = true) | |
| Long findRanking( | |
| @Param("targetScore") int targetScore, | |
| @Param("targetMaxCpm") int targetMaxCpm, | |
| @Param("targetAcc") double targetAcc | |
| ); |
| private List<TypingHistoryResponse> getTypingHistories(Member member) { | ||
| List<Typing> typings = typingRepository.findByMemberOrderByCreatedDateDesc(member); | ||
| return typings.stream() | ||
| .map(TypingHistoryResponse::from) | ||
| .toList(); | ||
| } |
There was a problem hiding this comment.
getTypingHistories에서 typing.getPhrase().getSentence()를 접근하면서 Typing.phrase가 LAZY라면 타이핑 건수만큼 추가 쿼리가 발생(N+1)합니다. TypingRepository에 phrase를 fetch join 하거나, 필요한 컬럼만 조회하는 projection DTO 쿼리를 추가해서 한 번의 쿼리로 가져오도록 개선하는 게 좋습니다.
#️⃣ 연관된 이슈
#51
📝 작업 내용
Feat
Refactor
Test
💬 리뷰 요구사항