Skip to content

Commit 9a8d715

Browse files
authored
Merge pull request #155 from prgrms-aibe-devcourse/feat/151-til-delete-api
[Feat] TIL 삭제 API 구현
2 parents 70ed14d + 07b5e45 commit 9a8d715

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.Rootin.domain.ai.repository;
2+
3+
import com.Rootin.domain.ai.entity.AiResultTil;
4+
import com.Rootin.domain.ai.entity.AiResultTilId;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
9+
import org.springframework.transaction.annotation.Transactional;
10+
11+
public interface AiResultTilRepository extends JpaRepository<AiResultTil, AiResultTilId> {
12+
13+
// 파생 delete 쿼리(SELECT → N번 DELETE) 대신 단건 벌크 삭제로 성능 개선
14+
// orphanRemoval 우회에 따른 1차 캐시 불일치 위험 — TilService.delete()는 AiResult를
15+
// 로드하지 않으므로 현재는 안전하나, 같은 트랜잭션 내 AiResult 로드 추가 시 주의 필요
16+
@Transactional
17+
@Modifying
18+
@Query("DELETE FROM AiResultTil a WHERE a.til.id = :tilId")
19+
void deleteByTilId(@Param("tilId") Long tilId);
20+
}

src/main/java/com/Rootin/domain/garden/repository/WateringLogRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import com.Rootin.domain.garden.entity.WateringLog;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
56
import org.springframework.data.jpa.repository.Query;
67
import org.springframework.data.repository.query.Param;
78
import org.springframework.stereotype.Repository;
9+
import org.springframework.transaction.annotation.Transactional;
810

911
import java.time.LocalDateTime;
1012
import java.util.List;
@@ -31,6 +33,12 @@ public interface WateringLogRepository extends JpaRepository<WateringLog, Long>
3133
*/
3234
boolean existsByPostId(Long postId);
3335

36+
// TIL 삭제 시 연관 물주기 로그 일괄 제거
37+
@Transactional
38+
@Modifying
39+
@Query("DELETE FROM WateringLog w WHERE w.postId = :postId")
40+
void deleteByPostId(@Param("postId") Long postId);
41+
3442
/**
3543
* 특정 사용자가 소유한 특정 화분의 물주기 로그 중 가장 최근에 물을 준 로그 1건을 안전하게 조회합니다.
3644
* 대시보드 화면에 해당 화분의 "마지막 물 준 시간"을 노출할 때 사용하며,

src/main/java/com/Rootin/domain/til/service/TilService.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.Rootin.domain.til.service;
22

3+
import com.Rootin.domain.ai.repository.AiResultTilRepository;
34
import com.Rootin.domain.garden.entity.Pot;
45
import com.Rootin.domain.garden.repository.PotRepository;
6+
import com.Rootin.domain.garden.repository.WateringLogRepository;
57
import com.Rootin.domain.garden.service.ExperienceService;
68
import com.Rootin.domain.til.dto.request.DraftSaveRequest;
79
import com.Rootin.domain.til.dto.request.TilCreateRequest;
@@ -32,6 +34,8 @@ public class TilService {
3234
private final UserRepository userRepository;
3335
private final PotRepository potRepository;
3436
private final ExperienceService experienceService;
37+
private final AiResultTilRepository aiResultTilRepository;
38+
private final WateringLogRepository wateringLogRepository;
3539

3640
@Transactional
3741
public TilResponse create(Long userId, TilCreateRequest request) {
@@ -101,6 +105,11 @@ public TilResponse update(Long tilId, Long userId, TilUpdateRequest request) {
101105
public void delete(Long tilId, Long userId) {
102106
Til til = getTilOrThrow(tilId);
103107
validateOwner(til, userId);
108+
// FK 참조 테이블을 먼저 정리한 뒤 TIL 삭제
109+
// - ai_result_til: 중간 테이블만 제거, ai_results 레코드는 유지
110+
// - watering_log: TIL에 귀속된 물주기 이력 제거 (경험치 중복 방지 unique 제약 해소)
111+
aiResultTilRepository.deleteByTilId(tilId);
112+
wateringLogRepository.deleteByPostId(tilId);
104113
tilRepository.delete(til);
105114
}
106115

@@ -138,6 +147,8 @@ public void deleteDraft(Long userId, Long potId) {
138147
Til til = tilRepository.findFirstByUserIdAndPotIdAndStatus(userId, potId, PostStatus.DRAFT)
139148
.orElseThrow(() -> CustomException.notFound("임시저장된 TIL이 없습니다."));
140149
validateOwner(til, userId);
150+
// DRAFT 상태 TIL은 watering_log·ai_result_til 레코드가 생성되지 않으므로 별도 FK 정리 불필요.
151+
// 향후 AI 분석 또는 물주기가 DRAFT 단계까지 확장될 경우 delete()와 동일한 패턴 적용 필요.
141152
tilRepository.delete(til);
142153
}
143154

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.Rootin.domain.til.service;
2+
3+
import com.Rootin.domain.ai.repository.AiResultTilRepository;
4+
import com.Rootin.domain.garden.entity.Pot;
5+
import com.Rootin.domain.garden.repository.PotRepository;
6+
import com.Rootin.domain.garden.repository.WateringLogRepository;
7+
import com.Rootin.domain.garden.service.ExperienceService;
8+
import com.Rootin.domain.til.entity.Til;
9+
import com.Rootin.domain.til.repository.TagRepository;
10+
import com.Rootin.domain.til.repository.TilRepository;
11+
import com.Rootin.domain.user.entity.User;
12+
import com.Rootin.domain.user.repository.UserRepository;
13+
import com.Rootin.global.exception.CustomException;
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.DisplayName;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.ExtendWith;
18+
import org.mockito.InjectMocks;
19+
import org.mockito.Mock;
20+
import org.mockito.junit.jupiter.MockitoExtension;
21+
import org.springframework.http.HttpStatus;
22+
import org.springframework.test.util.ReflectionTestUtils;
23+
24+
import java.util.Optional;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
28+
import static org.mockito.BDDMockito.given;
29+
import static org.mockito.Mockito.*;
30+
31+
@ExtendWith(MockitoExtension.class)
32+
class TilServiceTest {
33+
34+
@InjectMocks
35+
private TilService tilService;
36+
37+
@Mock
38+
private TilRepository tilRepository;
39+
40+
@Mock
41+
private TagRepository tagRepository;
42+
43+
@Mock
44+
private UserRepository userRepository;
45+
46+
@Mock
47+
private PotRepository potRepository;
48+
49+
@Mock
50+
private ExperienceService experienceService;
51+
52+
@Mock
53+
private AiResultTilRepository aiResultTilRepository;
54+
55+
@Mock
56+
private WateringLogRepository wateringLogRepository;
57+
58+
private User owner;
59+
private Til til;
60+
61+
@BeforeEach
62+
void setUp() {
63+
owner = new User();
64+
ReflectionTestUtils.setField(owner, "id", 1L);
65+
66+
Pot pot = Pot.builder()
67+
.userId(1L)
68+
.title("테스트 화분")
69+
.level(1)
70+
.totalExp(0)
71+
.build();
72+
ReflectionTestUtils.setField(pot, "id", 10L);
73+
74+
til = new Til();
75+
ReflectionTestUtils.setField(til, "id", 100L);
76+
ReflectionTestUtils.setField(til, "user", owner);
77+
ReflectionTestUtils.setField(til, "pot", pot);
78+
}
79+
80+
// ─── delete() ────────────────────────────────────────────────────
81+
82+
@Test
83+
@DisplayName("정상 삭제 — aiResultTil·wateringLog 정리 후 til 삭제, 삭제 순서 보장")
84+
void delete_success_removesRelatedRecordsInOrder() {
85+
given(tilRepository.findById(100L)).willReturn(Optional.of(til));
86+
87+
tilService.delete(100L, 1L);
88+
89+
// aiResultTil → wateringLog → til 순서 검증
90+
var inOrder = inOrder(aiResultTilRepository, wateringLogRepository, tilRepository);
91+
inOrder.verify(aiResultTilRepository).deleteByTilId(100L);
92+
inOrder.verify(wateringLogRepository).deleteByPostId(100L);
93+
inOrder.verify(tilRepository).delete(til);
94+
}
95+
96+
@Test
97+
@DisplayName("타인 TIL 삭제 시도 → 403")
98+
void delete_forbidden_when_not_owner() {
99+
given(tilRepository.findById(100L)).willReturn(Optional.of(til));
100+
101+
assertThatThrownBy(() -> tilService.delete(100L, 2L))
102+
.isInstanceOf(CustomException.class)
103+
.satisfies(e -> assertThat(((CustomException) e).getStatus()).isEqualTo(HttpStatus.FORBIDDEN));
104+
105+
verifyNoInteractions(aiResultTilRepository, wateringLogRepository);
106+
verify(tilRepository, never()).delete(any());
107+
}
108+
109+
@Test
110+
@DisplayName("존재하지 않는 tilId 삭제 → 404")
111+
void delete_notFound_when_til_not_exists() {
112+
given(tilRepository.findById(999L)).willReturn(Optional.empty());
113+
114+
assertThatThrownBy(() -> tilService.delete(999L, 1L))
115+
.isInstanceOf(CustomException.class)
116+
.satisfies(e -> assertThat(((CustomException) e).getStatus()).isEqualTo(HttpStatus.NOT_FOUND));
117+
118+
verifyNoInteractions(aiResultTilRepository, wateringLogRepository);
119+
verify(tilRepository, never()).delete(any());
120+
}
121+
122+
}

0 commit comments

Comments
 (0)