결정(9/21): 공용 하나, 개인 로컬 없음.
- backend apps/snippets: Snippet(name PK·desc·body·category) + SnippetUsage(사람별 count/last_used).
GET/POST /api/v1/snippets, PUT/DELETE /snippets/{name}, POST /snippets/{name}/use — 옛 브릿지 5개와 1:1,
이름 정규화·중복 409·빈 내용 400 규칙 동일. import_snippets 커맨드로 옛 snippets.db 적재. 테스트 3개
- frontend snippets.api.ts 만 HTTP 로 교체(훅·페이지·타입 무변경). 테스트 axios-mock-adapter 로
- ponytail: Tauri 로컬 SQLite 커맨드는 미사용 상태로 남김. 지울 때 Rust+브릿지 테스트 함께
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
78 lines
3.2 KiB
Python
78 lines
3.2 KiB
Python
"""스니펫 API — 프론트 `features/snippets/api/snippets.api.ts` 5개 호출과 1:1. 옛 Tauri 브릿지 규칙 그대로.
|
|
|
|
GET /api/v1/snippets → Snippet[] (내 usage 합쳐서)
|
|
POST /api/v1/snippets {name,desc,body,category} → Snippet (이름 중복이면 409)
|
|
PUT /api/v1/snippets/{name} {desc,body,category} → Snippet
|
|
DELETE /api/v1/snippets/{name} → {name}
|
|
POST /api/v1/snippets/{name}/use → {name, usageCount, lastUsed}
|
|
"""
|
|
|
|
from django.db import IntegrityError, transaction
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils import timezone
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from common.envelope import CodedError
|
|
|
|
from .models import Snippet, SnippetUsage
|
|
|
|
|
|
def normalize_name(name: str) -> str:
|
|
return (name or "").strip().upper().replace(" ", "_")
|
|
|
|
|
|
def _validate(body: dict, *, need_name: bool) -> dict:
|
|
name = normalize_name(body.get("name", ""))
|
|
if need_name and not name:
|
|
raise CodedError(400, "VALIDATION_ERROR", "이름을 입력해야 함")
|
|
if not (body.get("body") or "").strip():
|
|
raise CodedError(400, "VALIDATION_ERROR", "내용을 입력해야 함")
|
|
return {
|
|
"name": name,
|
|
"desc": body.get("desc") or "",
|
|
"body": body["body"],
|
|
"category": (body.get("category") or "").strip() or "코드",
|
|
}
|
|
|
|
|
|
class SnippetListView(APIView):
|
|
def get(self, request):
|
|
usage = {u.snippet_id: u for u in SnippetUsage.objects.filter(user=request.user)}
|
|
return Response([s.as_dto(usage.get(s.name)) for s in Snippet.objects.all()])
|
|
|
|
def post(self, request):
|
|
d = _validate(request.data, need_name=True)
|
|
try:
|
|
with transaction.atomic(): # 중복 INSERT 실패가 바깥 트랜잭션을 깨지 않게(savepoint)
|
|
s = Snippet.objects.create(**d, created_by=request.user)
|
|
except IntegrityError:
|
|
raise CodedError(409, "CONFLICT", f"이미 있는 이름임: {d['name']}")
|
|
return Response(s.as_dto(), status=201)
|
|
|
|
|
|
class SnippetDetailView(APIView):
|
|
def put(self, request, name: str):
|
|
s = get_object_or_404(Snippet, name=normalize_name(name))
|
|
d = _validate({**request.data, "name": s.name}, need_name=False)
|
|
s.desc, s.body, s.category = d["desc"], d["body"], d["category"]
|
|
s.save(update_fields=["desc", "body", "category", "updated_at"])
|
|
usage = SnippetUsage.objects.filter(snippet=s, user=request.user).first()
|
|
return Response(s.as_dto(usage))
|
|
|
|
def delete(self, request, name: str):
|
|
s = get_object_or_404(Snippet, name=normalize_name(name))
|
|
deleted = s.name # delete() 가 pk 를 None 으로 지움
|
|
s.delete()
|
|
return Response({"name": deleted})
|
|
|
|
|
|
class SnippetUseView(APIView):
|
|
def post(self, request, name: str):
|
|
s = get_object_or_404(Snippet, name=normalize_name(name))
|
|
u, _ = SnippetUsage.objects.get_or_create(snippet=s, user=request.user)
|
|
u.count += 1
|
|
u.last_used = timezone.now()
|
|
u.save(update_fields=["count", "last_used"])
|
|
return Response({"name": s.name, "usageCount": u.count, "lastUsed": int(u.last_used.timestamp())})
|