Skip to content

Commit 6646f33

Browse files
authored
Merge pull request #15 from NET-ZERO-FitFit/feature/clothes-delete
[FEAT] 판매 글 삭제 API 추가
2 parents bbc51a7 + 1d39a41 commit 6646f33

6 files changed

Lines changed: 77 additions & 2 deletions

File tree

src/main/java/fitfit/domain/clothes/controller/ClothesRestController.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ public ApiResponse<ClothesResponseDTO.CreateClothesResponse> registerClothes(
5151
return ApiResponse.onSuccess(response);
5252
}
5353

54+
@DeleteMapping("/{clothesId}")
55+
@Operation(summary = "판매 옷 삭제 API", description = "자신이 등록한 판매 옷을 삭제하는 API입니다.")
56+
@ApiResponses({
57+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "OK, 성공"),
58+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "MEMBER4001", description = "존재하지 않는 회원입니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
59+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CLOTHES4004", description = "존재하지 않는 옷입니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
60+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CLOTHES4003", description = "삭제 권한이 없는 사용자입니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class)))
61+
})
62+
public ApiResponse<String> deleteClothes(
63+
@RequestHeader(value = "Authorization") String authorization,
64+
@PathVariable(name = "clothesId") Long clothesId) {
65+
clothesCommandService.deleteClothes(authorization, clothesId);
66+
return ApiResponse.onSuccess("판매 옷 삭제에 성공했습니다.");
67+
}
68+
5469
@GetMapping("/search")
5570
@Operation(summary = "판매 옷 일반 검색 API", description = """
5671
키워드 검색을 통해 판매 옷 정보를 가져오는 API입니다. (존재하지 않는 키워드인 경우 빈 리스트로 응답)

src/main/java/fitfit/domain/clothes/dto/ClothesRequestDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static class CreateClothesRequest {
2424
private Integer price;
2525

2626
@NotNull(message = "카테고리 ID는 필수입니다.")
27-
@Schema(description = "카테고리 ID", example = "1")
27+
@Schema(description = "카테고리 ID", example = "8")
2828
private Long categoryId;
2929

3030
@NotBlank(message = "스타일은 필수입니다.")

src/main/java/fitfit/domain/clothes/service/ClothesCommandService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
public interface ClothesCommandService {
99
ClothesResponseDTO.CreateClothesResponse createClothes(String authorization,
1010
ClothesRequestDTO.CreateClothesRequest requestDto) throws IOException;
11+
12+
void deleteClothes(String authorization, Long clothesId);
1113
}

src/main/java/fitfit/domain/clothes/service/ClothesCommandServiceImpl.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.util.StringUtils;
2222

2323
import java.io.IOException;
24+
import java.util.List;
2425

2526
@Service
2627
@RequiredArgsConstructor
@@ -88,4 +89,35 @@ private void validateCreateClothesRequest(ClothesRequestDTO.CreateClothesRequest
8889
throw new ClothesHandler(ErrorStatus.CLOTHES_SIZE_NOT_FOUND);
8990
}
9091
}
92+
93+
@Override
94+
public void deleteClothes(String authorization, Long clothesId) {
95+
Long memberId = jwtProvider.getMemberIdAndValidateToken(authorization);
96+
97+
Member member = memberRepository.findById(memberId)
98+
.orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));
99+
100+
Clothes clothes = clothesRepository.findById(clothesId)
101+
.orElseThrow(() -> new ClothesHandler(ErrorStatus.CLOTHES_NOT_FOUND));
102+
103+
// 권한 검증: 옷의 소유주인지 확인
104+
if (!clothes.getSeller().equals(member)) {
105+
// FORBIDDEN_ACCESS와 같은 적절한 에러를 ErrorStatus에 정의했다고 가정합니다.
106+
throw new ClothesHandler(ErrorStatus.FORBIDDEN_ACCESS);
107+
}
108+
109+
// S3에서 피팅 이미지 삭제
110+
if (StringUtils.hasText(clothes.getFittingImage())) {
111+
s3Service.deleteFile(clothes.getFittingImage());
112+
}
113+
114+
// S3에서 일반 디스플레이 이미지들 삭제
115+
List<ClothesImage> clothesImages = clothes.getImages();
116+
if (clothesImages != null && !clothesImages.isEmpty()) {
117+
clothesImages.forEach(image -> s3Service.deleteFile(image.getImageUrl()));
118+
}
119+
120+
// 데이터베이스에서 옷 정보 삭제 (Cascade 설정에 따라 ClothesImage도 함께 삭제됨)
121+
clothesRepository.delete(clothes);
122+
}
91123
}

src/main/java/fitfit/global/apiPayload/code/status/ErrorStatus.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public enum ErrorStatus implements BaseErrorCode {
4949
// 옷 관련
5050
CLOTHES_SIZE_NOT_FOUND(HttpStatus.BAD_REQUEST, "CLOTHES4001", "사이즈 정보가 하나 이상 존재해야 합니다."),
5151
ADDRESS_NOT_FOUND_FOR_MEETUP(HttpStatus.BAD_REQUEST, "CLOTHES4002", "직거래 시 주소 정보는 필수입니다."),
52+
FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN, "CLOTHES4003", "삭제 권한이 없는 사용자입니다."),
53+
CLOTHES_NOT_FOUND(HttpStatus.NOT_FOUND, "CLOTHES4004", "존재하지 않는 옷입니다."),
5254

5355
//계좌 관련
5456
ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND, "ACCOUNT4001", "등록된 계좌 정보를 찾을 수 없습니다."),

src/main/java/fitfit/global/s3/S3Service.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package fitfit.global.s3;
22

33
import com.amazonaws.services.s3.AmazonS3Client;
4+
import com.amazonaws.services.s3.model.DeleteObjectRequest;
45
import com.amazonaws.services.s3.model.ObjectMetadata;
56
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
68
import org.springframework.beans.factory.annotation.Value;
79
import org.springframework.stereotype.Service;
8-
import org.springframework.web.multipart.MultipartFile;
910

1011
import java.io.ByteArrayInputStream;
1112
import java.io.IOException;
13+
import java.net.URL;
14+
import java.net.URLDecoder;
15+
import java.nio.charset.StandardCharsets;
1216
import java.util.Base64;
1317
import java.util.UUID;
1418

1519
@Service
20+
@Slf4j
1621
@RequiredArgsConstructor
1722
public class S3Service {
1823

@@ -47,4 +52,23 @@ private ObjectMetadata createMetadata(Long size, String contentType) {
4752
metadata.setContentType(contentType);
4853
return metadata;
4954
}
55+
56+
public void deleteFile(String fileUrl) {
57+
try {
58+
URL url = new URL(fileUrl);
59+
String path = url.getPath(); // URL에서 path(경로) 부분 추출 (e.g., "/bucket/key" 또는 "/key")
60+
String fileKey = path.substring(1); // 맨 앞의 "/" 제거
61+
62+
// Path-style URL(e.g., "bucket/key")인 경우, 경로 맨 앞의 버킷 이름을 제거하여 순수 키만 남김
63+
if (fileKey.startsWith(bucket + "/")) {
64+
fileKey = fileKey.substring(bucket.length() + 1);
65+
}
66+
67+
fileKey = URLDecoder.decode(fileKey, StandardCharsets.UTF_8);
68+
amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, fileKey));
69+
log.info("S3 파일 삭제 성공: {}", fileKey);
70+
} catch (Exception e) {
71+
log.error("S3 파일 삭제 실패. URL: {}, Error: {}", fileUrl, e.getMessage());
72+
}
73+
}
5074
}

0 commit comments

Comments
 (0)