Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,32 @@ public class PotBadgeMemberController {
@GetMapping("/pots/{pot_id}")
public ResponseEntity<ApiResponse<List<PotBadgeMemberDto>>> getBadgeMembersByPotId(
@PathVariable("pot_id") Long potId) {

List<PotBadgeMemberDto> badgeMembers = potBadgeMemberService.getBadgeMembersByPotId(potId);
return ResponseEntity.ok(ApiResponse.onSuccess(badgeMembers));
}

@Operation(summary = "팟에서 가장 많은 `투두를 완료한' 멤버에게 뱃지 부여")
@Operation(summary = "팟에서 가장 많은 `투두를 완료한' 멤버에게 '할 일 정복자' 뱃지 부여")
@PostMapping("/{potId}")
@ApiErrorCodeExamples({
ErrorStatus.BADGE_NOT_FOUND,
ErrorStatus.BADGE_INSUFFICIENT_TODO_COUNTS,
ErrorStatus.POT_MEMBER_NOT_FOUND
})
public ResponseEntity<ApiResponse<Void>> assignBadgeToTopMembers(
@PathVariable Long potId) {
public ResponseEntity<ApiResponse<Void>> assignBadgeToTopMembers(@PathVariable Long potId) {
badgeService.assignBadgeToTopMembers(potId);
return ResponseEntity.ok(ApiResponse.onSuccess(null));
}


@Operation(summary = "전체 프로젝트 업무 수 대비 개인이 담당한 업무 수 비율이 큰 사람에게 '없어서는 안 될 능력자' 뱃지 부여")
@PostMapping("/{potId}/task-badge")
@ApiErrorCodeExamples({
ErrorStatus.BADGE_NOT_FOUND,
ErrorStatus.BADGE_INSUFFICIENT_TOP_MEMBERS,
ErrorStatus.POT_MEMBER_NOT_FOUND
})
public ResponseEntity<ApiResponse<Void>> assignTaskBadgeToTopMembers(@PathVariable Long potId) {
badgeService.assignTaskBadgeToTopMembers(potId);
return ResponseEntity.ok(ApiResponse.onSuccess(null));
}
Comment on lines +49 to +59
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

신규 Task 배지 부여 API: 트랜잭션/권한/멱등성 점검 필요

  • 현재 Service의 assignTaskBadgeToTopMembers는 트랜잭션 애노테이션이 없습니다. 배지 생성이 다건으로 수행되므로 전체 작업의 원자성을 위해 @Transactional 적용이 필요합니다(서비스 파일에서 제안 패치 포함).
  • 권한: 아무 사용자나 호출 가능한지 확인 필요합니다. 최소한 팟 관리자/소유자만 수행 가능하도록 보안 정책을 점검해 주세요(컨트롤러 또는 서비스 레벨).
  • 멱등성: 동일 요청 반복 시 동일 멤버에 중복 배지가 생성될 수 있습니다. 중복 방지를 위한 존재 여부 체크 또는 DB 유니크 제약(예: badge_id + pot_member_id)을 권장합니다(서비스 파일에서 제안 패치 포함).

원하시면 엔드투엔드 테스트/보안 설정 점검까지 같이 도와드리겠습니다.


서비스 레이어에 트랜잭션 애노테이션 적용 여부 및 컨트롤러 보안 설정 검증을 위해 아래 스크립트를 실행해주세요.


🏁 Script executed:

#!/bin/bash
# assignTaskBadgeToTopMembers 메서드 정의 및 애노테이션 확인
rg -n "assignTaskBadgeToTopMembers" -A3 -B3 src/main/java

# PotBadgeMemberController 보안 애노테이션 확인
rg -n "@PreAuthorize" -A2 -B2 src/main/java/stackpot/stackpot/badge/controller/PotBadgeMemberController.java

# 컨트롤러 클래스 상단 확인
sed -n '1,50p' src/main/java/stackpot/stackpot/badge/controller/PotBadgeMemberController.java

Length of output: 4709


트랜잭션·권한·멱등성 보강 필요

아래 사항을 반영해 주세요.

• 서비스 레이어에 @Transactional 적용

  • BadgeServiceImpl 클래스나 assignTaskBadgeToTopMembers 메서드에 반드시 트랜잭션 경계를 지정해야 합니다.
    예시:
@Service
@RequiredArgsConstructor
@Transactional              // 클래스 단위로 모든 메서드에 적용
public class BadgeServiceImpl implements BadgeService {
    // …
}

또는

@Override
@Transactional              // 해당 메서드에만 적용
public void assignTaskBadgeToTopMembers(Long potId) { … }

• 권한 검사(@PreAuthorize) 추가

  • 팟 관리자/소유자만 호출할 수 있도록 컨트롤러 메서드에 메소드 시큐리티를 걸어주세요.
@PreAuthorize("hasAnyRole('POT_ADMIN','POT_OWNER')")
@PostMapping("/{potId}/task-badge")
public ResponseEntity<ApiResponse<Void>> assignTaskBadgeToTopMembers(@PathVariable Long potId) { … }

• 멱등성 보장 로직 또는 DB 제약

  • 동일 멤버에 중복 배지 발급을 막기 위해 서비스에서 존재 여부 확인 후 삽입하거나, DB에 (pot_member_id, badge_id) 같은 유니크 제약을 추가하세요.
if (badgeRepository.existsByPotMemberIdAndBadgeType(memberId, TASK_BADGE))
    return;
// else insert…

원하시면 엔드투엔드 테스트 작성 및 스프링 시큐리티 설정 점검도 함께 도와드리겠습니다.

}

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import stackpot.stackpot.badge.entity.Badge;

public interface BadgeService {
Badge getDefaultBadge();
Badge getBadge(Long badgeId);
void assignBadgeToTopMembers(Long potId);
void assignTaskBadgeToTopMembers(Long potId);
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import stackpot.stackpot.badge.repository.PotMemberBadgeRepository;
import stackpot.stackpot.pot.entity.mapping.PotMember;
import stackpot.stackpot.pot.repository.PotMemberRepository;
import stackpot.stackpot.task.service.TaskQueryService;
import stackpot.stackpot.todo.entity.enums.TodoStatus;
import stackpot.stackpot.todo.repository.UserTodoRepository;

Expand All @@ -29,12 +30,11 @@ public class BadgeServiceImpl implements BadgeService {
private final PotMemberRepository potMemberRepository;
private final PotMemberBadgeRepository potMemberBadgeRepository;
private final PotBadgeMemberConverter potBadgeMemberConverter;

private static final Long DEFAULT_BADGE_ID = 1L;
private final TaskQueryService taskQueryService;

@Override
public Badge getDefaultBadge() {
return badgeRepository.findBadgeByBadgeId(DEFAULT_BADGE_ID)
public Badge getBadge(Long badgeId) {
return badgeRepository.findBadgeByBadgeId(badgeId)
.orElseThrow(() -> new PotHandler(BADGE_NOT_FOUND));
}

Expand All @@ -57,10 +57,10 @@ public void assignBadgeToTopMembers(Long potId) {
List<PotMember> topPotMembers = topUserIds.stream()
.map(userId -> potMemberRepository.findByPot_PotIdAndUser_Id(potId, userId)
.orElseThrow(() -> new PotHandler(ErrorStatus.POT_MEMBER_NOT_FOUND)))
.collect(Collectors.toList());
.toList();

// 4. 기본 배지 부여
Badge badge = getDefaultBadge();
// 4. Todo 배지 부여
Badge badge = getBadge(1L);
for (PotMember potMember : topPotMembers) {
PotMemberBadge potMemberBadge = PotMemberBadge.builder()
.badge(badge)
Expand All @@ -69,5 +69,27 @@ public void assignBadgeToTopMembers(Long potId) {
potMemberBadgeRepository.save(potMemberBadge);
}
}

@Override
public void assignTaskBadgeToTopMembers(Long potId) {
List<Long> potMemberIds = potMemberRepository.selectPotMemberIdsByPotId(potId);
if (potMemberIds.isEmpty()) {
throw new PotHandler(ErrorStatus.POT_MEMBER_NOT_FOUND);
}

List<PotMember> top2PotMembers = taskQueryService.getTop2TaskCountByPotMemberId(potMemberIds);
if (top2PotMembers.size() < 2) {
throw new PotHandler(ErrorStatus.BADGE_INSUFFICIENT_TOP_MEMBERS);
}

Badge badge = getBadge(2L);
for (PotMember potMember : top2PotMembers) {
PotMemberBadge potMemberBadge = PotMemberBadge.builder()
.badge(badge)
.potMember(potMember)
.build();
potMemberBadgeRepository.save(potMemberBadge);
}
}
Comment on lines +73 to +93
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

[중요] 트랜잭션/멱등성/배치 저장 개선

  • 트랜잭션: 다건 저장의 원자성을 위해 @Transactional 필요.
  • 멱등성: 동일 호출 반복 시 중복 배지 생성 가능. 존재 체크 후 신규만 저장 필요.
  • 성능: 루프 내 save 대신 saveAll로 배치 저장.

권장 패치:

     @Override
-    public void assignTaskBadgeToTopMembers(Long potId) {
+    @Transactional
+    public void assignTaskBadgeToTopMembers(Long potId) {
         List<Long> potMemberIds = potMemberRepository.selectPotMemberIdsByPotId(potId);
-        if (potMemberIds.isEmpty()) {
+        if (potMemberIds.isEmpty()) {
             throw new PotHandler(ErrorStatus.POT_MEMBER_NOT_FOUND);
         }
 
         List<PotMember> top2PotMembers = taskQueryService.getTop2TaskCountByPotMemberId(potMemberIds);
         if (top2PotMembers.size() < 2) {
             throw new PotHandler(ErrorStatus.BADGE_INSUFFICIENT_TOP_MEMBERS);
         }
 
-        Badge badge = getBadge(2L);
-        for (PotMember potMember : top2PotMembers) {
-            PotMemberBadge potMemberBadge = PotMemberBadge.builder()
-                    .badge(badge)
-                    .potMember(potMember)
-                    .build();
-            potMemberBadgeRepository.save(potMemberBadge);
-        }
+        Badge badge = getBadge(2L); // 또는 상수/코드명 사용
+        // 멱등성: 이미 보유한 대상 제외
+        List<PotMemberBadge> newBadges = top2PotMembers.stream()
+                .filter(pm -> !potMemberBadgeRepository.existsByBadgeAndPotMember(badge, pm))
+                .map(pm -> PotMemberBadge.builder().badge(badge).potMember(pm).build())
+                .collect(Collectors.toList());
+        if (!newBadges.isEmpty()) {
+            potMemberBadgeRepository.saveAll(newBadges);
+        }
     }

Repository 보조 메서드(외부 파일 추가 필요):

// PotMemberBadgeRepository.java
boolean existsByBadgeAndPotMember(Badge badge, PotMember potMember);
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/badge/service/BadgeServiceImpl.java around
lines 73 to 93, the method assignTaskBadgeToTopMembers lacks transactionality,
idempotency checks, and uses per-entity saves; annotate the method with
@Transactional, for each top PotMember check
repository.existsByBadgeAndPotMember(badge, potMember) and skip existing
assignments, collect only new PotMemberBadge instances into a list and call
potMemberBadgeRepository.saveAll(newBadges) once to perform a batched insert;
also add the repository helper boolean existsByBadgeAndPotMember(Badge badge,
PotMember potMember) to PotMemberBadgeRepository.

}

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ public class FeedCommentConverter {
public FeedCommentResponseDto.AllFeedCommentDto toAllFeedCommentDto(FeedCommentDto.FeedCommentInfoDto dto, Long currentUserId) {
return FeedCommentResponseDto.AllFeedCommentDto.builder()
.userId(dto.getUserId())
.userName(dto.getUserName() + " " + dto.getRole().getVegetable())
.role(dto.getRole())
.userName(dto.getUserName() + " 새싹")
.isCommentWriter(Objects.equals(dto.getUserId(), currentUserId))
.isFeedWriter(Objects.equals(dto.getFeedWriterId(), dto.getUserId()))
.commentId(dto.getCommentId())
Expand All @@ -30,26 +29,24 @@ public FeedCommentResponseDto.AllFeedCommentDto toAllFeedCommentDto(FeedCommentD
.build();
}

public FeedCommentResponseDto.FeedCommentCreateDto toFeedCommentCreateDto(Long userId, String userName, Role role, Boolean isWriter,
public FeedCommentResponseDto.FeedCommentCreateDto toFeedCommentCreateDto(Long userId, String userName, Boolean isWriter,
Long commentId, String comment, LocalDateTime createdAt) {
return FeedCommentResponseDto.FeedCommentCreateDto.builder()
.userId(userId)
.userName(userName)
.role(role)
.userName(userName + " 새싹")
.isWriter(isWriter)
.commentId(commentId)
.comment(comment)
.createdAt(createdAt)
.build();
}

public FeedCommentResponseDto.FeedReplyCommentCreateDto toFeedReplyCommentCreateDto(Long userId, String userName, Role role, Boolean isWriter,
public FeedCommentResponseDto.FeedReplyCommentCreateDto toFeedReplyCommentCreateDto(Long userId, String userName, Boolean isWriter,
Long commentId, String comment, Long parentCommentId,
LocalDateTime createdAt) {
return FeedCommentResponseDto.FeedReplyCommentCreateDto.builder()
.userId(userId)
.userName(userName)
.role(role)
.userName(userName + " 새싹")
.isWriter(isWriter)
.commentId(commentId)
.comment(comment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public class FeedCommentDto {
public static class FeedCommentInfoDto {
private Long userId;
private String userName;
private Role role;
private Long feedWriterId; // Feed 작성자
private Long commentId;
private String comment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
public class FeedCommentInfoDto {
private Long userId;
private String userName;
private Role role;
private Long feedWriterId;
private Long commentId;
private String comment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public class FeedCommentResponseDto {
public static class AllFeedCommentDto {
private Long userId;
private String userName;
private Role role;
private Boolean isCommentWriter;
private Boolean isFeedWriter;
private Long commentId;
Expand All @@ -35,7 +34,6 @@ public static class AllFeedCommentDto {
public static class FeedCommentCreateDto {
private Long userId;
private String userName;
private Role role;
private Boolean isWriter;
private Long commentId;
private String comment;
Expand All @@ -49,7 +47,6 @@ public static class FeedCommentCreateDto {
public static class FeedReplyCommentCreateDto {
private Long userId;
private String userName;
private Role role;
private Boolean isWriter;
private Long commentId;
private String comment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ public interface FeedCommentRepository extends JpaRepository<FeedComment, Long>
@Query("select fc from FeedComment fc where fc.id = :commentId")
Optional<FeedComment> findByCommentId(@Param("commentId") Long commentId);

@Query("select new stackpot.stackpot.feed.dto.FeedCommentInfoDto(fc.user.id, fc.user.nickname, fc.user.role, " +
@Query("select new stackpot.stackpot.feed.dto.FeedCommentDto$FeedCommentInfoDto(fc.user.id, fc.user.nickname, " +
"fc.feed.user.id, fc.id, fc.comment, fc.parent.id, fc.createdAt) " +
"from FeedComment fc where fc.feed.feedId = :feedId")
List<FeedCommentDto.FeedCommentInfoDto> findAllCommentInfoDtoByFeedId(@Param("feedId") Long feedId);

@Query("SELECT COUNT(fc) FROM FeedComment fc WHERE fc.feed.feedId = :feedId")
Long countByFeedId(@Param("feedId") Long feedId);
}

Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ public boolean toggleLike(Long feedId) {
FeedLike savedFeedLike = feedLikeRepository.save(feedLike);

NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createFeedLikeNotification(
feed.getFeedId(), savedFeedLike.getLikeId(), user.getId(), user.getRole());
feed.getFeedId(), savedFeedLike.getLikeId(), user.getId());

applicationEventPublisher.publishEvent(new FeedLikeEvent(feed.getUser().getUserId(), dto));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ public FeedCommentResponseDto.FeedCommentCreateDto createFeedComment(FeedComment
Boolean isWriter = Objects.equals(user.getId(), feed.getUser().getUserId());

NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createdFeedCommentNotification(
feedId, feedComment.getId(), user.getId(), user.getRole());
feedId, feedComment.getId(), user.getId());

applicationEventPublisher.publishEvent(new FeedCommentEvent(feed.getUser().getUserId(), null, dto));

return feedCommentConverter.toFeedCommentCreateDto(user.getUserId(), user.getNickname(), user.getRole(), isWriter,
return feedCommentConverter.toFeedCommentCreateDto(user.getUserId(), user.getNickname(), isWriter,
feedComment.getId(), comment, feedComment.getCreatedAt());
}

Expand All @@ -73,11 +73,11 @@ public FeedCommentResponseDto.FeedReplyCommentCreateDto createFeedReplyComment(L
Boolean isWriter = Objects.equals(user.getId(), feed.getUser().getUserId());

NotificationResponseDto.UnReadNotificationDto dto = notificationCommandService.createdFeedCommentNotification(
feedId, feedComment.getId(), user.getId(), user.getRole());
feedId, feedComment.getId(), user.getId());

applicationEventPublisher.publishEvent(new FeedCommentEvent(feed.getUser().getUserId(), parent.getUser().getUserId(), dto));

return feedCommentConverter.toFeedReplyCommentCreateDto(user.getUserId(), user.getNickname(), user.getRole(), isWriter,
return feedCommentConverter.toFeedReplyCommentCreateDto(user.getUserId(), user.getNickname(), isWriter,
feedComment.getId(), comment, parent.getId(), feedComment.getCreatedAt());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,19 @@ public NotificationResponseDto.UnReadNotificationDto toUnReadNotificationDto(Not
return NotificationResponseDto.UnReadNotificationDto.builder()
.notificationId(unReadNotificationDto.getNotificationId())
.potOrFeedId(unReadNotificationDto.getPotOrFeedId())
.role(unReadNotificationDto.getRole())
.userName(unReadNotificationDto.getUserName() + " " + unReadNotificationDto.getRole().getVegetable())
.userName(unReadNotificationDto.getUserName() + " 새싹")
.type(unReadNotificationDto.getType())
.content(unReadNotificationDto.getContent())
.createdAt(unReadNotificationDto.getCreatedAt().format(DATE_FORMATTER))
.build();
}

public NotificationResponseDto.UnReadNotificationDto toUnReadNotificationDto(
Long notificationId, Long potOrFeedId, Role role, String userName, String type, String content, LocalDateTime createdAt) {
Long notificationId, Long potOrFeedId, String userName, String type, String content, LocalDateTime createdAt) {
return NotificationResponseDto.UnReadNotificationDto.builder()
.notificationId(notificationId)
.potOrFeedId(potOrFeedId)
.role(role)
.userName(userName + " " + role.getVegetable())
.userName(userName + " 새싹")
.type(type)
.content(content)
.createdAt(createdAt.format(DATE_FORMATTER))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public class NotificationDto {
public static class UnReadNotificationDto {
private Long notificationId;
private Long potOrFeedId;
private Role role;
private String userName;
private String type;
private String content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public class NotificationResponseDto {
public static class UnReadNotificationDto {
private Long notificationId;
private Long potOrFeedId;
private Role role;
private String userName;
private String type;
private String content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
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.role," +
"fcn.id, fcn.feedComment.feed.feedId, " +
"fcn.feedComment.user.nickname, 'FeedComment', fcn.feedComment.comment, fcn.createdAt) " +
"FROM FeedCommentNotification fcn " +
"WHERE fcn.isRead = false AND (" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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.role," +
"fln.id, fln.feedLike.feed.feedId, " +
"fln.feedLike.user.nickname, 'FeedLike', null, fln.createdAt) " +
"FROM FeedLikeNotification fln " +
"WHERE fln.isRead = false and fln.feedLike.feed.user.id = :userId ")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
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.role, pan.potApplication.user.nickname, " +
"pan.id, pan.potApplication.pot.potId, pan.potApplication.user.nickname, " +
"'PotApplication', null, pan.createdAt) " +
"FROM PotApplicationNotification pan " +
"WHERE pan.isRead = false AND pan.potApplication.pot.user.id = :userId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
public interface PotCommentNotificationRepository extends JpaRepository<PotCommentNotification, Long> {

@Query("SELECT new stackpot.stackpot.notification.dto.NotificationDto$UnReadNotificationDto(" +
"pcn.id, pcn.potComment.pot.potId, pcn.potComment.user.role, " +
"pcn.id, pcn.potComment.pot.potId, " +
"pcn.potComment.user.nickname, 'PotComment', pcn.potComment.comment, pcn.createdAt) " +
"FROM PotCommentNotification pcn " +
"WHERE pcn.isRead = false AND (" +
Expand Down
Loading