diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java b/src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java index d9c6b65c..22285d19 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java @@ -21,6 +21,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @RequiredArgsConstructor @Service @Transactional(readOnly = true) @@ -78,4 +80,16 @@ public void deleteFoodTruckImagesFromS3(Long ownerId, Long foodTruckId, DeleteFo foodTruckImageService.deleteFoodTruckImagesFromS3(owner, foodTruckId, request); } + + public FoodTruckDetailResponse getFoodTruckDetails(Long memberId, Long foodTruckId) { + User member = memberValidator.validateAndGetMember(memberId); + + return foodTruckInfoService.getFoodTruckDetails(member, foodTruckId); + } + + public List searchFoodTruckMenus(Long foodTruckId, String keyword, Long memberId) { + User member = memberValidator.validateAndGetMember(memberId); + + return foodTruckMenuService.searchFoodTruckMenus(foodTruckId, keyword, member); + } } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/application/info/FoodTruckInfoService.java b/src/main/java/konkuk/chacall/domain/foodtruck/application/info/FoodTruckInfoService.java index 64c2fea1..60f6d792 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/application/info/FoodTruckInfoService.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/application/info/FoodTruckInfoService.java @@ -9,6 +9,7 @@ import konkuk.chacall.domain.foodtruck.presentation.dto.request.DateRangeRequest; import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckSearchRequest; import konkuk.chacall.domain.foodtruck.presentation.dto.request.UpdateFoodTruckInfoRequest; +import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckDetailResponse; import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckResponse; import konkuk.chacall.domain.member.domain.repository.SavedFoodTruckRepository; import konkuk.chacall.domain.region.domain.model.Region; @@ -133,4 +134,24 @@ private void syncServiceAreas(FoodTruck foodTruck, Set requestedRegionIds) foodTruckServiceAreaRepository.deleteAll(serviceAreasToRemove); } } + + public FoodTruckDetailResponse getFoodTruckDetails(User member, Long foodTruckId) { + FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.FOOD_TRUCK_NOT_FOUND)); + + foodTruck.validateApprovedStatus(); + + switch(member.getRole()) { + case MEMBER -> foodTruck.validateViewableStatusForMember(); + case OWNER -> foodTruck.validateViewableStatusForOwner(member.getUserId()); + case ADMIN -> {} + } + + List foodTruckServiceAreas = foodTruckServiceAreaRepository.findAllByFoodTruckId(foodTruckId); + List availableDates = availableDateRepository.findAllByFoodTruckId(foodTruckId); + + boolean isSaved = savedFoodTruckRepository.existsByMemberIdAndFoodTruckId(member.getUserId(), foodTruckId); + + return FoodTruckDetailResponse.from(foodTruck, foodTruckServiceAreas, availableDates, isSaved); + } } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/application/menu/FoodTruckMenuService.java b/src/main/java/konkuk/chacall/domain/foodtruck/application/menu/FoodTruckMenuService.java index 6eaaef5a..258fc5a5 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/application/menu/FoodTruckMenuService.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/application/menu/FoodTruckMenuService.java @@ -3,10 +3,9 @@ import konkuk.chacall.domain.foodtruck.domain.model.Menu; import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; import konkuk.chacall.domain.foodtruck.domain.repository.MenuRepository; -import konkuk.chacall.domain.foodtruck.domain.value.FoodTruckStatus; import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckMenuRequest; import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckMenuResponse; -import konkuk.chacall.domain.owner.presentation.dto.response.MyFoodTruckMenuResponse; +import konkuk.chacall.domain.user.domain.model.User; import konkuk.chacall.global.common.dto.CursorPagingRequest; import konkuk.chacall.global.common.dto.CursorPagingResponse; import konkuk.chacall.global.common.dto.SortType; @@ -48,4 +47,16 @@ public CursorPagingResponse getFoodTruckMenus(Long foodTr return CursorPagingResponse.of(content, FoodTruckMenuResponse::menuId, menuSlice.hasNext()); } + + public List searchFoodTruckMenus(Long foodTruckId, String keyword, User member) { + if(!foodTruckRepository.existsById(foodTruckId)) { + throw new EntityNotFoundException(ErrorCode.FOOD_TRUCK_NOT_FOUND); + } + + List menus = menuRepository.searchByKeyword(foodTruckId, keyword); + + return menus.stream() + .map(FoodTruckMenuResponse::from) + .toList(); + } } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java b/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java index aea4a7d8..8eed743f 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java @@ -36,4 +36,10 @@ public static AvailableDate createAvailableDate(LocalDate startDate, LocalDate e .foodTruck(foodTruck) .build(); } + + public String formatDate() { + return startAt + " ~ " + endAt; + } + + } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java b/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java index fb6b62cb..aa13f095 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java @@ -143,6 +143,13 @@ public String getServiceAreas(List serviceAreaList) { .collect(Collectors.joining(", ")); } + // 푸드트럭의 운영 기간을 반환해주는 메서드 + public List getAvailableDates(List availableDateList) { + return availableDateList.stream() + .map(AvailableDate::formatDate) + .collect(Collectors.toList()); + } + public void changeViewedStatus(FoodTruckViewedStatus targetViewedStatus) { if(this.foodTruckViewedStatus == targetViewedStatus) { throw new DomainRuleException(ErrorCode.INVALID_FOOD_TRUCK_STATUS_TRANSITION); @@ -155,4 +162,16 @@ public void changeViewedStatus(FoodTruckViewedStatus targetViewedStatus) { this.foodTruckViewedStatus = targetViewedStatus; } + + public void validateViewableStatusForMember() { + if (this.foodTruckViewedStatus != FoodTruckViewedStatus.ON) { + throw new DomainRuleException(ErrorCode.FOOD_TRUCK_NOT_VIEWABLE); + } + } + + public void validateViewableStatusForOwner(Long userId) { + if(!isOwnedBy(userId)) { // 자신이 소유한 푸드트럭이 아니면 + validateViewableStatusForMember(); + } + } } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java b/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java index 434641a0..4d27221d 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java @@ -6,9 +6,14 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; + public interface AvailableDateRepository extends JpaRepository { @Modifying @Query("DELETE FROM AvailableDate ad WHERE ad.foodTruck.foodTruckId = :foodTruckId") void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId); + + @Query("SELECT ad FROM AvailableDate ad WHERE ad.foodTruck.foodTruckId = :foodTruckId") + List findAllByFoodTruckId(Long foodTruckId); } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java b/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java index b11ef709..b2d36912 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java @@ -1,7 +1,6 @@ package konkuk.chacall.domain.foodtruck.domain.repository; import konkuk.chacall.domain.foodtruck.domain.model.Menu; -import konkuk.chacall.domain.foodtruck.domain.value.MenuViewedStatus; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,6 +8,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface MenuRepository extends JpaRepository { @@ -70,4 +70,14 @@ Slice findVisibleMenusAsc(@Param("foodTruckId") Long foodTruckId, """) Optional findByMenuIdAndFoodTruckId(@Param("menuId") Long menuId, @Param("foodTruckId") Long foodTruckId); + + @Query(""" + select m + from Menu m + where m.foodTruck.foodTruckId = :foodTruckId + and m.name like concat('%', :keyword, '%') + and m.menuViewedStatus = konkuk.chacall.domain.foodtruck.domain.value.MenuViewedStatus.ON + order by m.menuId desc + """) + List searchByKeyword(Long foodTruckId, String keyword); } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java b/src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java index f6b19cb1..c84df2e0 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java @@ -25,6 +25,8 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.*; +import java.util.List; + @Tag(name = "FoodTruck API", description = "푸드트럭 관련 API") @RestController @RequiredArgsConstructor @@ -106,7 +108,7 @@ public BaseResponse> getFoodTruckMen description = "승인이 완료된 나의 푸드트럭 정보를 기입하거나 수정합니다." ) @ExceptionDescription(SwaggerResponseDescription.UPDATE_FOOD_TRUCK_INFO) - @PutMapping("{foodTruckId}") + @PutMapping("/{foodTruckId}") public BaseResponse updateMyFoodTruckInfo( @Parameter(description = "푸드트럭 ID", example = "1") @PathVariable final Long foodTruckId, @Valid @RequestBody final UpdateFoodTruckInfoRequest request, @@ -120,7 +122,7 @@ public BaseResponse updateMyFoodTruckInfo( description = "S3에서 푸드트럭/메뉴 이미지 객체를 삭제합니다. 사용자가 기존 푸드트럭/메뉴 이미지를 삭제했을 경우 호출해주세요." ) @ExceptionDescription(SwaggerResponseDescription.DELETE_FOOD_TRUCK_IMAGES) - @DeleteMapping("{foodTruckId}/images") + @DeleteMapping("/{foodTruckId}/images") public BaseResponse deleteFoodTruckImagesFromS3( @Parameter(description = "푸드트럭 ID", example = "1") @PathVariable final Long foodTruckId, @Valid @RequestBody final DeleteFoodTruckImagesRequest request, @@ -130,5 +132,30 @@ public BaseResponse deleteFoodTruckImagesFromS3( return BaseResponse.ok(null); } + @Operation( + summary = "푸드트럭 상세조회", + description = "푸드트럭의 상세 정보를 조회합니다." + ) + @ExceptionDescription(SwaggerResponseDescription.GET_FOOD_TRUCK_DETAILS) + @GetMapping("/{foodTruckId}") + public BaseResponse getFoodTruckDetails( + @Parameter(description = "푸드트럭 ID", example = "1") @PathVariable final Long foodTruckId, + @Parameter(hidden = true) @UserId final Long memberId + ) { + return BaseResponse.ok(foodTruckService.getFoodTruckDetails(memberId, foodTruckId)); + } + @Operation( + summary = "푸드트럭 메뉴 검색", + description = "푸드트럭 메뉴를 이름으로 검색합니다." + ) + @ExceptionDescription(SwaggerResponseDescription.SEARCH_FOOD_TRUCK_MENUS) + @GetMapping("/{foodTruckId}/menus/search") + public BaseResponse> searchFoodTruckMenus( + @Parameter(description = "푸드트럭 ID", example = "1") @PathVariable final Long foodTruckId, + @Parameter(description = "검색 키워드", example = "치킨") @RequestParam("keyword") final String keyword, + @Parameter(hidden = true) @UserId final Long memberId + ) { + return BaseResponse.ok(foodTruckService.searchFoodTruckMenus(foodTruckId, keyword, memberId)); + } } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckDetailResponse.java b/src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckDetailResponse.java new file mode 100644 index 00000000..0ec65f2a --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckDetailResponse.java @@ -0,0 +1,69 @@ +package konkuk.chacall.domain.foodtruck.presentation.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import konkuk.chacall.domain.foodtruck.domain.model.AvailableDate; +import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; +import konkuk.chacall.domain.foodtruck.domain.model.FoodTruckServiceArea; +import konkuk.chacall.domain.foodtruck.domain.value.FoodTruckInfo; + +import java.util.List; + +public record FoodTruckDetailResponse( + @Schema(description = "푸드트럭 식별자", example = "1") + Long foodTruckId, + @Schema(description = "푸드트럭 이름", example = "푸드트럭") + String name, + @Schema(description = "푸드트럭 설명", example = "맛있는 푸드트럭입니다.") + String description, + @Schema(description = "푸드트럭 전화번호", example = "010-1234-5678") + String phoneNumber, + @Schema(description = "푸드트럭 활동 시간", example = "09:00-20:00") + String activeTime, + @Schema(description = "시간 협의 필요 여부", example = "false") + Boolean timeDiscussRequired, + @Schema(description = "호출 가능 지역", example = "서울 광진구, 서울 강남구, 서울 영등포구") + String serviceAreas, + @Schema(description = "푸드트럭 메뉴 카테고리 (라벨 리스트)", example = "[\"한식\",\"분식\"]") + List menuCategories, + @Schema(description = "푸드트럭 제공 가능 수량", example = "200인분 미만") + String availableQuantity, + @Schema(description = "전기 사용 필요 여부", example = "필요") + String needElectricity, + @Schema(description = "결제 방법", example = "무관") + String paymentMethod, + @Schema(description = "푸드트럭 제공 가능 날짜 리스트", example = "[\"2025-10-01 ~ 2025-10-10\",\"2025-11-01 ~ 2025-11-10\"]") + List availableDates, + @Schema(description = "푸드트럭 사진 URL 리스트", example = "[\"http://image.png\",\"http://image2.png\",\"http://image3.png\"]") + List photoUrl, + @Schema(description = "운영 정보", example = "운영정보") + String operatingInfo, + @Schema(description = "추가 옵션 정보", example = "안녕하세요") + String option, + @Schema(description = "푸드트럭 평균 평점", example = "4.5") + Double averageRating, + @Schema(description = "현재 사용자가 저장한 푸드트럭인지 여부", example = "true") + Boolean isSaved +) { + public static FoodTruckDetailResponse from(FoodTruck foodTruck, List serviceAreas, List availableDates, Boolean isSaved) { + FoodTruckInfo foodTruckInfo = foodTruck.getFoodTruckInfo(); + return new FoodTruckDetailResponse( + foodTruck.getFoodTruckId(), + foodTruckInfo.getName(), + foodTruckInfo.getDescription(), + foodTruckInfo.getPhoneNumber(), + foodTruckInfo.getActiveTime(), + foodTruckInfo.getTimeDiscussRequired(), + foodTruck.getServiceAreas(serviceAreas), + foodTruckInfo.getMenuCategoryList().getMenuCategoryLabelList(), + foodTruckInfo.getAvailableQuantity().getValue(), + foodTruckInfo.getNeedElectricity().getValue(), + foodTruckInfo.getPaymentMethod().getValue(), + foodTruck.getAvailableDates(availableDates), + foodTruckInfo.getFoodTruckPhotoList().getUrls(), + foodTruckInfo.getOperatingInfo(), + foodTruckInfo.getOption(), + foodTruck.getRatingInfo().getAverageRating(), + isSaved + ); + } +} diff --git a/src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java b/src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java index dc82731f..089cb300 100644 --- a/src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java +++ b/src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java @@ -48,4 +48,14 @@ Slice findMemberSavedFoodTruckWithCursor( Set findSavedTruckIdsIn(@Param("userId") Long userId, @Param("foodTruckIds") List foodTruckIds); + @Query(""" + select exists ( + select s + from SavedFoodTruck s + where s.member.userId = :userId + and s.foodTruck.foodTruckId = :foodTruckId + ) + """) + boolean existsByMemberIdAndFoodTruckId(@Param("userId") Long userId, + @Param("foodTruckId") Long foodTruckId); } diff --git a/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java index 4dcb4978..cee2d804 100644 --- a/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java @@ -83,6 +83,7 @@ public enum ErrorCode implements ResponseCode { AVAILABLE_QUANTITY_MISMATCH(HttpStatus.BAD_REQUEST, 110007, "제조 가능 수량 값이 올바르지 않습니다."), NEED_ELECTRICITY_MISMATCH(HttpStatus.BAD_REQUEST, 110008, "전기 사용 여부 값이 올바르지 않습니다."), PAYMENT_METHOD_MISMATCH(HttpStatus.BAD_REQUEST, 110009, "결제 수단 값이 올바르지 않습니다."), + FOOD_TRUCK_NOT_VIEWABLE(HttpStatus.FORBIDDEN, 110010, "노출 가능한 상태의 푸드트럭이 아닙니다."), /** * Image diff --git a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java index 32c2b4c0..eb65e196 100644 --- a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java @@ -246,6 +246,19 @@ public enum SwaggerResponseDescription { FOOD_TRUCK_NOT_FOUND, FOOD_TRUCK_NOT_OWNED ))), + GET_FOOD_TRUCK_DETAILS(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + USER_FORBIDDEN, + FOOD_TRUCK_NOT_FOUND, + FOOD_TRUCK_NOT_OWNED, + FOOD_TRUCK_NOT_VIEWABLE, + FOOD_TRUCK_NOT_APPROVED + ))), + SEARCH_FOOD_TRUCK_MENUS(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + USER_FORBIDDEN, + FOOD_TRUCK_NOT_FOUND + ))), // Default DEFAULT(new LinkedHashSet<>())