api 1차 포매팅
parent
4f42efde18
commit
9d306eb68e
|
|
@ -0,0 +1,7 @@
|
||||||
|
from .clinics import router as clinics_router
|
||||||
|
from .analyses import router as analyses_router
|
||||||
|
from .reports import router as reports_router
|
||||||
|
from .plans import router as plans_router
|
||||||
|
from .channels import router as channels_router
|
||||||
|
|
||||||
|
routers = [clinics_router, analyses_router, reports_router, plans_router, channels_router]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
from common.deps import verify_api_key
|
||||||
|
from models.analysis import AnalysisCreate, AnalysisStartResponse, AnalysisStatusResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/analyses", tags=["analyses"], dependencies=[Depends(verify_api_key)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", status_code=status.HTTP_202_ACCEPTED, response_model=AnalysisStartResponse)
|
||||||
|
async def start_analysis(body: AnalysisCreate):
|
||||||
|
return AnalysisStartResponse(
|
||||||
|
analysis_run_id="22222222-2222-2222-2222-222222222222",
|
||||||
|
clinic_id=body.clinic_id or "11111111-1111-1111-1111-111111111111",
|
||||||
|
status="discovering",
|
||||||
|
estimated_seconds=90,
|
||||||
|
poll_url="/api/analyses/22222222-2222-2222-2222-222222222222/status",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{run_id}/status", response_model=AnalysisStatusResponse)
|
||||||
|
async def get_analysis_status(run_id: str):
|
||||||
|
return AnalysisStatusResponse(
|
||||||
|
analysis_run_id=run_id,
|
||||||
|
status="collecting",
|
||||||
|
progress=0.45,
|
||||||
|
current_step="채널 데이터 수집 중",
|
||||||
|
channel_errors={},
|
||||||
|
completed_at=None,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from common.deps import verify_api_key
|
||||||
|
from models.channel import ChannelVerifyRequest, ChannelVerifyResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/channels", tags=["channels"], dependencies=[Depends(verify_api_key)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/verify", response_model=ChannelVerifyResponse)
|
||||||
|
async def verify_channels(body: ChannelVerifyRequest):
|
||||||
|
return ChannelVerifyResponse(
|
||||||
|
youtube={
|
||||||
|
"handle": body.youtube,
|
||||||
|
"verified": True,
|
||||||
|
"display_name": "바노바기 BANOBAGI",
|
||||||
|
"followers": 12345,
|
||||||
|
} if body.youtube else None,
|
||||||
|
instagram=[
|
||||||
|
{"handle": handle, "verified": "unverifiable", "note": "Instagram 로그인 벽"}
|
||||||
|
for handle in body.instagram
|
||||||
|
] if body.instagram else None,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
from common.deps import verify_api_key
|
||||||
|
from models.clinic import ClinicCreate, ClinicCreateResponse, ClinicHistoryResponse, RunSummary
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/clinics", tags=["clinics"], dependencies=[Depends(verify_api_key)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", status_code=status.HTTP_201_CREATED, response_model=ClinicCreateResponse)
|
||||||
|
async def create_clinic(body: ClinicCreate):
|
||||||
|
return ClinicCreateResponse(
|
||||||
|
id="11111111-1111-1111-1111-111111111111",
|
||||||
|
url=body.url,
|
||||||
|
name=body.name,
|
||||||
|
created_at="2026-04-20T09:00:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}/history", response_model=ClinicHistoryResponse)
|
||||||
|
async def get_clinic_history(id: str):
|
||||||
|
return ClinicHistoryResponse(
|
||||||
|
clinic_id=id,
|
||||||
|
runs=[
|
||||||
|
RunSummary(
|
||||||
|
run_id="22222222-2222-2222-2222-222222222222",
|
||||||
|
status="complete",
|
||||||
|
started_at="2026-04-20T09:00:00Z",
|
||||||
|
completed_at="2026-04-20T09:01:30Z",
|
||||||
|
overall_score=82,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
metrics_timeseries={
|
||||||
|
"youtube_subscribers": [{"date": "2026-04-20", "value": 12345}]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
from common.deps import verify_api_key
|
||||||
|
from models.plan import PlanCreate, PlanResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/plans", tags=["plans"], dependencies=[Depends(verify_api_key)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", status_code=status.HTTP_201_CREATED, response_model=PlanResponse)
|
||||||
|
async def create_plan(body: PlanCreate):
|
||||||
|
return PlanResponse(
|
||||||
|
id="33333333-3333-3333-3333-333333333333",
|
||||||
|
analysis_run_id="22222222-2222-2222-2222-222222222222",
|
||||||
|
brand_guide={},
|
||||||
|
channel_strategies=[],
|
||||||
|
content_strategy={},
|
||||||
|
calendar=[],
|
||||||
|
created_at="2026-04-20T09:10:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}", response_model=PlanResponse)
|
||||||
|
async def get_plan(id: str):
|
||||||
|
return PlanResponse(
|
||||||
|
id=id,
|
||||||
|
analysis_run_id="22222222-2222-2222-2222-222222222222",
|
||||||
|
brand_guide={},
|
||||||
|
channel_strategies=[],
|
||||||
|
content_strategy={},
|
||||||
|
calendar=[],
|
||||||
|
created_at="2026-04-20T09:10:00Z",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from common.deps import verify_api_key
|
||||||
|
from models.report import ReportResponse, ClinicInfo
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/reports", tags=["reports"], dependencies=[Depends(verify_api_key)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{run_id}", response_model=ReportResponse)
|
||||||
|
async def get_report(run_id: str):
|
||||||
|
return ReportResponse(
|
||||||
|
id=run_id,
|
||||||
|
clinic=ClinicInfo(name="바노바기성형외과", url="https://www.banobagi.com"),
|
||||||
|
overall_score=82,
|
||||||
|
youtube={},
|
||||||
|
instagram={},
|
||||||
|
facebook={},
|
||||||
|
naver_place={},
|
||||||
|
naver_blog={},
|
||||||
|
gangnam_unni={},
|
||||||
|
conversion_strategy={},
|
||||||
|
roadmap=[],
|
||||||
|
kpis=[],
|
||||||
|
generated_at="2026-04-20T09:01:30Z",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import os
|
||||||
|
from fastapi import Header, HTTPException
|
||||||
|
|
||||||
|
async def verify_api_key(x_api_key: str = Header(...)):
|
||||||
|
if x_api_key != os.getenv("API_KEY"):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from api import routers
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
for router in routers:
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Channels(BaseModel):
|
||||||
|
youtube: str | None = None
|
||||||
|
instagram: list[str] | str | None = None
|
||||||
|
facebook: str | None = None
|
||||||
|
naver_blog: str | None = None
|
||||||
|
gangnam_unni: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AnalysisOptions(BaseModel):
|
||||||
|
skip_vision: bool = False
|
||||||
|
skip_perplexity: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class AnalysisCreate(BaseModel):
|
||||||
|
clinic_id: str | None = None
|
||||||
|
url: str | None = None
|
||||||
|
channels: Channels
|
||||||
|
options: AnalysisOptions = AnalysisOptions()
|
||||||
|
|
||||||
|
|
||||||
|
class AnalysisStartResponse(BaseModel):
|
||||||
|
analysis_run_id: str
|
||||||
|
clinic_id: str
|
||||||
|
status: str
|
||||||
|
estimated_seconds: int
|
||||||
|
poll_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class AnalysisStatusResponse(BaseModel):
|
||||||
|
analysis_run_id: str
|
||||||
|
status: str
|
||||||
|
progress: float
|
||||||
|
current_step: str
|
||||||
|
channel_errors: dict
|
||||||
|
completed_at: str | None
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelVerifyRequest(BaseModel):
|
||||||
|
youtube: str | None = None
|
||||||
|
instagram: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelVerifyResponse(BaseModel):
|
||||||
|
youtube: dict[str, Any] | None = None
|
||||||
|
instagram: list[dict[str, Any]] | None = None
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ClinicCreate(BaseModel):
|
||||||
|
url: str
|
||||||
|
name: str
|
||||||
|
name_en: str | None = None
|
||||||
|
address: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ClinicCreateResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
url: str
|
||||||
|
name: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
|
||||||
|
class RunSummary(BaseModel):
|
||||||
|
run_id: str
|
||||||
|
status: str
|
||||||
|
started_at: str
|
||||||
|
completed_at: str | None
|
||||||
|
overall_score: int | None
|
||||||
|
|
||||||
|
|
||||||
|
class ClinicHistoryResponse(BaseModel):
|
||||||
|
clinic_id: str
|
||||||
|
runs: list[RunSummary]
|
||||||
|
metrics_timeseries: dict
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PlanCreate(BaseModel):
|
||||||
|
report_id: str
|
||||||
|
regenerate: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class PlanResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
analysis_run_id: str
|
||||||
|
brand_guide: dict
|
||||||
|
channel_strategies: list
|
||||||
|
content_strategy: dict
|
||||||
|
calendar: list
|
||||||
|
created_at: str
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ClinicInfo(BaseModel):
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class ReportResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
clinic: ClinicInfo
|
||||||
|
overall_score: int
|
||||||
|
youtube: dict
|
||||||
|
instagram: dict
|
||||||
|
facebook: dict
|
||||||
|
naver_place: dict
|
||||||
|
naver_blog: dict
|
||||||
|
gangnam_unni: dict
|
||||||
|
conversion_strategy: dict
|
||||||
|
roadmap: list
|
||||||
|
kpis: list
|
||||||
|
generated_at: str
|
||||||
|
|
@ -3,7 +3,7 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: o2o-infinith-api
|
container_name: o2o-infinith-backend
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
# API_CONTRACT — MVP 엔드포인트 8개
|
||||||
|
|
||||||
|
**단일 진실 소스.** 프런트(`infinith-web`) ↔ 백엔드(`infinith-api`) 간 계약.
|
||||||
|
|
||||||
|
변경 절차:
|
||||||
|
1. 이 문서 수정
|
||||||
|
2. `app/schemas/*.py` + `app/routers/*.py` 구현 변경
|
||||||
|
3. `GET /openapi.json` 결과를 PR 에 diff 로 첨부
|
||||||
|
4. 프런트 `npx openapi-typescript` 로 타입 재생성
|
||||||
|
5. 양쪽 PR 크로스 리뷰 후 머지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 공통 사항
|
||||||
|
|
||||||
|
- **Base URL (dev):** `http://localhost:8000`
|
||||||
|
- **Base URL (staging/prod):** Railway 배포 후 확정
|
||||||
|
- **인증:** 모든 엔드포인트 `X-API-Key: <secret>` 헤더 필수 (MVP)
|
||||||
|
- 추후 OAuth/JWT 전환 예정 — 현재는 `.env` 의 `API_KEY` 값과 대조
|
||||||
|
- **콘텐츠 타입:** `application/json`
|
||||||
|
- **시간:** 전부 ISO 8601 UTC (예: `2026-04-20T09:00:00Z`)
|
||||||
|
- **UUID:** 모두 UUID v4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. `POST /api/clinics` — 병원 등록
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"url": "https://www.banobagi.com",
|
||||||
|
"name": "바노바기성형외과",
|
||||||
|
"name_en": "BANOBAGI",
|
||||||
|
"address": "서울시 강남구 논현로 842"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response `201 Created`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"url": "https://www.banobagi.com",
|
||||||
|
"name": "바노바기성형외과",
|
||||||
|
"created_at": "2026-04-20T09:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error
|
||||||
|
- `409` — 동일 URL 병원 이미 존재
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `POST /api/analyses` — 분석 시작 (**MVP 핵심**)
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clinic_id": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"channels": {
|
||||||
|
"youtube": "@banobagi",
|
||||||
|
"instagram": ["@banobagi_official"],
|
||||||
|
"facebook": "banobagiofficial",
|
||||||
|
"naver_blog": "https://blog.naver.com/banobagi",
|
||||||
|
"gangnam_unni": "https://www.gangnamunni.com/hospital/1234"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"skip_vision": false,
|
||||||
|
"skip_perplexity": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `clinic_id` 대신 `url` 만 제공 시, 서버가 `clinic_service.upsert_by_url()` 로 자동 생성
|
||||||
|
- 채널 필드는 **전부 선택적** — 최소 1개 이상만 제공되면 OK
|
||||||
|
- `instagram` 은 여러 계정 가능 (예: 본원 + 분원)
|
||||||
|
|
||||||
|
### Response `202 Accepted`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"analysis_run_id": "22222222-2222-2222-2222-222222222222",
|
||||||
|
"clinic_id": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"status": "discovering",
|
||||||
|
"estimated_seconds": 90,
|
||||||
|
"poll_url": "/api/analyses/22222222-2222-2222-2222-222222222222/status"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error
|
||||||
|
- `404` — `clinic_id` 존재 X
|
||||||
|
- `422` — 채널 하나도 제공 안됨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `GET /api/analyses/{run_id}/status` — 진행 상태 폴링
|
||||||
|
|
||||||
|
### Response `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"analysis_run_id": "22222222-2222-2222-2222-222222222222",
|
||||||
|
"status": "collecting",
|
||||||
|
"progress": 0.45,
|
||||||
|
"current_step": "채널 데이터 수집 중",
|
||||||
|
"channel_errors": {
|
||||||
|
"gangnam_unni": "로그인 벽으로 스크래핑 실패"
|
||||||
|
},
|
||||||
|
"completed_at": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status lifecycle
|
||||||
|
```
|
||||||
|
pending → discovering → collecting → generating → complete
|
||||||
|
↘ partial (일부 채널 실패)
|
||||||
|
↘ error (치명적 실패)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend 폴링 가이드
|
||||||
|
- `status ∈ {complete, partial, error}` 일 때까지 2초 간격 폴링
|
||||||
|
- `complete` 또는 `partial` → `GET /api/reports/{run_id}` 로 이동
|
||||||
|
- `error` → `channel_errors._pipeline` 에 원인 메시지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `GET /api/reports/{run_id}` — 리포트 조회
|
||||||
|
|
||||||
|
### Response `200 OK`
|
||||||
|
|
||||||
|
`src/types/report.ts` 의 `MarketingReport` 타입과 100% 호환 JSON. 프런트는 이 응답을 `transformReport.ts` 에 전달.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "22222222-...",
|
||||||
|
"clinic": {
|
||||||
|
"name": "바노바기성형외과",
|
||||||
|
"url": "https://www.banobagi.com"
|
||||||
|
},
|
||||||
|
"overall_score": 82,
|
||||||
|
"youtube": { ... },
|
||||||
|
"instagram": { ... },
|
||||||
|
"facebook": { ... },
|
||||||
|
"naver_place": { ... },
|
||||||
|
"naver_blog": { ... },
|
||||||
|
"gangnam_unni": { ... },
|
||||||
|
"conversion_strategy": { ... },
|
||||||
|
"roadmap": [ ... ],
|
||||||
|
"kpis": [ ... ],
|
||||||
|
"generated_at": "2026-04-20T09:01:30Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error
|
||||||
|
- `404` — run 존재 X
|
||||||
|
- `409` — 아직 `complete/partial` 아님 (`status: collecting` 등)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. `POST /api/plans` — 콘텐츠 기획 생성
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"report_id": "22222222-...",
|
||||||
|
"regenerate": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `regenerate=false` (기본) — 기존 플랜 있으면 그대로 반환
|
||||||
|
- `regenerate=true` — 강제로 새 플랜 생성
|
||||||
|
|
||||||
|
### Response `201 Created` (신규) or `200 OK` (기존)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "33333333-...",
|
||||||
|
"analysis_run_id": "22222222-...",
|
||||||
|
"brand_guide": { ... },
|
||||||
|
"channel_strategies": [ ... ],
|
||||||
|
"content_strategy": { ... },
|
||||||
|
"calendar": [ ... ],
|
||||||
|
"created_at": "2026-04-20T09:10:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. `GET /api/plans/{id}` — 기획 조회
|
||||||
|
|
||||||
|
`MarketingPlan` 타입 (src/types/plan.ts) 반환. 응답 구조는 5번과 동일.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. `GET /api/clinics/{id}/history` — 분석 이력
|
||||||
|
|
||||||
|
### Response `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clinic_id": "11111111-...",
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"run_id": "22222222-...",
|
||||||
|
"status": "complete",
|
||||||
|
"started_at": "2026-04-20T09:00:00Z",
|
||||||
|
"completed_at": "2026-04-20T09:01:30Z",
|
||||||
|
"overall_score": 82
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics_timeseries": {
|
||||||
|
"youtube_subscribers": [
|
||||||
|
{"date": "2026-04-20", "value": 12345},
|
||||||
|
{"date": "2026-05-20", "value": 13200}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. `POST /api/channels/verify` — 핸들 실시간 검증
|
||||||
|
|
||||||
|
입력 폼의 "이 계정이 맞나요?" 버튼용.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"youtube": "@banobagi",
|
||||||
|
"instagram": ["@banobagi_official"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"youtube": {
|
||||||
|
"handle": "@banobagi",
|
||||||
|
"verified": true,
|
||||||
|
"display_name": "바노바기 BANOBAGI",
|
||||||
|
"followers": 12345
|
||||||
|
},
|
||||||
|
"instagram": [
|
||||||
|
{
|
||||||
|
"handle": "@banobagi_official",
|
||||||
|
"verified": "unverifiable",
|
||||||
|
"note": "Instagram 로그인 벽"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `verified: true` — 존재 확인
|
||||||
|
- `verified: false` — 404/미존재
|
||||||
|
- `verified: "unverifiable"` — 로그인 벽 등으로 확인 불가 (UI는 경고만, 진행 허용)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (post-MVP)
|
||||||
|
|
||||||
|
- 결제 / 구독
|
||||||
|
- 경쟁사 분석 (자동 추천)
|
||||||
|
- URL 자동 발견 (홈페이지 크롤링 기반)
|
||||||
|
- 콘텐츠 스튜디오 API (이미지 생성, 캡션 생성)
|
||||||
|
- 성과 분석 / 스케줄링 자동 배포
|
||||||
|
- 팀/조직/권한 관리
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
fastapi==0.136.0
|
fastapi==0.136.0
|
||||||
uvicorn[standard]==0.44.0
|
uvicorn[standard]==0.44.0
|
||||||
pydantic==2.13.2
|
pydantic==2.13.2
|
||||||
pydantic-settings==2.13.1
|
python-dotenv==1.2.2
|
||||||
redis==7.4.0
|
redis==7.4.0
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
python-jose[cryptography]==3.5.0
|
python-jose[cryptography]==3.5.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue