Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9b3253e
[feat] 관련 에러코드 추가 (#88)
hd0rable Aug 11, 2025
1dfd597
[feat] 관련 에러코드 스웨거 추가 (#88)
hd0rable Aug 11, 2025
0109487
[feat] 투표 삭제 권한 검증 투표 도메인 관련 함수 추가 (#188)
hd0rable Aug 11, 2025
08b0407
[feat] 투표 삭제 컨트롤러 작성 (#188)
hd0rable Aug 11, 2025
495adfc
[feat] VoteCommandPersistenceAdapter.delete 작성 (#188)
hd0rable Aug 11, 2025
126a53f
[feat] VoteCommandPort.delete 작성 (#188)
hd0rable Aug 11, 2025
35e92f9
[feat] VoteDeleteCommand dto 작성 (#188)
hd0rable Aug 11, 2025
323f51c
[feat] VoteDeleteResponse dto 작성 (#188)
hd0rable Aug 11, 2025
75f7d21
[feat] VoteDeleteService.deleteVote 작성 (#188)
hd0rable Aug 11, 2025
05801f3
[feat] VoteDeleteUseCase.deleteVote 작성 (#188)
hd0rable Aug 11, 2025
264d455
[feat] VoteItemJpaRepository.deleteAllByVoteId 작성 (#188)
hd0rable Aug 11, 2025
656c5df
[feat] 테스트용 updateCommentCount 작성 (#188)
hd0rable Aug 11, 2025
8caf81f
[feat] VoteJpaRepository.findByPostIdAndStatus 작성 (#188)
hd0rable Aug 11, 2025
9ace812
[feat] VoteParticipantJpaRepository.deleteAllByVoteId 작성 (#188)
hd0rable Aug 11, 2025
38ced96
[remove] 더미파일 삭제 (#188)
hd0rable Aug 11, 2025
961e588
[test] 투표 단위 도메인 테스트 코드 추가 (#188)
hd0rable Aug 11, 2025
8414d0c
[test] 투표 삭제 통합 테스트 코드 작성 (#188)
hd0rable Aug 11, 2025
86a08d3
[test] 투표 항목 테스트 팩토리 메서드 작성 (#188)
hd0rable Aug 11, 2025
6eedfbf
[test] 설명 오타 쉊ㅇ (#188)
hd0rable Aug 11, 2025
b4b564d
[test] 누락된 게시글 좋아요 검증 (#188)
hd0rable Aug 11, 2025
1c65217
[fix] 소프트 딜리트 되는 대상 액티브한 엔티티 조회로 수정 (#188)
hd0rable Aug 11, 2025
e6f95a9
[fix] import 수정 (#188)
hd0rable Aug 11, 2025
901f29d
[refactor] 서브쿼리로 수정 (#188)
hd0rable Aug 11, 2025
58915dc
[chore] 일시적 develop 브랜치 운영서버 머지 허용 (#161)
hd0rable Aug 13, 2025
7e3cefc
Merge remote-tracking branch 'origin/develop' into feat/#188-vote-delete
hd0rable Aug 13, 2025
324a65e
Merge remote-tracking branch 'origin/develop' into feat/#188-vote-delete
hd0rable Aug 13, 2025
05f4e53
Merge remote-tracking branch 'origin' into feat/#188-vote-delete
seongjunnoh Aug 14, 2025
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
1 change: 1 addition & 0 deletions .github/workflows/cd-workflow-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- 'main'
- 'develop'
Copy link

@coderabbitai coderabbitai bot Aug 13, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

develop 브랜치 푸시 시 프로덕션 CD가 실행됩니다 — 의도된가요? 매우 위험합니다

이 워크플로우는 PROD 시크릿/호스트(EC2_HOST_PROD, APPLICATION_YML_PROD)를 사용해 실제 배포까지 수행합니다. develop에 푸시만 해도 프로덕션 배포가 일어나는 구조라, 오배포 리스크가 큽니다. 최소한 main 전용으로 제한하거나, 배포/푸시 단계에 조건을 걸어 main에서만 실행되도록 가드해주세요.

Option A: develop 트리거 제거(가장 안전)

   push:
     branches:
       - 'main'
-      - 'develop'
📝 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
- 'develop'
push:
branches:
- 'main'
🤖 Prompt for AI Agents
.github/workflows/cd-workflow-prod.yml lines 7-7: the workflow is triggered on
pushes to the 'develop' branch which causes full production CD to run — change
the trigger to prevent accidental prod deploys by removing 'develop' and
limiting the workflow to 'main' (or add a job-level guard). Replace the push
branches list so it only includes 'main' (or add an if: condition on the
deployment job such as github.ref == 'refs/heads/main') and verify secrets/hosts
are only used when that condition is true; alternatively remove the develop
trigger entirely to adopt Option A.

Copy link
Collaborator

Choose a reason for hiding this comment

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

의도 된거야

Copy link

Choose a reason for hiding this comment

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

@seongjunnoh 알겠습니다! develop 브랜치에서 프로덕션 배포가 의도된 워크플로우라면 그대로 진행하시면 됩니다. 팀의 배포 전략에 따른 결정이군요.


✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#192
File: .github/workflows/cd-workflow-prod.yml:7-7
Timestamp: 2025-08-13T07:52:24.145Z
Learning: seongjunnoh는 develop 브랜치에서 프로덕션 CD가 트리거되는 것을 의도적으로 설정하여 사용하고 있으며, 이는 팀의 배포 전략에 따른 결정이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.258Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.


permissions:
contents: read
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public enum ErrorCode implements ResponseCode {
VOTE_NOT_FOUND(HttpStatus.NOT_FOUND, 110000, "존재하지 않는 VOTE 입니다."),
VOTE_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 110001, "총평이 될 수 없는 VOTE 입니다."),
INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다."),
VOTE_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 110003, "투표 접근 권한이 없습니다."),


/**
* 120000 : voteItem error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ public enum SwaggerResponseDescription {
VOTE_ITEM_NOT_VOTED_CANNOT_CANCEL,
VOTE_ITEM_COUNT_CANNOT_BE_NEGATIVE
))),
VOTE_DELETE(new LinkedHashSet<>(Set.of(
ROOM_ACCESS_FORBIDDEN,
VOTE_NOT_FOUND,
VOTE_ACCESS_FORBIDDEN
))),


// FEED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

ACTIVE 필터로 업데이트 대상 조회 — 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) 복합 인덱스 부재 확인 — 추가가 필요합니다
JPA 엔티티(@Index/@table(indexes)) 상에 (post_id, status) 복합 인덱스가 정의되어 있지 않습니다. 해당 조회는 핫패스이므로, DB 성능 저하를 방지하기 위해 스키마 마이그레이션 또는 JPA 어노테이션을 통해 인덱스를 추가해 주세요.

수정 대상 예시:

  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedJpaEntity.java
  • src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteJpaEntity.java
  • src/main/java/konkuk/thip/record/adapter/out/persistence/RecordJpaEntity.java
@Entity
@Table(name = "feed",
       indexes = @Index(
           name = "idx_feed_post_id_status",
           columnList = "post_id, status"
       )
)
public class FeedJpaEntity { … }
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java
around lines 78-79, the repository query uses (post_id, status) but the JPA
entities lack a composite index; add a composite index for (post_id, status) to
the FeedJpaEntity (and similarly to VoteJpaEntity and RecordJpaEntity) by
annotating the entity with a @Table(indexes = @Index(name =
"idx_<entity>_post_id_status", columnList = "post_id, status")) or create a DB
migration that adds the same composite index to the underlying tables; ensure
the column names in columnList match the physical column names, rebuild, and
apply migrations to prevent hot-path query slowdown.

feedJpaEntity.updateFrom(feed);

Expand Down Expand Up @@ -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)
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public Optional<Record> findById(Long id) {

@Override
public void delete(Record record) {
RecordJpaEntity recordJpaEntity = recordJpaRepository.findById(record.getId()).orElseThrow(
RecordJpaEntity recordJpaEntity = recordJpaRepository.findByPostIdAndStatus(record.getId(),ACTIVE).orElseThrow(
() -> new EntityNotFoundException(RECORD_NOT_FOUND)
);

Expand All @@ -60,7 +60,7 @@ public void delete(Record record) {

@Override
public void update(Record record) {
RecordJpaEntity recordJpaEntity = recordJpaRepository.findById(record.getId()).orElseThrow(
RecordJpaEntity recordJpaEntity = recordJpaRepository.findByPostIdAndStatus(record.getId(),ACTIVE).orElseThrow(
() -> new EntityNotFoundException(RECORD_NOT_FOUND)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@
import konkuk.thip.vote.adapter.in.web.request.VoteCreateRequest;
import konkuk.thip.vote.adapter.in.web.request.VoteRequest;
import konkuk.thip.vote.adapter.in.web.response.VoteCreateResponse;
import konkuk.thip.vote.adapter.in.web.response.VoteDeleteResponse;
import konkuk.thip.vote.adapter.in.web.response.VoteResponse;
import konkuk.thip.vote.application.port.in.VoteCreateUseCase;
import konkuk.thip.vote.application.port.in.VoteDeleteUseCase;
import konkuk.thip.vote.application.port.in.VoteUseCase;
import konkuk.thip.vote.application.port.in.dto.VoteResult;
import konkuk.thip.vote.application.port.in.dto.VoteDeleteCommand;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import static konkuk.thip.common.swagger.SwaggerResponseDescription.RECORD_DELETE;
import static konkuk.thip.common.swagger.SwaggerResponseDescription.VOTE;

@Tag(name = "Vote Command API", description = "투표 상태변경 관련 API")
Expand All @@ -28,6 +29,7 @@
public class VoteCommandController {

private final VoteCreateUseCase voteCreateUseCase;
private final VoteDeleteUseCase voteDeleteUseCase;
private final VoteUseCase voteUseCase;

@Operation(
Expand Down Expand Up @@ -59,4 +61,17 @@ public BaseResponse<VoteResponse> vote(
VoteResult voteResult = voteUseCase.vote(request.toCommand(userId, roomId, voteId));
return BaseResponse.ok(VoteResponse.of(voteResult.postId(), voteResult.roomId(), voteResult.voteItems()));
}

@Operation(
summary = "투표 삭제",
description = "사용자가 투표를 삭제합니다."
)
@ExceptionDescription(RECORD_DELETE)
@DeleteMapping("/rooms/{roomId}/vote/{voteId}")
public BaseResponse<VoteDeleteResponse> deleteVote(
@Parameter(description = "삭제하려는 투표 ID", example = "1") @PathVariable("voteId") final Long voteId,
@Parameter(description = "삭제하려는 투표가 작성된 모임 ID", example = "1") @PathVariable("roomId") final Long roomId,
@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(VoteDeleteResponse.of(voteDeleteUseCase.deleteVote(new VoteDeleteCommand(roomId, voteId, userId))));
}
}
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
Expand Up @@ -48,4 +48,9 @@ public VoteJpaEntity updateFrom(Vote vote) {
public void updateLikeCount(int likeCount) {
this.likeCount = likeCount;
}

@VisibleForTesting
public void updateCommentCount(int commentCount) {
this.commentCount = commentCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
);

Expand All @@ -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);
}

Expand Down Expand Up @@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

import konkuk.thip.vote.adapter.out.jpa.VoteItemJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface VoteItemJpaRepository extends JpaRepository<VoteItemJpaEntity, Long> {

List<VoteItemJpaEntity> findAllByVoteJpaEntity_PostId(Long voteId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM VoteItemJpaEntity vi WHERE vi.voteJpaEntity.postId = :voteId")
void deleteAllByVoteId(@Param("voteId") Long voteId);
}
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
Expand Up @@ -2,7 +2,9 @@

import konkuk.thip.vote.adapter.out.jpa.VoteParticipantJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Optional;
Expand All @@ -11,4 +13,16 @@
public interface VoteParticipantJpaRepository extends JpaRepository<VoteParticipantJpaEntity, Long>, VoteParticipantQueryRepository {
@Query("SELECT vp FROM VoteParticipantJpaEntity vp WHERE vp.userJpaEntity.userId = :userId AND vp.voteItemJpaEntity.voteItemId = :voteItemId")
Optional<VoteParticipantJpaEntity> findVoteParticipantByUserIdAndVoteItemId(Long userId, Long voteItemId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM VoteParticipantJpaEntity vp
WHERE vp.voteItemJpaEntity.voteItemId IN (
SELECT vi.voteItemId
FROM VoteItemJpaEntity vi
WHERE vi.voteJpaEntity.postId = :voteId
)
""")
void deleteAllByVoteId(@Param("voteId") Long voteId);

}
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
Expand Up @@ -44,4 +44,6 @@ default VoteItem getVoteItemByIdOrThrow(Long id) {
void deleteVoteParticipant(VoteParticipant voteParticipant);

void updateVoteItem(VoteItem voteItem);

void delete(Vote vote);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package konkuk.thip.vote.application.service;

import jakarta.transaction.Transactional;
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

이렇게 애그리거트 단위로 삭제를 진행하니까 확실히 재사용성이 높네요!! LGTM

}
}
17 changes: 17 additions & 0 deletions src/main/java/konkuk/thip/vote/domain/Vote.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻

}
8 changes: 8 additions & 0 deletions src/test/java/konkuk/thip/common/util/TestEntityFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ public static VoteJpaEntity createVote(UserJpaEntity user, RoomJpaEntity room) {
.build();
}

public static VoteItemJpaEntity createVoteItem(String itemName, VoteJpaEntity vote) {
return VoteItemJpaEntity.builder()
.itemName(itemName)
.count(0)
.voteJpaEntity(vote)
.build();
}

public static CommentJpaEntity createComment(PostJpaEntity post, UserJpaEntity user,PostType postType) {
return CommentJpaEntity.builder()
.content("댓글 내용")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,12 @@ void deleteFeed_success() throws Exception {
assertThat(commentJpaRepository.findById(comment.getCommentId()).get().getStatus()).isEqualTo(INACTIVE);

// 5) 댓글 좋아요 삭제
long commentLikeCountAfter = commentLikeJpaRepository.count();
assertThat(commentLikeCountAfter).isEqualTo(0);
assertThat(commentLikeJpaRepository.count()).isEqualTo(0);

// 6) 피드 저장 관계 삭제
assertTrue(savedFeedJpaRepository.findAll().isEmpty());

// 7) 게시글 좋아요(PostLike) 삭제
assertThat(postLikeJpaRepository.count()).isEqualTo(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ void deleteRecord_success() throws Exception {
assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent();

// 3) 댓글 좋아요 삭제
long commentLikeCountAfter = commentLikeJpaRepository.count();
assertThat(commentLikeCountAfter).isEqualTo(0);
assertThat(commentLikeJpaRepository.count()).isEqualTo(0);

// 4) 게시글 좋아요(PostLike) 삭제
assertThat(postLikeJpaRepository.count()).isEqualTo(0);

}
}
2 changes: 1 addition & 1 deletion src/test/java/konkuk/thip/record/domain/RecordTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ void validateDeletable_byOtherRoomId_throws(){
}

@Test
@DisplayName("validateDeletable: 피드의 작성자면서, 전달된 roomId가 기록의 roomId와 일치할 경우 기록을 삭제 할 수 있다.")
@DisplayName("validateDeletable: 기록의 작성자면서, 전달된 roomId가 기록의 roomId와 일치할 경우 기록을 삭제 할 수 있다.")
void validateDeletable_byCreator_byRoomId_Success(){
Record record = createWithCommentRecord();
assertDoesNotThrow(() -> record.validateDeletable(CREATOR_ID,ROOM_ID));
Expand Down
Loading