-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 투표 삭제 api 개발 #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feat] 투표 삭제 api 개발 #192
Changes from all commits
9b3253e
1dfd597
0109487
08b0407
495adfc
126a53f
35e92f9
323f51c
75f7d21
05801f3
264d455
656c5df
8caf81f
9ace812
38ced96
961e588
8414d0c
86a08d3
6eedfbf
b4b564d
1c65217
e6f95a9
901f29d
58915dc
7e3cefc
324a65e
05f4e53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ on: | |
| push: | ||
| branches: | ||
| - 'main' | ||
| - 'develop' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,7 +75,7 @@ public Long save(Feed feed) { | |
|
|
||
| @Override | ||
| public Long update(Feed feed) { | ||
| FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getId()) | ||
| FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostIdAndStatus(feed.getId(),ACTIVE) | ||
| .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); | ||
|
Comment on lines
+78
to
79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainACTIVE 필터로 업데이트 대상 조회 — OK. (post_id, status) 복합 인덱스 존재 여부 확인 권장 해당 조회가 핫패스일 가능성이 높습니다. DB에 (post_id, status) 복합 인덱스가 없으면 성능 저하가 발생할 수 있습니다. 엔티티(@Index) 또는 스키마 마이그레이션에 인덱스 정의가 있는지 확인 부탁드립니다. 🏁 Script executed: #!/bin/bash
# 엔티티에 @Index로 복합 인덱스 선언 여부 점검
rg -n "@Index\\(" -g "src/**.java"
# Feed/Record JPA 엔티티에서 postId/status 컬럼 매핑 확인
rg -n "class .*FeedJpaEntity|class .*RecordJpaEntity" -A 50 -g "src/**.java" | rg -n "postId|status"
# Repository 시그니처 확인(참고)
rg -n "findByPostIdAndStatus\\(" -g "src/**.java"Length of output: 636 🏁 Script executed: #!/bin/bash
# 1) @Index 어노테이션으로 복합 인덱스 선언 여부 확인
rg -n '@Index' -g 'src/**/*.java'
# 2) @Table(indexes = …) 형태로 인덱스 설정 확인
rg -n '@Table' -g 'src/**/*.java' | rg -n 'indexes'
# 3) FeedJpaEntity에서 postId/status 컬럼 매핑 확인
rg -n 'class .*FeedJpaEntity' -A 50 -g 'src/**/*.java' | rg -n 'postId\|status'
# 4) Repository 메서드 시그니처(findByPostIdAndStatus) 확인
rg -n 'findByPostIdAndStatus' -g 'src/**/*.java'Length of output: 3366 (post_id, status) 복합 인덱스 부재 확인 — 추가가 필요합니다 수정 대상 예시:
@Entity
@Table(name = "feed",
indexes = @Index(
name = "idx_feed_post_id_status",
columnList = "post_id, status"
)
)
public class FeedJpaEntity { … }🤖 Prompt for AI Agents |
||
| feedJpaEntity.updateFrom(feed); | ||
|
|
||
|
|
@@ -115,7 +115,7 @@ private void applyFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { | |
| public void saveSavedFeed(Long userId, Long feedId) { | ||
| UserJpaEntity user = userJpaRepository.findById(userId) | ||
| .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); | ||
| FeedJpaEntity feed = feedJpaRepository.findById(feedId) | ||
| FeedJpaEntity feed = feedJpaRepository.findByPostIdAndStatus(feedId,ACTIVE) | ||
| .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); | ||
| SavedFeedJpaEntity entity = SavedFeedJpaEntity.builder() | ||
| .userJpaEntity(user) | ||
|
|
@@ -131,7 +131,7 @@ public void deleteSavedFeed(Long userId, Long feedId) { | |
|
|
||
| @Override | ||
| public void delete(Feed feed) { | ||
| FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getId()) | ||
| FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostIdAndStatus(feed.getId(),ACTIVE) | ||
| .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); | ||
|
|
||
| feedTagJpaRepository.deleteAllByFeedId(feedJpaEntity.getPostId()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package konkuk.thip.vote.adapter.in.web.response; | ||
|
|
||
| public record VoteDeleteResponse(Long roomId) { | ||
| public static VoteDeleteResponse of(Long roomId) { | ||
| return new VoteDeleteResponse(roomId); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| import static konkuk.thip.common.entity.StatusType.ACTIVE; | ||
| import static konkuk.thip.common.exception.code.ErrorCode.*; | ||
|
|
||
| @Repository | ||
|
|
@@ -58,7 +59,7 @@ public void saveAllVoteItems(List<VoteItem> voteItems) { | |
| if (voteItems.isEmpty()) return; | ||
|
|
||
| Long voteId = voteItems.get(0).getVoteId(); | ||
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteId).orElseThrow( | ||
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostIdAndStatus(voteId,ACTIVE).orElseThrow( | ||
| () -> new EntityNotFoundException(VOTE_NOT_FOUND) | ||
| ); | ||
|
|
||
|
|
@@ -71,7 +72,7 @@ public void saveAllVoteItems(List<VoteItem> voteItems) { | |
|
|
||
| @Override | ||
| public Optional<Vote> findById(Long id) { | ||
| return voteJpaRepository.findById(id) | ||
| return voteJpaRepository.findByPostIdAndStatus(id,ACTIVE) | ||
| .map(voteMapper::toDomainEntity); | ||
| } | ||
hd0rable marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
@@ -135,10 +136,23 @@ public void updateVoteItem(VoteItem voteItem) { | |
| voteItemJpaRepository.save(voteItemJpaEntity.updateFrom(voteItem)); | ||
| } | ||
|
|
||
| @Override | ||
| public void delete(Vote vote) { | ||
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostIdAndStatus(vote.getId(),ACTIVE).orElseThrow( | ||
| () -> new EntityNotFoundException(VOTE_NOT_FOUND) | ||
| ); | ||
|
|
||
| voteParticipantJpaRepository.deleteAllByVoteId(voteJpaEntity.getPostId()); | ||
| voteItemJpaRepository.deleteAllByVoteId(voteJpaEntity.getPostId()); | ||
|
|
||
| voteJpaEntity.softDelete(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 저번에 jpa entity 내부에 status 의 상태를 변경하는 (= soft delete) 전용 메서드 뚫어놓고, 이를 변경 감지 기능을 활용해서 soft delete 처리한다는 부분이 이 코드였군요! 확인했습니다 |
||
| voteJpaRepository.save(voteJpaEntity); | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public void updateVote(Vote vote) { | ||
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(vote.getId()).orElseThrow( | ||
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostIdAndStatus(vote.getId(),ACTIVE).orElseThrow( | ||
| () -> new EntityNotFoundException(VOTE_NOT_FOUND) | ||
| ); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,11 @@ | ||
| package konkuk.thip.vote.adapter.out.persistence.repository; | ||
|
|
||
| import konkuk.thip.common.entity.StatusType; | ||
| import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface VoteJpaRepository extends JpaRepository<VoteJpaEntity, Long>, VoteQueryRepository { | ||
| import java.util.Optional; | ||
|
|
||
| public interface VoteJpaRepository extends JpaRepository<VoteJpaEntity, Long>, VoteQueryRepository { | ||
| Optional<VoteJpaEntity> findByPostIdAndStatus(Long postId, StatusType status); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package konkuk.thip.vote.application.port.in; | ||
|
|
||
| import konkuk.thip.vote.application.port.in.dto.VoteDeleteCommand; | ||
|
|
||
| public interface VoteDeleteUseCase { | ||
| Long deleteVote(VoteDeleteCommand command); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package konkuk.thip.vote.application.port.in.dto; | ||
|
|
||
| public record VoteDeleteCommand( | ||
| Long roomId, | ||
|
|
||
| Long voteId, | ||
|
|
||
| Long userId | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package konkuk.thip.vote.application.service; | ||
|
|
||
| import jakarta.transaction.Transactional; | ||
hd0rable marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import konkuk.thip.comment.application.port.out.CommentCommandPort; | ||
| import konkuk.thip.post.application.port.out.PostLikeCommandPort; | ||
| import konkuk.thip.room.application.service.validator.RoomParticipantValidator; | ||
| import konkuk.thip.vote.application.port.in.VoteDeleteUseCase; | ||
| import konkuk.thip.vote.application.port.in.dto.VoteDeleteCommand; | ||
| import konkuk.thip.vote.application.port.out.VoteCommandPort; | ||
| import konkuk.thip.vote.domain.Vote; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class VoteDeleteService implements VoteDeleteUseCase { | ||
|
|
||
| private final VoteCommandPort voteCommandPort; | ||
| private final CommentCommandPort commentCommandPort; | ||
| private final PostLikeCommandPort postLikeCommandPort; | ||
|
|
||
| private final RoomParticipantValidator roomParticipantValidator; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public Long deleteVote(VoteDeleteCommand command) { | ||
|
|
||
| // 1. 방 참여자 검증 | ||
| roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId()); | ||
|
|
||
| // 2. 투표 조회 및 검증 | ||
| Vote vote = voteCommandPort.getByIdOrThrow(command.voteId()); | ||
| // 2-1. 투표 삭제 권한 검증 | ||
| vote.validateDeletable(command.userId(),command.roomId()); | ||
|
|
||
| // 3. 투표 삭제 | ||
| // 3-1. 투표 게시글 댓글 삭제 | ||
| commentCommandPort.softDeleteAllByPostId(command.voteId()); | ||
| // 3-2. 투표 게시글 좋아요 삭제 | ||
| postLikeCommandPort.deleteAllByPostId(command.voteId()); | ||
|
Comment on lines
+37
to
+40
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 이미 댓글, 댓글 좋아요, 게시글 좋아요 의 hard/soft delete 메서드를 구현해두셨군요!! 방 삭제 구현할떄 잘 쓰겠습니다!! LGTM |
||
| // 3-3. 투표 삭제 | ||
| voteCommandPort.delete(vote); | ||
|
|
||
| return command.roomId(); | ||
|
Comment on lines
+36
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이렇게 애그리거트 단위로 삭제를 진행하니까 확실히 재사용성이 높네요!! LGTM |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,4 +90,21 @@ private void checkCommentCountNotUnderflow() { | |
| throw new InvalidStateException(COMMENT_COUNT_UNDERFLOW); | ||
| } | ||
| } | ||
|
|
||
| private void validateCreator(Long userId) { | ||
| if (!this.creatorId.equals(userId)) { | ||
| throw new InvalidStateException(VOTE_ACCESS_FORBIDDEN, new IllegalArgumentException("투표 작성자만 투표를 수정/삭제할 수 있습니다.")); | ||
| } | ||
| } | ||
|
|
||
| public void validateDeletable(Long userId,Long roomId) { | ||
| validateRoomId(roomId); | ||
| validateCreator(userId); | ||
| } | ||
|
|
||
| private void validateRoomId(Long roomId) { | ||
| if (!this.roomId.equals(roomId)) { | ||
| throw new InvalidStateException(VOTE_ACCESS_FORBIDDEN, new IllegalArgumentException("투표가 해당 방에 속하지 않습니다.")); | ||
| } | ||
| } | ||
|
Comment on lines
+94
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏻 |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
develop 브랜치 푸시 시 프로덕션 CD가 실행됩니다 — 의도된가요? 매우 위험합니다
이 워크플로우는 PROD 시크릿/호스트(EC2_HOST_PROD, APPLICATION_YML_PROD)를 사용해 실제 배포까지 수행합니다. develop에 푸시만 해도 프로덕션 배포가 일어나는 구조라, 오배포 리스크가 큽니다. 최소한 main 전용으로 제한하거나, 배포/푸시 단계에 조건을 걸어 main에서만 실행되도록 가드해주세요.
Option A: develop 트리거 제거(가장 안전)
push: branches: - 'main' - - 'develop'📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의도 된거야
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seongjunnoh 알겠습니다! develop 브랜치에서 프로덕션 배포가 의도된 워크플로우라면 그대로 진행하시면 됩니다. 팀의 배포 전략에 따른 결정이군요.
✏️ Learnings added
🧠 Learnings used