diff --git a/clokey-api/src/main/java/org/clokey/domain/history/dto/request/HistoryCreateRequest.java b/clokey-api/src/main/java/org/clokey/domain/history/dto/request/HistoryCreateRequest.java index 0056d2d7..03eb649d 100644 --- a/clokey-api/src/main/java/org/clokey/domain/history/dto/request/HistoryCreateRequest.java +++ b/clokey-api/src/main/java/org/clokey/domain/history/dto/request/HistoryCreateRequest.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.*; +import java.time.LocalDate; import java.util.List; @Schema(description = "기록 생성 요청") @@ -13,6 +14,9 @@ public record HistoryCreateRequest( description = "기록의 내용", example = "안녕 오늘 오지게 덥다 ㄷㄷ;; 근데 한달 뒤면 가을임 벌써 가을 기대 만발ㅋ") String content, + @NotNull(message = "기록 작성 날짜는 비워둘 수 없습니다.") + @Schema(description = "기록 작성 날짜", example = "2026-04-12") + LocalDate historyDate, @NotNull(message = "상황 ID는 비워둘 수 없습니다.") @Schema(description = "상황 ID", example = "7") Long situationId, @NotNull(message = "스타일 목록은 비워둘 수 없습니다.") diff --git a/clokey-api/src/main/java/org/clokey/domain/history/exception/HistoryErrorCode.java b/clokey-api/src/main/java/org/clokey/domain/history/exception/HistoryErrorCode.java index 673a3e2e..13b74fe9 100644 --- a/clokey-api/src/main/java/org/clokey/domain/history/exception/HistoryErrorCode.java +++ b/clokey-api/src/main/java/org/clokey/domain/history/exception/HistoryErrorCode.java @@ -9,6 +9,7 @@ @AllArgsConstructor public enum HistoryErrorCode implements BaseErrorCode { BANNED_HISTORY(400, "HISTORY_4001", "신고당한 기록은 조회할 수 없습니다."), + HISTORY_ALREADY_EXISTS(400, "HISTORY_4002", "해당 날짜의 기록이 이미 존재합니다."), LIMITED_AUTHORITY(403, "HISTORY_4031", "기록에 대한 접근 권한이 없습니다."), BLOCKED_AUTHORITY(403, "HISTORY_4032", "기록 작성자를 차단했거나 차단 당했습니다"), diff --git a/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java index eedf9442..52dd42c5 100644 --- a/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java @@ -75,6 +75,9 @@ public class HistoryServiceImpl implements HistoryService { @Transactional public HistoryCreateResponse createHistory(HistoryCreateRequest request) { final Member currentMember = memberUtil.getCurrentMember(); + final LocalDate historyDate = request.historyDate(); + + validateDuplicateHistoryDate(currentMember.getId(), historyDate); final Situation situation = getSituationById(request.situationId()); @@ -94,7 +97,7 @@ public HistoryCreateResponse createHistory(HistoryCreateRequest request) { final String content = Optional.ofNullable(request.content()).map(String::trim).orElse(null); final History history = - History.createHistory(LocalDate.now(KST), content, currentMember, situation); + History.createHistory(historyDate, content, currentMember, situation); historyRepository.save(history); List images = new ArrayList<>(); @@ -516,6 +519,12 @@ private Map validateAndLoadStyles(List styleIds) { return styleMap; } + private void validateDuplicateHistoryDate(Long memberId, LocalDate historyDate) { + if (historyRepository.existsByMemberIdAndHistoryDate(memberId, historyDate)) { + throw new BaseCustomException(HistoryErrorCode.HISTORY_ALREADY_EXISTS); + } + } + private Map validateAndLoadClothes(Member member, List clothIds) { if (clothIds == null || clothIds.isEmpty()) { return Map.of(); diff --git a/clokey-api/src/main/java/org/clokey/domain/like/repository/MemberLikeRepository.java b/clokey-api/src/main/java/org/clokey/domain/like/repository/MemberLikeRepository.java index f43add4d..2d736825 100644 --- a/clokey-api/src/main/java/org/clokey/domain/like/repository/MemberLikeRepository.java +++ b/clokey-api/src/main/java/org/clokey/domain/like/repository/MemberLikeRepository.java @@ -39,6 +39,15 @@ List findLikedHistoriesByMemberId( @Query("delete from MemberLike ml where ml.history.id = :historyId") void deleteAllByHistoryId(Long historyId); + @Modifying + @Query( + """ + delete from MemberLike ml + where ml.member.id = :memberId + and ml.history.member.id = :historyOwnerId + """) + void deleteAllByMemberIdAndHistoryOwnerId(Long memberId, Long historyOwnerId); + @Query( """ select ml.history.id diff --git a/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java index 27825525..8b252815 100644 --- a/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java @@ -4,6 +4,7 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.clokey.domain.history.repository.HistoryRepository; +import org.clokey.domain.like.repository.MemberLikeRepository; import org.clokey.domain.member.dto.request.DuplicatedNicknameCheckRequest; import org.clokey.domain.member.dto.request.ProfileUpdateRequest; import org.clokey.domain.member.dto.response.*; @@ -40,6 +41,7 @@ public class MemberServiceImpl implements MemberService { private final FollowRepository followRepository; private final PendingFollowRepository pendingFollowRepository; private final BlockRepository blockRepository; + private final MemberLikeRepository memberLikeRepository; private final ApplicationEventPublisher eventPublisher; @@ -95,6 +97,8 @@ public void toggleBlockStatus(Long memberId) { if (existingBlock.isPresent()) { blockRepository.delete(existingBlock.get()); } else { + memberLikeRepository.deleteAllByMemberIdAndHistoryOwnerId( + blocker.getId(), blocked.getId()); Block block = Block.createBlock(blocker, blocked); blockRepository.save(block); } diff --git a/clokey-api/src/test/java/org/clokey/domain/history/controller/HistoryControllerTest.java b/clokey-api/src/test/java/org/clokey/domain/history/controller/HistoryControllerTest.java index 0b707140..d9f3999f 100644 --- a/clokey-api/src/test/java/org/clokey/domain/history/controller/HistoryControllerTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/history/controller/HistoryControllerTest.java @@ -7,6 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDate; import java.util.List; import org.clokey.domain.history.dto.request.HistoryCreateRequest; import org.clokey.domain.history.dto.request.HistoryImagesUploadRequest; @@ -88,6 +89,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -127,6 +129,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( longContent, + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -150,11 +153,41 @@ class 기록_생성_요청_시 { .andExpect(jsonPath("$.result.content").value("기록의 내용은 최대 120자까지 가능합니다.")); } + @Test + void 기록작성날짜가_null이면_예외가_발생한다() throws Exception { + HistoryCreateRequest request = + new HistoryCreateRequest( + "testContent", + null, + 1L, + List.of(1L, 2L), + List.of("testHashtag1", "testHashtag2"), + List.of( + new HistoryCreateRequest.Payload( + "testUrl", + List.of( + new HistoryCreateRequest.ClothTag( + 1L, 0.42, 0.73))))); + + ResultActions perform = + mockMvc.perform( + post("/histories") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.isSuccess").value(false)) + .andExpect(jsonPath("$.code").value("COMMON400")) + .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")) + .andExpect(jsonPath("$.result.historyDate").value("기록 작성 날짜는 비워둘 수 없습니다.")); + } + @Test void 상황ID가_null이면_예외가_발생한다() throws Exception { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), null, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -183,6 +216,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, null, List.of("testHashtag1", "testHashtag2"), @@ -211,6 +245,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(), List.of("testHashtag1", "testHashtag2"), @@ -239,6 +274,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L, 3L, 4L), List.of("testHashtag1", "testHashtag2"), @@ -267,6 +303,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -290,6 +327,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -313,6 +351,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -350,6 +389,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -373,6 +413,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -401,6 +442,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -430,6 +472,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -458,6 +501,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -487,6 +531,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -515,6 +560,7 @@ class 기록_생성_요청_시 { HistoryCreateRequest request = new HistoryCreateRequest( "testContent", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of(" "), diff --git a/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java b/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java index 99c46e3b..6943e5e1 100644 --- a/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/history/service/HistoryServiceImplTest.java @@ -7,6 +7,7 @@ import static org.mockito.BDDMockito.given; import java.time.LocalDate; +import java.time.ZoneId; import java.util.List; import org.clokey.IntegrationTest; import org.clokey.TransactionUtil; @@ -59,6 +60,8 @@ @RecordApplicationEvents class HistoryServiceImplTest extends IntegrationTest { + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + @Autowired private TransactionUtil transactionUtil; @Autowired private MemberRepository memberRepository; @@ -192,6 +195,7 @@ void setUp() { HistoryCreateRequest request = new HistoryCreateRequest( "testContent 1 ", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -217,8 +221,9 @@ void setUp() { .extracting( h -> h.getMember().getId(), h -> h.getSituation().getId(), + History::getHistoryDate, History::getContent) - .containsExactly(1L, 1L, "testContent 1"); + .containsExactly(1L, 1L, LocalDate.of(2026, 4, 12), "testContent 1"); List images = historyImageRepository.findByHistoryId(history.getId()); assertThat(images).hasSize(2); @@ -253,12 +258,39 @@ void setUp() { assertThat(historyHashtags).hasSize(2); } + @Test + void 같은_날짜의_기록이_이미_존재하면_예외가_발생한다() { + // given + Member currentMember = + transactionUtil.getResult(() -> memberRepository.findById(1L).orElseThrow()); + Situation situation = + transactionUtil.getResult(() -> situationRepository.findById(1L).orElseThrow()); + historyRepository.save( + History.createHistory( + LocalDate.of(2026, 4, 12), "existing", currentMember, situation)); + + HistoryCreateRequest request = + new HistoryCreateRequest( + "testContent 1 ", + LocalDate.of(2026, 4, 12), + 1L, + List.of(1L, 2L), + List.of("testHashtag1", "testHashtag2"), + List.of(new Payload("testUrl1", null))); + + // when & then + assertThatThrownBy(() -> historyService.createHistory(request)) + .isInstanceOf(BaseCustomException.class) + .hasMessage(HistoryErrorCode.HISTORY_ALREADY_EXISTS.getMessage()); + } + @Test void 존재하지_않는_상황_ID를_포함하는_경우_예외가_발생한다() { // given HistoryCreateRequest request = new HistoryCreateRequest( "hi", + LocalDate.of(2026, 4, 12), 999L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -281,6 +313,7 @@ void setUp() { HistoryCreateRequest request = new HistoryCreateRequest( "testContent 1 ", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 10L), List.of("testHashtag1", "testHashtag2"), @@ -302,6 +335,7 @@ void setUp() { HistoryCreateRequest request = new HistoryCreateRequest( "testContent 1 ", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -318,6 +352,7 @@ void setUp() { HistoryCreateRequest request = new HistoryCreateRequest( "testContent 1 ", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -334,6 +369,7 @@ void setUp() { HistoryCreateRequest request = new HistoryCreateRequest( "testContent 1 ", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2"), @@ -354,6 +390,7 @@ void setUp() { HistoryCreateRequest request = new HistoryCreateRequest( "testContent 1 ", + LocalDate.of(2026, 4, 12), 1L, List.of(1L, 2L), List.of("testHashtag1", "testHashtag2", "testHashtag3"), @@ -1300,10 +1337,10 @@ void setUp() { situationRepository.save(situation); History todayHistory = - History.createHistory(LocalDate.now(), "testContent", member1, situation); + History.createHistory(LocalDate.now(KST), "testContent", member1, situation); History otherHistory = History.createHistory( - LocalDate.now().minusDays(1), "oldContent", member2, situation); + LocalDate.now(KST).minusDays(1), "oldContent", member2, situation); historyRepository.saveAll(List.of(todayHistory, otherHistory)); } diff --git a/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java b/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java index 5fdedf9e..1fb7fce5 100644 --- a/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java @@ -3,10 +3,14 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.given; +import java.time.LocalDate; import java.util.List; import java.util.Optional; import org.clokey.IntegrationTest; import org.clokey.TransactionUtil; +import org.clokey.domain.history.repository.HistoryRepository; +import org.clokey.domain.history.repository.SituationRepository; +import org.clokey.domain.like.repository.MemberLikeRepository; import org.clokey.domain.member.dto.request.DuplicatedNicknameCheckRequest; import org.clokey.domain.member.dto.request.ProfileUpdateRequest; import org.clokey.domain.member.dto.response.BlockedMemberResponse; @@ -22,6 +26,9 @@ import org.clokey.exception.BaseCustomException; import org.clokey.global.paging.SortDirection; import org.clokey.global.util.MemberUtil; +import org.clokey.history.entity.History; +import org.clokey.history.entity.Situation; +import org.clokey.like.entity.MemberLike; import org.clokey.member.entity.Block; import org.clokey.member.entity.Follow; import org.clokey.member.entity.Member; @@ -51,6 +58,9 @@ class MemberServiceTest extends IntegrationTest { @Autowired private FollowRepository followRepository; @Autowired private PendingFollowRepository pendingFollowRepository; @Autowired private BlockRepository blockRepository; + @Autowired private HistoryRepository historyRepository; + @Autowired private SituationRepository situationRepository; + @Autowired private MemberLikeRepository memberLikeRepository; @Autowired private TransactionUtil transactionUtil; @MockitoBean private MemberUtil memberUtil; @@ -396,6 +406,28 @@ void setUp() { .containsExactly(1L, 2L); } + @Test + void 차단하면_내가_눌렀던_상대_기록_좋아요를_삭제한다() { + // given + Member blocker = memberRepository.findById(1L).orElseThrow(); + Member blocked = memberRepository.findById(2L).orElseThrow(); + Situation situation = situationRepository.save(Situation.createSituation("daily")); + History blockedHistory = + historyRepository.save( + History.createHistory( + LocalDate.of(2026, 4, 12), "content", blocked, situation)); + memberLikeRepository.save(MemberLike.createMemberLike(blocker, blockedHistory)); + + // when + memberService.toggleBlockStatus(2L); + + // then + assertThat( + memberLikeRepository.findByMemberIdAndHistoryId( + blocker.getId(), blockedHistory.getId())) + .isEmpty(); + } + @Test void 차단_상태라면_차단을_해제한다() { // given