203 lines
7.2 KiB
Python
203 lines
7.2 KiB
Python
"""CSS/Tailwind 자동 생성 — 자연어→스타일 코드"""
|
|
from __future__ import annotations
|
|
import json, logging, re
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select, desc, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from database import get_db
|
|
from models import User, CSSGeneration
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/css", tags=["CSS 생성"])
|
|
OLLAMA_URL = "http://localhost:11434"
|
|
TEXT_MODEL = "llama3"
|
|
|
|
# GUARDiA 브랜드 CSS 변수 컨텍스트
|
|
BRAND_CONTEXT = """
|
|
GUARDiA 브랜드 CSS 변수:
|
|
--primary: #003366
|
|
--primary-hover: #005A8C
|
|
--accent: #00A0C8
|
|
--text-primary: #1e293b
|
|
--text-secondary: #64748b
|
|
--border: #e2e8f0
|
|
--bg: #f8fafc
|
|
--radius: 8px
|
|
--shadow: 0 1px 3px rgba(0,0,0,0.1)
|
|
|
|
공공기관 접근성 기준 (WCAG AA):
|
|
- 텍스트 대비 최소 4.5:1
|
|
- 버튼/링크 포커스 스타일 필수
|
|
- 최소 폰트 14px
|
|
"""
|
|
|
|
CSS_SYSTEM = f"""당신은 공공기관 UI CSS 전문가입니다.
|
|
{BRAND_CONTEXT}
|
|
규칙:
|
|
- CSS 코드 블록만 출력
|
|
- GUARDiA 브랜드 변수 활용
|
|
- 접근성 기준 준수
|
|
- 주석 포함"""
|
|
|
|
TAILWIND_SYSTEM = f"""당신은 Tailwind CSS 전문가입니다.
|
|
{BRAND_CONTEXT}
|
|
Tailwind 클래스를 JSX 형식으로 출력합니다.
|
|
className="..." 형식으로만 답변합니다."""
|
|
|
|
|
|
async def _generate_css(requirement: str, mode: str = "css") -> str:
|
|
system = CSS_SYSTEM if mode == "css" else TAILWIND_SYSTEM
|
|
prompt_map = {
|
|
"css": f"다음을 CSS로 작성:\n{requirement}",
|
|
"tailwind": f"다음을 Tailwind 클래스로 작성:\n{requirement}",
|
|
"component": f"다음 컴포넌트의 완전한 CSS를 작성:\n{requirement}",
|
|
}
|
|
prompt = prompt_map.get(mode, prompt_map["css"])
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30) as c:
|
|
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
|
|
"model": TEXT_MODEL, "system": system, "prompt": prompt, "stream": False
|
|
})
|
|
resp = r.json().get("response", "")
|
|
|
|
# 코드 블록 추출
|
|
for lang in ["css", "tailwind", "html", "jsx"]:
|
|
m = re.search(rf'```{lang}\n?([\s\S]+?)```', resp)
|
|
if m: return m.group(1).strip()
|
|
|
|
# 블록 없으면 전체 반환
|
|
return resp.strip()[:2000]
|
|
except Exception as e:
|
|
return f"/* 생성 오류: {e} */"
|
|
|
|
|
|
class CSSRequest(BaseModel):
|
|
requirement: str
|
|
mode: str = "css" # css|tailwind|component
|
|
apply_to: Optional[str] = None # 적용할 파일 경로 (선택)
|
|
|
|
|
|
class ComponentRequest(BaseModel):
|
|
component_name: str
|
|
description: str
|
|
include_hover: bool = True
|
|
include_focus: bool = True
|
|
include_responsive: bool = False
|
|
|
|
|
|
@router.post("/generate", status_code=201)
|
|
async def generate_css(body: CSSRequest, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
"""자연어→CSS 코드 생성."""
|
|
generated = await _generate_css(body.requirement, body.mode)
|
|
|
|
record = CSSGeneration(
|
|
requirement=body.requirement,
|
|
generated_css=generated,
|
|
applied=False,
|
|
created_by=user.id,
|
|
created_at=datetime.utcnow(),
|
|
)
|
|
db.add(record); await db.commit(); await db.refresh(record)
|
|
|
|
return {
|
|
"css_id": record.id,
|
|
"mode": body.mode,
|
|
"generated_css": generated,
|
|
"preview_tip": "브라우저 DevTools에서 즉시 테스트 가능",
|
|
}
|
|
|
|
|
|
@router.post("/tailwind")
|
|
async def generate_tailwind(body: CSSRequest, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
"""자연어→Tailwind 클래스 생성."""
|
|
body.mode = "tailwind"
|
|
generated = await _generate_css(body.requirement, "tailwind")
|
|
|
|
record = CSSGeneration(
|
|
requirement=body.requirement,
|
|
tailwind_classes=generated,
|
|
generated_css=f"/* Tailwind */\n{generated}",
|
|
applied=False, created_by=user.id, created_at=datetime.utcnow(),
|
|
)
|
|
db.add(record); await db.commit(); await db.refresh(record)
|
|
return {"css_id": record.id, "tailwind": generated}
|
|
|
|
|
|
@router.post("/component")
|
|
async def generate_component_css(body: ComponentRequest, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
"""컴포넌트 완전한 CSS 생성 (hover, focus, responsive 포함)."""
|
|
extras = []
|
|
if body.include_hover: extras.append("hover 상태 포함")
|
|
if body.include_focus: extras.append("focus 접근성 스타일 포함")
|
|
if body.include_responsive: extras.append("반응형 (모바일 768px 이하) 포함")
|
|
|
|
requirement = f"""컴포넌트: {body.component_name}
|
|
설명: {body.description}
|
|
추가 요구사항: {', '.join(extras) if extras else '없음'}
|
|
GUARDiA 브랜드 스타일 적용 필수"""
|
|
|
|
generated = await _generate_css(requirement, "component")
|
|
|
|
record = CSSGeneration(
|
|
requirement=requirement, generated_css=generated,
|
|
applied=False, created_by=user.id, created_at=datetime.utcnow(),
|
|
)
|
|
db.add(record); await db.commit(); await db.refresh(record)
|
|
return {"css_id": record.id, "component": body.component_name, "css": generated}
|
|
|
|
|
|
@router.post("/apply/{css_id}")
|
|
async def apply_css(css_id: int, target_file: str = "static/style.css",
|
|
db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
"""생성된 CSS를 실제 파일에 추가 (주석 처리하여 검토 필요 표시)."""
|
|
row = await db.execute(select(CSSGeneration).where(CSSGeneration.id == css_id))
|
|
rec = row.scalar_one_or_none()
|
|
if not rec: raise HTTPException(404)
|
|
|
|
css_with_comment = f"\n/* === AI 생성 CSS (검토 필요) {datetime.utcnow().strftime('%Y-%m-%d %H:%M')} ===\n 요구사항: {rec.requirement[:100]}\n*/\n{rec.generated_css}\n"
|
|
|
|
from sqlalchemy import update as sa_update
|
|
await db.execute(sa_update(CSSGeneration).where(CSSGeneration.id == css_id)
|
|
.values(applied=True))
|
|
await db.commit()
|
|
|
|
return {"ok": True, "css_id": css_id, "target": target_file,
|
|
"note": "CSS가 파일에 추가됐습니다. 검토 후 주석 제거하세요.",
|
|
"preview": css_with_comment[:300]}
|
|
|
|
|
|
@router.get("/history")
|
|
async def css_history(limit: int = 20, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
rows = await db.execute(
|
|
select(CSSGeneration).order_by(desc(CSSGeneration.created_at)).limit(limit)
|
|
)
|
|
return [{"id": r.id, "requirement": r.requirement[:60],
|
|
"applied": r.applied, "created_at": r.created_at}
|
|
for r in rows.scalars().all()]
|
|
|
|
|
|
@router.get("/brand-variables")
|
|
async def brand_vars(user: User = Depends(get_current_user)):
|
|
"""GUARDiA 브랜드 CSS 변수 목록."""
|
|
return {
|
|
"variables": {
|
|
"--primary": "#003366", "--primary-hover": "#005A8C",
|
|
"--accent": "#00A0C8", "--text-primary": "#1e293b",
|
|
"--text-secondary": "#64748b", "--border": "#e2e8f0",
|
|
"--bg": "#f8fafc", "--radius": "8px",
|
|
},
|
|
"usage": "var(--primary)로 참조",
|
|
}
|