CustomerQueryController.java

package com.deveagles.be15_deveagles_be.features.customers.query.controller;

import com.deveagles.be15_deveagles_be.common.dto.ApiResponse;
import com.deveagles.be15_deveagles_be.common.dto.PagedResponse;
import com.deveagles.be15_deveagles_be.common.dto.PagedResult;
import com.deveagles.be15_deveagles_be.common.exception.BusinessException;
import com.deveagles.be15_deveagles_be.common.exception.ErrorCode;
import com.deveagles.be15_deveagles_be.features.auth.command.application.model.CustomUser;
import com.deveagles.be15_deveagles_be.features.customers.query.dto.request.CustomerSearchQuery;
import com.deveagles.be15_deveagles_be.features.customers.query.dto.response.CustomerDetailResponse;
import com.deveagles.be15_deveagles_be.features.customers.query.dto.response.CustomerListResponse;
import com.deveagles.be15_deveagles_be.features.customers.query.dto.response.CustomerResponse;
import com.deveagles.be15_deveagles_be.features.customers.query.dto.response.CustomerSearchResult;
import com.deveagles.be15_deveagles_be.features.customers.query.service.CustomerQueryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "고객 조회", description = "고객 조회 및 검색 API")
@RestController
@RequestMapping("/customers")
@RequiredArgsConstructor
@Validated
@Slf4j
public class CustomerQueryController {

  private final CustomerQueryService customerQueryService;

  @Operation(
      summary = "고객 상세 조회",
      description = "고객 ID로 고객의 상세 정보를 조회합니다. 등급명, 유입경로명, 태그 정보 등을 포함합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "고객 조회 성공",
        content = @Content(schema = @Schema(implementation = CustomerDetailResponse.class))),
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "404",
        description = "고객을 찾을 수 없음")
  })
  @GetMapping("/{customerId}")
  public ResponseEntity<ApiResponse<CustomerDetailResponse>> getCustomerDetail(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "고객 ID", required = true, example = "1") @PathVariable
          Long customerId) {
    log.info("고객 상세 조회 요청 - 고객ID: {}, 매장ID: {}", customerId, user.getShopId());

    CustomerDetailResponse response =
        customerQueryService
            .getCustomerDetail(customerId, user.getShopId())
            .orElseThrow(
                () -> new BusinessException(ErrorCode.CUSTOMER_NOT_FOUND, "고객을 찾을 수 없습니다."));
    return ResponseEntity.ok(ApiResponse.success(response));
  }

  @Operation(summary = "전화번호로 고객 조회", description = "전화번호로 고객을 조회합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "고객 조회 성공",
        content = @Content(schema = @Schema(implementation = CustomerResponse.class))),
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "404",
        description = "고객을 찾을 수 없음")
  })
  @GetMapping("/phone/{phoneNumber}")
  public ResponseEntity<ApiResponse<CustomerResponse>> getCustomerByPhoneNumber(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "전화번호", required = true, example = "01012345678") @PathVariable
          String phoneNumber) {
    log.info("전화번호로 고객 조회 요청 - 전화번호: {}, 매장ID: {}", phoneNumber, user.getShopId());

    CustomerResponse customer =
        customerQueryService
            .getCustomerByPhoneNumber(phoneNumber, user.getShopId())
            .orElseThrow(() -> new BusinessException(ErrorCode.CUSTOMER_NOT_FOUND));

    return ResponseEntity.ok(ApiResponse.success(customer));
  }

  @Operation(
      summary = "매장별 고객 목록 조회",
      description =
          "매장 ID로 상세한 고객 목록을 조회합니다. 고객명, 연락처, 담당자, 메모, 방문횟수, 실매출액, 최근방문일, 태그, 등급, 생일 정보를 포함합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "고객 목록 조회 성공"),
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "400",
        description = "잘못된 요청 파라미터")
  })
  @GetMapping("/list")
  public ResponseEntity<ApiResponse<List<CustomerListResponse>>> getCustomersByShopId(
      @AuthenticationPrincipal CustomUser user) {
    log.info("매장별 고객 목록 조회 요청 - 매장ID: {}", user.getShopId());

    List<CustomerListResponse> customers = customerQueryService.getCustomerList(user.getShopId());
    return ResponseEntity.ok(ApiResponse.success(customers));
  }

  @Operation(summary = "매장별 고객 목록 조회 (페이징)", description = "매장 ID로 상세한 고객 목록을 페이징으로 조회합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "고객 목록 조회 성공")
  })
  @GetMapping("/list/paged")
  public ResponseEntity<ApiResponse<PagedResponse<CustomerListResponse>>> getCustomersByShopIdPaged(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
          int page,
      @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
          int size) {
    log.info("매장별 고객 목록 페이징 조회 요청 - 매장ID: {}, 페이지: {}, 크기: {}", user.getShopId(), page, size);

    Pageable pageable = Pageable.ofSize(size).withPage(page);
    Page<CustomerListResponse> customerPage =
        customerQueryService.getCustomerListPaged(user.getShopId(), pageable);
    PagedResponse<CustomerListResponse> response =
        PagedResponse.from(PagedResult.from(customerPage));
    return ResponseEntity.ok(ApiResponse.success(response));
  }

  @Operation(summary = "고객 통합 검색", description = "다양한 조건으로 고객을 검색하고 페이징 결과를 반환합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "고객 검색 성공",
        content = @Content(schema = @Schema(implementation = PagedResponse.class)))
  })
  @GetMapping("/search")
  public ResponseEntity<ApiResponse<PagedResponse<CustomerSearchResult>>> searchCustomers(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "검색 키워드 (이름 또는 전화번호)", example = "홍길동")
          @RequestParam(required = false)
          String keyword,
      @Parameter(description = "고객 등급 ID", example = "1") @RequestParam(required = false)
          Long customerGradeId,
      @Parameter(description = "성별 (M/F)", example = "M") @RequestParam(required = false)
          String gender,
      @Parameter(description = "마케팅 동의 여부", example = "true") @RequestParam(required = false)
          Boolean marketingConsent,
      @Parameter(description = "알림 동의 여부", example = "true") @RequestParam(required = false)
          Boolean notificationConsent,
      @Parameter(description = "삭제된 고객 포함 여부", example = "false")
          @RequestParam(required = false, defaultValue = "false")
          Boolean includeDeleted,
      @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
          int page,
      @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
          int size,
      @Parameter(description = "정렬 기준 필드", example = "createdAt")
          @RequestParam(defaultValue = "createdAt")
          String sortBy,
      @Parameter(description = "정렬 방향 (ASC/DESC)", example = "DESC")
          @RequestParam(defaultValue = "DESC")
          String sortDirection) {

    log.info(
        "고객 통합 검색 요청 - 매장ID: {}, 키워드: {}, 등급ID: {}, 성별: {}",
        user.getShopId(),
        keyword,
        customerGradeId,
        gender);

    CustomerSearchQuery query =
        new CustomerSearchQuery(
            user.getShopId(),
            keyword,
            customerGradeId != null ? List.of(customerGradeId) : null,
            null, // tagIds
            gender,
            marketingConsent,
            notificationConsent,
            false, // excludeDormant
            null, // dormantMonths
            false, // excludeRecentMessage
            null, // recentMessageDays
            includeDeleted,
            page,
            size,
            sortBy,
            sortDirection);

    PagedResult<CustomerSearchResult> pagedResult = customerQueryService.advancedSearch(query);
    PagedResponse<CustomerSearchResult> response = PagedResponse.from(pagedResult);

    return ResponseEntity.ok(ApiResponse.success(response));
  }

  @Operation(summary = "매장별 고객 수 조회", description = "매장의 전체 고객 수를 조회합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "고객 수 조회 성공")
  })
  @GetMapping("/count")
  public ResponseEntity<ApiResponse<Long>> getCustomerCountByShopId(
      @AuthenticationPrincipal CustomUser user) {
    log.info("매장별 고객 수 조회 요청 - 매장ID: {}", user.getShopId());

    long count = customerQueryService.getCustomerCountByShopId(user.getShopId());
    return ResponseEntity.ok(ApiResponse.success(count));
  }

  @Operation(summary = "전화번호 중복 확인", description = "전화번호의 중복 여부를 확인합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "중복 확인 성공")
  })
  @GetMapping("/exists")
  public ResponseEntity<ApiResponse<Boolean>> checkPhoneNumberExists(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "전화번호", required = true, example = "01012345678") @RequestParam
          String phoneNumber) {
    log.info("전화번호 중복 확인 요청 - 전화번호: {}, 매장ID: {}", phoneNumber, user.getShopId());

    boolean exists = customerQueryService.existsByPhoneNumber(phoneNumber, user.getShopId());
    return ResponseEntity.ok(ApiResponse.success(exists));
  }
}