Skip to content

Conversation

@Hyun0828
Copy link
Collaborator

@Hyun0828 Hyun0828 commented Aug 23, 2025

PR 타입(하나 이상의 PR 타입을 선택해주세요)

  • 기능 추가
  • 기능 삭제
  • 버그 수정
  • 의존성, 환경 변수, 빌드 관련 코드 업데이트
  • 리팩터링

반영 브랜치

dev->main

작업 내용

알림 응답 DTO 수정

테스트 결과

Summary by CodeRabbit

  • 신기능

    • 시리즈별 피드 필터링(seriesId) 지원으로 마이페이지·피드 조회 고도화
    • 다른 사용자의 팟 어필 콘텐츠 조회 API 및 사용자 소개 조회 API 추가
    • 팟 종료 알림 유형 추가 및 실시간(SSE) 전송 지원
    • 팟 요약에 팟명·멤버 여부(isMember) 포함
  • 변경사항

    • 알림 엔드포인트 기본 경로를 /notifications로 변경
    • 알림 응답에서 단일 ID를 potId와 feedId로 분리(클라이언트 매핑 필요)
    • 알림 제목/메시지 문구 현지화 및 형식 통일

Hyun0828 and others added 7 commits August 22, 2025 00:00
* [FEAT/#423]
- 다른 사람 마이페이지 '여기서 저는요' 모달 조회

* [FEAT/#423]
- 끓인팟 요약 조회: 팟 이름이랑 팟 멤버인지 여부 (boolean) 추가

* [FEAT/#423]
- 권한 위임 API 오류 수정

* [FEAT/#423]
- 여기서 저는요 작성 및 수정 API 버그 수정
[FIX/#414] 마이페이지 피드 조회 필터링
[Feat/#424] 알림 응답 DTO 수정
[Feat/#424] 알림 응답 DTO 2차 수정
@coderabbitai
Copy link

coderabbitai bot commented Aug 23, 2025

Walkthrough

알림에 “팟 종료” 흐름과 관련 엔티티/리포지토리/이벤트/SSE 처리를 추가하고, 미읽음 알림 DTO를 potId/feedId로 분리 및 메시지 재구성. 피드 조회에 seriesId 기반 필터/커서 페이징을 도입하고, 팟 요약 DTO에 potName/isMember를 추가. 일부 REST 경로·트랜잭션/Gradle 의존성 변경 포함.

Changes

Cohort / File(s) Summary
Dependencies
build.gradle
Hibernate 관련 의존성 섹션 추가(스프링 데이터 JPA 스타터 및 hibernate-commons-annotations 선언).
Feed query: series 필터/페이징
src/main/java/.../feed/repository/FeedRepository.java, .../feed/service/FeedQueryService.java, .../feed/service/FeedQueryServiceImpl.java, .../user/controller/UserController.java
seriesId 파라미터 추가 및 분기 로직으로 series별 조회와 커서 페이징 지원. 컨트롤러에 query param 노출.
알림 DTO/컨버터 구조 변경
src/main/java/.../notification/dto/NotificationDto.java, .../notification/dto/NotificationResponseDto.java, .../notification/converter/NotificationConverter.java
potOrFeedId 제거 → potId, feedId 분리; 사용자명 null 안전 처리 및 DTO 변환 시그니처 변경.
알림: 리포지토리 프로젝션 갱신
src/main/java/.../notification/repository/FeedLikeNotificationRepository.java, .../FeedCommentNotificationRepository.java, .../PotCommentNotificationRepository.java, .../PotApplicationNotificationRepository.java
JPQL 프로젝션을 7-인자 DTO 생성자로 변경, 한글 라벨/문구 생성(CONCAT) 적용, JOIN·WHERE 재작성.
팟 종료 알림: 엔티티/레포/타입
src/main/java/.../notification/entity/PotEndNotification.java, .../notification/repository/PotEndNotificationRepository.java, .../notification/entity/enums/NotificationType.java
PotEndNotification 엔티티·레포지토리 추가, POT_END enum 항목 및 읽음 처리 로직/한글 매핑 추가.
팟 종료 이벤트 흐름
src/main/java/.../notification/event/PotEndEvent.java, .../notification/service/NotificationCommandService.java, .../pot/service/pot/PotCommandServiceImpl.java, .../event/SseService.java, .../notification/service/NotificationQueryService.java
팟 종료 시 UnRead DTO 생성 → 저장 → PotEndEvent 발행 → SseService에서 후속 처리(회원별 SseEmitter로 전송) → 미읽음 조회에 포함. 커맨드 서비스에 생성/삭제 메서드 추가.
Notification API 경로 변경
src/main/java/.../notification/controller/NotificationController.java
컨트롤러 베이스 경로 /api/notifications/notifications, 읽음 API에 설명 추가.
Pot 요약/멤버/마이팟 서비스
src/main/java/.../pot/dto/PotSummaryDto.java, .../pot/converter/PotConverter.java, .../pot/converter/PotMemberConverter.java, .../pot/service/pot/MyPotService.java, .../pot/service/pot/MyPotServiceImpl.java
PotSummaryDtopotName/isMember 필드 추가, PotConverter.toDto 시그니처 변경 및 isMember 반영. MyPotService에 사용자별 호소글 조회 추가, 위임 로직 리팩터.
User API 추가 및 feed 파라미터 확장
src/main/java/.../user/controller/UserController.java
GET /potAppealContent/{pot_id}/{user_id} 및 GET /description/{userId} 추가. feed 관련 endpoints에 seriesId 파라미터 추가.
트랜잭션 어노테이션 변경
src/main/java/.../feed/service/FeedCommandServiceImpl.java, .../user/service/UserCommandServiceImpl.java
jakarta.transaction.Transactionalorg.springframework.transaction.annotation.Transactional로 변경(임포트만).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Scheduler as 시스템(팟 종료 트리거)
  participant PCS as PotCommandServiceImpl
  participant NCS as NotificationCommandService
  participant PENR as PotEndNotificationRepository
  participant AEP as ApplicationEventPublisher
  participant SSE as SseService
  participant Member as PotMember(SSE)

  Scheduler->>PCS: patchPotWithRecruitments(potId,...)
  PCS->>NCS: createdPotEndNotification(potId)
  NCS->>PENR: save(PotEndNotification)
  NCS-->>PCS: UnReadNotificationDto
  PCS->>AEP: publish PotEndEvent(potId, dto)
  AEP-->>SSE: PotEndEvent
  SSE->>Member: send SSE "Notification" with dto (per active emitter)
  note over SSE,Member: 전송 실패 시 해당 emitter 제거
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

✨feat

Suggested reviewers

  • leesumin0526

Poem

팟이 끝나니 토깽이 뛰어나와,
"끝났어요!" 소식에 귀가 쫑긋해요.
새싹 이름 달고 알림이 쾅쾅,
시리즈 줄 세워 피드도 차곡차곡,
깡충! 이벤트로 전해요 — 냐옹? 🐰✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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

Caution

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

⚠️ Outside diff range comments (5)
src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (1)

40-47: PotMemberAppealResponseDto 매핑 시 userId NPE 방지 필요

검증 결과, PotMemberAppealResponseDto.userId 필드는 Long 타입으로 null 허용이 확인되었습니다.
따라서 entity.getUser()null일 수 있는 상황에서 entity.getUser().getId()를 직접 호출할 경우 NPE가 발생합니다.

수정 대상:

  • src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java
    toDto 메서드 내 userId 매핑 부분

권장 변경사항:

         .potId(entity.getPot().getPotId())
-        .userId(entity.getUser().getId())
+        // user가 null일 수 있으므로 null 체크 후 매핑
+        .userId(entity.getUser() != null ? entity.getUser().getId() : null)
         .roleName(roleName)
         .nickname(nicknameWithRole)
         .appealContent(entity.getAppealContent())

또한, 유사하게 toKaKaoCreatorDtotoKaKaoMemberDto 메서드에서도 entity.getUser()에 대한 null 안전성을 한 번 더 검토해 주시기 바랍니다.

src/main/java/stackpot/stackpot/feed/service/FeedCommandServiceImpl.java (1)

198-205: 좋아요 취소 시 알림도 함께 삭제하도록 구체 구현 제안

현재는 TODO만 존재합니다. 좋아요 취소 시 생성된 “좋아요 알림”을 식별 가능한 키(likeId 등)로 제거해야 합니다. 아래처럼 likeId를 보존 후 알림 삭제를 호출하는 편이 안전합니다.

         if (existingLike.isPresent()) {
             // 이미 좋아요가 있다면 삭제 (좋아요 취소)
-            feedLikeRepository.delete(existingLike.get());
+            Long likeId = existingLike.get().getLikeId();
+            feedLikeRepository.delete(existingLike.get());
             feed.setLikeCount(feed.getLikeCount() - 1);
             feedRepository.save(feed);
-            // todo 좋아요 알림 삭제
+            // 좋아요 알림 삭제
+            notificationCommandService.deleteFeedLikeNotification(likeId);
             return false; // 좋아요 취소
         } else {

추가 고려사항

  • 동시 토글로 인한 likeCount 음수화 방지: Math.max(0, likeCount-1) 또는 DB 카운트 기반 재계산을 권장합니다.
  • 알림 취소 이벤트가 클라이언트에 반영되어야 한다면, LikeCancelEvent 발행도 고려해 주세요.
src/main/java/stackpot/stackpot/notification/converter/NotificationConverter.java (1)

17-39: userName 접미사("새싹") 중복 및 null 표시 문제

  • 현재 컨버터가 userName에 무조건 " 새싹"을 덧붙이는 반면, 일부 서비스(예: NotificationCommandService)에서 이미 "새싹"이 붙은 이름을 전달하고 있어 "새싹 새싹" 중복이 발생합니다.
  • PotEndNotificationRepository는 userName을 null로 투영하므로 현재 로직대로면 "null 새싹"이 노출됩니다.

컨버터에서 접미사를 “필요할 때만” 붙이고, null은 그대로 두도록 방어 코드를 넣어 주세요.

     public NotificationResponseDto.UnReadNotificationDto toUnReadNotificationDto(NotificationDto.UnReadNotificationDto unReadNotificationDto) {
-        return NotificationResponseDto.UnReadNotificationDto.builder()
-                .notificationId(unReadNotificationDto.getNotificationId())
-                .potId(unReadNotificationDto.getPotId())
-                .feedId(unReadNotificationDto.getFeedId())
-                .userName(unReadNotificationDto.getUserName() + " 새싹")
-                .type(unReadNotificationDto.getType())
-                .content(unReadNotificationDto.getContent())
-                .createdAt(unReadNotificationDto.getCreatedAt().format(DATE_FORMATTER))
-                .build();
+        String displayName = unReadNotificationDto.getUserName();
+        if (displayName != null && !displayName.endsWith("새싹")) {
+            displayName = displayName + " 새싹";
+        }
+        return NotificationResponseDto.UnReadNotificationDto.builder()
+                .notificationId(unReadNotificationDto.getNotificationId())
+                .potId(unReadNotificationDto.getPotId())
+                .feedId(unReadNotificationDto.getFeedId())
+                .userName(displayName)
+                .type(unReadNotificationDto.getType())
+                .content(unReadNotificationDto.getContent())
+                .createdAt(unReadNotificationDto.getCreatedAt().format(DATE_FORMATTER))
+                .build();
     }
 
     public NotificationResponseDto.UnReadNotificationDto toUnReadNotificationDto(
-            Long notificationId, Long potId, Long feedId, String userName, String type, String content, LocalDateTime createdAt) {
-        return NotificationResponseDto.UnReadNotificationDto.builder()
-                .notificationId(notificationId)
-                .potId(potId)
-                .feedId(feedId)
-                .userName(userName + " 새싹")
-                .type(type)
-                .content(content)
-                .createdAt(createdAt.format(DATE_FORMATTER))
-                .build();
+            Long notificationId, Long potId, Long feedId, String userName, String type, String content, LocalDateTime createdAt) {
+        String displayName = userName;
+        if (displayName != null && !displayName.endsWith("새싹")) {
+            displayName = displayName + " 새싹";
+        }
+        return NotificationResponseDto.UnReadNotificationDto.builder()
+                .notificationId(notificationId)
+                .potId(potId)
+                .feedId(feedId)
+                .userName(displayName)
+                .type(type)
+                .content(content)
+                .createdAt(createdAt.format(DATE_FORMATTER))
+                .build();
     }

추가로, 서비스 단에서는 raw 닉네임을 넘기고 접미사 부여는 오직 컨버터에서만 수행하도록 정렬하는 것을 권장합니다. 원하시면 관련 서비스 메서드의 호출부 정리 패치를 제안드리겠습니다.

src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java (1)

310-340: 권한 위임 트랜잭션 및 자기 위임 방지 로직 추가 필요

rg 결과, MyPotService 인터페이스에는

String patchDelegate(Long potId, Long memberId);

로 선언되어 있으며 구현체의 파라미터 이름(potMemberId) 차이만 있을 뿐 메서드 시그니처(Long, Long)는 일치해 인터페이스 변경은 필요 없습니다.

아직 적용되지 않은 아래 두 가지를 반드시 반영해 주세요:

  • MyPotServiceImpl.patchDelegate 메서드에 @Transactional 애노테이션 추가
  • prevOwner와 newOwner 동일 시 DB 쓰기 방지용 자기 위임 검증 로직 추가
@@ src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java
-    public String patchDelegate(Long potId, Long potMemberId) {
+    @Transactional
+    public String patchDelegate(Long potId, Long potMemberId) {
         User user = authService.getCurrentUser();
@@
         PotMember newOwner = potMemberRepository.findById(potMemberId)
                 .orElseThrow(() -> new PotHandler(ErrorStatus.INVALID_MEMBER));
+
+        // 자기 위임 방지
+        if (prevOwner.getId().equals(newOwner.getId())) {
+            return "변경 사항 없음(동일 사용자로 권한 위임 요청)";
+        }
src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java (1)

123-123: 메서드명 통일 필요 — createdcreate
퍼블릭 API로 제공되는 메서드명이 다른 메서드(createPot…, createFeedLike…)와 일관되지 않아, 호출부 전체를 함께 변경해야 합니다.

수정 대상 위치:

  • src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java (123행)
  • src/main/java/stackpot/stackpot/feed/service/FeedCommentCommandService.java (50행, 75행)

제안하는 리팩터링:

--- a/src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java
+++ b/src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java
@@ -123,7 +123,7 @@ public class NotificationCommandService {
-    public NotificationResponseDto.UnReadNotificationDto createdFeedCommentNotification(Long feedId, Long commentId, Long userId) {
+    public NotificationResponseDto.UnReadNotificationDto createFeedCommentNotification(Long feedId, Long commentId, Long userId) {
         FeedComment feedComment = feedCommentQueryService.selectFeedCommentByCommentId(commentId);
         if (feedComment.getFeed().getUser().getId().equals(userId)) {
             return null;
--- a/src/main/java/stackpot/stackpot/feed/service/FeedCommentCommandService.java
+++ b/src/main/java/stackpot/stackpot/feed/service/FeedCommentCommandService.java
@@ -47,7 +47,7 @@ public class FeedCommentCommandService {
-        NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createdFeedCommentNotification(
+        NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createFeedCommentNotification(
                 feedId, feedComment.getId(), user.getId());
@@ -72,7 +72,7 @@ public class FeedCommentCommandService {
-        NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createdFeedCommentNotification(
+        NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createFeedCommentNotification(
                 feedId, feedComment.getId(), user.getId());
♻️ Duplicate comments (2)
src/main/java/stackpot/stackpot/notification/repository/PotCommentNotificationRepository.java (1)

16-31: 알림 메시지 조합 로직의 중복 및 i18n 적용성 개선 제안

피드/팟 댓글/지원 등 각 알림 리포지토리에서 유사한 CONCAT 패턴이 반복됩니다. 하드코딩 메시지를 Converter/전용 MessageBuilder로 이관하고, 닉네임 가공은 RoleNameMapper.getWriterNickname을 일관 사용하도록 표준화하면 유지보수/다국어 대응이 수월해집니다.

src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java (1)

15-31: 메시지 하드코딩/중복 규칙 이슈 — 공통화 권장

문구/접미사(“새싹”) 규칙이 다수 리포지토리에서 반복되고 있어 추후 변경 비용이 큽니다. 공통 MessageBuilder/Converter로 이관하는 방향을 본 PR 내 다른 코멘트와 동일하게 권장드립니다.

🧹 Nitpick comments (32)
src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (2)

69-75: 중복 로직 제거: 닉네임+역할 표시 생성 로직 공통화 제안

toDto, toKaKaoMemberDto, toKaKaoCreatorDto 모두 사용자 탈퇴/NULL, 역할 표시명 매핑 로직이 중복됩니다. 유틸/프라이빗 메서드로 추출하면 일관성, 버그 가능성, 테스트 용이성이 개선됩니다.

예시(이 파일 내 private 메서드):

private static String nicknameWithDisplayRole(PotMember entity, String roleNameCode) {
    String displayRole = RoleNameMapper.mapRoleName(roleNameCode != null ? roleNameCode : "멤버");
    String baseNickname = (entity.getUser() != null) ? entity.getUser().getNickname() : "(알 수 없음)";
    // 탈퇴한 사용자라면 역할 제외하려면 여기에서 분기하도록 파라미터 추가/옵션화 가능
    return (entity.getUser() == null || entity.getUser().isDeleted())
            ? baseNickname
            : baseNickname + " " + displayRole;
}

Also applies to: 27-37


50-50: 이름 표기 일관성(nit): KaKao → Kakao

메서드명 toKaKaoCreatorDto/toKaKaoMemberDto의 대소문자 표기가 일반적인 "Kakao"와 다릅니다. IDE 리팩터링으로 일괄 변경을 고려해 주세요. 외부 API/문서와의 용어 일관성이 좋아집니다.

Also applies to: 62-62

src/main/java/stackpot/stackpot/pot/dto/PotSummaryDto.java (1)

14-15: isMember는 primitive로, 기본값 명확화 권장

null 3-state가 필요 없다면 primitive로 전환하여 NPE/직렬화 이슈를 줄이는 편이 낫습니다. 필요 시 기본값을 명확히 해 주세요.

-    private Boolean isMember;
+    private boolean isMember;

또는 null을 허용해야 한다면, @Builder.Default로 기본값을 지정해 두면 안전합니다.

-    private Boolean isMember;
+    @Builder.Default
+    private Boolean isMember = Boolean.FALSE;

컨버터(PotConverter.toDto)의 신규 필드 세팅이 누락되지 않았는지 함께 확인 부탁드립니다.

src/main/java/stackpot/stackpot/notification/entity/PotEndNotification.java (2)

26-28: Setter 의미를 더 명확히: 파라미터 없는 markAsRead/markAsUnread 제공 제안

불필요한 null 대입을 막고 호출 의도를 명확하게 합니다.

-    public void updateIsRead(Boolean isRead) {
-        this.isRead = isRead;
-    }
+    public void markAsRead() { this.isRead = Boolean.TRUE; }
+    public void markAsUnread() { this.isRead = Boolean.FALSE; }

20-24: Found file: src/main/java/stackpot/stackpot/notification/entity/PotEndNotification.java

Inspecting lines 1–200:

package stackpot.stackpot.notification.entity;

import jakarta.persistence.*;
import lombok.*;
import stackpot.stackpot.common.BaseEntity;
import stackpot.stackpot.pot.entity.Pot;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class PotEndNotification extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "pot_end_notification_id")
    private Long id;

    private Boolean isRead;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "pot_id")
    private Pot pot;

    public void updateIsRead(Boolean isRead) {
        this.isRead = isRead;
    }
}

검증 결과:

  • recipient(User) 필드가 존재하지 않아, 모든 사용자에 대해 단일 PotEndNotification 인스턴스만 생성됩니다.
  • isReadBoolean으로 선언되어 있고 기본값(false)이 설정되어 있지 않아, 읽음 상태가 null일 수 있습니다.
  • @OneToOne 매핑으로 Pot과 1:1 관계이며, 다수 사용자별 알림 저장이 불가능합니다.

따라서 원래 리뷰 코멘트에서 지적한 “유저 차원 누락” 이슈가 코드베이스에 그대로 존재함을 확인했습니다. 아래와 같이 리팩터링이 필요합니다.

수정 제안 요약

  • recipient@ManyToOne으로 추가하여 사용자 식별자 포함
  • 엔티티 클래스에 테이블 레벨 @UniqueConstraint(recipient_id, pot_id) 적용
  • isRead 기본값 false 초기화
  • Pot 삭제시 알림도 함께 제거되는 @OnDelete(action = OnDeleteAction.CASCADE) 설정
  • Query/Command 레이어에 recipient 조건 추가 여부 확인
  • 마이그레이션 시 기존 알림에 대해 recipient를 채워주는 스크립트 작성 필요
src/main/java/stackpot/stackpot/notification/service/NotificationQueryService.java (3)

16-16: 와일드카드 import 지양 권장(충돌·리뷰 가독성 이슈 예방)

명시적 import로 전환해 주면 추후 타입 충돌과 리뷰 난이도를 낮출 수 있습니다.

-import stackpot.stackpot.notification.repository.*;
+import stackpot.stackpot.notification.repository.FeedCommentNotificationRepository;
+import stackpot.stackpot.notification.repository.FeedLikeNotificationRepository;
+import stackpot.stackpot.notification.repository.PotApplicationNotificationRepository;
+import stackpot.stackpot.notification.repository.PotCommentNotificationRepository;
+import stackpot.stackpot.notification.repository.PotEndNotificationRepository;

55-55: 주석 오탈자 수정 제안: ‘끓이면’ → ‘끝나면/종료되면’

사용자/리뷰어 혼동을 줄이기 위해 용어를 통일해 주세요.

-// 6. 팟 종료 알림 : 팟을 끓이면 팟 멤버들에게 팟 종료 알림을 보낸다.
+// 6. 팟 종료 알림 : 팟이 종료되면 팟 멤버들에게 팟 종료 알림을 보낸다.

59-65: createdAt 정렬은 변환 전(LocalDateTime) 기준이 더 안전

현재는 DTO 변환 후(String createdAt) 정렬합니다. 문자열 형식 변경 시 정렬 안정성이 깨질 수 있습니다. 변환 전에 NotificationDto.UnReadNotificationDto.getCreatedAt()(LocalDateTime) 기준으로 정렬하고, 이후 매핑을 권장합니다.

예시(참고용):

List<NotificationDto.UnReadNotificationDto> combined = Stream.of(
        potApplicationNotifications,
        potCommentNotifications,
        feedLikeNotifications,
        feedCommentNotifications,
        potEndNotifications
).flatMap(List::stream)
 .sorted(Comparator.comparing(NotificationDto.UnReadNotificationDto::getCreatedAt).reversed())
 .toList();

return combined.stream()
        .map(notificationConverter::toUnReadNotificationDto)
        .toList();

대안: 변환 후에도 ISO 8601(예: yyyy-MM-dd'T'HH:mm:ss)로 고정 포맷이라면 문자열 정렬도 동등하지만, 포맷 변경 리스크를 제거하려면 상기 방식이 더 견고합니다.

NotificationConverter.toUnReadNotificationDto(...)가 어떤 포맷으로 createdAt을 문자열화하는지 확인해 주세요(ISO 8601 고정 여부).

src/main/java/stackpot/stackpot/notification/dto/NotificationDto.java (1)

19-24: potId/feedId 분리 👍 — 상호배타성(Exactly-one) 제약 명시/검증을 권장

이 DTO는 알림 유형에 따라 potId 또는 feedId 중 하나만 의미가 있습니다.

  • 계약을 주석/Javadoc으로 명시하고,
  • 정적 팩토리(ofPot(...), ofFeed(...)) 또는 빌더 검증으로 “둘 다 null 아님/둘 다 설정” 케이스를 차단하면 후행 버그를 줄일 수 있습니다.

예시(참고용):

public static UnReadNotificationDto ofPot(Long notificationId, Long potId, String userName, String type, String content, LocalDateTime createdAt) {
    return UnReadNotificationDto.builder()
            .notificationId(notificationId)
            .potId(potId)
            .userName(userName)
            .type(type)
            .content(content)
            .createdAt(createdAt)
            .build();
}

repository 단의 프로젝션/쿼리 결과가 새 필드 레이아웃(potId, feedId 분리)에 모두 반영되었는지 확인해 주세요.

src/main/java/stackpot/stackpot/event/SseService.java (1)

174-195: PotEnd 실시간 전파 로직 적절 — 주석 정정 및 공통 전송 헬퍼로 중복 제거 제안

  • 주석 오탈자: ‘끓인’ → ‘끝낸/종료한’.
  • 동일한 전송 패턴이 여러 이벤트 메서드에 반복됩니다. 공통 헬퍼로 추출하면 가독성과 유지보수성이 개선됩니다.

적용 예(해당 범위 내 변경):

-/**
- * PotEndNotification
- * 팟을 끓인 후 팟 멤버들에게 실시간 알림 전송
- */
+/**
+ * PotEndNotification
+ * 팟을 종료한 후 팟 멤버들에게 실시간 알림 전송
+ */
 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
 public void sendPotEndNotification(PotEndEvent event) {
     Long potId = event.getPotId();
     List<Long> userIds = potMemberQueryService.selectUserIdsAboutPotMembersByPotId(potId);
-    for (Long userId : userIds) {
-        SseEmitter emitter = userId != null ? emitters.get(userId) : null;
-        if (emitter != null) {
-            try {
-                emitter.send(SseEmitter.event()
-                        .name("Notification")
-                        .data(event.getUnReadNotificationDto(), MediaType.APPLICATION_JSON));
-            } catch (Exception e) {
-                emitters.remove(userId);
-            }
-        }
-    }
+    for (Long userId : userIds) {
+        sendNotificationToUser(userId, event.getUnReadNotificationDto());
+    }
 }

헬퍼(클래스 내부, 공통 사용) 예:

private void sendNotificationToUser(Long userId, Object payload) {
    if (userId == null) return;
    SseEmitter emitter = emitters.get(userId);
    if (emitter == null) return;
    try {
        emitter.send(SseEmitter.event()
                .name("Notification")
                .data(payload, MediaType.APPLICATION_JSON));
    } catch (Exception e) {
        emitters.remove(userId);
    }
}
src/main/java/stackpot/stackpot/notification/dto/NotificationResponseDto.java (1)

3-7: 클라이언트 응답에서 null 필드 제외 직렬화 권장(potId/feedId)

알림 유형에 따라 potId 또는 feedId가 null일 수 있습니다. 불필요한 null 노출을 줄이기 위해 NON_NULL 직렬화를 권장합니다.

+import com.fasterxml.jackson.annotation.JsonInclude;
@@
     @Builder
     @Getter
     @NoArgsConstructor
     @AllArgsConstructor
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class UnReadNotificationDto {
         private Long notificationId;
         private Long potId;
         private Long feedId;
         private String userName;
         private String type;
         private String content;
         private String createdAt;
     }

또한 createdAt 문자열 포맷(예: ISO 8601) 명세를 고정하고, 서비스단 정렬은 가능하면 변환 전(LocalDateTime) 기준으로 수행하는 것을 권장드립니다.

NotificationConverter.toUnReadNotificationDto(...)의 날짜 포맷(타임존 포함)과 API 문서의 표기 일관성을 확인해 주세요.

Also applies to: 17-24

src/main/java/stackpot/stackpot/feed/service/FeedQueryService.java (1)

12-14: FeedQueryService 시그니처에 seriesId nullability 및 하위호환성 보강 제안

인터페이스에 seriesId의 센티넬(0) 관례와 null 가능성을 명시하고, 기존 3-파라미터 시그니처를 default 메서드로 유지해 컴파일 타임 호환성을 보완하는 것을 권장합니다. 구현부는 가능하면 0 대신 null을 “전체 조회”로 통일해 센티넬 값을 회피하세요.

수정 대상:

  • src/main/java/stackpot/stackpot/feed/service/FeedQueryService.java
    • 메서드 시그니처에 @nullable 추가 및 import
    • 구 시그니처용 default 오버로드 추가
  • src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java
    • if (seriesId == 0) 대신 if (seriesId == null) 처리로 통일

제안 1) @nullable 명시 및 import 추가

-import java.util.Map;
+import org.springframework.lang.Nullable;
+import java.util.Map;

 public interface FeedQueryService {
-    UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize, Long seriesId);
+    UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize, @Nullable Long seriesId);

     // ... 생략 ...

-    UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize, Long seriesId);
+    UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize, @Nullable Long seriesId);
 }

제안 2) 구 시그니처 유지용 default 오버로드 추가

public interface FeedQueryService {
    // …기존 메서드 선언…

    /** 기존 3-파라미터 호출 호환성 보장 */
    default UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize) {
        return getFeedsByUserId(userId, nextCursor, pageSize, null);
    }
    default UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize) {
        return getFeeds(nextCursor, pageSize, null);
    }
}

Optional impl 리팩터:

  • FeedQueryServiceImpl.java 내 seriesId 처리
-    if (seriesId == 0) {
+    if (seriesId == null) {
         feeds = (nextCursor == null)
             ? feedRepository.findByUser_Id(userId, pageable)
             : feedRepository.findByUserIdAndFeedIdBefore(userId, nextCursor, pageable);
     } else {
         // seriesId 필터링 로직
     }
src/main/java/stackpot/stackpot/notification/event/PotEndEvent.java (1)

1-12: 이벤트 페이로드 안정성·결합도 개선(불변 필드/DTO 직접 의존 최소화)

이벤트는 되도록 불변이 안전합니다. 또한 표현 계층 DTO를 이벤트가 직접 들고 있으면 레이어 결합도가 올라갑니다.

권장사항:

  • 필드를 불변으로 지정.
  • 가능하면 이벤트에는 최소한의 도메인 데이터만 담고, DTO 변환은 핸들러에서 수행.

불변화 예시:

-    private Long potId;
-    private NotificationResponseDto.UnReadNotificationDto unReadNotificationDto;
+    private final Long potId;
+    private final NotificationResponseDto.UnReadNotificationDto unReadNotificationDto;

레이어 결합 완화 대안(별도 제안):

public class PotEndEvent {
    private final Long potId;
    private final Long notificationId;
    // 필요시 부가 식별자/원시 값만 포함
}
src/main/java/stackpot/stackpot/notification/entity/enums/NotificationType.java (2)

39-46: POT_END read 처리 추가 LGTM — 예외 메시지/리포지토리 배선만 최종 확인

패턴(각 타입이 자신의 read 구현)과 일관되고, idempotent 업데이트(true로 덮어쓰기)도 문제 없습니다. PotEndNotificationRepository 빈 등록과 트랜잭션 전파만 최종 확인 바랍니다.

추가 제안(선택):

  • 예외 메시지에 id 포함해 디버깅 용이성 향상.
  • 유닛 테스트: POT_END read 성공/미존재 예외 케이스 추가.

53-58: from(String) 매핑 견고성 강화(대소문자/양식 허용 및 null 가드)

FE가 영문 enum 값(POT_END)을 직접 보낼 가능성에 대비해 케이스를 추가하고, null/blank 입력 방어를 권장합니다.

가벼운 보강 예시:

-        return switch (type) {
+        if (type == null || type.isBlank()) {
+            throw new NotificationHandler(ErrorStatus.INVALID_NOTIFICATION_TYPE);
+        }
+        return switch (type.trim()) {
             case "팟 지원 알림" -> POT_APPLICATION;
             case "팟 댓글 알림" -> POT_COMMENT;
             case "피드 좋아요 알림" -> FEED_LIKE;
             case "피드 댓글 알림" -> FEED_COMMENT;
-            case "팟 종료 알림" -> POT_END;
+            case "팟 종료 알림", "POT_END" -> POT_END;
             default -> throw new NotificationHandler(ErrorStatus.INVALID_NOTIFICATION_TYPE);
         };

장기적으로는 각 enum에 label 필드를 두고 양방향 매핑(코드<->라벨)을 일원화하는 것이 안전합니다.

src/main/java/stackpot/stackpot/notification/controller/NotificationController.java (1)

34-37: API 문서화 보강 좋습니다

type 전달 규약을 명시해 통신 오류를 줄일 수 있습니다. 위의 NotificationType.from 보강(대소문자/영문 enum 허용, null 가드)과 함께 가면 더 견고해집니다.

src/main/java/stackpot/stackpot/pot/service/pot/PotCommandServiceImpl.java (2)

59-61: 이벤트 발행 책임 주입은 적절. 단, 트랜잭션 경계 이후 발행 보장 필요

ApplicationEventPublisher를 직접 주입해 즉시 이벤트를 발행하면, 리스너가 트랜잭션 커밋 전에 실행되어 아직 커밋되지 않은 데이터(예: 생성된 PotEndNotification)를 조회하지 못할 수 있습니다. 리스너 측에 @TransactionalEventListener(phase = AFTER_COMMIT)를 사용했는지 확인해 주세요. 없다면 AFTER_COMMIT 보장을 위한 전략을 도입하는 것을 권장합니다.


215-217: 팟 종료 알림 이벤트 발행 시점/널 가드 보강

  • createdPotEndNotification(...)이 현재는 null을 반환하지 않지만, 패턴상 일부 알림 생성 메서드는 조건에 따라 null을 돌려줍니다. 방어적으로 널 가드를 추가하는 편이 안전합니다.
  • 또한 이벤트 발행은 커밋 이후 실행되도록 보장(혹은 리스너에서 AFTER_COMMIT 사용)하세요.

예시 수정안:

-        NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createdPotEndNotification(potId);
-        applicationEventPublisher.publishEvent(new PotEndEvent(potId, dto));
+        NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createdPotEndNotification(potId);
+        if (dto != null) {
+            applicationEventPublisher.publishEvent(new PotEndEvent(potId, dto));
+        }
src/main/java/stackpot/stackpot/notification/repository/FeedLikeNotificationRepository.java (1)

15-21: 7-파라미터 프로젝션으로의 정렬 일치 확인

NotificationDto.UnReadNotificationDto(알림ID, potId, feedId, userName, type, content, createdAt) 순서에 맞게 JPQL이 구성되어 있어 일관성 좋습니다. userName은 원시 닉네임을 제공하고, NotificationConverter에서 접미사(" 새싹")를 부여하는 패턴을 유지하면 중복 접미사 문제를 피할 수 있습니다.

정렬 보장을 위해 최신순 정렬을 추가하는 것도 고려해 보세요.

-            "WHERE fln.isRead = false and fln.feedLike.feed.user.id = :userId ")
+            "WHERE fln.isRead = false and fln.feedLike.feed.user.id = :userId " +
+            "ORDER BY fln.createdAt DESC")
src/main/java/stackpot/stackpot/notification/repository/PotApplicationNotificationRepository.java (1)

16-22: JPQL에서 메시지 문자열 하드코딩(CONCAT) → i18n/일관성 이슈 가능

한글 문구를 JPQL에 직접 하드코딩하고 CONCAT으로 조합하면 다국어(i18n) 적용과 문구 통일이 어려워집니다. 또한 닉네임에 "새싹" 접미사를 부여하는 규칙(RoleNameMapper.getWriterNickname)과 충돌(중복 표기)할 소지도 있습니다. 메시지 조합은 Converter/Service 레이어에서 통일된 헬퍼(예: RoleNameMapper 활용)로 처리하고, JPQL은 최소 필드만 투영하는 구조로 정리하는 것을 권장드립니다.

src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java (2)

183-183: 읽기 전용 메서드에 @transactional 적용 방식 개선

getFeedsByUserId는 조회 전용이므로 readOnly 트랜잭션이 더 적합합니다. jakarta.transaction.Transactional에는 readOnly 옵션이 없으므로 Spring @transactional(readOnly = true)로 교체하거나 애노테이션을 제거하는 것을 권장합니다.

적용 예시(해당 메서드 한정):

-    @Transactional
+    @Transactional(readOnly = true)
     public UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize, Long seriesId) {

추가로, 파일 상단 import도 Spring 트랜잭션 애노테이션으로 교체해야 합니다:

// 교체 필요
import org.springframework.transaction.annotation.Transactional;

205-208: 주석 처리된 오래된 코드 제거 권장

주석으로 남아있는 이전 쿼리 경로는 혼란을 초래할 수 있습니다. Git 이력이 있으므로 과감히 삭제하는 편이 가독성과 유지보수성에 유리합니다.

-//        List<Feed> feeds = (nextCursor == null)
-//                ? feedRepository.findByUser_Id(userId, pageable)
-//                : feedRepository.findByUserIdAndFeedIdBefore(userId, nextCursor, pageable);
src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java (1)

83-86: 에러 코드의 도메인 구체화 제안

COMPLETED 상태가 아닐 때 _BAD_REQUEST 대신 도메인 특화 에러(예: POT_NOT_COMPLETED 등)를 정의/사용하면 클라이언트 분기가 명확해집니다. 현재 공용 코드를 유지해야 한다면 그대로 두셔도 됩니다.

src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java (5)

16-19: 와일드카드 임포트 지양 권장

entity.*, repository.* 와일드카드는 충돌 가능성과 리뷰 난이도를 높입니다. 변경 폭이 큰 PR(merge)일수록 명시 임포트가 안전합니다.


91-94: 알림 본문에 댓글 내용 직접 연결 — 구분자/길이 제한 권장

현재 "…달렸어요." + comment로 바로 연결되어 가독성 저하 및 과도한 길이(푸시/SSE 페이로드 초과) 문제가 생길 수 있습니다. 간단히 구분자 추가 및 미리보기 길이 제한을 권장합니다.

-                "팟 댓글 알림", potComment.getUser().getNickname() + "새싹님의 댓글이 달렸어요." + potComment.getComment(), newPcn.getCreatedAt());
+                "팟 댓글 알림", potComment.getUser().getNickname() + "새싹님의 댓글이 달렸어요: " + previewComment(potComment.getComment(), 60), newPcn.getCreatedAt());
-                "피드 댓글 알림", feedComment.getUser().getNickname() + "새싹님의 댓글이 달렸어요." + feedComment.getComment(), newFcn.getCreatedAt());
+                "피드 댓글 알림", feedComment.getUser().getNickname() + "새싹님의 댓글이 달렸어요: " + previewComment(feedComment.getComment(), 60), newFcn.getCreatedAt());

도우미 메서드(동일 클래스 내 private 정적 메서드로 제안):

private static String previewComment(String text, int limit) {
    if (text == null) return "";
    String s = text.strip();
    return s.length() > limit ? s.substring(0, limit) + "…" : s;
}

Also applies to: 137-140


112-115: 피드 좋아요 알림 문구/서식 일관화

다른 알림과 동일하게 구두점·구분자를 일관화하면 UX가 좋아집니다. 위 댓글 알림과 동일한 포맷 가이드를 적용하는 것을 권장합니다.


56-69: 쓰기 메서드에 @transactional 누락 — 일관 적용 권장

저장 로직이 있는 퍼블릭 서비스 메서드에 @Transactional을 통일 적용하면 예외 시 롤백 보장이 명확해집니다. 현재 delete@transactional인데 create는 일부 미적용입니다.

예시:

-    public NotificationResponseDto.UnReadNotificationDto createPotApplicationNotification(Long potId, Long applicationId, String userName) {
+    @Transactional
+    public NotificationResponseDto.UnReadNotificationDto createPotApplicationNotification(Long potId, Long applicationId, String userName) {

같은 방식으로 createPotCommentNotification, createFeedLikeNotification, create(d)FeedCommentNotification, createdPotEndNotification에 적용 권장.

Also applies to: 76-94, 101-115, 123-140, 147-157


147-157: 팟 종료 알림: 발신자 표시명 및 트랜잭션 경계 확인 요청

  • 발신자(표시명)
    • 현재 pot.getUser().getNickname() + "새싹"으로 설정되어 있습니다.
    • 기획 상 “팟 소유자”가 직접 보내는 알림인지, 시스템(또는 팟 자체)에서 전원에게 보내는 알림인지 확인이 필요합니다.
    • 시스템 알림이라면 "STACKPOT" 또는 팟 이름(pot.getPotName())으로 대체하는 것이 더 자연스럽습니다.

  • 트랜잭션 경계
    createdPotEndNotification 메서드는 potEndNotificationRepository.save(...)로 DB에 쓰기를 수행합니다.
    • 같은 클래스의 deletePotEndNotification 메서드에는 이미 @Transactional이 적용되어 있으므로, 일관성을 위해 아래처럼 @Transactional을 추가할 것을 권장합니다.

    파일: src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java
    라인: 147–157

    @@ -147,7 +147,9 @@ public class NotificationCommandService {
    -    public NotificationResponseDto.UnReadNotificationDto createdPotEndNotification(Long potId) {
    +    @Transactional
    +    public NotificationResponseDto.UnReadNotificationDto createdPotEndNotification(Long potId) {
           Pot pot = potQueryService.getPotByPotId(potId);
           …
src/main/java/stackpot/stackpot/user/controller/UserController.java (4)

20-20: 와일드카드 임포트 지양 권장

stackpot.stackpot.pot.dto.* 대신 실제 사용 DTO만 명시 임포트하면 API 스펙 파악과 충돌 회피에 유리합니다.


435-436: size 파라미터 상한선 권장(과도한 페이지 크기 방지)

defaultValue="10"만으론 대용량 요청 방지가 어렵습니다. 상한(ex. 50/100)을 정해 초과 시 클램핑 또는 400을 반환하세요.

예시(간단 클램핑):

int pageSize = Math.min(size, 50);

혹은 Bean Validation을 사용할 경우(클래스에 @Validated 추가 필요):

@RequestParam(name="size", defaultValue="10") @Max(50) int size

Also applies to: 454-455


435-440: seriesId 매직 넘버 제거 및 null로 표현 권장
defaultValue = "0" 대신 required = false만 남겨두어, null을 “전체”로 해석하도록 변경하세요.
FeedQueryService의 getFeedsByUserId(Long userId, Long nextCursor, int pageSize, Long seriesId) 시그니처는 이미 Long seriesId로 null 처리가 가능하므로 별도 수정이 필요 없습니다.

수정 예시 (UserController.java:435–440):

-    @RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId
+    @RequestParam(value = "seriesId", required = false) Long seriesId

454-458: seriesId 기본값 0 제거 시 서비스 로직 null 처리 추가 필요
defaultValue="0"를 제거하면 컨트롤러에서 seriesIdnull이 되고, 서비스 쪽 if (seriesId == 0) 검사에서 NullPointerException이 발생합니다. 컨트롤러뿐만 아니라 서비스 구현도 함께 null 체크로 단순화하세요.

  • 수정 위치
    • src/main/java/stackpot/stackpot/user/controller/UserController.java: 라인 454–458
    • src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java: 라인 250–259 (getFeeds)
    • src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java: 라인 204–217 (getFeedsByUserId)
// UserController.java
- @RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId
+ @RequestParam(value = "seriesId", required = false) Long seriesId
// FeedQueryServiceImpl.getFeeds(...)
- if (seriesId == 0) {
+ if (seriesId == null) {
      // 전체 피드 조회
  } else {
      // 시리즈별 피드 조회
  }
// FeedQueryServiceImpl.getFeedsByUserId(...)
- if (seriesId == 0) {
+ if (seriesId == null) {
      // 전체 피드 조회
  } else {
      // 시리즈별 피드 조회
  }
📜 Review details

Configuration used: Path: .coderabbit.yaml

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 e47b7eb and cbdf24f.

⛔ Files ignored due to path filters (1)
  • src/main/resources/global-bundle.pem is excluded by !**/*.pem
📒 Files selected for processing (28)
  • build.gradle (1 hunks)
  • src/main/java/stackpot/stackpot/event/SseService.java (3 hunks)
  • src/main/java/stackpot/stackpot/feed/repository/FeedRepository.java (1 hunks)
  • src/main/java/stackpot/stackpot/feed/service/FeedCommandServiceImpl.java (2 hunks)
  • src/main/java/stackpot/stackpot/feed/service/FeedQueryService.java (1 hunks)
  • src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java (3 hunks)
  • src/main/java/stackpot/stackpot/notification/controller/NotificationController.java (2 hunks)
  • src/main/java/stackpot/stackpot/notification/converter/NotificationConverter.java (2 hunks)
  • src/main/java/stackpot/stackpot/notification/dto/NotificationDto.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/dto/NotificationResponseDto.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/entity/PotEndNotification.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/entity/enums/NotificationType.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/event/PotEndEvent.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/repository/FeedLikeNotificationRepository.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/repository/PotApplicationNotificationRepository.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/repository/PotCommentNotificationRepository.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/repository/PotEndNotificationRepository.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java (6 hunks)
  • src/main/java/stackpot/stackpot/notification/service/NotificationQueryService.java (3 hunks)
  • src/main/java/stackpot/stackpot/pot/converter/PotConverter.java (1 hunks)
  • src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (1 hunks)
  • src/main/java/stackpot/stackpot/pot/dto/PotSummaryDto.java (1 hunks)
  • src/main/java/stackpot/stackpot/pot/service/pot/MyPotService.java (1 hunks)
  • src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java (3 hunks)
  • src/main/java/stackpot/stackpot/pot/service/pot/PotCommandServiceImpl.java (4 hunks)
  • src/main/java/stackpot/stackpot/user/controller/UserController.java (8 hunks)
  • src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/main/java/stackpot/stackpot/notification/event/PotEndEvent.java (1)
src/main/java/stackpot/stackpot/notification/dto/NotificationResponseDto.java (1)
  • NotificationResponseDto (11-26)
src/main/java/stackpot/stackpot/notification/entity/PotEndNotification.java (3)
src/main/java/stackpot/stackpot/notification/event/PotEndEvent.java (1)
  • Getter (7-12)
src/main/java/stackpot/stackpot/notification/dto/NotificationDto.java (1)
  • Builder (13-25)
src/main/java/stackpot/stackpot/notification/dto/NotificationResponseDto.java (1)
  • Builder (13-25)
src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (1)
src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java (1)
  • RoleNameMapper (7-43)
src/main/java/stackpot/stackpot/notification/repository/PotEndNotificationRepository.java (1)
src/main/java/stackpot/stackpot/notification/dto/NotificationDto.java (1)
  • NotificationDto (11-26)
src/main/java/stackpot/stackpot/pot/service/pot/PotCommandServiceImpl.java (3)
src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java (1)
  • Service (26-164)
src/main/java/stackpot/stackpot/feed/service/FeedCommentCommandService.java (1)
  • Service (22-97)
src/main/java/stackpot/stackpot/notification/dto/NotificationResponseDto.java (1)
  • NotificationResponseDto (11-26)
src/main/java/stackpot/stackpot/notification/converter/NotificationConverter.java (1)
src/main/java/stackpot/stackpot/notification/dto/NotificationResponseDto.java (1)
  • NotificationResponseDto (11-26)
src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java (1)
src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java (1)
  • RoleNameMapper (7-43)
🔇 Additional comments (17)
src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (2)

58-58: owner 동적 반영 굿

기존 상수값(true) 고정에서 실제 entity.isOwner() 반영으로 수정한 점 좋아요. 도메인 일관성과 유지보수성 모두 개선됩니다.


16-25: 잘못된 지적: PotRole Enum이 별도로 존재하지 않으며, 둘 다 Role 타입을 사용합니다.

해당 코드를 확인한 결과,

  • PotMember.roleName 필드 타입은 stackpot.stackpot.user.entity.enums.Role 입니다.
  • PotApplication.getPotRole() 역시 같은 Role Enum을 반환하도록 정의되어 있습니다.

따라서 원 리뷰에서 우려하신 “PotRole vs User.Role 타입 혼용” 문제는 존재하지 않으며, 별도 매핑 함수나 null 처리 없이도 타입·의미 일관성이 유지됩니다.

Likely an incorrect or invalid review comment.

src/main/java/stackpot/stackpot/feed/service/FeedCommandServiceImpl.java (1)

15-15: 스프링 @transactional로 통일 OK

프레임워크 일관성이 좋아졌습니다.

src/main/java/stackpot/stackpot/notification/service/NotificationQueryService.java (1)

34-34: PotEndNotificationRepository 주입 및 메서드 시그니처 확인 완료 — 인덱스 최적화 검토 필요

NotificationQueryService.java에서 potEndNotificationRepository 주입이 올바르게 이루어졌으며,
PotEndNotificationRepository 인터페이스(src/main/java/.../PotEndNotificationRepository.java:12)와
findAllUnReadPotEndNotificationsByUserId(Long userId) 메서드 선언(src/main/java/.../PotEndNotificationRepository.java:25)을 확인했습니다.

대량 알림 처리 성능을 위해 아래 항목을 검토 부탁드립니다.

  • USER_ID 컬럼 또는 JOIN 경로(pen.pot.potMembers.user.id)에 적절한 DB 인덱스 생성 여부
  • 인덱스 생성 시점과 운영 환경 영향 평가
src/main/java/stackpot/stackpot/feed/repository/FeedRepository.java (1)

57-60: 시리즈 필터 페이징: 인덱스·정렬·테스트 추가 검토 필요

다음 사항을 코드베이스에서 확인했습니다. 개선을 위해 검토가 필요합니다:

  • Feed 엔티티에 복합 인덱스 선언이 없습니다
    • 파일: src/main/java/stackpot/stackpot/feed/entity/Feed.java
    @Table(indexes = …) 또는 @Index 애노테이션 미발견

  • Repository 메서드 테스트 케이스 부재
    src/test/java 경로에 FeedRepository 관련 테스트 파일 미발견

  • DB 마이그레이션 스크립트(예: Flyway/Liquibase)에서 인덱스 생성 SQL 부재
    src/main/resources/db/migration 또는 .sql 파일 검색 시 인덱스 생성 문구 없음

권장사항 (선택적 리팩토링):

  • 성능: user_id, series_id, feed_id(DESC) 순서의 복합 인덱스 추가 검토
  • 정렬: cursor 기반 페이징 일관성 확보를 위해 Pageable 기본 정렬(feedId DESC) 보장
  • 테스트: series 필터 유/무 및 cursor 유/무 조합별 통합 테스트 작성
  • 동작 명세: series가 null인 경우 제외 여부 명확화

필요 시 구현, 마이그레이션, 테스트 작성에 대한 도움을 드릴 수 있습니다.

src/main/java/stackpot/stackpot/pot/service/pot/PotCommandServiceImpl.java (1)

288-288: LGTM

반환값 처리만 수정된 것으로 보이며 동작에 문제 없습니다.

src/main/java/stackpot/stackpot/pot/converter/PotConverter.java (1)

134-141: 모든 PotConverter.toDto 호출이 새 시그니처로 이행됨 확인
rg 검증 결과, PotConverter.toDto(Pot) 단일 인자 호출은 더 이상 존재하지 않습니다.

src/main/java/stackpot/stackpot/notification/repository/PotEndNotificationRepository.java (1)

41-44: 삭제 쿼리 적합

특정 팟의 종료 알림 일괄 삭제 요구사항에 부합합니다. 트랜잭션 경계 내 호출만 보장해 주세요.

src/main/java/stackpot/stackpot/notification/repository/PotApplicationNotificationRepository.java (1)

16-22: 확인 완료: UnReadNotificationDto 생성자와 JPQL 파라미터 일치

UnReadNotificationDto가 가진 7개의 필드 순서와 타입(Long, Long, Long, String, String, String, LocalDateTime)이
PotApplicationNotificationRepository의 JPQL 프로젝션(pan.id, pan.potApplication.pot.potId, null, pan.potApplication.user.nickname, '팟 지원 알림', CONCAT(...), pan.createdAt)과 완전히 일치함을 확인했습니다.
따라서 InstantiationException/IllegalArgumentException 발생 우려가 없습니다.

src/main/java/stackpot/stackpot/notification/repository/PotCommentNotificationRepository.java (1)

16-31: 명시적 JOIN과 수신자 판별 조건 로직 정교화가 적절합니다

top-level 댓글과 대댓글 케이스를 분기해 pot 소유자/부모 댓글 작성자에게 도달하도록 한 WHERE 조건이 명확하며, 7-인자 DTO 프로젝션과도 일치합니다. 현재 요구사항에 부합하는 구현으로 보입니다.

src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java (1)

209-217: seriesId null 안전 보장 — Controller에서 항상 기본값 0L 전달
현재 getFeedsByUserIdgetFeeds 호출부 둘 다

@RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId

를 사용해 seriesId가 무조건 0L로 초기화되므로, 서비스 레이어에 null이 전달되지 않아 NPE 우려가 없습니다.

• UserController.java:427–437 (getFeedsByUserId)
• UserController.java:452–460 (getFeeds)

따라서 제안된 seriesId == null 가드는 불필요합니다.

Likely an incorrect or invalid review comment.

src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java (1)

158-162: 요약 DTO에 isMember 반영은 사용자 경험 향상에 유용합니다

요약 응답에 멤버 여부를 포함하면 프론트에서 액션 가이드를 분기하기 쉬워집니다. 변경 방향에 동의합니다.

src/main/java/stackpot/stackpot/notification/service/NotificationCommandService.java (4)

37-37: PotQueryService 주입 추가 LGTM

팟 종료 알림 플로우에 필요한 의존성 추가가 적절합니다.


43-43: PotEndNotificationRepository 주입 추가 LGTM

종료 알림 저장소 도입이 의도와 일치합니다.


67-69: toUnReadNotificationDto 파라미터 일치 확인 완료

NotificationConverter.java의 메소드 선언부(라인 29–31)와 NotificationCommandService.java의 호출부(라인 66–68, 91–93, 112–114, 137–139, 154–156)에서 전달하는 7개 인자(Long notificationId, Long potId, Long feedId, String userName, String type, String content, LocalDateTime createdAt)의 개수·순서·타입이 모두 일치함을 확인했습니다.
dev→main 병합 후에도 런타임 오류 우려가 없습니다.


78-83: 리뷰 코멘트 검토: User 엔티티에 iduserId는 서로 다른 필드입니다.
현재 코드베이스에서

  • getId()는 DB PK(Long id)를,
  • getUserId()는 서비스 레벨의 비즈니스 식별자(예: OAuth 공급자 ID 등)를 반환합니다.

따라서 “모두 getId()로 통일하라”는 제안은 잘못된 전제이며, 전역 리팩토링은 오히려 의도된 동작을 깨뜨릴 위험이 큽니다.
특정 알림 생성 로직에서 DB ID 비교가 필요할 때만 getId()를, 비즈니스 식별자로 토큰 생성·로그 등에 활용할 때만 getUserId()를 사용하도록 코드를 유지하세요.

Likely an incorrect or invalid review comment.

src/main/java/stackpot/stackpot/user/controller/UserController.java (1)

375-387: 특정 사용자 소개 조회 추가 LGTM

나/타 사용자 분리된 조회 라인이 명확합니다. 서비스 예외 전파 시 에러 코드 문서화가 되어 있어 일관됩니다.

Comment on lines +92 to 95
//hibernate
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.hibernate.common:hibernate-commons-annotations:6.0.6.Final'
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🛠️ Refactor suggestion

🧩 Analysis chain

JPA 스타터 중복 및 하이버네이트 개별 의존성 수동 지정은 제거 권장

  • spring-boot-starter-data-jpa 가 이미 Line 28에 선언되어 있습니다. 중복 선언은 버전 충돌/중복 의존성 증가를 유발할 수 있습니다.
  • hibernate-commons-annotations는 Boot BOM이 관리합니다. 특별한 이슈가 없다면 수동 버전 고정은 지양하세요.
-    //hibernate
-    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
-    implementation 'org.hibernate.common:hibernate-commons-annotations:6.0.6.Final'

중복/충돌 가능성을 빠르게 점검하려면:

추가 정리 예시(참고용): com.mysql:mysql-connector-j 하나만 유지하고, mysql:mysql-connector-java는 제거하는 구성이 최신 권장입니다.

dependencies {
  runtimeOnly 'com.mysql:mysql-connector-j'
  // mysql:mysql-connector-java 는 제거
}

🏁 Script executed:

#!/bin/bash
# JPA 스타터 중복 여부
rg -n "spring-boot-starter-data-jpa" build.gradle -n

# MySQL 커넥터 중복/혼재 여부
rg -n "(com\.mysql:mysql-connector-j|mysql:mysql-connector-java)" build.gradle -n

Length of output: 494


🏁 Script executed:

#!/bin/bash
# 하이버네이트 수동 버전 지정 위치 확인
rg -n "hibernate-commons-annotations" build.gradle -n

Length of output: 137


중복 및 불필요 의존성 제거 필요

아래 사항들을 반영해 빌드 스크립트에서 중복·불필요 의존성을 제거해주세요.

  • spring-boot-starter-data-jpa 중복
    • 28줄에 이미 선언된 JPA 스타터를 93줄(또는 마지막 deps 블록)에서 다시 선언하고 있습니다. 중복 선언은 버전 충돌 및 빌드 속도 저하를 유발하므로 제거가 필요합니다.
  • hibernate-commons-annotations 수동 버전 고정
    • 94줄의 org.hibernate.common:hibernate-commons-annotations:6.0.6.Final 은 Spring Boot BOM이 관리하는 항목입니다. 특별한 이슈가 없다면 수동 버전 고정 대신 BOM에 위임하세요.
  • MySQL 커넥터 중복/혼재
    • 37줄에 com.mysql:mysql-connector-j 를 선언하면서
    • 38, 62, 78줄에 mysql:mysql-connector-java:8.0.33 을 여러 차례 선언하고 있습니다.
    runtimeOnly 'com.mysql:mysql-connector-j' 하나만 남기고 나머지는 모두 제거하세요.
 dependencies {
-   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
-   implementation 'org.hibernate.common:hibernate-commons-annotations:6.0.6.Final'
-   runtimeOnly 'mysql:mysql-connector-java:8.0.33'
-   implementation 'mysql:mysql-connector-java:8.0.33'
-   implementation 'mysql:mysql-connector-java:8.0.33'
+   // spring-boot-starter-data-jpa, hibernate-commons-annotations: BOM 관리
+   // mysql-connector-java: com.mysql:mysql-connector-j 로 통일
    runtimeOnly 'com.mysql:mysql-connector-j'
 }
📝 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
//hibernate
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.hibernate.common:hibernate-commons-annotations:6.0.6.Final'
}
dependencies {
// spring-boot-starter-data-jpa, hibernate-commons-annotations: BOM 관리
// mysql-connector-java: com.mysql:mysql-connector-j 로 통일
runtimeOnly 'com.mysql:mysql-connector-j'
}

Comment on lines 251 to 259
if (seriesId == 0) {
feeds = (nextCursor == null)
? feedRepository.findByUser_Id(user.getId(), pageable)
: feedRepository.findByUserIdAndFeedIdBefore(user.getId(), nextCursor, pageable);
} else {
feeds = (nextCursor == null)
? feedRepository.findByUser_IdAndSeries_SeriesId(user.getId(), seriesId, pageable)
: feedRepository.findByUser_IdAndSeries_SeriesIdAndFeedIdBefore(user.getId(), seriesId, nextCursor, pageable);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

getFeeds에서도 동일한 NPE 가능성 — 동일 가드 적용 필요

위와 동일하게 seriesId == 0 비교에 null이 들어오면 NPE가 발생합니다. 동일한 방어 로직을 적용해 주세요.

-        if (seriesId == 0) {
+        if (seriesId == null || seriesId == 0L) {
             feeds = (nextCursor == null)
                     ? feedRepository.findByUser_Id(user.getId(), pageable)
                     : feedRepository.findByUserIdAndFeedIdBefore(user.getId(), nextCursor, pageable);
         } else {
             feeds = (nextCursor == null)
                     ? feedRepository.findByUser_IdAndSeries_SeriesId(user.getId(), seriesId, pageable)
                     : feedRepository.findByUser_IdAndSeries_SeriesIdAndFeedIdBefore(user.getId(), seriesId, nextCursor, pageable);
         }
📝 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
if (seriesId == 0) {
feeds = (nextCursor == null)
? feedRepository.findByUser_Id(user.getId(), pageable)
: feedRepository.findByUserIdAndFeedIdBefore(user.getId(), nextCursor, pageable);
} else {
feeds = (nextCursor == null)
? feedRepository.findByUser_IdAndSeries_SeriesId(user.getId(), seriesId, pageable)
: feedRepository.findByUser_IdAndSeries_SeriesIdAndFeedIdBefore(user.getId(), seriesId, nextCursor, pageable);
}
// guard against null or "0" seriesId to avoid NPE on unboxing
if (seriesId == null || seriesId == 0L) {
feeds = (nextCursor == null)
? feedRepository.findByUser_Id(user.getId(), pageable)
: feedRepository.findByUserIdAndFeedIdBefore(user.getId(), nextCursor, pageable);
} else {
feeds = (nextCursor == null)
? feedRepository.findByUser_IdAndSeries_SeriesId(user.getId(), seriesId, pageable)
: feedRepository.findByUser_IdAndSeries_SeriesIdAndFeedIdBefore(user.getId(), seriesId, nextCursor, pageable);
}
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java around
lines 251-259, the code compares seriesId to 0 which will throw an NPE if
seriesId is null; update the guard to check for null first (e.g., if (seriesId
== null || seriesId == 0)) and branch accordingly so null is handled the same
way as 0 before calling repository methods.


@RestController
@RequestMapping("/api/notifications")
@RequestMapping("/notifications")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

베이스 경로 변경은 외부 호환성 파괴 — 기존 경로 병행 제공 권장

/api/notifications → /notifications 변경은 클라이언트/게이트웨이/문서 전반에 영향이 큽니다. 최소 한 리리스 동안 구 경로를 병행 제공해 다운타임/배포 순서 문제를 방지하세요.

안전한 이행을 위한 최소 수정:

-@RequestMapping("/notifications")
+@RequestMapping({"/notifications", "/api/notifications"})

체크리스트:

  • API 게이트웨이/리버스 프록시 라우팅 업데이트
  • Swagger/OpenAPI 서버 URL/예제 경로 수정
  • 클라이언트 앱/SDK 호출 경로 동시 지원 기간 공지
📝 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
@RequestMapping("/notifications")
@RequestMapping({"/notifications", "/api/notifications"})

Comment on lines 15 to 31
@Query("SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
"fcn.id, fcn.feedComment.feed.feedId, " +
"fcn.feedComment.user.nickname, 'FeedComment', fcn.feedComment.comment, fcn.createdAt) " +
"fcn.id, " +
"null, " +
"f.feedId, " +
"u.nickname, " +
"'피드 댓글 알림', " +
"CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. ', fc.comment), " +
"fcn.createdAt) " +
"FROM FeedCommentNotification fcn " +
"WHERE fcn.isRead = false AND (" +
" (fcn.feedComment.parent is null AND fcn.feedComment.feed.user.id = :userId) OR " +
" (fcn.feedComment.parent is not null AND " +
" (fcn.feedComment.parent.user.id = :userId OR fcn.feedComment.feed.user.id = :userId)" +
" ))")
"JOIN fcn.feedComment fc " +
"JOIN fc.user u " +
"JOIN fc.feed f " +
"LEFT JOIN fc.parent fcp " +
"WHERE fcn.isRead = false AND " +
"((fc.parent is null AND f.user.id = :userId) OR " +
" (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")
List<NotificationDto.UnReadNotificationDto> findAllUnReadNotificationsByUserId(Long userId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

@query에서 명명 파라미터(:userId) 사용하지만 메서드 파라미터에 @param 누락 — 런타임 바인딩 오류 위험

스프링이 컴파일러의 -parameters 설정을 사용하지 않는 환경이라면, @param 누락으로 인해 :userId 바인딩에 실패할 수 있습니다. 다른 리포지토리(PotCommentNotificationRepository 등)와도 일관되게 @param을 추가해 주세요.

-    List<NotificationDto.UnReadNotificationDto> findAllUnReadNotificationsByUserId(Long userId);
+    List<NotificationDto.UnReadNotificationDto> findAllUnReadNotificationsByUserId(@Param("userId") Long userId);
📝 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
@Query("SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
"fcn.id, fcn.feedComment.feed.feedId, " +
"fcn.feedComment.user.nickname, 'FeedComment', fcn.feedComment.comment, fcn.createdAt) " +
"fcn.id, " +
"null, " +
"f.feedId, " +
"u.nickname, " +
"'피드 댓글 알림', " +
"CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. ', fc.comment), " +
"fcn.createdAt) " +
"FROM FeedCommentNotification fcn " +
"WHERE fcn.isRead = false AND (" +
" (fcn.feedComment.parent is null AND fcn.feedComment.feed.user.id = :userId) OR " +
" (fcn.feedComment.parent is not null AND " +
" (fcn.feedComment.parent.user.id = :userId OR fcn.feedComment.feed.user.id = :userId)" +
" ))")
"JOIN fcn.feedComment fc " +
"JOIN fc.user u " +
"JOIN fc.feed f " +
"LEFT JOIN fc.parent fcp " +
"WHERE fcn.isRead = false AND " +
"((fc.parent is null AND f.user.id = :userId) OR " +
" (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")
List<NotificationDto.UnReadNotificationDto> findAllUnReadNotificationsByUserId(Long userId);
@Query("SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
"fcn.id, " +
"null, " +
"f.feedId, " +
"u.nickname, " +
"'피드 댓글 알림', " +
"CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. ', fc.comment), " +
"fcn.createdAt) " +
"FROM FeedCommentNotification fcn " +
"JOIN fcn.feedComment fc " +
"JOIN fc.user u " +
"JOIN fc.feed f " +
"LEFT JOIN fc.parent fcp " +
"WHERE fcn.isRead = false AND " +
"((fc.parent is null AND f.user.id = :userId) OR " +
" (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")
List<NotificationDto.UnReadNotificationDto> findAllUnReadNotificationsByUserId(@Param("userId") Long userId);
🤖 Prompt for AI Agents
In
src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java
around lines 15 to 31, the JPQL uses a named parameter :userId but the
repository method parameter lacks a @Param annotation which can cause runtime
binding failures in environments without -parameters; update the method
signature to annotate the Long userId parameter with @Param("userId") and add
the necessary import (org.springframework.data.repository.query.Param) so Spring
can bind the named parameter correctly, matching conventions used in other
repositories.

Comment on lines +14 to +25
@Query("SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
"pen.id, " +
"pen.pot.potId, " +
"null, " +
"null, " +
"'팟 종료 알림', " +
"CONCAT(pen.pot.potName, '이 다 끓었어요. 내 역할을 소개해 보세요!'), " +
"pen.createdAt) " +
"FROM PotEndNotification pen " +
"JOIN pen.pot.potMembers pm " +
"WHERE pen.isRead = false AND pm.user.id = :userId")
List<NotificationDto.UnReadNotificationDto> findAllUnReadPotEndNotificationsByUserId(@Param("userId") Long userId);
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

userName을 null로 투영 → "null 새싹" 노출 버그

현재 userName 인자를 null로 생성하고 있어, 컨버터에서 " 새싹" 접미사를 붙일 때 "null 새싹"이 화면에 노출될 수 있습니다. 알림의 주체가 팟 생성자라면 해당 닉네임을 투영해야 합니다.

-            "pen.pot.potId, " +
-            "null, " +
-            "null, " +
+            "pen.pot.potId, " +
+            "null, " +
+            "pen.pot.user.nickname, " +
             "'팟 종료 알림', " +
             "CONCAT(pen.pot.potName, '이 다 끓었어요. 내 역할을 소개해 보세요!'), " +

또한 중복 멤버 조인으로 동일 사용자에게 중복 행이 반환되지 않는지(동일 유저가 potMembers에 단 한 번만 포함된다는 가정) 확인 부탁드립니다. 필요시 DISTINCT를 추가해 중복을 제거할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# Verify the UnReadNotificationDto constructor signature
rg -n "class UnReadNotificationDto" -C5 --type java
rg -nP "UnReadNotificationDto\\s*\\(" -C3 --type java

# Inspect the PotMember entity for uniqueness constraints
rg -n "class PotMember" -C5 --type java
rg -n "@Table" -C5 --type java | rg -C3 -i "PotMember"

# Check the mapping of potMembers in Pot entity
rg -n "potMembers" -C3 --type java

Length of output: 42792


팟 종료 알림 userName 투영 누락 및 중복 조인 검토 필요

  • PotEndNotificationRepository의 JPQL SELECT 절에서 userName을 null로 투영하고 있어, 프론트엔드에서 “null 새싹” 버그가 발생합니다.
    null 대신 pen.pot.user.nickname을 투영하도록 수정해주세요.
    (파일: src/main/java/stackpot/stackpot/notification/repository/PotEndNotificationRepository.java, 14–25행)

  • JOIN pen.pot.potMembers pm 구문으로 조회 시, 대상 사용자가 potMembers에 한 번만 매핑된다는 가정을 확인해주세요.
    만약 중복 행이 반환될 가능성이 있다면 JPQL에 DISTINCT를 추가해 중복을 제거하는 것을 권장드립니다.

🤖 Prompt for AI Agents
In
src/main/java/stackpot/stackpot/notification/repository/PotEndNotificationRepository.java
around lines 14–25, the JPQL SELECT currently projects a null for the userName
causing frontend "null 새싹" bugs; change that projection to pen.pot.user.nickname
instead of null, and review the JOIN pen.pot.potMembers pm to ensure the target
user appears only once—if duplicate rows are possible, add DISTINCT to the
SELECT (or use SELECT DISTINCT new ...) to eliminate duplicates while keeping
the same projection and WHERE filter by :userId.

Comment on lines 62 to 66
public PotMemberInfoResponseDto toKaKaoMemberDto(PotMember entity) {
String roleName = entity.getPotApplication() != null
? entity.getPotApplication().getPotRole().name()
String roleName = entity.getRoleName() != null
? entity.getRoleName().name()
: "멤버";

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

외부 계약 확인 필요: potRole에 기본값 "멤버" 주입은 프로토콜 위반 소지

현재 기본값이 "멤버" 문자열(로컬라이즈된 표시명)에 가깝습니다. 외부/프론트 계약이 코드 값(예: BACKEND/FRONTEND/…)을 기대한다면, null 또는 정의된 "UNKNOWN" 코드로 내려야 합니다. 계약이 문자열 표시명을 기대한다면 현 구현이 맞지만, 혼동되면 장애 포인트가 됩니다.

다음 스크립트로 potRole 사용처/계약을 빠르게 훑어보세요.

계약이 코드 값을 요구한다면 아래처럼 기본값만 조정하세요.

-        String roleName = entity.getRoleName() != null
-                ? entity.getRoleName().name()
-                : "멤버";
+        String roleName = (entity.getRoleName() != null) ? entity.getRoleName().name() : null; // 표시명은 아래에서 별도 처리

🏁 Script executed:

#!/bin/bash
# potRole이 어디서 어떤 값(코드/표시명)으로 소비되는지 확인
rg -n -C3 '\bpotRole\b' --type=java
# Kakao 관련 전송/직렬화 경로에서 potRole 기대값 확인
rg -n -C3 'Kakao|KaKao|카카오' --type=java

Length of output: 34188


PotMemberConverter의 potRole 기본값 로직 수정 필요

현재 toKaKaoMemberDto 메서드에서 entity.getRoleName()null인 경우에 "멤버"(디스플레이명)를 그대로 potRole 필드에 할당하고 있습니다. 하지만 API 계약상 potRole은 클라이언트·프론트엔드가 처리 가능한 코드 값(예: BACKEND, FRONTEND 등)이어야 하며, "멤버"와 같은 로컬라이즈된 표시명은 유효한 코드가 아닙니다.
이로 인해

  • 클라이언트에서 코드 기반 로직(예: Role.valueOf(potRole)) 수행 시 IllegalArgumentException 발생
  • API 응답의 일관성(요청·응답 간 포맷 매칭) 훼손

등의 문제가 생길 수 있습니다.

수정 제안:

  • potRole 필드는 entity.getRoleName().name() 또는 null/정의된 “UNKNOWN” 코드로만 내려보냅니다.
  • 표시명은 RoleNameMapper.mapRoleName(...)을 통해 nickname 조합에만 사용합니다.
--- a/src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java
+++ b/src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java
@@ -61,9 +61,14 @@ public class PotMemberConverter {
     public PotMemberInfoResponseDto toKaKaoMemberDto(PotMember entity) {
-        String roleName = entity.getRoleName() != null
-                ? entity.getRoleName().name()
-                : "멤버";
-        String nicknameWithRole = entity.getUser().getNickname() + " " + RoleNameMapper.mapRoleName(roleName);
+        // potRole에는 반드시 enum 코드만 전달 (null 가능)
+        String roleCode = (entity.getRoleName() != null)
+                ? entity.getRoleName().name()
+                : null; // 또는 별도 UNKNOWN 코드
+        // 표시명 매핑은 nickname 조합에만 사용
+        String displayName = RoleNameMapper.mapRoleName(roleCode);
+        String nicknameWithRole = entity.getUser().getNickname() + " " + displayName;
 
-        return PotMemberInfoResponseDto.builder()
-                .potMemberId(entity.getPotMemberId())
-                .nickname(nicknameWithRole)
-                .potRole(roleName)
-                .owner(false)
-                .build();
+        return PotMemberInfoResponseDto.builder()
+                .potMemberId(entity.getPotMemberId())
+                .nickname(nicknameWithRole)
+                .potRole(roleCode)
+                .owner(false)
+                .build();
     }
 }

• 적용 파일:

  • src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (라인 62–66)
    • 검증 포인트:
  • PotMemberInfoResponseDto.potRole 필드는 항상 enum 이름(Role.name()) 또는 null/UNKNOWN 코드여야 함
  • nickname 조합 시 RoleNameMapper.mapRoleName(...)으로 로컬라이즈된 표시명 사용
📝 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
public PotMemberInfoResponseDto toKaKaoMemberDto(PotMember entity) {
String roleName = entity.getPotApplication() != null
? entity.getPotApplication().getPotRole().name()
String roleName = entity.getRoleName() != null
? entity.getRoleName().name()
: "멤버";
public PotMemberInfoResponseDto toKaKaoMemberDto(PotMember entity) {
// potRole에는 반드시 enum 코드만 전달 (null 가능)
String roleCode = (entity.getRoleName() != null)
? entity.getRoleName().name()
: null; // 또는 별도 UNKNOWN 코드
// 표시명 매핑은 nickname 조합에만 사용
String displayName = RoleNameMapper.mapRoleName(roleCode);
String nicknameWithRole = entity.getUser().getNickname() + " " + displayName;
-
- String roleName = entity.getRoleName() != null
- ? entity.getRoleName().name()
- : "멤버";
- String nicknameWithRole = entity.getUser().getNickname() + " " + RoleNameMapper.mapRoleName(roleName);
-
return PotMemberInfoResponseDto.builder()
.potMemberId(entity.getPotMemberId())
.nickname(nicknameWithRole)
.potRole(roleCode)
.owner(false)
.build();
}

// 사용자의 진행 중인 팟 조회
List<OngoingPotResponseDto> getMyPots();
AppealContentDto getAppealContent(Long potId);
AppealContentDto getUserAppealContent(Long potId, Long targetUserId);
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

신규 메서드 추가 방향은 합리적이나, 계약(권한/예외/Null) 명확화 필요

  • 어떤 사용자가 누구의 appeal content를 조회할 수 있는지(본인/팟 멤버/리더/관리자 등) 권한 모델을 인터페이스 수준에서 명확히 해주세요.
  • targetUserId가 팟 멤버가 아니거나 pot이 COMPLETED 상태가 아닐 때의 예외 규약을 주석/Javadoc으로 고정해 두면 구현 간 불일치가 줄어듭니다.
  • 네이밍은 getAppealContentForUser도 고려 가능(선택). 기존 getAppealContent(Long potId)와의 의미 구분이 더욱 뚜렷해집니다.

다음 스크립트로 구현/엔드포인트 연결 상태를 빠르게 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# MyPotService 구현체들이 새 메서드를 모두 구현하는지
rg -nP 'class\s+\w+\s+implements\s+MyPotService\b' -C2
rg -n 'getUserAppealContent\s*\(' -C2

# 컨트롤러 라우팅 연결 확인(엔드포인트 키워드 기반)
rg -n 'potAppealContent' -C3 --type=java

Length of output: 4020


인터페이스에 Javadoc 및 권한·예외 계약 명확화 필요

서비스·컨트롤러 연결은 정상적으로 확인되었습니다. 다만 새로 추가된 메서드에 대해 아래 사항을 반영해 주세요:

  • MyPotService#getUserAppealContent(Long potId, Long targetUserId) 인터페이스 선언부에 Javadoc을 추가하고,
    • 허용된 호출자 범위(본인, 팟 멤버, 리더, 관리자 등)를 명시
    • 반환되는 AppealContentDto의 역할을 간략히 설명
  • 잘못된 targetUserId(팟 멤버가 아님)거나 pot 상태가 COMPLETED가 아닐 때 던질 예외(예: EntityNotFoundException, IllegalStateException 등)와 조건을 Javadoc에 기록하여 구현 간 불일치 최소화
  • (선택) 메서드명 getAppealContentForUser 검토: 기존 getAppealContent(Long potId)와 의미가 더 명확히 구분됩니다

❗ 현재 구현체(MyPotServiceImpl)와 UserController 쪽 엔드포인트(/potAppealContent/{pot_id}/{user_id})는 정상 연결되어 있습니다. 위 Javadoc·계약 명세만 추가해 주시면 감사하겠습니다.

🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/pot/service/pot/MyPotService.java around line
16, add a Javadoc for getUserAppealContent(Long potId, Long targetUserId) that
(1) states the allowed callers (e.g., the pot owner, any pot member, pot leader,
or administrators as applicable), (2) briefly describes the returned
AppealContentDto and its purpose (what fields/intent it conveys), (3) documents
error conditions and thrown exceptions (e.g., throw EntityNotFoundException if
targetUserId is not a pot member or potId not found, throw IllegalStateException
if pot state is not COMPLETED), and (4) optionally note a suggested rename to
getAppealContentForUser if you prefer clearer distinction from
getAppealContent(Long). Ensure the Javadoc mirrors the actual behavior in
MyPotServiceImpl and the controller so implementation/contract remain
consistent.

Comment on lines +75 to +112
@Transactional
@Override
public AppealContentDto getUserAppealContent(Long potId, Long targetUserId) {
// 1) 팟 조회
Pot pot = potRepository.findById(potId)
.orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND));

// 2) 상태 체크
if (!"COMPLETED".equals(pot.getPotStatus())) {
log.error("해당 팟은 COMPLETED 상태가 아닙니다.");
throw new GeneralException(ErrorStatus._BAD_REQUEST);
}

// 3) 대상 유저의 PotMember 조회
PotMember potMember = potMemberRepository
.findByPot_PotIdAndUser_Id(potId, targetUserId)
.orElse(null);

String appealContent = (potMember != null) ? potMember.getAppealContent() : null;

// 4) 역할 조회 (한글명 매핑)
String userPotRole = potMemberRepository.findRoleByUserId(potId, targetUserId)
.map(role -> RoleNameMapper.getKoreanRoleName(role.name()))
.orElse(null);

// 5) 뱃지 조회
List<BadgeDto> myBadges = potMemberBadgeRepository
.findByPotMember_Pot_PotIdAndPotMember_User_Id(potId, targetUserId)
.stream()
.map(pmb -> new BadgeDto(
pmb.getBadge().getBadgeId(),
pmb.getBadge().getName()
))
.collect(Collectors.toList());

// 6) DTO 변환
return potDetailConverter.toCompletedPotDetailDto(appealContent, userPotRole, myBadges);
}
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

타 사용자 어필/뱃지 열람에 대한 접근 제어 재확인 필요

COMPLETED 상태 제한은 있으나, 현재 로그인 사용자가 해당 팟의 멤버/소유자인지 검증이 없습니다. 요구사항상 누구나(COMPLETED이면) 열람 가능한 것이 맞는지 확인 부탁드립니다. 멤버/소유자에게만 허용이 맞다면 아래와 같이 접근 제어를 추가하세요.

     public AppealContentDto getUserAppealContent(Long potId, Long targetUserId) {
-        // 1) 팟 조회
+        // 1) 팟 조회
         Pot pot = potRepository.findById(potId)
                 .orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND));

         // 2) 상태 체크
         if (!"COMPLETED".equals(pot.getPotStatus())) {
             log.error("해당 팟은 COMPLETED 상태가 아닙니다.");
             throw new GeneralException(ErrorStatus._BAD_REQUEST);
         }
+
+        // 2-1) 접근 제어: 소유자 또는 팟 멤버만 열람 허용 (요구사항에 따라 조정)
+        User viewer = authService.getCurrentUser();
+        boolean canView = pot.getUser().getId().equals(viewer.getId())
+                || potMemberRepository.existsByPotAndUser(pot, viewer);
+        if (!canView) {
+            throw new PotHandler(ErrorStatus.POT_FORBIDDEN);
+        }

🏁 Script executed:

#!/bin/bash
set -e

# 1) MyPotServiceImpl에서 authService 필드나 사용 여부 확인
rg -n "authService" -g "src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java"

# 2) 클래스 헤더와 필드 선언부 출력 (주입된 빈 확인)
sed -n '1,100p' src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java

# 3) AuthService라는 이름의 빈(인터페이스/클래스) 전역 검색
rg -n "class AuthService" -g "src/main/java"

Length of output: 5623


보안: COMPLETED 팟 어필/뱃지 조회 시 권한 검증 로직 추가 필요
현재 getUserAppealContent 메서드는 팟 상태만 “COMPLETED”인지 확인하고, 호출 사용자의 소유자/멤버 여부는 검증하지 않고 있습니다. 요구사항상 COMPLETED 상태만으로 모든 사용자가 조회 가능한지 명확히 확인해 주세요.
만약 “팟 소유자 및 해당 팟의 멤버만 조회 가능”이 맞다면, 아래와 같이 필터링 로직을 추가해야 합니다.

     // 2) 상태 체크
     if (!"COMPLETED".equals(pot.getPotStatus())) {
         log.error("해당 팟은 COMPLETED 상태가 아닙니다.");
         throw new GeneralException(ErrorStatus._BAD_REQUEST);
     }
+
+    // 2-1) 접근 제어: 소유자 또는 팟 멤버만 조회 허용
+    User currentUser = authService.getCurrentUser();
+    boolean isOwner = pot.getUser().getId().equals(currentUser.getId());
+    boolean isMember = potMemberRepository
+        .findByPot_PotIdAndUser_Id(potId, currentUser.getId())
+        .isPresent();
+    if (!isOwner && !isMember) {
+        throw new PotHandler(ErrorStatus.POT_FORBIDDEN);
+    }
  • PotMemberRepository에 existsByPotAndUser(Pot, User)가 없다면, 위 예시처럼
    findByPot_PotIdAndUser_Id(potId, currentUser.getId()).isPresent()를 사용하거나
    적절한 existsBy… 메서드를 추가해주세요.
  • 요구사항이 “COMPLETED 팟은 누구나 열람”이라면, 위 로직 없이도 무방하니 확인 후 적용 바랍니다.
📝 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
@Transactional
@Override
public AppealContentDto getUserAppealContent(Long potId, Long targetUserId) {
// 1) 팟 조회
Pot pot = potRepository.findById(potId)
.orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND));
// 2) 상태 체크
if (!"COMPLETED".equals(pot.getPotStatus())) {
log.error("해당 팟은 COMPLETED 상태가 아닙니다.");
throw new GeneralException(ErrorStatus._BAD_REQUEST);
}
// 3) 대상 유저의 PotMember 조회
PotMember potMember = potMemberRepository
.findByPot_PotIdAndUser_Id(potId, targetUserId)
.orElse(null);
String appealContent = (potMember != null) ? potMember.getAppealContent() : null;
// 4) 역할 조회 (한글명 매핑)
String userPotRole = potMemberRepository.findRoleByUserId(potId, targetUserId)
.map(role -> RoleNameMapper.getKoreanRoleName(role.name()))
.orElse(null);
// 5) 뱃지 조회
List<BadgeDto> myBadges = potMemberBadgeRepository
.findByPotMember_Pot_PotIdAndPotMember_User_Id(potId, targetUserId)
.stream()
.map(pmb -> new BadgeDto(
pmb.getBadge().getBadgeId(),
pmb.getBadge().getName()
))
.collect(Collectors.toList());
// 6) DTO 변환
return potDetailConverter.toCompletedPotDetailDto(appealContent, userPotRole, myBadges);
}
@Transactional
@Override
public AppealContentDto getUserAppealContent(Long potId, Long targetUserId) {
// 1) 팟 조회
Pot pot = potRepository.findById(potId)
.orElseThrow(() -> new PotHandler(ErrorStatus.POT_NOT_FOUND));
// 2) 상태 체크
if (!"COMPLETED".equals(pot.getPotStatus())) {
log.error("해당 팟은 COMPLETED 상태가 아닙니다.");
throw new GeneralException(ErrorStatus._BAD_REQUEST);
}
// 2-1) 접근 제어: 소유자 또는 팟 멤버만 조회 허용
User currentUser = authService.getCurrentUser();
boolean isOwner = pot.getUser().getId().equals(currentUser.getId());
boolean isMember = potMemberRepository
.findByPot_PotIdAndUser_Id(potId, currentUser.getId())
.isPresent();
if (!isOwner && !isMember) {
throw new PotHandler(ErrorStatus.POT_FORBIDDEN);
}
// 3) 대상 유저의 PotMember 조회
PotMember potMember = potMemberRepository
.findByPot_PotIdAndUser_Id(potId, targetUserId)
.orElse(null);
String appealContent = (potMember != null) ? potMember.getAppealContent() : null;
// 4) 역할 조회 (한글명 매핑)
String userPotRole = potMemberRepository.findRoleByUserId(potId, targetUserId)
.map(role -> RoleNameMapper.getKoreanRoleName(role.name()))
.orElse(null);
// 5) 뱃지 조회
List<BadgeDto> myBadges = potMemberBadgeRepository
.findByPotMember_Pot_PotIdAndPotMember_User_Id(potId, targetUserId)
.stream()
.map(pmb -> new BadgeDto(
pmb.getBadge().getBadgeId(),
pmb.getBadge().getName()
))
.collect(Collectors.toList());
// 6) DTO 변환
return potDetailConverter.toCompletedPotDetailDto(appealContent, userPotRole, myBadges);
}
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java around
lines 75 to 112, add authorization to ensure only the pot owner or a pot member
can view appeal content/badges when the pot is COMPLETED (unless the product
requirement explicitly allows public access). After loading the Pot and
verifying COMPLETED, obtain the current user's id (from the security context),
then check membership/ownership using either an existsByPotAndUser(Pot, User)
repository method or findByPot_PotIdAndUser_Id(potId,
currentUserId).isPresent(); if the check fails throw a suitable access/forbidden
exception. If requirements state COMPLETED pots are public, document that and
leave the method unchanged.

Comment on lines +316 to +329
@GetMapping("/potAppealContent/{pot_id}/{user_id}")
@Operation(
summary = "다른 사람 마이페이지 '여기서 저는요' 모달 조회 API",
description = "'끓인 팟 상세보기 모달'에 쓰이는 Role, Badge, Appeal Content를 반환합니다."
)
@ApiErrorCodeExamples({
ErrorStatus.USER_NOT_FOUND,
})
public ResponseEntity<ApiResponse<AppealContentDto>> getAppealContent(
@PathVariable(name = "pot_id") Long potId,
@PathVariable(name = "user_id") Long userId) {
AppealContentDto response = myPotService.getUserAppealContent(potId, userId);
return ResponseEntity.ok(ApiResponse.onSuccess(response));
}
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

타 사용자 ‘여기서 저는요’ 조회 엔드포인트 신설 — 권한/에러 스펙 보완 제안

  • 퍼블릭 GET으로 보이는데, 조회 권한(해당 팟 참여자만 조회 가능 등)을 서비스 층에서 강제하는지 확인이 필요합니다. 컨트롤러 레벨에서라도 403 케이스를 문서화해 주세요.
  • 에러 스펙에 POT_NOT_FOUND, POT_FORBIDDEN(또는 동등 코드) 추가 권장.
     @ApiErrorCodeExamples({
-            ErrorStatus.USER_NOT_FOUND,
+            ErrorStatus.USER_NOT_FOUND,
+            ErrorStatus.POT_NOT_FOUND,
+            ErrorStatus.POT_FORBIDDEN,
     })

권한 체크 위치 확인:


🏁 Script executed:

#!/bin/bash
rg -nP --type=java 'getUserAppealContent\s*\(' -C3

Length of output: 2558


🏁 Script executed:

#!/bin/bash
# 전체 getUserAppealContent 메서드 구현 확인
rg -nP --type=java 'public AppealContentDto getUserAppealContent' -C50 src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java

Length of output: 4255


권한 검증 추가 및 에러 스펙 보강 필요

현재 UserController#getAppealContent(/potAppealContent/{pot_id}/{user_id}) 엔드포인트는 서비스 호출만 수행하며, 실제로 요청을 보낸 현재 사용자(authService.getCurrentUser())가 해당 팟의 참여자인지 전혀 검증하지 않습니다. 따라서 누구나 다른 유저의 ‘여기서 저는요’ 내용을 조회할 수 있는 보안 취약점이 존재합니다.

• Service (MyPotServiceImpl#getUserAppealContent) 레벨에서

  • authService.getCurrentUser() 호출 후
  • potMemberRepository.findByPot_PotIdAndUser_Id(potId, currentUserId) 로 조회하여
  • 참여자가 아닐 경우 PotHandler(ErrorStatus.POT_FORBIDDEN) 또는 GeneralException(ErrorStatus.POT_FORBIDDEN)을 던져 403 처리

• Controller (UserController#getAppealContent) 레벨에서는

  • 403 Forbidden 케이스를 @ApiErrorCodeExamples 에 명시하여 API 문서에 반영

@ApiErrorCodeExamples 업데이트 제안

     @ApiErrorCodeExamples({
-            ErrorStatus.USER_NOT_FOUND,
+            ErrorStatus.POT_NOT_FOUND,      // 팟이 없을 때
+            ErrorStatus.POT_FORBIDDEN,     // 참여자 아닐 때
+            ErrorStatus._BAD_REQUEST       // COMPLETED 상태가 아닐 때
     })

위 사항은 ACL(접근 제어) 미검증으로 인한 심각한 보안 이슈이므로 반드시 반영해 주세요.

📝 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
@GetMapping("/potAppealContent/{pot_id}/{user_id}")
@Operation(
summary = "다른 사람 마이페이지 '여기서 저는요' 모달 조회 API",
description = "'끓인 팟 상세보기 모달'에 쓰이는 Role, Badge, Appeal Content를 반환합니다."
)
@ApiErrorCodeExamples({
ErrorStatus.USER_NOT_FOUND,
})
public ResponseEntity<ApiResponse<AppealContentDto>> getAppealContent(
@PathVariable(name = "pot_id") Long potId,
@PathVariable(name = "user_id") Long userId) {
AppealContentDto response = myPotService.getUserAppealContent(potId, userId);
return ResponseEntity.ok(ApiResponse.onSuccess(response));
}
@GetMapping("/potAppealContent/{pot_id}/{user_id}")
@Operation(
summary = "다른 사람 마이페이지 '여기서 저는요' 모달 조회 API",
description = "'끓인 팟 상세보기 모달'에 쓰이는 Role, Badge, Appeal Content를 반환합니다."
)
@ApiErrorCodeExamples({
ErrorStatus.POT_NOT_FOUND, // 팟이 없을 때
ErrorStatus.POT_FORBIDDEN, // 참여자 아닐 때
ErrorStatus._BAD_REQUEST // COMPLETED 상태가 아닐 때
})
public ResponseEntity<ApiResponse<AppealContentDto>> getAppealContent(
@PathVariable(name = "pot_id") Long potId,
@PathVariable(name = "user_id") Long userId) {
AppealContentDto response = myPotService.getUserAppealContent(potId, userId);
return ResponseEntity.ok(ApiResponse.onSuccess(response));
}
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/controller/UserController.java around
lines 316-329, the endpoint does not enforce that the current authenticated user
is a member of the pot, allowing anyone to read another user's appeal content;
update the implementation and docs: in the service
(MyPotServiceImpl#getUserAppealContent) call authService.getCurrentUser(),
lookup membership with potMemberRepository.findByPot_PotIdAndUser_Id(potId,
currentUserId) and if not present throw PotHandler(ErrorStatus.POT_FORBIDDEN) or
GeneralException(ErrorStatus.POT_FORBIDDEN) so it returns 403; then update the
controller's @ApiErrorCodeExamples on getAppealContent to include
ErrorStatus.POT_FORBIDDEN so API docs reflect the forbidden case.

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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

스프링 @transactional로의 전환 적절

Spring Boot 표준에 맞춘 올바른 변경입니다. 트랜잭션 경계와 롤백 정책은 기존과 동일하게 동작할 가능성이 높습니다.

레포 전역에서 jakarta.transaction.Transactional 잔존 여부를 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# jakarta.transaction.Transactional 잔존 import 검색
rg -nP "import\s+jakarta\.transaction\.Transactional" -g '!**/build/**'

Length of output: 1253


jakarta.transaction.Transactional 임포트 일괄 교체 필요

레포지토리 전역에서 여전히 jakarta.transaction.Transactional 을 사용 중인 파일들이 확인되었습니다. 아래 파일들의 import 구문을 모두
org.springframework.transaction.annotation.Transactional 로 교체해 주세요.

  • src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java
  • src/main/java/stackpot/stackpot/user/service/TokenService.java
  • src/main/java/stackpot/stackpot/pot/repository/PotSaveRepository.java
  • src/main/java/stackpot/stackpot/pot/repository/PotRecruitmentDetailsRepository.java
  • src/main/java/stackpot/stackpot/pot/service/potMember/PotMemberQueryServiceImpl.java
  • src/main/java/stackpot/stackpot/pot/service/potMember/PotMemberCommandServiceImpl.java
  • src/main/java/stackpot/stackpot/save/service/SaveServiceImpl.java
  • src/main/java/stackpot/stackpot/pot/service/pot/MyPotServiceImpl.java
  • src/main/java/stackpot/stackpot/task/service/TaskQueryServiceImpl.java
  • src/main/java/stackpot/stackpot/task/service/TaskCommandServiceImpl.java

이 외에도 동일 패턴의 파일이 있을 수 있으니, jakarta.transaction.Transactional 임포트가 남아있는지 재검토 후 일괄 수정을 완료해 주시기 바랍니다.

🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java
around lines 9-9, the file currently imports jakarta.transaction.Transactional;
replace that import with
org.springframework.transaction.annotation.Transactional. Do the same
replacement for the listed files (FeedQueryServiceImpl.java, TokenService.java,
PotSaveRepository.java, PotRecruitmentDetailsRepository.java,
PotMemberQueryServiceImpl.java, PotMemberCommandServiceImpl.java,
SaveServiceImpl.java, MyPotServiceImpl.java, TaskQueryServiceImpl.java,
TaskCommandServiceImpl.java) and sweep the repo for any remaining
jakarta.transaction.Transactional imports to update them to
org.springframework.transaction.annotation.Transactional, then rebuild to verify
no remaining compilation issues.

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

Caution

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

⚠️ Outside diff range comments (2)
src/main/java/stackpot/stackpot/user/controller/UserController.java (2)

412-415: 컴파일 에러: ResponseEntity 제네릭 타입 불일치

메서드 시그니처는 ResponseEntity<ApiResponse<Void>>인데, 반환은 ResponseEntity<Void>입니다. 제네릭 불일치로 컴파일 실패합니다. 204를 유지하려면 반환 타입을 ResponseEntity<Void>로 바꾸세요.

패치(diff):

-    public ResponseEntity<ApiResponse<Void>> deleteMyDescription() {
+    public ResponseEntity<Void> deleteMyDescription() {
         userCommandService.deleteDescription();
         return ResponseEntity.noContent().build();
     }

61-66: 오탈자 수정: “유요한/유요하지” → “유효한/유효하지”

사용자-facing 문서이므로 맞춤법을 바로잡아 주세요.

패치(diff):

-                            description = "유요한 토큰",
+                            description = "유효한 토큰",
@@
-                            description = "유요하지 않은 토큰",
+                            description = "유효하지 않은 토큰",
♻️ Duplicate comments (2)
src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java (1)

30-30: 이전 리뷰의 @param 누락 이슈가 해결되었습니다 — 잘 처리하셨습니다

명명 파라미터 :userId와 메서드 파라미터 바인딩이 일관되게 맞춰졌습니다.

src/main/java/stackpot/stackpot/user/controller/UserController.java (1)

316-329: 타 사용자 ‘여기서 저는요’ 조회 — 접근 제어 및 에러 스펙 보강 필요

이 엔드포인트는 현재 사용자(요청자)가 해당 팟의 참여자인지 확인하는 접근 제어가 문서상 드러나지 않고, @ApiErrorCodeExamples에도 403/404가 누락되어 있습니다. 이전 코멘트에서도 동일 이슈가 지적되었습니다. 서비스 레벨에서 반드시 멤버십 검증을 수행하고(미참여 시 403), 문서에도 반영해 주세요.

문서 보강(diff):

     @ApiErrorCodeExamples({
-            ErrorStatus.USER_NOT_FOUND,
+            ErrorStatus.USER_NOT_FOUND,
+            ErrorStatus.POT_NOT_FOUND,
+            ErrorStatus.POT_FORBIDDEN,
+            ErrorStatus._BAD_REQUEST
     })

검증 스크립트(서비스에서 권한 체크 존재 여부 확인):

#!/bin/bash
# MyPotServiceImpl#getUserAppealContent 내 권한 검증(현재 사용자/멤버십/403) 여부 점검

# 1) 구현 위치 탐색
rg -nP --type=java 'class\s+MyPotServiceImpl\b' -C2 src/main/java

# 2) getUserAppealContent 구현 본문 확인
rg -nP --type=java -C5 'getUserAppealContent\s*\(' src/main/java

# 3) 접근 제어 흔적 탐색
rg -nP --type=java 'authService\.getCurrentUser|getCurrentUser\s*\(|POT_FORBIDDEN|findByPot_PotIdAndUser_Id' src/main/java
🧹 Nitpick comments (10)
src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java (4)

27-29: 정렬 기준 미지정 — 최신순 정렬로 결과 안정성 보장 제안

읽지 않은 알림 목록은 보통 최신순 정렬이 필요합니다. 정렬이 없으면 DB마다 반환 순서가 달라질 수 있습니다.

추가 패치:

-            " (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")
+            " (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId))) " +
+            "ORDER BY fcn.createdAt DESC")

20-20: 알림 문구 가독성 소폭 개선 제안

현 문구는 “{nickname} 새싹님의 댓글이 달렸어요.”로 읽히는데 어색할 수 있습니다. 브랜드 톤앤매너에 따라 다르나, 일반적인 한국어 문장으로는 아래가 자연스럽습니다.

대안(문구만 변경, CONCAT 중첩은 첫 코멘트 패치로 해결됨):

-            "CONCAT(CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. '), fc.comment), " +
+            "CONCAT(CONCAT(u.nickname, '님이 댓글을 남겼어요: '), fc.comment), " +

32-34: 수정 쿼리 실행 시 영속성 컨텍스트 정리 옵션 고려

벌크 삭제 후 동일 트랜잭션 내에서 1차 캐시에 남은 엔티티와의 불일치를 피하려면 자동 flush/clear 옵션을 켜는 것을 권장합니다. 서비스 레이어 트랜잭션 정책에 따라 선택해 주세요.

제안 패치:

-    @Modifying
+    @Modifying(clearAutomatically = true, flushAutomatically = true)
     @Query("DELETE FROM FeedCommentNotification fcn WHERE fcn.feedComment.id = :feedCommentId")
     void deleteByFeedCommentId(@Param("feedCommentId") Long feedCommentId);

14-29: (선택) 페이징 지원 메서드 오버로드 추가 제안

읽지 않은 알림은 누적될 수 있어 페이징이 필요합니다. Page 반환과 countQuery를 갖춘 오버로드 추가를 권합니다.

참고 예시:

@Query(
  value = "SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
          "fcn.id, null, f.feedId, u.nickname, '피드 댓글 알림', " +
          "CONCAT(CONCAT(u.nickname, '님이 댓글을 남겼어요: '), fc.comment), fcn.createdAt) " +
          "FROM FeedCommentNotification fcn " +
          "JOIN fcn.feedComment fc " +
          "JOIN fc.user u " +
          "JOIN fc.feed f " +
          "LEFT JOIN fc.parent fcp " +
          "WHERE fcn.isRead = false AND u.id <> :userId AND " +
          "((fc.parent is null AND f.user.id = :userId) OR " +
          " (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))",
  countQuery = "SELECT COUNT(fcn) FROM FeedCommentNotification fcn " +
               "JOIN fcn.feedComment fc " +
               "JOIN fc.user u " +
               "JOIN fc.feed f " +
               "LEFT JOIN fc.parent fcp " +
               "WHERE fcn.isRead = false AND u.id <> :userId AND " +
               "((fc.parent is null AND f.user.id = :userId) OR " +
               " (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))"
)
org.springframework.data.domain.Page<NotificationDto.UnReadNotificationDto>
findAllUnReadNotificationsByUserId(@Param("userId") Long userId,
                                   org.springframework.data.domain.Pageable pageable);
src/main/java/stackpot/stackpot/user/controller/UserController.java (6)

20-20: 와일드카드 import 지양하고 명시적 import로 변경하세요

스타 import(*)는 불필요한 의존을 유발하고 정적 분석/리뷰 난이도를 올립니다. 실제 사용 타입만 명시적으로 import 하세요.

적용 예시(diff):

-import stackpot.stackpot.pot.dto.*;
+import stackpot.stackpot.pot.dto.AppealContentDto;
+import stackpot.stackpot.pot.dto.CompletedPotRequestDto;
+import stackpot.stackpot.pot.dto.OngoingPotResponseDto;
+import stackpot.stackpot.pot.dto.PotResponseDto;
+import stackpot.stackpot.pot.dto.PotSummaryDto;

180-182: API 문서 설명 문구 간결화 제안

설명에서 “string입니다” 보다는 “문자열 리스트입니다” 등 도메인 용어를 사용하면 명확합니다. 또한 interests 예시가 여러 곳에 중복되는데 공통 상수/스키마로 중앙화 고려 바랍니다.


276-278: 프로필 수정 API 설명 통일

위 회원가입 API와 interests 설명이 중복됩니다. 중복을 줄이고 “역할/관심사/한줄소개/카카오ID”의 유효성 규칙(허용 길이·값 집합)을 명시하면 API 소비자 이해에 도움이 됩니다.


331-331: REST 경로 정렬 제안: /pots/{pot_id}/summary 형태 고려

현재 /potSummary/{pot_id}는 리소스 중심 REST 컨벤션과 다소 어긋납니다. 호환성 영향을 감안하되, 신규 클라이언트 기준으로는 /pots/{pot_id}/summary로 노출하는 버전드 경로 추가를 권장합니다.


421-423: seriesId=0 센티넬 값 지양 — 미전달(null)로 ‘전체보기’ 표현

매직 넘버 0은 혼동을 야기합니다. 파라미터 미전달(null)을 ‘전체보기’로 해석하도록 바꾸고, 문서도 그에 맞춰 수정하세요.

문서/파라미터 수정(diff):

-            description = "userId에 해당하는 사용자의 시리즈 코멘트와 피드를 반환합니다. 피드는 커서 기반 페이지네이션을 지원합니다. \n" +
-                    "시리즈가 '전체보기' 일 때는 seriesId = 0"
+            description = "userId에 해당하는 사용자의 시리즈 코멘트와 피드를 반환합니다. 피드는 커서 기반 페이지네이션을 지원합니다.\n" +
+                    "시리즈 '전체보기'는 seriesId 파라미터를 생략(미전달)하세요."

446-448: 문서 일관화: ‘전체보기’는 파라미터 미전달(null)로 표현

위 사용자별 피드와 동일한 가이드로 정리해 주세요.

문서 수정(diff):

-            description = "로그인한 사용자의 시리즈 코멘트와 피드를 반환합니다. 피드는 커서 기반 페이지네이션을 지원합니다. \n" +
-                    "시리즈가 '전체보기' 일 때는 seriesId = 0"
+            description = "로그인한 사용자의 시리즈 코멘트와 피드를 반환합니다. 피드는 커서 기반 페이지네이션을 지원합니다.\n" +
+                    "시리즈 '전체보기'는 seriesId 파라미터를 생략(미전달)하세요."
📜 Review details

Configuration used: Path: .coderabbit.yaml

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 cbdf24f and dc83ab2.

📒 Files selected for processing (4)
  • src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java (3 hunks)
  • src/main/java/stackpot/stackpot/notification/converter/NotificationConverter.java (1 hunks)
  • src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java (1 hunks)
  • src/main/java/stackpot/stackpot/user/controller/UserController.java (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/stackpot/stackpot/feed/service/FeedQueryServiceImpl.java
  • src/main/java/stackpot/stackpot/notification/converter/NotificationConverter.java
🔇 Additional comments (3)
src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java (1)

15-21: 해당 DTO 설계는 문제가 없습니다.

확인 결과

  • UnReadNotificationDto 내부 클래스는 public static class로 선언되어 있어 JPQL 생성자 표현식에서 인스턴스화에 문제가 없습니다.
  • potId 필드가 원시형이 아닌 Long(박싱 타입)으로 정의되어 있어, 두 번째 인자로 null을 전달해도 NPE 없이 바인딩됩니다.

리뷰 코멘트를 resolve 합니다.

src/main/java/stackpot/stackpot/user/controller/UserController.java (2)

254-254: 변경 사항 영향 없음

서식/개행 수준의 변경으로 보이며 동작에 영향 없습니다.


268-268: LGTM — 마이페이지 조회 시그니처 유지

자기 자신 조회 엔드포인트 시그니처/동작 변경 없음으로 판단됩니다.

"f.feedId, " +
"u.nickname, " +
"'피드 댓글 알림', " +
"CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. ', fc.comment), " +
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

JPQL CONCAT 3개 인자 사용은 JPA 표준 위반 — 런타임 에러 가능성이 큽니다

JPQL 표준 CONCAT은 2개 인자만 허용합니다. 현재 3개 인자를 사용하고 있어 데이터베이스/방언에 따라 즉시 실패할 수 있습니다. 중첩 CONCAT으로 치환해 주세요.

다음 패치를 적용하세요:

-            "CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. ', fc.comment), " +
+            "CONCAT(CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. '), fc.comment), " +
📝 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
"CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. ', fc.comment), " +
"CONCAT(CONCAT(u.nickname, ' 새싹님의 댓글이 달렸어요. '), fc.comment), " +
🤖 Prompt for AI Agents
In
src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java
around line 20, the JPQL uses CONCAT with three arguments which violates the JPA
spec and can cause runtime errors; replace the three-argument call with nested
CONCAT calls (e.g., CONCAT(CONCAT(part1, part2), part3)) so the nickname,
literal text, and comment are concatenated via two-argument CONCAT nesting.

Comment on lines +27 to +29
"WHERE fcn.isRead = false AND " +
"((fc.parent is null AND f.user.id = :userId) OR " +
" (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")
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

자기 자신의 활동에 대한 알림 제외 조건 누락

댓글 작성자(u)가 수신자(:userId)와 동일한 경우까지 매칭되어 자기 자신의 행동에 대한 알림이 노출될 수 있습니다. 일반적으로는 제외하는 것이 기대 동작입니다. 확인 후 제외 조건을 추가해 주세요.

제안 패치:

-            "WHERE fcn.isRead = false AND " +
+            "WHERE fcn.isRead = false AND u.id <> :userId AND " +
             "((fc.parent is null AND f.user.id = :userId) OR " +
             " (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")

의도적으로 자기 알림을 허용하는 요구사항이라면 본 제안은 무시하셔도 됩니다. 요구사항만 확인 부탁드립니다.


🏁 Script executed:

#!/usr/bin/env bash
set -e
# Show 15 lines of context around the HQL query to verify alias definitions
rg -n '"WHERE fcn.isRead' -C15 src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java || true

Length of output: 1352


자기 자신의 활동에 대한 알림 제외 조건 추가 필요

아래 쿼리에서 댓글 작성자(u)가 수신자(:userId)와 동일한 경우에도 알림이 조회되고 있으므로, u.id <> :userId 조건을 추가해야 합니다.

문제가 되는 위치:

  • 파일: src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java
  • 메서드: findAllUnReadNotificationsByUserId
  • 쿼리 WHERE 절 시작 지점 (27~29행)

제안 수정(diff):

-            "WHERE fcn.isRead = false AND " +
+            "WHERE fcn.isRead = false AND u.id <> :userId AND " +
             "((fc.parent is null AND f.user.id = :userId) OR " +
             " (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")

요구사항상 본인이 발생시킨 댓글 알림까지 허용해야 하는 특별한 이유가 없다면, 위 패치를 적용해 주세요.

📝 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
"WHERE fcn.isRead = false AND " +
"((fc.parent is null AND f.user.id = :userId) OR " +
" (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")
"WHERE fcn.isRead = false AND u.id <> :userId AND " +
"((fc.parent is null AND f.user.id = :userId) OR " +
" (fc.parent is not null AND (fcp.user.id = :userId OR f.user.id = :userId)))")
🤖 Prompt for AI Agents
In
src/main/java/stackpot/stackpot/notification/repository/FeedCommentNotificationRepository.java
around lines 27-29, the WHERE clause currently returns notifications even when
the comment author (alias u) is the same as the recipient (:userId); update the
WHERE clause to exclude self-generated notifications by adding the condition
u.id <> :userId so that only comments authored by others are returned (ensure
the condition is applied to both parent-null and parent-not-null branches and
that the :userId parameter is already available in the query).

Comment on lines +436 to 441
@RequestParam(value = "size", defaultValue = "10") int size,

@RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId
) {
UserMyPageResponseDto mypage = feedQueryService.getFeedsByUserId(userId, cursor, size);
UserMyPageResponseDto mypage = feedQueryService.getFeedsByUserId(userId, cursor, size, seriesId);
return ResponseEntity.ok(ApiResponse.onSuccess(mypage));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

요청 파라미터 유효성 검증 추가(@validated + 제약 어노테이션)

cursor/size/seriesId에 최소한의 제약이 없어 잘못된 값으로 부하 유발 우려가 있습니다. 메서드 파라미터에 제약을 추가하고 클래스에 @Validated를 적용하세요.

파라미터 제약(diff):

-            @RequestParam(value = "cursor", required = false) Long cursor,
+            @jakarta.validation.constraints.PositiveOrZero
+            @RequestParam(value = "cursor", required = false) Long cursor,
@@
-            @RequestParam(value = "size", defaultValue = "10") int size,
+            @jakarta.validation.constraints.Min(1)
+            @jakarta.validation.constraints.Max(50)
+            @RequestParam(value = "size", defaultValue = "10") int size,
@@
-            @RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId
+            @jakarta.validation.constraints.PositiveOrZero
+            @RequestParam(value = "seriesId", required = false) Long seriesId

클래스 수준 추가(파일 상단에 별도 적용):

// 추가 import
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.PositiveOrZero;

// 클래스 어노테이션 추가
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

필요 시 제가 일괄 패치 PR 생성 도와드리겠습니다.

🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/controller/UserController.java around
lines 436-441, the request params cursor/size/seriesId lack validation; add
class-level @Validated and annotate the method params with validation
constraints (e.g., cursor as @PositiveOrZero, size as @Min(1) and optional
@Max(100) or project-appropriate max, seriesId as @PositiveOrZero) and import
the validation annotations and
org.springframework.validation.annotation.Validated (use
jakarta.validation.constraints.* for Min/Max/PositiveOrZero). Ensure the
controller class declaration is annotated with @Validated and adjust imports; no
business logic changes required.

Comment on lines +456 to +459
@RequestParam(name = "size", defaultValue = "10") int size,
@RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId
) {
UserMyPageResponseDto mypage = feedQueryService.getFeeds(cursor, size);
UserMyPageResponseDto mypage = feedQueryService.getFeeds(cursor, size, seriesId);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동일한 파라미터 유효성 검증 적용 필요

위 사용자별 피드와 동일하게 cursor/size/seriesId에 제약을 부여해 주세요.

파라미터 제약(diff):

-            @RequestParam(name = "cursor", required = false) Long cursor,
+            @jakarta.validation.constraints.PositiveOrZero
+            @RequestParam(name = "cursor", required = false) Long cursor,
@@
-            @RequestParam(name = "size", defaultValue = "10") int size,
+            @jakarta.validation.constraints.Min(1)
+            @jakarta.validation.constraints.Max(50)
+            @RequestParam(name = "size", defaultValue = "10") int size,
@@
-            @RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId
+            @jakarta.validation.constraints.PositiveOrZero
+            @RequestParam(value = "seriesId", required = false) Long seriesId
📝 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
@RequestParam(name = "size", defaultValue = "10") int size,
@RequestParam(value = "seriesId", required = false, defaultValue = "0") Long seriesId
) {
UserMyPageResponseDto mypage = feedQueryService.getFeeds(cursor, size);
UserMyPageResponseDto mypage = feedQueryService.getFeeds(cursor, size, seriesId);
@GetMapping("/feeds")
public UserMyPageResponseDto getFeeds(
// cursor must be zero or positive
@jakarta.validation.constraints.PositiveOrZero
@RequestParam(name = "cursor", required = false) Long cursor,
// size must be between 1 and 50, defaulting to 10
@jakarta.validation.constraints.Min(1)
@jakarta.validation.constraints.Max(50)
@RequestParam(name = "size", defaultValue = "10") int size,
// seriesId must be zero or positive (optional)
@jakarta.validation.constraints.PositiveOrZero
@RequestParam(value = "seriesId", required = false) Long seriesId
) {
UserMyPageResponseDto mypage = feedQueryService.getFeeds(cursor, size, seriesId);
// …
}
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/controller/UserController.java around
lines 456 to 459, the request parameters cursor/size/seriesId lack the same
validation constraints applied to the other user feed endpoint; add the
identical validation annotations used there (e.g., @PositiveOrZero/@Min for
cursor, @Min/@Max or @Positive for size, and @PositiveOrZero or @Min for
seriesId as applicable), ensure the controller class or method is annotated with
@Validated so javax/hibernate validation runs, and keep the existing
defaultValue settings while updating the parameter declarations to include those
validation annotations to enforce the same rules.

@Hyun0828 Hyun0828 merged commit f40b2e9 into main Aug 23, 2025
2 checks passed
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.

3 participants