Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import konkuk.chacall.domain.foodtruck.application.image.FoodTruckImageService;
import konkuk.chacall.domain.foodtruck.application.info.FoodTruckInfoService;
import konkuk.chacall.domain.foodtruck.application.menu.FoodTruckMenuService;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckMenuRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckNameDuplicateCheckRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckSearchRequest;
import konkuk.chacall.global.common.storage.dto.ImageRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckMenuResponse;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckNameDuplicateCheckResponse;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckResponse;
import konkuk.chacall.global.common.storage.dto.ImageResponse;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.*;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.*;
import konkuk.chacall.domain.owner.application.validator.OwnerValidator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,45 @@
import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck;
import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.DeleteFoodTruckImagesRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.ImageRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.ImageResponse;
import konkuk.chacall.domain.user.domain.model.User;
import konkuk.chacall.global.common.exception.BusinessException;
import konkuk.chacall.global.common.exception.code.ErrorCode;
import konkuk.chacall.global.common.storage.S3Service;
import konkuk.chacall.global.common.storage.util.AllowedFileExtension;
import konkuk.chacall.global.common.storage.dto.ImageRequest;
import konkuk.chacall.global.common.storage.dto.ImageResponse;
import konkuk.chacall.domain.user.domain.model.User;
import konkuk.chacall.global.common.storage.presign.PresignedUrlService;
import konkuk.chacall.global.common.storage.util.CdnUrlResolver;
import konkuk.chacall.global.common.storage.util.KeyUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class FoodTruckImageService {

private final S3Service s3Service;
private final CdnUrlResolver cdnUrlResolver;
private final PresignedUrlService presignedUrlService;
private static final int MAX_FOOD_TRUCK_IMAGE_COUNT = 9;

private final FoodTruckRepository foodTruckRepository;
// private static final int MAX_MENU_IMAGE_COUNT = 5;
private final S3Service s3Service;
private final CdnUrlResolver cdnUrlResolver;

public ImageResponse createFoodTruckImagePresignedUrl(ImageRequest request, User owner) {
List<String> fileExtensions = request.fileExtensions();

if(fileExtensions.size() > MAX_FOOD_TRUCK_IMAGE_COUNT) {
throw new BusinessException(ErrorCode.INVALID_IMAGE_COUNT,
new IllegalArgumentException("푸드트럭 이미지는 1장 이상 " + MAX_FOOD_TRUCK_IMAGE_COUNT + "장 이하로 등록해야 합니다. 입력된 이미지 개수: " + fileExtensions.size()));
}

AllowedFileExtension.checkAllowedExtension(fileExtensions);

var imageInfos = fileExtensions.stream()
.map(extension -> {
String baseKey = KeyUtils.buildFoodTruckImageKey(owner.getUserId());
String keyWithExt = baseKey + "." + extension.toLowerCase();

String presignedUrl = s3Service.generatePresignedUrl(keyWithExt);
String fileUrl = s3Service.getFileUrl(keyWithExt);

return ImageResponse.ImageInfo.of(presignedUrl, fileUrl);
})
.toList();

return ImageResponse.of(imageInfos);
return presignedUrlService.generatePresignedUrls(
request,
owner.getUserId(),
MAX_FOOD_TRUCK_IMAGE_COUNT,
KeyUtils::buildFoodTruckImageKey
);
}

public ImageResponse createMenuImagePresignedUrl(ImageRequest request, User owner) {
List<String> fileExtensions = request.fileExtensions();

AllowedFileExtension.checkAllowedExtension(fileExtensions);

var imageInfos = fileExtensions.stream()
.map(extension -> {
String baseKey = KeyUtils.buildMenuImageKey(owner.getUserId());
String keyWithExt = baseKey + "." + extension.toLowerCase();

String presignedUrl = s3Service.generatePresignedUrl(keyWithExt);
String fileUrl = s3Service.getFileUrl(keyWithExt);

return ImageResponse.ImageInfo.of(presignedUrl, fileUrl);
})
.toList();

return ImageResponse.of(imageInfos);
return presignedUrlService.generatePresignedUrls(
request,
owner.getUserId(),
null,
KeyUtils::buildMenuImageKey
);
}

public void deleteFoodTruckImagesFromS3(User owner, Long foodTruckId, DeleteFoodTruckImagesRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import konkuk.chacall.global.common.exception.DomainRuleException;
import konkuk.chacall.global.common.exception.code.ErrorCode;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;

import java.util.List;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -44,9 +45,8 @@ public class FoodTruck extends BaseEntity {
@Column(nullable = false, length = 3)
private FoodTruckViewedStatus foodTruckViewedStatus = FoodTruckViewedStatus.OFF;

//todo 컬럼 추가 후 기존 데이터 변경한 다음 다시 주석 해제할 예정
@Builder.Default
// @Column(nullable = false)
@ColumnDefault("false")
private Boolean canChangeViewedStatus = false;

@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import konkuk.chacall.domain.foodtruck.application.FoodTruckService;
import konkuk.chacall.global.common.storage.dto.ImageRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckMenuRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckNameDuplicateCheckRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckSearchRequest;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckMenuResponse;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckNameDuplicateCheckResponse;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckResponse;
import konkuk.chacall.global.common.storage.dto.ImageResponse;
import konkuk.chacall.domain.foodtruck.presentation.dto.request.*;
import konkuk.chacall.domain.foodtruck.presentation.dto.response.*;
import konkuk.chacall.global.common.annotation.ExceptionDescription;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck;
import konkuk.chacall.domain.foodtruck.domain.value.FoodTruckInfo;

import java.util.List;

public record SavedFoodTruckResponse(
@Schema(description = "푸드트럭 식별자", example = "1")
Long foodTruckId,
Expand All @@ -13,6 +15,8 @@ public record SavedFoodTruckResponse(
String photoUrl,
@Schema(description = "푸드트럭 설명", example = "맛있는 푸드트럭입니다.")
String description,
@Schema(description = "푸드트럭 음식 카테고리 (라벨 리스트)", example = "[\"한식\",\"분식\"]")
List<String> menuCategories,
@Schema(description = "푸드트럭 평균 평점", example = "4.5")
Double averageRating,
@Schema(description = "푸드트럭 평점 수", example = "100")
Expand All @@ -25,6 +29,7 @@ public static SavedFoodTruckResponse of(FoodTruck foodTruck) {
foodTruckInfo.getName(),
foodTruckInfo.getFoodTruckPhotoList().getMainPhotoUrl(), // 대표 사진 (첫 번째 사진)
foodTruckInfo.getDescription(),
foodTruckInfo.getMenuCategoryList().getMenuCategoryLabelList(),
foodTruck.getRatingInfo().getAverageRating(),
foodTruck.getRatingInfo().getRatingCount()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package konkuk.chacall.domain.owner.application;

import jakarta.validation.Valid;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

@Valid import 사용 여부를 확인하세요.

이 PR의 변경사항에서 @Valid 어노테이션이 사용되는 것이 보이지 않습니다. 실제로 사용되지 않는다면 불필요한 import일 수 있습니다.

다음 스크립트로 해당 어노테이션의 사용 여부를 확인하세요:


🏁 Script executed:

#!/bin/bash
# Description: Check if @Valid annotation is actually used in OwnerService

rg -n "@Valid" src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java

Length of output: 0


🏁 Script executed:

#!/bin/bash
# View the OwnerService.java file to confirm the import and check for any @Valid usage
cat src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java | head -50

Length of output: 2396


🏁 Script executed:

#!/bin/bash
# Check full file and search for @Valid usage one more time to be thorough
wc -l src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java
rg -n "@Valid" src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java || echo "No @Valid found in file"

Length of output: 99


jakarta.validation.Valid import를 제거하세요.

파일의 223줄 전체를 검색한 결과, @Valid 어노테이션이 어디에도 사용되지 않습니다. 3번 줄의 import 문은 불필요하므로 삭제해야 합니다.

🤖 Prompt for AI Agents
src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java lines 3:
remove the unused import "import jakarta.validation.Valid;" since a project-wide
search shows @Valid is not used anywhere in this file; delete that import line
(or run your IDE's optimize/organize imports) and rebuild to ensure no
compilation issues.

import konkuk.chacall.domain.owner.application.myfoodtruckmenu.MyFoodTruckMenuService;
import konkuk.chacall.domain.owner.presentation.dto.request.MyFoodTruckMenuRequest;
import konkuk.chacall.domain.owner.presentation.dto.response.MyFoodTruckMenuResponse;
Expand All @@ -14,6 +15,10 @@
import konkuk.chacall.global.common.dto.CursorPagingResponse;
import konkuk.chacall.global.common.dto.CursorPagingRequest;
import konkuk.chacall.global.common.dto.SortType;
import konkuk.chacall.global.common.storage.dto.ImageRequest;
import konkuk.chacall.global.common.storage.dto.ImageResponse;
import konkuk.chacall.global.common.storage.presign.PresignedUrlService;
import konkuk.chacall.global.common.storage.util.KeyUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -30,6 +35,9 @@ public class OwnerService {
private final OwnerReservationService ownerReservationService;
private final MyFoodTruckService myFoodTruckService;
private final MyFoodTruckMenuService myFoodTruckMenuService;
private final PresignedUrlService presignedUrlService;

private static final int FOOD_TRUCK_DOCUMENT_IMAGE_MAX_COUNT = 6;

// 파사드에서 사장님 검증을 거침으로써 서비스 로직에서는 사장님 검증에 신경쓰지 않도록 책임 분리
private final OwnerValidator ownerValidator;
Expand Down Expand Up @@ -200,4 +208,16 @@ public void createNewFoodTruck(Long ownerId, FoodTruckCreateRequest request) {
// 푸드트럭 최초 등록
myFoodTruckService.createNewFoodTruck(owner, request);
}

public ImageResponse createFoodTruckDocumentPresignedUrls(ImageRequest request, Long ownerId) {
// 사장님인지 먼저 검증
User owner = ownerValidator.validateAndGetOwner(ownerId);

return presignedUrlService.generatePresignedUrls(
request,
owner.getUserId(),
FOOD_TRUCK_DOCUMENT_IMAGE_MAX_COUNT,
KeyUtils::buildFoodTruckDocumentImageKey
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import konkuk.chacall.global.common.dto.BaseResponse;
import konkuk.chacall.global.common.dto.CursorPagingRequest;
import konkuk.chacall.global.common.dto.CursorPagingResponse;
import konkuk.chacall.global.common.storage.dto.ImageRequest;
import konkuk.chacall.global.common.storage.dto.ImageResponse;
import konkuk.chacall.global.common.swagger.SwaggerResponseDescription;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -299,4 +301,17 @@ public BaseResponse<Void> createNewFoodTruck(
ownerService.createNewFoodTruck(ownerId, request);
return BaseResponse.ok(null);
}

@Operation(
summary = "푸드트럭 관련 서류 업로드를 위한 presigned URL 발급",
description = "푸드트럭 관련 서류 업로드를 위한 presigned URL을 발급받습니다."
)
@ExceptionDescription(SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL)

@coderabbitai coderabbitai Bot Oct 25, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

CI 컴파일 오류: SwaggerResponseDescription 상수 미존재

SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL 심볼을 찾을 수 없어 CI가 실패합니다. 상수명 오타(PRESIGNED) 여부 확인 후 하나를 선택해 주세요.

  • 이미 존재하는 상수명으로 교체(권장): 예) OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGNED_URL
  • 또는 SwaggerResponseDescription에 동일 키를 신규 정의

빠른 수정을 위한 예시(교체 가정):

-@ExceptionDescription(SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL)
+@ExceptionDescription(SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGNED_URL)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: CI with Gradle

[error] 309-309: Cannot find symbol: SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL.

🤖 Prompt for AI Agents
In src/main/java/konkuk/chacall/domain/owner/presentation/OwnerController.java
around line 309, the annotation references a non-existent constant
SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL due to a
typo; replace that reference with the correct existing constant name (e.g.,
OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGNED_URL) or alternatively add the exact
PRESIGEND key to SwaggerResponseDescription. Update the reference to the correct
constant name to match the enum/class definition, rebuild to confirm CI passes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이 부분 한 번만 확인부탁드립니다~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check all constants in SwaggerResponseDescription.java
cat src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java | grep -E "^\s*(OWNER_|GET_)" | head -40

Length of output: 1504


🏁 Script executed:

#!/bin/bash
# Search for any FOOD_TRUCK_DOCUMENT related constants
rg -n "FOOD_TRUCK_DOCUMENT" --type=java

Length of output: 451


🏁 Script executed:

#!/bin/bash
# Check for PRESIGNED vs PRESIGEND typos in the codebase
rg -n "PRESIGEND" --type=java

Length of output: 1112


@ksg1227 네, 확인했습니다!

문제 확인:
OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL 상수가 SwaggerResponseDescription.java에 정의되어 있지 않아 컴파일 오류가 발생하고 있습니다.

해결 방법:
SwaggerResponseDescription.java 파일에 해당 상수를 추가해야 합니다. 기존 패턴을 참고하여 다음과 같이 추가하시면 됩니다:

OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL(new LinkedHashSet<>(Set.of(
    OWNER_NOT_FOUND,
    INVALID_IMAGE_COUNT,
    INVALID_FILE_EXTENSION
))),

참고 사항:
코드베이스 전반에 걸쳐 "PRESIGNED"가 아닌 "PRESIGEND"로 일관되게 사용되고 있습니다:

  • GET_USER_PROFILE_PRESIGEND_URL
  • GET_FOOD_TRUCK_PRESIGEND_URL
  • GET_MENU_PRESIGEND_URL

기존 코드와의 일관성을 위해 PRESIGEND 철자를 유지하는 것이 좋을 것 같습니다. 혹시 전체적으로 PRESIGNED로 수정하고 싶으시다면 별도로 알려주세요!


🧬 Code Graph Analysis Results

[src/main/java/konkuk/chacall/domain/owner/application/reservation/OwnerReservationService.java, lines 27-90]

@RequiredArgsConstructor
@Service
public class OwnerReservationService {

    private final ReservationRepository reservationRepository;
    private final UserRepository userRepository;

    public CursorPagingResponse<OwnerReservationHistoryResponse> getOwnerReservations(Long ownerId, ReservationViewType viewType, Long lastCursor, int pageSize) {
        // 예약 목록 조회
        Slice<Reservation> ownerReservationSlice = findReservations(ownerId, viewType.getStatuses(), lastCursor, pageSize);
        List<Reservation> ownerReservationList = ownerReservationSlice.getContent();

        // 손님 ID 리스트로 User 정보 한 번에 조회
        Map<Long, User> customerMap = getCustomerMap(ownerReservationList);

        // 미리 조회한 User 정보를 사용해 DTO 로 변환
        List<OwnerReservationHistoryResponse> responses = mapToReservationHistory(ownerReservationList, customerMap);

        return CursorPagingResponse.of(responses, OwnerReservationHistoryResponse::reservationId, ownerReservationSlice.hasNext());
    }

    public OwnerReservationDetailResponse getReservationDetail(Long ownerId, Long reservationId) {
        // ID로 예약 정보와 연관된 모든 데이터 한 번에 조회
        Reservation reservation = reservationRepository.findByIdWithDetails(reservationId)
                .orElseThrow(() -> new EntityNotFoundException(ErrorCode.RESERVATION_NOT_FOUND));

        reservation.validateFoodTruckOwner(ownerId);

        // DTO 로 변환하여 반환
        return OwnerReservationDetailResponse.of(reservation, reservation.getMember());
    }

    /**
     * 예약 목록 조회
     */
    private Slice<Reservation> findReservations(Long ownerId, Set<ReservationStatus> statuses, Long lastCursor, int pageSize) {
        return reservationRepository
                .findOwnerReservationsByStatusWithCursor(ownerId, statuses, lastCursor, PageRequest.of(0, pageSize));
    }

    /**
     * 예약 목록에 포함된 손님 정보 Map 조회
     */
    private Map<Long, User> getCustomerMap(List<Reservation> reservations) {
        List<Long> customerIds = reservations.stream()
                .map(reservation -> reservation.getMember().getUserId())
                .toList();

        return userRepository.findAllByUserIdInAndRoleAndStatus(customerIds, Role.MEMBER, BaseStatus.ACTIVE).stream()
                .collect(Collectors.toMap(User::getUserId, user -> user));
    }

    /**
     * 예약 목록과 손님 정보를 조합하여 DTO 리스트로 변환
     */
    private List<OwnerReservationHistoryResponse> mapToReservationHistory(List<Reservation> reservations, Map<Long, User> customerMap) {
        return reservations.stream()
                .map(reservation -> {
                    User customer = customerMap.get(reservation.getMember().getUserId());
                    return OwnerReservationHistoryResponse.of(reservation, customer);
                })
                .toList();
    }
}

[src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java, lines 30-144]

@RequiredArgsConstructor
@Service
public class MyFoodTruckService {

    private final FoodTruckRepository foodTruckRepository;
    private final FoodTruckServiceAreaRepository foodTruckServiceAreaRepository;
    private final MenuRepository menuRepository;
    private final ReservationRepository reservationRepository;
    private final SavedFoodTruckRepository savedFoodTruckRepository;
    private final AvailableDateRepository availableDateRepository;
    private final RatingRepository ratingRepository;
    private final FoodTruckDocumentRepository foodTruckDocumentRepository;

    public CursorPagingResponse<MyFoodTruckResponse> getMyFoodTrucks(CursorPagingRequest request, Long ownerId) {
        // 1. 커서 기반으로 푸드트럭 Slice 조회
        Slice<FoodTruck> foodTruckSlice = findFoodTrucks(ownerId, request.cursor(), request.size());
        List<FoodTruck> foodTrucks = foodTruckSlice.getContent();

        // 2. 호출 가능 지역 정보 Map 조회
        Map<Long, List<FoodTruckServiceArea>> serviceAreaMap = getServiceAreaMap(foodTrucks);

        // 3. DTO 리스트 생성
        List<MyFoodTruckResponse> responses = mapToMyFoodTruckResponse(foodTrucks, serviceAreaMap);

        return CursorPagingResponse.of(responses, MyFoodTruckResponse::foodTruckId, foodTruckSlice.hasNext());
    }

    public void deleteMyFoodTruck(Long ownerId, Long foodTruckId) {
        // 푸드트럭 조회 및 소유권 확인
        FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId)
                .orElseThrow(() -> new EntityNotFoundException(ErrorCode.FOOD_TRUCK_NOT_FOUND));

        foodTruck.validateOwner(ownerId);

        // 푸드트럭 호출 가능 지역 삭제
        foodTruckServiceAreaRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 메뉴 삭제
        menuRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 가능한 일정대 삭제
        availableDateRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 관련 평점 삭제
        ratingRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 관련 예약 삭제
        reservationRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 저장한 푸드트럭 삭제
        savedFoodTruckRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 삭제
        foodTruckRepository.delete(foodTruck);
    }

    /**
     * 커서 기반으로 페이징된 푸드트럭 목록 조회
     */
    private Slice<FoodTruck> findFoodTrucks(Long ownerId, Long lastCursor, int pageSize) {
        return foodTruckRepository.findByOwnerUserIdWithCursor(ownerId, lastCursor, PageRequest.of(0, pageSize));
    }

    /**
     * 푸드트럭 목록에 포함된 호출 가능 지역 정보 Map 조회
     */
    private Map<Long, List<FoodTruckServiceArea>> getServiceAreaMap(List<FoodTruck> foodTrucks) {
        List<Long> foodTruckIds = foodTrucks.stream()
                .map(FoodTruck::getFoodTruckId)
                .toList();

        List<FoodTruckServiceArea> serviceAreas = foodTruckServiceAreaRepository
                .findAllWithRegionByFoodTruckIdIn(foodTruckIds);

        return serviceAreas.stream()
                .collect(Collectors.groupingBy(sa -> sa.getFoodTruck().getFoodTruckId()));
    }

    public void updateFoodTruckViewedStatus(Long ownerId, Long foodTruckId, UpdateFoodTruckViewedStatusRequest request) {

        // 본인 소유인지, 푸드트럭이 승인 완료된 상태인지 검증
        FoodTruck foodTruck = foodTruckRepository.findByFoodTruckIdAndOwnerIdAndFoodTruckStatus(foodTruckId, ownerId, FoodTruckStatus.APPROVED)
                .orElseThrow(() -> new BusinessException(ErrorCode.FOOD_TRUCK_NOT_APPROVED));

        // 메뉴 표시 여부 변경 및 상태 전이 검증
        foodTruck.changeViewedStatus(request.status());
    }

    public void createNewFoodTruck(User owner, FoodTruckCreateRequest request) {
        // 1. 비어 있는 푸드트럭 엔티티 생성 (null 필드 초기화)
        FoodTruck foodTruck = FoodTruck.createEmptyFoodTruck(owner, request.name());
        foodTruckRepository.save(foodTruck);

        // 2. 서류 등록 로직 (사업자등록증 + 기타 5장)
        FoodTruckDocument business = FoodTruckDocument.create(foodTruck, DocumentType.BUSINESS_REGISTRATION, request.businessRegistrationUrl());
        foodTruckDocumentRepository.save(business);

        List<FoodTruckDocument> otherDocs = request.otherDocumentUrls().stream()
                .map(url -> FoodTruckDocument.create(foodTruck, DocumentType.OTHER, url))
                .toList();

        foodTruckDocumentRepository.saveAll(otherDocs);
    }

    private List<MyFoodTruckResponse> mapToMyFoodTruckResponse(List<FoodTruck> foodTrucks, Map<Long, List<FoodTruckServiceArea>> serviceAreaMap) {
        return foodTrucks.stream()
                .map(foodTruck -> {
                    List<FoodTruckServiceArea> serviceAreas = serviceAreaMap.getOrDefault(foodTruck.getFoodTruckId(), List.of());
                    return MyFoodTruckResponse.of(foodTruck, serviceAreas);
                })
                .toList();
    }


}

[src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java, lines 28-223]

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class OwnerService {

    private final BankAccountService bankAccountService;
    private final ChatTemplateService chatTemplateService;
    private final OwnerReservationService ownerReservationService;
    private final MyFoodTruckService myFoodTruckService;
    private final MyFoodTruckMenuService myFoodTruckMenuService;
    private final PresignedUrlService presignedUrlService;

    private static final int FOOD_TRUCK_DOCUMENT_IMAGE_MAX_COUNT = 6;

    // 파사드에서 사장님 검증을 거침으로써 서비스 로직에서는 사장님 검증에 신경쓰지 않도록 책임 분리
    private final OwnerValidator ownerValidator;

    @Transactional
    public void registerBankAccount(RegisterBankAccountRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        // 검증된 유저 정보를 넘겨 계좌 등록 로직 호출
        bankAccountService.registerBankAccount(request, owner);
    }

    public BankAccountResponse getBankAccount(Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 계좌 조회 로직 호출
        return bankAccountService.getBankAccount(ownerId);
    }

    @Transactional
    public void updateBankAccount(Long ownerId, Long bankAccountId, UpdateBankAccountRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 계좌 수정 로직 호출
        bankAccountService.updateBankAccount(ownerId, bankAccountId, request);
    }

    @Transactional
    public void deleteBankAccount(Long ownerId, Long bankAccountId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 계좌 삭제 로직 호출
        bankAccountService.deleteBankAccount(ownerId, bankAccountId);
    }

    @Transactional
    public void registerChatTemplate(RegisterChatTemplateRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 등록 로직 호출
        chatTemplateService.registerChatTemplate(request, owner);
    }

    public List<ChatTemplateResponse> getChatTemplates(Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 목록 조회 로직 호출
        return chatTemplateService.getChatTemplates(ownerId);
    }

    @Transactional
    public void updateChatTemplate(UpdateChatTemplateRequest request, Long ownerId, Long chatTemplateId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 수정 로직 호출
        chatTemplateService.updateChatTemplate(request, chatTemplateId);
    }

    @Transactional
    public void deleteChatTemplate(Long ownerId, Long chatTemplateId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 삭제 로직 호출
        chatTemplateService.deleteChatTemplate(chatTemplateId);
    }

    public CursorPagingResponse<OwnerReservationHistoryResponse> getOwnerReservations(GetReservationHistoryRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 예약 내역 조회 로직 호출
        CursorPagingRequest cursorPagingRequest = request.pagingOrDefault(SortType.NEWEST);
        return ownerReservationService.getOwnerReservations(ownerId, request.viewType(), cursorPagingRequest.cursor(), cursorPagingRequest.size());
    }

    public OwnerReservationDetailResponse getReservationDetail(Long ownerId, Long reservationId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 예약 내역 상세 조회 로직 호출
        return ownerReservationService.getReservationDetail(ownerId, reservationId);
    }

    public CursorPagingResponse<MyFoodTruckResponse> getMyFoodTrucks(CursorPagingRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 목록 조회 로직 호출
        CursorPagingRequest cursorPagingRequest = request.pagingOrDefault(SortType.NEWEST);
        return myFoodTruckService.getMyFoodTrucks(cursorPagingRequest, ownerId);
    }

    @Transactional
    public void deleteMyFoodTruck(Long ownerId, Long foodTruckId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 삭제 로직 호출
        myFoodTruckService.deleteMyFoodTruck(ownerId, foodTruckId);
    }

    @Transactional
    public void updateFoodTruckViewedStatus(Long ownerId, Long foodTruckId, UpdateFoodTruckViewedStatusRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 표시 상태 변경
        myFoodTruckService.updateFoodTruckViewedStatus(ownerId, foodTruckId, request);
    }

    public CursorPagingResponse<MyFoodTruckMenuResponse> getMyFoodTruckMenus(Long ownerId, Long foodTruckId, MyFoodTruckMenuRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 조회
        return myFoodTruckMenuService.getMyFoodTruckMenus(ownerId, foodTruckId, request);
    }

    @Transactional
    public void registerMenu(Long ownerId, Long foodTruckId, RegisterMenuRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 추가
        myFoodTruckMenuService.registerMenu(ownerId, foodTruckId, request);
    }

    @Transactional
    public void updateMenuStatus(Long ownerId, Long foodTruckId, Long menuId, UpdateMenuStatusRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 상태 변경
        myFoodTruckMenuService.updateMenuStatus(ownerId, foodTruckId, menuId, request);
    }

    @Transactional
    public void updateMenu(Long ownerId, Long foodTruckId, Long menuId, UpdateMenuRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 수정
        myFoodTruckMenuService.updateMenu(ownerId, foodTruckId, menuId, request);
    }

    @Transactional
    public void deleteMenu(Long ownerId, Long foodTruckId, Long menuId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 삭제
        myFoodTruckMenuService.deleteMenu(ownerId, foodTruckId, menuId);
    }

    @Transactional
    public void createNewFoodTruck(Long ownerId, FoodTruckCreateRequest request) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        // 푸드트럭 최초 등록
        myFoodTruckService.createNewFoodTruck(owner, request);
    }

    public ImageResponse createFoodTruckDocumentPresignedUrls(ImageRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        return presignedUrlService.generatePresignedUrls(
                request,
                owner.getUserId(),
                FOOD_TRUCK_DOCUMENT_IMAGE_MAX_COUNT,
                KeyUtils::buildFoodTruckDocumentImageKey
        );
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

어라 왜 CI가 실패하나 봤더니 이것 때문이었군요. 수정하겠습니다~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@PostMapping("/me/food-truck-documents/images")
public BaseResponse<ImageResponse> createFoodTruckDocumentPresignedUrls(
@Valid @RequestBody final ImageRequest request,
@Parameter(hidden = true) @UserId final Long ownerId
) {
return BaseResponse.ok(ownerService.createFoodTruckDocumentPresignedUrls(request, ownerId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import konkuk.chacall.domain.user.domain.model.User;
import konkuk.chacall.global.common.exception.EntityNotFoundException;
import konkuk.chacall.global.common.exception.code.ErrorCode;
import konkuk.chacall.global.common.storage.PdfService;
import konkuk.chacall.global.common.storage.pdf.PdfService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package konkuk.chacall.domain.user.application;

import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck;
import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository;
import konkuk.chacall.domain.user.application.admin.AdminService;
import konkuk.chacall.domain.user.application.validator.AdminValidator;
import konkuk.chacall.domain.user.presentation.dto.request.ApproveFoodTruckStatusRequest;
Expand All @@ -11,6 +9,10 @@
import konkuk.chacall.domain.user.presentation.dto.response.UserResponse;
import konkuk.chacall.global.common.exception.EntityNotFoundException;
import konkuk.chacall.global.common.exception.code.ErrorCode;
import konkuk.chacall.global.common.storage.dto.ImageRequest;
import konkuk.chacall.global.common.storage.dto.ImageResponse;
import konkuk.chacall.global.common.storage.presign.PresignedUrlService;
import konkuk.chacall.global.common.storage.util.KeyUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -25,6 +27,9 @@ public class UserService {
private final AdminService adminService;
private final AdminValidator adminValidator;

private final PresignedUrlService presignedUrlService;
private static final int USER_PROFILE_IMAGE_MAX_COUNT = 1;

public UserResponse getUserInfo(Long userId) {
return userRepository.findById(userId)
.map(UserResponse::from)
Expand All @@ -45,4 +50,16 @@ public void approveFoodTruckStatus(Long userId, Long foodTruckId, ApproveFoodTru

adminService.approveFoodTruckStatus(foodTruckId, request);
}

public ImageResponse createUserImagePresignedUrl(Long userId, ImageRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND));

return presignedUrlService.generatePresignedUrls(
request,
user.getUserId(),
USER_PROFILE_IMAGE_MAX_COUNT,
KeyUtils::buildUserProfileImageKey
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import konkuk.chacall.global.common.annotation.ExceptionDescription;
import konkuk.chacall.global.common.annotation.UserId;
import konkuk.chacall.global.common.dto.BaseResponse;
import konkuk.chacall.global.common.storage.dto.ImageRequest;
import konkuk.chacall.global.common.storage.dto.ImageResponse;
import konkuk.chacall.global.common.swagger.SwaggerResponseDescription;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
Expand Down Expand Up @@ -64,4 +66,17 @@ public BaseResponse<Void> approveFoodTruckStatus (
userService.approveFoodTruckStatus(userId, foodTruckId, request);
return BaseResponse.ok(null);
}

@Operation(
summary = "프로필 변경을 위한 presigned URL 발급",
description = "프로필 변경을 위한 presigned URL을 발급합니다."
)
@ExceptionDescription(SwaggerResponseDescription.GET_USER_PROFILE_PRESIGEND_URL)
@PostMapping("/me/images")
public BaseResponse<ImageResponse> getProfilePresignedUrl(
@Valid @RequestBody final ImageRequest request,
@Parameter(hidden = true) @UserId final Long userId
) {
return BaseResponse.ok(userService.createUserImagePresignedUrl(userId, request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public enum ErrorCode implements ResponseCode {
NEED_ELECTRICITY_MISMATCH(HttpStatus.BAD_REQUEST, 110008, "전기 사용 여부 값이 올바르지 않습니다."),
PAYMENT_METHOD_MISMATCH(HttpStatus.BAD_REQUEST, 110009, "결제 수단 값이 올바르지 않습니다."),

/**
* Image
*/
INVALID_IMAGE_COUNT(HttpStatus.BAD_REQUEST, 115001, "이미지 개수가 유효하지 않습니다."),
INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, 115002, "허용되지 않는 파일 확장자입니다."),

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package konkuk.chacall.domain.foodtruck.presentation.dto.request;
package konkuk.chacall.global.common.storage.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package konkuk.chacall.domain.foodtruck.presentation.dto.response;
package konkuk.chacall.global.common.storage.dto;

import io.swagger.v3.oas.annotations.media.Schema;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package konkuk.chacall.global.common.storage;
package konkuk.chacall.global.common.storage.pdf;

import konkuk.chacall.domain.reservation.domain.model.Reservation;
import konkuk.chacall.global.common.annotation.HelperService;
import konkuk.chacall.global.common.storage.S3Service;
import konkuk.chacall.global.common.storage.util.CdnUrlResolver;
import konkuk.chacall.global.common.storage.util.PdfGenerator;
import konkuk.chacall.global.common.storage.util.PdfTemplateRenderer;
Expand Down
Loading