Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import com.eatsfine.eatsfine.domain.storetable.exception.status.StoreTableSuccessStatus;
import com.eatsfine.eatsfine.domain.storetable.service.StoreTableCommandService;
import com.eatsfine.eatsfine.domain.storetable.service.StoreTableQueryService;
import com.eatsfine.eatsfine.domain.tableimage.status.TableImageSuccessStatus;
import com.eatsfine.eatsfine.global.apiPayload.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;

Expand Down Expand Up @@ -66,4 +69,24 @@ public ApiResponse<StoreTableResDto.TableDeleteDto> deleteTable(
) {
return ApiResponse.of(StoreTableSuccessStatus._TABLE_DELETED, storeTableCommandService.deleteTable(storeId, tableId));
}

@PostMapping(
value = "/stores/{storeId}/tables/{tableId}/table-image",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
public ApiResponse<StoreTableResDto.UploadTableImageDto> uploadTableImage(
@PathVariable Long storeId,
@PathVariable Long tableId,
@RequestPart("tableImage") MultipartFile tableImage
) {
return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_UPLOAD_SUCCESS, storeTableCommandService.uploadTableImage(storeId, tableId, tableImage));
}
Comment on lines +73 to +83
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

URL path inconsistency with requirements.

The PR description and issue #79 specify the endpoint path as /api/v1/stores/{storeId}/tables/{tableId}/image, but the implementation uses /table-image. Consider aligning with the documented API specification.

Additionally, the @Override annotation appears to be missing if this method is declared in StoreTableControllerDocs.

Suggested path alignment
     `@PostMapping`(
-            value = "/stores/{storeId}/tables/{tableId}/table-image",
+            value = "/stores/{storeId}/tables/{tableId}/image",
             consumes = MediaType.MULTIPART_FORM_DATA_VALUE
     )
+    `@Override`
     public ApiResponse<StoreTableResDto.UploadTableImageDto> uploadTableImage(
🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableController.java`
around lines 73 - 83, The controller method uploadTableImage currently maps to
"/stores/{storeId}/tables/{tableId}/table-image" and may be missing an
`@Override`; change the `@PostMapping` path to
"/stores/{storeId}/tables/{tableId}/image" to match the API spec/issue `#79` and
add the `@Override` annotation above uploadTableImage if this method implements
the signature from StoreTableControllerDocs; update any related references to
the mapping constant if used so the endpoint path stays consistent.


@DeleteMapping("/stores/{storeId}/tables/{tableId}/table-image")
public ApiResponse<StoreTableResDto.DeleteTableImageDto> deleteTableImage(
@PathVariable Long storeId,
@PathVariable Long tableId
) {
return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_DELETE_SUCCESS, storeTableCommandService.deleteTableImage(storeId, tableId));
}
Comment on lines +85 to +91
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same path inconsistency and missing annotation.

Same observations as the upload endpoint - the path uses /table-image instead of /image per the API specification, and @Override annotation should be added for interface implementation consistency.

Suggested fix
-    `@DeleteMapping`("/stores/{storeId}/tables/{tableId}/table-image")
+    `@Override`
+    `@DeleteMapping`("/stores/{storeId}/tables/{tableId}/image")
     public ApiResponse<StoreTableResDto.DeleteTableImageDto> deleteTableImage(
🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableController.java`
around lines 85 - 91, The Delete endpoint mapping in StoreTableController is
using the wrong path segment and missing the `@Override` annotation; update the
`@DeleteMapping` on the deleteTableImage method to use
"/stores/{storeId}/tables/{tableId}/image" (matching the upload endpoint and API
spec) and add the `@Override` annotation above the deleteTableImage method to
indicate it implements the interface, leaving the return logic
(ApiResponse.of(..., storeTableCommandService.deleteTableImage(storeId,
tableId))) and TableImageSuccessStatus reference unchanged.

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import com.eatsfine.eatsfine.global.apiPayload.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;

Expand Down Expand Up @@ -187,4 +190,58 @@ ApiResponse<StoreTableResDto.TableDeleteDto> deleteTable(
@Parameter(description = "테이블 ID", required = true, example = "1")
Long tableId
);

@Operation(
summary = "테이블 이미지 등록",
description = """
특정 테이블의 이미지를 등록합니다.

- 테이블당 1개의 이미지만 등록 가능합니다.
- 기존 이미지가 있는 경우 자동으로 삭제되고 새 이미지로 교체됩니다.
- S3 저장 경로: stores/{storeId}/tables/{tableId}/
"""
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "테이블 이미지 등록 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 (빈 파일, 지원하지 않는 파일 형식 등)"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "가게 또는 테이블을 찾을 수 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "S3 업로드 실패")
})
ApiResponse<StoreTableResDto.UploadTableImageDto> uploadTableImage(
@Parameter(description = "가게 ID", required = true, example = "1")
Long storeId,

@Parameter(description = "테이블 ID", required = true, example = "1")
Long tableId,

@Parameter(
description = "업로드할 테이블 이미지 파일",
required = true,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
)
MultipartFile tableImage
);

@Operation(
summary = "테이블 이미지 삭제",
description = """
특정 테이블의 이미지를 삭제합니다.

- 등록된 이미지가 없는 경우 404 에러가 발생합니다.
- S3에서 이미지가 삭제되고, DB의 이미지 URL도 null로 업데이트됩니다.
- 삭제 후 다시 이미지를 등록할 수 있습니다.
"""
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "테이블 이미지 삭제 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "테이블이 해당 가게에 속하지 않음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "가게, 테이블을 찾을 수 없거나 이미지가 등록되지 않음")
})
ApiResponse<StoreTableResDto.DeleteTableImageDto> deleteTableImage(
@Parameter(description = "가게 ID", required = true, example = "1")
Long storeId,

@Parameter(description = "테이블 ID", required = true, example = "1")
Long tableId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ public static StoreTableResDto.SlotListDto toSlotListDto(int totalCount, int ava
.build();
}

public static StoreTableResDto.TableDetailDto toTableDetailDto(StoreTable table, LocalDate targetDate, int totalSlotCount, int availableSlotCount) {
public static StoreTableResDto.TableDetailDto toTableDetailDto(StoreTable table, LocalDate targetDate, int totalSlotCount, int availableSlotCount, String tableImageUrl) {
return StoreTableResDto.TableDetailDto.builder()
.tableId(table.getId())
.minSeatCount(table.getMinSeatCount())
.maxSeatCount(table.getMaxSeatCount())
.tableImageUrl(table.getTableImageUrl())
.tableImageUrl(tableImageUrl)
.rating(table.getRating())
.reviewCount(0) // 리뷰 기능 미구현으로 0 반환
.seatsType(table.getSeatsType())
Expand Down Expand Up @@ -94,4 +94,17 @@ public static StoreTableResDto.TableDeleteDto toTableDeleteDto(StoreTable table)
.tableId(table.getId())
.build();
}

public static StoreTableResDto.UploadTableImageDto toUploadTableImageDto(Long tableId, String tableImageUrl) {
return StoreTableResDto.UploadTableImageDto.builder()
.tableId(tableId)
.tableImageUrl(tableImageUrl)
.build();
}

public static StoreTableResDto.DeleteTableImageDto toDeleteTableImageDto(Long tableId) {
return StoreTableResDto.DeleteTableImageDto.builder()
.tableId(tableId)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,15 @@ public record UpdatedTableDto(
public record TableDeleteDto(
Long tableId
) {}

@Builder
public record UploadTableImageDto(
Long tableId,
String tableImageUrl
) {}

@Builder
public record DeleteTableImageDto(
Long tableId
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,14 @@ public void updateSeatCount(int minSeatCount, int maxSeatCount) {
public void updateSeatsType(SeatsType seatsType) {
this.seatsType = seatsType;
}

// 테이블 이미지 업로드
public void updateTableImage(String imageKey) {
this.tableImageUrl = imageKey;
}

// 테이블 이미지 삭제
public void deleteTableImage() {
this.tableImageUrl = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import com.eatsfine.eatsfine.domain.storetable.dto.req.StoreTableReqDto;
import com.eatsfine.eatsfine.domain.storetable.dto.res.StoreTableResDto;
import org.springframework.web.multipart.MultipartFile;

public interface StoreTableCommandService {
StoreTableResDto.TableCreateDto createTable(Long storeId, StoreTableReqDto.TableCreateDto dto);

StoreTableResDto.TableUpdateResultDto updateTable(Long storeId, Long tableId, StoreTableReqDto.TableUpdateDto dto);

StoreTableResDto.TableDeleteDto deleteTable(Long storeId, Long tableId);

StoreTableResDto.UploadTableImageDto uploadTableImage(Long storeId, Long tableId, MultipartFile tableImage);

StoreTableResDto.DeleteTableImageDto deleteTableImage(Long storeId, Long tableId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.eatsfine.eatsfine.domain.storetable.service;

import com.eatsfine.eatsfine.domain.booking.repository.BookingRepository;
import com.eatsfine.eatsfine.domain.image.exception.ImageException;
import com.eatsfine.eatsfine.domain.image.status.ImageErrorStatus;
import com.eatsfine.eatsfine.domain.store.exception.StoreException;
import com.eatsfine.eatsfine.domain.store.repository.StoreRepository;
import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus;
Expand All @@ -16,9 +18,11 @@
import com.eatsfine.eatsfine.domain.table_layout.exception.TableLayoutException;
import com.eatsfine.eatsfine.domain.table_layout.exception.status.TableLayoutErrorStatus;
import com.eatsfine.eatsfine.domain.table_layout.repository.TableLayoutRepository;
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.math.BigDecimal;
import java.time.LocalDate;
Expand All @@ -35,6 +39,7 @@ public class StoreTableCommandServiceImpl implements StoreTableCommandService {
private final TableLayoutRepository tableLayoutRepository;
private final StoreTableRepository storeTableRepository;
private final BookingRepository bookingRepository;
private final S3Service s3Service;

// 테이블 생성
@Override
Expand Down Expand Up @@ -160,6 +165,58 @@ public StoreTableResDto.TableDeleteDto deleteTable(Long storeId, Long tableId) {
return StoreTableConverter.toTableDeleteDto(table);
}

// 테이블 이미지 업로드
@Override
public StoreTableResDto.UploadTableImageDto uploadTableImage(Long storeId, Long tableId, MultipartFile tableImage) {
storeRepository.findById(storeId)
.orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND));

StoreTable table = storeTableRepository.findById(tableId)
.orElseThrow(() -> new StoreTableException(StoreTableErrorStatus._TABLE_NOT_FOUND));

StoreTableValidator.validateTableBelongsToStore(table, storeId);

if (tableImage == null || tableImage.isEmpty()) {
throw new ImageException(ImageErrorStatus.EMPTY_FILE);
}

// 기존 이미지가 존재할 경우 삭제
if (table.getTableImageUrl() != null && !table.getTableImageUrl().isBlank()) {
s3Service.deleteByKey(table.getTableImageUrl());
}

String key = s3Service.upload(tableImage, "stores/" + storeId + "/tables/" + tableId);

table.updateTableImage(key);

// URL 변환 및 응답
String tableImageUrl = s3Service.toUrl(key);

return StoreTableConverter.toUploadTableImageDto(tableId, tableImageUrl);
}

@Override
public StoreTableResDto.DeleteTableImageDto deleteTableImage(Long storeId, Long tableId) {
storeRepository.findById(storeId)
.orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND));

StoreTable table = storeTableRepository.findById(tableId)
.orElseThrow(() -> new StoreTableException(StoreTableErrorStatus._TABLE_NOT_FOUND));

StoreTableValidator.validateTableBelongsToStore(table, storeId);

// 이미지가 존재하는지 확인
if (table.getTableImageUrl() == null || table.getTableImageUrl().isBlank()) {
throw new ImageException(ImageErrorStatus._IMAGE_NOT_FOUND);
}

s3Service.deleteByKey(table.getTableImageUrl());

table.deleteTableImage();

return StoreTableConverter.toDeleteTableImageDto(tableId);
}

private String generateTableNumber(TableLayout layout) {
List<StoreTable> tables = layout.getTables();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.eatsfine.eatsfine.domain.storetable.validator.StoreTableValidator;
import com.eatsfine.eatsfine.domain.tableblock.entity.TableBlock;
import com.eatsfine.eatsfine.domain.tableblock.repository.TableBlockRepository;
import com.eatsfine.eatsfine.global.s3.S3Service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -32,6 +33,7 @@ public class StoreTableQueryServiceImpl implements StoreTableQueryService{
private final StoreTableRepository storeTableRepository;
private final TableBlockRepository tableBlockRepository;
private final BookingRepository bookingRepository;
private final S3Service s3Service;

// 테이블 슬롯 조회
@Override
Expand All @@ -57,6 +59,7 @@ public StoreTableResDto.SlotListDto getTableSlots(Long storeId, Long tableId, Lo
);
}

// 테이블 상세 조회
@Override
public StoreTableResDto.TableDetailDto getTableDetail(Long storeId, Long tableId, LocalDate targetDate) {
storeRepository.findById(storeId)
Expand All @@ -73,11 +76,15 @@ public StoreTableResDto.TableDetailDto getTableDetail(Long storeId, Long tableId

SlotCalculator.SlotCalculationResult result = SlotCalculator.calculateSlots(storeTable, targetDate, tableBlocks, bookedTimes);

// S3 Key -> Url 변환
String tableImageUrl = s3Service.toUrl(storeTable.getTableImageUrl());

return StoreTableConverter.toTableDetailDto(
storeTable,
targetDate,
result.totalSlotCount(),
result.availableSlotCount()
result.availableSlotCount(),
tableImageUrl
);
}
}