feat(music-lab): 파이프라인 하드 삭제 엔드포인트 DELETE /api/music/pipeline/{id}
cancel(state→cancelled, active/failed 뷰에서만 제거)만으론 status=all 뷰에
행이 남아 옛 dead 파이프라인을 완전히 치울 수 없었음. DELETE로 하드 삭제 추가.
- db.delete_pipeline(pid)→bool: 자식행(pipeline_feedback, pipeline_jobs) 먼저
삭제 후 video_pipelines 삭제(SQLite FK 미강제라 명시적 cascade). 존재 여부 bool.
- DELETE /api/music/pipeline/{id}: 없으면 404, 있으면 {"ok":true,"deleted":id}.
상태 가드 없음(관리자 정리 용도, cancel과 동일한 단순 정책).
- 테스트 3종(삭제+404+자식행 cascade) TDD Red→Green. music-lab 152 passed.
- CLAUDE.md 엔드포인트 카탈로그 갱신.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
This commit is contained in:
@@ -268,6 +268,7 @@ docker compose up -d
|
|||||||
| POST/GET | `/api/music/compile` (+ `/compiles/{id}/export`) | 컴파일 |
|
| POST/GET | `/api/music/compile` (+ `/compiles/{id}/export`) | 컴파일 |
|
||||||
| POST/GET/DELETE | `/api/music/video-project` (+ `/{id}/render`, `/export`) | 영상 프로젝트 |
|
| POST/GET/DELETE | `/api/music/video-project` (+ `/{id}/render`, `/export`) | 영상 프로젝트 |
|
||||||
| ALL | `/api/music/pipeline` (생성/start/feedback/cancel/publish/retry/telegram-msg/lookup) | YouTube 자동화 파이프라인. `POST /{id}/retry`=실패 step 재개(publish+업로드완료 시 409) |
|
| ALL | `/api/music/pipeline` (생성/start/feedback/cancel/publish/retry/telegram-msg/lookup) | YouTube 자동화 파이프라인. `POST /{id}/retry`=실패 step 재개(publish+업로드완료 시 409) |
|
||||||
|
| DELETE | `/api/music/pipeline/{id}` | 파이프라인 행 하드 삭제(자식 jobs/feedback 포함, 전체 목록에서 제거). 없으면 404 |
|
||||||
| GET/PUT | `/api/music/setup` | 파이프라인 설정 |
|
| GET/PUT | `/api/music/setup` | 파이프라인 설정 |
|
||||||
| GET | `/api/music/youtube/auth-url`, `/callback`, `/status`; POST `/disconnect` | YouTube OAuth |
|
| GET | `/api/music/youtube/auth-url`, `/callback`, `/status`; POST `/disconnect` | YouTube OAuth |
|
||||||
| GET/POST/PUT/DELETE | `/api/music/revenue` (+ `/dashboard`) | 수익 기록 |
|
| GET/POST/PUT/DELETE | `/api/music/revenue` (+ `/dashboard`) | 수익 기록 |
|
||||||
|
|||||||
@@ -1100,6 +1100,19 @@ def get_pipeline(pid: int) -> Optional[Dict[str, Any]]:
|
|||||||
return _parse_pipeline_row(row)
|
return _parse_pipeline_row(row)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_pipeline(pid: int) -> bool:
|
||||||
|
"""파이프라인과 자식행(pipeline_feedback, pipeline_jobs)을 하드 삭제.
|
||||||
|
|
||||||
|
SQLite FK를 강제하지 않으므로 자식행을 명시적으로 먼저 삭제한다.
|
||||||
|
파이프라인이 존재했으면 True, 없었으면 False.
|
||||||
|
"""
|
||||||
|
with _conn() as conn:
|
||||||
|
conn.execute("DELETE FROM pipeline_feedback WHERE pipeline_id = ?", (pid,))
|
||||||
|
conn.execute("DELETE FROM pipeline_jobs WHERE pipeline_id = ?", (pid,))
|
||||||
|
cur = conn.execute("DELETE FROM video_pipelines WHERE id = ?", (pid,))
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
def update_pipeline_state(pid: int, state: str, **fields) -> None:
|
def update_pipeline_state(pid: int, state: str, **fields) -> None:
|
||||||
"""파이프라인 state를 갱신하고 옵션 컬럼을 함께 업데이트한다.
|
"""파이프라인 state를 갱신하고 옵션 컬럼을 함께 업데이트한다.
|
||||||
|
|
||||||
|
|||||||
@@ -1133,6 +1133,14 @@ def cancel_pipeline(pid: int):
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/music/pipeline/{pid}")
|
||||||
|
def delete_pipeline_endpoint(pid: int):
|
||||||
|
"""파이프라인 행을 하드 삭제(전체 목록에서 완전 제거). 없으면 404."""
|
||||||
|
if not _db_module.delete_pipeline(pid):
|
||||||
|
raise HTTPException(404)
|
||||||
|
return {"ok": True, "deleted": pid}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/music/pipeline/{pid}/retry", status_code=202)
|
@app.post("/api/music/pipeline/{pid}/retry", status_code=202)
|
||||||
async def retry_pipeline(pid: int, bg: BackgroundTasks):
|
async def retry_pipeline(pid: int, bg: BackgroundTasks):
|
||||||
from .pipeline.state_machine import STEPS
|
from .pipeline.state_machine import STEPS
|
||||||
|
|||||||
@@ -105,6 +105,29 @@ def test_cancel_pipeline(client):
|
|||||||
assert db.get_pipeline(pid)["state"] == "cancelled"
|
assert db.get_pipeline(pid)["state"] == "cancelled"
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_pipeline_removes_from_db(client):
|
||||||
|
pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"]
|
||||||
|
r = client.request("DELETE", f"/api/music/pipeline/{pid}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["ok"] is True
|
||||||
|
assert db.get_pipeline(pid) is None
|
||||||
|
all_ids = [p["id"] for p in client.get("/api/music/pipeline?status=all").json()["pipelines"]]
|
||||||
|
assert pid not in all_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_pipeline_not_found_returns_404(client):
|
||||||
|
r = client.request("DELETE", "/api/music/pipeline/99999")
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_pipeline_removes_child_jobs(client):
|
||||||
|
pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"]
|
||||||
|
db.create_pipeline_job(pid, "cover")
|
||||||
|
assert len(db.list_pipeline_jobs(pid)) == 1
|
||||||
|
client.request("DELETE", f"/api/music/pipeline/{pid}")
|
||||||
|
assert db.list_pipeline_jobs(pid) == []
|
||||||
|
|
||||||
|
|
||||||
def test_setup_get_returns_defaults(client):
|
def test_setup_get_returns_defaults(client):
|
||||||
r = client.get("/api/music/setup")
|
r = client.get("/api/music/setup")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|||||||
Reference in New Issue
Block a user