sync: update from workspace (latest ITSM/CICD/DR changes)
This commit is contained in:
parent
7d092126eb
commit
031882732e
7
main.py
7
main.py
@ -424,6 +424,13 @@ app.include_router(skill_miner.router) # 자동 스킬 획득
|
|||||||
app.include_router(finetune_pipeline.router) # LoRA 파인튜닝 파이프라인
|
app.include_router(finetune_pipeline.router) # LoRA 파인튜닝 파이프라인
|
||||||
app.include_router(ai_dashboard.router) # AI 뇌 엔진 대시보드
|
app.include_router(ai_dashboard.router) # AI 뇌 엔진 대시보드
|
||||||
|
|
||||||
|
# ── 디자인 AI + 스마트 UX ──────────────────────────────────────────────────
|
||||||
|
from routers import design_analyzer, icon_generator, css_generator, smart_ux
|
||||||
|
app.include_router(design_analyzer.router) # 디자인 SR AI 자동화 (Ollama llava)
|
||||||
|
app.include_router(icon_generator.router) # SVG 아이콘 생성
|
||||||
|
app.include_router(css_generator.router) # 자연어→CSS/Tailwind
|
||||||
|
app.include_router(smart_ux.router) # 다음명령 제시 + 음성처리
|
||||||
|
|
||||||
|
|
||||||
# ── 개방망 보안 헤더 미들웨어 ────────────────────────────────────────────────
|
# ── 개방망 보안 헤더 미들웨어 ────────────────────────────────────────────────
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
|
|||||||
57
models.py
57
models.py
@ -6079,3 +6079,60 @@ class PluginRecord(Base):
|
|||||||
usage_count = Column(Integer, default=0)
|
usage_count = Column(Integer, default=0)
|
||||||
installed_at = Column(DateTime, default=func.now())
|
installed_at = Column(DateTime, default=func.now())
|
||||||
last_updated = Column(DateTime, default=func.now())
|
last_updated = Column(DateTime, default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# ── 디자인 AI + 스마트 UX (2026-06-03 크롤링 기반)
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class DesignSR(Base):
|
||||||
|
"""디자인 SR AI 자동 처리 이력."""
|
||||||
|
__tablename__ = "tb_design_sr"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sr_id = Column(Integer, nullable=True)
|
||||||
|
design_type = Column(String(30), nullable=False) # CSS_CHANGE|ICON_REQUEST|UI_REVIEW|LAYOUT
|
||||||
|
requirement = Column(Text, nullable=False)
|
||||||
|
screenshot_path = Column(String(500), nullable=True)
|
||||||
|
ai_analysis = Column(Text, nullable=True)
|
||||||
|
generated_code = Column(Text, nullable=True)
|
||||||
|
status = Column(String(20), default="PENDING")
|
||||||
|
resolved_by_ai = Column(Boolean, default=False)
|
||||||
|
created_by = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratedIcon(Base):
|
||||||
|
"""AI 생성 SVG 아이콘."""
|
||||||
|
__tablename__ = "tb_generated_icon"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
svg_code = Column(Text, nullable=False)
|
||||||
|
category = Column(String(50), default="custom")
|
||||||
|
primary_color = Column(String(10), default="#003366")
|
||||||
|
size = Column(Integer, default=24)
|
||||||
|
created_by = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class CSSGeneration(Base):
|
||||||
|
"""CSS 자동 생성 이력."""
|
||||||
|
__tablename__ = "tb_css_generation"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
requirement = Column(Text, nullable=False)
|
||||||
|
generated_css = Column(Text, nullable=True)
|
||||||
|
tailwind_classes = Column(Text, nullable=True)
|
||||||
|
applied = Column(Boolean, default=False)
|
||||||
|
created_by = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHistory(Base):
|
||||||
|
"""음성/명령 사용 이력 (다음명령 학습용)."""
|
||||||
|
__tablename__ = "tb_command_history"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("tb_user.id"), nullable=False, index=True)
|
||||||
|
command = Column(String(200), nullable=False)
|
||||||
|
room = Column(String(100), default="general")
|
||||||
|
success = Column(Boolean, default=True)
|
||||||
|
used_at = Column(DateTime, default=func.now())
|
||||||
|
|||||||
202
routers/css_generator.py
Normal file
202
routers/css_generator.py
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
"""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)로 참조",
|
||||||
|
}
|
||||||
279
routers/design_analyzer.py
Normal file
279
routers/design_analyzer.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
"""
|
||||||
|
GUARDiA 디자인 AI — Ollama llava 비전으로 스크린샷 분석 + 디자인 SR 자동 처리
|
||||||
|
|
||||||
|
엔드포인트:
|
||||||
|
POST /api/design/analyze — 스크린샷 Ollama llava 분석
|
||||||
|
POST /api/design/review — 디자인 리뷰 보고서 생성
|
||||||
|
POST /api/design/auto-resolve — 디자인 SR 자동 분류·처리
|
||||||
|
GET /api/design/sr-queue — 디자인 SR 목록
|
||||||
|
GET /api/design/stats — 디자인 SR 통계
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import base64, json, logging, re
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
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, DesignSR
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter(prefix="/api/design", tags=["디자인 AI"])
|
||||||
|
|
||||||
|
OLLAMA_URL = "http://localhost:11434"
|
||||||
|
VISION_MODEL = "llava:7b"
|
||||||
|
TEXT_MODEL = "llama3"
|
||||||
|
UPLOAD_DIR = Path("/opt/guardia/app/uploads/icons")
|
||||||
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 디자인 SR 키워드 분류
|
||||||
|
DESIGN_KEYWORDS = {
|
||||||
|
"CSS_CHANGE": ["색상", "color", "배경", "background", "버튼", "button",
|
||||||
|
"폰트", "font", "글꼴", "크기", "size", "여백", "margin", "패딩"],
|
||||||
|
"ICON_REQUEST": ["아이콘", "icon", "이미지", "image", "그림", "픽토그램",
|
||||||
|
"svg", "로고", "logo", "심볼"],
|
||||||
|
"LAYOUT_CHANGE": ["레이아웃", "layout", "정렬", "align", "위치", "position",
|
||||||
|
"배치", "그리드", "grid", "열", "column"],
|
||||||
|
"UI_REVIEW": ["ui", "ux", "전체", "개편", "리뷰", "review", "개선",
|
||||||
|
"사용성", "스크린샷", "screenshot"],
|
||||||
|
"TYPOGRAPHY": ["폰트", "font", "텍스트", "text", "글자", "글씨", "크기"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_design_sr(text: str) -> str:
|
||||||
|
text_lower = text.lower()
|
||||||
|
for dtype, keywords in DESIGN_KEYWORDS.items():
|
||||||
|
if any(kw in text_lower for kw in keywords):
|
||||||
|
return dtype
|
||||||
|
return "GENERAL"
|
||||||
|
|
||||||
|
|
||||||
|
async def _analyze_with_vision(image_bytes: bytes, prompt: str) -> str:
|
||||||
|
"""Ollama llava로 이미지 분석."""
|
||||||
|
img_b64 = base64.b64encode(image_bytes).decode()
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=60) as c:
|
||||||
|
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
|
||||||
|
"model": VISION_MODEL,
|
||||||
|
"prompt": prompt,
|
||||||
|
"images": [img_b64],
|
||||||
|
"stream": False,
|
||||||
|
})
|
||||||
|
return r.json().get("response", "분석 실패")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Vision 분석 실패: {e}")
|
||||||
|
return f"비전 분석 오류: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
async def _text_generate(prompt: str, system: str = "") -> str:
|
||||||
|
"""Ollama 텍스트 생성."""
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
return r.json().get("response", "")
|
||||||
|
except Exception as e:
|
||||||
|
return f"생성 오류: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewRequest(BaseModel):
|
||||||
|
requirement: str
|
||||||
|
sr_id: Optional[int] = None
|
||||||
|
design_type: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AutoResolveIn(BaseModel):
|
||||||
|
sr_id: Optional[int] = None
|
||||||
|
requirement: str
|
||||||
|
priority: str = "MEDIUM"
|
||||||
|
|
||||||
|
|
||||||
|
# ── 엔드포인트 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.post("/analyze")
|
||||||
|
async def analyze_screenshot(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
question: str = Form("이 UI의 개선점을 한국어로 3가지 제안해줘"),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""스크린샷 업로드 → Ollama llava 분석 → 개선 제안."""
|
||||||
|
if not file.content_type.startswith("image/"):
|
||||||
|
raise HTTPException(400, "이미지 파일만 업로드 가능합니다")
|
||||||
|
|
||||||
|
img_bytes = await file.read()
|
||||||
|
if len(img_bytes) > 10 * 1024 * 1024:
|
||||||
|
raise HTTPException(413, "10MB 이하 이미지만 지원합니다")
|
||||||
|
|
||||||
|
prompt = f"""당신은 공공기관 UI/UX 전문가입니다.
|
||||||
|
이 스크린샷을 분석하고 다음 질문에 답하세요: {question}
|
||||||
|
|
||||||
|
분석 기준:
|
||||||
|
1. 가독성 (폰트 크기, 대비, 간격)
|
||||||
|
2. 일관성 (색상, 컴포넌트 스타일)
|
||||||
|
3. 접근성 (KWCAG 2.1 준수)
|
||||||
|
4. 공공기관 표준 UI 가이드 적합성
|
||||||
|
|
||||||
|
구체적인 CSS 수정안 또는 디자인 가이드를 포함해주세요."""
|
||||||
|
|
||||||
|
analysis = await _analyze_with_vision(img_bytes, prompt)
|
||||||
|
|
||||||
|
# DB 저장
|
||||||
|
design_sr = DesignSR(
|
||||||
|
design_type="UI_REVIEW",
|
||||||
|
requirement=question,
|
||||||
|
ai_analysis=analysis,
|
||||||
|
status="ANALYZED",
|
||||||
|
resolved_by_ai=False,
|
||||||
|
created_by=user.id,
|
||||||
|
created_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
db.add(design_sr)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(design_sr)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"analysis_id": design_sr.id,
|
||||||
|
"analysis": analysis,
|
||||||
|
"model": VISION_MODEL,
|
||||||
|
"tip": "분석 결과를 SR에 첨부하거나 /api/design/auto-resolve로 자동 처리하세요",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/review")
|
||||||
|
async def design_review(body: ReviewRequest, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""텍스트 요구사항 기반 디자인 리뷰 보고서."""
|
||||||
|
design_type = body.design_type or _classify_design_sr(body.requirement)
|
||||||
|
|
||||||
|
system = """당신은 공공기관 UI/UX 디자인 전문가입니다.
|
||||||
|
디자인 요구사항을 분석하고 구체적인 구현 방안을 제시합니다.
|
||||||
|
CSS 코드, SVG, 또는 Tailwind 클래스를 포함한 실용적인 답변을 제공합니다."""
|
||||||
|
|
||||||
|
prompt = f"""디자인 요구사항: {body.requirement}
|
||||||
|
분류: {design_type}
|
||||||
|
|
||||||
|
다음 형식으로 답변해주세요:
|
||||||
|
1. 분석: (요구사항 해석)
|
||||||
|
2. 구현 방안: (CSS/SVG 코드 포함)
|
||||||
|
3. 적용 방법: (단계별 가이드)
|
||||||
|
4. 주의사항: (공공기관 접근성 기준)"""
|
||||||
|
|
||||||
|
analysis = await _text_generate(prompt, system)
|
||||||
|
|
||||||
|
design_sr = DesignSR(
|
||||||
|
sr_id=body.sr_id,
|
||||||
|
design_type=design_type,
|
||||||
|
requirement=body.requirement,
|
||||||
|
ai_analysis=analysis,
|
||||||
|
status="REVIEWED",
|
||||||
|
resolved_by_ai=False,
|
||||||
|
created_by=user.id,
|
||||||
|
created_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
db.add(design_sr)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(design_sr)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"review_id": design_sr.id,
|
||||||
|
"design_type": design_type,
|
||||||
|
"analysis": analysis,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/auto-resolve")
|
||||||
|
async def auto_resolve_sr(body: AutoResolveIn, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""디자인 SR 자동 분류 + AI 처리."""
|
||||||
|
design_type = _classify_design_sr(body.requirement)
|
||||||
|
|
||||||
|
# 유형별 특화 처리
|
||||||
|
if design_type == "CSS_CHANGE":
|
||||||
|
system = "CSS 전문가. 요구사항을 정확한 CSS 코드로 변환. 코드 블록만 출력."
|
||||||
|
prompt = f"다음 디자인 변경을 CSS로 작성 (GUARDiA 변수: --primary: #003366):\n{body.requirement}"
|
||||||
|
elif design_type == "ICON_REQUEST":
|
||||||
|
system = "SVG 아이콘 전문가. 24x24 픽토그램 SVG 코드만 출력."
|
||||||
|
prompt = f"다음 아이콘을 SVG로 생성 (viewBox='0 0 24 24', fill='#003366'):\n{body.requirement}"
|
||||||
|
elif design_type == "TYPOGRAPHY":
|
||||||
|
system = "CSS 타이포그래피 전문가. 폰트/텍스트 CSS만 출력."
|
||||||
|
prompt = f"다음 타이포그래피 요구사항을 CSS로:\n{body.requirement}"
|
||||||
|
else:
|
||||||
|
system = "UI/UX 디자인 전문가. 구체적 개선안 제시."
|
||||||
|
prompt = f"다음 디자인 요구사항 처리 방안:\n{body.requirement}"
|
||||||
|
|
||||||
|
generated = await _text_generate(prompt, system)
|
||||||
|
|
||||||
|
# 코드 블록 추출
|
||||||
|
code_match = re.search(r'```(?:css|svg|html)?\n?([\s\S]+?)```', generated)
|
||||||
|
generated_code = code_match.group(1).strip() if code_match else generated[:500]
|
||||||
|
|
||||||
|
design_sr = DesignSR(
|
||||||
|
sr_id=body.sr_id,
|
||||||
|
design_type=design_type,
|
||||||
|
requirement=body.requirement,
|
||||||
|
ai_analysis=generated,
|
||||||
|
generated_code=generated_code,
|
||||||
|
status="AI_RESOLVED",
|
||||||
|
resolved_by_ai=True,
|
||||||
|
created_by=user.id,
|
||||||
|
created_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
db.add(design_sr)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(design_sr)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"design_sr_id": design_sr.id,
|
||||||
|
"design_type": design_type,
|
||||||
|
"generated_code": generated_code,
|
||||||
|
"full_analysis": generated,
|
||||||
|
"resolved_by_ai": True,
|
||||||
|
"next_step": "운영자 검토 후 /api/design/sr-queue에서 승인하세요",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sr-queue")
|
||||||
|
async def get_sr_queue(limit: int = 20, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
rows = await db.execute(
|
||||||
|
select(DesignSR).order_by(desc(DesignSR.created_at)).limit(limit)
|
||||||
|
)
|
||||||
|
items = rows.scalars().all()
|
||||||
|
return [
|
||||||
|
{"id": s.id, "design_type": s.design_type, "requirement": s.requirement[:80],
|
||||||
|
"status": s.status, "resolved_by_ai": s.resolved_by_ai,
|
||||||
|
"has_code": bool(s.generated_code), "created_at": s.created_at}
|
||||||
|
for s in items
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
async def design_stats(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||||
|
total = (await db.execute(select(func.count(DesignSR.id)))).scalar() or 0
|
||||||
|
ai_resolved = (await db.execute(
|
||||||
|
select(func.count(DesignSR.id)).where(DesignSR.resolved_by_ai == True)
|
||||||
|
)).scalar() or 0
|
||||||
|
by_type = {}
|
||||||
|
for dt in ["CSS_CHANGE", "ICON_REQUEST", "UI_REVIEW", "LAYOUT_CHANGE", "TYPOGRAPHY"]:
|
||||||
|
cnt = (await db.execute(
|
||||||
|
select(func.count(DesignSR.id)).where(DesignSR.design_type == dt)
|
||||||
|
)).scalar() or 0
|
||||||
|
by_type[dt] = cnt
|
||||||
|
return {
|
||||||
|
"total": total, "ai_resolved": ai_resolved,
|
||||||
|
"ai_resolution_rate": round(ai_resolved / max(total, 1) * 100, 1),
|
||||||
|
"by_type": by_type, "vision_model": VISION_MODEL,
|
||||||
|
}
|
||||||
194
routers/icon_generator.py
Normal file
194
routers/icon_generator.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
"""SVG 아이콘 생성 — 텍스트 설명→SVG 코드 자동 생성"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json, logging, re
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.responses import Response
|
||||||
|
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, GeneratedIcon
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter(prefix="/api/icon", tags=["아이콘 생성"])
|
||||||
|
OLLAMA_URL = "http://localhost:11434"
|
||||||
|
TEXT_MODEL = "llama3"
|
||||||
|
|
||||||
|
# 내장 아이콘 템플릿 (GUARDiA 브랜드 #003366)
|
||||||
|
BUILTIN_ICONS = {
|
||||||
|
"server": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#003366"><rect x="2" y="3" width="20" height="6" rx="1"/><rect x="2" y="11" width="20" height="6" rx="1"/><circle cx="19" cy="6" r="1.5" fill="white"/><circle cx="19" cy="14" r="1.5" fill="white"/></svg>',
|
||||||
|
"alert": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#003366"><path d="M12 2L2 20h20L12 2z"/><text x="11" y="17" font-size="9" fill="white">!</text></svg>',
|
||||||
|
"check": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#003366" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>',
|
||||||
|
"deploy": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#003366"><path d="M12 2l8 4v6c0 5-4 9-8 10C8 21 4 17 4 12V6l8-4z"/></svg>',
|
||||||
|
"user": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#003366"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>',
|
||||||
|
"database": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#003366"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5"/><path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3" fill="none" stroke="white" stroke-width="1"/></svg>',
|
||||||
|
"network": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#003366"><circle cx="12" cy="12" r="3"/><circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/><circle cx="4" cy="18" r="2"/><circle cx="20" cy="18" r="2"/><line x1="12" y1="9" x2="4" y2="6" stroke="#003366" stroke-width="1.5"/><line x1="12" y1="9" x2="20" y2="6" stroke="#003366" stroke-width="1.5"/><line x1="12" y1="15" x2="4" y2="18" stroke="#003366" stroke-width="1.5"/><line x1="12" y1="15" x2="20" y2="18" stroke="#003366" stroke-width="1.5"/></svg>',
|
||||||
|
"security": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#003366"><path d="M12 2l8 4v5c0 5.5-3.5 10-8 11C7.5 21 4 16.5 4 11V6l8-4z"/><path d="M9 12l2 2 4-4" fill="none" stroke="white" stroke-width="2"/></svg>',
|
||||||
|
}
|
||||||
|
|
||||||
|
SVG_SYSTEM_PROMPT = """당신은 SVG 아이콘 전문 디자이너입니다.
|
||||||
|
요구사항에 맞는 24x24 픽토그램 SVG 코드를 생성합니다.
|
||||||
|
|
||||||
|
규칙:
|
||||||
|
1. viewBox="0 0 24 24" 사용
|
||||||
|
2. xmlns="http://www.w3.org/2000/svg" 포함
|
||||||
|
3. 기본 색상: fill="{color}"
|
||||||
|
4. 단순하고 명확한 픽토그램 스타일
|
||||||
|
5. SVG 코드만 출력 (다른 텍스트 없음)
|
||||||
|
6. 반드시 완전한 SVG 태그로 시작하고 끝내기"""
|
||||||
|
|
||||||
|
|
||||||
|
async def _generate_svg(description: str, color: str = "#003366", size: int = 24) -> str:
|
||||||
|
"""Ollama로 SVG 아이콘 생성."""
|
||||||
|
system = SVG_SYSTEM_PROMPT.replace("{color}", color)
|
||||||
|
prompt = f"다음 아이콘을 SVG로 생성 (viewBox 0 0 {size} {size}): {description}"
|
||||||
|
|
||||||
|
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", "")
|
||||||
|
|
||||||
|
# SVG 추출
|
||||||
|
svg_match = re.search(r'(<svg[\s\S]+?</svg>)', resp, re.IGNORECASE)
|
||||||
|
if svg_match:
|
||||||
|
return svg_match.group(1)
|
||||||
|
|
||||||
|
# 코드 블록에서 추출
|
||||||
|
block_match = re.search(r'```(?:svg|xml)?\n?([\s\S]+?)```', resp)
|
||||||
|
if block_match:
|
||||||
|
candidate = block_match.group(1).strip()
|
||||||
|
if '<svg' in candidate:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
# 폴백: 기본 아이콘
|
||||||
|
return _default_icon(description, color, size)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"SVG 생성 실패: {e}")
|
||||||
|
return _default_icon(description, color, size)
|
||||||
|
|
||||||
|
|
||||||
|
def _default_icon(name: str, color: str, size: int) -> str:
|
||||||
|
"""폴백: 텍스트 이니셜 SVG."""
|
||||||
|
initial = name[0].upper() if name else "?"
|
||||||
|
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}">'
|
||||||
|
f'<circle cx="{size//2}" cy="{size//2}" r="{size//2-1}" fill="{color}"/>'
|
||||||
|
f'<text x="{size//2}" y="{size//2+4}" text-anchor="middle" '
|
||||||
|
f'font-size="{size//2}" fill="white" font-family="sans-serif">{initial}</text></svg>')
|
||||||
|
|
||||||
|
|
||||||
|
class IconRequest(BaseModel):
|
||||||
|
description: str
|
||||||
|
color: str = "#003366"
|
||||||
|
size: int = 24
|
||||||
|
category: str = "custom"
|
||||||
|
save: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class BatchRequest(BaseModel):
|
||||||
|
icons: list[IconRequest]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/generate", status_code=201)
|
||||||
|
async def generate_icon(body: IconRequest, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""텍스트 설명→SVG 아이콘 생성."""
|
||||||
|
# 내장 아이콘 확인
|
||||||
|
for builtin_name, builtin_svg in BUILTIN_ICONS.items():
|
||||||
|
if builtin_name in body.description.lower():
|
||||||
|
return {"icon_id": f"builtin-{builtin_name}", "name": builtin_name,
|
||||||
|
"svg_code": builtin_svg, "source": "builtin",
|
||||||
|
"download_url": f"/api/icon/builtin/{builtin_name}"}
|
||||||
|
|
||||||
|
svg = await _generate_svg(body.description, body.color, body.size)
|
||||||
|
|
||||||
|
if not body.save:
|
||||||
|
return {"svg_code": svg, "source": "generated"}
|
||||||
|
|
||||||
|
icon = GeneratedIcon(
|
||||||
|
name=body.description[:50], description=body.description,
|
||||||
|
svg_code=svg, category=body.category,
|
||||||
|
primary_color=body.color, size=body.size,
|
||||||
|
created_by=user.id, created_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
db.add(icon); await db.commit(); await db.refresh(icon)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"icon_id": icon.id, "name": icon.name, "svg_code": svg,
|
||||||
|
"source": "ai_generated", "model": TEXT_MODEL,
|
||||||
|
"download_url": f"/api/icon/{icon.id}/download",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/batch")
|
||||||
|
async def batch_generate(body: BatchRequest, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""여러 아이콘 일괄 생성."""
|
||||||
|
results = []
|
||||||
|
for req in body.icons[:10]: # 최대 10개
|
||||||
|
svg = await _generate_svg(req.description, req.color, req.size)
|
||||||
|
icon = GeneratedIcon(
|
||||||
|
name=req.description[:50], description=req.description,
|
||||||
|
svg_code=svg, category=req.category, primary_color=req.color, size=req.size,
|
||||||
|
created_by=user.id, created_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
db.add(icon)
|
||||||
|
results.append({"description": req.description, "svg_preview": svg[:100] + "..."})
|
||||||
|
await db.commit()
|
||||||
|
return {"generated": len(results), "icons": results}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/library")
|
||||||
|
async def icon_library(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||||
|
# 내장 아이콘
|
||||||
|
builtins = [{"id": f"builtin-{k}", "name": k, "source": "builtin",
|
||||||
|
"download_url": f"/api/icon/builtin/{k}"} for k in BUILTIN_ICONS]
|
||||||
|
# 생성 아이콘
|
||||||
|
rows = await db.execute(
|
||||||
|
select(GeneratedIcon).order_by(desc(GeneratedIcon.created_at)).limit(50)
|
||||||
|
)
|
||||||
|
custom = [{"id": ic.id, "name": ic.name, "category": ic.category,
|
||||||
|
"source": "ai_generated", "download_url": f"/api/icon/{ic.id}/download"}
|
||||||
|
for ic in rows.scalars().all()]
|
||||||
|
return {"builtin": builtins, "custom": custom, "total": len(builtins) + len(custom)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/builtin/{name}")
|
||||||
|
async def get_builtin(name: str):
|
||||||
|
"""내장 아이콘 SVG 반환."""
|
||||||
|
if name not in BUILTIN_ICONS:
|
||||||
|
raise HTTPException(404, f"내장 아이콘 '{name}' 없음")
|
||||||
|
return Response(content=BUILTIN_ICONS[name], media_type="image/svg+xml",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{name}.svg"'})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{icon_id}/download")
|
||||||
|
async def download_icon(icon_id: int, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
row = await db.execute(select(GeneratedIcon).where(GeneratedIcon.id == icon_id))
|
||||||
|
icon = row.scalar_one_or_none()
|
||||||
|
if not icon: raise HTTPException(404)
|
||||||
|
return Response(content=icon.svg_code, media_type="image/svg+xml",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{icon.name}.svg"'})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/customize/{icon_id}")
|
||||||
|
async def customize_icon(icon_id: int, color: str = "#003366", size: int = 24,
|
||||||
|
db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||||
|
"""색상·크기 변경."""
|
||||||
|
row = await db.execute(select(GeneratedIcon).where(GeneratedIcon.id == icon_id))
|
||||||
|
icon = row.scalar_one_or_none()
|
||||||
|
if not icon: raise HTTPException(404)
|
||||||
|
customized = re.sub(r'fill="[^"]*"', f'fill="{color}"', icon.svg_code)
|
||||||
|
return {"svg_code": customized, "color": color, "size": size}
|
||||||
221
routers/smart_ux.py
Normal file
221
routers/smart_ux.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
"""
|
||||||
|
GUARDiA 스마트 UX — 다음명령 제시 + 음성처리
|
||||||
|
|
||||||
|
엔드포인트:
|
||||||
|
POST /api/ux/next-commands — Ollama 다음명령 3개 예측
|
||||||
|
GET /api/ux/quick-commands — 자주 쓰는 명령 목록
|
||||||
|
POST /api/ux/learn — 명령 사용 학습
|
||||||
|
GET /api/ux/suggestions — 컨텍스트 기반 추천
|
||||||
|
POST /api/ux/voice-process — 음성 텍스트→명령 매핑
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json, logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
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, CommandHistory
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter(prefix="/api/ux", tags=["스마트 UX"])
|
||||||
|
OLLAMA_URL = "http://localhost:11434"
|
||||||
|
TEXT_MODEL = "llama3"
|
||||||
|
|
||||||
|
# GUARDiA 기본 명령어 (빠른 접근용)
|
||||||
|
DEFAULT_COMMANDS = [
|
||||||
|
"/sr create", "/server status", "/deploy",
|
||||||
|
"/incident create", "/dashboard", "/kb search",
|
||||||
|
"/cmdb server", "/assign me", "/sr list",
|
||||||
|
"/server check all", "/alert list", "/report generate",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 음성 명령어 매핑 (한국어 → ITSM 명령)
|
||||||
|
VOICE_COMMAND_MAP = {
|
||||||
|
"서버 상태": "/server status", "서버 확인": "/server status",
|
||||||
|
"SR 만들어": "/sr create", "서비스 요청": "/sr create",
|
||||||
|
"배포 시작": "/deploy", "배포해줘": "/deploy",
|
||||||
|
"장애 보고": "/incident create", "인시던트": "/incident create",
|
||||||
|
"대시보드": "/dashboard", "현황 보기": "/dashboard",
|
||||||
|
"담당자 배정": "/assign", "KB 검색": "/kb search",
|
||||||
|
"보고서": "/report generate", "리포트": "/report generate",
|
||||||
|
"경보 목록": "/alert list", "알림 확인": "/alert list",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NextCommandIn(BaseModel):
|
||||||
|
recent_messages: List[str] = []
|
||||||
|
context: str = "" # 현재 방/작업 컨텍스트
|
||||||
|
user_role: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class LearnIn(BaseModel):
|
||||||
|
command: str
|
||||||
|
room: str = "general"
|
||||||
|
success: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceIn(BaseModel):
|
||||||
|
text: str # 음성→텍스트 변환 결과
|
||||||
|
context: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/next-commands")
|
||||||
|
async def next_commands(body: NextCommandIn, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""Ollama로 다음 명령어 3개 예측."""
|
||||||
|
|
||||||
|
# 1. 사용자 자주 쓰는 명령 조회
|
||||||
|
rows = await db.execute(
|
||||||
|
select(CommandHistory.command, func.count(CommandHistory.id).label("cnt"))
|
||||||
|
.where(CommandHistory.user_id == user.id,
|
||||||
|
CommandHistory.used_at > datetime.utcnow() - timedelta(days=7))
|
||||||
|
.group_by(CommandHistory.command)
|
||||||
|
.order_by(desc("cnt")).limit(5)
|
||||||
|
)
|
||||||
|
frequent = [r[0] for r in rows.all()]
|
||||||
|
|
||||||
|
# 2. Ollama 컨텍스트 분석
|
||||||
|
recent = body.recent_messages[-5:] if body.recent_messages else []
|
||||||
|
context_str = "\n".join(recent) if recent else "대화 없음"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15) as c:
|
||||||
|
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
|
||||||
|
"model": TEXT_MODEL,
|
||||||
|
"system": """GUARDiA ITSM 운영 어시스턴트.
|
||||||
|
사용 가능 명령: /sr create, /server status, /deploy, /incident create,
|
||||||
|
/kb search, /dashboard, /assign, /cmdb, /report, /alert
|
||||||
|
JSON 배열로만 답변: ["/cmd1", "/cmd2", "/cmd3"]""",
|
||||||
|
"prompt": f"최근 대화:\n{context_str}\n\n컨텍스트: {body.context}\n다음에 유용한 명령 3개:",
|
||||||
|
"stream": False,
|
||||||
|
})
|
||||||
|
resp = r.json().get("response", "[]")
|
||||||
|
|
||||||
|
# JSON 파싱 시도
|
||||||
|
import re
|
||||||
|
arr_match = re.search(r'\[([^\]]+)\]', resp)
|
||||||
|
if arr_match:
|
||||||
|
raw = arr_match.group(0)
|
||||||
|
suggested = json.loads(raw)
|
||||||
|
else:
|
||||||
|
suggested = DEFAULT_COMMANDS[:3]
|
||||||
|
except Exception:
|
||||||
|
suggested = DEFAULT_COMMANDS[:3]
|
||||||
|
|
||||||
|
# 3. 자주 쓰는 명령 우선 포함
|
||||||
|
all_cmds = frequent[:2] + [c for c in suggested if c not in frequent]
|
||||||
|
final = all_cmds[:3] if all_cmds else DEFAULT_COMMANDS[:3]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"commands": final,
|
||||||
|
"source": "ai+history",
|
||||||
|
"frequent": frequent[:3],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/quick-commands")
|
||||||
|
async def quick_commands(db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""자주 쓰는 명령 + 기본 명령 목록."""
|
||||||
|
rows = await db.execute(
|
||||||
|
select(CommandHistory.command, func.count(CommandHistory.id).label("cnt"))
|
||||||
|
.where(CommandHistory.user_id == user.id)
|
||||||
|
.group_by(CommandHistory.command)
|
||||||
|
.order_by(desc("cnt")).limit(8)
|
||||||
|
)
|
||||||
|
user_frequent = [{"command": r[0], "use_count": r[1]} for r in rows.all()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"frequent": user_frequent,
|
||||||
|
"defaults": [{"command": c, "use_count": 0} for c in DEFAULT_COMMANDS[:6]],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/learn")
|
||||||
|
async def learn_command(body: LearnIn, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""명령 사용 기록 학습 (클릭/실행 시 호출)."""
|
||||||
|
log = CommandHistory(
|
||||||
|
user_id=user.id,
|
||||||
|
command=body.command,
|
||||||
|
room=body.room,
|
||||||
|
success=body.success,
|
||||||
|
used_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
db.add(log); await db.commit()
|
||||||
|
return {"ok": True, "learned": body.command}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/suggestions")
|
||||||
|
async def get_suggestions(context: str = "", db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""현재 컨텍스트 기반 명령 추천."""
|
||||||
|
ctx_lower = context.lower()
|
||||||
|
|
||||||
|
# 키워드 기반 규칙 추천
|
||||||
|
rules = {
|
||||||
|
"서버": ["/server status", "/server check all", "/cmdb server"],
|
||||||
|
"배포": ["/deploy", "/deploy status", "/deploy rollback"],
|
||||||
|
"sr": ["/sr create", "/sr list", "/sr assign"],
|
||||||
|
"인시던트": ["/incident create", "/incident list", "/incident assign"],
|
||||||
|
"보고서": ["/report generate", "/report monthly", "/sla report"],
|
||||||
|
}
|
||||||
|
for keyword, cmds in rules.items():
|
||||||
|
if keyword in ctx_lower:
|
||||||
|
return {"suggestions": cmds, "matched_context": keyword}
|
||||||
|
|
||||||
|
# 기본 추천
|
||||||
|
return {"suggestions": DEFAULT_COMMANDS[:5], "matched_context": "default"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/voice-process")
|
||||||
|
async def voice_process(body: VoiceIn, db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(get_current_user)):
|
||||||
|
"""음성 텍스트→ITSM 명령 매핑 + Ollama 자연어 처리."""
|
||||||
|
text = body.text.strip()
|
||||||
|
|
||||||
|
# 1. 직접 매핑 확인
|
||||||
|
for korean, cmd in VOICE_COMMAND_MAP.items():
|
||||||
|
if korean in text:
|
||||||
|
return {
|
||||||
|
"original_text": text,
|
||||||
|
"mapped_command": cmd,
|
||||||
|
"confidence": "high",
|
||||||
|
"method": "keyword_mapping",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Ollama 자연어 처리
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15) as c:
|
||||||
|
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
|
||||||
|
"model": TEXT_MODEL,
|
||||||
|
"system": """ITSM 명령어 매퍼.
|
||||||
|
사용자 음성을 ITSM 슬래시 명령으로 변환.
|
||||||
|
가능한 명령: /sr create, /server status, /deploy, /incident create, /dashboard
|
||||||
|
슬래시 명령만 출력 (설명 없음).""",
|
||||||
|
"prompt": f"음성: '{text}'\n→ ITSM 명령:",
|
||||||
|
"stream": False,
|
||||||
|
})
|
||||||
|
resp = r.json().get("response", "").strip()
|
||||||
|
import re
|
||||||
|
cmd_match = re.search(r'/\w+(?:\s+\w+)*', resp)
|
||||||
|
if cmd_match:
|
||||||
|
return {"original_text": text, "mapped_command": cmd_match.group(),
|
||||||
|
"confidence": "medium", "method": "ollama"}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"original_text": text,
|
||||||
|
"mapped_command": None,
|
||||||
|
"confidence": "low",
|
||||||
|
"method": "failed",
|
||||||
|
"suggestion": "텍스트를 직접 입력하세요",
|
||||||
|
}
|
||||||
228
static/app.js
228
static/app.js
@ -367,6 +367,11 @@ function renderCurrentView() {
|
|||||||
else if (currentView === "batch_ssh") renderBatchSsh();
|
else if (currentView === "batch_ssh") renderBatchSsh();
|
||||||
else if (currentView === "asset_qr") renderAssetQr();
|
else if (currentView === "asset_qr") renderAssetQr();
|
||||||
else if (currentView === "notification_rules") renderNotificationRules();
|
else if (currentView === "notification_rules") renderNotificationRules();
|
||||||
|
// ── 디자인 AI + 스마트 UX 뷰 ──
|
||||||
|
else if (currentView === "design_dashboard") renderDesignDashboard();
|
||||||
|
else if (currentView === "design_icon") renderDesignIcon();
|
||||||
|
else if (currentView === "design_css") renderDesignCSS();
|
||||||
|
else if (currentView === "design_review") renderDesignReview();
|
||||||
// ── GUARDiA Brain 뷰 ──
|
// ── GUARDiA Brain 뷰 ──
|
||||||
else if (currentView === "brain_dashboard") renderBrainDashboard();
|
else if (currentView === "brain_dashboard") renderBrainDashboard();
|
||||||
else if (currentView === "ai_memory") renderAiMemory();
|
else if (currentView === "ai_memory") renderAiMemory();
|
||||||
@ -4761,3 +4766,226 @@ async function installPlugin(name) {
|
|||||||
showToast(d.ok ? `${name} 설치됨` : (d.message||"이미 설치됨"), d.ok?"success":"info");
|
showToast(d.ok ? `${name} 설치됨` : (d.message||"이미 설치됨"), d.ok?"success":"info");
|
||||||
if(d.ok) loadPlugins();
|
if(d.ok) loadPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── GUARDiA 디자인 AI + 스마트 UX 뷰
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function renderDesignDashboard() {
|
||||||
|
document.getElementById("content").innerHTML = `
|
||||||
|
<h2>🎨 디자인 AI — SR 자동화</h2>
|
||||||
|
<p style="color:#64748b;margin-bottom:16px">디자인 수정 SR을 AI가 자동 분류·처리. Ollama llava 비전 분석 + SVG 아이콘 생성 + CSS 자동 생성.</p>
|
||||||
|
<div id="design-stats" style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px">로딩 중...</div>
|
||||||
|
${_nextCard("빠른 디자인 SR 처리","⚡",`
|
||||||
|
<textarea id="design-req" class="form-control" rows="3" placeholder="예: 로그인 버튼 색상을 #003366으로 변경, 또는 서버 아이콘 픽토그램 스타일 24px 생성"></textarea>
|
||||||
|
<button class="btn btn-primary" style="margin-top:8px" onclick="autoResolveDesignSR()">🤖 AI 자동 처리</button>
|
||||||
|
<div id="design-result" style="margin-top:12px"></div>
|
||||||
|
`)}
|
||||||
|
${_nextCard("디자인 SR 이력","📋","<div id=\"design-queue\">로딩 중...</div>")}`;
|
||||||
|
loadDesignData();
|
||||||
|
}
|
||||||
|
async function autoResolveDesignSR() {
|
||||||
|
const req = document.getElementById("design-req").value;
|
||||||
|
if(!req) return showToast("요구사항 입력 필요","error");
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
showToast("AI 처리 중...","info");
|
||||||
|
const r = await fetch("/api/design/auto-resolve",{method:"POST",
|
||||||
|
headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({requirement:req})});
|
||||||
|
const d = await r.json();
|
||||||
|
const el = document.getElementById("design-result");
|
||||||
|
el.innerHTML = `<div style="border:1px solid #e2e8f0;border-radius:10px;padding:14px">
|
||||||
|
<div style="font-size:11px;color:#64748b;margin-bottom:6px">분류: ${d.design_type} | AI 처리: ${d.resolved_by_ai?"✅":"❌"}</div>
|
||||||
|
${d.generated_code?`<pre style="font-size:11px;background:#f8fafc;padding:10px;border-radius:6px;overflow:auto;max-height:200px;white-space:pre-wrap">${d.generated_code}</pre>`:""}
|
||||||
|
<p style="font-size:12px;color:#64748b;margin-top:8px">${d.next_step||""}</p>
|
||||||
|
</div>`;
|
||||||
|
loadDesignData();
|
||||||
|
}
|
||||||
|
async function loadDesignData() {
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
const [stats, queue] = await Promise.all([
|
||||||
|
fetch("/api/design/stats",{headers:{Authorization:`Bearer ${t}`}}).then(r=>r.json()).catch(()=>({})),
|
||||||
|
fetch("/api/design/sr-queue?limit=10",{headers:{Authorization:`Bearer ${t}`}}).then(r=>r.json()).catch(()=>[]),
|
||||||
|
]);
|
||||||
|
const se = document.getElementById("design-stats");
|
||||||
|
if(se) se.innerHTML=[
|
||||||
|
{icon:"📋",label:"전체 디자인 SR",val:stats.total||0},
|
||||||
|
{icon:"🤖",label:"AI 처리됨",val:stats.ai_resolved||0},
|
||||||
|
{icon:"📈",label:"자동화율",val:`${stats.ai_resolution_rate||0}%`},
|
||||||
|
{icon:"🔍",label:"비전 모델",val:"llava:7b"},
|
||||||
|
].map(s=>`<div style="background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:14px;text-align:center">
|
||||||
|
<div style="font-size:20px">${s.icon}</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#003366">${s.val}</div>
|
||||||
|
<div style="font-size:11px;color:#64748b">${s.label}</div>
|
||||||
|
</div>`).join("");
|
||||||
|
const qe = document.getElementById("design-queue");
|
||||||
|
if(qe) qe.innerHTML = queue.length ? queue.map(s=>`<div style="padding:10px;border-bottom:1px solid #f1f5f9;font-size:13px;display:flex;justify-content:space-between">
|
||||||
|
<div><span style="padding:2px 8px;background:#eff6ff;color:#1d4ed8;border-radius:8px;font-size:10px;margin-right:6px">${s.design_type}</span>${s.requirement}</div>
|
||||||
|
<div style="font-size:11px"><span style="color:${s.resolved_by_ai?"#166534":"#92400e"}">${s.status}</span></div>
|
||||||
|
</div>`).join("") : "<p style=\"color:#94a3b8;padding:12px;text-align:center\">처리된 디자인 SR 없음</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDesignIcon() {
|
||||||
|
document.getElementById("content").innerHTML = `
|
||||||
|
<h2>🎨 SVG 아이콘 생성</h2>
|
||||||
|
<p style="color:#64748b;margin-bottom:16px">텍스트로 설명하면 SVG 아이콘을 자동 생성합니다. GUARDiA 브랜드 색상(#003366) 기본 적용.</p>
|
||||||
|
${_nextCard("아이콘 생성","✨",`
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:8px">
|
||||||
|
<input id="icon-desc" class="form-control" placeholder="아이콘 설명 (예: 서버 픽토그램 24px, 데이터베이스 심볼)">
|
||||||
|
<input id="icon-color" class="form-control" type="color" value="#003366" style="width:50px;padding:2px">
|
||||||
|
<button class="btn btn-primary" onclick="generateIcon()">생성</button>
|
||||||
|
</div>
|
||||||
|
<div id="icon-result" style="margin-top:10px"></div>
|
||||||
|
`)}
|
||||||
|
${_nextCard("내장 아이콘 라이브러리","📚","<div id=\"icon-library\">로딩 중...</div>")}`;
|
||||||
|
loadIconLibrary();
|
||||||
|
}
|
||||||
|
async function generateIcon() {
|
||||||
|
const desc = document.getElementById("icon-desc").value;
|
||||||
|
const color = document.getElementById("icon-color").value;
|
||||||
|
if(!desc) return showToast("아이콘 설명 입력","error");
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
showToast("아이콘 생성 중...","info");
|
||||||
|
const r = await fetch("/api/icon/generate",{method:"POST",
|
||||||
|
headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({description:desc,color,save:true})});
|
||||||
|
const d = await r.json();
|
||||||
|
const el = document.getElementById("icon-result");
|
||||||
|
if(d.svg_code) {
|
||||||
|
el.innerHTML = `<div style="display:flex;gap:16px;align-items:center;padding:12px;border:1px solid #e2e8f0;border-radius:8px">
|
||||||
|
<div style="width:60px;height:60px;border:1px solid #e2e8f0;border-radius:8px;padding:8px;background:#f8fafc">${d.svg_code}</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px;color:#64748b;margin-bottom:4px">출처: ${d.source||"AI 생성"}</div>
|
||||||
|
<a href="${d.download_url||"#"}" style="color:#003366;font-size:12px">SVG 다운로드 ↗</a>
|
||||||
|
</div>
|
||||||
|
<pre style="flex:1;font-size:10px;background:#f8fafc;padding:8px;border-radius:4px;overflow:auto;max-height:80px">${d.svg_code.substring(0,200)}</pre>
|
||||||
|
</div>`;
|
||||||
|
showToast("아이콘 생성 완료","success");
|
||||||
|
loadIconLibrary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadIconLibrary() {
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
const lib = await fetch("/api/icon/library",{headers:{Authorization:`Bearer ${t}`}}).then(r=>r.json()).catch(()=>({}));
|
||||||
|
const el = document.getElementById("icon-library");
|
||||||
|
if(!el) return;
|
||||||
|
const builtins = lib.builtin||[];
|
||||||
|
el.innerHTML = `<div style="display:flex;flex-wrap:wrap;gap:8px">
|
||||||
|
${builtins.map(ic=>`<div style="text-align:center;padding:8px;border:1px solid #e2e8f0;border-radius:8px;width:70px">
|
||||||
|
<div style="font-size:10px;color:#64748b">${ic.name}</div>
|
||||||
|
<a href="${ic.download_url}" style="font-size:11px;color:#003366">SVG↗</a>
|
||||||
|
</div>`).join("")}
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#64748b;margin-top:8px">내장 ${builtins.length}개 | 생성 ${(lib.custom||[]).length}개</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDesignCSS() {
|
||||||
|
document.getElementById("content").innerHTML = `
|
||||||
|
<h2>✍️ CSS 자동 생성</h2>
|
||||||
|
<p style="color:#64748b;margin-bottom:16px">자연어로 디자인 요구사항을 설명하면 CSS 코드를 자동 생성합니다. GUARDiA 브랜드 변수 자동 적용.</p>
|
||||||
|
${_nextCard("CSS 생성","⚙️",`
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:8px">
|
||||||
|
<select id="css-mode" class="form-control" style="width:130px">
|
||||||
|
<option value="css">CSS</option>
|
||||||
|
<option value="tailwind">Tailwind</option>
|
||||||
|
<option value="component">컴포넌트</option>
|
||||||
|
</select>
|
||||||
|
<input id="css-req" class="form-control" placeholder="예: 기본 버튼 스타일, 호버·포커스 포함, 브랜드 컬러">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="generateCSS()">✍️ CSS 생성</button>
|
||||||
|
<div id="css-result" style="margin-top:12px"></div>
|
||||||
|
`)}
|
||||||
|
${_nextCard("GUARDiA 브랜드 변수","🎨",`
|
||||||
|
<div id="brand-vars">로딩 중...</div>
|
||||||
|
`)}
|
||||||
|
${_nextCard("생성 이력","📋","<div id=\"css-history\">로딩 중...</div>")}`;
|
||||||
|
loadCSSData();
|
||||||
|
}
|
||||||
|
async function generateCSS() {
|
||||||
|
const req = document.getElementById("css-req").value;
|
||||||
|
const mode = document.getElementById("css-mode").value;
|
||||||
|
if(!req) return showToast("요구사항 입력","error");
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
showToast("CSS 생성 중...","info");
|
||||||
|
const r = await fetch("/api/css/generate",{method:"POST",
|
||||||
|
headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({requirement:req,mode})});
|
||||||
|
const d = await r.json();
|
||||||
|
document.getElementById("css-result").innerHTML = `<div>
|
||||||
|
<div style="font-size:11px;color:#64748b;margin-bottom:6px">CSS ID: ${d.css_id}</div>
|
||||||
|
<pre style="font-size:12px;background:#1e293b;color:#e2e8f0;padding:14px;border-radius:8px;overflow:auto;max-height:300px;white-space:pre-wrap">${d.generated_css||""}</pre>
|
||||||
|
<button onclick="copyToClipboard(document.querySelector('pre').textContent)" style="margin-top:6px;padding:4px 10px;border:1px solid #e2e8f0;border-radius:4px;font-size:11px;cursor:pointer;background:#fff">📋 복사</button>
|
||||||
|
<p style="font-size:12px;color:#64748b;margin-top:6px">${d.preview_tip||""}</p>
|
||||||
|
</div>`;
|
||||||
|
loadCSSData();
|
||||||
|
}
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard?.writeText(text).then(()=>showToast("클립보드 복사됨","success")).catch(()=>showToast("복사 실패","error"));
|
||||||
|
}
|
||||||
|
async function loadCSSData() {
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
const [vars, hist] = await Promise.all([
|
||||||
|
fetch("/api/css/brand-variables",{headers:{Authorization:`Bearer ${t}`}}).then(r=>r.json()).catch(()=>({})),
|
||||||
|
fetch("/api/css/history",{headers:{Authorization:`Bearer ${t}`}}).then(r=>r.json()).catch(()=>[]),
|
||||||
|
]);
|
||||||
|
const ve = document.getElementById("brand-vars");
|
||||||
|
if(ve) ve.innerHTML = Object.entries(vars.variables||{}).map(([k,v])=>
|
||||||
|
`<div style="padding:4px 0;font-size:12px;font-family:monospace"><span style="color:#003366">${k}</span>: ${v}</div>`).join("");
|
||||||
|
const he = document.getElementById("css-history");
|
||||||
|
if(he) he.innerHTML = hist.length ? hist.map(h=>`<div style="padding:8px;border-bottom:1px solid #f1f5f9;font-size:13px">
|
||||||
|
<div>${h.requirement}</div>
|
||||||
|
<span style="font-size:11px;color:${h.applied?"#166534":"#64748b"}">${h.applied?"✅ 적용됨":"대기"}</span>
|
||||||
|
</div>`).join("") : "<p style=\"color:#94a3b8;padding:12px;text-align:center\">생성 이력 없음</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDesignReview() {
|
||||||
|
document.getElementById("content").innerHTML = `
|
||||||
|
<h2>🔍 스크린샷 UI 분석</h2>
|
||||||
|
<p style="color:#64748b;margin-bottom:16px">스크린샷을 업로드하면 Ollama llava가 UI/UX를 분석하고 개선 제안을 제공합니다.</p>
|
||||||
|
${_nextCard("스크린샷 분석","📸",`
|
||||||
|
<div style="margin-bottom:8px">
|
||||||
|
<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px">스크린샷 파일</label>
|
||||||
|
<input type="file" id="review-file" accept="image/*" class="form-control" style="margin-bottom:8px">
|
||||||
|
<input id="review-q" class="form-control" placeholder="분석 질문 (기본: 이 UI의 개선점 3가지 제안)" style="margin-bottom:8px">
|
||||||
|
<button class="btn btn-primary" onclick="analyzeScreenshot()">🔍 AI 분석</button>
|
||||||
|
</div>
|
||||||
|
<div id="review-result" style="margin-top:12px"></div>
|
||||||
|
`)}
|
||||||
|
${_nextCard("텍스트 디자인 리뷰","✍️",`
|
||||||
|
<textarea id="review-text" class="form-control" rows="3" placeholder="텍스트로 디자인 요구사항 설명 (스크린샷 없이 텍스트만으로 분석)"></textarea>
|
||||||
|
<button class="btn btn-primary" style="margin-top:8px" onclick="textReview()">분석</button>
|
||||||
|
<div id="text-review-result" style="margin-top:10px"></div>
|
||||||
|
`)}`;
|
||||||
|
}
|
||||||
|
async function analyzeScreenshot() {
|
||||||
|
const file = document.getElementById("review-file").files[0];
|
||||||
|
const q = document.getElementById("review-q").value || "이 UI의 개선점을 한국어로 3가지 제안해줘";
|
||||||
|
if(!file) return showToast("이미지 파일 선택 필요","error");
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
showToast("llava 비전 분석 중... (30초 내외 소요)","info");
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file",file); form.append("question",q);
|
||||||
|
const r = await fetch("/api/design/analyze",{method:"POST",headers:{Authorization:`Bearer ${t}`},body:form});
|
||||||
|
const d = await r.json();
|
||||||
|
document.getElementById("review-result").innerHTML = `<div style="border:1px solid #e2e8f0;border-radius:10px;padding:16px">
|
||||||
|
<div style="font-size:11px;color:#64748b;margin-bottom:8px">분석 ID: ${d.analysis_id} | 모델: ${d.model||"llava:7b"}</div>
|
||||||
|
<div style="font-size:13px;line-height:1.7;white-space:pre-wrap">${d.analysis||""}</div>
|
||||||
|
</div>`;
|
||||||
|
showToast("분석 완료","success");
|
||||||
|
}
|
||||||
|
async function textReview() {
|
||||||
|
const req = document.getElementById("review-text").value;
|
||||||
|
if(!req) return showToast("요구사항 입력","error");
|
||||||
|
const t = localStorage.getItem("token")||"";
|
||||||
|
showToast("AI 리뷰 생성 중...","info");
|
||||||
|
const r = await fetch("/api/design/review",{method:"POST",
|
||||||
|
headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({requirement:req})});
|
||||||
|
const d = await r.json();
|
||||||
|
document.getElementById("text-review-result").innerHTML = `<div style="border:1px solid #e2e8f0;border-radius:10px;padding:16px">
|
||||||
|
<div style="font-size:11px;color:#64748b;margin-bottom:8px">분류: ${d.design_type}</div>
|
||||||
|
<div style="font-size:13px;line-height:1.7;white-space:pre-wrap">${d.analysis||""}</div>
|
||||||
|
</div>`;
|
||||||
|
showToast("리뷰 완료","success");
|
||||||
|
}
|
||||||
|
|||||||
@ -211,6 +211,20 @@
|
|||||||
<div class="nav-sub-item" data-view="white_label">브랜딩 설정</div>
|
<div class="nav-sub-item" data-view="white_label">브랜딩 설정</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 디자인 AI + 스마트 UX ────────────────────── -->
|
||||||
|
<div class="nav-separator"></div>
|
||||||
|
|
||||||
|
<div class="nav-group-header" onclick="toggleNavGroup(this)" aria-expanded="true">
|
||||||
|
<span class="nav-icon">🎨</span><span>디자인 AI</span>
|
||||||
|
<span class="nav-arrow" aria-hidden="true">▾</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-group-body" role="group" style="display:block">
|
||||||
|
<div class="nav-sub-item" data-view="design_dashboard">디자인 SR 대시보드</div>
|
||||||
|
<div class="nav-sub-item" data-view="design_icon">아이콘 생성</div>
|
||||||
|
<div class="nav-sub-item" data-view="design_css">CSS 자동 생성</div>
|
||||||
|
<div class="nav-sub-item" data-view="design_review">스크린샷 분석</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── GUARDiA Brain — AI 지능화 엔진 ──────────── -->
|
<!-- ── GUARDiA Brain — AI 지능화 엔진 ──────────── -->
|
||||||
<div class="nav-separator"></div>
|
<div class="nav-separator"></div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user