Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5b0d76d
[feat] 관련 에러코드 추가 (#175)
hd0rable Aug 8, 2025
25ba9ba
[feat] Record 삭제관련 도메인 코드 검증 추가 (#175)
hd0rable Aug 8, 2025
b229535
[feat] 기록 삭제 컨트롤러 작성 (#175)
hd0rable Aug 8, 2025
3ef2cef
[feat] RecordCommandPersistenceAdapter.delete 작성 (#175)
hd0rable Aug 8, 2025
59dcda0
[feat] RecordCommandPort.delete 작성 (#175)
hd0rable Aug 8, 2025
084fd31
[feat] RecordDeleteCommand dto 작성 (#175)
hd0rable Aug 8, 2025
83347f0
[feat] RecordDeleteResponse dto 작성 (#175)
hd0rable Aug 8, 2025
1d37ddc
[feat] 기록삭제 유즈케이스 RecordDeleteUseCase 작성 (#175)
hd0rable Aug 8, 2025
11b150e
[feat] 기록삭제 유즈케이스 구현체 RecordDeleteService 작성 (#175)
hd0rable Aug 8, 2025
daccf42
[feat] 테스트용 updateCommentCount 추가 (#175)
hd0rable Aug 8, 2025
8fab215
[feat] RecordJpaRepository.findByPostIdAndStatus 작성 (#175)
hd0rable Aug 8, 2025
322d9c3
[feat] 관련 에러코드 스웨거 추가 (#175)
hd0rable Aug 8, 2025
74db07f
[feat] 기록 삭제 통합 테스트코드 작성 (#175)
hd0rable Aug 8, 2025
deac37e
[feat] 기록 삭제관련 record 단위도메인 테스트코드 추가 (#175)
hd0rable Aug 8, 2025
d5db07a
[remove] 더미 파일 삭제 (#175)
hd0rable Aug 8, 2025
2437805
[test] 테스트 코드 수정 (#175)
hd0rable Aug 8, 2025
d5b677f
[refactor] Update/delete 쿼리 시 @Modifying(clearAutomatically = true, f…
hd0rable Aug 10, 2025
60e9a0c
[test] 테스트 코드 수정 (#175)
hd0rable Aug 10, 2025
aba65e6
Merge remote-tracking branch 'origin/feat/#160-feed-delete' into feat…
hd0rable Aug 10, 2025
e854b84
[refactor] Update/delete 쿼리 시 @Modifying(clearAutomatically = true, f…
hd0rable Aug 10, 2025
a219ccd
[refactor] 기록 삭제 방 아이디 검증 캡슐화 (#175)
hd0rable Aug 10, 2025
d369a33
[refactor] 기록 삭제 소프트 딜리트 전략 수정 (#175)
hd0rable Aug 10, 2025
f4fe287
[chore] 피드 삭제 관련 주석 수정(#175)
hd0rable Aug 10, 2025
2519adf
[test] 피드 삭제 테스트 코드 수정(#175)
hd0rable Aug 10, 2025
53d8930
[test] 기록 삭제 테스트 코드 수정(#175)
hd0rable Aug 10, 2025
72c85b7
[test] 기록 도메인 단위 테스트 코드 수정(#175)
hd0rable Aug 10, 2025
0a80247
Merge branch 'develop' into feat/#175-record-delete
hd0rable Aug 11, 2025
187a2e4
[teat] 테스트 코드 수정 (#160)
hd0rable Aug 11, 2025
21f7a9d
Merge remote-tracking branch 'origin/develop' into feat/#175-record-d…
hd0rable Aug 11, 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public interface SavedBookJpaRepository extends JpaRepository<SavedBookJpaEntity
"WHERE s.userJpaEntity.userId = :userId AND s.bookJpaEntity.bookId = :bookId")
boolean existsByUserIdAndBookId(Long userId, Long bookId);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
Copy link
Contributor

Choose a reason for hiding this comment

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

엇 이건 왜 추가하신거죠? (어떤 기능인지 몰라서,,ㅎ)

Copy link
Contributor

Choose a reason for hiding this comment

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

#173 (comment)

앞 pr에서 코래가 친절하게 설명해주었습니다~ ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

피드 삭제 에서 토끼가 말해준것처럼 jpa의 영속성의 1차 캐시를 비워주는 역할을합니다! 다른 트랜잭션에서 수행되는 메서드라편 필요없겠지만 UPATE,DELETE 쿼리를 날릴 시에 같은 여러 엔티티들을 한 트랜잭션에 처리하기 때문에 캐시 불일치로인한 오류를 막기 위해 추가했습니닷

Copy link
Contributor

Choose a reason for hiding this comment

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

고수 LG™

@Query("DELETE FROM SavedBookJpaEntity s WHERE s.userJpaEntity.userId = :userId AND s.bookJpaEntity.bookId = :bookId")
void deleteByUserIdAndBookId(Long userId, Long bookId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Long>, CommentQueryRepository {
Optional<CommentJpaEntity> findByCommentIdAndStatus(Long commentId, StatusType status);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId")
void softDeleteAllByPostId(@Param("postId") Long postId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface CommentLikeJpaRepository extends JpaRepository<CommentLikeJpaEn
List<CommentLikeJpaEntity> findAllByUserId(@Param("userId") Long userId);


@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId AND cl.commentJpaEntity.commentId = :commentId")
void deleteByUserIdAndCommentId(@Param("userId") Long userId, @Param("commentId") Long commentId);

Expand All @@ -24,14 +24,14 @@ public interface CommentLikeJpaRepository extends JpaRepository<CommentLikeJpaEn
"WHERE cl.userJpaEntity.userId = :userId AND cl.commentJpaEntity.commentId = :commentId")
boolean existsByUserIdAndCommentId(@Param("userId") Long userId, @Param("commentId") Long commentId);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.commentJpaEntity.commentId = :commentId")
void deleteAllByCommentId(@Param("commentId") Long commentId);

@Query("SELECT c.commentJpaEntity.commentId FROM CommentLikeJpaEntity c WHERE c.userJpaEntity.userId = :userId AND c.commentJpaEntity.commentId IN :commentIds")
Set<Long> findCommentIdsLikedByUser(@Param("commentIds") Set<Long> commentIds, @Param("userId") Long userId);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM CommentLikeJpaEntity cl
WHERE cl.commentJpaEntity.commentId IN (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public enum ErrorCode implements ResponseCode {
RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, 130000, "존재하지 않는 RECORD 입니다."),
RECORD_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 130001, "총평이 될 수 없는 RECORD 입니다."),
INVALID_RECORD_PAGE_RANGE(HttpStatus.BAD_REQUEST, 130002, "RECORD의 page 값이 유효하지 않습니다."),
RECORD_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 130003, "기록 접근 권한이 없습니다."),

/**
* 140000 : roomParticipant error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ public enum SwaggerResponseDescription {
BOOK_NOT_FOUND,
ROOM_ACCESS_FORBIDDEN
))),
RECORD_DELETE(new LinkedHashSet<>(Set.of(
ROOM_ACCESS_FORBIDDEN,
RECORD_NOT_FOUND,
RECORD_ACCESS_FORBIDDEN
))),

// Vote
VOTE_CREATE(new LinkedHashSet<>(Set.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

public interface ContentJpaRepository extends JpaRepository<ContentJpaEntity, Long>{

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM ContentJpaEntity c WHERE c.postJpaEntity.postId = :feedId")
void deleteAllByFeedId(@Param("feedId") Long feedId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public interface FeedTagJpaRepository extends JpaRepository<FeedTagJpaEntity, Lo
""")
List<FeedIdAndTagProjection> findFeedIdAndTagsByFeedIds(@Param("feedIds") List<Long> feedIds);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM FeedTagJpaEntity ft WHERE ft.feedJpaEntity.postId = :feedId")
void deleteAllByFeedId(@Param("feedId") Long feedId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import java.util.Set;

public interface SavedFeedJpaRepository extends JpaRepository<SavedFeedJpaEntity, Long> {
@Modifying

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.userJpaEntity.userId = :userId AND sf.feedJpaEntity.postId = :feedId")
void deleteByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId);

Expand All @@ -20,7 +21,7 @@ public interface SavedFeedJpaRepository extends JpaRepository<SavedFeedJpaEntity
@Query("SELECT s.feedJpaEntity.postId FROM SavedFeedJpaEntity s WHERE s.userJpaEntity.userId = :userId AND s.feedJpaEntity.postId IN :feedIds")
Set<Long> findSavedFeedIdsByUserIdAndFeedIds(@Param("userId") Long userId, @Param("feedIds") Set<Long> feedIds);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId")
int deleteAllByFeedId(@Param("feedId") Long feedId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ public void deleteFeed(Long feedId, Long userId) {

// TODO S3 이미지 삭제 이벤트 기반 처리 or 배치 삭제
// 3. 피드 삭제
// 3-1. 피드 게시물 댓글 삭제
commentCommandPort.softDeleteAllByPostId(feedId);
// 3-2. 피드 게시물 좋아요 삭제
postLikeCommandPort.deleteAllByPostId(feedId);
// 3-3. 피드 삭제 및 관련 엔티티(피드_태그, 콘텐츠, 피드 저장) 삭제
feedCommandPort.delete(feed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ Set<Long> findPostIdsLikedByUser(@Param("postIds") Set<Long> postIds,
"WHERE pl.userJpaEntity.userId = :userId AND pl.postJpaEntity.postId = :postId")
boolean existsByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM PostLikeJpaEntity pl WHERE pl.userJpaEntity.userId = :userId AND pl.postJpaEntity.postId = :postId")
void deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);

@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM PostLikeJpaEntity pl WHERE pl.postJpaEntity.postId = :postId")
void deleteAllByPostId(@Param("postId") Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@Repository
public interface RecentSearchJpaRepository extends JpaRepository<RecentSearchJpaEntity, Long>, RecentSearchQueryRepository {
@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE RecentSearchJpaEntity r SET r.modifiedAt = CURRENT_TIMESTAMP WHERE r.recentSearchId = :recentSearchId")
void updateModifiedAt(@Param("recentSearchId") Long recentSearchId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
import konkuk.thip.common.swagger.annotation.ExceptionDescription;
import konkuk.thip.record.adapter.in.web.request.RecordCreateRequest;
import konkuk.thip.record.adapter.in.web.response.RecordCreateResponse;
import konkuk.thip.record.adapter.in.web.response.RecordDeleteResponse;
import konkuk.thip.record.application.port.in.RecordCreateUseCase;
import konkuk.thip.record.application.port.in.RecordDeleteUseCase;
import konkuk.thip.record.application.port.in.dto.RecordDeleteCommand;
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.*;

Expand All @@ -23,6 +23,7 @@
@RequiredArgsConstructor
public class RecordCommandController {
private final RecordCreateUseCase recordCreateUseCase;
private final RecordDeleteUseCase recordDeleteUseCase;

@Operation(
summary = "기록 생성",
Expand All @@ -41,4 +42,17 @@ public BaseResponse<RecordCreateResponse> createRecord(
));
}

@Operation(
summary = "기록 삭제",
description = "사용자가 기록을 삭제합니다."
)
@ExceptionDescription(RECORD_DELETE)
@DeleteMapping("/rooms/{roomId}/record/{recordId}")
public BaseResponse<RecordDeleteResponse> deleteRecord(
@Parameter(description = "삭제하려는 기록 ID", example = "1") @PathVariable("recordId") final Long recordId,
@Parameter(description = "삭제하려는 기록이 작성된 모임 ID", example = "1") @PathVariable("roomId") final Long roomId,
@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(RecordDeleteResponse.of(recordDeleteUseCase.deleteRecord(new RecordDeleteCommand(roomId, recordId, userId))));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package konkuk.thip.record.adapter.in.web.response;

public record RecordDeleteResponse(Long roomId) {
public static RecordDeleteResponse of(Long roomId) {
return new RecordDeleteResponse(roomId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,10 @@ 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 @@ -15,6 +15,7 @@

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 @@ -43,10 +44,20 @@ public Long saveRecord(Record record) {

@Override
public Optional<Record> findById(Long id) {
return recordJpaRepository.findById(id)
return recordJpaRepository.findByPostIdAndStatus(id, ACTIVE)
.map(recordMapper::toDomainEntity);
}

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

recordJpaEntity.softDelete();
recordJpaRepository.save(recordJpaEntity);
}

@Override
public void update(Record record) {
RecordJpaEntity recordJpaEntity = recordJpaRepository.findById(record.getId()).orElseThrow(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package konkuk.thip.record.adapter.out.persistence.repository;

import konkuk.thip.common.entity.StatusType;
import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RecordJpaRepository extends JpaRepository<RecordJpaEntity, Long>, RecordQueryRepository {
import java.util.Optional;

public interface RecordJpaRepository extends JpaRepository<RecordJpaEntity, Long>, RecordQueryRepository {
Optional<RecordJpaEntity> findByPostIdAndStatus(Long postId, StatusType status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package konkuk.thip.record.application.port.in;

import konkuk.thip.record.application.port.in.dto.RecordDeleteCommand;

public interface RecordDeleteUseCase {
Long deleteRecord(RecordDeleteCommand command);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package konkuk.thip.record.application.port.in.dto;

public record RecordDeleteCommand(
Long roomId,

Long recordId,

Long userId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ default Record getByIdOrThrow(Long id) {
return findById(id)
.orElseThrow(() -> new EntityNotFoundException(RECORD_NOT_FOUND));
}

void delete(Record record);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package konkuk.thip.record.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.record.application.port.in.RecordDeleteUseCase;
import konkuk.thip.record.application.port.in.dto.RecordDeleteCommand;
import konkuk.thip.record.application.port.out.RecordCommandPort;
import konkuk.thip.record.domain.Record;
import konkuk.thip.room.application.service.validator.RoomParticipantValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RecordDeleteService implements RecordDeleteUseCase {

private final RecordCommandPort recordCommandPort;
private final CommentCommandPort commentCommandPort;
private final PostLikeCommandPort postLikeCommandPort;

private final RoomParticipantValidator roomParticipantValidator;

@Override
@Transactional
public Long deleteRecord(RecordDeleteCommand command) {

// 1. 방 참여자 검증
roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId());

// 2. 기록 조회 및 검증
Record record = recordCommandPort.getByIdOrThrow(command.recordId());
// 2-1. 기록 삭제 권한 검증
record.validateDeletable(command.userId(),command.roomId());

// 3. 기록 삭제
// 3-1. 기록 게시물 댓글 삭제
commentCommandPort.softDeleteAllByPostId(command.recordId());
// 3-2. 피드 게시물 좋아요 삭제
postLikeCommandPort.deleteAllByPostId(command.recordId());
// 3-3. 기록 삭제
recordCommandPort.delete(record);

return command.roomId();
}
}
20 changes: 19 additions & 1 deletion src/main/java/konkuk/thip/record/domain/Record.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,22 @@ private void checkCommentCountNotUnderflow() {
throw new InvalidStateException(COMMENT_COUNT_UNDERFLOW);
}
}
}

private void validateCreator(Long userId) {
if (!this.creatorId.equals(userId)) {
throw new InvalidStateException(RECORD_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(RECORD_ACCESS_FORBIDDEN, new IllegalArgumentException("기록이 해당 방에 속하지 않습니다."));
}
}

}
Loading