450 lines
19 KiB
Python
450 lines
19 KiB
Python
from typing import Literal, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from app.utils.prompts.schemas import MarketingPromptOutput
|
|
|
|
class AttributeInfo(BaseModel):
|
|
"""음악 속성 정보"""
|
|
|
|
genre: str = Field(..., description="음악 장르")
|
|
vocal: str = Field(..., description="보컬 스타일")
|
|
tempo: str = Field(..., description="템포")
|
|
mood: str = Field(..., description="분위기")
|
|
|
|
|
|
class GenerateRequestImg(BaseModel):
|
|
"""이미지 URL 스키마"""
|
|
|
|
url: str = Field(..., description="이미지 URL")
|
|
name: Optional[str] = Field(None, description="이미지명 (없으면 URL에서 추출)")
|
|
|
|
|
|
class GenerateRequestInfo(BaseModel):
|
|
"""생성 요청 정보 스키마 (이미지 제외)"""
|
|
|
|
customer_name: str = Field(..., description="고객명/가게명")
|
|
region: str = Field(..., description="지역명")
|
|
detail_region_info: Optional[str] = Field(None, description="상세 지역 정보")
|
|
attribute: AttributeInfo = Field(..., description="음악 속성 정보")
|
|
language: str = Field(
|
|
default="Korean",
|
|
description="가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)",
|
|
)
|
|
|
|
|
|
class GenerateRequest(GenerateRequestInfo):
|
|
"""기본 생성 요청 스키마 (이미지 없음, JSON body)
|
|
|
|
이미지 없이 프로젝트 정보만 전달합니다.
|
|
"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
"customer_name": "스테이 머뭄",
|
|
"region": "군산",
|
|
"detail_region_info": "군산 신흥동 말랭이 마을",
|
|
"attribute": {
|
|
"genre": "K-Pop",
|
|
"vocal": "Raspy",
|
|
"tempo": "110 BPM",
|
|
"mood": "happy",
|
|
},
|
|
"language": "Korean",
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
class GenerateUrlsRequest(GenerateRequestInfo):
|
|
"""URL 기반 생성 요청 스키마 (JSON body)
|
|
|
|
GenerateRequestInfo를 상속받아 이미지 목록을 추가합니다.
|
|
"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
"customer_name": "스테이 머뭄",
|
|
"region": "군산",
|
|
"detail_region_info": "군산 신흥동 말랭이 마을",
|
|
"attribute": {
|
|
"genre": "K-Pop",
|
|
"vocal": "Raspy",
|
|
"tempo": "110 BPM",
|
|
"mood": "happy",
|
|
},
|
|
"language": "Korean",
|
|
"images": [
|
|
{"url": "https://example.com/images/image_001.jpg"},
|
|
{"url": "https://example.com/images/image_002.jpg", "name": "외관"},
|
|
],
|
|
}
|
|
}
|
|
)
|
|
|
|
images: list[GenerateRequestImg] = Field(
|
|
..., description="이미지 URL 목록", min_length=1
|
|
)
|
|
|
|
|
|
class GenerateUploadResponse(BaseModel):
|
|
"""파일 업로드 기반 생성 응답 스키마"""
|
|
|
|
task_id: str = Field(..., description="작업 고유 식별자 (UUID7)")
|
|
status: Literal["processing", "completed", "failed"] = Field(
|
|
..., description="작업 상태"
|
|
)
|
|
message: str = Field(..., description="응답 메시지")
|
|
uploaded_count: int = Field(..., description="업로드된 이미지 개수")
|
|
|
|
|
|
class GenerateResponse(BaseModel):
|
|
"""생성 응답 스키마"""
|
|
|
|
task_id: str = Field(..., description="작업 고유 식별자 (UUID7)")
|
|
status: Literal["processing", "completed", "failed"] = Field(
|
|
..., description="작업 상태"
|
|
)
|
|
message: str = Field(..., description="응답 메시지")
|
|
|
|
|
|
class CrawlingRequest(BaseModel):
|
|
"""크롤링 요청 스키마"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
"url": "https://map.naver.com/p/search/%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84/place/1133638931?c=14.70,0,0,0,dh&placePath=/photo?businessCategory=pension&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5×tamp=202512191123&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5×tamp=202512191007&from=map&entry=bmp&filterType=%EC%97%85%EC%B2%B4&businessCategory=pension"
|
|
}
|
|
}
|
|
)
|
|
|
|
url: str = Field(..., description="네이버 지도 장소 URL")
|
|
|
|
class AutoCompleteRequest(BaseModel):
|
|
"""자동완성 요청 스키마"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
'title': '<b>스테이</b>,<b>머뭄</b>',
|
|
'address': '전북특별자치도 군산시 신흥동 63-18',
|
|
'roadAddress': '전북특별자치도 군산시 절골길 18',
|
|
}
|
|
}
|
|
)
|
|
|
|
title: str = Field(..., description="네이버 검색 place API Title")
|
|
address: str = Field(..., description="네이버 검색 place API 지번주소")
|
|
roadAddress: Optional[str] = Field(None, description="네이버 검색 place API 도로명주소")
|
|
|
|
|
|
class AccommodationSearchItem(BaseModel):
|
|
"""숙박 검색 결과 아이템"""
|
|
|
|
title: str = Field(..., description="숙소명 (HTML 태그 포함 가능)")
|
|
address: str = Field(..., description="지번 주소")
|
|
roadAddress: str = Field(default="", description="도로명 주소")
|
|
|
|
|
|
class AccommodationSearchResponse(BaseModel):
|
|
"""숙박 자동완성 검색 응답"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
"query": "스테이 머뭄",
|
|
"count": 2,
|
|
"items": [
|
|
{
|
|
"title": "<b>스테이</b>,<b>머뭄</b>",
|
|
"address": "전북특별자치도 군산시 신흥동 63-18",
|
|
"roadAddress": "전북특별자치도 군산시 절골길 18",
|
|
},
|
|
{
|
|
"title": "머뭄<b>스테이</b>",
|
|
"address": "전북특별자치도 군산시 비응도동 123",
|
|
"roadAddress": "전북특별자치도 군산시 비응로 456",
|
|
},
|
|
],
|
|
}
|
|
}
|
|
)
|
|
|
|
query: str = Field(..., description="검색어")
|
|
count: int = Field(..., description="검색 결과 수")
|
|
items: list[AccommodationSearchItem] = Field(
|
|
default_factory=list, description="검색 결과 목록"
|
|
)
|
|
|
|
class ProcessedInfo(BaseModel):
|
|
"""가공된 장소 정보 스키마"""
|
|
|
|
customer_name: str = Field(..., description="고객명/가게명 (base_info.name)")
|
|
region: str = Field(..., description="지역명 (roadAddress에서 추출한 시 이름)")
|
|
detail_region_info: str = Field(..., description="상세 지역 정보 (roadAddress)")
|
|
|
|
|
|
# class MarketingAnalysisDetail(BaseModel):
|
|
# detail_title : str = Field(..., description="디테일 카테고리 이름")
|
|
# detail_description : str = Field(..., description="해당 항목 설명")
|
|
|
|
# class MarketingAnalysisReport(BaseModel):
|
|
# """마케팅 분석 리포트 스키마"""
|
|
# summary : str = Field(..., description="비즈니스 한 줄 요약")
|
|
# details : list[MarketingAnalysisDetail] = Field(default_factory=list, description="개별 디테일")
|
|
|
|
# class MarketingAnalysis(BaseModel):
|
|
# """마케팅 분석 결과 스키마"""
|
|
|
|
# # report: MarketingAnalysisReport = Field(..., description="마케팅 분석 리포트")
|
|
# tags: list[str] = Field(default_factory=list, description="추천 태그 목록")
|
|
# selling_points: list[str] = Field(default_factory=list, description="추천 부대시설 목록")
|
|
|
|
|
|
class CrawlingResponse(BaseModel):
|
|
"""크롤링 응답 스키마"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
"status": "completed",
|
|
"image_list": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
|
|
"image_count": 2,
|
|
"processed_info": {
|
|
"customer_name": "스테이 머뭄",
|
|
"region": "군산",
|
|
"detail_region_info": "전북특별자치도 군산시 절골길 18"
|
|
},
|
|
"marketing_analysis": {
|
|
"brand_identity": {
|
|
"location_feature_analysis": "전북 군산시 절골길 일대는 도시의 편의성과 근교의 한적함을 동시에 누릴 수 있어 ‘조용한 재충전’ 수요에 적합합니다. 군산의 레트로 감성과 주변 관광 동선 결합이 쉬워 1~2박 체류형 여행지로 매력적입니다.",
|
|
"concept_scalability": "‘머뭄’이라는 네이밍을 ‘잠시 멈춰 머무는 시간’으로 확장해, 느린 체크인·명상/독서 큐레이션·로컬 티/다과 등 체류 경험형 서비스로 고도화가 가능합니다. 로컬 콘텐츠(군산 빵/커피, 근대문화 투어)와 결합해 패키지화하면 재방문 명분을 만들 수 있습니다."
|
|
},
|
|
"market_positioning": {
|
|
"category_definition": "군산 감성 ‘슬로우 스테이’ 프라이빗 숙소",
|
|
"core_value": "아무것도 하지 않아도 회복되는 ‘멈춤의 시간’"
|
|
},
|
|
"target_persona": [
|
|
{
|
|
"persona": "번아웃 회복형 직장인 커플: 주말에 조용히 쉬며 리셋을 원하는 2인 여행자",
|
|
"age": {
|
|
"min_age": 27,
|
|
"max_age": 39
|
|
},
|
|
"favor_target": [
|
|
"조용한 동네 분위기",
|
|
"미니멀/내추럴 인테리어",
|
|
"편안한 침구와 숙면 환경",
|
|
"셀프 체크인 선호",
|
|
"카페·맛집 연계 동선"
|
|
],
|
|
"decision_trigger": "‘조용히 쉬는 데 최적화’된 프라이빗함과 숙면 컨디션(침구/동선/소음 차단) 확신"
|
|
},
|
|
{
|
|
"persona": "감성 기록형 친구 여행: 사진과 무드를 위해 공간을 선택하는 2~3인 여성 그룹",
|
|
"age": {
|
|
"min_age": 23,
|
|
"max_age": 34
|
|
},
|
|
"favor_target": [
|
|
"자연광 좋은 공간",
|
|
"감성 소품/컬러 톤",
|
|
"포토존(거울/창가/테이블)",
|
|
"와인·디저트 페어링",
|
|
"야간 무드 조명"
|
|
],
|
|
"decision_trigger": "사진이 ‘그대로 작품’이 되는 포토 스팟과 야간 무드 연출 요소"
|
|
},
|
|
{
|
|
"persona": "로컬 탐험형 소도시 여행자: 군산의 레트로/로컬 콘텐츠를 깊게 즐기는 커플·솔로",
|
|
"age": {
|
|
"min_age": 28,
|
|
"max_age": 45
|
|
},
|
|
"favor_target": [
|
|
"근대문화/레트로 감성",
|
|
"로컬 맛집·빵집 투어",
|
|
"동선 효율(차로 이동 용이)",
|
|
"체크아웃 후 관광 연계",
|
|
"조용한 밤"
|
|
],
|
|
"decision_trigger": "‘군산 로컬 동선’과 결합하기 좋은 위치 + 숙소 자체의 휴식 완성도"
|
|
}
|
|
],
|
|
"selling_points": [
|
|
{
|
|
"category": "LOCATION",
|
|
"description": "군산 감성 동선",
|
|
"score": 88
|
|
},
|
|
{
|
|
"category": "HEALING",
|
|
"description": "멈춤이 되는 쉼",
|
|
"score": 92
|
|
},
|
|
{
|
|
"category": "PRIVACY",
|
|
"description": "방해 없는 머뭄",
|
|
"score": 86
|
|
},
|
|
{
|
|
"category": "NIGHT MOOD",
|
|
"description": "밤이 예쁜 조명",
|
|
"score": 84
|
|
},
|
|
{
|
|
"category": "PHOTO SPOT",
|
|
"description": "자연광 포토존",
|
|
"score": 83
|
|
},
|
|
{
|
|
"category": "SHORT GETAWAY",
|
|
"description": "주말 리셋 스테이",
|
|
"score": 89
|
|
},
|
|
{
|
|
"category": "HOSPITALITY",
|
|
"description": "세심한 웰컴감",
|
|
"score": 80
|
|
}
|
|
],
|
|
"target_keywords": [
|
|
"군산숙소",
|
|
"군산감성숙소",
|
|
"전북숙소추천",
|
|
"군산여행",
|
|
"커플스테이",
|
|
"주말여행",
|
|
"감성스테이",
|
|
"조용한숙소",
|
|
"힐링스테이",
|
|
"스테이머뭄"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
status: str = Field(
|
|
default="completed",
|
|
description="처리 상태 (completed: 성공, failed: ChatGPT 분석 실패)"
|
|
)
|
|
image_list: Optional[list[str]] = Field(None, description="이미지 URL 목록")
|
|
image_count: int = Field(..., description="이미지 개수")
|
|
processed_info: Optional[ProcessedInfo] = Field(
|
|
None, description="가공된 장소 정보 (customer_name, region, detail_region_info)"
|
|
)
|
|
marketing_analysis: Optional[MarketingPromptOutput] = Field(
|
|
None, description="마케팅 분석 결과 . 실패 시 null"
|
|
)
|
|
|
|
|
|
class ErrorResponse(BaseModel):
|
|
"""에러 응답 스키마"""
|
|
|
|
success: bool = Field(default=False, description="요청 성공 여부")
|
|
error_code: str = Field(..., description="에러 코드")
|
|
message: str = Field(..., description="에러 메시지")
|
|
detail: Optional[str] = Field(None, description="상세 에러 정보")
|
|
|
|
|
|
# =============================================================================
|
|
# Image Upload Schemas
|
|
# =============================================================================
|
|
|
|
|
|
class ImageUrlItem(BaseModel):
|
|
"""이미지 URL 아이템 스키마"""
|
|
|
|
url: str = Field(..., description="외부 이미지 URL")
|
|
name: Optional[str] = Field(None, description="이미지명 (없으면 URL에서 추출)")
|
|
|
|
|
|
class ImageUploadRequest(BaseModel):
|
|
"""이미지 업로드 요청 스키마 (JSON body 부분)
|
|
|
|
URL 이미지 목록을 전달합니다.
|
|
바이너리 파일은 multipart/form-data로 별도 전달됩니다.
|
|
"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
"images": [
|
|
{"url": "https://example.com/images/image_001.jpg"},
|
|
{"url": "https://example.com/images/image_002.jpg", "name": "외관"},
|
|
]
|
|
}
|
|
}
|
|
)
|
|
|
|
images: Optional[list[ImageUrlItem]] = Field(
|
|
None, description="외부 이미지 URL 목록"
|
|
)
|
|
|
|
|
|
class ImageUploadResultItem(BaseModel):
|
|
"""업로드된 이미지 결과 아이템"""
|
|
|
|
id: int = Field(..., description="이미지 ID")
|
|
img_name: str = Field(..., description="이미지명")
|
|
img_url: str = Field(..., description="이미지 URL")
|
|
img_order: int = Field(..., description="이미지 순서")
|
|
source: Literal["url", "file", "blob"] = Field(
|
|
..., description="이미지 소스 (url: 외부 URL, file: 로컬 서버, blob: Azure Blob)"
|
|
)
|
|
|
|
|
|
class ImageUploadResponse(BaseModel):
|
|
"""이미지 업로드 응답 스키마"""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"example": {
|
|
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
|
"total_count": 3,
|
|
"url_count": 2,
|
|
"file_count": 1,
|
|
"saved_count": 3,
|
|
"images": [
|
|
{
|
|
"id": 1,
|
|
"img_name": "외관",
|
|
"img_url": "https://example.com/images/image_001.jpg",
|
|
"img_order": 0,
|
|
"source": "url",
|
|
},
|
|
{
|
|
"id": 2,
|
|
"img_name": "내부",
|
|
"img_url": "https://example.com/images/image_002.jpg",
|
|
"img_order": 1,
|
|
"source": "url",
|
|
},
|
|
{
|
|
"id": 3,
|
|
"img_name": "uploaded_image.jpg",
|
|
"img_url": "/media/image/2024-01-15/0694b716-dbff-7219-8000-d08cb5fce431/uploaded_image_002.jpg",
|
|
"img_order": 2,
|
|
"source": "file",
|
|
},
|
|
],
|
|
"image_urls": [
|
|
"https://example.com/images/image_001.jpg",
|
|
"https://example.com/images/image_002.jpg",
|
|
"/media/image/2024-01-15/0694b716-dbff-7219-8000-d08cb5fce431/uploaded_image_002.jpg",
|
|
],
|
|
}
|
|
}
|
|
)
|
|
|
|
task_id: str = Field(..., description="작업 고유 식별자 (새로 생성된 UUID7)")
|
|
total_count: int = Field(..., description="총 업로드된 이미지 개수")
|
|
url_count: int = Field(..., description="URL로 등록된 이미지 개수")
|
|
file_count: int = Field(..., description="파일로 업로드된 이미지 개수")
|
|
saved_count: int = Field(..., description="Image 테이블에 저장된 row 수")
|
|
images: list[ImageUploadResultItem] = Field(..., description="업로드된 이미지 목록")
|
|
image_urls: list[str] = Field(..., description="Image 테이블에 저장된 현재 task_id의 이미지 URL 목록")
|