Skip to content

fix: 나의 성장 조회시 발생하는 오류#95

Merged
ohhamma merged 31 commits intodevelopfrom
fix/94-log-growth
Aug 30, 2025
Merged

fix: 나의 성장 조회시 발생하는 오류#95
ohhamma merged 31 commits intodevelopfrom
fix/94-log-growth

Conversation

@ohhamma
Copy link
Member

@ohhamma ohhamma commented Aug 29, 2025

🌱 관련 이슈

📌 작업 내용 및 특이사항

  • querydsl6으로 업그레이드
  • 명시적 캐스팅으로 sum() 집계시 타입 추론 가능하도록 수정
  • sum 결과값이 null인 경우 각 필드값에 null 들어가도록 수정

📝 참고사항

📚 기타

Summary by CodeRabbit

  • 신규 기능
    • 없음
  • 버그 수정
    • 이번달/지난달 경계 기준을 명확히 적용해 월별 변화 수치가 잘못 표기되던 문제를 수정했습니다.
    • 집계 그룹화 및 최댓값/최솟값 선정 로직을 개선해 누락 사례와 결과 정확성을 향상시켰습니다.
  • 리팩터링
    • 월별 변화 계산·집계 로직을 단순화·통합해 일관성과 안정성을 높였습니다.
    • 응답 DTO 필드명이 변경되어 연동 확인이 필요합니다 (change → monthlyChange, 원시 타입→참조 타입).
  • 기타(Chores)
    • Querydsl 관련 빌드 의존성 버전을 업데이트했습니다.

@ohhamma ohhamma self-assigned this Aug 29, 2025
@ohhamma ohhamma added the 🐛 fix 버그 및 에러 픽스 label Aug 29, 2025
@gemini-code-assist
Copy link

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai
Copy link

coderabbitai bot commented Aug 29, 2025

Walkthrough

노트 생성 시점을 이번달/지난달으로 분류해 Case 표현식으로 +1/−1/0을 계산하고 액션별로 합산(monthlyChange)한 뒤 Tuple로 받아 매핑해 최대/최솟값을 선택하도록 리포지토리 쿼리와 DTO 필드명이 변경되었고, Querydsl 관련 Gradle 의존성이 변경되었습니다.

Changes

Cohort / File(s) Summary
Repository: monthly-change 쿼리 리팩토링
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java
CaseBuilder 기반 monthlyChange 도입(이번달: +1, 지난달: -1, 그외: 0); monthlyChangeSum 집계(.sumLong().coalesce(0L)); select(action, monthlyChangeSum)groupBy(action.id)List<Tuple> 수신; Tuple → ActionChange 매핑; max/min 선택 로직 도입.
DTO: ActionChange 레코드 필드명 변경
src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java
레코드 필드명 변경: changemonthlyChange (Long) 및 접근자명 monthlyChange()로 갱신.
DTO: SimpleActionChange 레코드 필드명/타입 변경
src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java
필드명/타입 변경: change(long) → monthlyChange(Long); from(ActionChange)에서 널 처리 추가 및 actionChange.monthlyChange() 사용으로 매핑 업데이트.
Build: Querydsl 의존성 변경
build.gradle
Querydsl 라이브러리 버전/그룹 변경: com.querydsl:querydsl-jpa:5.0.0:jakartaio.github.openfeign.querydsl:querydsl-jpa:6.10.1 및 어노테이션 프로세서 io.github.openfeign.querydsl:querydsl-apt:6.10.1:jpa로 변경.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Service
  participant Repo as NoteCustomRepositoryImpl
  participant DB as Database

  rect rgba(0,128,96,0.06)
  Service->>Repo: findMostIncreasedPositiveActionChange(memberId, roomId)
  Repo->>DB: SELECT action.id, SUM(CASE WHEN createdThisMonth THEN 1 WHEN createdLastMonth THEN -1 ELSE 0 END) AS monthlyChangeSum\nFROM note JOIN action ... WHERE room.id=? AND receiver.id=? AND actionCategory.emotionType=? GROUP BY action.id
  DB-->>Repo: List<Tuple>
  Repo->>Repo: map Tuple -> new ActionChange(action, monthlyChangeSum)
  Repo->>Repo: results.stream().max(Comparator.comparing(ActionChange::monthlyChange)).orElse(null)
  Repo-->>Service: ActionChange or null
  end

  rect rgba(0,64,128,0.04)
  Service->>Repo: findMostDecreasedNegativeActionChange(memberId, roomId)
  Repo->>DB: (same query)
  DB-->>Repo: List<Tuple>
  Repo->>Repo: map Tuple -> new ActionChange(...)
  Repo->>Repo: results.stream().min(Comparator.comparing(ActionChange::monthlyChange)).orElse(null)
  Repo-->>Service: ActionChange or null
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Hibernate InterpretationException 발생 쿼리 수정 / GET /log/growth 오류 해결 [#94]

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Querydsl dependency/group 변경 (build.gradle) Gradle 의존성 변경은 쿼리 로직 수정과는 별개로 보이며, 이 이슈(#94)의 쿼리 해석 오류 직접 수정 목적과 명확히 연결되어 있지 않음.

Possibly related PRs

  • feat: 나의 성장 조회 #70 — 유사한 NoteCustomRepositoryImpl 및 ActionChange/SimpleActionChange 관련 변경이 포함되어 있어 코드 레벨 연관성 높음.

Suggested reviewers

  • duol9

📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d41e4d5 and 5e66d65.

📒 Files selected for processing (3)
  • build.gradle (1 hunks)
  • src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java (1 hunks)
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • build.gradle
  • src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-deploy
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/94-log-growth

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (1)

204-219: 지난달 구간 상한 계산 오프바이원 버그 가능성

today가 말일인 경우 지난달 마지막 날이 포함되지 않거나, 달 길이 차이(31↔30↔28/29)에 따라 구간 길이가 어긋날 수 있습니다. YearMonth로 동일한 일수 기준(1일~min(오늘 일, 지난달 말일))의 [시작, 다음날 시작] 범위를 명확히 계산하세요.

-    private NumberExpression<Long> countLastMonthNotes(LocalDate today) {
-        LocalDate lastMonthOfToday = today.minusMonths(1);
-
-        LocalDateTime startOfLastMonth = lastMonthOfToday.withDayOfMonth(1).atStartOfDay();
-        LocalDateTime endOfLastMonth = lastMonthOfToday.plusDays(1).atStartOfDay();
-        if (today.getDayOfMonth() == today.lengthOfMonth()) {
-            endOfLastMonth =
-                    lastMonthOfToday.with(TemporalAdjusters.lastDayOfMonth()).atStartOfDay();
-        }
+    private NumberExpression<Long> countLastMonthNotes(LocalDate today) {
+        java.time.YearMonth lastYm = java.time.YearMonth.from(today).minusMonths(1);
+        LocalDateTime startOfLastMonth = lastYm.atDay(1).atStartOfDay();
+        int day = Math.min(today.getDayOfMonth(), lastYm.lengthOfMonth());
+        LocalDateTime endOfLastMonth = lastYm.atDay(day).plusDays(1).atStartOfDay();
         return new CaseBuilder()
                 .when(note.createdAt.goe(startOfLastMonth).and(note.createdAt.lt(endOfLastMonth)))
                 .then(1L)
                 .otherwise(0L)
                 .sum();
     }

추가로 import java.time.YearMonth;를 파일 상단에 추가하세요.

🧹 Nitpick comments (3)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (3)

81-90: 해석 오류(InterpretationException) 원인 제거에 적합한 변경

타입 안전한 new QActionChange(action, increaseCount) 사용으로 Hibernate 타입 추론 이슈를 회피합니다. 다만 Action 엔티티 전체를 가져오므로 과도 로딩/지연초기화 이슈 가능성을 고려해, 응답에 필요한 필드만 스칼라로 투영하는 전용 DTO도 검토해 주세요(예: action.text, change).


101-110: MOST decreased 계산 투영 방식 적절

new QActionChange(action, decreaseCount) 전환 적절합니다. 동률 발생 시 결정성을 위해 보조 정렬 기준(예: action.id.asc()) 추가를 고려해 주세요.

-                .orderBy(decreaseCount.asc())
+                .orderBy(decreaseCount.asc(), action.id.asc())

193-201: 네이밍 혼동: endOfThisMonth는 ‘내일 0시’입니다

실제 의미는 “오늘 다음날 0시”이므로 변수명을 정정하면 오독을 줄일 수 있습니다.

-        LocalDateTime endOfThisMonth = today.plusDays(1).atStartOfDay();
+        LocalDateTime endOfTodayPlusOne = today.plusDays(1).atStartOfDay();
...
-                .when(note.createdAt.goe(startOfThisMonth).and(note.createdAt.lt(endOfThisMonth)))
+                .when(note.createdAt.goe(startOfThisMonth).and(note.createdAt.lt(endOfTodayPlusOne)))
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e5b9e0d and bb29cba.

📒 Files selected for processing (3)
  • src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java (1 hunks)
  • src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java (1 hunks)
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-deploy
🔇 Additional comments (3)
src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java (1)

5-5: NPE 방지: from 메서드에서 actionChange 또는 actionChange.getAction()이 null일 경우 NPE 발생
호출부가 절대 null을 넘기지 않는지 확인하거나, 그렇지 않다면 해당 메서드에 방어 로직(Null 체크 및 기본값 처리)을 추가하세요.

src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (1)

136-152: 루프 스텝을 7일 단위로 변경 검토 요청

루프에서 endDateTime을 하루 단위가 아닌 1주 단위로 감소하도록 수정이 필요해 보입니다.

- endDateTime = endDateTime.minusDays(1);
+ endDateTime = endDateTime.minusWeeks(1);

UI/요구사항이 일별 슬라이딩 윈도우인지, 비중첩 주별(7일) 집계인지 확인 부탁드립니다.

src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java (1)

7-18: ActionChange 클래스: 필드를 final로 선언하고 @EqualsAndHashCode·@tostring 추가
필드 actionchangefinal로 선언하고 Lombok의 @EqualsAndHashCode, @ToString을 추가하면 값 객체로서의 불변성·동등성을 보장합니다. QueryDSL @QueryProjection과도 충돌이 없습니다.

  • change가 절대 null이 아니라면 Long 대신 원시형 long 사용을 검토하세요. null 가능성이 있다면 현 상태를 유지하십시오.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (1)

8-8: QActionChange 사용 시 APT(Annotation Processor) 설정/생성물 확인 필요

@QueryProjection 기반 QActionChange 사용은 적절합니다. 다만 빌드 스크립트에 QueryDSL APT 설정이 없으면 컴파일 실패합니다. 이전 코멘트에서도 동일 지적이 있었으므로 재확인 바랍니다.

다음 스크립트로 Gradle 설정 및 사용처를 점검해 주세요:

#!/bin/bash
# QueryDSL APT 설정 및 QActionChange 사용 여부 확인
rg -n -C2 -g 'build.gradle*' -P 'annotationProcessor\\s+["'\'']com\\.querydsl:querydsl-apt.*(jakarta|jpa)["'\'']|kapt\\s+["'\'']com\\.querydsl:querydsl-apt.*(jakarta|jpa)["'\'']'
rg -n -C2 -P '\bQActionChange\b' src/main/java

필요하시면 Gradle(Groovy/KTS) 설정 스니펫을 만들어 드리겠습니다.

🧹 Nitpick comments (1)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (1)

25-25: 정렬 전략 확정 후 불필요한 import 정리

현재 Comparator는 스트림 정렬에만 사용됩니다. DB 정렬로 복원하거나 max/min로 대체할 경우 import가 불필요해질 수 있으니 최종 전략에 맞춰 정리해 주세요.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 93bbab4 and 27229c1.

📒 Files selected for processing (1)
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-deploy

Comment on lines +105 to +115
List<ActionChange> results = queryFactory
.select(new QActionChange(action, decreaseCount.longValue()))
.from(note)
.join(note.action, action)
.join(action.actionCategory, actionCategory)
.where(isThisRoom(roomId)
.and(isReceiver(memberId))
.and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
.groupBy(action)
.orderBy(decreaseCount.asc())
.fetchFirst();
.fetch();

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

NEGATIVE 구간도 DB 정렬로 일관되게 처리하고 동순위 안정화

위와 동일한 이유로 인메모리 정렬 대신 DB에서 최소값 1건만 조회하세요. 동순위는 action.id로 안정화합니다.

-        List<ActionChange> results = queryFactory
-                .select(new QActionChange(action, decreaseCount.longValue()))
+        return queryFactory
+                .select(new QActionChange(action, decreaseCount.longValue()))
                 .from(note)
                 .join(note.action, action)
                 .join(action.actionCategory, actionCategory)
                 .where(isThisRoom(roomId)
                         .and(isReceiver(memberId))
                         .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
-                .groupBy(action)
-                .fetch();
-
-        return results.stream()
-                .sorted(Comparator.comparing(ActionChange::getChange))
-                .findFirst()
-                .orElse(null);
+                .groupBy(action)
+                .orderBy(decreaseCount.longValue().asc(), action.id.asc())
+                .fetchFirst();
  • 인메모리로 유지한다면 다음처럼 O(n) min()을 사용하세요:
-        return results.stream()
-                .sorted(Comparator.comparing(ActionChange::getChange))
-                .findFirst()
-                .orElse(null);
+        return results.stream()
+                .min(Comparator.comparing(ActionChange::getChange))
+                .orElse(null);

Also applies to: 116-120


다음 스크립트를 실행하여 decreaseCount 정의 및 queryFactory 타입을 확인하겠습니다.


🏁 Script executed:

# 1. 클래스 헤더 및 import 확인
sed -n '1,200p' src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java

# 2. decreaseCount 정의 위치 검색
rg -n "decreaseCount" -C3 src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java

# 3. queryFactory 선언부 검색
rg -n "queryFactory" -C3 src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java

Length of output: 12325


DB 정렬·fetchFirst()로 in-memory 정렬 제거 및 동순위 안정화
findMostDecreasedNegativeActionChange 메서드에서 현재 결과를 모두 조회(fetch)한 뒤 Java 스트림으로 정렬하는 대신, DB 쿼리 단계에서 ORDER BY decreaseCount ASC, action.id ASCfetchFirst()를 사용해 최소값 1건만 조회하세요.

-    List<ActionChange> results = queryFactory
-            .select(new QActionChange(action, decreaseCount.longValue()))
-            .from(note)
-            .join(note.action, action)
-            .join(action.actionCategory, actionCategory)
-            .where(isThisRoom(roomId)
-                    .and(isReceiver(memberId))
-                    .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
-            .groupBy(action)
-            .fetch();
-
-    return results.stream()
-            .sorted(Comparator.comparing(ActionChange::getChange))
-            .findFirst()
-            .orElse(null);
+    return queryFactory
+            .select(new QActionChange(action, decreaseCount.longValue()))
+            .from(note)
+            .join(note.action, action)
+            .join(action.actionCategory, actionCategory)
+            .where(isThisRoom(roomId)
+                    .and(isReceiver(memberId))
+                    .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
+            .groupBy(action)
+            .orderBy(decreaseCount.asc(), action.id.asc())
+            .fetchFirst();

—인메모리 처리를 유지해야 한다면, 스트림 정렬 대신 min()을 사용하세요:

-    return results.stream()
-            .sorted(Comparator.comparing(ActionChange::getChange))
-            .findFirst()
-            .orElse(null);
+    return results.stream()
+            .min(Comparator.comparing(ActionChange::getChange))
+            .orElse(null);

동일한 로직을 findMostIncreasedPositiveActionChange 에도 적용하세요 (increaseCount.desc(), action.id.asc() + fetchFirst()).

🤖 Prompt for AI Agents
In
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java
around lines 105 to 115, the query currently fetches all ActionChange rows and
sorts in-memory; change the QueryDSL query to order at the DB level and return
only the first row by adding .orderBy(decreaseCount.asc(), action.id.asc()) and
using .fetchFirst() so the database returns the single minimum entry; apply the
same pattern for findMostIncreasedPositiveActionChange with
.orderBy(increaseCount.desc(), action.id.asc()) and .fetchFirst(); if you must
keep in-memory processing instead, replace the stream sorting with
.min(Comparator.comparing(...)) to avoid unnecessary full sorting.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (3)

13-13: Tuple/Comparator import 제거하고 @QueryProjection(QActionChange)로 일관화하세요

이 파일은 Tuple+스트림 매핑을 사용하지만, PR 설명대로 DTO에 @QueryProjection을 적용했다면 QActionChange로 직접 매핑하는 편이 타입 안정성과 성능 모두에 유리합니다. 아래 리팩토링을 적용하면 두 import가 불필요해집니다.

- import com.querydsl.core.Tuple;
+ import com.example.wini.domain.log.dto.response.QActionChange;
...
- import java.util.Comparator;

Also applies to: 25-25


82-96: DB에서 정렬 및 상위 1건만 조회(fetchFirst)로 변경 + QActionChange 직접 프로젝션
인메모리 전건 fetch 후 정렬(O(n log n))은 불필요합니다. DB가 group/order/limit 1을 수행하도록 바꾸고, 이미 적용한 @QueryProjection을 활용해 바로 QActionChange로 받으세요. 동순위 안정화를 위해 action.id ASC를 세컨더리 키로 둡니다. 아울러 2개월 범위로 where 절을 제한합니다.

-        List<Tuple> resultsTuple = queryFactory
-                .select(action, increaseCount)
-                .from(note)
-                .join(note.action, action)
-                .join(action.actionCategory, actionCategory)
-                .where(isThisRoom(roomId)
-                        .and(isReceiver(memberId))
-                        .and(actionCategory.emotionType.eq(EmotionType.POSITIVE)))
-                .groupBy(action)
-                .fetch();
-
-        return resultsTuple.stream()
-                .map(tuple -> new ActionChange(tuple.get(action), tuple.get(increaseCount)))
-                .max(Comparator.comparing(ActionChange::getChange))
-                .orElse(null);
+        return queryFactory
+                .select(new QActionChange(action, increaseCount))
+                .from(note)
+                .join(note.action, action)
+                .join(action.actionCategory, actionCategory)
+                .where(isThisRoom(roomId)
+                        .and(isReceiver(memberId))
+                        .and(actionCategory.emotionType.eq(EmotionType.POSITIVE))
+                        // 2개월 범위로 스캔 제한
+                        .and(note.createdAt.goe(today.minusMonths(1).withDayOfMonth(1).atStartOfDay()))
+                        .and(note.createdAt.lt(today.plusDays(1).atStartOfDay())))
+                .groupBy(action)
+                .orderBy(increaseCount.desc(), action.id.asc())
+                .fetchFirst();

102-106: NEGATIVE 구간도 동일하게: DB 정렬 + fetchFirst, QActionChange 사용
위와 동일한 이유로 전건 fetch/인메모리 정렬을 제거하고, 최소값 1건만 DB에서 조회하세요. 동순위 안정화 포함.

-        NumberExpression<Long> decreaseCount = calculateMonthlyChange(today);
+        NumberExpression<Long> decreaseCount = calculateMonthlyChange(today);
-        List<Tuple> resultsTuple = queryFactory
-                .select(action, decreaseCount)
-                .from(note)
-                .join(note.action, action)
-                .join(action.actionCategory, actionCategory)
-                .where(isThisRoom(roomId)
-                        .and(isReceiver(memberId))
-                        .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
-                .groupBy(action)
-                .fetch();
-
-        return resultsTuple.stream()
-                .map(tuple -> new ActionChange(tuple.get(action), tuple.get(decreaseCount)))
-                .min(Comparator.comparing(ActionChange::getChange))
-                .orElse(null);
+        return queryFactory
+                .select(new QActionChange(action, decreaseCount))
+                .from(note)
+                .join(note.action, action)
+                .join(action.actionCategory, actionCategory)
+                .where(isThisRoom(roomId)
+                        .and(isReceiver(memberId))
+                        .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE))
+                        // 2개월 범위로 스캔 제한
+                        .and(note.createdAt.goe(today.minusMonths(1).withDayOfMonth(1).atStartOfDay()))
+                        .and(note.createdAt.lt(today.plusDays(1).atStartOfDay())))
+                .groupBy(action)
+                .orderBy(decreaseCount.asc(), action.id.asc())
+                .fetchFirst();

Also applies to: 107-121

🧹 Nitpick comments (1)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (1)

77-81: 월별 증감치 계산 방식 전환은 합리적(빼기 → 부호합), 다만 타입 고정 옵션 고려

sum(case…) - sum(case…) 대신 단일 CaseBuilder(+1/-1)의 합으로 바꾼 것은 Hibernate 6의 argType NPE 회피에 효과적입니다. 다만 특정 DB/방언 조합에서 여전히 타입 해석 문제가 재현되면 increase/decrease 식을 한 번 감싸 Long 타입을 강제하는 안전장치를 추가하세요.

+ import com.querydsl.core.types.dsl.Expressions;
@@
- NumberExpression<Long> increaseCount = calculateMonthlyChange(today);
+ NumberExpression<Long> increaseCount = calculateMonthlyChange(today);
+ // (옵션) Hibernate 6 NPE 방지용 타입 고정
+ // NumberExpression<Long> increaseCount =
+ //     Expressions.numberTemplate(Long.class, "{0}", calculateMonthlyChange(today));

또한 현재는 전 기간을 스캔하고 Case로 0을 누적합니다. where 절에 “지난달 1일 00:00 ≤ createdAt < 내일 00:00” 범위를 추가하면 스캔 범위가 2개월로 제한되어 성능에 유리합니다(아래 다른 코멘트의 diff에 포함). 기존 countThisMonthNotes/countLastMonthNotes는 더 이상 쓰이지 않으므로 제거 또는 @deprecated 권장.

Also applies to: 232-251

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 27229c1 and 2a53534.

📒 Files selected for processing (1)
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-deploy
  • GitHub Check: build

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (2)

8-8: QActionChange 사용에 따른 APT(Annotation Processor) 설정 검증 필요

@QueryProjectionQActionChange 전환은 적절합니다. 다만 QueryDSL APT 설정이 누락되면 빌드 시 QActionChange 생성 실패로 런타임 오류가 납니다. 빌드 스크립트(kapt/annotationProcessor)와 소스셋을 확인해 주세요.

다음 스크립트로 빌드 설정을 점검해 보세요:

#!/bin/bash
# QueryDSL APT 설정 및 Q타입 사용처 점검
rg -n -C2 -g 'build.gradle*' -P '(kapt|annotationProcessor).*querydsl-apt.*:jpa'
rg -n -C2 '\bQActionChange\b' src/main/java

26-26: DB에서 정렬·상위 1건만 조회하세요(fetchFirst) — in-memory 정렬 제거 및 동순위 안정화

현재 전체 fetch() 후 Java 스트림으로 max/min을 구하고 있어 불필요한 전송/정렬 비용이 큽니다. DB에서 ORDER BY + fetchFirst()로 상위 1건만 가져오면 성능과 결정성이 개선되고, Hibernate 6의 타입 추론 이슈를 피할 가능성도 높습니다. 동순위는 action.id로 안정화하세요.

아래처럼 변경을 제안합니다:

- import java.util.Comparator;
+ // Comparator 불필요 → 제거

@@
-        List<ActionChange> results = queryFactory
-                .select(new QActionChange(action, increaseCount))
+        return queryFactory
+                .select(new QActionChange(action, increaseCount))
                 .from(note)
                 .join(note.action, action)
                 .join(action.actionCategory, actionCategory)
                 .where(isThisRoom(roomId)
                         .and(isReceiver(memberId))
                         .and(actionCategory.emotionType.eq(EmotionType.POSITIVE)))
                 .groupBy(action)
-                .fetch();
-
-        return results.stream()
-                .max(Comparator.comparing(ActionChange::getChange))
-                .orElse(null);
+                .orderBy(increaseCount.desc(), action.id.asc())
+                .fetchFirst();

@@
-        List<ActionChange> results = queryFactory
-                .select(new QActionChange(action, decreaseCount))
+        return queryFactory
+                .select(new QActionChange(action, decreaseCount))
                 .from(note)
                 .join(note.action, action)
                 .join(action.actionCategory, actionCategory)
                 .where(isThisRoom(roomId)
                         .and(isReceiver(memberId))
                         .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
                 .groupBy(action)
-                .fetch();
-
-        return results.stream()
-                .min(Comparator.comparing(ActionChange::getChange))
-                .orElse(null);
+                .orderBy(decreaseCount.asc(), action.id.asc())
+                .fetchFirst();

참고: 만약 여전히 Hibernate 6에서 InterpretationException(argType == null)이 발생하면, orderBy(increaseCount.longValue().desc())처럼 .longValue()를 적용하거나, Expressions.numberTemplate(Long.class, "{0}", increaseCount)로 숫자 타입을 강제하는 우회도 고려하세요.

Also applies to: 80-89, 91-93, 101-110, 112-114

🧹 Nitpick comments (2)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (2)

78-79: 월 증감 계산식을 공통 메서드로 추출한 점 좋습니다

두 메서드에서 동일 표현식을 재사용해 일관성과 유지보수성이 좋아졌습니다. 이 값은 “이번 달 - 지난 달” 순증을 나타내므로 명칭을 monthlyChange 등으로 통일하면 의미 전달이 더 명확합니다(선택).

Also applies to: 99-100


18-18: Expressions.asDateTime 대신 파라미터 바인딩 사용 권장 — 실행 계획 캐시 및 DB 호환성 개선

note.createdAt.goe(Expressions.asDateTime(...))는 리터럴 캐스팅을 유발해 DB 계획 캐시 재사용성이 떨어질 수 있습니다. LocalDateTime 값을 직접 바인딩하면 단순하고 이식성이 좋습니다. 또한 Expressions import도 제거 가능합니다.

- import com.querydsl.core.types.dsl.Expressions;
@@
-        return new CaseBuilder()
-                .when(note.createdAt
-                        .goe(Expressions.asDateTime(startOfThisMonth))
-                        .and(note.createdAt.lt(Expressions.asDateTime(endOfThisMonth))))
+        return new CaseBuilder()
+                .when(note.createdAt.goe(startOfThisMonth)
+                        .and(note.createdAt.lt(endOfThisMonth)))
                 .then(1L)
-                .when(note.createdAt
-                        .goe(Expressions.asDateTime(startOfLastMonth))
-                        .and(note.createdAt.lt(Expressions.asDateTime(endOfLastMonth))))
+                .when(note.createdAt.goe(startOfLastMonth)
+                        .and(note.createdAt.lt(endOfLastMonth)))
                 .then(-1L)
                 .otherwise(0L)
                 .sum();

Also applies to: 197-218

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2a53534 and 01d34b4.

📒 Files selected for processing (1)
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-deploy
🔇 Additional comments (1)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (1)

78-78: 시간대 기준 확인 요청(서버 TZ vs. DB TZ 불일치 가능성)

LocalDate.now()를 기준으로 월 경계를 계산합니다. 애플리케이션 JVM 시간대와 DB 서버/컬럼 시간대가 다르면 경계 오차가 날 수 있습니다. 운영 환경의 TZ 정책(예: Asia/Seoul 고정)과 JPA/Hibernate의 JDBC 시간대 설정을 확인해 주세요.

Also applies to: 99-99

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java (1)

5-5: @QueryProjection 누락: 생성자에 부착하고 레포지토리에서 QActionChange 사용으로 일관화하세요

QueryDSL 타입 세이프 프로젝션을 위해 레코드의 canonical 생성자에 @QueryProjection이 필요합니다. 현재 상태에선 Projections.constructor(...) 사용 시 런타임 타입 미스매치/해석 오류 재발 여지가 큽니다. 레포지토리에서는 new QActionChange(...)로 교체하는 것을 권장합니다.

다음 패치를 제안합니다:

-public record ActionChange(Action action, long monthlyChange) {}
+public record ActionChange(Action action, long monthlyChange) {
+  @com.querydsl.core.annotations.QueryProjection
+  public ActionChange {}
+}

검증 스크립트:

#!/bin/bash
# 1) @QueryProjection 적용 여부
rg -n '@QueryProjection' src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java

# 2) QActionChange 사용 여부
rg -n '\bQActionChange\b' src/main/java

# 3) 기존 constructor 기반 프로젝션 잔존 여부
rg -nP --type=java -C2 'Projections\.constructor\(\s*ActionChange\.class' src/main/java

# 4) Querydsl APT 설정 확인 (Gradle)
rg -n -C2 -g 'build.gradle*' -P 'annotationProcessor\s+["'\'']com\.querydsl:querydsl-apt.*:(jpa|jakarta)["'\'']'

# 5) 합산/계산식 Long 타입 강제 여부(예: numberTemplate(Long.class, ...))
rg -nP --type=java -C2 'Expressions\.numberTemplate\s*\(\s*Long\.class' src/main/java
🧹 Nitpick comments (1)
src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java (1)

5-5: DTO에 엔티티(Action) 직접 포함 시 직렬화/N+1 리스크

응답 DTO 단계까지 전달된다면 지연 로딩/직렬화 순환 참조 및 N+1 이슈가 생길 수 있습니다. 필요 필드만 담는 경량 DTO(actionId, actionName 등)로 매핑하거나, 조회 시점에 명시적 fetch/프로젝션으로 해결하는 방안을 고려해 주세요.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c6f0728 and 291e860.

📒 Files selected for processing (3)
  • src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java (1 hunks)
  • src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java (1 hunks)
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-deploy
  • GitHub Check: build
🔇 Additional comments (1)
src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java (1)

5-5: long으로의 통일 Good

집계(SUM/CASE) 결과 타입과 정합성이 맞아 안정성이 올라갑니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (2)

88-97: 정렬·상위 1건 반환은 DB에 위임 + QActionChange로 직접 투영

모든 그룹을 fetch 후 스트림 정렬하는 대신 DB에서 정렬하고 1건만 받으면 비용/결정성이 개선됩니다. 또한 @QueryProjection을 활용해 직접 DTO로 투영하세요. tie-breaker로 action.id ASC를 추가해 안정화하세요.

-        List<Tuple> results = queryFactory
-                .select(action, monthlyChangeSum)
-                .from(note)
-                .join(note.action, action)
-                .join(action.actionCategory, actionCategory)
-                .where(isThisRoom(roomId)
-                        .and(isReceiver(memberId))
-                        .and(actionCategory.emotionType.eq(EmotionType.POSITIVE)))
-                .groupBy(action.id)
-                .fetch();
-
-        return results.stream()
-                .map(t -> new ActionChange(t.get(action), t.get(monthlyChangeSum)))
-                .max(Comparator.comparing(ActionChange::monthlyChange))
-                .orElse(null);
+        return queryFactory
+                .select(new QActionChange(action, monthlyChangeSum))
+                .from(note)
+                .join(note.action, action)
+                .join(action.actionCategory, actionCategory)
+                .where(isThisRoom(roomId)
+                        .and(isReceiver(memberId))
+                        .and(actionCategory.emotionType.eq(EmotionType.POSITIVE)))
+                .groupBy(action)
+                .orderBy(monthlyChangeSum.desc(), action.id.asc())
+                .fetchFirst();

참고: 만약 @QueryProjection을 당장 쓰기 어렵다면 Tuple을 유지하되 .orderBy(...).fetchFirst()로 바꾸고 null 체크 후 매핑만 수행하세요.

Also applies to: 99-102


119-128: NEGATIVE도 DB 정렬+fetchFirst로 일관 처리

최솟값 선정을 DB에 위임하고 tie-breaker를 명시하세요. DTO 직접 투영을 권장합니다.

-        List<Tuple> results = queryFactory
-                .select(action, monthlyChangeSum)
-                .from(note)
-                .join(note.action, action)
-                .join(action.actionCategory, actionCategory)
-                .where(isThisRoom(roomId)
-                        .and(isReceiver(memberId))
-                        .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
-                .groupBy(action.id)
-                .fetch();
-
-        return results.stream()
-                .map(t -> new ActionChange(t.get(action), t.get(monthlyChangeSum)))
-                .min(Comparator.comparing(ActionChange::monthlyChange))
-                .orElse(null);
+        return queryFactory
+                .select(new QActionChange(action, monthlyChangeSum))
+                .from(note)
+                .join(note.action, action)
+                .join(action.actionCategory, actionCategory)
+                .where(isThisRoom(roomId)
+                        .and(isReceiver(memberId))
+                        .and(actionCategory.emotionType.eq(EmotionType.NEGATIVE)))
+                .groupBy(action)
+                .orderBy(monthlyChangeSum.asc(), action.id.asc())
+                .fetchFirst();

Also applies to: 130-133

🧹 Nitpick comments (6)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (6)

13-13: @QueryProjection을 쓸 거라면 Tuple import 제거 가능

DTO에 @QueryProjection을 붙였다면 Tuple 기반 매핑을 없애고 QActionChange로 투영하세요. 그러면 타입 안정성과 가독성이 개선되고 런타임 타입 해석 이슈도 줄어듭니다.

-import com.querydsl.core.Tuple;
+import com.example.wini.domain.log.dto.response.QActionChange;

25-25: DB 정렬+fetchFirst 적용 시 Comparator 불필요

정렬을 DB로 옮기면 in-memory Comparator가 필요 없습니다.

-import java.util.Comparator;

78-86: sum + coalesce(Long) 처리 OK. 중복 로직은 헬퍼로 추출 권장

타입을 Long으로 고정하고 coalesce(0L) 한 점은 적절합니다. 동일 블록이 아래 메서드에 중복되므로 헬퍼로 추출을 권장합니다.

아래 메서드를 추가하고 두 곳에서 재사용:

private NumberExpression<Long> monthlyChangeSum(LocalDate today) {
    NumberExpression<Long> mc = new CaseBuilder()
            .when(isCreatedThisMonth(today)).then(1L)
            .when(isCreatedLastMonth(today)).then(-1L)
            .otherwise(0L);
    return mc.sum().coalesce(0L);
}

109-118: NEGATIVE 쿼리도 월변화 합계 생성 로직 중복 — 헬퍼 재사용 권장

위와 동일한 monthlyChangeSum 생성이 반복됩니다. 앞서 제안한 헬퍼로 통일해 유지보수성을 높이세요.


96-96: groupBy(action.id) → groupBy(action)로 JPA 호환성 강화

엔티티를 select 하면서 id만 groupBy 하면 DB/벼전 조합에 따라 경고나 오류가 나올 수 있습니다. 엔티티 Path로 groupBy 해 JPA 스펙과 일치시키는 편이 안전합니다.

-                .groupBy(action.id)
+                .groupBy(action)

Also applies to: 127-127


225-233: 지난 달 경계도 OK — 테스트 용이성 위해 Clock 주입 고려

테스트에서 고정 시점을 다루기 쉽도록 Clock을 주입해 today를 계산하는 방식을 고려해 보세요.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 291e860 and 5dc6b0c.

📒 Files selected for processing (3)
  • src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java (1 hunks)
  • src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java (1 hunks)
  • src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/example/wini/domain/log/dto/response/ActionChange.java
  • src/main/java/com/example/wini/domain/log/dto/response/SimpleActionChange.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-deploy
🔇 Additional comments (2)
src/main/java/com/example/wini/domain/note/repository/NoteCustomRepositoryImpl.java (2)

216-223: 이번 달 경계 설정(포함/제외) 적절함 — TZ 차이만 확인

[YYYY-MM-01 00:00, 다음 달 00:00) 범위 설정은 바람직합니다. 다만 createdAt이 DB UTC, 애플리케이션이 시스템 TZ라면 경계 오차가 생길 수 있으니 ZoneId/Clock 주입 여부를 확인해 주세요.


85-86: .longValue() 중복 캐스팅 여부 확인 필요
monthlyChange.sum()이 이미 NumberExpression을 반환하므로 불필요하다면 .longValue()를 제거해 단순화하세요. Hibernate 6 타입 추론 회피 목적이라면 유지 가능합니다. QueryDSL Q타입 생성 및 사용 여부를 직접 검증해 주세요.

Copy link
Collaborator

@duol9 duol9 left a comment

Choose a reason for hiding this comment

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

호환성이 이리도 안맞는다니 ㅠㅠㅠ 고생하셧습니다!!!!!!!!!!!!!

@ohhamma ohhamma merged commit 8584d70 into develop Aug 30, 2025
3 checks passed
@ohhamma ohhamma deleted the fix/94-log-growth branch August 30, 2025 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 fix 버그 및 에러 픽스

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 나의 성장 조회시 발생하는 오류

2 participants