diff --git a/src/main/java/javalab/umc7th_mission/apiPayload/code/status/ErrorStatus.java b/src/main/java/javalab/umc7th_mission/apiPayload/code/status/ErrorStatus.java index 5298851..5528c83 100644 --- a/src/main/java/javalab/umc7th_mission/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/javalab/umc7th_mission/apiPayload/code/status/ErrorStatus.java @@ -26,8 +26,9 @@ public enum ErrorStatus implements BaseErrorCode { // 예시,,, ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."), - //음식 카테고리 없음 - FOOD_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "FOOD4001", "해당 음식 카테고리가 없습니다."); + //해당 지역 혹은 카테고리 없음 + FOOD_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "FOOD4001", "해당 음식 카테고리가 없습니다."), + REGION_NOT_FOUND(HttpStatus.NOT_FOUND, "REGION4001", "해당 지역이 없습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/javalab/umc7th_mission/apiPayload/exception/ExceptionAdvice.java b/src/main/java/javalab/umc7th_mission/apiPayload/exception/ExceptionAdvice.java index 77ab641..141afac 100644 --- a/src/main/java/javalab/umc7th_mission/apiPayload/exception/ExceptionAdvice.java +++ b/src/main/java/javalab/umc7th_mission/apiPayload/exception/ExceptionAdvice.java @@ -86,6 +86,26 @@ public ResponseEntity handleFoodCategoryNotFoundException(FoodCategoryNo ); } + //RegionNotFoundException에 대한 에러 처리 + @ExceptionHandler(RegionNotFoundException.class) + public ResponseEntity handleRegionNotFoundException(RegionNotFoundException e, HttpServletRequest request) { + WebRequest webRequest = new ServletWebRequest(request); + + ApiResponse body = ApiResponse.onFailure( + ErrorStatus.REGION_NOT_FOUND.getCode(), + ErrorStatus.REGION_NOT_FOUND.getMessage(), + null + ); + + return super.handleExceptionInternal( + e, + body, + new HttpHeaders(), + ErrorStatus.REGION_NOT_FOUND.getHttpStatus(), + webRequest + ); + } + private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) { diff --git a/src/main/java/javalab/umc7th_mission/apiPayload/exception/RegionNotFoundException.java b/src/main/java/javalab/umc7th_mission/apiPayload/exception/RegionNotFoundException.java new file mode 100644 index 0000000..36fafae --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/apiPayload/exception/RegionNotFoundException.java @@ -0,0 +1,16 @@ +package javalab.umc7th_mission.apiPayload.exception; + +import javalab.umc7th_mission.apiPayload.code.status.ErrorStatus; +import org.springframework.http.HttpStatus; + +public class RegionNotFoundException extends RuntimeException { + private final HttpStatus status = HttpStatus.NOT_FOUND; + + public RegionNotFoundException(ErrorStatus errorStatus) { + super(errorStatus.getMessage()); + } + + public HttpStatus getStatus() { + return status; + } +} diff --git a/src/main/java/javalab/umc7th_mission/converter/StoreCategoryConverter.java b/src/main/java/javalab/umc7th_mission/converter/StoreCategoryConverter.java new file mode 100644 index 0000000..eadcb3b --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/converter/StoreCategoryConverter.java @@ -0,0 +1,19 @@ +package javalab.umc7th_mission.converter; + +import javalab.umc7th_mission.domain.FoodCategory; +import javalab.umc7th_mission.domain.mapping.StoreCategory; + +import java.util.List; +import java.util.stream.Collectors; + +public class StoreCategoryConverter { + public static List toStoreCategoryList(List foodCategoryList){ + + return foodCategoryList.stream() + .map(foodCategory -> + StoreCategory.builder() + .foodCategory(foodCategory) + .build() + ).collect(Collectors.toList()); + } +} diff --git a/src/main/java/javalab/umc7th_mission/converter/StoreConverter.java b/src/main/java/javalab/umc7th_mission/converter/StoreConverter.java new file mode 100644 index 0000000..f3769a5 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/converter/StoreConverter.java @@ -0,0 +1,40 @@ +package javalab.umc7th_mission.converter; + +import javalab.umc7th_mission.domain.Member; +import javalab.umc7th_mission.domain.Region; +import javalab.umc7th_mission.domain.Store; +import javalab.umc7th_mission.domain.mapping.StoreAddress; +import javalab.umc7th_mission.web.dto.MemberRequestDTO; +import javalab.umc7th_mission.web.dto.StoreRequestDTO; +import javalab.umc7th_mission.web.dto.StoreResponseDTO; + +public class StoreConverter { + + public static StoreResponseDTO.AddResultDTO toAddResultDTO(Store store) { + return new StoreResponseDTO.AddResultDTO( + store.getId(), + store.getCreatedAt() + ); + } + + public static Store toStore(StoreRequestDTO.AddDto request) { + return Store.builder() + .name(request.getName()) + .phoneNumber(request.getPhoneNumber()) + .description(request.getDescription()) + .openingHours(request.getOpeningHours()) + .closingHours(request.getClosingHours()) + .build(); + } + + public static StoreAddress toStoreAddress(Store store, Region region, StoreRequestDTO.AddDto request) { + return StoreAddress.builder() + .region(region) + .store(store) + .detailAddress(request.getDetailedAddress()) + .zipCode(request.getZipcode()) + .build(); + } +} + + diff --git a/src/main/java/javalab/umc7th_mission/domain/Store.java b/src/main/java/javalab/umc7th_mission/domain/Store.java index deb4856..beea1ce 100644 --- a/src/main/java/javalab/umc7th_mission/domain/Store.java +++ b/src/main/java/javalab/umc7th_mission/domain/Store.java @@ -38,8 +38,10 @@ public class Store extends BaseEntity { @Column(nullable = false, length = 255) private String closingHours; - @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private List addressList = new ArrayList<>(); + //24.11.20 StoreAddress와의 관계 재설정!! + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "address_id") + private StoreAddress address; @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List categoryList = new ArrayList<>(); diff --git a/src/main/java/javalab/umc7th_mission/domain/mapping/StoreAddress.java b/src/main/java/javalab/umc7th_mission/domain/mapping/StoreAddress.java index 358179c..4711f20 100644 --- a/src/main/java/javalab/umc7th_mission/domain/mapping/StoreAddress.java +++ b/src/main/java/javalab/umc7th_mission/domain/mapping/StoreAddress.java @@ -23,8 +23,7 @@ public class StoreAddress extends BaseEntity { @JoinColumn(name = "region_id", referencedColumnName = "id") private Region region; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "store_id", referencedColumnName = "id") + @OneToOne(mappedBy = "address",fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Store store; @Column(nullable = false, length = 100) diff --git a/src/main/java/javalab/umc7th_mission/repository/MemberMissionRepository/MemberMissionRepositoryImpl.java b/src/main/java/javalab/umc7th_mission/repository/MemberMissionRepository/MemberMissionRepositoryImpl.java index 96f7be8..1d3a6e4 100644 --- a/src/main/java/javalab/umc7th_mission/repository/MemberMissionRepository/MemberMissionRepositoryImpl.java +++ b/src/main/java/javalab/umc7th_mission/repository/MemberMissionRepository/MemberMissionRepositoryImpl.java @@ -78,7 +78,7 @@ public Long countCompletedMissionByMember(Long memberId, String regionName) { .from(memberMission) .join(memberMission.mission, mission) .join(mission.store, store) - .join(storeAddress, storeAddress) + .join(store.address, storeAddress) .where( memberMission.member.id.eq(memberId), memberMission.status.eq(MissionStatus.COMPLETE), diff --git a/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepository.java b/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepository.java new file mode 100644 index 0000000..3a55634 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepository.java @@ -0,0 +1,9 @@ +package javalab.umc7th_mission.repository.StoreAddressRepository; + +import javalab.umc7th_mission.domain.mapping.StoreAddress; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface StoreAddressRepository extends JpaRepository, StoreAddressRepositoryCustom { +} diff --git a/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepositoryCustom.java b/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepositoryCustom.java new file mode 100644 index 0000000..6efaf9a --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepositoryCustom.java @@ -0,0 +1,4 @@ +package javalab.umc7th_mission.repository.StoreAddressRepository; + +public interface StoreAddressRepositoryCustom { +} diff --git a/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepositoryImpl.java b/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepositoryImpl.java new file mode 100644 index 0000000..eb78b78 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/repository/StoreAddressRepository/StoreAddressRepositoryImpl.java @@ -0,0 +1,4 @@ +package javalab.umc7th_mission.repository.StoreAddressRepository; + +public class StoreAddressRepositoryImpl implements StoreAddressRepositoryCustom { +} diff --git a/src/main/java/javalab/umc7th_mission/service/StoreService/StoreCommandService.java b/src/main/java/javalab/umc7th_mission/service/StoreService/StoreCommandService.java new file mode 100644 index 0000000..c3c91d4 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/service/StoreService/StoreCommandService.java @@ -0,0 +1,9 @@ +package javalab.umc7th_mission.service.StoreService; + +import javalab.umc7th_mission.domain.Store; +import javalab.umc7th_mission.web.dto.StoreRequestDTO; + +public interface StoreCommandService { + // 가게 저장 + Store addStore(StoreRequestDTO.AddDto request); +} diff --git a/src/main/java/javalab/umc7th_mission/service/StoreService/StoreCommandServiceImpl.java b/src/main/java/javalab/umc7th_mission/service/StoreService/StoreCommandServiceImpl.java new file mode 100644 index 0000000..0b29fe3 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/service/StoreService/StoreCommandServiceImpl.java @@ -0,0 +1,62 @@ +package javalab.umc7th_mission.service.StoreService; + +import jakarta.transaction.Transactional; +import javalab.umc7th_mission.apiPayload.code.status.ErrorStatus; +import javalab.umc7th_mission.apiPayload.exception.FoodCategoryNotFoundException; +import javalab.umc7th_mission.apiPayload.exception.RegionNotFoundException; +import javalab.umc7th_mission.converter.StoreCategoryConverter; +import javalab.umc7th_mission.converter.StoreConverter; +import javalab.umc7th_mission.domain.FoodCategory; +import javalab.umc7th_mission.domain.Region; +import javalab.umc7th_mission.domain.Store; +import javalab.umc7th_mission.domain.mapping.StoreAddress; +import javalab.umc7th_mission.domain.mapping.StoreCategory; +import javalab.umc7th_mission.repository.FoodCategoryRepository.FoodCategoryRepository; +import javalab.umc7th_mission.repository.RegionRepository.RegionRepository; +import javalab.umc7th_mission.repository.StoreAddressRepository.StoreAddressRepository; +import javalab.umc7th_mission.repository.StoreRepository.StoreRepository; +import javalab.umc7th_mission.web.dto.StoreRequestDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class StoreCommandServiceImpl implements StoreCommandService { + private final StoreRepository storeRepository; + private final StoreAddressRepository storeAddressRepository; + private final RegionRepository regionRepository; + private final FoodCategoryRepository foodCategoryRepository; + + @Override + @Transactional + public Store addStore(StoreRequestDTO.AddDto request) { + Region region = regionRepository.findByName(request.getRegion()); + if (region == null) { + throw new RegionNotFoundException(ErrorStatus.REGION_NOT_FOUND); + } + + + Store newStore = StoreConverter.toStore(request); + StoreAddress storeAddress = StoreConverter.toStoreAddress(newStore, region, request); + + //StoreCategory 생성필요 + //foodCategory list 조회 + List foodCategoryList = request.getStoreCategory().stream() + .map(category -> { + return foodCategoryRepository.findById(category).orElseThrow(() -> new FoodCategoryNotFoundException(ErrorStatus.FOOD_CATEGORY_NOT_FOUND)); + }).collect(Collectors.toList()); + + List storeCategoryList = StoreCategoryConverter.toStoreCategoryList(foodCategoryList); + storeCategoryList.forEach(memberPrefer -> {memberPrefer.setStore(newStore);}); + + newStore.setCategoryList(storeCategoryList); + storeAddress.setStore(newStore); + newStore.setAddress(storeAddress); + + storeAddressRepository.save(storeAddress); + return storeRepository.save(newStore); + } +} diff --git a/src/main/java/javalab/umc7th_mission/web/controller/StoreRestController.java b/src/main/java/javalab/umc7th_mission/web/controller/StoreRestController.java new file mode 100644 index 0000000..997f319 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/web/controller/StoreRestController.java @@ -0,0 +1,28 @@ +package javalab.umc7th_mission.web.controller; + +import jakarta.validation.Valid; +import javalab.umc7th_mission.apiPayload.ApiResponse; +import javalab.umc7th_mission.converter.StoreConverter; +import javalab.umc7th_mission.domain.Store; +import javalab.umc7th_mission.service.StoreService.StoreCommandService; +import javalab.umc7th_mission.web.dto.StoreRequestDTO; +import javalab.umc7th_mission.web.dto.StoreResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/stores") +public class StoreRestController { + private final StoreCommandService storeCommandService; + + @PostMapping("/") + public ApiResponse join(@RequestBody @Valid StoreRequestDTO.AddDto request){ + + Store store = storeCommandService.addStore(request); + return ApiResponse.onSuccess(StoreConverter.toAddResultDTO(store)); + } +} diff --git a/src/main/java/javalab/umc7th_mission/web/dto/StoreRequestDTO.java b/src/main/java/javalab/umc7th_mission/web/dto/StoreRequestDTO.java new file mode 100644 index 0000000..42fb0c0 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/web/dto/StoreRequestDTO.java @@ -0,0 +1,71 @@ +package javalab.umc7th_mission.web.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +import java.util.List; + +public class StoreRequestDTO { + + @Getter + public static class AddDto { + @NotBlank(message = "가게 이름은 필수 항목입니다.") + @Size(max = 30, message = "최대 30자까지 입력 가능합니다.") + private final String name; + + @NotBlank(message = "전화번호는 필수 항목입니다.") + //전화번호 형식 검증 정규식 + @Pattern(regexp = "^(02|0\\d{2})-\\d{3,4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다.") + private final String phoneNumber; + + @NotBlank(message = "가게 설명은 필수 항목입니다.") + @Size(max = 500, message = "최대 500자까지 입력 가능합니다.") + private final String description; + + @NotBlank(message = "오픈 시간은 필수 항목입니다.") + @Size(max = 255, message = "최대 255자까지 입력 가능하니다.") + @Pattern( + regexp = "^([01]?[0-9]|2[0-3]):[0-5][0-9]$", + message = "오픈 시간은 'HH:mm' 형식이어야 합니다. (예: 09:00, 23:59)" + ) + private final String openingHours; + + @NotBlank(message = "마감 시간은 필수 항목입니다.") + @Size(max = 255, message = "최대 255자까지 입력 가능하니다.") + @Pattern( + regexp = "^([01]?[0-9]|2[0-3]):[0-5][0-9]$", + message = "마감 시간은 'HH:mm' 형식이어야 합니다. (예: 09:00, 23:59)" + ) + private final String closingHours; + + private final String region; + private final String detailedAddress; + private final String zipcode; + private final List storeCategory; + + public AddDto( + String name, + String phoneNumber, + String description, + String openingHours, + String closingHours, + + String region, + String detailedAddress, + String zipcode, + List storeCategory + ) { + this.name = name; + this.phoneNumber = phoneNumber; + this.description = description; + this.openingHours = openingHours; + this.closingHours = closingHours; + this.region = region; + this.detailedAddress = detailedAddress; + this.zipcode = zipcode; + this.storeCategory = storeCategory; + } + } +} diff --git a/src/main/java/javalab/umc7th_mission/web/dto/StoreResponseDTO.java b/src/main/java/javalab/umc7th_mission/web/dto/StoreResponseDTO.java new file mode 100644 index 0000000..5e33057 --- /dev/null +++ b/src/main/java/javalab/umc7th_mission/web/dto/StoreResponseDTO.java @@ -0,0 +1,24 @@ +package javalab.umc7th_mission.web.dto; + +import java.time.LocalDateTime; + +public class StoreResponseDTO { + public static class AddResultDTO { + private final Long storeId; + private final LocalDateTime createdAt; + + public AddResultDTO(Long storeId, LocalDateTime createdAt) { + this.storeId = storeId; + this.createdAt = createdAt; + } + + // 명시적 Getter (Jackson 직렬화를 위해 필요) + public Long getMemberId() { + return storeId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + } +}