Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0711109
[REFACTOR]: url -> key로 변수명 변경 및 imageOrder 필드 추가
twodo0 Jan 18, 2026
7244d0d
[BUILD]: AWS S3 관련 의존성 추가
twodo0 Jan 18, 2026
fe7c096
[FEAT]: S3Config, S3Service 추가
twodo0 Jan 18, 2026
8054000
[FEAT]: 이미지 업로드 관련 예외 및 에러 상태코드 추가
twodo0 Jan 18, 2026
30e5e29
[FEAT]: 가게 메인 이미지 등록 응답 DTO 및 성공 상태코드 추가
twodo0 Jan 18, 2026
5dd6d4c
[FEAT]: 가게 메인 이미지 등록 로직 구현
twodo0 Jan 18, 2026
aa94926
[FEAT]: 가게 매인 이미지 조회 응답 DTO 및 성공 상태코드 추가
twodo0 Jan 18, 2026
ce25f9d
[FEAT]: 가게 메인 이미지 조회 로직 구현
twodo0 Jan 18, 2026
7abb965
[FEAT]: 테이블 이미지 등록 응답 DTO 및 성공 상태코드 추가
twodo0 Jan 18, 2026
5320bdd
[FEAT]: 가게 테이블 이미지(TableImage) 등록 로직 구현
twodo0 Jan 18, 2026
6c37796
[FEAT]: 가게 테이블 이미지(TableImage) 조회 로직 구현
twodo0 Jan 18, 2026
91a30a9
[REFACTOR]: 테이블 이미지 등록 및 조회 성공 코드 수정
twodo0 Jan 18, 2026
ead2f17
[FEAT]: 테이블 이미지 삭제 응답 DTO 및 성공 상태코드 추가
twodo0 Jan 19, 2026
1e72917
[FEAT]: 가게 테이블 이미지(TableImage) 삭제 로직 구현
twodo0 Jan 19, 2026
036a9fe
[CHORE]: application.yml에 AWS S3 설정 추가
twodo0 Jan 20, 2026
d70f2a5
[FIX]: StoreSuccessStatus 충돌 해결
twodo0 Jan 20, 2026
908ea07
[FIX]: 에러 상태코드 중복 수정
twodo0 Jan 20, 2026
791e84e
[CHORE]: application-test.yml에 aws s3 설정 추가
twodo0 Jan 20, 2026
62079b2
[FIX]: S3 설정 위치 수정
twodo0 Jan 20, 2026
12d1c43
[REFACTOR]: AWS S3 설정값 환경변수 처리
twodo0 Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"

// amazon
implementation platform('software.amazon.awssdk:bom:2.25.17')

implementation 'software.amazon.awssdk:s3'
implementation 'software.amazon.awssdk:auth'
}
// --- QueryDSL ---
def generated = 'build/generated/sources/annotationProcessor/java/main'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.eatsfine.eatsfine.domain.image.exception;


import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode;
import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException;

public class ImageException extends GeneralException {
public ImageException(BaseErrorCode code) {
super(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.eatsfine.eatsfine.domain.image.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 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", "해당하는 이미지가 존재하지 않습니다.")
;


private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public ErrorReasonDto getReason() {
return ErrorReasonDto.builder()
.isSuccess(true)
.message(message)
.code(code)
.build();
}

@Override
public ErrorReasonDto getReasonHttpStatus() {
return ErrorReasonDto.builder()
.httpStatus(httpStatus)
.isSuccess(true)
.code(code)
.message(message)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "Store", description = "식당 조회 및 관리 API")
@RestController
Expand Down Expand Up @@ -71,4 +73,30 @@ public ApiResponse<StoreResDto.StoreUpdateDto> updateStoreBasicInfo(
}


@Operation(
summary = "식당 대표 이미지 등록",
description = "식당의 대표 이미지를 등록합니다."
)
@PostMapping(
value = "/stores/{storeId}/main-image",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
public ApiResponse<StoreResDto.UploadMainImageDto> uploadMainImage(
@RequestPart("mainImage")MultipartFile mainImage,
@PathVariable Long storeId
){
return ApiResponse.of(StoreSuccessStatus._STORE_MAIN_IMAGE_UPLOAD_SUCCESS, storeCommandService.uploadMainImage(storeId, mainImage));
}

@Operation(
summary = "식당 대표 이미지 조회",
description = "식당의 대표 이미지를 조회합니다."
)
@GetMapping("/stores/{storeId}/main-image")
public ApiResponse<StoreResDto.GetMainImageDto> getMainImage(
@PathVariable Long storeId
) {
return ApiResponse.of(StoreSuccessStatus._STORE_MAIN_IMAGE_GET_SUCCESS, storeQueryService.getMainImage(storeId));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours;
import com.eatsfine.eatsfine.domain.store.dto.StoreResDto;
import com.eatsfine.eatsfine.domain.store.entity.Store;
import org.springframework.web.multipart.MultipartFile;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;

Expand All @@ -26,7 +26,7 @@ public static StoreResDto.StoreSearchDto toSearchDto(Store store, Double distanc
.rating(store.getRating())
.reviewCount(null) // 리뷰 도메인 구현 이후 추가 예정
.distance(distance)
.mainImageUrl(store.getMainImageUrl())
.mainImageUrl(store.getMainImageKey())
.isOpenNow(isOpenNow)
.build();
}
Expand All @@ -46,7 +46,7 @@ public static StoreResDto.StoreDetailDto toDetailDto(Store store, boolean isOpen
.category(store.getCategory())
.rating(store.getRating())
.reviewCount(null) // reviewCount는 추후 리뷰 로직 구현 시 추가 예정
.mainImageUrl(store.getMainImageUrl())
.mainImageUrl(store.getMainImageKey())
.tableImageUrls(Collections.emptyList()) // tableImages는 추후 사진 등록 API 구현 시 추가 예정
.depositAmount(store.calculateDepositAmount())
.businessHours(
Expand All @@ -65,5 +65,19 @@ public static StoreResDto.StoreUpdateDto toUpdateDto(Long storeId, List<String>
.updatedFields(updatedFields)
.build();
}

public static StoreResDto.UploadMainImageDto toUploadMainImageDto(Long storeId, String mainImageUrl) {
return StoreResDto.UploadMainImageDto.builder()
.storeId(storeId)
.mainImageUrl(mainImageUrl)
.build();
}

public static StoreResDto.GetMainImageDto toGetMainImageDto(Long storeId, String key) {
return StoreResDto.GetMainImageDto.builder()
.storeId(storeId)
.mainImageUrl(key)
.build();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public record StoreDetailDto(

// 가게 대표 이미지 등록 응답
@Builder
public record uploadMainImageResDto(
public record UploadMainImageDto(
Long storeId,
String mainImageUrl
) {}

Expand All @@ -80,5 +81,11 @@ public record StoreUpdateDto(
Long storeId,
List<String> updatedFields
){};
// 가게 대표 이미지 조회 응답
@Builder
public record GetMainImageDto(
Long storeId,
String mainImageUrl
) {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class Store extends BaseEntity {
private String address;

@Column(name = "main_image_url")
private String mainImageUrl;
private String mainImageKey;

@Builder.Default
@Column(name = "rating", precision = 2, scale = 1, nullable = false)
Expand Down Expand Up @@ -95,7 +95,6 @@ public class Store extends BaseEntity {
@OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TableImage> tableImages = new ArrayList<>();

// StoreTable이 아닌 TableLayout 엔티티 참조

@Builder.Default
@OneToMany(mappedBy = "store")
Expand Down Expand Up @@ -130,6 +129,11 @@ public void removeTableImage(TableImage tableImage) {
tableImage.assignStore(null);
}

// 가게 메인 이미지 등록
public void updateMainImageKey(String mainImageKey) {
this.mainImageKey = mainImageKey;
}

// 특정 요일의 영업시간 조회 메서드
public BusinessHours getBusinessHoursByDay(DayOfWeek dayOfWeek) {
return this.businessHours.stream()
Expand All @@ -146,6 +150,7 @@ public Optional<BusinessHours> findBusinessHoursByDay(DayOfWeek dayOfWeek) {
.findFirst();
}

// 예약금 계산 메서드
public BigDecimal calculateDepositAmount() {
return BigDecimal.valueOf(minPrice)
.multiply(BigDecimal.valueOf(depositRate.getPercent()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import com.eatsfine.eatsfine.domain.store.dto.StoreReqDto;
import com.eatsfine.eatsfine.domain.store.dto.StoreResDto;
import org.springframework.web.multipart.MultipartFile;

public interface StoreCommandService {
StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto storeCreateDto);
StoreResDto.StoreUpdateDto updateBasicInfo(Long storeId, StoreReqDto.StoreUpdateDto storeUpdateDto);
StoreResDto.UploadMainImageDto uploadMainImage(Long storeId, MultipartFile file);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.eatsfine.eatsfine.domain.businesshours.converter.BusinessHoursConverter;
import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours;
import com.eatsfine.eatsfine.domain.businesshours.validator.BusinessHoursValidator;
import com.eatsfine.eatsfine.domain.image.exception.ImageException;
import com.eatsfine.eatsfine.domain.image.status.ImageErrorStatus;
import com.eatsfine.eatsfine.domain.region.entity.Region;
import com.eatsfine.eatsfine.domain.region.repository.RegionRepository;
import com.eatsfine.eatsfine.domain.region.status.RegionErrorStatus;
Expand All @@ -19,6 +21,8 @@

import java.util.ArrayList;
import java.util.List;
import com.eatsfine.eatsfine.global.s3.S3Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@Transactional
Expand All @@ -27,7 +31,9 @@ public class StoreCommandServiceImpl implements StoreCommandService {

private final StoreRepository storeRepository;
private final RegionRepository regionRepository;
private final S3Service s3Service;

// 가게 등록
@Override
public StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto dto) {
Region region = regionRepository.findById(dto.regionId())
Expand All @@ -42,7 +48,7 @@ public StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto dto) {
.businessNumber(dto.businessNumber())
.description(dto.description())
.address(dto.address())
.mainImageUrl(null) // 별도 API로 구현
.mainImageKey(null) // 별도 API로 구현
.region(region)
.phoneNumber(dto.phoneNumber())
.category(dto.category())
Expand Down Expand Up @@ -77,15 +83,37 @@ public StoreResDto.StoreUpdateDto updateBasicInfo(Long storeId, StoreReqDto.Stor
public List<String> extractUpdatedFields(StoreReqDto.StoreUpdateDto dto) {
List<String> updated = new ArrayList<>();

if(dto.storeName() != null) updated.add("storeName");
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");
if (dto.storeName() != null) updated.add("storeName");
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");

return updated;
}
// 가게 메인 이미지 등록
@Override
public StoreResDto.UploadMainImageDto uploadMainImage(Long storeId, MultipartFile file) {
Store store = storeRepository.findById(storeId).orElseThrow(
() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)
);

if(file.isEmpty()) {
throw new ImageException(ImageErrorStatus.EMPTY_FILE);
}

if(store.getMainImageKey() != null) {
s3Service.deleteByKey(store.getMainImageKey());
}

String key = s3Service.upload(file, "stores/" + storeId + "/main");
store.updateMainImageKey(key);

String mainImageUrl = s3Service.toUrl(store.getMainImageKey());

return StoreConverter.toUploadMainImageDto(store.getId(), mainImageUrl);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ StoreResDto.StoreSearchResDto search(

StoreResDto.StoreDetailDto getStoreDetail(Long storeId);

StoreResDto.GetMainImageDto getMainImage(Long storeId);

boolean isOpenNow(Store store, LocalDateTime now);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
import com.eatsfine.eatsfine.domain.store.dto.StoreResDto;
import com.eatsfine.eatsfine.domain.store.dto.projection.StoreSearchResult;
import com.eatsfine.eatsfine.domain.store.entity.Store;
import com.eatsfine.eatsfine.domain.store.enums.Category;
import com.eatsfine.eatsfine.domain.store.enums.StoreSortType;
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.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.DayOfWeek;
import java.time.LocalDateTime;
Expand All @@ -23,9 +23,11 @@

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class StoreQueryServiceImpl implements StoreQueryService {

private final StoreRepository storeRepository;
private final S3Service s3Service;

// 식당 검색
@Override
Expand Down Expand Up @@ -73,6 +75,15 @@ public StoreResDto.StoreDetailDto getStoreDetail(Long storeId) {
return StoreConverter.toDetailDto(store, isOpenNow(store, LocalDateTime.now()));
}

// 식당 대표 이미지 조회
@Override
public StoreResDto.GetMainImageDto getMainImage(Long storeId) {
Store store = storeRepository.findById(storeId)
.orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND));

return StoreConverter.toGetMainImageDto(storeId, s3Service.toUrl(store.getMainImageKey()));
}

// 현재 영업 여부 계산 (실시간 계산)
@Override
public boolean isOpenNow(Store store, LocalDateTime now) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ public enum StoreSuccessStatus implements BaseCode {

_STORE_CREATED(HttpStatus.CREATED, "STORE201", "성공적으로 가게를 등록했습니다."),

_STORE_UPDATE_SUCCESS(HttpStatus.OK, "STORE2004", "성공적으로 가게 기본 정보를 수정했습니다.")
_STORE_UPDATE_SUCCESS(HttpStatus.OK, "STORE2004", "성공적으로 가게 기본 정보를 수정했습니다."),

_STORE_MAIN_IMAGE_UPLOAD_SUCCESS(HttpStatus.OK, "STORE2005", "성공적으로 가게 대표 이미지를 업로드했습니다."),

_STORE_MAIN_IMAGE_GET_SUCCESS(HttpStatus.OK, "STORE2005", "성공적으로 가게 대표 이미지를 조회했습니다.")
;


Expand Down
Loading