From 6e140e0d4e6988a4ab59661c3cfeb6d6d7a46895 Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Mon, 26 Jan 2026 00:55:59 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{S3Ser?= =?UTF-8?q?vice=20-=20upload=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84}=20?= =?UTF-8?q?https://github.com/CampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../be/domain/s3/service/S3Service.java | 92 +++++++++++++++++++ .../be/domain/s3/util/FileUtil.java | 13 +++ .../be/global/exception/ErrorCode.java | 10 +- .../com/campustable/be/s3/S3ServiceTest.java | 39 ++++++++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/campustable/be/domain/s3/service/S3Service.java create mode 100644 src/main/java/com/campustable/be/domain/s3/util/FileUtil.java create mode 100644 src/test/java/com/campustable/be/s3/S3ServiceTest.java diff --git a/build.gradle b/build.gradle index fb9ee7c..d064ded 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ dependencies { // JavaNetCookieJar implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.3' + + // S3 + implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.1.0' } tasks.named('test') { diff --git a/src/main/java/com/campustable/be/domain/s3/service/S3Service.java b/src/main/java/com/campustable/be/domain/s3/service/S3Service.java new file mode 100644 index 0000000..253a435 --- /dev/null +++ b/src/main/java/com/campustable/be/domain/s3/service/S3Service.java @@ -0,0 +1,92 @@ +package com.campustable.be.domain.s3.service; + +import com.campustable.be.domain.s3.util.FileUtil; +import com.campustable.be.global.exception.CustomException; +import com.campustable.be.global.exception.ErrorCode; +import io.awspring.cloud.s3.S3Exception; +import io.awspring.cloud.s3.S3Resource; +import io.awspring.cloud.s3.S3Template; +import jakarta.transaction.Transactional; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@Slf4j +@RequiredArgsConstructor +public class S3Service { + + private final S3Template s3Template; + + @Value("${spring.cloud.aws.s3.bucket}") + private String bucket; + + + /** + * 파일 검증 및 원본 파일명 반환 + * + * @param file 요청된 MultipartFile + * @return 원본 파일명 + */ + private static String validateAndExtractFilename(MultipartFile file) { + // 파일 검증 + if (FileUtil.isNullOrEmpty(file)) { + log.error("파일이 비어있거나 존재하지 않습니다."); + throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); + } + + // 원본 파일 명 검증 + String originalFilename = file.getOriginalFilename(); + + // CommonUtil nvl 메소드 + String nvl; + + if (originalFilename == null) { + nvl = ""; + } else if (originalFilename.equals("")) { + nvl = ""; + } else if (originalFilename.isBlank()) { + nvl = ""; + } else { + nvl = originalFilename; + } + + if (nvl.isEmpty()) { + log.error("원본 파일명이 비어있거나 존재하지 않습니다."); + throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); + } + return originalFilename; + } + + public String uploadFile(MultipartFile file) { + + String originalFilename = validateAndExtractFilename(file); + + String storedPath = UUID.randomUUID() + "_" + originalFilename; + log.debug("생성된 파일명: {}", storedPath); + + try (InputStream inputStream = file.getInputStream()) { + + S3Resource resource = s3Template.upload(bucket, storedPath, inputStream); + + String s3Url = resource.getURL().toString(); + log.info("S3 업로드 성공: {}", s3Url); + + return s3Url; + + } catch (S3Exception e) { + log.error("AmazonServiceException - S3 파일 업로드 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); + throw new CustomException(ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR); + } catch (IOException e) { + log.error("IOException - 파일 스트림 처리 중 에러 발생. 원본 파일명: {}, 파일명: {} 에러: {}", originalFilename, storedPath, e.getMessage()); + throw new CustomException(ErrorCode.S3_UPLOAD_ERROR); + } + + } + +} diff --git a/src/main/java/com/campustable/be/domain/s3/util/FileUtil.java b/src/main/java/com/campustable/be/domain/s3/util/FileUtil.java new file mode 100644 index 0000000..f988f89 --- /dev/null +++ b/src/main/java/com/campustable/be/domain/s3/util/FileUtil.java @@ -0,0 +1,13 @@ +package com.campustable.be.domain.s3.util; + +import lombok.experimental.UtilityClass; +import org.springframework.web.multipart.MultipartFile; + +@UtilityClass +public class FileUtil { + + public boolean isNullOrEmpty(MultipartFile file){ + return file == null || file.isEmpty() || file.getOriginalFilename() == null; + } + +} diff --git a/src/main/java/com/campustable/be/global/exception/ErrorCode.java b/src/main/java/com/campustable/be/global/exception/ErrorCode.java index 7c847ba..2fef543 100644 --- a/src/main/java/com/campustable/be/global/exception/ErrorCode.java +++ b/src/main/java/com/campustable/be/global/exception/ErrorCode.java @@ -77,10 +77,18 @@ public enum ErrorCode { //Order INVALID_ORDER_STATUS(HttpStatus.BAD_REQUEST, "올바르지 않은 주문 상태 변경입니다."), + ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 주문을 찾을 수 없습니다."), //OrderItem - ORDER_ITEM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 주문메뉴를 찾을 수 없습니다."); + ORDER_ITEM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 주문메뉴를 찾을 수 없습니다."), + + //S3 + INVALID_FILE_REQUEST(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 요청입니다."), + + S3_UPLOAD_AMAZON_CLIENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 클라이언트 에러로 인해 파일 업로드에 실패했습니다."), + + S3_UPLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 파일 업로드 중 오류 발생"); private final HttpStatus status; private final String message; diff --git a/src/test/java/com/campustable/be/s3/S3ServiceTest.java b/src/test/java/com/campustable/be/s3/S3ServiceTest.java new file mode 100644 index 0000000..bcc789d --- /dev/null +++ b/src/test/java/com/campustable/be/s3/S3ServiceTest.java @@ -0,0 +1,39 @@ +package com.campustable.be.s3; + +import com.campustable.be.domain.s3.service.S3Service; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; // 가짜 파일 만드는 도구 + +import java.io.IOException; + +@SpringBootTest +class S3ServiceTest { + + @Autowired + private S3Service s3Service; + + @Test + @DisplayName("S3 이미지 업로드 테스트") + void uploadTest() throws IOException { + // 1. 가짜 이미지 파일 생성 (이름, 원래이름, 타입, 내용) + // MockMultipartFile은 스프링 테스트에서 제공하는 '가짜 파일'입니다. + MockMultipartFile fakeImage = new MockMultipartFile( + "image", // 필드명 + "test-image.jpg", // 파일명 + "image/jpeg", // 파일 타입 + "Hello S3".getBytes() // 파일 내용 (바이트) + ); + + // 2. 업로드 실행! + String url = s3Service.uploadFile(fakeImage); + + // 3. 결과 출력 + System.out.println("========================================"); + System.out.println("🎉 업로드 성공!"); + System.out.println("📍 이미지 주소: " + url); + System.out.println("========================================"); + } +} \ No newline at end of file From a340bb494822103dc53e6bedac5bb27401784e0f Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Mon, 26 Jan 2026 00:57:24 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{S3=20?= =?UTF-8?q?upload=20=EA=B8=B0=EB=8A=A5=20test=20=EC=82=AD=EC=A0=9C}=20http?= =?UTF-8?q?s://github.com/CampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/campustable/be/s3/S3ServiceTest.java | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 src/test/java/com/campustable/be/s3/S3ServiceTest.java diff --git a/src/test/java/com/campustable/be/s3/S3ServiceTest.java b/src/test/java/com/campustable/be/s3/S3ServiceTest.java deleted file mode 100644 index bcc789d..0000000 --- a/src/test/java/com/campustable/be/s3/S3ServiceTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.campustable.be.s3; - -import com.campustable.be.domain.s3.service.S3Service; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.mock.web.MockMultipartFile; // 가짜 파일 만드는 도구 - -import java.io.IOException; - -@SpringBootTest -class S3ServiceTest { - - @Autowired - private S3Service s3Service; - - @Test - @DisplayName("S3 이미지 업로드 테스트") - void uploadTest() throws IOException { - // 1. 가짜 이미지 파일 생성 (이름, 원래이름, 타입, 내용) - // MockMultipartFile은 스프링 테스트에서 제공하는 '가짜 파일'입니다. - MockMultipartFile fakeImage = new MockMultipartFile( - "image", // 필드명 - "test-image.jpg", // 파일명 - "image/jpeg", // 파일 타입 - "Hello S3".getBytes() // 파일 내용 (바이트) - ); - - // 2. 업로드 실행! - String url = s3Service.uploadFile(fakeImage); - - // 3. 결과 출력 - System.out.println("========================================"); - System.out.println("🎉 업로드 성공!"); - System.out.println("📍 이미지 주소: " + url); - System.out.println("========================================"); - } -} \ No newline at end of file From 2a231f8318258b699fe78b241f12564d41c98a53 Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Mon, 26 Jan 2026 01:59:26 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{menu?= =?UTF-8?q?=20image=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84}=20https://github.com/C?= =?UTF-8?q?ampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/controller/MenuController.java | 14 ++++++++++++++ .../be/domain/menu/dto/MenuRequest.java | 4 +--- .../be/domain/menu/service/MenuService.java | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java b/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java index 291ec2c..0dbe41f 100644 --- a/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java +++ b/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java @@ -8,10 +8,12 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/menu") @@ -70,6 +72,18 @@ public ResponseEntity createMenu(@Valid @RequestBody MenuRequest c return ResponseEntity.status(HttpStatus.CREATED).body(createMenu); } + @PostMapping(value = "/{menu_id}/image" ,consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @LogMonitoringInvocation + public ResponseEntity uploadMenuImage( + @PathVariable(name = "menu_id") Long menuId, + @RequestParam("image") MultipartFile image){ + + MenuResponse response = menuService.uploadMenuImage(menuId, image); + + return ResponseEntity.ok(response); + + } + @Override @PatchMapping("/{menu_id}") @LogMonitoringInvocation diff --git a/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java b/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java index 5f42a07..e75df03 100644 --- a/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java +++ b/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java @@ -26,8 +26,6 @@ public class MenuRequest { @Min(value = 0, message = "가격은 0원 이상이어야 합니다.") private Integer price; - @NotBlank(message = "이미지를 위한url은 필수입니다.") - private String menuUrl; @NotNull(message = "판매 가능 여부는 필수입니다.") private Boolean available; @@ -40,7 +38,7 @@ public Menu toEntity(Category category) { .category(category) .menuName(this.getMenuName()) .price(this.getPrice()) - .menuUrl(this.getMenuUrl()) + .menuUrl(null) .available(this.getAvailable()) .stockQuantity(this.getStockQuantity()) .build(); diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index 146c24e..6a18a2f 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -10,6 +10,7 @@ import com.campustable.be.domain.menu.dto.MenuUpdateRequest; import com.campustable.be.domain.menu.entity.Menu; import com.campustable.be.domain.menu.repository.MenuRepository; +import com.campustable.be.domain.s3.service.S3Service; import com.campustable.be.global.exception.CustomException; import com.campustable.be.global.exception.ErrorCode; import java.util.ArrayList; @@ -19,6 +20,7 @@ import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; +import org.springframework.web.multipart.MultipartFile; @Slf4j @@ -29,6 +31,7 @@ public class MenuService { private final MenuRepository menuRepository; private final CategoryRepository categoryRepository; private final CafeteriaService cafeteriaService; + private final S3Service s3Service; @Transactional @@ -55,6 +58,22 @@ public MenuResponse createMenu(MenuRequest request) { } + @Transactional + public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { + Menu menu = menuRepository.findById(menuId) + .orElseThrow(()-> new CustomException(ErrorCode.MENU_NOT_FOUND)); + + if (image == null || image.isEmpty()) { + throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); + } + + String menuUrl = s3Service.uploadFile(image); + + menu.setMenuUrl(menuUrl); + + return MenuResponse.from(menuRepository.save(menu)); + } + @Transactional(readOnly = true) public MenuResponse getMenuById(Long menuId) { From 8430fef2f8f755403c5edc6e7647ebca4d0df87a Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Mon, 26 Jan 2026 14:38:37 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EC=8B=9D=EB=8B=B9=EB=B3=84=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1}=20https://github.com/CampusTable/campus-tab?= =?UTF-8?q?le-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/campustable/be/domain/menu/service/MenuService.java | 6 +++++- .../com/campustable/be/domain/s3/service/S3Service.java | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index 6a18a2f..43d9658 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -67,7 +67,11 @@ public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); } - String menuUrl = s3Service.uploadFile(image); + String cafeteriaName = menu.getCategory().getCafeteria().getName(); + + String dirName = "menu/"+cafeteriaName; + + String menuUrl = s3Service.uploadFile(image, dirName); menu.setMenuUrl(menuUrl); diff --git a/src/main/java/com/campustable/be/domain/s3/service/S3Service.java b/src/main/java/com/campustable/be/domain/s3/service/S3Service.java index 253a435..b11cb32 100644 --- a/src/main/java/com/campustable/be/domain/s3/service/S3Service.java +++ b/src/main/java/com/campustable/be/domain/s3/service/S3Service.java @@ -63,11 +63,11 @@ private static String validateAndExtractFilename(MultipartFile file) { return originalFilename; } - public String uploadFile(MultipartFile file) { + public String uploadFile(MultipartFile file, String dirName) { String originalFilename = validateAndExtractFilename(file); - String storedPath = UUID.randomUUID() + "_" + originalFilename; + String storedPath = dirName + "/" + UUID.randomUUID() + "_" + originalFilename; log.debug("생성된 파일명: {}", storedPath); try (InputStream inputStream = file.getInputStream()) { From 6ebcd328882d1dfebd1c9656dc4bdd8b5b9437d8 Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Mon, 26 Jan 2026 16:03:58 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EC=83=9D=EC=84=B1=EC=8B=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?+=20docs=20=EC=88=98=EC=A0=95}=20https://github.com/CampusTable?= =?UTF-8?q?/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/controller/MenuController.java | 9 ++++--- .../menu/controller/MenuControllerDocs.java | 27 +++++++++++++++++-- .../be/domain/menu/dto/MenuRequest.java | 3 +++ .../be/domain/menu/service/MenuService.java | 22 ++++++++------- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java b/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java index 0dbe41f..0109ec2 100644 --- a/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java +++ b/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java @@ -64,14 +64,17 @@ public ResponseEntity> getAllMenusByCafeteriaId( } @Override - @PostMapping + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @LogMonitoringInvocation - public ResponseEntity createMenu(@Valid @RequestBody MenuRequest createRequest){ - MenuResponse createMenu = menuService.createMenu(createRequest); + public ResponseEntity createMenu( + @Valid @ModelAttribute MenuRequest request + ){ + MenuResponse createMenu = menuService.createMenu(request, request.getImage()); return ResponseEntity.status(HttpStatus.CREATED).body(createMenu); } + @Override @PostMapping(value = "/{menu_id}/image" ,consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @LogMonitoringInvocation public ResponseEntity uploadMenuImage( diff --git a/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java b/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java index a1d3479..e528e25 100644 --- a/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java +++ b/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java @@ -14,6 +14,7 @@ import org.springframework.http.ResponseEntity; import java.util.List; +import org.springframework.web.multipart.MultipartFile; /** * 메뉴 관리 시스템의 API 명세를 정의하는 인터페이스입니다. @@ -77,7 +78,7 @@ ResponseEntity> getAllMenusByCafeteriaId( /** * 새로운 메뉴를 시스템에 등록합니다. (관리자 권한 필요) - * * @param menuRequest 생성할 메뉴의 상세 정보 DTO + * * @param request 생성할 메뉴의 상세 정보 DTO + 이미지 파일 * @return 생성된 메뉴 정보를 담은 ResponseEntity */ @Operation(summary = "신규 메뉴 생성 (관리자 전용)", description = "새로운 메뉴를 등록합니다.") @@ -88,7 +89,29 @@ ResponseEntity> getAllMenusByCafeteriaId( @ApiResponse(responseCode = "409", description = "이미 존재하는 메뉴입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - ResponseEntity createMenu(MenuRequest menuRequest); + ResponseEntity createMenu( + @Parameter(description = "메뉴 정보 및 이미지 파일") MenuRequest request + ); + + /** + * 메뉴에 이미지를 업로드 합니다. + * * @param menuId 이미지를 등록할 메뉴의 ID + * @param image 업로드할 이미지 파일 + * @return 이미지 업로드가 완료된 메뉴 정보를 담은 ResponseEntity + */ + @Operation( + summary = "메뉴 이미지 개별 업로드/수정", + description = "메뉴에 사진을 추가합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "이미지 업로드 및 경로 업데이트 성공"), + @ApiResponse(responseCode = "404", description = "해당 ID의 메뉴를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 파일 요청입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + ResponseEntity uploadMenuImage( + @Parameter(description = "대상 메뉴 ID", example = "1") Long menuId, + @Parameter(description = "업로드할 이미지 파일") MultipartFile image + ); /** * 기존 메뉴 정보를 수정합니다. (관리자 권한 필요) diff --git a/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java b/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java index e75df03..9858225 100644 --- a/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java +++ b/src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java @@ -10,6 +10,7 @@ import java.math.BigDecimal; import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; @Getter @Setter @@ -32,6 +33,8 @@ public class MenuRequest { private Integer stockQuantity; + private MultipartFile image; + public Menu toEntity(Category category) { return Menu.builder() diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index 43d9658..054ce46 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -35,7 +35,7 @@ public class MenuService { @Transactional - public MenuResponse createMenu(MenuRequest request) { + public MenuResponse createMenu(MenuRequest request, MultipartFile image) { Category category = categoryRepository.findById(request.getCategoryId()) .orElseThrow(() -> { @@ -43,18 +43,20 @@ public MenuResponse createMenu(MenuRequest request) { return new CustomException(ErrorCode.CATEGORY_NOT_FOUND); }); - Optional existingMenu = menuRepository.findByCategoryAndMenuName( - category, - request.getMenuName() - ); + menuRepository.findByCategoryAndMenuName(category, request.getMenuName()) + .ifPresent(menu -> { + log.error("createMenu: 이미 해당 카테고리에 menu가 존재합니다. 생성이 아닌 수정을 통해 진행해주세요."); + throw new CustomException(ErrorCode.MENU_ALREADY_EXISTS); + }); + + Menu menu = request.toEntity(category); + Menu savedMenu = menuRepository.save(menu); - if (existingMenu.isPresent()) { - log.error("createMenu: 이미 해당 카테고리에 menu가 존재합니다. 생성이 아닌 수정을 통해 진행해주세요."); - throw new CustomException(ErrorCode.MENU_ALREADY_EXISTS); + if(image != null && !image.isEmpty()) { + uploadMenuImage(savedMenu.getId(), image); } - Menu menu = request.toEntity(category); - return MenuResponse.from(menuRepository.save(menu)); + return MenuResponse.from(savedMenu); } From 57dfdbcf28be41900a8307f1e5c3b4b8370a4062 Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Tue, 27 Jan 2026 21:11:18 +0900 Subject: [PATCH 06/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{s3=20?= =?UTF-8?q?deletefile=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84}=20https:/?= =?UTF-8?q?/github.com/CampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/domain/menu/service/MenuService.java | 22 +++++--- .../be/domain/s3/service/S3Service.java | 53 ++++++++++++++----- .../be/global/common/CommonUtil.java | 16 ++++++ .../be/global/exception/ErrorCode.java | 8 ++- 4 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/campustable/be/global/common/CommonUtil.java diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index 054ce46..07419b2 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -52,7 +52,7 @@ public MenuResponse createMenu(MenuRequest request, MultipartFile image) { Menu menu = request.toEntity(category); Menu savedMenu = menuRepository.save(menu); - if(image != null && !image.isEmpty()) { + if (image != null && !image.isEmpty()) { uploadMenuImage(savedMenu.getId(), image); } @@ -63,15 +63,19 @@ public MenuResponse createMenu(MenuRequest request, MultipartFile image) { @Transactional public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { Menu menu = menuRepository.findById(menuId) - .orElseThrow(()-> new CustomException(ErrorCode.MENU_NOT_FOUND)); + .orElseThrow(() -> new CustomException(ErrorCode.MENU_NOT_FOUND)); if (image == null || image.isEmpty()) { throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); } - String cafeteriaName = menu.getCategory().getCafeteria().getName(); + if (menu.getMenuUrl() != null && !menu.getMenuUrl().isBlank()) { + s3Service.deleteFile(menu.getMenuUrl()); + } + + String cafeteriaName = menu.getCategory().getCafeteria().getName(); - String dirName = "menu/"+cafeteriaName; + String dirName = "menu/" + cafeteriaName; String menuUrl = s3Service.uploadFile(image, dirName); @@ -84,7 +88,7 @@ public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { public MenuResponse getMenuById(Long menuId) { Menu menu = menuRepository.findById(menuId) - .orElseThrow(()->{ + .orElseThrow(() -> { log.error("getMenuById : 유효하지않은 menuId"); return new CustomException(ErrorCode.MENU_NOT_FOUND); }); @@ -162,10 +166,12 @@ public void deleteMenu(Long menuId) { if (menu.isEmpty()) { log.error("menuId not found {}", menuId); throw new CustomException(ErrorCode.MENU_NOT_FOUND); - } else { - menuRepository.delete(menu.get()); } + if(menu.get().getMenuUrl() != null && !menu.get().getMenuUrl().isBlank()) { + s3Service.deleteFile(menu.get().getMenuUrl()); + } + menuRepository.deleteById(menuId); } -} + } diff --git a/src/main/java/com/campustable/be/domain/s3/service/S3Service.java b/src/main/java/com/campustable/be/domain/s3/service/S3Service.java index b11cb32..6fcad62 100644 --- a/src/main/java/com/campustable/be/domain/s3/service/S3Service.java +++ b/src/main/java/com/campustable/be/domain/s3/service/S3Service.java @@ -1,6 +1,7 @@ package com.campustable.be.domain.s3.service; import com.campustable.be.domain.s3.util.FileUtil; +import com.campustable.be.global.common.CommonUtil; import com.campustable.be.global.exception.CustomException; import com.campustable.be.global.exception.ErrorCode; import io.awspring.cloud.s3.S3Exception; @@ -43,20 +44,7 @@ private static String validateAndExtractFilename(MultipartFile file) { // 원본 파일 명 검증 String originalFilename = file.getOriginalFilename(); - // CommonUtil nvl 메소드 - String nvl; - - if (originalFilename == null) { - nvl = ""; - } else if (originalFilename.equals("")) { - nvl = ""; - } else if (originalFilename.isBlank()) { - nvl = ""; - } else { - nvl = originalFilename; - } - - if (nvl.isEmpty()) { + if (CommonUtil.nvl(originalFilename, "").isEmpty()) { log.error("원본 파일명이 비어있거나 존재하지 않습니다."); throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); } @@ -89,4 +77,41 @@ public String uploadFile(MultipartFile file, String dirName) { } + public void deleteFile(String storedPath) { + if (CommonUtil.nvl(storedPath, "").isEmpty()) { + log.warn("요청된 파일 경로가 없습니다."); + return; + } + + try { + + String key = extractKeyFromUrl(storedPath); + + s3Template.deleteObject(bucket, key); + log.info("S3 파일 삭제 성공: {}", storedPath); + + } catch (S3Exception e) { + log.error("S3Exception - S3 파일 삭제 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); + throw new CustomException(ErrorCode.S3_DELETE_AMAZON_SERVICE_ERROR); + } catch (RuntimeException e) { + // 기존 AmazonClientException 역할 (네트워크 등 클라이언트 에러) + log.error("RuntimeException - S3 파일 삭제 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); + throw new CustomException(ErrorCode.S3_DELETE_AMAZON_CLIENT_ERROR); + } catch (Exception e) { + log.error("S3 파일 삭제 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); + throw new CustomException(ErrorCode.S3_DELETE_ERROR); + + } + } + + private String extractKeyFromUrl(String fileUrl) { + if (fileUrl.contains(".com/")) { + String key = fileUrl.substring(fileUrl.lastIndexOf(".com/") + 5); + log.info("추출된 S3 Key: [{}]", key); + return key; + } + return fileUrl; + } + + } diff --git a/src/main/java/com/campustable/be/global/common/CommonUtil.java b/src/main/java/com/campustable/be/global/common/CommonUtil.java new file mode 100644 index 0000000..d1e9b24 --- /dev/null +++ b/src/main/java/com/campustable/be/global/common/CommonUtil.java @@ -0,0 +1,16 @@ +package com.campustable.be.global.common; + +public class CommonUtil { + + public static String nvl(String str1,String str2){ + if (str1 == null) { // str1 이 null 인 경우 + return str2; + } else if (str1.equals("null")) { // str1 이 문자열 "null" 인 경우 + return str2; + } else if (str1.isBlank()) { // str1 이 "" or " " 인 경우 + return str2; + } + return str1; + } + +} diff --git a/src/main/java/com/campustable/be/global/exception/ErrorCode.java b/src/main/java/com/campustable/be/global/exception/ErrorCode.java index 2fef543..a59699b 100644 --- a/src/main/java/com/campustable/be/global/exception/ErrorCode.java +++ b/src/main/java/com/campustable/be/global/exception/ErrorCode.java @@ -88,7 +88,13 @@ public enum ErrorCode { S3_UPLOAD_AMAZON_CLIENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 클라이언트 에러로 인해 파일 업로드에 실패했습니다."), - S3_UPLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 파일 업로드 중 오류 발생"); + S3_UPLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 파일 업로드 중 오류 발생"), + + S3_DELETE_AMAZON_SERVICE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 서비스 에러로 인해 파일 삭제에 실패했습니다."), + + S3_DELETE_AMAZON_CLIENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 클라이언트 에러로 인해 파일 삭제에 실패했습니다."), + + S3_DELETE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 파일 삭제 중 오류 발생"); private final HttpStatus status; private final String message; From 06ad745307366f6ba9ef181837203e043030062e Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Tue, 27 Jan 2026 22:32:52 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{menu?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A0=84=EC=9A=A9=20API=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=B6=84=EB=A6=AC}=20https://github.com/C?= =?UTF-8?q?ampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/controller/MenuController.java | 18 +++++------ .../menu/controller/MenuControllerDocs.java | 30 +++++++------------ 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java b/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java index 0109ec2..ddc4f8a 100644 --- a/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java +++ b/src/main/java/com/campustable/be/domain/menu/controller/MenuController.java @@ -16,7 +16,7 @@ import org.springframework.web.multipart.MultipartFile; @RestController -@RequestMapping("/api/menu") +@RequestMapping("/api") @RequiredArgsConstructor public class MenuController implements MenuControllerDocs { @@ -25,7 +25,7 @@ public class MenuController implements MenuControllerDocs { @Override - @GetMapping + @GetMapping("/menus") @LogMonitoringInvocation public ResponseEntity> getAllMenus(){ @@ -37,7 +37,7 @@ public ResponseEntity> getAllMenus(){ @Override @LogMonitoringInvocation - @GetMapping("/category/{category_id}") + @GetMapping("/category/{category_id}/menus") public ResponseEntity> getAllMenusByCategoryId( @PathVariable(name = "category_id") Long categoryId){ @@ -49,14 +49,14 @@ public ResponseEntity> getAllMenusByCategoryId( @Override @LogMonitoringInvocation - @GetMapping("/{menuId}") + @GetMapping("/menus/{menuId}") public ResponseEntity getMenuById(@PathVariable Long menuId){ return ResponseEntity.ok(menuService.getMenuById(menuId)); } @Override @LogMonitoringInvocation - @GetMapping("/cafeteria/{cafeteria-id}") + @GetMapping("/menus/cafeteria/{cafeteria-id}") public ResponseEntity> getAllMenusByCafeteriaId( @PathVariable(name = "cafeteria-id") Long cafeteriaId ) { @@ -64,7 +64,7 @@ public ResponseEntity> getAllMenusByCafeteriaId( } @Override - @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/admin/menus", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @LogMonitoringInvocation public ResponseEntity createMenu( @Valid @ModelAttribute MenuRequest request @@ -75,7 +75,7 @@ public ResponseEntity createMenu( } @Override - @PostMapping(value = "/{menu_id}/image" ,consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/admin/menus/{menu_id}/image" ,consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @LogMonitoringInvocation public ResponseEntity uploadMenuImage( @PathVariable(name = "menu_id") Long menuId, @@ -88,7 +88,7 @@ public ResponseEntity uploadMenuImage( } @Override - @PatchMapping("/{menu_id}") + @PatchMapping("/admin/menus/{menu_id}") @LogMonitoringInvocation public ResponseEntity updateMenu( @PathVariable(name = "menu_id") Long menuId, @@ -101,7 +101,7 @@ public ResponseEntity updateMenu( @Override @LogMonitoringInvocation - @DeleteMapping("/{menu_id}") + @DeleteMapping("/admin/menus/{menu_id}") public ResponseEntity deleteMenu( @PathVariable(name = "menu_id") Long menuId) { diff --git a/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java b/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java index e528e25..35b88ff 100644 --- a/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java +++ b/src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java @@ -25,7 +25,6 @@ public interface MenuControllerDocs { /** * 시스템에 등록된 모든 메뉴 목록을 조회합니다. - * * @return 메뉴 정보 리스트를 담은 ResponseEntity */ @Operation(summary = "메뉴 전체 조회", description = "모든 메뉴 목록을 조회합니다.") @ApiResponse(responseCode = "200", description = "조회 성공") @@ -33,8 +32,6 @@ public interface MenuControllerDocs { /** * 고유 식별자를 통해 단일 메뉴의 상세 정보를 조회합니다. - * * @param menuId 조회하고자 하는 메뉴의 ID - * @return 해당 메뉴의 상세 정보를 담은 ResponseEntity */ @Operation(summary = "단일 메뉴 상세 조회", description = "특정 ID에 해당하는 메뉴의 상세 정보를 조회합니다.") @ApiResponses({ @@ -48,8 +45,6 @@ ResponseEntity getMenuById( /** * 특정 카테고리에 속한 모든 메뉴를 조회합니다. - * * @param categoryId 카테고리 고유 식별자 - * @return 해당 카테고리의 메뉴 리스트를 담은 ResponseEntity */ @Operation(summary = "카테고리별 메뉴 조회", description = "특정 카테고리 ID에 해당하는 메뉴 목록을 조회합니다.") @ApiResponses({ @@ -63,8 +58,6 @@ ResponseEntity> getAllMenusByCategoryId( /** * 특정 식당에서 제공하는 모든 메뉴를 조회합니다. - * * @param cafeteriaId 식당 고유 식별자 - * @return 해당 식당의 메뉴 리스트를 담은 ResponseEntity */ @Operation(summary = "식당별 메뉴 조회", description = "식당 ID에 해당하는 메뉴 목록을 조회합니다.") @ApiResponses({ @@ -94,32 +87,31 @@ ResponseEntity createMenu( ); /** - * 메뉴에 이미지를 업로드 합니다. + * 메뉴에 이미지를 업로드 합니다. (관리자 권한 필요) * * @param menuId 이미지를 등록할 메뉴의 ID * @param image 업로드할 이미지 파일 * @return 이미지 업로드가 완료된 메뉴 정보를 담은 ResponseEntity */ @Operation( - summary = "메뉴 이미지 개별 업로드/수정", - description = "메뉴에 사진을 추가합니다." + summary = "메뉴 이미지 개별 업로드/수정 (관리자 전용)", + description = "메뉴의 사진을 추가하거나 수정합니다." ) @ApiResponses({ @ApiResponse(responseCode = "200", description = "이미지 업로드 및 경로 업데이트 성공"), - @ApiResponse(responseCode = "404", description = "해당 ID의 메뉴를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "400", description = "유효하지 않은 파일 요청입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + @ApiResponse(responseCode = "404", description = "해당 ID의 메뉴를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 파일 요청입니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) ResponseEntity uploadMenuImage( @Parameter(description = "대상 메뉴 ID", example = "1") Long menuId, - @Parameter(description = "업로드할 이미지 파일") MultipartFile image + @Parameter(description = "업로드할 이미지 파일") MultipartFile image ); /** * 기존 메뉴 정보를 수정합니다. (관리자 권한 필요) - * * @param menuId 수정할 메뉴의 ID - * @param menuUpdateRequest 수정할 내용이 담긴 DTO - * @return 수정 완료된 메뉴 정보를 담은 ResponseEntity */ - @Operation(summary = "메뉴 정보 수정 (관리자 전용)", description = "특정 ID의 메뉴 정보를 수정합니다.") + @Operation(summary = "메뉴 정보 수정 (관리자 전용)", description = "특정 ID의 메뉴 정보를 수정합니다. (이미지 제외)") @ApiResponses({ @ApiResponse(responseCode = "200", description = "메뉴 수정 성공"), @ApiResponse(responseCode = "400", description = "입력값 오류", @@ -129,13 +121,11 @@ ResponseEntity uploadMenuImage( }) ResponseEntity updateMenu( @Parameter(description = "수정할 메뉴 ID", example = "1") Long menuId, - MenuUpdateRequest menuUpdateRequest + @Parameter(description = "수정할 메뉴 정보") MenuUpdateRequest menuUpdateRequest ); /** * 특정 메뉴를 시스템에서 삭제합니다. (관리자 권한 필요) - * * @param menuId 삭제할 메뉴의 ID - * @return 삭제 성공 시 빈 바디를 담은 ResponseEntity (204 No Content) */ @Operation(summary = "메뉴 삭제 (관리자 전용)", description = "특정 ID의 메뉴를 삭제합니다.") @ApiResponses({ From c2f800f1e827758903b40279b9d66f31207dddbd Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Sun, 1 Feb 2026 22:21:32 +0900 Subject: [PATCH 08/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{sprin?= =?UTF-8?q?g-cloud-aws-starter-s3=20=EB=B2=84=EC=A0=84=203.1.0=20->=203.4.?= =?UTF-8?q?2,=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=ED=9B=84=20=EC=97=85=EB=A1=9C=EB=93=9C=20->=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=ED=9B=84=20=EC=82=AD=EC=A0=9C=20}=20https?= =?UTF-8?q?://github.com/CampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../be/domain/menu/service/MenuService.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index d064ded..03ca3ee 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.3' // S3 - implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.1.0' + implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2' } tasks.named('test') { diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index 07419b2..255756d 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -69,19 +69,19 @@ public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); } - if (menu.getMenuUrl() != null && !menu.getMenuUrl().isBlank()) { - s3Service.deleteFile(menu.getMenuUrl()); - } - + String oldUrl = menu.getMenuUrl(); String cafeteriaName = menu.getCategory().getCafeteria().getName(); - String dirName = "menu/" + cafeteriaName; - String menuUrl = s3Service.uploadFile(image, dirName); + String newUrl = s3Service.uploadFile(image, dirName); + menu.setMenuUrl(newUrl); + Menu savedMenu = menuRepository.save(menu); - menu.setMenuUrl(menuUrl); + if(oldUrl != null && !oldUrl.isBlank()){ + s3Service.deleteFile(oldUrl); + } - return MenuResponse.from(menuRepository.save(menu)); + return MenuResponse.from(savedMenu); } @Transactional(readOnly = true) From 3f389098cfafb9d11e43a7dad590972a03d63676 Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Sun, 1 Feb 2026 22:51:01 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{crate?= =?UTF-8?q?Menu:=EC=9D=B4=EB=AF=B8=EC=A7=80=ED=8F=AC=ED=95=A8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20url=EA=B9=8C?= =?UTF-8?q?=EC=A7=80=20=EB=B0=98=ED=99=98,=EA=B8=B0=EC=A1=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=EC=8B=9C=20=EB=A1=9C=EA=B9=85=EB=A7=8C=20=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B2=B0=EA=B3=BC=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80}=20https://github.com/CampusTable/campus-table-be/iss?= =?UTF-8?q?ues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../campustable/be/domain/menu/service/MenuService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index 255756d..ebd45f3 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -53,7 +53,7 @@ public MenuResponse createMenu(MenuRequest request, MultipartFile image) { Menu savedMenu = menuRepository.save(menu); if (image != null && !image.isEmpty()) { - uploadMenuImage(savedMenu.getId(), image); + return uploadMenuImage(savedMenu.getId(), image); } return MenuResponse.from(savedMenu); @@ -78,7 +78,11 @@ public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { Menu savedMenu = menuRepository.save(menu); if(oldUrl != null && !oldUrl.isBlank()){ - s3Service.deleteFile(oldUrl); + try { + s3Service.deleteFile(oldUrl); + }catch (Exception e){ + log.warn("uploadMenuImage: 기존 이미지 삭제 실패. oldUrl={}", oldUrl, e); + } } return MenuResponse.from(savedMenu); From 15d0e12cc11fdade74eef11c94b11749cacb0646 Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Sun, 1 Feb 2026 23:03:57 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EC=A0=80=EC=9E=A5=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C}?= =?UTF-8?q?=20https://github.com/CampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/domain/menu/service/MenuService.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index ebd45f3..095ba9b 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -75,12 +75,22 @@ public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { String newUrl = s3Service.uploadFile(image, dirName); menu.setMenuUrl(newUrl); - Menu savedMenu = menuRepository.save(menu); + Menu savedMenu; + try { + savedMenu = menuRepository.save(menu); + } catch (Exception e) { + try { + s3Service.deleteFile(newUrl); + } catch (Exception ex) { + log.warn("uploadMenuImage: 신규 이미지 정리 실패. newUrl={}", newUrl, ex); + } + throw new CustomException(ErrorCode.S3_DELETE_ERROR); + } - if(oldUrl != null && !oldUrl.isBlank()){ + if (oldUrl != null && !oldUrl.isBlank()) { try { s3Service.deleteFile(oldUrl); - }catch (Exception e){ + } catch (Exception e) { log.warn("uploadMenuImage: 기존 이미지 삭제 실패. oldUrl={}", oldUrl, e); } } @@ -171,11 +181,15 @@ public void deleteMenu(Long menuId) { log.error("menuId not found {}", menuId); throw new CustomException(ErrorCode.MENU_NOT_FOUND); } - if(menu.get().getMenuUrl() != null && !menu.get().getMenuUrl().isBlank()) { - s3Service.deleteFile(menu.get().getMenuUrl()); + if (menu.get().getMenuUrl() != null && !menu.get().getMenuUrl().isBlank()) { + try { + s3Service.deleteFile(menu.get().getMenuUrl()); + } catch (Exception e) { + log.warn("deleteMenu: 이미지 삭제 실패. menuId={}, url={}", menuId, menu.get().getMenuUrl(), e); + } } menuRepository.deleteById(menuId); } - } +} From 202ca1197796f8eb7c031555dc60d171ac7a3b81 Mon Sep 17 00:00:00 2001 From: kjh0718 Date: Sun, 1 Feb 2026 23:14:57 +0900 Subject: [PATCH 11/11] =?UTF-8?q?=EB=A9=94=EB=89=B4=5F=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=5F=ED=8C=8C=EC=9D=BC=5F=EC=97=85=EB=A1=9C=EB=93=9C=5F?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=5F=EC=B6=94=EA=B0=80=20:=20feat=20:=20{?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95}=20https://github.com/C?= =?UTF-8?q?ampusTable/campus-table-be/issues/76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/campustable/be/domain/menu/service/MenuService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java index 095ba9b..04f1ba1 100644 --- a/src/main/java/com/campustable/be/domain/menu/service/MenuService.java +++ b/src/main/java/com/campustable/be/domain/menu/service/MenuService.java @@ -84,7 +84,8 @@ public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { } catch (Exception ex) { log.warn("uploadMenuImage: 신규 이미지 정리 실패. newUrl={}", newUrl, ex); } - throw new CustomException(ErrorCode.S3_DELETE_ERROR); + log.error("uploadMenuImage: 메뉴 저장 실패. menuId={}", menuId, e); + throw e; } if (oldUrl != null && !oldUrl.isBlank()) {