Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ dependencies {

//
implementation 'org.springframework.boot:spring-boot-starter-actuator'

//hibernate
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.hibernate.common:hibernate-commons-annotations:6.0.6.Final'
}
Comment on lines +92 to 95
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'
}



Expand Down
28 changes: 24 additions & 4 deletions src/main/java/stackpot/stackpot/event/SseService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
import stackpot.stackpot.chat.service.chatroom.ChatRoomQueryService;
import stackpot.stackpot.chat.service.chatroominfo.ChatRoomInfoQueryService;
import stackpot.stackpot.common.util.AuthService;
import stackpot.stackpot.notification.event.FeedCommentEvent;
import stackpot.stackpot.notification.event.FeedLikeEvent;
import stackpot.stackpot.notification.event.PotApplicationEvent;
import stackpot.stackpot.notification.event.PotCommentEvent;
import stackpot.stackpot.notification.event.*;
import stackpot.stackpot.pot.dto.UserMemberIdDto;
import stackpot.stackpot.pot.service.potMember.PotMemberQueryService;

Expand All @@ -34,6 +31,7 @@
@Slf4j
public class SseService {

// user 마다 SseEmitter 객체 저장
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();

private final ChatRoomQueryService chatRoomQueryService;
Expand Down Expand Up @@ -173,6 +171,28 @@ public void sendFeedLikeNotification(FeedLikeEvent event) {
}
}

/**
* 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);
}
}
}
}

private ChatRoomResponseDto.ChatRoomListDto createChatRoomListDto(UserMemberIdDto ids) {
Long potMemberId = ids.getPotMemberId();
Long potId = ids.getPotId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ List<Feed> findFeeds(
// 커서 기반 페이징 조회
List<Feed> findByUserIdAndFeedIdBefore(Long userId, Long cursorFeedId, Pageable pageable);

// 시리즈 필터링 추가된 페이징 조회
List<Feed> findByUser_IdAndSeries_SeriesId(Long userId, Long seriesId, Pageable pageable);
List<Feed> findByUser_IdAndSeries_SeriesIdAndFeedIdBefore(Long userId, Long seriesId, Long cursorFeedId, Pageable pageable);

@Modifying
@Query("DELETE FROM Feed f WHERE f.user.id = :userId")
void deleteByUserId(@Param("userId") Long userId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package stackpot.stackpot.feed.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
Expand All @@ -13,6 +12,7 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import stackpot.stackpot.apiPayload.code.status.ErrorStatus;
import stackpot.stackpot.apiPayload.exception.handler.FeedHandler;
import stackpot.stackpot.apiPayload.exception.handler.UserHandler;
Expand Down Expand Up @@ -200,7 +200,7 @@ public boolean toggleLike(Long feedId) {
feedLikeRepository.delete(existingLike.get());
feed.setLikeCount(feed.getLikeCount() - 1);
feedRepository.save(feed);

// todo 좋아요 알림 삭제
return false; // 좋아요 취소
} else {
// 좋아요 추가
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
public interface FeedQueryService {
FeedResponseDto.FeedPreviewList getPreViewFeeds(String category, String sort, Long cursor, int limit);
FeedResponseDto.AuthorizedFeedDto getFeed(Long feedId);
UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize);
UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize, Long seriesId);
// FeedResponseDto.FeedPreviewList searchByUserIdByKeyword(Long userId, Long nextCursor, int pageSize);
UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize);
UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize, Long seriesId);
Map<Long, String> getMySeries();
Long getLikeCount(Long feedId);
Feed getFeedByFeedId(Long feedId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public FeedResponseDto.AuthorizedFeedDto getFeed(Long feedId) {
}

@Transactional
public UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize) {
public UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int pageSize, Long seriesId) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAuthenticated = authentication != null
&& !(authentication instanceof AnonymousAuthenticationToken)
Expand All @@ -202,9 +202,19 @@ public UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int
: List.of();

Pageable pageable = PageRequest.of(0, pageSize, Sort.by(Sort.Direction.DESC, "feedId"));
List<Feed> feeds = (nextCursor == null)
? feedRepository.findByUser_Id(userId, pageable)
: feedRepository.findByUserIdAndFeedIdBefore(userId, nextCursor, pageable);
// List<Feed> feeds = (nextCursor == null)
// ? feedRepository.findByUser_Id(userId, pageable)
// : feedRepository.findByUserIdAndFeedIdBefore(userId, nextCursor, pageable);
List<Feed> feeds;
if (seriesId == 0) {
feeds = (nextCursor == null)
? feedRepository.findByUser_Id(userId, pageable)
: feedRepository.findByUserIdAndFeedIdBefore(userId, nextCursor, pageable);
} else {
feeds = (nextCursor == null)
? feedRepository.findByUser_IdAndSeries_SeriesId(userId, seriesId, pageable)
: feedRepository.findByUser_IdAndSeries_SeriesIdAndFeedIdBefore(userId, seriesId, nextCursor, pageable);
}

List<FeedResponseDto.FeedDto> feedDtos = feeds.stream()
.map(feed -> {
Expand Down Expand Up @@ -233,13 +243,20 @@ public UserMyPageResponseDto getFeedsByUserId(Long userId, Long nextCursor, int
}

@Override
public UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize) {
public UserMyPageResponseDto getFeeds(Long nextCursor, int pageSize, Long seriesId) {
User user = authService.getCurrentUser();

Pageable pageable = PageRequest.of(0, pageSize, Sort.by(Sort.Direction.DESC, "feedId"));
List<Feed> feeds = (nextCursor == null)
? feedRepository.findByUser_Id(user.getId(), pageable)
: feedRepository.findByUserIdAndFeedIdBefore(user.getId(), nextCursor, pageable);
List<Feed> feeds;
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.


List<Long> likedFeedIds = feedLikeRepository.findFeedIdsByUserId(user.getId());
List<Long> savedFeedIds = feedSaveRepository.findFeedIdsByUserId(user.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import java.util.List;

@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"})

@RequiredArgsConstructor
public class NotificationController {

Expand All @@ -31,7 +31,10 @@ public ResponseEntity<ApiResponse<List<NotificationResponseDto.UnReadNotificatio
return ResponseEntity.ok(ApiResponse.onSuccess(notificationQueryService.getAllUnReadNotifications()));
}

@Operation(summary = "알림 읽음 처리 API")
@Operation(
summary = "알림 읽음 처리 API",
description = "NotificationType은 알림 조회 응답의 type 필드를 그대로 보내면 됩니다"
)
@ApiErrorCodeExamples({
ErrorStatus.NOTIFICATION_NOT_FOUND,
ErrorStatus.INVALID_NOTIFICATION_TYPE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public class NotificationConverter {
public NotificationResponseDto.UnReadNotificationDto toUnReadNotificationDto(NotificationDto.UnReadNotificationDto unReadNotificationDto) {
return NotificationResponseDto.UnReadNotificationDto.builder()
.notificationId(unReadNotificationDto.getNotificationId())
.potOrFeedId(unReadNotificationDto.getPotOrFeedId())
.potId(unReadNotificationDto.getPotId())
.feedId(unReadNotificationDto.getFeedId())
.userName(unReadNotificationDto.getUserName() + " 새싹")
.type(unReadNotificationDto.getType())
.content(unReadNotificationDto.getContent())
Expand All @@ -26,10 +27,11 @@ public NotificationResponseDto.UnReadNotificationDto toUnReadNotificationDto(Not
}

public NotificationResponseDto.UnReadNotificationDto toUnReadNotificationDto(
Long notificationId, Long potOrFeedId, String userName, String type, String content, LocalDateTime createdAt) {
Long notificationId, Long potId, Long feedId, String userName, String type, String content, LocalDateTime createdAt) {
return NotificationResponseDto.UnReadNotificationDto.builder()
.notificationId(notificationId)
.potOrFeedId(potOrFeedId)
.potId(potId)
.feedId(feedId)
.userName(userName + " 새싹")
.type(type)
.content(content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ public class NotificationDto {
@AllArgsConstructor
public static class UnReadNotificationDto {
private Long notificationId;
private Long potOrFeedId;
private Long potId;
private Long feedId;
private String userName;
private String type;
private String content;
private String type; // 알림 종류
private String content; // 알림 내용
private LocalDateTime createdAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class NotificationResponseDto {
@AllArgsConstructor
public static class UnReadNotificationDto {
private Long notificationId;
private Long potOrFeedId;
private Long potId;
private Long feedId;
private String userName;
private String type;
private String content;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,25 @@ public void read(Long id, NotificationCommandService service) {
.orElseThrow(() -> new NotificationHandler(ErrorStatus.NOTIFICATION_NOT_FOUND))
.updateIsRead(true);
}
},
POT_END {
@Override
public void read(Long id, NotificationCommandService service){
service.getPotEndNotificationRepository().findById(id)
.orElseThrow(() -> new NotificationHandler(ErrorStatus.NOTIFICATION_NOT_FOUND))
.updateIsRead(true);
}
};

public abstract void read(Long id, NotificationCommandService service);

public static NotificationType from(String type) {
return switch (type) {
case "PotApplication" -> POT_APPLICATION;
case "PotComment" -> POT_COMMENT;
case "FeedLike" -> FEED_LIKE;
case "FeedComment" -> FEED_COMMENT;
case "팟 지원 알림" -> POT_APPLICATION;
case "팟 댓글 알림" -> POT_COMMENT;
case "피드 좋아요 알림" -> FEED_LIKE;
case "피드 댓글 알림" -> FEED_COMMENT;
case "팟 종료 알림" -> POT_END;
default -> throw new NotificationHandler(ErrorStatus.INVALID_NOTIFICATION_TYPE);
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package stackpot.stackpot.notification.event;

import lombok.AllArgsConstructor;
import lombok.Getter;
import stackpot.stackpot.notification.dto.NotificationResponseDto;

@Getter
@AllArgsConstructor
public class PotEndEvent {
private Long potId;
private NotificationResponseDto.UnReadNotificationDto unReadNotificationDto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@
public interface FeedCommentNotificationRepository extends JpaRepository<FeedCommentNotification, Long> {

@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), " +
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.

"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)))")
Comment on lines +27 to +29
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).

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.


@Modifying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.security.core.parameters.P;
import stackpot.stackpot.notification.dto.NotificationDto;
import stackpot.stackpot.notification.entity.FeedLikeNotification;

import java.util.List;
import java.util.Optional;

public interface FeedLikeNotificationRepository extends JpaRepository<FeedLikeNotification, Long> {

@Query("SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
"fln.id, fln.feedLike.feed.feedId, " +
"fln.feedLike.user.nickname, 'FeedLike', null, fln.createdAt) " +
"fln.id, " +
"null, " +
"fln.feedLike.feed.feedId, " +
"fln.feedLike.user.nickname, " +
"'피드 좋아요 알림', " +
"CONCAT(fln.feedLike.user.nickname, ' 새싹님이 내 피드에 좋아요를 눌렀어요. 확인해 보세요!'), " +
"fln.createdAt) " +
"FROM FeedLikeNotification fln " +
"WHERE fln.isRead = false and fln.feedLike.feed.user.id = :userId ")
List<NotificationDto.UnReadNotificationDto> findAllUnReadNotificationsByUserId(@Param("userId") Long userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
public interface PotApplicationNotificationRepository extends JpaRepository<PotApplicationNotification, Long> {

@Query("SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
"pan.id, pan.potApplication.pot.potId, pan.potApplication.user.nickname, " +
"'PotApplication', null, pan.createdAt) " +
"pan.id, " +
"pan.potApplication.pot.potId, " +
"null, " +
"pan.potApplication.user.nickname, " +
"'팟 지원 알림', " +
"CONCAT(pan.potApplication.user.nickname, ' 새싹님이 내 팟에 지원했어요. 확인해 보세요!'), " +
"pan.createdAt) " +
"FROM PotApplicationNotification pan " +
"WHERE pan.isRead = false AND pan.potApplication.pot.user.id = :userId")
List<NotificationDto.UnReadNotificationDto> findAllUnReadNotificationsByUserId(@Param("userId") Long userId);
Expand Down
Loading