모델 API 설계 - 100-hours-a-week/KTB4-1st-wiki GitHub Wiki
바꾸까 서비스의 AI 서버(FastAPI)가 제공하는 API 명세입니다.
에러 응답 공통 규칙: 400/422(사용자가 고칠 수 있는 오류)는 항상
message를 포함해 구체적으로 안내한다. 500(internal_error, 서버·모델 처리 실패)은 사용자가 직접 고칠 수 있는 게 아니므로message없이 코드만 반환하고, 프론트가 공통 안내 문구("일시적인 오류입니다. 다시 시도해주세요")를 보여준다.internal_error는 AI 모델 요청 한도 초과·응답 지연·응답 파싱 실패 등, 1회 재시도 후에도 실패한 모든 기술적 오류를 통칭한다.
사용하는 API의 엔드포인트와 기능을 정리한다.
| 기능 번호 | Method | Endpoint | 기능 |
|---|---|---|---|
| 기능1 | POST | /api/vision/analyze-item |
상품 이미지 1~3장으로 검증 + 제품 식별 + 등록용 제목/설명 초안 생성 |
| 기능2 | POST | /api/moderation/check-text |
최종 제목·내용 검증 |
| 기능3 | POST | /api/value/estimate-price |
시세 검색 및 가치 범위(기준가/하한/상한) 산정 |
| 기능4 | POST | /api/chatbot/ask |
정책 문서 검색(RAG) 기반 챗봇 답변 및 화면 이동 제안 |
| 기능5 | POST | /api/search/rank-candidates |
취향 신호 기반 후보 물건 순위 매김 |
| 기능6 | POST | /api/chat/refine-message |
채팅 메시지 톤 다듬기 |
각 API의 입력값과 출력값을 정리한다.
상품 이미지 1~3장을 받아 (1) 부적절 콘텐츠 검증, (2) 제품 식별, (3) 등록용 제목·상세설명 초안 작성을 한 번의 Claude API 호출로 처리한다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| images | file[] | O | 상품 이미지 배열 (jpg/jpeg/png/webp, 최대 5mb, 1~3장) |
요청 예시 (multipart/form-data — JSON 아님)
images: hetbahn_pack1.jpg, hetbahn_pack2.jpg
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| isAppropriate | boolean | O | 사진 검증 결과 |
| rejectionReason | string | - | 부적절한 경우 사유, 적절하면 null |
| title | string | O (confidence < 0.25이면 null) | 게시글 제목 |
| content | string | O (confidence < 0.25이면 null) | 게시글 상세내용 |
| confidence | float | O | 0.0~1.0 확신도 |
응답 예시
{
"isAppropriate": true,
"rejectionReason": null,
"title": "햇반 210g 새 제품 4개 교환해요",
"content": "햇반 210g 새 제품입니다. 24개입 세트 중 4개를 교환하려고요. 미개봉 상태입니다.",
"confidence": 0.97
}
claude-sonnet-5 (비전 지원). 사진 1~3장을 함께 보고 검증(금지 품목/상품과 무관한 사진/서로 다른 물건 혼합 여부, 단 세트상품·클로즈업샷은 정상 처리), 제품 식별(불확실하면 null, confidence 표시), 등록용 title(30자 이내)·content(2000자 이내, 가격 언급 금지) 작성을 지시하는 프롬프트 사용. Claude 응답의 isAppropriate/confidence 값에 따라 AI 서버가 200/422/500으로 매핑한다.
| 상태 코드 | 상황 | 응답 예시 |
|---|---|---|
| 400 | 이미지 파일이 없거나 지원하지 않는 형식 | {"error": "invalid_input", "message": "이미지 파일이 없거나 지원하지 않는 형식이에요. jpg/jpeg/png/webp 파일을 올려주세요."} |
| 413 | 이미지 파일 하나당 5mb 초과 | {"error": "file_too_large", "message": "이미지 파일 하나당 5MB를 초과했어요. 용량을 줄여 다시 업로드해주세요."} |
| 422 | 검증된 이미지 (금지 품목) | {"error": "prohibited_item", "message": "거래할 수 없는 품목이 포함된 사진이에요. 다른 사진으로 다시 올려주세요."} |
| 422 | 검증된 이미지 (상품과 무관한 사진 포함) | {"error": "unrelated_photo", "message": "상품과 무관한 사진이 섞여 있어요. 교환할 물건이 담긴 사진으로 다시 올려주세요."} |
| 422 | 검증된 이미지 (서로 다른 물건 여러 개 혼합) | {"error": "multiple_items_mixed", "message": "서로 다른 물건 사진이 섞여 있어요. 하나의 물건 사진만 올려주세요."} |
| 500 | AI 모델 처리 실패 (요청 한도 초과·응답 지연·응답 파싱 실패, 1회 재시도 후에도 실패) | {"error": "internal_error"} |
- 요청이 실패하면(500 등) 검열 결과를 알 수 없으므로 등록을 진행할 수 없다. 재업로드/재시도를 안내한다.
- confidence < 0.25면 title/content를 null로 두고 사용자가 직접 입력하게 한다. 등록 절차 자체는 막지 않는다.
최종 확정된 제목과 상세내용을 검증해 게시 가능 여부를 판단한다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| title | string | O | 최종 제목 |
| content | string | O | 최종 상세 내용 |
요청 예시
{
"title": "햇반 210g 작은공기",
"content": "24개입 세트로 구매했는데 다 못 먹을 것 같아서 4개만 교환하려고요. 미개봉 새 제품이고 유통기한은 2027년 6월까지예요."
}
성공(200) 시 응답 본문 없음. 상태 코드만으로 통과 여부를 판단하며, 422/500일 때만 본문의
message(422) 또는error(500)를 확인한다.
claude-haiku-4.5 (텍스트 전용, 기능1보다 가볍고 빠른 모델). 욕설/비속어/혐오 표현, 금지 품목 언급, 사기성 문구, 개인 연락처 노출, 가격 언급, 광고성 스팸, 서로 무관한 여러 물건 혼재 — 7가지 기준으로 검증하고 애매하면 차단하도록 지시하는 프롬프트 사용. isAppropriate 값에 따라 200/422로 매핑한다.
| 상태 코드 | 상황 | 응답 예시 |
|---|---|---|
| 400 | 제목이 없음 | {"error": "missing_title", "message": "제목을 입력해주세요."} |
| 400 | 내용이 없음 | {"error": "missing_content", "message": "상세 내용을 입력해주세요."} |
| 422 | 욕설·비속어·혐오 표현 | {"error": "offensive_language", "message": "부적절한 표현이 포함되어 있어요. 표현을 다듬어 다시 시도해주세요."} |
| 422 | 금지 품목 언급 | {"error": "prohibited_item", "message": "거래할 수 없는 품목이 언급되어 있어요. 내용을 확인해주세요."} |
| 422 | 사기성 문구 | {"error": "fraud_language", "message": "사기 의심 표현이 포함되어 있어요. 내용을 확인해주세요."} |
| 422 | 개인 연락처 노출 | {"error": "personal_contact", "message": "개인 연락처는 채팅으로만 주고받을 수 있어요. 게시글에서 연락처를 제거해주세요."} |
| 422 | 가격 언급 | {"error": "price_mention", "message": "게시글에는 가격을 직접 언급할 수 없어요. 가격 관련 내용은 제거해주세요."} |
| 422 | 광고성 스팸 | {"error": "spam_content", "message": "광고성 문구가 포함되어 있어요. 내용을 확인해주세요."} |
| 422 | 서로 다른 물건 여러 개가 하나의 게시물에 섞임 | {"error": "multiple_items_mixed", "message": "서로 다른 물건이 하나의 게시물에 섞여 있어요. 하나의 물건만 등록해주세요."} |
| 500 | AI 모델 처리 실패 (요청 한도 초과·응답 지연·응답 파싱 실패, 1회 재시도 후에도 실패) | {"error": "internal_error"} |
- 요청 실패 시(500 등) 등록을 진행할 수 없다. 재시도를 안내한다.
- 422는 명확한 거절이므로 message를 안내하고 텍스트 수정을 유도한다.
- 동기 처리 — 통과(200)해야만 다음 단계(가치 평가, 2.3)로 진행된다.
최종 제목·내용을 바탕으로 web_search로 시세를 조사하고, 사용자 선호도를 반영해 가치 범위를 산정한다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| title | string | O | 최종 제목 |
| content | string | O | 최종 상세 내용 |
| valueTolerance | float | O | "가치 유연성" 슬라이더 값 (0.0~1.0) |
| tradeSpeed | float | O | "판매 속도" 슬라이더 값 (0.0~1.0) |
요청 예시
{
"title": "햇반 210g 작은공기",
"content": "24개입 세트로 구매했는데 다 못 먹을 것 같아서 4개만 교환하려고요. 미개봉 새 제품이에요.",
"valueTolerance": 0.6,
"tradeSpeed": 0.4
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| referenceUnitPrice | int | O | 검색/추정 기반 기준가 |
| minUnitPrice | int | O | 사용자 선호도 반영한 하한 |
| maxUnitPrice | int | O | 기준가와 동일 (상한) |
| priceSource | string | O | "new" 또는 "estimated" |
| confidence | float | O (priceSource가 "new"이면 null) | estimated일 때만 의미 있음 |
응답 예시
{
"referenceUnitPrice": 1300,
"minUnitPrice": 1090,
"maxUnitPrice": 1300,
"priceSource": "new",
"confidence": null
}
claude-sonnet-5 (web_search 도구 활성화). 여러 쇼핑몰에서 가격을 검색해 개당 가격으로 통일, 검색 결과가 없으면 자체 추정값 + source: "estimated" 반환하도록 지시. Claude가 반환한 prices 배열은 이후 서버 코드(LLM 아님)가 직접 계산한다.
1. prices가 3개 이상이면 이상치 제거 (IQR 기준). 제거 결과 0개가 되면 원본 전체 사용.
2개 이하면 이상치 제거 생략.
2. 평균 → referenceUnitPrice (10원 단위 반올림)
3. discountRate = (valueTolerance × 0.7 + tradeSpeed × 0.3) × 0.3 (최대 할인폭 30%)
4. maxUnitPrice = referenceUnitPrice
minUnitPrice = referenceUnitPrice × (1 − discountRate), 10원 단위 반올림
| 상태 코드 | 상황 | 응답 예시 |
|---|---|---|
| 400 | 제목(title) 누락 | {"error": "missing_title", "message": "제목을 입력해주세요."} |
| 400 | 상세 설명(content) 누락 | {"error": "missing_content", "message": "상세 내용을 입력해주세요."} |
| 400 | 가치 허용 범위(valueTolerance) 누락 | {"error": "missing_value_tolerance", "message": "가치 유연성 값을 입력해주세요."} |
| 400 | 거래 속도 선호도(tradeSpeed) 누락 | {"error": "missing_trade_speed", "message": "판매 속도 선호도 값을 입력해주세요."} |
| 500 | AI 모델 처리 실패 (요청 한도 초과·응답 지연·응답 파싱 실패, 1회 재시도 후에도 실패) | {"error": "internal_error"} |
- 기능2 통과 직후 동기로 호출된다. 실패 시(500) 가치 데이터를 알 수 없으므로 등록을 차단하고 재시도를 안내한다.
- 검색 실패해도 Claude가 estimated로 값을 채우므로, 값이 비는 경우는 API 호출 자체가 실패했을 때(500)뿐이다.
- 2.2가 실패(422/500)하면 이 요청은 트리거되지 않는다.
사용자 메시지를 받아 관련 정책 문서를 검색(RAG)하고, 정책 답변 생성 또는 화면 이동(navigation)을 제안한다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| message | string | O | 사용자 채팅 |
| history | array | X | 이전 대화 목록 (최근 10개로 제한) |
요청 예시
{
"message": "탈퇴는 어떻게 해요?",
"history": []
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| answer | string | O | LLM 답변 |
| action | object / null | - | 화면 이동 정보 (label, screen enum), 없으면 null |
응답 예시
{
"answer": "탈퇴는 마이페이지 > 계정 설정 > 탈퇴하기에서 진행하실 수 있어요. 탈퇴 후 7일 뒤 데이터가 완전히 삭제돼요.",
"action": { "label": "마이페이지로 이동", "screen": "MY_PAGE" }
}
claude-sonnet-5. 정책 문서 12개 섹션은 임베딩 모델로 사전 임베딩해 ChromaDB에 저장해두며(사용할 임베딩 모델 : dragonkue/multilingual-e5-small-ko-v2), 사용자 메시지도 동일한 임베딩 모델로 임베딩해 ChromaDB에서 유사도 상위 1~2개 섹션을 검색(RAG)한 뒤 그 내용만 근거로 답변하거나, 화면 이동 요청이면 지정된 enum(MY_PAGE/ITEM_LIST/ITEM_ADD/CHAT_LIST/GROUP_MANAGE/INTEREST_LIST/INQUIRY) 중 하나로 이동 버튼을 제안하도록 지시. 문서에 없는 내용은 추측 금지, 답을 못 찾으면 1:1 문의로 안내.
| 상태 코드 | 상황 | 응답 예시 |
|---|---|---|
| 400 | 입력창에 아무 입력이 없음 | {"error": "empty_message", "message": "메시지를 입력해주세요"} |
| 500 | AI 모델 처리 실패 (요청 한도 초과·응답 지연·응답 파싱 실패, 1회 재시도 후에도 실패) | {"error": "internal_error"} |
- 실패 시(500) 채팅창에 오류 메시지와 재전송 버튼 표시. 다른 화면에 영향 없음.
- 정책 문서에서 못 찾은 경우는 에러가 아니라 200 정상 응답(1:1 문의 안내)으로 처리.
- 물건 등록 흐름(2.1~2.3)과 독립적 — 이 요청의 성패가 다른 기능에 영향을 주지 않는다.
Backend가 필터링한 후보 물건 목록과 취향 신호를 받아 LLM으로 의미 기반 순위를 매겨 반환한다.
| 필드 | 타입 | 필수 | 설명 | 출처 |
|---|---|---|---|---|
| candidates | array | O | 후보 물건 목록 (item_id, title, description) | Backend 필터링 결과 |
| preference_text | string | - | 요청자가 직접 입력한 취향 텍스트 | Frontend 요청에서 전달 |
| wishlist_titles | array | - | 찜한 물건 제목 목록 | Backend DB 조회 |
| recent_search_keywords | array | - | 최근 검색 키워드 (최대 10개) | Backend DB 조회 |
| requested_item_titles | array | - | 요청 이력 (최신순) | Backend DB 조회 |
| completed_trade_titles | array | - | 거래완료 이력 (최신순) | Backend DB 조회 |
preference_text 이하 5개는 전부 선택이지만, 이 엔드포인트가 호출된다는 것 자체가 "최소 하나는 값이 있다"는 뜻이다.
요청 예시
{
"candidates": [
{ "item_id": "item_101", "title": "다우니 섬유유연제 4L 새 제품 여분", "description": "대용량으로 구매했는데 2병이 생겨서 1병 교환합니다. 미개봉 새 제품" },
{ "item_id": "item_102", "title": "질레트 퓨전 면도날 8입 중 여분 4개", "description": "새 제품 미개봉, 다른 브랜드로 바꾸면서 여분이 생겼어요" },
{ "item_id": "item_103", "title": "동서 맥심 스틱커피 100개입 중 여분 30개", "description": "새 제품, 다 못 마실 것 같아 나눔합니다" }
],
"preference_text": "세제나 생필품 여분 있으면 좋아요",
"wishlist_titles": ["질레트 퓨전 면도날 새 제품 여분"],
"recent_search_keywords": ["섬유유연제", "생필품 나눔"],
"requested_item_titles": [],
"completed_trade_titles": ["다우니 섬유유연제 4L 새 제품 여분"]
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| results | array | O | 순위 매겨진 item_id 배열 (최대 5개, 취향에 맞는 후보가 없으면 빈 배열) |
응답 예시
{ "results": ["item_101", "item_102"] }
claude-sonnet-5. 후보 물건의 title/description을 취향 신호와 의미적으로 비교해 최대 5개까지 순위 순으로 선택하도록 지시(맞는 후보가 없으면 빈 배열 허용). completed_trade_titles(실거래 완료)를 requested_item_titles(요청만)보다 강한 신호로, 최신 신호에 더 무게를 두도록 지시.
| 상태 코드 | 상황 | 응답 예시 |
|---|---|---|
| 400 | candidates가 비어있음 | {"error": "missing_candidates", "message": "추천할 후보 물건이 없어요."} |
| 500 | AI 모델 처리 실패 (요청 한도 초과·응답 지연·응답 파싱 실패, 1회 재시도 후에도 실패) | {"error": "internal_error"} |
- AI 모델 처리 실패(500)는 기술적 오류이므로 fallback 없이 그대로 에러 처리.
- candidates가 비어있는 400 호출은 정상 흐름에선 발생하지 않아야 하며, 방어적으로만 처리.
-
results: []는 오류가 아니라 정상 응답(취향에 맞는 후보 없음)으로 간주. - 취향 신호가 전부 없으면 이 엔드포인트는 아예 호출되지 않는다.
사용자가 입력한 메시지의 내용은 유지한 채 표현을 상대방 톤에 맞게 다듬는다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
original_text |
string | O | 사용자가 작성한 원본 메시지. 공백/빈 문자열 불가 |
my_profile |
object | O | 메시지를 작성하는 사용자의 대화 스타일 정보 (3항목) |
opponent_profile |
object | O | 상대방의 대화 스타일 정보 (3항목) |
my_profile / opponent_profile은 동일한 3개 필드로 구성된다.
| 필드 | 타입 | 옵션 예시 |
|---|---|---|
| tone | string | 정중하게 / 친근하게 / 간결하게 |
| explanation_style | string | 핵심만 짧게 / 적당히 설명 / 자세히 설명 |
| opinion_expression | string | 분명하게 / 자연스럽게 / 부드럽게 |
요청 예시
{
"original_text": "그건 좀 아닌 것 같은데요 다른 걸로 주세요",
"my_profile": {
"tone": "정중하게",
"explanation_style": "적당히 설명",
"opinion_expression": "분명하게"
},
"opponent_profile": {
"tone": "친근하게",
"explanation_style": "자세히 설명",
"opinion_expression": "부드럽게"
}
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
refined_text |
string | O | 다듬어진 메시지 텍스트 (미리보기용, 자동 적용 아님) |
응답 예시
{ "refined_text": "말씀 주신 조건은 조금 어려울 것 같아요. 혹시 다른 물건으로 조율해볼 수 있을까요?" }
claude-haiku-4.5. 두 층위로 구분해 처리한다.
- 시스템 고정 규칙 (프로필과 무관하게 항상 적용): 원문의 사실관계(가격/수량/조건/날짜)는 절대 유지, 항상 존댓말 사용, 이모지 제거
-
프로필 기반 조정 (말투/설명 스타일/의견 표현 3항목):
opponent_profile을 중심 기준으로 반영하고,my_profile은 결과가 사용자 본인 스타일에서 크게 벗어나지 않도록 참고하는 보조 가드레일로 사용
| 상태 코드 | 상황 | 응답 예시 |
|---|---|---|
| 400 |
original_text가 빈 문자열이거나 공백만 있음 |
{"error": "empty_text", "message": "다듬을 메시지를 먼저 입력해주세요."} |
| 500 | AI 모델 처리 실패 (요청 한도 초과·응답 지연·응답 파싱 실패, 1회 재시도 후에도 실패) | {"error": "internal_error"} |
- 500 실패 시 원본 텍스트를 입력창에 그대로 유지, 사용자가 원문 전송 또는 직접 수정 가능.
- "ㅇㅇ", "네" 같은 짧은 캐주얼 표현은 에러 처리하지 않고 존댓말 톤으로만 살짝 다듬어 반환될 수 있음.
- 참여자 검증과 프로필 조회는 Backend가 이 API 호출 전에 미리 끝내므로, 이 API 자체는 채팅방 식별자나 소유권을 별도로 확인하지 않는다.
API가 전체 서비스에서 담당하는 역할과 연동 구조를 설명한다.
사용자
↓
Frontend
↓
Backend
↓
AI API Server (FastAPI)
↓
Claude API (모델) / 서버 알고리즘
↓
Backend
↓
Frontend
- Backend는 사용자 인증·리소스 소유권 검증을 마친 뒤, 필요한 데이터를 AI API로 전달한다.
- AI API는 Claude 모델 호출(및 일부는 서버 알고리즘 후처리)로 결과를 생성한다.
- AI API는 Claude의 원본 응답을 그대로 넘기지 않고, 외부 API 응답(200/4xx/5xx)으로 매핑하는 계층 역할을 겸한다.
- 생성된 결과를 Backend에 반환하여 서비스 기능에 활용한다.
아래는 엔드포인트별로 위 공통 구조와 달라지는 지점(동기/비동기 여부, 다른 기능과의 의존 관계, 서버 알고리즘 개입 등)을 정리한 것이다.
AI 서버 (FastAPI)
POST /api/vision/analyze-item 요청 수신
│
▼
Claude Vision API (claude-sonnet-5)
검열 + 제품 식별 + 등록용 초안 작성 (JSON 응답)
│
▼
AI 서버 (FastAPI): 응답 매핑 처리
│
┌────┴────┐
▼ ▼
검증 실패 검증 통과
(422) (200)
│ │
▼ ┌────┴────┐
사진 ▼ ▼
재업로드 confidence confidence
안내 ≥ 0.25 < 0.25
│ │
▼ ▼
title/content title/content = null
그대로 반환 직접 입력 안내
검증(422)은 1~3장을 모두 함께 보고 판단하며, 세트 상품·클로즈업 샷은 정상으로 간주한다. "물건은 맞는데 흐릿하거나 애매한 사진"은 검열 대상이 아니다 — 통과(true)하되 confidence만 낮게 매겨진다.
AI 서버 (FastAPI)
POST /api/moderation/check-text 요청 수신
│
▼
Claude API (claude-haiku-4.5)
제목·내용 검증 (JSON 응답)
│
▼
AI 서버 (FastAPI): 응답 매핑 처리
│
┌────┴────┐
▼ ▼
검증 실패 검증 통과
(422) (200, 본문 없음)
│ │
▼ ▼
텍스트 수정 다음 단계(가치 평가) 진행
안내 반환 (등록·DB 저장은 이 API 범위 밖)
"서로 무관한 여러 물건 혼재 금지" 기준은 3.1(사진 검증)에도 동일하게 적용된다. 통과 여부만 반환할 뿐, 실제 DB 저장이나 다음 단계 호출을 직접 수행하지 않는다.
AI 서버 (FastAPI)
POST /api/value/estimate-price 요청 수신
│
▼
Claude API (claude-sonnet-5, web_search 활성화)
쇼핑몰 가격 검색 + 산정 (JSON 응답: prices, source, confidence)
│
▼
AI 서버 (FastAPI): 알고리즘 처리
(이상치 제거 → 평균=referenceUnitPrice → 할인율 계산 → minUnitPrice)
│
▼
AI 서버 (FastAPI): 응답 매핑 처리
│
┌────┴────┐
▼ ▼
처리 실패 처리 성공
(500) (200)
│ │
▼ ▼
등록 차단, referenceUnitPrice / minUnitPrice /
재시도 안내 maxUnitPrice / priceSource / confidence 반환
(사용자에게 노출되지 않음, 매칭에만 사용)
3.2(텍스트 검열) 통과 직후, 등록하기 흐름 안에서 동기로 실행된다. prices 배열의 이상치 제거→평균→할인율 계산은 LLM이 아니라 서버 코드가 수행한다. maxUnitPrice는 항상 referenceUnitPrice와 동일하다.
AI 서버 (FastAPI)
POST /api/chatbot/ask 요청 수신
│
▼
메시지 임베딩 → ChromaDB에서 12개 정책 섹션과 유사도 비교 → 상위 1~2개 추출
│
▼
Claude API (claude-sonnet-5)
정책 답변 생성 또는 화면 이동 판단 (JSON 응답: answer, action)
│
▼
AI 서버 (FastAPI): 응답 파싱 및 반환
│
▼
[프론트] answer 표시 + action 있으면 버튼 렌더링, 클릭 시 이동
정책 문서 12개 섹션은 사전에 임베딩되어 ChromaDB에 저장되어 있으며, 매 요청마다 새로 계산하지 않는다. 3.1~3.3(물건 등록 흐름)과 달리 독립적인 기능이다.
[Frontend] 스마트 검색 실행
│ group_id 함께 전달
▼
[Backend] 본인 인증 + 모임 소속 확인
│
▼
[Backend] 후보 필터링 (알고리즘)
본인 물건 제외, 내 총 가치범위와 겹치는 후보만 추출
후보 50개 초과 시 최신순으로 50개로 축소
│
▼
[Backend] 취향 신호 일괄 조회 (DB)
│
┄┄┄┄┄┄┄ 이 아래부터 AI 서버 영역 ┄┄┄┄┄┄┄
│
신호가 하나라도 있음
▼
[AI 서버] POST /api/search/rank-candidates
│
▼
Claude API (claude-sonnet-5)
후보 전체를 직접 읽고 취향 신호와 의미적으로 비교, 순위 매김
│
▼
[AI 서버] 응답 매핑 → 성공(200) / 실패(500)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│
신호 전혀 없음 → AI 서버 호출 없이 [Backend]가 가격 근접순 반환
│
▼
[Backend] item_id로 상세정보 재조회 → [Frontend] 카드 표시
AI 서버는 이미 필터링·조회가 끝난 데이터를 받아 "순위를 매기는" 역할만 한다. 취향 신호가 전부 비어 있으면 AI 서버는 아예 호출되지 않는다.
[Frontend] 메시지 작성 완료 후 [AI 다듬기] 클릭
│
▼
[Backend] 참여자 검증 + 양측 프로필 조회 (물건정보·대화내역 미조회)
▼
[AI Server] → [Claude] 톤 조정
│
▼
[Frontend] 원본/다듬어진 버전 나란히 미리보기
│
▼
[Frontend] [적용하기] → 입력창 텍스트 교체 (전송 아님)
AI 서버 요청에는 채팅방 식별자가 포함되지 않으며, Backend가 넘겨주는 두 프로필(3항목씩)과 원문 텍스트만으로 동작하는 stateless 구조다. [적용하기]는 입력창 텍스트 교체 동작일 뿐, 메시지 전송과는 분리된 별개의 사용자 행동이다.
구현한 API를 실제로 호출하고 결과를 확인한다.
아래 요청/응답은 명세에 맞춰 구성한 예시값입니다. 실제 서버를 호출해 받은 응답이 아니므로, 향후 수정할 예정입니다.
요청 예시 (multipart/form-data — JSON 아님, 파일 2장 첨부)
images: hetbahn_pack1.jpg, hetbahn_pack2.jpg
응답 예시
{
"isAppropriate": true,
"rejectionReason": null,
"title": "햇반 210g 새 제품 4개 교환해요",
"content": "햇반 210g 제품입니다. 24개입 세트로 구매했는데 다 못 먹을 것 같아서 4개만 교환하려고요. 미개봉 새 제품이고 유통기한은 2027년 6월까지예요.",
"confidence": 0.97
}
응답 예시 (검증 실패, 422)
{
"error": "unrelated_photo",
"message": "상품과 무관한 사진이 섞여 있어요. 교환할 물건이 담긴 사진으로 다시 올려주세요"
}
요청 예시
{
"title": "햇반 210g 새 제품 4개 교환해요",
"content": "24개입 세트로 구매했는데 다 못 먹을 것 같아서 4개만 교환하려고요. 미개봉 새 제품이고 유통기한은 2027년 6월까지예요."
}
응답 예시 (통과, 200)
본문 없음 — 상태 코드 200만 반환.
요청 예시 (차단 케이스 — 개인 연락처)
{
"title": "햇반 24개입 세트에서 4개 팔아요, 연락은 010-1234-5678로 주세요",
"content": "미개봉 새 제품, 가격 협의 가능합니다."
}
응답 예시 (차단, 422)
{ "error": "personal_contact", "message": "개인 연락처는 채팅으로만 주고받을 수 있어요. 게시글에서 연락처를 제거해주세요." }
요청 예시 (차단 케이스 — 가격 언급)
{
"title": "햇반 210g 4개 교환해요",
"content": "미개봉 새 제품이고, 개당 1000원 정도로 생각하시면 돼요."
}
응답 예시 (차단, 422)
{ "error": "price_mention", "message": "게시글에는 가격을 직접 언급할 수 없어요. 가격 관련 내용은 제거해주세요." }
요청 예시
{
"title": "햇반 210g 새 제품 4개 교환해요",
"content": "24개입 세트로 구매했는데 다 못 먹을 것 같아서 4개만 교환하려고요. 미개봉 새 제품이에요.",
"valueTolerance": 0.6,
"tradeSpeed": 0.4
}
응답 예시
{
"referenceUnitPrice": 1300,
"minUnitPrice": 1090,
"maxUnitPrice": 1300,
"priceSource": "new",
"confidence": null
}
응답 예시 (검색 실패, estimated 전환)
{
"referenceUnitPrice": 12000,
"minUnitPrice": 10060,
"maxUnitPrice": 12000,
"priceSource": "estimated",
"confidence": 0.4
}
요청 예시
{
"message": "탈퇴는 어떻게 해요?",
"history": []
}
응답 예시
{
"answer": "탈퇴는 마이페이지 > 계정 설정 > 탈퇴하기에서 진행하실 수 있어요. 탈퇴 후 7일 뒤 데이터가 완전히 삭제돼요.",
"action": { "label": "마이페이지로 이동", "screen": "MY_PAGE" }
}
요청 예시 (정책 매칭 실패)
{
"message": "환불 정책이 뭐예요?",
"history": []
}
응답 예시
{
"answer": "정확한 답변을 찾지 못했어요. 1:1 문의로 연결해드릴까요?",
"action": { "label": "1:1 문의하기", "screen": "INQUIRY" }
}
요청 예시
{
"candidates": [
{ "item_id": "item_101", "title": "다우니 섬유유연제 4L 새 제품 여분", "description": "대용량으로 구매했는데 2병이 생겨서 1병 교환합니다. 미개봉 새 제품" },
{ "item_id": "item_102", "title": "질레트 퓨전 면도날 8입 중 여분 4개", "description": "새 제품 미개봉, 다른 브랜드로 바꾸면서 여분이 생겼어요" },
{ "item_id": "item_103", "title": "동서 맥심 스틱커피 100개입 중 여분 30개", "description": "새 제품, 다 못 마실 것 같아 나눔합니다" }
],
"preference_text": "세제나 생필품 여분 있으면 좋아요",
"wishlist_titles": ["질레트 퓨전 면도날 새 제품 여분"],
"recent_search_keywords": ["섬유유연제", "생필품 나눔"],
"requested_item_titles": [],
"completed_trade_titles": ["다우니 섬유유연제 4L 새 제품 여분"]
}
응답 예시
{ "results": ["item_101", "item_102"] }
요청 예시
{
"original_text": "그건 좀 아닌 것 같은데요 다른 걸로 주세요",
"my_profile": {
"tone": "정중하게",
"explanation_style": "적당히 설명",
"opinion_expression": "분명하게"
},
"opponent_profile": {
"tone": "친근하게",
"explanation_style": "자세히 설명",
"opinion_expression": "부드럽게"
}
}
응답 예시
{ "refined_text": "말씀 주신 조건은 조금 어려울 것 같아요. 혹시 다른 물건으로 조율해볼 수 있을까요?" }