Skip to content

Commit 2733a0a

Browse files
authored
[Feat/#123] 기록 삭제 api 및 presigned url api 추가 (#228)
* feat: delete history api 구현 * feat: presignedurl 발급 api 추가 * refactor: test 수정 * refactor: summary 및 description 수정
1 parent 1c68c4e commit 2733a0a

11 files changed

Lines changed: 378 additions & 14 deletions

File tree

clokey-api/src/main/java/org/clokey/domain/comment/repository/CommentRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ public interface CommentRepository extends JpaRepository<Comment, Long>, Comment
1212
void deleteReplies(Long parentId);
1313

1414
long countByHistoryIdAndBannedFalse(Long historyId);
15+
16+
@Modifying
17+
@Query("delete from Comment c where c.history.id = :historyId")
18+
void deleteAllByHistoryId(Long historyId);
1519
}

clokey-api/src/main/java/org/clokey/domain/history/controller/HistoryController.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import lombok.RequiredArgsConstructor;
77
import org.clokey.code.GlobalBaseSuccessCode;
88
import org.clokey.domain.history.dto.request.HistoryCreateRequest;
9+
import org.clokey.domain.history.dto.request.HistoryImagesUploadRequest;
910
import org.clokey.domain.history.dto.request.HistoryUpdateRequest;
1011
import org.clokey.domain.history.dto.response.DailyHistoryResponse;
1112
import org.clokey.domain.history.dto.response.HistoryClothTagListResponse;
1213
import org.clokey.domain.history.dto.response.HistoryCreateResponse;
14+
import org.clokey.domain.history.dto.response.HistoryImagesPresignedUrlResponse;
1315
import org.clokey.domain.history.dto.response.HistoryOwnershipCheckResponse;
1416
import org.clokey.domain.history.dto.response.MonthlyHistoryResponse;
1517
import org.clokey.domain.history.dto.response.SituationListResponse;
@@ -28,6 +30,18 @@ public class HistoryController {
2830

2931
private final HistoryService historyService;
3032

33+
@PostMapping("/images")
34+
@Operation(
35+
operationId = "History_getHistoryUploadPresignedUrl",
36+
summary = "기록 이미지 업로드용 presignedUrl 발급",
37+
description = "기록 이미지 업로드용 presignedUrl을 발급합니다.")
38+
public BaseResponse<HistoryImagesPresignedUrlResponse> getHistoryUploadPresignedUrl(
39+
@Valid @RequestBody HistoryImagesUploadRequest request) {
40+
HistoryImagesPresignedUrlResponse response =
41+
historyService.getHistoryUploadPresignedUrls(request);
42+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, response);
43+
}
44+
3145
@PostMapping
3246
@Operation(
3347
operationId = "History_createHistory",
@@ -112,4 +126,14 @@ public BaseResponse<HistoryOwnershipCheckResponse> checkHistoryOwnership(
112126
HistoryOwnershipCheckResponse response = historyService.checkHistoryOwnership(historyId);
113127
return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response);
114128
}
129+
130+
@DeleteMapping("/{historyId}")
131+
@Operation(
132+
operationId = "History_deleteHistory",
133+
summary = "기록 삭제",
134+
description = "기록 ID를 통해 기록을 삭제합니다.")
135+
public BaseResponse<Void> deleteHistory(@PathVariable Long historyId) {
136+
historyService.deleteHistory(historyId);
137+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.NO_CONTENT, null);
138+
}
115139
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.clokey.domain.history.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.Valid;
5+
import jakarta.validation.constraints.NotBlank;
6+
import jakarta.validation.constraints.NotEmpty;
7+
import java.util.List;
8+
import org.clokey.enums.FileExtension;
9+
import org.clokey.global.annotation.Enum;
10+
11+
public record HistoryImagesUploadRequest(
12+
@NotEmpty(message = "업로드할 파일들의 정보가 비어있을 수 없습니다.") @Valid @Schema(description = "업로드 요청 리스트")
13+
List<Payload> payloads) {
14+
@Schema(name = "HistoryImagesUploadRequestPayload")
15+
public record Payload(
16+
@Enum(message = "파일 확장자는 비어있을 수 없으며 PNG, JPG, JPEG, WEBP, HEIC, HEIF만 가능합니다.")
17+
@Schema(description = "파일 확장자", defaultValue = "JPEG")
18+
FileExtension fileExtension,
19+
@NotBlank(message = "MD5 해시값이 비어있을 수 없습니다.")
20+
@Schema(description = "S3 업로드시 파일의 무결성을 확인하기 위한 md5 해시")
21+
String md5Hashes) {}
22+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.clokey.domain.history.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import java.util.List;
5+
6+
public record HistoryImagesPresignedUrlResponse(
7+
@Schema(description = "생성된 presigned url 리스트") List<String> urls) {
8+
public static HistoryImagesPresignedUrlResponse of(List<String> urls) {
9+
return new HistoryImagesPresignedUrlResponse(urls);
10+
}
11+
}

clokey-api/src/main/java/org/clokey/domain/history/repository/HistoryClothTagRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.data.jpa.repository.JpaRepository;
66
import org.springframework.data.jpa.repository.Modifying;
77
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
89

910
public interface HistoryClothTagRepository
1011
extends JpaRepository<HistoryClothTag, Long>, HistoryClothTagRepositoryCustom {
@@ -16,6 +17,10 @@ public interface HistoryClothTagRepository
1617
@Query("DELETE FROM HistoryClothTag hct WHERE hct.cloth.id = :clothId")
1718
void deleteAllByClothId(Long clothId);
1819

20+
@Modifying
21+
@Query("DELETE FROM HistoryClothTag hct WHERE hct.historyImage.id in :historyImageIds")
22+
void deleteAllByHistoryImageIdIn(@Param("historyImageIds") List<Long> historyImageIds);
23+
1924
@Query(
2025
"select hct from HistoryClothTag hct "
2126
+ "join fetch hct.cloth "

clokey-api/src/main/java/org/clokey/domain/history/service/HistoryService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.clokey.domain.history.service;
22

33
import org.clokey.domain.history.dto.request.HistoryCreateRequest;
4+
import org.clokey.domain.history.dto.request.HistoryImagesUploadRequest;
45
import org.clokey.domain.history.dto.request.HistoryUpdateRequest;
56
import org.clokey.domain.history.dto.response.*;
67

@@ -20,4 +21,9 @@ public interface HistoryService {
2021
MonthlyHistoryResponse getMonthlyHistory(Long memberId, int year, int month);
2122

2223
HistoryOwnershipCheckResponse checkHistoryOwnership(Long historyId);
24+
25+
void deleteHistory(Long historyId);
26+
27+
HistoryImagesPresignedUrlResponse getHistoryUploadPresignedUrls(
28+
HistoryImagesUploadRequest request);
2329
}

clokey-api/src/main/java/org/clokey/domain/history/service/HistoryServiceImpl.java

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
import org.clokey.domain.cloth.repository.ClothRepository;
1111
import org.clokey.domain.comment.repository.CommentRepository;
1212
import org.clokey.domain.history.dto.request.HistoryCreateRequest;
13+
import org.clokey.domain.history.dto.request.HistoryImagesUploadRequest;
1314
import org.clokey.domain.history.dto.request.HistoryUpdateRequest;
1415
import org.clokey.domain.history.dto.response.DailyHistoryResponse;
1516
import org.clokey.domain.history.dto.response.HistoryClothTagListResponse;
1617
import org.clokey.domain.history.dto.response.HistoryCreateResponse;
18+
import org.clokey.domain.history.dto.response.HistoryImagesPresignedUrlResponse;
1719
import org.clokey.domain.history.dto.response.HistoryOwnershipCheckResponse;
1820
import org.clokey.domain.history.dto.response.MonthlyHistoryResponse;
1921
import org.clokey.domain.history.dto.response.SituationListResponse;
@@ -22,15 +24,20 @@
2224
import org.clokey.domain.history.exception.SituationErrorCode;
2325
import org.clokey.domain.history.exception.StyleErrorCode;
2426
import org.clokey.domain.history.repository.*;
27+
import org.clokey.domain.image.event.ImagesDeleteEvent;
2528
import org.clokey.domain.like.repository.MemberLikeRepository;
2629
import org.clokey.domain.member.repository.BlockRepository;
2730
import org.clokey.domain.member.repository.MemberRepository;
2831
import org.clokey.domain.report.repository.ReportRepository;
32+
import org.clokey.enums.ImageType;
2933
import org.clokey.exception.BaseCustomException;
3034
import org.clokey.global.util.MemberUtil;
3135
import org.clokey.history.entity.*;
3236
import org.clokey.member.entity.Member;
3337
import org.clokey.member.enums.Visibility;
38+
import org.clokey.report.enums.TargetType;
39+
import org.clokey.util.S3Util;
40+
import org.springframework.context.ApplicationEventPublisher;
3441
import org.springframework.stereotype.Service;
3542
import org.springframework.transaction.annotation.Transactional;
3643

@@ -55,6 +62,8 @@ public class HistoryServiceImpl implements HistoryService {
5562
private final BlockRepository blockRepository;
5663
private final ReportRepository reportRepository;
5764
private final MemberRepository memberRepository;
65+
private final ApplicationEventPublisher eventPublisher;
66+
private final S3Util s3Util;
5867

5968
@Override
6069
@Transactional
@@ -311,6 +320,55 @@ public HistoryOwnershipCheckResponse checkHistoryOwnership(Long historyId) {
311320
return HistoryOwnershipCheckResponse.of(isOwner);
312321
}
313322

323+
@Override
324+
@Transactional
325+
public void deleteHistory(Long historyId) {
326+
final Member currentMember = memberUtil.getCurrentMember();
327+
final History history = getHistoryById(historyId);
328+
329+
validateHistoryOwner(history, currentMember.getId());
330+
331+
List<HistoryImage> images = historyImageRepository.findByHistoryId(historyId);
332+
List<Long> imageIds = images.stream().map(HistoryImage::getId).toList();
333+
334+
if (!imageIds.isEmpty()) {
335+
deleteClothTagsByHistoryImageIds(imageIds);
336+
historyImageRepository.deleteAll(images);
337+
}
338+
339+
clearStylesAndHashtags(historyId);
340+
memberLikeRepository.deleteAllByHistoryId(historyId);
341+
commentRepository.deleteAllByHistoryId(historyId);
342+
reportRepository.deleteAllByTargetTypeAndTargetId(TargetType.HISTORY, historyId);
343+
344+
historyRepository.delete(history);
345+
346+
List<String> imageUrls =
347+
images.stream().map(HistoryImage::getImageUrl).filter(Objects::nonNull).toList();
348+
if (!imageUrls.isEmpty()) {
349+
eventPublisher.publishEvent(ImagesDeleteEvent.of(imageUrls));
350+
}
351+
}
352+
353+
@Override
354+
public HistoryImagesPresignedUrlResponse getHistoryUploadPresignedUrls(
355+
HistoryImagesUploadRequest request) {
356+
final Member currentMember = memberUtil.getCurrentMember();
357+
358+
List<String> presignedUrls =
359+
request.payloads().stream()
360+
.map(
361+
payload ->
362+
s3Util.createPresignedUrl(
363+
ImageType.HISTORY_IMAGE,
364+
currentMember.getId(),
365+
payload.fileExtension(),
366+
payload.md5Hashes()))
367+
.toList();
368+
369+
return HistoryImagesPresignedUrlResponse.of(presignedUrls);
370+
}
371+
314372
private HistoryImage getHistoryImageById(Long historyImageId) {
315373
return historyImageRepository
316374
.findById(historyImageId)
@@ -484,31 +542,20 @@ private void validateHistoryOwner(History history, Long memberId) {
484542
private void clearStylesAndHashtags(Long historyId) {
485543
List<HistoryStyle> styles = historyStyleRepository.findByHistoryId(historyId);
486544
if (!styles.isEmpty()) {
487-
historyStyleRepository.deleteAllInBatch(styles);
545+
historyStyleRepository.deleteAll(styles);
488546
}
489547

490548
List<HistoryHashtag> hashtags = historyHashtagRepository.findByHistoryId(historyId);
491549
if (!hashtags.isEmpty()) {
492-
historyHashtagRepository.deleteAllInBatch(hashtags);
550+
historyHashtagRepository.deleteAll(hashtags);
493551
}
494552
}
495553

496554
private void deleteClothTagsByHistoryImageIds(List<Long> historyImageIds) {
497555
if (historyImageIds == null || historyImageIds.isEmpty()) {
498556
return;
499557
}
500-
List<HistoryClothTag> historyClothTags =
501-
historyImageIds.stream()
502-
.flatMap(
503-
imageId ->
504-
historyClothTagRepository
505-
.findByHistoryImageId(imageId)
506-
.stream())
507-
.toList();
508-
509-
if (!historyClothTags.isEmpty()) {
510-
historyClothTagRepository.deleteAllInBatch(historyClothTags);
511-
}
558+
historyClothTagRepository.deleteAllByHistoryImageIdIn(historyImageIds);
512559
}
513560

514561
private Member getMemberById(Long memberId) {

clokey-api/src/main/java/org/clokey/domain/like/repository/MemberLikeRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.clokey.like.entity.MemberLike;
66
import org.springframework.data.domain.Pageable;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
89
import org.springframework.data.jpa.repository.Query;
910

1011
public interface MemberLikeRepository extends JpaRepository<MemberLike, Long> {
@@ -33,4 +34,8 @@ List<MemberLike> findLikedHistoriesByMemberId(
3334
List<MemberLike> findLikeMembersByHistoryId(Long historyId, Long lastLikeId, Pageable pageable);
3435

3536
Optional<MemberLike> findByMemberIdAndHistoryId(Long memberId, Long historyId);
37+
38+
@Modifying
39+
@Query("delete from MemberLike ml where ml.history.id = :historyId")
40+
void deleteAllByHistoryId(Long historyId);
3641
}

clokey-api/src/main/java/org/clokey/domain/report/repository/ReportRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55
import org.clokey.report.enums.ReportStatus;
66
import org.clokey.report.enums.TargetType;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
9+
import org.springframework.data.jpa.repository.Query;
810

911
public interface ReportRepository extends JpaRepository<Report, Long> {
1012
boolean existsByTargetTypeAndTargetIdAndReportStatusIsNot(
1113
TargetType targetType, Long TargetId, ReportStatus reportStatus);
1214

1315
Optional<Report> findTopByReported_IdAndReportStatusOrderByCreatedAtDesc(
1416
Long memberId, ReportStatus reportStatus);
17+
18+
@Modifying
19+
@Query("delete from Report r where r.targetType = :targetType and r.targetId = :targetId")
20+
void deleteAllByTargetTypeAndTargetId(TargetType targetType, Long targetId);
1521
}

clokey-api/src/test/java/org/clokey/domain/history/controller/HistoryControllerTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
package org.clokey.domain.history.controller;
22

33
import static org.mockito.BDDMockito.given;
4+
import static org.mockito.BDDMockito.willDoNothing;
45
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
56
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
67
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
78

89
import com.fasterxml.jackson.databind.ObjectMapper;
910
import java.util.List;
1011
import org.clokey.domain.history.dto.request.HistoryCreateRequest;
12+
import org.clokey.domain.history.dto.request.HistoryImagesUploadRequest;
1113
import org.clokey.domain.history.dto.request.HistoryUpdateRequest;
1214
import org.clokey.domain.history.dto.response.DailyHistoryResponse;
1315
import org.clokey.domain.history.dto.response.HistoryClothTagListResponse;
1416
import org.clokey.domain.history.dto.response.HistoryCreateResponse;
17+
import org.clokey.domain.history.dto.response.HistoryImagesPresignedUrlResponse;
1518
import org.clokey.domain.history.dto.response.HistoryOwnershipCheckResponse;
1619
import org.clokey.domain.history.dto.response.MonthlyHistoryResponse;
1720
import org.clokey.domain.history.dto.response.SituationListResponse;
1821
import org.clokey.domain.history.dto.response.StyleListResponse;
1922
import org.clokey.domain.history.service.HistoryService;
23+
import org.clokey.enums.FileExtension;
2024
import org.junit.jupiter.api.Nested;
2125
import org.junit.jupiter.api.Test;
2226
import org.junit.jupiter.params.ParameterizedTest;
@@ -40,6 +44,40 @@ public class HistoryControllerTest {
4044

4145
@MockitoBean private HistoryService historyService;
4246

47+
@Nested
48+
class 옷_업로드_presigned_url_발급_요청_시 {
49+
50+
@Test
51+
void 유효한_요청이면_기록_이미지_업로드_Presigned_URL들을_반환한다() throws Exception {
52+
// given
53+
HistoryImagesUploadRequest request =
54+
new HistoryImagesUploadRequest(
55+
List.of(
56+
new HistoryImagesUploadRequest.Payload(
57+
FileExtension.JPEG, "testMd5Hash1"),
58+
new HistoryImagesUploadRequest.Payload(
59+
FileExtension.PNG, "testMd5Hash2")));
60+
61+
HistoryImagesPresignedUrlResponse response =
62+
new HistoryImagesPresignedUrlResponse(List.of("testUrl1", "testUrl2"));
63+
64+
given(historyService.getHistoryUploadPresignedUrls(request)).willReturn(response);
65+
66+
// when & then
67+
ResultActions perform =
68+
mockMvc.perform(
69+
post("/histories/images")
70+
.contentType(MediaType.APPLICATION_JSON)
71+
.content(objectMapper.writeValueAsString(request)));
72+
73+
perform.andExpect(status().isOk())
74+
.andExpect(jsonPath("$.isSuccess").value(true))
75+
.andExpect(jsonPath("$.code").value("COMMON201"))
76+
.andExpect(jsonPath("$.result.urls[0]").value("testUrl1"))
77+
.andExpect(jsonPath("$.result.urls[1]").value("testUrl2"));
78+
}
79+
}
80+
4381
@Nested
4482
class 기록_생성_요청_시 {
4583

@@ -845,4 +883,21 @@ void testCheckHistoryOwnership() throws Exception {
845883
.andExpect(jsonPath("$.result.isOwner").value(true));
846884
}
847885
}
886+
887+
@Nested
888+
class 기록_삭제_요청_시 {
889+
890+
@Test
891+
void 유효한_요청이면_기록을_삭제한다() throws Exception {
892+
// given
893+
willDoNothing().given(historyService).deleteHistory(1L);
894+
895+
// when & then
896+
ResultActions perform = mockMvc.perform(delete("/histories/1"));
897+
898+
perform.andExpect(status().isOk())
899+
.andExpect(jsonPath("$.isSuccess").value(true))
900+
.andExpect(jsonPath("$.code").value("COMMON204"));
901+
}
902+
}
848903
}

0 commit comments

Comments
 (0)