"""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)로 참조", }