diff --git a/apps/pre-processing-service/app/api/endpoints/product.py b/apps/pre-processing-service/app/api/endpoints/product.py index f5a91272..8a1d8feb 100644 --- a/apps/pre-processing-service/app/api/endpoints/product.py +++ b/apps/pre-processing-service/app/api/endpoints/product.py @@ -1,5 +1,4 @@ from fastapi import APIRouter, Request, HTTPException -from app.decorators.logging import log_api_call from ...errors.CustomException import ( InvalidItemDataException, ItemNotFoundException, diff --git a/apps/pre-processing-service/app/api/endpoints/sample.py b/apps/pre-processing-service/app/api/endpoints/sample.py deleted file mode 100644 index f6d586fb..00000000 --- a/apps/pre-processing-service/app/api/endpoints/sample.py +++ /dev/null @@ -1,45 +0,0 @@ -from fastapi import APIRouter -from ...model.schemas import * -from app.utils.response import Response - -router = APIRouter() - - -@router.get("/") -async def root(): - return {"message": "sample API"} - - -@router.post("/keywords/search", summary="네이버 키워드 검색") -async def search(request: RequestNaverSearch): - return Response.ok({"test": "hello world"}) - - -@router.post("/blogs/rag/create", summary="RAG 기반 블로그 콘텐츠 생성") -async def rag_create(request: RequestBlogCreate): - return Response.ok({"test": "hello world"}) - - -@router.post("/blogs/publish", summary="블로그 콘텐츠 배포") -async def publish(request: RequestBlogPublish): - return Response.ok({"test": "hello world"}) - - -@router.post("/products/search", summary="상품 검색") -async def product_search(request: RequestSadaguSearch): - return Response.ok({"test": "hello world"}) - - -@router.post("/products/match", summary="상품 매칭") -async def product_match(request: RequestSadaguMatch): - return Response.ok({"test": "hello world"}) - - -@router.post("/products/similarity", summary="상품 유사도 분석") -async def product_similarity(request: RequestSadaguSimilarity): - return Response.ok({"test": "hello world"}) - - -@router.post("/products/crawl", summary="상품 상세 정보 크롤링") -async def product_crawl(request: RequestSadaguCrawl): - return Response.ok({"test": "hello world"}) diff --git a/apps/pre-processing-service/app/api/endpoints/test.py b/apps/pre-processing-service/app/api/endpoints/test.py index ca1a43b5..9dfc5d40 100644 --- a/apps/pre-processing-service/app/api/endpoints/test.py +++ b/apps/pre-processing-service/app/api/endpoints/test.py @@ -5,14 +5,12 @@ from fastapi import APIRouter from sqlalchemy import text -from app.decorators.logging import log_api_call from ...errors.CustomException import * from fastapi import APIRouter from typing import Mapping, Any, Dict from ...model.schemas import * from ...service.blog.blog_create_service import BlogContentService from ...service.blog.naver_blog_post_service import NaverBlogPostService -from ...service.blog.tistory_blog_post_service import TistoryBlogPostService from ...service.crawl_service import CrawlService from ...service.keyword_service import keyword_search from ...service.match_service import MatchService @@ -27,7 +25,6 @@ @router.get("/hello/{name}", tags=["hello"]) -# @log_api_call async def say_hello(name: str): return {"message": f"Hello {name}"} @@ -122,8 +119,6 @@ async def processing_tester(): # tistory_service = TistoryBlogPostService() naverblogPostService = NaverBlogPostService() result = naverblogPostService.post_content( - # blog_id="wtecho331", - # blog_pw="wt505033@#", title=data.get("title"), content=data.get("content"), tags=data.get("tags"), diff --git a/apps/pre-processing-service/app/api/router.py b/apps/pre-processing-service/app/api/router.py index c1a2fcb4..d21eb2f3 100644 --- a/apps/pre-processing-service/app/api/router.py +++ b/apps/pre-processing-service/app/api/router.py @@ -1,6 +1,6 @@ # app/api/router.py from fastapi import APIRouter -from .endpoints import keywords, blog, product, test, sample +from .endpoints import keywords, blog, product, test from ..core.config import settings api_router = APIRouter() @@ -15,19 +15,9 @@ api_router.include_router(product.router, prefix="/products", tags=["product"]) # 모듈 테스터를 위한 endpoint -> 추후 삭제 예정 -api_router.include_router(test.router, prefix="/tests", tags=["Test"]) - -api_router.include_router(sample.router, prefix="/v0", tags=["Sample"]) +# api_router.include_router(test.router, prefix="/tests", tags=["Test"]) @api_router.get("/ping") async def root(): return {"message": "서버 실행중입니다."} - - -@api_router.get("/db") -def get_settings(): - """ - 환경 변수가 올바르게 로드되었는지 확인하는 엔드포인트 - """ - return {"환경": settings.env_name, "데이터베이스 URL": settings.db_url} diff --git a/apps/pre-processing-service/app/decorators/__init__.py b/apps/pre-processing-service/app/decorators/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/pre-processing-service/app/decorators/logging.py b/apps/pre-processing-service/app/decorators/logging.py deleted file mode 100644 index 23604a73..00000000 --- a/apps/pre-processing-service/app/decorators/logging.py +++ /dev/null @@ -1,85 +0,0 @@ -# app/decorators/logging.py - -from fastapi import Request -from loguru import logger -import functools -import time - - -def log_api_call(func): - """ - FastAPI API 호출에 대한 상세 정보를 로깅하는 데코레이터입니다. - IP 주소, User-Agent, URL, 메서드, 실행 시간 등을 기록합니다. - """ - - @functools.wraps(func) - async def wrapper(*args, **kwargs): - # 1. request 객체를 안전하게 가져옵니다. - # kwargs에서 'request'를 찾고, 없으면 args가 비어있지 않은 경우에만 args[0]을 시도합니다. - request: Request | None = kwargs.get("request") - if request is None and args and isinstance(args[0], Request): - request = args[0] - - # 2. 로깅에 사용할 추가 정보를 추출합니다. - client_ip: str | None = None - user_agent: str | None = None - if request: - client_ip = request.client.host - user_agent = request.headers.get("user-agent", "N/A") - - # 3. 요청 정보를 로그로 기록합니다. - log_context = {"func": func.__name__, "ip": client_ip, "user_agent": user_agent} - if request: - log_context.update( - { - "url": str(request.url), - "method": request.method, - } - ) - logger.info( - "API 호출 시작: URL='{url}' 메서드='{method}' 함수='{func}' IP='{ip}' User-Agent='{user_agent}'", - **log_context, - ) - else: - logger.info("API 호출 시작: 함수='{func}'", **log_context) - - start_time = time.time() - result = None - - try: - # 4. 원본 함수를 실행합니다. - result = await func(*args, **kwargs) - return result - except Exception as e: - # 5. 예외 발생 시 에러 로그를 기록합니다. - elapsed_time = time.time() - start_time - log_context["exception"] = e - log_context["elapsed"] = f"{elapsed_time:.4f}s" - - if request: - logger.error( - "API 호출 실패: URL='{url}' 메서드='{method}' IP='{ip}' 예외='{exception}' ({elapsed})", - **log_context, - ) - else: - logger.error( - "API 호출 실패: 함수='{func}' 예외='{exception}' ({elapsed})", - **log_context, - ) - raise # 예외를 다시 발생시켜 FastAPI가 처리하도록 합니다. - finally: - # 6. 성공적으로 완료되면 성공 로그를 기록합니다. - if result is not None: - elapsed_time = time.time() - start_time - log_context["elapsed"] = f"{elapsed_time:.4f}s" - if request: - logger.success( - "API 호출 성공: URL='{url}' 메서드='{method}' IP='{ip}' ({elapsed})", - **log_context, - ) - else: - logger.success( - "API 호출 성공: 함수='{func}' ({elapsed})", **log_context - ) - - return wrapper diff --git a/apps/pre-processing-service/app/errors/handlers.py b/apps/pre-processing-service/app/errors/handlers.py index 882a6078..c7b7339f 100644 --- a/apps/pre-processing-service/app/errors/handlers.py +++ b/apps/pre-processing-service/app/errors/handlers.py @@ -57,8 +57,8 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE """ # 변경점: ErrorBaseModel을 기본 구조로 사용하고, 추가 정보를 더함 base_error = ErrorBaseModel( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=ERROR_MESSAGES[status.HTTP_422_UNPROCESSABLE_ENTITY], + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, + detail=ERROR_MESSAGES[status.HTTP_422_UNPROCESSABLE_CONTENT], code="VALIDATION_ERROR", ) @@ -67,7 +67,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE response_content["details"] = exc.errors() return JSONResponse( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, content=response_content, ) diff --git a/apps/pre-processing-service/app/errors/messages.py b/apps/pre-processing-service/app/errors/messages.py index 80139492..ea82ec11 100644 --- a/apps/pre-processing-service/app/errors/messages.py +++ b/apps/pre-processing-service/app/errors/messages.py @@ -6,7 +6,7 @@ status.HTTP_401_UNAUTHORIZED: "인증이 필요합니다.", status.HTTP_403_FORBIDDEN: "접근 권한이 없습니다.", status.HTTP_404_NOT_FOUND: "요청하신 리소스를 찾을 수 없습니다.", - status.HTTP_422_UNPROCESSABLE_ENTITY: "입력 데이터가 유효하지 않습니다.", + status.HTTP_422_UNPROCESSABLE_CONTENT: "입력 데이터가 유효하지 않습니다.", status.HTTP_500_INTERNAL_SERVER_ERROR: "서버 내부 오류가 발생했습니다.", } diff --git a/apps/pre-processing-service/app/middleware/BackServiceLoggerDependency.py b/apps/pre-processing-service/app/middleware/BackServiceLoggerDependency.py deleted file mode 100644 index d18630f6..00000000 --- a/apps/pre-processing-service/app/middleware/BackServiceLoggerDependency.py +++ /dev/null @@ -1,124 +0,0 @@ -# import time -# from typing import Dict, Any, List, Optional -# from fastapi import Request -# from loguru import logger -# from contextvars import ContextVar -# -# trace_id_context: ContextVar[str] = ContextVar('trace_id', default="NO_TRACE_ID") -# -# -# class ServiceLoggingDependency: -# """ -# 서비스 로깅을 위한 의존성 클래스 -# :param service_type: 서비스 유형 (예: "CHUNKING", "PARSING", "EMBEDDING") -# :param track_params: 추적할 매개변수 이름 목록 -# :param response_trackers: 응답에서 추적할 필드 이름 목록 (딕셔너리) -# """ -# -# def __init__(self, service_type: str, -# track_params: List[str] = None, -# response_trackers: List[str] = None): -# self.service_type = service_type -# self.track_params = track_params or [] -# self.response_trackers = response_trackers or [] -# -# async def __call__(self, request: Request): -# """ -# 의존성 주입 시 호출되는 메서드 -# :param request: FastAPI Request 객체 -# :return: 서비스 유형과 추출된 매개변수 딕셔너리 -# """ -# trace_id = trace_id_context.get("NO_TRACE_ID") -# start_time = time.time() -# -# # 파라미터 추출 -# params = await self._extract_params(request) -# param_str = "" -# if params: -# param_strs = [f"{k}={v}" for k, v in params.items()] -# param_str = " " + " ".join(param_strs) -# -# logger.info(f"[{self.service_type}_START] trace_id={trace_id}{param_str}") -# -# # 응답 시 사용할 정보를 request.state에 저장 -# request.state.service_type = self.service_type -# request.state.start_time = start_time -# request.state.param_str = param_str -# request.state.response_trackers = self.response_trackers -# -# return {"service_type": self.service_type, "params": params} -# -# async def _extract_params(self, request: Request) -> Dict[str, Any]: -# """ -# 요청에서 추적 파라미터 추출 -# :param request: FastAPI Request 객체 -# :return: 추출된 매개변수 딕셔너리 -# """ -# params = {} -# -# try: -# # Query Parameters 추출 -# for key, value in request.query_params.items(): -# if key in self.track_params: -# params[key] = value -# -# # JSON Body 추출 -# try: -# json_body = await request.json() -# if json_body: -# for key, value in json_body.items(): -# if key in self.track_params: -# if isinstance(value, str) and len(value) > 50: -# params[f"{key}_length"] = len(value) -# elif isinstance(value, list): -# params[f"{key}_count"] = len(value) -# else: -# params[key] = value -# except: -# pass -# except: -# pass -# -# return params -# -# # 서비스 응답 시 성공 로그 함수 -# async def log_service_response_with_data(request: Request, response_data: Optional[Dict] = None): -# """ -# 서비스 응답 시 성공 로그 기록 -# :param request: FastAPI Request 객체 -# :param response_data: 응답 데이터 -# """ -# if hasattr(request.state, 'service_type'): -# trace_id = trace_id_context.get("NO_TRACE_ID") -# duration = time.time() - request.state.start_time -# -# # 기본 로그 문자열 -# log_parts = [f"[{request.state.service_type}_SUCCESS]", -# f"trace_id={trace_id}", -# f"execution_time={duration:.4f}s{request.state.param_str}"] -# -# # 응답 데이터에서 추적할 필드 추출 -# if response_data and hasattr(request.state, 'response_trackers'): -# response_params = [] -# for tracker in request.state.response_trackers: -# if tracker in response_data: -# value = response_data[tracker] -# if isinstance(value, dict): -# response_params.append(f"{tracker}_keys={list(value.keys())}") -# response_params.append(f"{tracker}_count={len(value)}") -# elif isinstance(value, list): -# response_params.append(f"{tracker}_count={len(value)}") -# else: -# response_params.append(f"{tracker}={value}") -# -# if response_params: -# log_parts.append(" ".join(response_params)) -# -# logger.info(" ".join(log_parts)) -# return None -# -# naver_search_dependency = ServiceLoggingDependency( -# "NAVER_CRAWLING", -# track_params=["job_id", "schedule_id", "tag", "category", "startDate", "endDate"], -# response_trackers=["keyword", "total_keyword"] -# ) diff --git a/apps/pre-processing-service/app/middleware/ServiceLoggerMiddleware.py b/apps/pre-processing-service/app/middleware/ServiceLoggerMiddleware.py index 30d3475b..e4e23185 100644 --- a/apps/pre-processing-service/app/middleware/ServiceLoggerMiddleware.py +++ b/apps/pre-processing-service/app/middleware/ServiceLoggerMiddleware.py @@ -54,10 +54,11 @@ def __init__( def _default_mappings(self) -> Dict[str, Dict]: """기본 서비스 매핑 설정""" + service_type = "TASK" return { # 네이버 키워드 검색 "/keywords/search": { - "service_type": "NAVER_CRAWLING", + "service_type": service_type, "track_params": [ "tag", "keyword", @@ -71,7 +72,7 @@ def _default_mappings(self) -> Dict[str, Dict]: }, # 블로그 RAG 콘텐츠 생성 "/blogs/rag/create": { - "service_type": "BLOG_RAG_CREATE", + "service_type": service_type, "track_params": [ "keyword", "product_info", @@ -91,7 +92,7 @@ def _default_mappings(self) -> Dict[str, Dict]: }, # 블로그 배포 "/blogs/publish": { - "service_type": "BLOG_PUBLISH", + "service_type": service_type, "track_params": [ "tag", "blog_id", @@ -115,7 +116,7 @@ def _default_mappings(self) -> Dict[str, Dict]: }, # 상품 검색 "/products/search": { - "service_type": "PRODUCT_SEARCH", + "service_type": service_type, "track_params": [ "keyword", "job_id", @@ -131,7 +132,7 @@ def _default_mappings(self) -> Dict[str, Dict]: }, # 상품 매칭 "/products/match": { - "service_type": "PRODUCT_MATCH", + "service_type": service_type, "track_params": [ "keyword", "search_results", @@ -148,7 +149,7 @@ def _default_mappings(self) -> Dict[str, Dict]: }, # 상품 유사도 분석 "/products/similarity": { - "service_type": "PRODUCT_SIMILARITY", + "service_type": service_type, "track_params": [ "keyword", "matched_products", @@ -167,7 +168,7 @@ def _default_mappings(self) -> Dict[str, Dict]: }, # 상품 크롤링 "/products/crawl": { - "service_type": "PRODUCT_CRAWL", + "service_type": service_type, "track_params": [ "tag", "product_url", @@ -184,6 +185,18 @@ def _default_mappings(self) -> Dict[str, Dict]: "status", ], }, + # 상품 이미지 번역 + "blogs/ocr/extract": { + "service_type": service_type, + "track_params": [], + "response_trackers": [], + }, + # 상품 이미지 S3업로드 + "products/s3-upload": { + "service_type": service_type, + "track_params": [], + "response_trackers": [], + }, } async def dispatch(self, request: Request, call_next): diff --git a/apps/pre-processing-service/app/test/test_sadagu_crawl.py b/apps/pre-processing-service/app/test/test_sadagu_crawl.py index 72e4f0df..cd879a7d 100644 --- a/apps/pre-processing-service/app/test/test_sadagu_crawl.py +++ b/apps/pre-processing-service/app/test/test_sadagu_crawl.py @@ -7,10 +7,9 @@ def test_crawl_success(): body = { - "tag": "detail", - "product_url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=886788894790", - "use_selenium": False, - "include_images": False, + "product_urls": [ + "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=886788894790", + ], } response = client.post("/products/crawl", json=body) @@ -20,8 +19,7 @@ def test_crawl_success(): data = response.json() assert data["success"] == True assert data["status"] == "OK" - assert data["data"]["product_url"] == body["product_url"] - assert "product_detail" in data["data"] + assert isinstance(data["data"]["crawled_products"], list) # def test_crawl_invalid_url(): diff --git a/apps/pre-processing-service/app/test/test_similarity_service.py b/apps/pre-processing-service/app/test/test_similarity_service.py index 6efbcdc1..250fd55b 100644 --- a/apps/pre-processing-service/app/test/test_similarity_service.py +++ b/apps/pre-processing-service/app/test/test_similarity_service.py @@ -9,124 +9,132 @@ def test_similarity_with_matched_products(): """매칭된 상품들 중에서 유사도 분석""" matched_products = [ { - "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=123", - "title": "925 실버 반지 여성용", - "match_info": { - "match_type": "exact", - "match_score": 1.0, - "match_reason": "완전 매칭", - }, + "product_id": 201, + "title": "15인치 노트북 백팩", + "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=22334455", }, { - "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=456", - "title": "반지 세트 커플링", - "match_info": { - "match_type": "morphological", - "match_score": 0.8, - "match_reason": "형태소 매칭", - }, + "product_id": 202, + "title": "노트북 파우치 13인치", + "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=66778899", }, - ] - - body = { - "keyword": "반지", - "matched_products": matched_products, - } - - response = client.post("/products/similarity", json=body) - print(f"Similarity Response: {response.json()}") - - assert response.status_code == 200 - data = response.json() - assert data["success"] == True - assert data["status"] == "OK" - assert data["data"]["keyword"] == body["keyword"] - - if data["data"]["selected_product"]: - assert "similarity_info" in data["data"]["selected_product"] - assert "similarity_score" in data["data"]["selected_product"]["similarity_info"] - assert data["data"]["reason"] is not None - - -def test_similarity_fallback_to_search_results(): - """매칭 실패시 전체 검색 결과에서 유사도 분석""" - search_results = [ { - "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=123", - "title": "실버 링 악세서리", + "product_id": 101, + "title": "Magsafe 자기 휴대폰 케이스 아이폰15", + "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=886788894790", }, { - "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=456", - "title": "골드 반지 여성", + "product_id": 102, + "title": "휴대 전화 보호 케이스 갤럭시 S24", + "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=1234567890", }, ] body = { "keyword": "반지", - "matched_products": [], # 매칭된 상품 없음 - "search_results": search_results, # 폴백용 - } - - response = client.post("/products/similarity", json=body) - print(f"Fallback Response: {response.json()}") - - assert response.status_code == 200 - data = response.json() - assert data["success"] == True - assert data["status"] == "OK" - - # 폴백 모드에서는 임계값을 통과한 경우에만 상품이 선택됨 - if data["data"]["selected_product"]: - assert "similarity_info" in data["data"]["selected_product"] - assert ( - data["data"]["selected_product"]["similarity_info"]["analysis_mode"] - == "fallback_similarity_only" - ) - - -def test_similarity_single_candidate(): - """후보가 1개만 있는 경우""" - single_product = [ - { - "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=123", - "title": "925 실버 반지 여성용", - "match_info": {"match_type": "exact", "match_score": 1.0}, - } - ] - - body = { - "keyword": "반지", - "matched_products": single_product, + "matched_products": matched_products, } response = client.post("/products/similarity", json=body) - print(f"Single candidate response: {response.json()}") + print(f"Similarity Response: {response.json()}") assert response.status_code == 200 data = response.json() assert data["success"] == True assert data["status"] == "OK" - assert data["data"]["selected_product"] is not None - assert ( - data["data"]["selected_product"]["similarity_info"]["analysis_type"] - == "single_candidate" - ) - - -def test_similarity_no_candidates(): - """후보가 없는 경우""" - body = { - "keyword": "반지", - "matched_products": [], - "search_results": [], - } + assert data["data"]["keyword"] == body["keyword"] + products = data["data"]["top_products"] + if products: + for product in products: + assert "product_id" in product + assert "title" in product + assert "url" in product + assert "rank" in product + assert "similarity_score" in product["similarity_info"] + # assert "analysis_type" in product["similarity_info"] + # assert "analysis_mode" in product["similarity_info"] + assert data["data"]["reason"] is not None - response = client.post("/products/similarity", json=body) - print(f"No candidates response: {response.json()}") - assert response.status_code == 200 - data = response.json() - assert data["success"] == True - assert data["status"] == "OK" - assert data["data"]["selected_product"] is None - assert "검색 결과가 모두 없음" in data["data"]["reason"] +# def test_similarity_fallback_to_search_results(): +# """매칭 실패시 전체 검색 결과에서 유사도 분석""" +# search_results = [ +# { +# "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=123", +# "title": "실버 링 악세서리", +# }, +# { +# "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=456", +# "title": "골드 반지 여성", +# }, +# ] +# +# body = { +# "keyword": "반지", +# "matched_products": [], # 매칭된 상품 없음 +# "search_results": search_results, # 폴백용 +# } +# +# response = client.post("/products/similarity", json=body) +# print(f"Fallback Response: {response.json()}") +# +# assert response.status_code == 200 +# data = response.json() +# assert data["success"] == True +# assert data["status"] == "OK" +# +# # 폴백 모드에서는 임계값을 통과한 경우에만 상품이 선택됨 +# if data["data"]["top_products"]: +# assert "similarity_info" in data["data"]["top_products"] +# assert ( +# data["data"]["top_products"]["similarity_info"]["analysis_mode"] +# == "fallback_similarity_only" +# ) +# +# +# def test_similarity_single_candidate(): +# """후보가 1개만 있는 경우""" +# single_product = [ +# { +# "url": "https://ssadagu.kr/shop/view.php?platform=1688&num_iid=123", +# "title": "925 실버 반지 여성용", +# "match_info": {"match_type": "exact", "match_score": 1.0}, +# } +# ] +# +# body = { +# "keyword": "반지", +# "matched_products": single_product, +# } +# +# response = client.post("/products/similarity", json=body) +# print(f"Single candidate response: {response.json()}") +# +# assert response.status_code == 200 +# data = response.json() +# assert data["success"] == True +# assert data["status"] == "OK" +# assert data["data"]["top_products"] is not None +# assert ( +# data["data"]["top_products"]["similarity_info"]["analysis_type"] +# == "single_candidate" +# ) +# +# +# def test_similarity_no_candidates(): +# """후보가 없는 경우""" +# body = { +# "keyword": "반지", +# "matched_products": [], +# "search_results": [], +# } +# +# response = client.post("/products/similarity", json=body) +# print(f"No candidates response: {response.json()}") +# +# assert response.status_code == 200 +# data = response.json() +# assert data["success"] == True +# assert data["status"] == "OK" +# assert data["data"]["top_products"] is None +# assert "검색 결과가 모두 없음" in data["data"]["reason"]