From 93aa4b0c54aa9e8567af41dc63c155f0a141641a Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Sun, 7 Jun 2026 20:13:54 +0900 Subject: [PATCH] manual-deploy 2026-06-07 20:13 --- backend/main.py | 9 + frontend/src/pages/JasperReports.tsx | 382 ++++++++++++++++++++ frontend/src/pages/PerfTestStudio.tsx | 483 ++++++++++++++++++++++++++ frontend/tsconfig.tsbuildinfo | 2 +- 4 files changed, 875 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/JasperReports.tsx create mode 100644 frontend/src/pages/PerfTestStudio.tsx diff --git a/backend/main.py b/backend/main.py index 6f64e28..a32f18e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -25,6 +25,15 @@ app.include_router(deploy.router, prefix="/api/deploy", tags=["deploy"]) app.include_router(config.router, prefix="/api/config", tags=["config"]) app.include_router(llm.router, prefix="/api/llm", tags=["llm"]) +# ── Gen6 확장 (2026-06-07) ──────────────────────────────────────────────── +from routers import ai_analytics2, platform_mgmt, adv_security_mgr, ops_automation, finops2, cross_system +app.include_router(ai_analytics2.router) # AI 분석 v2 (예측 KPI·이상패턴·AI리포트) +app.include_router(platform_mgmt.router) # 플랫폼 관리 (멀티클러스터·GitOps·배포맵) +app.include_router(adv_security_mgr.router) # 고급 보안 (ZeroTrust UI·위협헌팅·SOC) +app.include_router(ops_automation.router) # 운영 자동화 (노코드·런북·정책UI·스케줄러) +app.include_router(finops2.router) # FinOps v2 (비용최적화·예산예측·태깅) +app.include_router(cross_system.router) # 크로스 시스템 (ITSM 구독·데이터동기화) + @app.get("/health") async def health(): return {"status": "ok", "service": "guardia-manager", "port": 8002} diff --git a/frontend/src/pages/JasperReports.tsx b/frontend/src/pages/JasperReports.tsx new file mode 100644 index 0000000..d9f10e8 --- /dev/null +++ b/frontend/src/pages/JasperReports.tsx @@ -0,0 +1,382 @@ +import { useState, useEffect, useCallback } from 'react' +import axios from 'axios' + +const API = import.meta.env.VITE_ITSM_API ?? 'http://localhost:9001' + +type Category = 'DELIVERABLE' | 'MEETING_MINUTES' | 'REPORT' +type JobStatus = 'PENDING' | 'DONE' | 'FAILED' + +interface Template { + id: number + name: string + category: Category + output_format: 'PDF' | 'EXCEL' + is_builtin: boolean + field_count: number + created_at: string +} + +interface HistoryItem { + id: number + category: Category + title: string + status: JobStatus + output_format: 'PDF' | 'EXCEL' + file_size: number | null + error: string | null + created_at: string + download_url: string | null +} + +const CATEGORY_LABEL: Record = { + DELIVERABLE: '산출물', MEETING_MINUTES: '회의록', REPORT: '보고서', +} +const CATEGORY_COLOR: Record = { + DELIVERABLE: '#4f6ef7', MEETING_MINUTES: '#16a34a', REPORT: '#f59e0b', +} +const STATUS_COLOR: Record = { + PENDING: '#64748b', DONE: '#16a34a', FAILED: '#dc2626', +} +const STATUS_LABEL: Record = { + PENDING: '대기', DONE: '완료', FAILED: '실패', +} + +function fmtDate(d: string | null) { + if (!d) return '—' + return new Date(d).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) +} + +function fmtSize(n: number | null) { + if (!n) return '—' + if (n < 1024) return `${n} B` + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB` + return `${(n / 1024 / 1024).toFixed(1)} MB` +} + +function CatBadge({ category }: { category: Category }) { + return ( + {CATEGORY_LABEL[category]} + ) +} + +function StatusBadge({ status }: { status: JobStatus }) { + return ( + {STATUS_LABEL[status]} + ) +} + +async function downloadBlob(url: string, headers: Record, fallbackName: string) { + const r = await axios.get(url, { headers, responseType: 'blob' }) + const disposition: string = r.headers['content-disposition'] ?? '' + const m = /filename="?([^";]+)"?/.exec(disposition) + const filename = m ? m[1].trim() : fallbackName + const blobUrl = window.URL.createObjectURL(new Blob([r.data])) + const a = document.createElement('a') + a.href = blobUrl + a.download = filename + document.body.appendChild(a) + a.click() + a.remove() + window.URL.revokeObjectURL(blobUrl) +} + +export default function JasperReports() { + const token = localStorage.getItem('guardia_token') ?? '' + const headers = { Authorization: `Bearer ${token}` } + + const [templates, setTemplates] = useState([]) + const [history, setHistory] = useState([]) + const [loading, setLoading] = useState(false) + const [tab, setTab] = useState('DELIVERABLE') + const [generating, setGenerating] = useState(false) + + const [deliverable, setDeliverable] = useState({ + title: '', org_name: '지오정보기술', period_start: '', period_end: '', prepared_by: '', output_format: 'PDF' as 'PDF' | 'EXCEL', + }) + const [meeting, setMeeting] = useState({ + title: '', meeting_date: '', location: '온라인', chairman: '', + attendees: '', agenda_summary: '', decisions: '', action_items: '', next_meeting: '', output_format: 'PDF' as 'PDF' | 'EXCEL', + }) + const [report, setReport] = useState({ + title: '정기 운영 보고서', period_start: '', period_end: '', output_format: 'PDF' as 'PDF' | 'EXCEL', + }) + + const load = useCallback(async () => { + setLoading(true) + try { + const [tRes, hRes] = await Promise.all([ + axios.get(`${API}/api/jasper/templates`, { headers }), + axios.get(`${API}/api/jasper/history`, { params: { limit: 50 }, headers }), + ]) + setTemplates(tRes.data.templates) + setHistory(hRes.data.items) + } catch (e) { + console.error(e) + } finally { + setLoading(false) + } + }, [token]) + + useEffect(() => { load() }, [load]) + + const stats = { + templates: templates.length, + done: history.filter(h => h.status === 'DONE').length, + pending: history.filter(h => h.status === 'PENDING').length, + failed: history.filter(h => h.status === 'FAILED').length, + } + + function lines(s: string): string[] { + return s.split('\n').map(v => v.trim()).filter(Boolean) + } + + async function handleGenerate() { + setGenerating(true) + try { + let r + if (tab === 'DELIVERABLE') { + if (!deliverable.title.trim()) return alert('문서 제목을 입력하세요.') + r = await axios.post(`${API}/api/jasper/generate/deliverable`, { + ...deliverable, + period_start: deliverable.period_start || null, + period_end: deliverable.period_end || null, + prepared_by: deliverable.prepared_by || null, + }, { headers }) + } else if (tab === 'MEETING_MINUTES') { + if (!meeting.title.trim() || !meeting.meeting_date.trim()) return alert('회의명과 회의 일시는 필수입니다.') + r = await axios.post(`${API}/api/jasper/generate/meeting-minutes`, { + ...meeting, + attendees: lines(meeting.attendees), + decisions: lines(meeting.decisions), + action_items: lines(meeting.action_items), + }, { headers }) + } else { + r = await axios.post(`${API}/api/jasper/generate/report`, { + ...report, + period_start: report.period_start || null, + period_end: report.period_end || null, + }, { headers }) + } + if (r.data.ok) { + alert(`문서 생성 완료: ${r.data.title} (job #${r.data.job_id})\n다운로드는 이력 그리드에서 가능합니다.`) + } else { + alert(`생성 실패: ${r.data.error ?? '알 수 없는 오류'}`) + } + await load() + } catch (e: any) { + alert(`생성 실패: ${e.response?.data?.detail ?? e.message}`) + } finally { + setGenerating(false) + } + } + + async function handleDownload(item: HistoryItem) { + if (!item.download_url) return alert('완료된 문서만 다운로드할 수 있습니다.') + const ext = item.output_format === 'EXCEL' ? 'xlsx' : 'pdf' + try { + await downloadBlob(`${API}${item.download_url}`, headers, `${item.title}_${item.id}.${ext}`) + } catch (e: any) { + alert(e.response?.data?.detail ?? '다운로드 실패') + } + } + + const card = (label: string, val: number | undefined, color: string) => ( +
+
{val ?? 0}
+
{label}
+
+ ) + + return ( +
+
+

📄 Jasper 문서 자동작성

+ 산출물 · 회의록 · 보고서를 JRXML 템플릿 기반으로 PDF/Excel 자동 생성 + +
+ + {/* 통계 카드 */} +
+ {card('등록 템플릿', stats.templates, '#6366f1')} + {card('생성 완료', stats.done, '#16a34a')} + {card('대기', stats.pending, '#64748b')} + {card('실패', stats.failed, '#dc2626')} +
+ + {/* 카테고리 탭 */} +
+ {(['DELIVERABLE', 'MEETING_MINUTES', 'REPORT'] as Category[]).map(c => ( + + ))} +
+ + {/* 생성 폼 */} +
+ {tab === 'DELIVERABLE' && ( +
+ setDeliverable(p => ({ ...p, title: e.target.value }))} + placeholder="문서 제목" style={{ ...inputStyle, width: 220 }} /> + setDeliverable(p => ({ ...p, org_name: e.target.value }))} + placeholder="대상 기관/고객사명" style={{ ...inputStyle, width: 160 }} /> + setDeliverable(p => ({ ...p, period_start: e.target.value }))} style={inputStyle} /> + ~ + setDeliverable(p => ({ ...p, period_end: e.target.value }))} style={inputStyle} /> + setDeliverable(p => ({ ...p, prepared_by: e.target.value }))} + placeholder="작성자 (선택)" style={{ ...inputStyle, width: 120 }} /> + setDeliverable(p => ({ ...p, output_format: v }))} /> + +
+ )} + + {tab === 'MEETING_MINUTES' && ( +
+
+ setMeeting(p => ({ ...p, title: e.target.value }))} + placeholder="회의명" style={{ ...inputStyle, width: 200 }} /> + setMeeting(p => ({ ...p, meeting_date: e.target.value.replace('T', ' ') }))} style={inputStyle} /> + setMeeting(p => ({ ...p, location: e.target.value }))} + placeholder="장소" style={{ ...inputStyle, width: 120 }} /> + setMeeting(p => ({ ...p, chairman: e.target.value }))} + placeholder="주재자" style={{ ...inputStyle, width: 120 }} /> + setMeeting(p => ({ ...p, next_meeting: e.target.value }))} + placeholder="차기 회의 (선택)" style={{ ...inputStyle, width: 140 }} /> + setMeeting(p => ({ ...p, output_format: v }))} /> +
+