이미지 업로드 때 task_id 생성으로 변경, 가사 생성시 task_id 받아오는 것으로 변경

insta
bluebamus 2025-12-29 11:01:35 +09:00
parent f81d158f0f
commit c6d9edbb42
4 changed files with 130 additions and 53 deletions

View File

@ -179,11 +179,11 @@ IMAGES_JSON_EXAMPLE = """[
@router.post( @router.post(
"/image/upload/server/{task_id}", "/image/upload/server",
include_in_schema=False, include_in_schema=False,
summary="이미지 업로드 (로컬 서버)", summary="이미지 업로드 (로컬 서버)",
description=""" description="""
task_id에 연결된 이미지를 로컬 서버(media 폴더) 업로드합니다. 이미지를 로컬 서버(media 폴더) 업로드하고 새로운 task_id를 생성합니다.
## 요청 방식 ## 요청 방식
multipart/form-data 형식으로 전송합니다. multipart/form-data 형식으로 전송합니다.
@ -258,6 +258,10 @@ print(response.json())
## 저장 경로 ## 저장 경로
- 바이너리 파일: /media/image/{날짜}/{uuid7}/{파일명} - 바이너리 파일: /media/image/{날짜}/{uuid7}/{파일명}
- URL 이미지: 외부 URL 그대로 Image 테이블에 저장 - URL 이미지: 외부 URL 그대로 Image 테이블에 저장
## 반환 정보
- **task_id**: 새로 생성된 작업 고유 식별자
- **image_urls**: Image 테이블에 저장된 현재 task_id의 이미지 URL 목록
""", """,
response_model=ImageUploadResponse, response_model=ImageUploadResponse,
responses={ responses={
@ -267,7 +271,6 @@ print(response.json())
tags=["image"], tags=["image"],
) )
async def upload_images( async def upload_images(
task_id: str,
images_json: Optional[str] = Form( images_json: Optional[str] = Form(
default=None, default=None,
description="외부 이미지 URL 목록 (JSON 문자열)", description="외부 이미지 URL 목록 (JSON 문자열)",
@ -279,6 +282,9 @@ async def upload_images(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> ImageUploadResponse: ) -> ImageUploadResponse:
"""이미지 업로드 (URL + 바이너리 파일)""" """이미지 업로드 (URL + 바이너리 파일)"""
# task_id 생성
task_id = await generate_task_id()
# 1. 진입 검증: images_json 또는 files 중 하나는 반드시 있어야 함 # 1. 진입 검증: images_json 또는 files 중 하나는 반드시 있어야 함
has_images_json = images_json is not None and images_json.strip() != "" has_images_json = images_json is not None and images_json.strip() != ""
has_files = files is not None and len(files) > 0 has_files = files is not None and len(files) > 0
@ -405,6 +411,9 @@ async def upload_images(
saved_count = len(result_images) saved_count = len(result_images)
await session.commit() await session.commit()
# Image 테이블에서 현재 task_id의 이미지 URL 목록 조회
image_urls = [img.img_url for img in result_images]
return ImageUploadResponse( return ImageUploadResponse(
task_id=task_id, task_id=task_id,
total_count=len(result_images), total_count=len(result_images),
@ -412,14 +421,15 @@ async def upload_images(
file_count=len(valid_files), file_count=len(valid_files),
saved_count=saved_count, saved_count=saved_count,
images=result_images, images=result_images,
image_urls=image_urls,
) )
@router.post( @router.post(
"/image/upload/blob/{task_id}", "/image/upload/blob",
summary="이미지 업로드 (Azure Blob Storage)", summary="이미지 업로드 (Azure Blob Storage)",
description=""" description="""
task_id에 연결된 이미지를 Azure Blob Storage에 업로드합니다. 이미지를 Azure Blob Storage에 업로드하고 새로운 task_id를 생성합니다.
바이너리 파일은 로컬 서버에 저장하지 않고 Azure Blob에 직접 업로드됩니다. 바이너리 파일은 로컬 서버에 저장하지 않고 Azure Blob에 직접 업로드됩니다.
## 요청 방식 ## 요청 방식
@ -447,24 +457,25 @@ jpg, jpeg, png, webp, heic, heif
### cURL로 테스트 ### cURL로 테스트
```bash ```bash
# 바이너리 파일만 업로드 # 바이너리 파일만 업로드
curl -X POST "http://localhost:8000/image/upload/blob/test-task-001" \\ curl -X POST "http://localhost:8000/image/upload/blob" \\
-F "files=@/path/to/image1.jpg" \\ -F "files=@/path/to/image1.jpg" \\
-F "files=@/path/to/image2.png" -F "files=@/path/to/image2.png"
# URL + 바이너리 파일 동시 업로드 # URL + 바이너리 파일 동시 업로드
curl -X POST "http://localhost:8000/image/upload/blob/test-task-001" \\ curl -X POST "http://localhost:8000/image/upload/blob" \\
-F 'images_json=[{"url":"https://example.com/image.jpg"}]' \\ -F 'images_json=[{"url":"https://example.com/image.jpg"}]' \\
-F "files=@/path/to/local_image.jpg" -F "files=@/path/to/local_image.jpg"
``` ```
## 반환 정보 ## 반환 정보
- **task_id**: 작업 고유 식별자 - **task_id**: 새로 생성된 작업 고유 식별자
- **total_count**: 업로드된 이미지 개수 - **total_count**: 업로드된 이미지 개수
- **url_count**: URL로 등록된 이미지 개수 (Image 테이블에 외부 URL 그대로 저장) - **url_count**: URL로 등록된 이미지 개수 (Image 테이블에 외부 URL 그대로 저장)
- **file_count**: 파일로 업로드된 이미지 개수 (Azure Blob Storage에 저장) - **file_count**: 파일로 업로드된 이미지 개수 (Azure Blob Storage에 저장)
- **saved_count**: Image 테이블에 저장된 row - **saved_count**: Image 테이블에 저장된 row
- **images**: 업로드된 이미지 목록 - **images**: 업로드된 이미지 목록
- **source**: "url" (외부 URL) 또는 "blob" (Azure Blob Storage) - **source**: "url" (외부 URL) 또는 "blob" (Azure Blob Storage)
- **image_urls**: Image 테이블에 저장된 현재 task_id의 이미지 URL 목록
## 저장 경로 ## 저장 경로
- 바이너리 파일: Azure Blob Storage ({BASE_URL}/{task_id}/image/{파일명}) - 바이너리 파일: Azure Blob Storage ({BASE_URL}/{task_id}/image/{파일명})
@ -478,7 +489,6 @@ curl -X POST "http://localhost:8000/image/upload/blob/test-task-001" \\
tags=["image"], tags=["image"],
) )
async def upload_images_blob( async def upload_images_blob(
task_id: str,
images_json: Optional[str] = Form( images_json: Optional[str] = Form(
default=None, default=None,
description="외부 이미지 URL 목록 (JSON 문자열)", description="외부 이미지 URL 목록 (JSON 문자열)",
@ -490,6 +500,9 @@ async def upload_images_blob(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> ImageUploadResponse: ) -> ImageUploadResponse:
"""이미지 업로드 (URL + Azure Blob Storage)""" """이미지 업로드 (URL + Azure Blob Storage)"""
# task_id 생성
task_id = await generate_task_id()
# 1. 진입 검증 # 1. 진입 검증
has_images_json = images_json is not None and images_json.strip() != "" has_images_json = images_json is not None and images_json.strip() != ""
has_files = files is not None and len(files) > 0 has_files = files is not None and len(files) > 0
@ -609,6 +622,9 @@ async def upload_images_blob(
saved_count = len(result_images) saved_count = len(result_images)
await session.commit() await session.commit()
# Image 테이블에서 현재 task_id의 이미지 URL 목록 조회
image_urls = [img.img_url for img in result_images]
return ImageUploadResponse( return ImageUploadResponse(
task_id=task_id, task_id=task_id,
total_count=len(result_images), total_count=len(result_images),
@ -616,4 +632,5 @@ async def upload_images_blob(
file_count=len(valid_files) - len(skipped_files), file_count=len(valid_files) - len(skipped_files),
saved_count=saved_count, saved_count=saved_count,
images=result_images, images=result_images,
image_urls=image_urls,
) )

View File

@ -211,9 +211,50 @@ class ImageUploadResultItem(BaseModel):
class ImageUploadResponse(BaseModel): class ImageUploadResponse(BaseModel):
"""이미지 업로드 응답 스키마""" """이미지 업로드 응답 스키마"""
task_id: str = Field(..., description="작업 고유 식별자") 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="총 업로드된 이미지 개수") total_count: int = Field(..., description="총 업로드된 이미지 개수")
url_count: int = Field(..., description="URL로 등록된 이미지 개수") url_count: int = Field(..., description="URL로 등록된 이미지 개수")
file_count: int = Field(..., description="파일로 업로드된 이미지 개수") file_count: int = Field(..., description="파일로 업로드된 이미지 개수")
saved_count: int = Field(..., description="Image 테이블에 저장된 row 수") saved_count: int = Field(..., description="Image 테이블에 저장된 row 수")
images: list[ImageUploadResultItem] = Field(..., description="업로드된 이미지 목록") images: list[ImageUploadResultItem] = Field(..., description="업로드된 이미지 목록")
image_urls: list[str] = Field(..., description="Image 테이블에 저장된 현재 task_id의 이미지 URL 목록")

View File

@ -42,7 +42,6 @@ from app.lyric.schemas.lyric import (
LyricStatusResponse, LyricStatusResponse,
) )
from app.utils.chatgpt_prompt import ChatgptService from app.utils.chatgpt_prompt import ChatgptService
from app.utils.common import generate_task_id
from app.utils.pagination import PaginatedResponse, get_paginated from app.utils.pagination import PaginatedResponse, get_paginated
router = APIRouter(prefix="/lyric", tags=["lyric"]) router = APIRouter(prefix="/lyric", tags=["lyric"])
@ -159,6 +158,7 @@ async def get_lyric_by_task_id(
고객 정보를 기반으로 ChatGPT를 이용하여 가사를 생성합니다. 고객 정보를 기반으로 ChatGPT를 이용하여 가사를 생성합니다.
## 요청 필드 ## 요청 필드
- **task_id**: 작업 고유 식별자 (이미지 업로드 생성된 task_id, 필수)
- **customer_name**: 고객명/가게명 (필수) - **customer_name**: 고객명/가게명 (필수)
- **region**: 지역명 (필수) - **region**: 지역명 (필수)
- **detail_region_info**: 상세 지역 정보 (선택) - **detail_region_info**: 상세 지역 정보 (선택)
@ -180,6 +180,7 @@ async def get_lyric_by_task_id(
``` ```
POST /lyric/generate POST /lyric/generate
{ {
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"customer_name": "스테이 머뭄", "customer_name": "스테이 머뭄",
"region": "군산", "region": "군산",
"detail_region_info": "군산 신흥동 말랭이 마을", "detail_region_info": "군산 신흥동 말랭이 마을",
@ -220,9 +221,10 @@ async def generate_lyric(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> GenerateLyricResponse: ) -> GenerateLyricResponse:
"""고객 정보를 기반으로 가사를 생성합니다.""" """고객 정보를 기반으로 가사를 생성합니다."""
task_id = await generate_task_id(session=session, table_name=Project) task_id = request_body.task_id
print( print(
f"[generate_lyric] START - task_id: {task_id}, customer_name: {request_body.customer_name}, region: {request_body.region}" f"[generate_lyric] START - task_id: {task_id}, "
f"customer_name: {request_body.customer_name}, region: {request_body.region}"
) )
try: try:

View File

@ -25,7 +25,7 @@ Lyric API Schemas
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, ConfigDict, Field
class GenerateLyricRequest(BaseModel): class GenerateLyricRequest(BaseModel):
@ -37,6 +37,7 @@ class GenerateLyricRequest(BaseModel):
Example Request: Example Request:
{ {
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"customer_name": "스테이 머뭄", "customer_name": "스테이 머뭄",
"region": "군산", "region": "군산",
"detail_region_info": "군산 신흥동 말랭이 마을", "detail_region_info": "군산 신흥동 말랭이 마을",
@ -44,17 +45,21 @@ class GenerateLyricRequest(BaseModel):
} }
""" """
model_config = { model_config = ConfigDict(
"json_schema_extra": { json_schema_extra={
"example": { "example": {
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"customer_name": "스테이 머뭄", "customer_name": "스테이 머뭄",
"region": "군산", "region": "군산",
"detail_region_info": "군산 신흥동 말랭이 마을", "detail_region_info": "군산 신흥동 말랭이 마을",
"language": "Korean", "language": "Korean",
} }
} }
} )
task_id: str = Field(
..., description="작업 고유 식별자 (이미지 업로드 시 생성된 task_id)"
)
customer_name: str = Field(..., description="고객명/가게명") customer_name: str = Field(..., description="고객명/가게명")
region: str = Field(..., description="지역명") region: str = Field(..., description="지역명")
detail_region_info: Optional[str] = Field(None, description="상세 지역 정보") detail_region_info: Optional[str] = Field(None, description="상세 지역 정보")
@ -76,26 +81,20 @@ class GenerateLyricResponse(BaseModel):
- ChatGPT API 오류 - ChatGPT API 오류
- ChatGPT 거부 응답 (I'm sorry, I cannot, I can't, I apologize ) - ChatGPT 거부 응답 (I'm sorry, I cannot, I can't, I apologize )
- 응답에 ERROR: 포함 - 응답에 ERROR: 포함
Example Response (Success):
{
"success": true,
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
"lyric": "인스타 감성의 스테이 머뭄...",
"language": "Korean",
"error_message": null
}
Example Response (Failure):
{
"success": false,
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
"lyric": null,
"language": "Korean",
"error_message": "I'm sorry, I can't comply with that request."
}
""" """
model_config = ConfigDict(
json_schema_extra={
"example": {
"success": True,
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"lyric": "인스타 감성의 스테이 머뭄\n군산 신흥동 말랭이 마을에서\n여유로운 하루를 보내며\n추억을 만들어가요",
"language": "Korean",
"error_message": None,
}
}
)
success: bool = Field(..., description="생성 성공 여부") success: bool = Field(..., description="생성 성공 여부")
task_id: Optional[str] = Field(None, description="작업 고유 식별자 (uuid7)") task_id: Optional[str] = Field(None, description="작업 고유 식별자 (uuid7)")
lyric: Optional[str] = Field(None, description="생성된 가사 (성공 시)") lyric: Optional[str] = Field(None, description="생성된 가사 (성공 시)")
@ -109,15 +108,18 @@ class LyricStatusResponse(BaseModel):
Usage: Usage:
GET /lyric/status/{task_id} GET /lyric/status/{task_id}
Returns the current processing status of a lyric generation task. Returns the current processing status of a lyric generation task.
Example Response:
{
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
"status": "completed",
"message": "가사 생성이 완료되었습니다."
}
""" """
model_config = ConfigDict(
json_schema_extra={
"example": {
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"status": "completed",
"message": "가사 생성이 완료되었습니다.",
}
}
)
task_id: str = Field(..., description="작업 고유 식별자") task_id: str = Field(..., description="작업 고유 식별자")
status: str = Field(..., description="처리 상태 (processing, completed, failed)") status: str = Field(..., description="처리 상태 (processing, completed, failed)")
message: str = Field(..., description="상태 메시지") message: str = Field(..., description="상태 메시지")
@ -129,18 +131,21 @@ class LyricDetailResponse(BaseModel):
Usage: Usage:
GET /lyric/{task_id} GET /lyric/{task_id}
Returns the generated lyric content for a specific task. Returns the generated lyric content for a specific task.
"""
Example Response: model_config = ConfigDict(
{ json_schema_extra={
"example": {
"id": 1, "id": 1,
"task_id": "019123ab-cdef-7890-abcd-ef1234567890", "task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"project_id": 1, "project_id": 1,
"status": "completed", "status": "completed",
"lyric_prompt": "...", "lyric_prompt": "고객명: 스테이 머뭄, 지역: 군산...",
"lyric_result": "생성된 가사...", "lyric_result": "인스타 감성의 스테이 머뭄\n군산 신흥동 말랭이 마을에서\n여유로운 하루를 보내며\n추억을 만들어가요",
"created_at": "2024-01-01T12:00:00" "created_at": "2024-01-15T12:00:00",
} }
""" }
)
id: int = Field(..., description="가사 ID") id: int = Field(..., description="가사 ID")
task_id: str = Field(..., description="작업 고유 식별자") task_id: str = Field(..., description="작업 고유 식별자")
@ -158,6 +163,18 @@ class LyricListItem(BaseModel):
Used as individual items in paginated lyric list responses. Used as individual items in paginated lyric list responses.
""" """
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": 1,
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"status": "completed",
"lyric_result": "인스타 감성의 스테이 머뭄\n군산 신흥동 말랭이 마을에서...",
"created_at": "2024-01-15T12:00:00",
}
}
)
id: int = Field(..., description="가사 ID") id: int = Field(..., description="가사 ID")
task_id: str = Field(..., description="작업 고유 식별자") task_id: str = Field(..., description="작업 고유 식별자")
status: str = Field(..., description="처리 상태") status: str = Field(..., description="처리 상태")