feat(snippets): 이름 규칙(영문 대문자·숫자·_, 3~50자)·실시간 중복/저장될 이름 표시, 설명 필수, 검색에 본문 ★머리줄·주석 포함(한글 검색), 메타 일괄 반영 명령 apply_snippet_meta
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
aa7481ffe9
commit
7a1498a791
@@ -7,6 +7,8 @@
|
||||
POST /api/v1/snippets/{name}/use → {name, usageCount, lastUsed}
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
@@ -18,19 +20,48 @@ from common.envelope import CodedError
|
||||
from .models import Snippet, SnippetUsage
|
||||
|
||||
|
||||
# 이름 규칙 — 프론트 features/snippets/core/name.ts 와 같음(바꾸면 양쪽 같이).
|
||||
# 영문 대문자·숫자·_, 문자로 시작, 3~50자, _ 연속·앞뒤 금지. 권장 "영역_대상_동작". 한글은 설명에.
|
||||
NAME_RE = re.compile(r"^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$")
|
||||
NAME_MIN, NAME_MAX = 3, 50
|
||||
|
||||
|
||||
def normalize_name(name: str) -> str:
|
||||
"""입력 → 저장될 이름. 대문자, 공백·하이픈·점→_, 그 외 기호·한글 버림, _ 정리."""
|
||||
s = re.sub(r"[\s\-.]+", "_", (name or "").upper())
|
||||
s = re.sub(r"[^A-Z0-9_]", "", s)
|
||||
return re.sub(r"_+", "_", s).strip("_")
|
||||
|
||||
|
||||
def lookup_name(name: str) -> str:
|
||||
"""URL 의 이름 → DB 키. 옛 규칙 이름(IF_SUBRC-ABAP 등)이 남아 있으면 그대로 찾아야 해서 정규화 안 함(대문자·공백만)."""
|
||||
return (name or "").strip().upper().replace(" ", "_")
|
||||
|
||||
|
||||
def name_problem(raw: str) -> str | None:
|
||||
name = normalize_name(raw)
|
||||
if not name:
|
||||
return "이름은 영문으로 — 한글은 설명에 적어" if re.search(r"[가-힣]", raw or "") else "이름을 입력해야 함"
|
||||
if not name[0].isalpha():
|
||||
return "이름은 영문자로 시작해야 함"
|
||||
if not NAME_MIN <= len(name) <= NAME_MAX:
|
||||
return f"이름은 {NAME_MIN}~{NAME_MAX}자"
|
||||
if not NAME_RE.match(name):
|
||||
return "영문 대문자·숫자·_ 만 쓸 수 있음"
|
||||
return None
|
||||
|
||||
|
||||
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 need_name and (problem := name_problem(body.get("name", ""))):
|
||||
raise CodedError(400, "VALIDATION_ERROR", problem)
|
||||
if not (body.get("desc") or "").strip():
|
||||
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 "",
|
||||
"desc": body["desc"].strip(),
|
||||
"body": body["body"],
|
||||
"category": (body.get("category") or "").strip() or "코드",
|
||||
}
|
||||
@@ -53,7 +84,7 @@ class SnippetListView(APIView):
|
||||
|
||||
class SnippetDetailView(APIView):
|
||||
def put(self, request, name: str):
|
||||
s = get_object_or_404(Snippet, name=normalize_name(name))
|
||||
s = get_object_or_404(Snippet, name=lookup_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"])
|
||||
@@ -61,7 +92,7 @@ class SnippetDetailView(APIView):
|
||||
return Response(s.as_dto(usage))
|
||||
|
||||
def delete(self, request, name: str):
|
||||
s = get_object_or_404(Snippet, name=normalize_name(name))
|
||||
s = get_object_or_404(Snippet, name=lookup_name(name))
|
||||
deleted = s.name # delete() 가 pk 를 None 으로 지움
|
||||
s.delete()
|
||||
return Response({"name": deleted})
|
||||
@@ -69,7 +100,7 @@ class SnippetDetailView(APIView):
|
||||
|
||||
class SnippetUseView(APIView):
|
||||
def post(self, request, name: str):
|
||||
s = get_object_or_404(Snippet, name=normalize_name(name))
|
||||
s = get_object_or_404(Snippet, name=lookup_name(name))
|
||||
u, _ = SnippetUsage.objects.get_or_create(snippet=s, user=request.user)
|
||||
u.count += 1
|
||||
u.last_used = timezone.now()
|
||||
|
||||
Reference in New Issue
Block a user