-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 푸드트럭 상세조회 API 구현 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d705877
4d18543
8dc5c08
3079d66
051574c
926f01e
02a7dbf
636caa4
f892e23
b53bdf1
cb6894b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Long> 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<FoodTruckServiceArea> foodTruckServiceAreas = foodTruckServiceAreaRepository.findAllByFoodTruckId(foodTruckId); | ||
| List<AvailableDate> availableDates = availableDateRepository.findAllByFoodTruckId(foodTruckId); | ||
|
|
||
| boolean isSaved = savedFoodTruckRepository.existsByMemberIdAndFoodTruckId(member.getUserId(), foodTruckId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SavedFoodTruckRepository의 버그로 인해 이 코드가 실패합니다.
🤖 Prompt for AI Agents |
||
|
|
||
| return FoodTruckDetailResponse.from(foodTruck, foodTruckServiceAreas, availableDates, isSaved); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<FoodTruckMenuResponse> getFoodTruckMenus(Long foodTr | |
|
|
||
| return CursorPagingResponse.of(content, FoodTruckMenuResponse::menuId, menuSlice.hasNext()); | ||
| } | ||
|
|
||
| public List<FoodTruckMenuResponse> searchFoodTruckMenus(Long foodTruckId, String keyword, User member) { | ||
| if(!foodTruckRepository.existsById(foodTruckId)) { | ||
| throw new EntityNotFoundException(ErrorCode.FOOD_TRUCK_NOT_FOUND); | ||
| } | ||
|
|
||
| List<Menu> menus = menuRepository.searchByKeyword(foodTruckId, keyword); | ||
|
|
||
| return menus.stream() | ||
| .map(FoodTruckMenuResponse::from) | ||
| .toList(); | ||
| } | ||
|
Comment on lines
+51
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR 목표에 명시된 역할별 유효성 검증 로직이 누락되었습니다. PR 설명에 따르면 다음과 같은 역할별 검증이 필요합니다:
현재 구현은 다음과 같이 수정하세요: public List<FoodTruckMenuResponse> searchFoodTruckMenus(Long foodTruckId, String keyword, User member) {
- if(!foodTruckRepository.existsById(foodTruckId)) {
- throw new EntityNotFoundException(ErrorCode.FOOD_TRUCK_NOT_FOUND);
- }
+ 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<Menu> menus = menuRepository.searchByKeyword(foodTruckId, keyword);
return menus.stream()
.map(FoodTruckMenuResponse::from)
.toList();
}
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,6 +143,13 @@ public String getServiceAreas(List<FoodTruckServiceArea> serviceAreaList) { | |
| .collect(Collectors.joining(", ")); | ||
| } | ||
|
|
||
| // 푸드트럭의 운영 기간을 반환해주는 메서드 | ||
| public List<String> getAvailableDates(List<AvailableDate> 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(); | ||
| } | ||
| } | ||
|
Comment on lines
+166
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 검증 로직 따로 뺀 것 좋습니다~ |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<CursorPagingResponse<FoodTruckMenuResponse>> getFoodTruckMen | |
| description = "승인이 완료된 나의 푸드트럭 정보를 기입하거나 수정합니다." | ||
| ) | ||
| @ExceptionDescription(SwaggerResponseDescription.UPDATE_FOOD_TRUCK_INFO) | ||
| @PutMapping("{foodTruckId}") | ||
| @PutMapping("/{foodTruckId}") | ||
| public BaseResponse<FoodTruckIdResponse> updateMyFoodTruckInfo( | ||
| @Parameter(description = "푸드트럭 ID", example = "1") @PathVariable final Long foodTruckId, | ||
| @Valid @RequestBody final UpdateFoodTruckInfoRequest request, | ||
|
|
@@ -120,7 +122,7 @@ public BaseResponse<FoodTruckIdResponse> updateMyFoodTruckInfo( | |
| description = "S3에서 푸드트럭/메뉴 이미지 객체를 삭제합니다. 사용자가 기존 푸드트럭/메뉴 이미지를 삭제했을 경우 호출해주세요." | ||
| ) | ||
| @ExceptionDescription(SwaggerResponseDescription.DELETE_FOOD_TRUCK_IMAGES) | ||
| @DeleteMapping("{foodTruckId}/images") | ||
| @DeleteMapping("/{foodTruckId}/images") | ||
| public BaseResponse<Void> deleteFoodTruckImagesFromS3( | ||
| @Parameter(description = "푸드트럭 ID", example = "1") @PathVariable final Long foodTruckId, | ||
| @Valid @RequestBody final DeleteFoodTruckImagesRequest request, | ||
|
|
@@ -130,5 +132,30 @@ public BaseResponse<Void> deleteFoodTruckImagesFromS3( | |
| return BaseResponse.ok(null); | ||
| } | ||
|
|
||
| @Operation( | ||
| summary = "푸드트럭 상세조회", | ||
| description = "푸드트럭의 상세 정보를 조회합니다." | ||
| ) | ||
| @ExceptionDescription(SwaggerResponseDescription.GET_FOOD_TRUCK_DETAILS) | ||
| @GetMapping("/{foodTruckId}") | ||
| public BaseResponse<FoodTruckDetailResponse> 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<List<FoodTruckMenuResponse>> 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)); | ||
| } | ||
|
Comment on lines
+148
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain빈 문자열 검증 확인 필요 메뉴 검색 엔드포인트가 잘 구현되어 있으나, 빈 문자열로 검색 시 의도하지 않은 결과(예: 모든 메뉴 반환)가 발생할 수 있습니다. 필요시 검증 스크립트로 서비스 레이어에서 키워드 검증이 있는지 확인: 🏁 Script executed: #!/bin/bash
# Description: Check if keyword validation exists in service layer
# Search for searchFoodTruckMenus method implementation
ast-grep --pattern $'searchFoodTruckMenus($$$) {
$$$
}'
# Also check for blank validation patterns
rg -nP --type=java -A10 'searchFoodTruckMenus.*keyword' -g '!**/test/**'Length of output: 2621 키워드 빈 문자열 검증 필요 코드 검토 결과, 현재 구현:
해결 방안 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String> 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<String> availableDates, | ||
| @Schema(description = "푸드트럭 사진 URL 리스트", example = "[\"http://image.png\",\"http://image2.png\",\"http://image3.png\"]") | ||
| List<String> 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<FoodTruckServiceArea> serviceAreas, List<AvailableDate> 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 | ||
| ); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.