CustomerElasticsearchController.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.features.auth.command.application.model.CustomUser;
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.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.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.PostMapping;
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/elasticsearch")
@RequiredArgsConstructor
@Validated
@Slf4j
public class CustomerElasticsearchController {

  private final CustomerQueryService customerQueryService;

  @Operation(summary = "개별 고객 엘라스틱서치 동기화", description = "특정 고객의 데이터를 엘라스틱서치에 동기화합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "동기화 성공"),
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "404",
        description = "고객을 찾을 수 없음")
  })
  @PostMapping("/sync/{customerId}")
  public ResponseEntity<ApiResponse<String>> syncCustomer(
      @Parameter(description = "고객 ID", required = true, example = "1") @PathVariable
          Long customerId) {
    log.info("고객 엘라스틱서치 동기화 요청 - 고객ID: {}", customerId);

    try {
      customerQueryService.syncCustomerToElasticsearch(customerId);
      return ResponseEntity.ok(ApiResponse.success("고객 데이터가 성공적으로 동기화되었습니다."));
    } catch (Exception e) {
      log.error("고객 동기화 실패 - 고객ID: {}, 오류: {}", customerId, e.getMessage());
      return ResponseEntity.ok(ApiResponse.success("고객 동기화 중 오류가 발생했습니다: " + e.getMessage()));
    }
  }

  @Operation(summary = "매장별 전체 고객 재인덱싱", description = "특정 매장의 모든 고객 데이터를 엘라스틱서치에 재인덱싱합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "재인덱싱 성공"),
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "400",
        description = "잘못된 매장 ID")
  })
  @PostMapping("/reindex")
  public ResponseEntity<ApiResponse<String>> reindexCustomers(
      @AuthenticationPrincipal CustomUser user) {
    log.info("매장별 고객 재인덱싱 요청 - 매장ID: {}", user.getShopId());

    try {
      customerQueryService.reindexAllCustomers(user.getShopId());
      return ResponseEntity.ok(ApiResponse.success("매장의 모든 고객 데이터가 성공적으로 재인덱싱되었습니다."));
    } catch (Exception e) {
      log.error("매장 재인덱싱 실패 - 매장ID: {}, 오류: {}", user.getShopId(), e.getMessage());
      return ResponseEntity.ok(ApiResponse.success("재인덱싱 중 오류가 발생했습니다: " + e.getMessage()));
    }
  }

  @Operation(summary = "자동완성 검색", description = "고객명/전화번호 자동완성을 위한 검색 결과를 반환합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "자동완성 검색 성공")
  })
  @GetMapping("/autocomplete")
  public ResponseEntity<ApiResponse<List<String>>> autocomplete(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "검색 키워드", required = true, example = "홍") @RequestParam
          String prefix) {
    log.info("자동완성 검색 요청 - 키워드: {}, 매장ID: {}", prefix, user.getShopId());

    try {
      List<String> suggestions = customerQueryService.autocomplete(prefix, user.getShopId());
      return ResponseEntity.ok(ApiResponse.success(suggestions));
    } catch (Exception e) {
      log.error("자동완성 검색 실패 - 키워드: {}, 매장ID: {}, 오류: {}", prefix, user.getShopId(), e.getMessage());
      return ResponseEntity.ok(ApiResponse.success(List.of()));
    }
  }

  @Operation(summary = "검색 결과 개수", description = "키워드로 검색되는 고객 수를 반환합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "검색 개수 조회 성공")
  })
  @GetMapping("/count")
  public ResponseEntity<ApiResponse<Long>> countByKeyword(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "검색 키워드", required = true, example = "홍길동") @RequestParam
          String keyword) {
    log.info("키워드별 고객 수 조회 요청 - 키워드: {}, 매장ID: {}", keyword, user.getShopId());

    try {
      long count = customerQueryService.countByKeyword(keyword, user.getShopId());
      return ResponseEntity.ok(ApiResponse.success(count));
    } catch (Exception e) {
      log.error(
          "키워드별 고객 수 조회 실패 - 키워드: {}, 매장ID: {}, 오류: {}", keyword, user.getShopId(), e.getMessage());
      return ResponseEntity.ok(ApiResponse.success(0L));
    }
  }

  @Operation(summary = "키워드 검색", description = "간단한 키워드로 고객을 검색합니다 (페이징 없음).")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "키워드 검색 성공")
  })
  @GetMapping("/search")
  public ResponseEntity<ApiResponse<List<CustomerSearchResult>>> searchByKeyword(
      @AuthenticationPrincipal CustomUser user,
      @Parameter(description = "검색 키워드", required = true, example = "홍길동") @RequestParam
          String keyword) {
    log.info("키워드 검색 요청 - 키워드: {}, 매장ID: {}", keyword, user.getShopId());

    try {
      List<CustomerSearchResult> results =
          customerQueryService.searchByKeyword(keyword, user.getShopId());
      return ResponseEntity.ok(ApiResponse.success(results));
    } catch (Exception e) {
      log.error("키워드 검색 실패 - 키워드: {}, 매장ID: {}, 오류: {}", keyword, user.getShopId(), e.getMessage());
      return ResponseEntity.ok(ApiResponse.success(List.of()));
    }
  }

  @Operation(summary = "엘라스틱서치 헬스체크", description = "엘라스틱서치 연결 상태를 확인합니다.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "헬스체크 완료")
  })
  @GetMapping("/health")
  public ResponseEntity<ApiResponse<String>> healthCheck() {
    log.info("엘라스틱서치 헬스체크 요청");

    try {
      customerQueryService.autocomplete("test", 1L);
      return ResponseEntity.ok(ApiResponse.success("엘라스틱서치가 정상적으로 작동 중입니다."));
    } catch (Exception e) {
      log.error("엘라스틱서치 헬스체크 실패: {}", e.getMessage());
      return ResponseEntity.ok(ApiResponse.success("엘라스틱서치 연결에 문제가 있습니다: " + e.getMessage()));
    }
  }

  @Operation(
      summary = "매장별 안전한 재인덱싱 (리셋)",
      description = "매장의 기존 인덱스를 삭제하고 새로 생성합니다. DB 데이터를 전부 갈은 경우 사용.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "안전한 재인덱싱 성공"),
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "400",
        description = "잘못된 매장 ID")
  })
  @PostMapping("/reindex/reset")
  public ResponseEntity<ApiResponse<String>> reindexCustomersWithReset(
      @AuthenticationPrincipal CustomUser user) {
    log.info("매장별 안전한 고객 재인덱싱 요청 - 매장ID: {}", user.getShopId());

    try {
      customerQueryService.reindexAllCustomersWithReset(user.getShopId());
      return ResponseEntity.ok(ApiResponse.success("매장의 고객 데이터가 안전하게 재인덱싱되었습니다."));
    } catch (Exception e) {
      log.error("매장 안전한 재인덱싱 실패 - 매장ID: {}, 오류: {}", user.getShopId(), e.getMessage());
      return ResponseEntity.ok(ApiResponse.success("안전한 재인덱싱 중 오류가 발생했습니다: " + e.getMessage()));
    }
  }

  @Operation(summary = "전체 매장 재인덱싱", description = "모든 매장의 고객 데이터를 재인덱싱합니다. 시스템 관리자만 사용.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "전체 재인덱싱 성공")
  })
  @PostMapping("/reindex/all")
  public ResponseEntity<ApiResponse<String>> reindexAllShopsCustomers() {
    log.info("전체 매장 고객 재인덱싱 요청");

    try {
      customerQueryService.reindexAllShopsCustomers();
      return ResponseEntity.ok(ApiResponse.success("모든 매장의 고객 데이터가 성공적으로 재인덱싱되었습니다."));
    } catch (Exception e) {
      log.error("전체 매장 재인덱싱 실패 - 오류: {}", e.getMessage());
      return ResponseEntity.ok(ApiResponse.success("전체 재인덱싱 중 오류가 발생했습니다: " + e.getMessage()));
    }
  }

  @Operation(
      summary = "전체 매장 안전한 재인덱싱 (리셋)",
      description = "모든 인덱스를 삭제하고 전체 재생성합니다. DB 전체 데이터를 갈은 경우 사용.")
  @ApiResponses({
    @io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "전체 안전한 재인덱싱 성공")
  })
  @PostMapping("/reindex/all/reset")
  public ResponseEntity<ApiResponse<String>> reindexAllShopsCustomersWithReset() {
    log.info("전체 매장 안전한 고객 재인덱싱 요청");

    try {
      customerQueryService.reindexAllShopsCustomersWithReset();
      return ResponseEntity.ok(ApiResponse.success("모든 매장의 고객 데이터가 안전하게 재인덱싱되었습니다."));
    } catch (Exception e) {
      log.error("전체 매장 안전한 재인덱싱 실패 - 오류: {}", e.getMessage());
      return ResponseEntity.ok(ApiResponse.success("전체 안전한 재인덱싱 중 오류가 발생했습니다: " + e.getMessage()));
    }
  }
}