From c1f230c6999ce865b5bef015b35f7a275a94610b Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 16:09:52 +0900 Subject: [PATCH 01/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/menu/dto/MenuReqDto.java | 43 +++++++++++++++++++ .../eatsfine/domain/menu/dto/MenuResDto.java | 43 +++++++++++++++++++ .../domain/menu/exception/MenuException.java | 10 +++++ 3 files changed, 96 insertions(+) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/exception/MenuException.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java new file mode 100644 index 0000000..1e6c8cd --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java @@ -0,0 +1,43 @@ +package com.eatsfine.eatsfine.domain.menu.dto; + +import com.eatsfine.eatsfine.domain.menu.enums.MenuCategory; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; + +import java.math.BigDecimal; +import java.util.List; + +public class MenuReqDto { + + public record MenuCreateDto( + @Valid + @NotNull + @Size(min = 1, message = "최소 1개 이상의 메뉴를 등록해야 합니다.") + List menus + ){} + + + public record MenuDto( + @NotBlank(message = "메뉴 이름은 필수입니다.") + String name, + + @Size(max = 500, message = "설명은 500자 이내여야 합니다.") + String description, + + @NotNull(message = "가격은 필수입니다.") + @Min(value = 0, message = "가격은 0원 이상이어야 합니다.") + BigDecimal price, + + @NotNull(message = "카테고리는 필수입니다.") + MenuCategory category, + + String imageKey // 이미지는 선택 사항이므로 검증 없음 (nullable) + ){} + + + public record MenuDeleteDto( + @NotNull + @Size(min = 1, message = "삭제할 메뉴를 최소 1개 이상 선택해주세요.") + List menuIds + ){} +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java new file mode 100644 index 0000000..1688efe --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java @@ -0,0 +1,43 @@ + +package com.eatsfine.eatsfine.domain.menu.dto; + +import com.eatsfine.eatsfine.domain.menu.enums.MenuCategory; +import lombok.Builder; + +import java.math.BigDecimal; +import java.util.List; + +public class MenuResDto { + + @Builder + public record ImageUploadDto( + String imageKey + ){} + + + @Builder + public record ImageDeleteDto( + String deletedImageKey + ){} + + @Builder + public record MenuCreateDto( + List menus + ){} + + @Builder + public record MenuDto( + Long menuId, + String name, + String description, + BigDecimal price, + MenuCategory category, + String imageKey + + ){} + + @Builder + public record MenuDeleteDto( + List deletedMenuIds + ){} +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/exception/MenuException.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/exception/MenuException.java new file mode 100644 index 0000000..c84882f --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/exception/MenuException.java @@ -0,0 +1,10 @@ +package com.eatsfine.eatsfine.domain.menu.exception; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; + +public class MenuException extends GeneralException { + public MenuException(BaseErrorCode code) { + super(code); + } +} From a54580d72cef1c5517ba0247e82726814bd534fa Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 16:10:21 +0900 Subject: [PATCH 02/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20API=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/status/MenuErrorStatus.java | 40 ++++++++++++++++ .../domain/menu/status/MenuSuccessStatus.java | 48 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuErrorStatus.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuErrorStatus.java new file mode 100644 index 0000000..b467af0 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuErrorStatus.java @@ -0,0 +1,40 @@ +package com.eatsfine.eatsfine.domain.menu.status; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MenuErrorStatus implements BaseErrorCode { + + _MENU_NOT_FOUND(HttpStatus.NOT_FOUND, "MENU404", "메뉴를 찾을 수 없습니다."), + + ; + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .message(message) + .code(code) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .isSuccess(false) + .httpStatus(httpStatus) + .message(message) + .code(code) + .build(); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java new file mode 100644 index 0000000..946a551 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java @@ -0,0 +1,48 @@ +package com.eatsfine.eatsfine.domain.menu.status; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseCode; +import com.eatsfine.eatsfine.global.apiPayload.code.ReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MenuSuccessStatus implements BaseCode { + + + _MENU_IMAGE_UPLOAD_SUCCESS(HttpStatus.CREATED, "MENU201", "메뉴 이미지 업로드 성공"), + + _MENU_IMAGE_DELETE_SUCCESS(HttpStatus.OK, "MENU200", "메뉴 이미지 삭제 성공"), + + + _MENU_CREATE_SUCCESS(HttpStatus.CREATED, "MENU202", "메뉴 생성 성공"), + + _MENU_DELETE_SUCCESS(HttpStatus.OK, "MENU2002", "메뉴 삭제 성공"), + + ; + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return ReasonDto.builder() + .isSuccess(true) + .message(message) + .code(code) + .build(); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return ReasonDto.builder() + .isSuccess(true) + .httpStatus(httpStatus) + .message(message) + .code(code) + .build(); + } +} From d62937003c212764f8802cd85f9b64187e24de57 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 16:11:40 +0900 Subject: [PATCH 03/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4,=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=93=B1=EB=A1=9D,?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/controller/MenuController.java | 67 ++++++++++ .../domain/menu/converter/MenuConverter.java | 47 +++++++ .../menu/repository/MenuRepository.java | 3 + .../menu/service/MenuCommandService.java | 13 ++ .../menu/service/MenuCommandServiceImpl.java | 119 ++++++++++++++++++ .../domain/store/status/StoreErrorStatus.java | 5 +- 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java new file mode 100644 index 0000000..8e61df7 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java @@ -0,0 +1,67 @@ +package com.eatsfine.eatsfine.domain.menu.controller; + +import com.eatsfine.eatsfine.domain.menu.dto.MenuReqDto; +import com.eatsfine.eatsfine.domain.menu.dto.MenuResDto; +import com.eatsfine.eatsfine.domain.menu.service.MenuCommandService; +import com.eatsfine.eatsfine.domain.menu.status.MenuSuccessStatus; +import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Menu", description = "가게 메뉴 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class MenuController { + + private final MenuCommandService menuCommandService; + + @Operation(summary = "메뉴 이미지 선 업로드 API", description = "메뉴 등록 전에 이미지를 먼저 업로드하고 KEY를 반환합니다.") + @PostMapping(value = "/stores/{storeId}/menus/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ApiResponse uploadImage( + @PathVariable Long storeId, + @RequestPart("image") MultipartFile file + ){ + return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_UPLOAD_SUCCESS, menuCommandService.uploadImage(storeId, file)); + } + + + @Operation(summary = "메뉴 이미지 삭제 API", description = """ + 메뉴 이미지를 삭제합니다. 이 API는 아래 두가지 시나리오 모두 처리합니다. + 1. 메뉴 등록 전: 메뉴 등록하다가 업로드만 된 이미지(고아 이미지)를 S3에서 삭제합니다. (가게 등록 전 이미지를 먼저 업로드하기 때문) + 2. 메뉴 등록 후: DB에 연결된 메뉴의 이미지를 S3에서 삭제하고, DB의 imageKey도 null로 업데이트합니다. + + """) + @DeleteMapping("/stores/{storeId}/menus/images") + public ApiResponse deleteImage( + @PathVariable Long storeId, + @RequestParam("key") String imageKey + ) { + + return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_DELETE_SUCCESS, menuCommandService.deleteImage(storeId,imageKey)); + } + + @Operation(summary = "메뉴 등록 API", description = "가게의 메뉴를 등록합니다.") + @PostMapping("/stores/{storeId}/menus") + public ApiResponse createMenus( + @PathVariable Long storeId, + @RequestBody @Valid MenuReqDto.MenuCreateDto dto + ) { + return ApiResponse.of(MenuSuccessStatus._MENU_CREATE_SUCCESS, menuCommandService.createMenus(storeId, dto)); + } + + @Operation(summary = "메뉴 삭제 API", description = "가게의 메뉴를 삭제합니다.") + @DeleteMapping("/stores/{storeId}/menus") + public ApiResponse deleteMenus( + @PathVariable Long storeId, + @RequestBody @Valid MenuReqDto.MenuDeleteDto dto + ) { + return ApiResponse.of(MenuSuccessStatus._MENU_DELETE_SUCCESS, menuCommandService.deleteMenus(storeId, dto)); + } + +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java new file mode 100644 index 0000000..dff0767 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java @@ -0,0 +1,47 @@ +package com.eatsfine.eatsfine.domain.menu.converter; + +import com.eatsfine.eatsfine.domain.menu.dto.MenuResDto; +import com.eatsfine.eatsfine.domain.menu.entity.Menu; + +import java.util.List; + +public class MenuConverter { + + + public static MenuResDto.ImageUploadDto toImageUploadDto(String imageKey){ + return MenuResDto.ImageUploadDto.builder() + .imageKey(imageKey) + .build(); + } + + public static MenuResDto.ImageDeleteDto toImageDeleteDto(String imageKey) { + return MenuResDto.ImageDeleteDto.builder() + .deletedImageKey(imageKey) + .build(); + } + + public static MenuResDto.MenuCreateDto toCreateDto(List menus){ + List menuDtos = menus.stream() + .map(menu -> MenuResDto.MenuDto.builder() + .menuId(menu.getId()) + .name(menu.getName()) + .description(menu.getDescription()) + .price(menu.getPrice()) + .category(menu.getMenuCategory()) + .imageKey(menu.getImageKey()) + .build() + ) + .toList(); + + return MenuResDto.MenuCreateDto.builder() + .menus(menuDtos) + .build(); + } + + public static MenuResDto.MenuDeleteDto toDeleteDto(List menuIds){ + return MenuResDto.MenuDeleteDto.builder() + .deletedMenuIds(menuIds) + .build(); + } + +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/repository/MenuRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/repository/MenuRepository.java index b0335d2..80c6562 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/repository/MenuRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/repository/MenuRepository.java @@ -3,5 +3,8 @@ import com.eatsfine.eatsfine.domain.menu.entity.Menu; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MenuRepository extends JpaRepository { + Optional findByImageKey(String imageKey); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java new file mode 100644 index 0000000..c2a2067 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java @@ -0,0 +1,13 @@ +package com.eatsfine.eatsfine.domain.menu.service; + +import com.eatsfine.eatsfine.domain.menu.dto.MenuReqDto; +import com.eatsfine.eatsfine.domain.menu.dto.MenuResDto; +import org.springframework.web.multipart.MultipartFile; + +public interface MenuCommandService { + MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file); + MenuResDto.ImageDeleteDto deleteImage(Long storeId, String imageKey); + MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto menuCreateDto); + MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto menuDeleteDto); + +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java new file mode 100644 index 0000000..0c65bfe --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -0,0 +1,119 @@ +package com.eatsfine.eatsfine.domain.menu.service; + +import com.eatsfine.eatsfine.domain.image.exception.ImageException; +import com.eatsfine.eatsfine.domain.image.status.ImageErrorStatus; +import com.eatsfine.eatsfine.domain.menu.converter.MenuConverter; +import com.eatsfine.eatsfine.domain.menu.dto.MenuReqDto; +import com.eatsfine.eatsfine.domain.menu.dto.MenuResDto; +import com.eatsfine.eatsfine.domain.menu.entity.Menu; +import com.eatsfine.eatsfine.domain.menu.exception.MenuException; +import com.eatsfine.eatsfine.domain.menu.repository.MenuRepository; +import com.eatsfine.eatsfine.domain.menu.status.MenuErrorStatus; +import com.eatsfine.eatsfine.domain.store.entity.Store; +import com.eatsfine.eatsfine.domain.store.exception.StoreException; +import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; +import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.global.s3.S3Service; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional +public class MenuCommandServiceImpl implements MenuCommandService { + + private final S3Service s3Service; + private final StoreRepository storeRepository; + private final MenuRepository menuRepository; + + @Override + public MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto dto) { + Store store = findAndVerifyStore(storeId); + + List menus = dto.menus().stream() + .map(menuDto -> { + Menu menu = Menu.builder() + .name(menuDto.name()) + .description(menuDto.description()) + .price(menuDto.price()) + .menuCategory(menuDto.category()) + .imageKey(menuDto.imageKey()) + .build(); + store.addMenu(menu); + return menu; + }) + .toList(); + menuRepository.saveAll(menus); + + return MenuConverter.toCreateDto(menus); + } + + @Override + public MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto dto) { + Store store = findAndVerifyStore(storeId); + + List menuIds = dto.menuIds(); + List menusToDelete = menuRepository.findAllById(dto.menuIds()); + + if(menusToDelete.size() != menuIds.size()) { + throw new MenuException(MenuErrorStatus._MENU_NOT_FOUND); + } + + // 모든 메뉴가 해당 가게 소유인지 확인하고, 부모 컬렉션에서 제거 + menusToDelete.forEach(menu -> { + verifyMenuBelongsToStore(menu, storeId); + store.removeMenu(menu); + }); + + return MenuConverter.toDeleteDto(menuIds); + } + @Override + public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { + Store store = findAndVerifyStore(storeId); + + if(file.isEmpty()) { + throw new ImageException(ImageErrorStatus.EMPTY_FILE); + } + + String path = "stores/" + storeId + "/menus"; + String key = s3Service.upload(file, path); + + return MenuConverter.toImageUploadDto(key); + } + + @Override + public MenuResDto.ImageDeleteDto deleteImage(Long storeId, String imageKey) { + // TODO: [보안] Spring Security 병합 후, 현재 로그인한 사용자가 이 메뉴가 속한 가게의 주인인지 확인 + + // imageKey로 DB에서 메뉴 찾아봄 + Optional menuOptional = menuRepository.findByImageKey(imageKey); + + // 이미 등록 완료된 사진인 경우에는 db에서 null처리 + menuOptional.ifPresent(menu -> { + verifyMenuBelongsToStore(menu, storeId); + menu.updateImageKey(null); + }); + + s3Service.deleteByKey(imageKey); + return MenuConverter.toImageDeleteDto(imageKey); + } + + private Store findAndVerifyStore(Long storeId) { + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + // TODO: [보안] Spring Security 병합 후, 현재 로그인한 사용자가 이 가게의 주인인지 확인하는 로직 추가 + return store; + } + + private void verifyMenuBelongsToStore(Menu menu, Long storeId) { + if (!menu.getStore().getId().equals(storeId)) { + // 다른 가게의 메뉴를 조작하려는 시도 방지 + throw new StoreException(StoreErrorStatus._STORE_NOT_OWNER); + } + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java index b52dda2..0e0f455 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java @@ -13,7 +13,10 @@ public enum StoreErrorStatus implements BaseErrorCode { _STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "해당하는 가게를 찾을 수 없습니다."), _STORE_DETAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE_DETAIL404", "가게 상세 정보를 찾을 수 없습니다."), - _STORE_NOT_OPEN_ON_DAY(HttpStatus.NOT_FOUND,"STORE4041" , "해당 영업시간 정보를 찾을 수 없습니다."),; + _STORE_NOT_OPEN_ON_DAY(HttpStatus.NOT_FOUND,"STORE4041" , "해당 영업시간 정보를 찾을 수 없습니다."), + + _STORE_NOT_OWNER(HttpStatus.FORBIDDEN, "STORE403", "해당 가게의 주인이 아닙니다."), + ; private final HttpStatus httpStatus; From 1d501f392f7b5f4c3ef91630194ff2180ff6ad17 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 16:47:06 +0900 Subject: [PATCH 04/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java | 11 +++++++++++ .../eatsfine/eatsfine/domain/menu/dto/MenuResDto.java | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java index 1e6c8cd..14566a7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java @@ -40,4 +40,15 @@ public record MenuDeleteDto( @Size(min = 1, message = "삭제할 메뉴를 최소 1개 이상 선택해주세요.") List menuIds ){} + + public record MenuUpdateDto( + @Size(min = 1, message = "메뉴 이름은 1글자 이상이어야 합니다.") + String name, + @Size(max = 500, message = "설명은 500자 이내여야 합니다.") + String description, + @Min(value = 0, message = "가격은 0원 이상이어야 합니다.") + BigDecimal price, + MenuCategory category, + String imageKey + ){} } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java index 1688efe..a06c89a 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java @@ -40,4 +40,14 @@ public record MenuDto( public record MenuDeleteDto( List deletedMenuIds ){} + + @Builder + public record MenuUpdateDto( + Long menuId, + String name, + String description, + BigDecimal price, + MenuCategory category, + String imageKey + ){} } From baf00a3cf00b7bd42d47a8af4da62c8b1623cfb0 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 16:48:22 +0900 Subject: [PATCH 05/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=84=B1=EA=B3=B5=20=EC=9D=91=EB=8B=B5=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java index 946a551..533d31b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java @@ -17,8 +17,8 @@ public enum MenuSuccessStatus implements BaseCode { _MENU_CREATE_SUCCESS(HttpStatus.CREATED, "MENU202", "메뉴 생성 성공"), - _MENU_DELETE_SUCCESS(HttpStatus.OK, "MENU2002", "메뉴 삭제 성공"), + _MENU_UPDATE_SUCCESS(HttpStatus.OK, "MENU2003", "메뉴 수정 성공"), ; From dc2906aa3ed981cde4cf454375496e6266bfe486 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 16:48:42 +0900 Subject: [PATCH 06/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/controller/MenuController.java | 14 +++++++-- .../domain/menu/converter/MenuConverter.java | 12 ++++++++ .../eatsfine/domain/menu/entity/Menu.java | 17 +++++++++++ .../menu/service/MenuCommandService.java | 1 + .../menu/service/MenuCommandServiceImpl.java | 30 +++++++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java index 8e61df7..0d4600c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java @@ -46,7 +46,7 @@ public ApiResponse deleteImage( return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_DELETE_SUCCESS, menuCommandService.deleteImage(storeId,imageKey)); } - @Operation(summary = "메뉴 등록 API", description = "가게의 메뉴를 등록합니다.") + @Operation(summary = "메뉴 등록 API", description = "가게의 메뉴들을 등록합니다.") @PostMapping("/stores/{storeId}/menus") public ApiResponse createMenus( @PathVariable Long storeId, @@ -55,7 +55,7 @@ public ApiResponse createMenus( return ApiResponse.of(MenuSuccessStatus._MENU_CREATE_SUCCESS, menuCommandService.createMenus(storeId, dto)); } - @Operation(summary = "메뉴 삭제 API", description = "가게의 메뉴를 삭제합니다.") + @Operation(summary = "메뉴 삭제 API", description = "가게의 메뉴들을 삭제합니다.") @DeleteMapping("/stores/{storeId}/menus") public ApiResponse deleteMenus( @PathVariable Long storeId, @@ -64,4 +64,14 @@ public ApiResponse deleteMenus( return ApiResponse.of(MenuSuccessStatus._MENU_DELETE_SUCCESS, menuCommandService.deleteMenus(storeId, dto)); } + @Operation(summary = "메뉴 수정 API", description = "가게의 메뉴를 수정합니다.") + @PatchMapping("/stores/{storeId}/menus/{menuId}") + public ApiResponse updateMenu( + @PathVariable Long storeId, + @PathVariable Long menuId, + @RequestBody @Valid MenuReqDto.MenuUpdateDto dto + ) { + return ApiResponse.of(MenuSuccessStatus._MENU_UPDATE_SUCCESS, menuCommandService.updateMenu(storeId, menuId, dto)); + } + } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java index dff0767..dccacd1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java @@ -44,4 +44,16 @@ public static MenuResDto.MenuDeleteDto toDeleteDto(List menuIds){ .build(); } + public static MenuResDto.MenuUpdateDto toUpdateDto(Menu menu){ + return MenuResDto.MenuUpdateDto.builder() + .menuId(menu.getId()) + .name(menu.getName()) + .description(menu.getDescription()) + .price(menu.getPrice()) + .category(menu.getMenuCategory()) + .imageKey(menu.getImageKey()) + .build(); + + } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java index 6a3f317..8cbe25b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java @@ -58,4 +58,21 @@ public void updateImageKey(String imageKey) { this.imageKey = imageKey; } + // --- 메뉴 정보 수정 메서드 --- + + public void updateName(String name) { + this.name = name; + } + + public void updateDescription(String description) { + this.description = description; + } + + public void updatePrice(BigDecimal price) { + this.price = price; + } + + public void updateCategory(MenuCategory menuCategory) { + this.menuCategory = menuCategory; + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java index c2a2067..cb70a65 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java @@ -9,5 +9,6 @@ public interface MenuCommandService { MenuResDto.ImageDeleteDto deleteImage(Long storeId, String imageKey); MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto menuCreateDto); MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto menuDeleteDto); + MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto menuUpdateDto); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index 0c65bfe..297ba95 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -72,6 +72,36 @@ public MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteD return MenuConverter.toDeleteDto(menuIds); } + + @Override + public MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto dto) { + Store store = findAndVerifyStore(storeId); + + // TODO: [보안] Spring Security 병합 후, 현재 로그인한 사용자가 이 가게의 주인인지 확인하는 로직 추가 + + Menu menu = menuRepository.findById(menuId) + .orElseThrow(() -> new MenuException(MenuErrorStatus._MENU_NOT_FOUND)); + + verifyMenuBelongsToStore(menu, storeId); + + // 이름, 설명, 가격, 카테고리 업데이트 + Optional.ofNullable(dto.name()).ifPresent(menu::updateName); + Optional.ofNullable(dto.description()).ifPresent(menu::updateDescription); + Optional.ofNullable(dto.price()).ifPresent(menu::updatePrice); + Optional.ofNullable(dto.category()).ifPresent(menu::updateCategory); + + Optional.ofNullable(dto.imageKey()).ifPresent(newImageKey -> { + // 기존 이미지가 있다면 S3에서 삭제 + if(menu.getImageKey() != null && !menu.getImageKey().isBlank()) { + s3Service.deleteByKey(menu.getImageKey()); + } + // 새로운 이미지 키로 업데이트 + menu.updateImageKey(newImageKey); + }); + + return MenuConverter.toUpdateDto(menu); + } + @Override public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { Store store = findAndVerifyStore(storeId); From 228996b7fe93613898810a47c2fb29d44033281e Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 16:56:25 +0900 Subject: [PATCH 07/27] =?UTF-8?q?[REFACTOR]:=20S3=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=84=B8=EB=B6=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/image/status/ImageErrorStatus.java | 4 +++- .../java/com/eatsfine/eatsfine/global/s3/S3Service.java | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java index f200815..44179c5 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java @@ -12,7 +12,9 @@ public enum ImageErrorStatus implements BaseErrorCode { EMPTY_FILE(HttpStatus.BAD_REQUEST, "IMAGE4001", "업로드할 파일이 비어 있습니다."), INVALID_FILE_TYPE(HttpStatus.BAD_REQUEST, "IMAGE4002", "지원하지 않는 파일 형식입니다."), S3_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "IMAGE5001", "이미지 업로드에 실패했습니다."), - _IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "IMAGE404", "해당하는 이미지가 존재하지 않습니다.") + _IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "IMAGE404", "해당하는 이미지가 존재하지 않습니다."), + _INVALID_IMAGE_KEY(HttpStatus.BAD_REQUEST, "IMAGE4003", "유효하지 않은 이미지 키입니다."), + _INVALID_S3_DIRECTORY(HttpStatus.BAD_REQUEST, "IMAGE4004", "유효하지 않은 S3 디렉토리입니다."), ; diff --git a/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java b/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java index 774fc36..9df76c1 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java +++ b/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java @@ -46,7 +46,9 @@ public String upload(MultipartFile file, String directory) { } public void deleteByKey(String key) { - if (key == null || key.isBlank()) return; + if (key == null || key.isBlank()) { + throw new ImageException(ImageErrorStatus._INVALID_IMAGE_KEY); + } s3Client.deleteObject(DeleteObjectRequest.builder() .bucket(bucket) @@ -61,7 +63,7 @@ public String toUrl(String key) { private String generateKey(MultipartFile file, String directory) { if(directory == null || directory.isBlank()) { - throw new IllegalArgumentException("S3 디렉토리는 비어있을 수 없습니다."); + throw new ImageException(ImageErrorStatus._INVALID_S3_DIRECTORY); } String extension = extractExtension(file.getOriginalFilename()); return directory + "/" + UUID.randomUUID() + extension; From d3a2389b0f69b8f9861ccab98e0e08348ee2a7ba Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 17:21:44 +0900 Subject: [PATCH 08/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20DTO=20=EB=B0=8F=20=EC=84=B1=EA=B3=B5=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/menu/dto/MenuResDto.java | 16 ++++++++++++++++ .../domain/menu/status/MenuSuccessStatus.java | 1 + 2 files changed, 17 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java index a06c89a..9820f50 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java @@ -50,4 +50,20 @@ public record MenuUpdateDto( MenuCategory category, String imageKey ){} + + @Builder + public record MenuListDto( + List menus + ){} + + @Builder + public record MenuDetailDto( + Long menuId, + String name, + String description, + BigDecimal price, + MenuCategory category, + String imageUrl, + boolean isSoldOut + ){} } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java index 533d31b..69c6b71 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java @@ -19,6 +19,7 @@ public enum MenuSuccessStatus implements BaseCode { _MENU_CREATE_SUCCESS(HttpStatus.CREATED, "MENU202", "메뉴 생성 성공"), _MENU_DELETE_SUCCESS(HttpStatus.OK, "MENU2002", "메뉴 삭제 성공"), _MENU_UPDATE_SUCCESS(HttpStatus.OK, "MENU2003", "메뉴 수정 성공"), + _MENU_LIST_SUCCESS(HttpStatus.OK, "MENU2004", "메뉴 조회 성공"), ; From b23b8207af3ac032aeb3f3e64df92f6bd2aa250d Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 17:22:33 +0900 Subject: [PATCH 09/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/controller/MenuController.java | 10 +++++ .../domain/menu/service/MenuQueryService.java | 7 +++ .../menu/service/MenuQueryServiceImpl.java | 45 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryService.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java index 0d4600c..fbc13d5 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java @@ -3,6 +3,7 @@ import com.eatsfine.eatsfine.domain.menu.dto.MenuReqDto; import com.eatsfine.eatsfine.domain.menu.dto.MenuResDto; import com.eatsfine.eatsfine.domain.menu.service.MenuCommandService; +import com.eatsfine.eatsfine.domain.menu.service.MenuQueryService; import com.eatsfine.eatsfine.domain.menu.status.MenuSuccessStatus; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -20,6 +21,7 @@ public class MenuController { private final MenuCommandService menuCommandService; + private final MenuQueryService menuQueryService; @Operation(summary = "메뉴 이미지 선 업로드 API", description = "메뉴 등록 전에 이미지를 먼저 업로드하고 KEY를 반환합니다.") @PostMapping(value = "/stores/{storeId}/menus/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -74,4 +76,12 @@ public ApiResponse updateMenu( return ApiResponse.of(MenuSuccessStatus._MENU_UPDATE_SUCCESS, menuCommandService.updateMenu(storeId, menuId, dto)); } + @Operation(summary = "메뉴 조회 API", description = "가게의 메뉴들을 조회합니다.") + @GetMapping("/stores/{storeId}/menus") + public ApiResponse getMenus( + @PathVariable Long storeId + ) { + return ApiResponse.of(MenuSuccessStatus._MENU_LIST_SUCCESS, menuQueryService.getMenus(storeId)); + + } } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryService.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryService.java new file mode 100644 index 0000000..f5c80cb --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryService.java @@ -0,0 +1,7 @@ +package com.eatsfine.eatsfine.domain.menu.service; + +import com.eatsfine.eatsfine.domain.menu.dto.MenuResDto; + +public interface MenuQueryService { + MenuResDto.MenuListDto getMenus(Long storeId); +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java new file mode 100644 index 0000000..b5a23ab --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java @@ -0,0 +1,45 @@ +package com.eatsfine.eatsfine.domain.menu.service; + +import com.eatsfine.eatsfine.domain.menu.dto.MenuResDto; +import com.eatsfine.eatsfine.domain.store.dto.StoreResDto; +import com.eatsfine.eatsfine.domain.store.entity.Store; +import com.eatsfine.eatsfine.domain.store.exception.StoreException; +import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; +import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.global.s3.S3Service; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MenuQueryServiceImpl implements MenuQueryService { + private final StoreRepository storeRepository; + private final S3Service s3Service; + + @Override + public MenuResDto.MenuListDto getMenus(Long storeId) { + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + + List menuDtos = store.getMenus().stream() + .map(menu -> MenuResDto.MenuDetailDto.builder() + .menuId(menu.getId()) + .name(menu.getName()) + .description(menu.getDescription()) + .price(menu.getPrice()) + .category(menu.getMenuCategory()) + .imageUrl(s3Service.toUrl(menu.getImageKey())) + .isSoldOut(menu.isSoldOut()) + .build() + ) + .toList(); + + return MenuResDto.MenuListDto.builder() + .menus(menuDtos) + .build(); + } +} From 80077721605f1850a246b395ac192bfa9ffdb71f Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 20:26:19 +0900 Subject: [PATCH 10/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20N+1=20=EB=B0=A9=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20fetch=20join=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/service/MenuQueryServiceImpl.java | 2 +- .../domain/store/repository/StoreRepository.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java index b5a23ab..ddb0e54 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuQueryServiceImpl.java @@ -22,7 +22,7 @@ public class MenuQueryServiceImpl implements MenuQueryService { @Override public MenuResDto.MenuListDto getMenus(Long storeId) { - Store store = storeRepository.findById(storeId) + Store store = storeRepository.findByIdWithMenus(storeId) .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); List menuDtos = store.getMenus().stream() diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java index 7c72fd3..7d6a3e4 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java @@ -2,7 +2,18 @@ import com.eatsfine.eatsfine.domain.store.entity.Store; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface StoreRepository extends JpaRepository, StoreRepositoryCustom { + + @Query(""" + select s from Store s left join fetch s.menus where s.id = :id + +""") + Optional findByIdWithMenus(@Param("id") Long id); + } \ No newline at end of file From 1f920ddf94e025460df642502b8370b42a4d4e41 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 20:26:54 +0900 Subject: [PATCH 11/27] =?UTF-8?q?[FEAT]:=20=ED=92=88=EC=A0=88=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EB=B3=80=EA=B2=BD=20DTO=20=EB=B0=8F=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20=EC=9D=91=EB=8B=B5=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java | 5 +++++ .../com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java | 6 ++++++ .../eatsfine/domain/menu/status/MenuSuccessStatus.java | 1 + 3 files changed, 12 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java index 14566a7..dc702f1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java @@ -51,4 +51,9 @@ public record MenuUpdateDto( MenuCategory category, String imageKey ){} + + public record SoldOutUpdateDto( + @NotNull + boolean isSoldOut + ){} } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java index 9820f50..35156ab 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java @@ -51,6 +51,12 @@ public record MenuUpdateDto( String imageKey ){} + @Builder + public record SoldOutUpdateDto( + Long menuId, + boolean isSoldOut + ){} + @Builder public record MenuListDto( List menus diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java index 69c6b71..1a696be 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/status/MenuSuccessStatus.java @@ -19,6 +19,7 @@ public enum MenuSuccessStatus implements BaseCode { _MENU_CREATE_SUCCESS(HttpStatus.CREATED, "MENU202", "메뉴 생성 성공"), _MENU_DELETE_SUCCESS(HttpStatus.OK, "MENU2002", "메뉴 삭제 성공"), _MENU_UPDATE_SUCCESS(HttpStatus.OK, "MENU2003", "메뉴 수정 성공"), + _SOLD_OUT_UPDATE_SUCCESS(HttpStatus.OK, "MENU2005", "품절 여부 변경 성공"), _MENU_LIST_SUCCESS(HttpStatus.OK, "MENU2004", "메뉴 조회 성공"), ; From 56c9214831c2351ae756a2e12544e65314159550 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 20:27:26 +0900 Subject: [PATCH 12/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=ED=92=88?= =?UTF-8?q?=EC=A0=88=EC=97=AC=EB=B6=80=20=EB=B3=80=EA=B2=BD=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/controller/MenuController.java | 10 ++++++++++ .../domain/menu/converter/MenuConverter.java | 7 +++++++ .../domain/menu/service/MenuCommandService.java | 1 + .../menu/service/MenuCommandServiceImpl.java | 15 +++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java index fbc13d5..4673f26 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java @@ -76,6 +76,16 @@ public ApiResponse updateMenu( return ApiResponse.of(MenuSuccessStatus._MENU_UPDATE_SUCCESS, menuCommandService.updateMenu(storeId, menuId, dto)); } + @Operation(summary = "품절 여부 변경 API", description = "메뉴의 품절 여부를 변경합니다.") + @PatchMapping("/stores/{storeId}/menus/{menuId}/sold-out") + public ApiResponse updateSoldOutStatus( + @PathVariable Long storeId, + @PathVariable Long menuId, + @RequestBody @Valid MenuReqDto.SoldOutUpdateDto dto + ){ + return ApiResponse.of(MenuSuccessStatus._SOLD_OUT_UPDATE_SUCCESS, menuCommandService.updateSoldOutStatus(storeId, menuId, dto.isSoldOut())); + } + @Operation(summary = "메뉴 조회 API", description = "가게의 메뉴들을 조회합니다.") @GetMapping("/stores/{storeId}/menus") public ApiResponse getMenus( diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java index dccacd1..d189f15 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java @@ -56,4 +56,11 @@ public static MenuResDto.MenuUpdateDto toUpdateDto(Menu menu){ } + public static MenuResDto.SoldOutUpdateDto toSoldOutUpdateDto(Menu menu){ + return MenuResDto.SoldOutUpdateDto.builder() + .menuId(menu.getId()) + .isSoldOut(menu.isSoldOut()) + .build(); + } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java index cb70a65..e1f5423 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java @@ -10,5 +10,6 @@ public interface MenuCommandService { MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto menuCreateDto); MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto menuDeleteDto); MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto menuUpdateDto); + MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId, boolean isSoldOut); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index 297ba95..1b14c0c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -102,6 +102,21 @@ public MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto return MenuConverter.toUpdateDto(menu); } + @Override + public MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId, boolean isSoldOut) { + findAndVerifyStore(storeId); + + Menu menu = menuRepository.findById(menuId) + .orElseThrow(() -> new MenuException(MenuErrorStatus._MENU_NOT_FOUND)); + + verifyMenuBelongsToStore(menu, storeId); + + menu.updateSoldOut(isSoldOut); + + return MenuConverter.toSoldOutUpdateDto(menu); + + } + @Override public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { Store store = findAndVerifyStore(storeId); From 5892557e88450af3e08366994a8d0ee9c3ded4dc Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 21:12:16 +0900 Subject: [PATCH 13/27] =?UTF-8?q?[FEAT]:=20=EB=A9=94=EB=89=B4=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8B=9C=20soft=20delete=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/menu/entity/Menu.java | 7 +++++++ .../menu/service/MenuCommandServiceImpl.java | 14 ++++++++++++-- .../eatsfine/domain/store/entity/Store.java | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java index 8cbe25b..908b250 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java @@ -5,8 +5,11 @@ import com.eatsfine.eatsfine.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; import java.math.BigDecimal; +import java.time.LocalDateTime; @Entity @Getter @@ -14,6 +17,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Table(name = "menu") +@SQLDelete(sql = "UPDATE menu SET deleted_at = NOW() WHERE id = ?") +@Where(clause = "deleted_at IS NULL") public class Menu extends BaseEntity { @Id @@ -44,6 +49,8 @@ public class Menu extends BaseEntity { @Column(name = "is_sold_out", nullable = false) private boolean isSoldOut = false; + private LocalDateTime deletedAt; + public void assignStore(Store store) { this.store = store; } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index 1b14c0c..8c1dcdb 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -64,12 +64,22 @@ public MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteD throw new MenuException(MenuErrorStatus._MENU_NOT_FOUND); } - // 모든 메뉴가 해당 가게 소유인지 확인하고, 부모 컬렉션에서 제거 + // 1. 모든 메뉴가 해당 가게 소유인지 확인하고, S3 이미지 삭제 menusToDelete.forEach(menu -> { verifyMenuBelongsToStore(menu, storeId); - store.removeMenu(menu); + // Soft Delete 시 연결된 S3 이미지도 함께 삭제 + if (menu.getImageKey() != null && !menu.getImageKey().isBlank()) { + s3Service.deleteByKey(menu.getImageKey()); + } }); + // 2. DB에서 Soft Delete 실행 + // Menu 엔티티의 @SQLDelete 어노테이션 덕분에 deleteAll이 UPDATE로 동작함 + menuRepository.deleteAll(menusToDelete); + + // 3. Store 컬렉션에서 제거 + store.getMenus().removeAll(menusToDelete); + return MenuConverter.toDeleteDto(menuIds); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index 286fa6c..466c7af 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -95,7 +95,7 @@ public class Store extends BaseEntity { @Builder.Default @BatchSize(size = 100) - @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) private List menus = new ArrayList<>(); From c15f24d2049675a7c79f1e7571cf5727cbd1d862 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 21:13:20 +0900 Subject: [PATCH 14/27] =?UTF-8?q?[FEAT]:=20@Valid,=20@RequestParam,=20@Pat?= =?UTF-8?q?hVaraible=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GeneralExceptionAdvice.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java index 0c01904..7b2212f 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -4,11 +4,13 @@ import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; +import jakarta.validation.ConstraintViolationException; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.ServletWebRequest; @@ -34,6 +36,26 @@ public ResponseEntity exception(Exception e, WebRequest request) { return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), request, e.getMessage()); } + // @Valid 유효성 검사 실패 시 (RequestBody) + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + return handleExceptionInternalFalse(e, ErrorStatus._BAD_REQUEST, headers, status, request, errorMessage); + } + + // @RequestParam, @PathVariable 유효성 검사 실패 시 (ConstraintViolationException) + @ExceptionHandler + public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + // 첫 번째 에러 메시지만 추출 + String errorMessage = e.getConstraintViolations().iterator().next().getMessage(); + return handleExceptionInternalFalse(e, ErrorStatus._BAD_REQUEST, HttpHeaders.EMPTY, ErrorStatus._BAD_REQUEST.getHttpStatus(), request, errorMessage); + } + // 3. 커스텀 예외용 내부 응답 생성 메서드 private ResponseEntity handleExceptionInternal(Exception e, BaseErrorCode code, HttpHeaders headers, HttpServletRequest request) { // 정의하신 ApiResponse.onFailure(BaseErrorCode code, T result)를 호출합니다. From 336dd3c4b0f6a2a1385dcc4a30db239045975d83 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Thu, 29 Jan 2026 23:52:07 +0900 Subject: [PATCH 15/27] =?UTF-8?q?[FEAT]:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=84=A0=20=EC=97=85=EB=A1=9C=EB=93=9C=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20S3=20=EC=88=98=EB=AA=85=20=EC=A3=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/service/MenuCommandServiceImpl.java | 52 +++++++++++++++---- .../eatsfine/global/s3/S3Service.java | 20 ++++++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index 8c1dcdb..d7b5db2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; @Service @RequiredArgsConstructor @@ -42,8 +43,22 @@ public MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateD .description(menuDto.description()) .price(menuDto.price()) .menuCategory(menuDto.category()) - .imageKey(menuDto.imageKey()) .build(); + + // 임시 이미지 키가 있는 경우, 영구 경로로 이동하고 키를 설정 + String tempImageKey = menuDto.imageKey(); + if (tempImageKey != null && !tempImageKey.isBlank()) { + // 1. 새로운 영구 키 생성 + String extension = s3Service.extractExtension(tempImageKey); + String permanentImageKey = "stores/" + storeId + "/menus/" + UUID.randomUUID() + extension; + + // 2. S3에서 객체 이동 (임시 -> 영구) + s3Service.moveObject(tempImageKey, permanentImageKey); + + // 3. 엔티티에 영구 키 저장 + menu.updateImageKey(permanentImageKey); + } + store.addMenu(menu); return menu; }) @@ -101,12 +116,30 @@ public MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto Optional.ofNullable(dto.category()).ifPresent(menu::updateCategory); Optional.ofNullable(dto.imageKey()).ifPresent(newImageKey -> { - // 기존 이미지가 있다면 S3에서 삭제 - if(menu.getImageKey() != null && !menu.getImageKey().isBlank()) { - s3Service.deleteByKey(menu.getImageKey()); + // 1. [Safety] 변경된 내용이 없으면 스킵 (프론트에서 기존 키를 그대로 보낸 경우) + if (newImageKey.equals(menu.getImageKey())) { + return; + } + + // 2. 새로운 이미지가 있다면 영구 경로로 이동 (Temp -> Perm) + if (newImageKey != null && !newImageKey.isBlank()) { + String extension = s3Service.extractExtension(newImageKey); + String permanentImageKey = "stores/" + storeId + "/menus/" + UUID.randomUUID() + extension; + s3Service.moveObject(newImageKey, permanentImageKey); + + // 3. 이동 성공 후, 기존 이미지가 있다면 삭제 (Old Perm -> Delete) + if (menu.getImageKey() != null && !menu.getImageKey().isBlank()) { + s3Service.deleteByKey(menu.getImageKey()); + } + + menu.updateImageKey(permanentImageKey); + } else { + // 4. 빈 문자열("")인 경우 -> 이미지 삭제 요청 + if (menu.getImageKey() != null && !menu.getImageKey().isBlank()) { + s3Service.deleteByKey(menu.getImageKey()); + } + menu.updateImageKey(null); } - // 새로운 이미지 키로 업데이트 - menu.updateImageKey(newImageKey); }); return MenuConverter.toUpdateDto(menu); @@ -135,10 +168,9 @@ public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { throw new ImageException(ImageErrorStatus.EMPTY_FILE); } - String path = "stores/" + storeId + "/menus"; - String key = s3Service.upload(file, path); - - return MenuConverter.toImageUploadDto(key); + // 이미지를 항상 임시 경로에 업로드 + String tempPath = "temp/menus"; + return MenuConverter.toImageUploadDto(s3Service.upload(file, tempPath)); } @Override diff --git a/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java b/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java index 9df76c1..ea8d620 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java +++ b/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java @@ -9,6 +9,7 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import java.io.IOException; @@ -56,6 +57,23 @@ public void deleteByKey(String key) { .build()); } + public void moveObject(String sourceKey, String destinationKey) { + if (sourceKey == null || destinationKey == null || sourceKey.isBlank() || destinationKey.isBlank()) { + throw new ImageException(ImageErrorStatus._INVALID_IMAGE_KEY); + } + + // 1. 객체 복사 + s3Client.copyObject(CopyObjectRequest.builder() + .sourceBucket(bucket) + .sourceKey(sourceKey) + .destinationBucket(bucket) + .destinationKey(destinationKey) + .build()); + + // 2. 원본(임시) 객체 삭제 + deleteByKey(sourceKey); + } + public String toUrl(String key) { if (key == null || key.isBlank()) return null; return baseUrl + "/" + key; @@ -69,7 +87,7 @@ private String generateKey(MultipartFile file, String directory) { return directory + "/" + UUID.randomUUID() + extension; } - private String extractExtension(String filename) { + public String extractExtension(String filename) { // public으로 변경 if (filename == null || !filename.contains(".")) { throw new ImageException(ImageErrorStatus.INVALID_FILE_TYPE); } From acccee29c4bbbc056590b31e2787b93d8011ddf8 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 01:08:56 +0900 Subject: [PATCH 16/27] =?UTF-8?q?[REFACTOR]:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20Url=20=EB=A6=AC=ED=84=B4=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/converter/MenuConverter.java | 20 ++++------------ .../eatsfine/domain/menu/dto/MenuReqDto.java | 2 +- .../eatsfine/domain/menu/dto/MenuResDto.java | 8 +++---- .../menu/service/MenuCommandServiceImpl.java | 24 +++++++++++++++---- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java index d189f15..702c318 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/converter/MenuConverter.java @@ -8,9 +8,10 @@ public class MenuConverter { - public static MenuResDto.ImageUploadDto toImageUploadDto(String imageKey){ + public static MenuResDto.ImageUploadDto toImageUploadDto(String imageKey, String imageUrl){ return MenuResDto.ImageUploadDto.builder() .imageKey(imageKey) + .imageUrl(imageUrl) .build(); } @@ -20,19 +21,8 @@ public static MenuResDto.ImageDeleteDto toImageDeleteDto(String imageKey) { .build(); } - public static MenuResDto.MenuCreateDto toCreateDto(List menus){ - List menuDtos = menus.stream() - .map(menu -> MenuResDto.MenuDto.builder() - .menuId(menu.getId()) - .name(menu.getName()) - .description(menu.getDescription()) - .price(menu.getPrice()) - .category(menu.getMenuCategory()) - .imageKey(menu.getImageKey()) - .build() - ) - .toList(); + public static MenuResDto.MenuCreateDto toCreateDto(List menuDtos) { return MenuResDto.MenuCreateDto.builder() .menus(menuDtos) .build(); @@ -44,14 +34,14 @@ public static MenuResDto.MenuDeleteDto toDeleteDto(List menuIds){ .build(); } - public static MenuResDto.MenuUpdateDto toUpdateDto(Menu menu){ + public static MenuResDto.MenuUpdateDto toUpdateDto(Menu menu, String updatedImageUrl){ return MenuResDto.MenuUpdateDto.builder() .menuId(menu.getId()) .name(menu.getName()) .description(menu.getDescription()) .price(menu.getPrice()) .category(menu.getMenuCategory()) - .imageKey(menu.getImageKey()) + .imageUrl(updatedImageUrl) .build(); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java index dc702f1..bd733fa 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuReqDto.java @@ -54,6 +54,6 @@ public record MenuUpdateDto( public record SoldOutUpdateDto( @NotNull - boolean isSoldOut + Boolean isSoldOut ){} } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java index 35156ab..0a8b419 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/dto/MenuResDto.java @@ -11,7 +11,8 @@ public class MenuResDto { @Builder public record ImageUploadDto( - String imageKey + String imageKey, // 메뉴 등록/수정 시 서버에 다시 보낼 키 + String imageUrl // 프론트엔드에서 즉시 미리보기를 위한 전체 URL ){} @@ -32,8 +33,7 @@ public record MenuDto( String description, BigDecimal price, MenuCategory category, - String imageKey - + String imageUrl ){} @Builder @@ -48,7 +48,7 @@ public record MenuUpdateDto( String description, BigDecimal price, MenuCategory category, - String imageKey + String imageUrl ){} @Builder diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index d7b5db2..2404a90 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -63,9 +63,21 @@ public MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateD return menu; }) .toList(); - menuRepository.saveAll(menus); - return MenuConverter.toCreateDto(menus); + List savedMenus = menuRepository.saveAll(menus); + + List menuDtos = savedMenus.stream().map( + menu -> MenuResDto.MenuDto.builder() + .menuId(menu.getId()) + .name(menu.getName()) + .description(menu.getDescription()) + .price(menu.getPrice()) + .category(menu.getMenuCategory()) + .imageUrl(s3Service.toUrl(menu.getImageKey())) + .build()) + .toList(); + + return MenuConverter.toCreateDto(menuDtos); } @Override @@ -142,7 +154,9 @@ public MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto } }); - return MenuConverter.toUpdateDto(menu); + String updatedImageUrl = s3Service.toUrl(menu.getImageKey()); + + return MenuConverter.toUpdateDto(menu, updatedImageUrl); } @Override @@ -170,7 +184,9 @@ public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { // 이미지를 항상 임시 경로에 업로드 String tempPath = "temp/menus"; - return MenuConverter.toImageUploadDto(s3Service.upload(file, tempPath)); + String imageKey = s3Service.upload(file, tempPath); + + return MenuConverter.toImageUploadDto(imageKey, s3Service.toUrl(imageKey)); } @Override From 61ec0113fb6207541d9460695869dcc4da9a295e Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 01:10:32 +0900 Subject: [PATCH 17/27] =?UTF-8?q?[FEAT]:=20=ED=92=88=EC=A0=88=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=EC=9D=B4=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=EA=B3=BC=20=EB=8F=99=EC=9D=BC=ED=95=98?= =?UTF-8?q?=EB=8B=A4=EB=A9=B4=20=EB=B0=94=EB=A1=9C=20=EB=A6=AC=ED=84=B4?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/menu/service/MenuCommandServiceImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index 2404a90..35647c9 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -168,6 +168,11 @@ public MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId verifyMenuBelongsToStore(menu, storeId); + // 기존 값과 동일하다면 바로 리턴 + if(menu.isSoldOut() == isSoldOut) { + return MenuConverter.toSoldOutUpdateDto(menu); + } + menu.updateSoldOut(isSoldOut); return MenuConverter.toSoldOutUpdateDto(menu); From 15e29810d7af59150a224bc1c3fc6addbf083bed Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 02:15:35 +0900 Subject: [PATCH 18/27] =?UTF-8?q?[REFACTOR]:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20API=EB=A5=BC=20=EC=9D=B4=EB=AF=B8=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EB=90=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EB=8A=94=20=EA=B2=83=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=97=AD=ED=95=A0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/controller/MenuController.java | 25 +++++++---------- .../menu/service/MenuCommandService.java | 2 +- .../menu/service/MenuCommandServiceImpl.java | 27 ++++++++++++------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java index 4673f26..cfc3a73 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java @@ -32,22 +32,6 @@ public ApiResponse uploadImage( return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_UPLOAD_SUCCESS, menuCommandService.uploadImage(storeId, file)); } - - @Operation(summary = "메뉴 이미지 삭제 API", description = """ - 메뉴 이미지를 삭제합니다. 이 API는 아래 두가지 시나리오 모두 처리합니다. - 1. 메뉴 등록 전: 메뉴 등록하다가 업로드만 된 이미지(고아 이미지)를 S3에서 삭제합니다. (가게 등록 전 이미지를 먼저 업로드하기 때문) - 2. 메뉴 등록 후: DB에 연결된 메뉴의 이미지를 S3에서 삭제하고, DB의 imageKey도 null로 업데이트합니다. - - """) - @DeleteMapping("/stores/{storeId}/menus/images") - public ApiResponse deleteImage( - @PathVariable Long storeId, - @RequestParam("key") String imageKey - ) { - - return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_DELETE_SUCCESS, menuCommandService.deleteImage(storeId,imageKey)); - } - @Operation(summary = "메뉴 등록 API", description = "가게의 메뉴들을 등록합니다.") @PostMapping("/stores/{storeId}/menus") public ApiResponse createMenus( @@ -86,6 +70,15 @@ public ApiResponse updateSoldOutStatus( return ApiResponse.of(MenuSuccessStatus._SOLD_OUT_UPDATE_SUCCESS, menuCommandService.updateSoldOutStatus(storeId, menuId, dto.isSoldOut())); } + @Operation(summary = "등록된 메뉴 이미지 삭제 API", description = "이미 등록된 메뉴의 이미지를 삭제합니다.") + @DeleteMapping("/stores/{storeId}/menus/{menuId}/image") + public ApiResponse deleteMenuImage( + @PathVariable Long storeId, + @PathVariable Long menuId + ) { + return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_DELETE_SUCCESS, menuCommandService.deleteMenuImage(storeId, menuId)); + } + @Operation(summary = "메뉴 조회 API", description = "가게의 메뉴들을 조회합니다.") @GetMapping("/stores/{storeId}/menus") public ApiResponse getMenus( diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java index e1f5423..862e407 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java @@ -6,7 +6,7 @@ public interface MenuCommandService { MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file); - MenuResDto.ImageDeleteDto deleteImage(Long storeId, String imageKey); + MenuResDto.ImageDeleteDto deleteMenuImage(Long storeId, Long menuId); MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto menuCreateDto); MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto menuDeleteDto); MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto menuUpdateDto); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index 35647c9..e07eb8c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -195,20 +195,27 @@ public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { } @Override - public MenuResDto.ImageDeleteDto deleteImage(Long storeId, String imageKey) { - // TODO: [보안] Spring Security 병합 후, 현재 로그인한 사용자가 이 메뉴가 속한 가게의 주인인지 확인 + public MenuResDto.ImageDeleteDto deleteMenuImage(Long storeId, Long menuId) { + findAndVerifyStore(storeId); - // imageKey로 DB에서 메뉴 찾아봄 - Optional menuOptional = menuRepository.findByImageKey(imageKey); + Menu menu = menuRepository.findById(menuId) + .orElseThrow(() -> new MenuException(MenuErrorStatus._MENU_NOT_FOUND)); - // 이미 등록 완료된 사진인 경우에는 db에서 null처리 - menuOptional.ifPresent(menu -> { - verifyMenuBelongsToStore(menu, storeId); - menu.updateImageKey(null); - }); + verifyMenuBelongsToStore(menu, storeId); + + String imageKey = menu.getImageKey(); + if (imageKey == null || imageKey.isBlank()) { + // 이미지가 없는 메뉴에 삭제 요청이 온 경우, 예외 + throw new ImageException(ImageErrorStatus._IMAGE_NOT_FOUND); + } + + // 1. S3에서 파일 삭제 s3Service.deleteByKey(imageKey); - return MenuConverter.toImageDeleteDto(imageKey); + // 2. DB에서 imageKey를 null로 업데이트 (Dirty Checking) + menu.updateImageKey(null); + + return MenuConverter.toImageDeleteDto(imageKey); // 삭제된 이미지의 키를 반환 } private Store findAndVerifyStore(Long storeId) { From bfc0d401a6336d5002631cd78de19f03d6997666 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 02:38:06 +0900 Subject: [PATCH 19/27] =?UTF-8?q?[REFACTOR]:=20soft=20delete=EB=90=9C=20Me?= =?UTF-8?q?nu=EB=8A=94=20=EC=95=88=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/store/repository/StoreRepository.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java index 7d6a3e4..88b0b94 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java @@ -11,7 +11,10 @@ public interface StoreRepository extends JpaRepository, StoreRepositoryCustom { @Query(""" - select s from Store s left join fetch s.menus where s.id = :id + select s from Store s + left join fetch s.menus m + where s.id = :id + and m.deletedAt IS NULL """) Optional findByIdWithMenus(@Param("id") Long id); From 36a98f45ac97263d21ccbd6693cc9459e3d0abb6 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 02:38:34 +0900 Subject: [PATCH 20/27] =?UTF-8?q?[REFACTOR]:=20@Where=20->=20@SQLRestricti?= =?UTF-8?q?on=20=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java index 908b250..83119ee 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.Where; import java.math.BigDecimal; @@ -18,7 +19,7 @@ @AllArgsConstructor @Table(name = "menu") @SQLDelete(sql = "UPDATE menu SET deleted_at = NOW() WHERE id = ?") -@Where(clause = "deleted_at IS NULL") +@SQLRestriction("deleted_at IS NULL") public class Menu extends BaseEntity { @Id From 83cfacafe6cedff140e886fabc0ccd1b3538bb44 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 02:39:09 +0900 Subject: [PATCH 21/27] =?UTF-8?q?[FIX]:=20isSuccess(false)=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/image/status/ImageErrorStatus.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java index 44179c5..cfd9740 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java @@ -25,7 +25,7 @@ public enum ImageErrorStatus implements BaseErrorCode { @Override public ErrorReasonDto getReason() { return ErrorReasonDto.builder() - .isSuccess(true) + .isSuccess(false) .message(message) .code(code) .build(); @@ -35,7 +35,7 @@ public ErrorReasonDto getReason() { public ErrorReasonDto getReasonHttpStatus() { return ErrorReasonDto.builder() .httpStatus(httpStatus) - .isSuccess(true) + .isSuccess(false) .code(code) .message(message) .build(); From c20560f28332972f6e2cd00ed98fe491047ec63d Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 14:57:12 +0900 Subject: [PATCH 22/27] =?UTF-8?q?[REFACTOR]:=20=EC=98=88=EC=95=BD=EA=B8=88?= =?UTF-8?q?=20=EC=A0=95=EC=B1=85=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20minPrice=20=ED=95=84=EB=93=9C=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/store/converter/StoreConverter.java | 1 - .../eatsfine/eatsfine/domain/store/dto/StoreResDto.java | 1 - .../com/eatsfine/eatsfine/domain/store/entity/Store.java | 8 -------- 3 files changed, 10 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/converter/StoreConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/store/converter/StoreConverter.java index 962c97a..6166ca2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/converter/StoreConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/converter/StoreConverter.java @@ -48,7 +48,6 @@ public static StoreResDto.StoreDetailDto toDetailDto(Store store, boolean isOpen .reviewCount(null) // reviewCount는 추후 리뷰 로직 구현 시 추가 예정 .mainImageUrl(store.getMainImageKey()) .tableImageUrls(Collections.emptyList()) // tableImages는 추후 사진 등록 API 구현 시 추가 예정 - .depositAmount(store.calculateDepositAmount()) .businessHours( store.getBusinessHours().stream() .map(BusinessHoursConverter::toSummary) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java index c9ee44a..8431ff6 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java @@ -54,7 +54,6 @@ public record StoreDetailDto( Category category, BigDecimal rating, Long reviewCount, - BigDecimal depositAmount, String mainImageUrl, List tableImageUrls, List businessHours, diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index 466c7af..50962c4 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -173,14 +173,6 @@ public Optional findBusinessHoursByDay(DayOfWeek dayOfWeek) { .findFirst(); } - // 예약금 계산 메서드 - public BigDecimal calculateDepositAmount() { - return BigDecimal.valueOf(minPrice) - .multiply(BigDecimal.valueOf(depositRate.getPercent())) - .divide(BigDecimal.valueOf(100), 0, RoundingMode.DOWN); - } - - // StoreTable에 대한 연관관계 편의 메서드는 추후 추가 예정 // 가게 기본 정보 변경 메서드 public void updateBasicInfo(StoreReqDto.StoreUpdateDto dto) { From 82059062f40be86597a49957f9a19ae3a1d91fdb Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 21:54:06 +0900 Subject: [PATCH 23/27] =?UTF-8?q?[REFACTOR]:=20=EB=82=A8=EC=95=84=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20minPrice=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EB=93=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/store/dto/StoreReqDto.java | 5 ----- .../com/eatsfine/eatsfine/domain/store/entity/Store.java | 6 ------ .../domain/store/service/StoreCommandServiceImpl.java | 2 -- 3 files changed, 13 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreReqDto.java b/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreReqDto.java index 798dad9..70e513c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreReqDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreReqDto.java @@ -53,9 +53,6 @@ public record StoreCreateDto( @NotNull(message = "카테고리는 필수입니다.") Category category, - @NotNull(message = "최소 메뉴 가격은 필수입니다.") - int minPrice, - @NotNull(message = "예약금 비율은 필수입니다.") DepositRate depositRate, @@ -79,8 +76,6 @@ public record StoreUpdateDto( Category category, - Integer minPrice, - DepositRate depositRate, Integer bookingIntervalMinutes diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index 50962c4..d8d45c0 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -82,9 +82,6 @@ public class Store extends BaseEntity { @Column(name = "booking_interval_minutes", nullable = false) private int bookingIntervalMinutes = 30; - @Column(name = "min_price", nullable = false) - private int minPrice; - @Enumerated(EnumType.STRING) @Column(name = "deposit_rate", nullable = false) private DepositRate depositRate; @@ -192,9 +189,6 @@ public void updateBasicInfo(StoreReqDto.StoreUpdateDto dto) { this.category = dto.category(); } - if(dto.minPrice() != null) { - this.minPrice = dto.minPrice(); - } if(dto.depositRate() != null) { this.depositRate = dto.depositRate(); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java index d8c4c34..e9054cc 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java @@ -67,7 +67,6 @@ public StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto dto) { .phoneNumber(dto.phoneNumber()) .category(dto.category()) .bookingIntervalMinutes(dto.bookingIntervalMinutes()) - .minPrice(dto.minPrice()) .depositRate(dto.depositRate()) .build(); @@ -101,7 +100,6 @@ public List extractUpdatedFields(StoreReqDto.StoreUpdateDto dto) { if (dto.description() != null) updated.add("description"); if (dto.phoneNumber() != null) updated.add("phoneNumber"); if (dto.category() != null) updated.add("category"); - if (dto.minPrice() != null) updated.add("minPrice"); if (dto.depositRate() != null) updated.add("depositRate"); if (dto.bookingIntervalMinutes() != null) updated.add("bookingIntervalMinutes"); From 7cb98b0671fbe95581cfd9036c9cd99ea062d452 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 22:48:06 +0900 Subject: [PATCH 24/27] =?UTF-8?q?[FEAT]:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20removeMenu=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/store/entity/Store.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index d8d45c0..1d56603 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -131,14 +131,6 @@ public void addMenu(Menu menu) { menu.assignStore(this); } - // 메뉴 삭제 - public void removeMenu(Menu menu) { - this.menus.remove(menu); - menu.assignStore(null); - } - - - public void addTableImage(TableImage tableImage) { this.tableImages.add(tableImage); tableImage.assignStore(this); From 074c8a4b9a8804674ae6c43d23d0d262176babbf Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 22:49:19 +0900 Subject: [PATCH 25/27] =?UTF-8?q?[REFACTOR]:=20=EB=A9=94=EB=89=B4=EA=B0=80?= =?UTF-8?q?=20=EC=97=86=EB=8A=94=20=EA=B0=80=EA=B2=8C=EB=8F=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=90=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/store/repository/StoreRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java index 88b0b94..6cd3f59 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/repository/StoreRepository.java @@ -14,7 +14,7 @@ public interface StoreRepository extends JpaRepository, StoreReposi select s from Store s left join fetch s.menus m where s.id = :id - and m.deletedAt IS NULL + and (m.deletedAt IS NULL or m.id IS NULL) """) Optional findByIdWithMenus(@Param("id") Long id); From d9cbf9cb206b4dd8e083bfda94bf8aaa21a24110 Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 22:51:27 +0900 Subject: [PATCH 26/27] =?UTF-8?q?[REFACTOR]:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=BB=A4=EB=B0=8B=20=EC=9D=B4=ED=9B=84=EC=97=90=20?= =?UTF-8?q?S3=EC=97=90=20=EC=A0=91=EA=B7=BC=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu/service/MenuCommandServiceImpl.java | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index e07eb8c..9685c38 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -15,8 +15,12 @@ import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; import com.eatsfine.eatsfine.global.s3.S3Service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Transaction; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -26,6 +30,7 @@ @Service @RequiredArgsConstructor @Transactional +@Slf4j public class MenuCommandServiceImpl implements MenuCommandService { private final S3Service s3Service; @@ -53,7 +58,17 @@ public MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateD String permanentImageKey = "stores/" + storeId + "/menus/" + UUID.randomUUID() + extension; // 2. S3에서 객체 이동 (임시 -> 영구) - s3Service.moveObject(tempImageKey, permanentImageKey); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit(){ + try{ + s3Service.moveObject(tempImageKey, permanentImageKey); + } catch (Exception e) { + log.error("temp에서 영구로 이동 실패. Source: {}, Dest: {}", tempImageKey, permanentImageKey); + } + } + + }); // 3. 엔티티에 영구 키 저장 menu.updateImageKey(permanentImageKey); @@ -96,7 +111,13 @@ public MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteD verifyMenuBelongsToStore(menu, storeId); // Soft Delete 시 연결된 S3 이미지도 함께 삭제 if (menu.getImageKey() != null && !menu.getImageKey().isBlank()) { - s3Service.deleteByKey(menu.getImageKey()); + String imageKey = menu.getImageKey(); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + s3Service.deleteByKey(imageKey); + } + }); } }); @@ -133,22 +154,39 @@ public MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto return; } - // 2. 새로운 이미지가 있다면 영구 경로로 이동 (Temp -> Perm) + // 새로운 이미지가 있다면 영구 경로로 이동 (Temp -> Perm) if (newImageKey != null && !newImageKey.isBlank()) { String extension = s3Service.extractExtension(newImageKey); String permanentImageKey = "stores/" + storeId + "/menus/" + UUID.randomUUID() + extension; - s3Service.moveObject(newImageKey, permanentImageKey); - - // 3. 이동 성공 후, 기존 이미지가 있다면 삭제 (Old Perm -> Delete) - if (menu.getImageKey() != null && !menu.getImageKey().isBlank()) { - s3Service.deleteByKey(menu.getImageKey()); - } + String oldImageKey = menu.getImageKey(); + + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + try { + s3Service.moveObject(newImageKey, permanentImageKey); + if (oldImageKey != null && !oldImageKey.isBlank()) { + s3Service.deleteByKey(oldImageKey); + } + } + catch (Exception e) { + log.error("메뉴 이미지를 s3에 업데이트하는 데에 실패했습니다.", e); + } + } + }); menu.updateImageKey(permanentImageKey); + } else { - // 4. 빈 문자열("")인 경우 -> 이미지 삭제 요청 + // 빈 문자열("")인 경우 -> 이미지 삭제 요청 if (menu.getImageKey() != null && !menu.getImageKey().isBlank()) { - s3Service.deleteByKey(menu.getImageKey()); + String oldImageKey = menu.getImageKey(); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + s3Service.deleteByKey(oldImageKey); + } + }); } menu.updateImageKey(null); } @@ -211,7 +249,13 @@ public MenuResDto.ImageDeleteDto deleteMenuImage(Long storeId, Long menuId) { } // 1. S3에서 파일 삭제 - s3Service.deleteByKey(imageKey); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + s3Service.deleteByKey(imageKey); + } + }); + // 2. DB에서 imageKey를 null로 업데이트 (Dirty Checking) menu.updateImageKey(null); From df1accc6a78adb1a7e8d232cba6dcebe57e52d1d Mon Sep 17 00:00:00 2001 From: twodo0 Date: Fri, 30 Jan 2026 22:52:12 +0900 Subject: [PATCH 27/27] =?UTF-8?q?[FEAT]:=20moveObject()=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99=20=EA=B2=BD=EB=A1=9C=EA=B0=80=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=95=9C=20=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java b/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java index ea8d620..d3fdb38 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java +++ b/src/main/java/com/eatsfine/eatsfine/global/s3/S3Service.java @@ -62,6 +62,10 @@ public void moveObject(String sourceKey, String destinationKey) { throw new ImageException(ImageErrorStatus._INVALID_IMAGE_KEY); } + if (sourceKey.equals(destinationKey)) { + throw new ImageException(ImageErrorStatus._INVALID_IMAGE_KEY); + } + // 1. 객체 복사 s3Client.copyObject(CopyObjectRequest.builder() .sourceBucket(bucket)