G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1210 lines
46 KiB
JavaScript
1210 lines
46 KiB
JavaScript
// GUARDiA × Paperclip AI 에이전트 구현 보고서 PPT 생성
|
||
const pptxgen = require("pptxgenjs");
|
||
|
||
const pres = new pptxgen();
|
||
pres.layout = "LAYOUT_16x9";
|
||
pres.author = "GUARDiA Team";
|
||
pres.title = "GUARDiA × Paperclip AI 에이전트 구현 보고서";
|
||
|
||
// ─── 색상 팔레트 ─────────────────────────────────────────
|
||
const C = {
|
||
navyDark: "0D1B3E", // 타이틀 배경
|
||
navy: "1E2761", // 사이드바
|
||
blue: "2E5BBA", // 강조 파랑
|
||
lightBlue: "4A90D9", // 서브 강조
|
||
ice: "CADCFC", // 연한 파랑 (배경 요소)
|
||
white: "FFFFFF",
|
||
offWhite: "F4F7FF", // 콘텐츠 슬라이드 배경
|
||
gray: "64748B",
|
||
lightGray: "E2E8F0",
|
||
green: "059669",
|
||
amber: "D97706",
|
||
red: "DC2626",
|
||
purple: "7C3AED",
|
||
teal: "0891B2",
|
||
orange: "EA580C",
|
||
};
|
||
|
||
const makeShadow = () => ({
|
||
type: "outer", blur: 6, offset: 3, angle: 135, color: "000000", opacity: 0.12
|
||
});
|
||
|
||
// ─── 슬라이드 헬퍼 ───────────────────────────────────────
|
||
function addTitleSlide(bgColor, title, subtitle, badge) {
|
||
const slide = pres.addSlide();
|
||
slide.background = { color: bgColor };
|
||
|
||
// 좌측 강조 바
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0, y: 0, w: 0.12, h: 5.625,
|
||
fill: { color: C.lightBlue }, line: { color: C.lightBlue }
|
||
});
|
||
|
||
// 우측 장식 원
|
||
slide.addShape(pres.shapes.OVAL, {
|
||
x: 7.5, y: -1.2, w: 4.5, h: 4.5,
|
||
fill: { color: C.navy, transparency: 60 }, line: { color: C.navy, transparency: 60 }
|
||
});
|
||
slide.addShape(pres.shapes.OVAL, {
|
||
x: 8.5, y: 2.8, w: 2.5, h: 2.5,
|
||
fill: { color: C.blue, transparency: 70 }, line: { color: C.blue, transparency: 70 }
|
||
});
|
||
|
||
if (badge) {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.4, y: 1.3, w: 2.2, h: 0.38,
|
||
fill: { color: C.lightBlue }, line: { color: C.lightBlue }
|
||
});
|
||
slide.addText(badge, {
|
||
x: 0.4, y: 1.3, w: 2.2, h: 0.38,
|
||
fontSize: 11, color: C.white, bold: true, align: "center", valign: "middle", margin: 0
|
||
});
|
||
}
|
||
|
||
slide.addText(title, {
|
||
x: 0.4, y: 1.9, w: 8.5, h: 1.4,
|
||
fontSize: 36, color: C.white, bold: true, align: "left", fontFace: "Calibri"
|
||
});
|
||
|
||
if (subtitle) {
|
||
slide.addText(subtitle, {
|
||
x: 0.4, y: 3.45, w: 7.5, h: 0.7,
|
||
fontSize: 16, color: C.ice, align: "left", fontFace: "Calibri"
|
||
});
|
||
}
|
||
|
||
// 하단 날짜 라인
|
||
slide.addShape(pres.shapes.LINE, {
|
||
x: 0.4, y: 5.0, w: 9.2, h: 0,
|
||
line: { color: C.lightBlue, width: 1 }
|
||
});
|
||
slide.addText("2026.05.25 | GUARDiA ITSM", {
|
||
x: 0.4, y: 5.1, w: 5, h: 0.35,
|
||
fontSize: 10, color: "8FAADC", align: "left", fontFace: "Calibri"
|
||
});
|
||
|
||
return slide;
|
||
}
|
||
|
||
function addContentSlide(title) {
|
||
const slide = pres.addSlide();
|
||
slide.background = { color: C.offWhite };
|
||
|
||
// 상단 헤더 바
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0, y: 0, w: 10, h: 0.72,
|
||
fill: { color: C.navy }, line: { color: C.navy }
|
||
});
|
||
|
||
// 좌측 포인트 바
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0, y: 0.72, w: 0.08, h: 4.9,
|
||
fill: { color: C.lightBlue }, line: { color: C.lightBlue }
|
||
});
|
||
|
||
slide.addText(title, {
|
||
x: 0.25, y: 0, w: 9.5, h: 0.72,
|
||
fontSize: 20, color: C.white, bold: true, align: "left", valign: "middle",
|
||
fontFace: "Calibri", margin: 0
|
||
});
|
||
|
||
return slide;
|
||
}
|
||
|
||
function addSectionSlide(number, title, subtitle) {
|
||
const slide = pres.addSlide();
|
||
slide.background = { color: C.blue };
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0, y: 0, w: 0.08, h: 5.625,
|
||
fill: { color: C.lightBlue }, line: { color: C.lightBlue }
|
||
});
|
||
|
||
slide.addText(`Phase ${number}`, {
|
||
x: 0.4, y: 1.4, w: 9, h: 0.7,
|
||
fontSize: 16, color: C.ice, bold: false, fontFace: "Calibri"
|
||
});
|
||
slide.addText(title, {
|
||
x: 0.4, y: 2.0, w: 9, h: 1.2,
|
||
fontSize: 32, color: C.white, bold: true, fontFace: "Calibri"
|
||
});
|
||
if (subtitle) {
|
||
slide.addText(subtitle, {
|
||
x: 0.4, y: 3.3, w: 8, h: 0.7,
|
||
fontSize: 15, color: C.ice, fontFace: "Calibri"
|
||
});
|
||
}
|
||
return slide;
|
||
}
|
||
|
||
function card(slide, x, y, w, h, headerColor, headerText, bodyLines) {
|
||
// 카드 본체
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w, h,
|
||
fill: { color: C.white },
|
||
line: { color: C.lightGray, width: 1 },
|
||
shadow: makeShadow()
|
||
});
|
||
// 카드 상단 컬러 바
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w, h: 0.32,
|
||
fill: { color: headerColor },
|
||
line: { color: headerColor }
|
||
});
|
||
slide.addText(headerText, {
|
||
x, y, w, h: 0.32,
|
||
fontSize: 11, color: C.white, bold: true,
|
||
align: "center", valign: "middle", fontFace: "Calibri", margin: 0
|
||
});
|
||
|
||
const textArr = bodyLines.map((line, i) => ({
|
||
text: line,
|
||
options: {
|
||
fontSize: 10.5,
|
||
color: i === 0 ? C.gray : "1E293B",
|
||
bold: i === 0,
|
||
breakLine: true
|
||
}
|
||
}));
|
||
// 마지막 요소는 breakLine 제거
|
||
if (textArr.length > 0) textArr[textArr.length - 1].options.breakLine = false;
|
||
|
||
slide.addText(textArr, {
|
||
x: x + 0.1, y: y + 0.38, w: w - 0.2, h: h - 0.45,
|
||
valign: "top", fontFace: "Calibri"
|
||
});
|
||
}
|
||
|
||
function infoBox(slide, x, y, w, h, accentColor, title, desc) {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w, h,
|
||
fill: { color: C.white },
|
||
line: { color: accentColor, width: 2 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 0.1, h,
|
||
fill: { color: accentColor },
|
||
line: { color: accentColor }
|
||
});
|
||
slide.addText(title, {
|
||
x: x + 0.18, y: y + 0.07, w: w - 0.25, h: 0.3,
|
||
fontSize: 11.5, color: "1E293B", bold: true, fontFace: "Calibri"
|
||
});
|
||
slide.addText(desc, {
|
||
x: x + 0.18, y: y + 0.36, w: w - 0.25, h: h - 0.42,
|
||
fontSize: 10.5, color: C.gray, fontFace: "Calibri", valign: "top"
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 1: 타이틀
|
||
// ═══════════════════════════════════════════════════════
|
||
addTitleSlide(
|
||
C.navyDark,
|
||
"GUARDiA × Paperclip\nAI 에이전트 구현 보고서",
|
||
"Phase 1~4 설계·구현·테스트 완료 보고",
|
||
"2026.05.25 완료"
|
||
);
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 2: 목차
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("목 차");
|
||
|
||
const items = [
|
||
{ n: "01", t: "GUARDiA ITSM 개요 & 배경", c: C.blue },
|
||
{ n: "02", t: "Paperclip 프레임워크 소개", c: C.purple },
|
||
{ n: "03", t: "Phase 1 — Paperclip 개발 도구 설정", c: C.teal },
|
||
{ n: "04", t: "Phase 2 — Ollama 로컬 LLM 구성", c: C.green },
|
||
{ n: "05", t: "Phase 3 — GUARDiA 에이전트 엔진", c: C.orange },
|
||
{ n: "06", t: "Phase 4 — 자율 운영 대시보드", c: C.blue },
|
||
{ n: "07", t: "테스트 결과 요약", c: C.green },
|
||
{ n: "08", t: "보안 제약사항 & 향후 로드맵", c: C.navy },
|
||
];
|
||
|
||
items.forEach((item, i) => {
|
||
const col = i < 4 ? 0 : 1;
|
||
const row = i % 4;
|
||
const x = 0.3 + col * 4.85;
|
||
const y = 0.88 + row * 1.1;
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 4.5, h: 0.9,
|
||
fill: { color: C.white },
|
||
line: { color: C.lightGray, width: 1 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 0.55, h: 0.9,
|
||
fill: { color: item.c },
|
||
line: { color: item.c }
|
||
});
|
||
slide.addText(item.n, {
|
||
x, y, w: 0.55, h: 0.9,
|
||
fontSize: 16, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
slide.addText(item.t, {
|
||
x: x + 0.62, y, w: 3.8, h: 0.9,
|
||
fontSize: 12.5, color: "1E293B", bold: false,
|
||
align: "left", valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 3: GUARDiA ITSM 개요
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("01 GUARDiA ITSM 개요");
|
||
|
||
// 왼쪽: 기존 기능
|
||
slide.addText("기존 GUARDiA 기능", {
|
||
x: 0.25, y: 0.85, w: 4.5, h: 0.4,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const existingFeatures = [
|
||
["SR 접수·처리", "서비스 요청 워크플로우"],
|
||
["SSL 인증서 관리", "만료 감시 & 갱신 알림"],
|
||
["PM 정기점검", "예방 유지보수 일정"],
|
||
["SI 프로젝트 관리", "WBS·이슈·리스크·테스트"],
|
||
["CMDB", "서버 자산 관리"],
|
||
["메신저·알림", "내부 커뮤니케이션"],
|
||
];
|
||
|
||
existingFeatures.forEach(([title, desc], i) => {
|
||
const y = 1.3 + i * 0.63;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.25, y, w: 4.5, h: 0.55,
|
||
fill: { color: C.offWhite }, line: { color: C.lightGray, width: 1 }
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.25, y, w: 0.07, h: 0.55,
|
||
fill: { color: C.blue }, line: { color: C.blue }
|
||
});
|
||
slide.addText([
|
||
{ text: title, options: { bold: true, color: "1E293B", breakLine: true } },
|
||
{ text: desc, options: { color: C.gray } }
|
||
], {
|
||
x: 0.4, y, w: 4.2, h: 0.55,
|
||
fontSize: 10.5, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
|
||
// 화살표
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 4.85, y: 2.5, w: 0.35, h: 0.6,
|
||
fill: { color: C.lightBlue }, line: { color: C.lightBlue }
|
||
});
|
||
slide.addText("AI\n추가", {
|
||
x: 4.82, y: 2.5, w: 0.41, h: 0.6,
|
||
fontSize: 9, color: C.white, bold: true, align: "center", valign: "middle", fontFace: "Calibri", margin: 0
|
||
});
|
||
|
||
// 오른쪽: AI 추가 목표
|
||
slide.addText("AI 에이전트 추가 목표", {
|
||
x: 5.3, y: 0.85, w: 4.4, h: 0.4,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const aiGoals = [
|
||
{ icon: "⚡", title: "반복 업무 자동화", desc: "장애 분류, KB 등록, SR 자동 생성", color: C.orange },
|
||
{ icon: "👁", title: "능동적 모니터링", desc: "SSL·WBS·PM 상태를 AI가 주기 감시", color: C.blue },
|
||
{ icon: "🤝", title: "사람-AI 협업", desc: "고위험 액션은 사람 승인 게이트 통과", color: C.green },
|
||
{ icon: "🔒", title: "완전 온프레미스", desc: "Ollama 로컬 LLM, 외부 API 완전 차단", color: C.purple },
|
||
];
|
||
|
||
aiGoals.forEach((g, i) => {
|
||
const y = 1.3 + i * 0.98;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 5.3, y, w: 4.4, h: 0.85,
|
||
fill: { color: C.white }, line: { color: g.color, width: 2 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 5.3, y, w: 0.1, h: 0.85,
|
||
fill: { color: g.color }, line: { color: g.color }
|
||
});
|
||
slide.addText([
|
||
{ text: `${g.icon} ${g.title}`, options: { bold: true, color: "1E293B", breakLine: true } },
|
||
{ text: g.desc, options: { color: C.gray } }
|
||
], {
|
||
x: 5.48, y, w: 4.1, h: 0.85,
|
||
fontSize: 11, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 4: Paperclip 프레임워크 소개
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("02 Paperclip 프레임워크 소개");
|
||
|
||
// 메인 설명
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.25, y: 0.85, w: 9.5, h: 0.72,
|
||
fill: { color: "EEF2FF" }, line: { color: C.lightBlue, width: 1 }
|
||
});
|
||
slide.addText([
|
||
{ text: "Paperclip", options: { bold: true, color: C.blue } },
|
||
{ text: " — AI 에이전트 오케스트레이션 오픈소스 프레임워크 (github.com/paperclipai/paperclip)", options: { color: "1E293B" } }
|
||
], {
|
||
x: 0.35, y: 0.85, w: 9.3, h: 0.72,
|
||
fontSize: 13, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
|
||
const features = [
|
||
{ title: "조직도 구조", desc: "CEO → CTO → 개발자/QA\n계층적 에이전트 관리", color: C.purple, icon: "🏢" },
|
||
{ title: "하트비트 시스템", desc: "에이전트가 주기적으로\n깨어나 작업 수행 후 대기", color: C.blue, icon: "💓" },
|
||
{ title: "이슈 추적", desc: "GitHub 스타일\n태스크/이슈 관리", color: C.teal, icon: "📋" },
|
||
{ title: "거버넌스 게이트", desc: "위험 수준에 따른\n사람 승인 워크플로우", color: C.orange, icon: "🔐" },
|
||
];
|
||
|
||
features.forEach((f, i) => {
|
||
const x = 0.25 + i * 2.4;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y: 1.72, w: 2.25, h: 1.95,
|
||
fill: { color: C.white }, line: { color: C.lightGray, width: 1 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y: 1.72, w: 2.25, h: 0.45,
|
||
fill: { color: f.color }, line: { color: f.color }
|
||
});
|
||
slide.addText(`${f.icon} ${f.title}`, {
|
||
x, y: 1.72, w: 2.25, h: 0.45,
|
||
fontSize: 11, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
slide.addText(f.desc, {
|
||
x: x + 0.1, y: 2.22, w: 2.05, h: 1.4,
|
||
fontSize: 11, color: "1E293B", align: "center", valign: "top", fontFace: "Calibri"
|
||
});
|
||
});
|
||
|
||
// GUARDiA 적용 전략
|
||
slide.addText("GUARDiA 적용 전략", {
|
||
x: 0.25, y: 3.8, w: 9.5, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const strategies = [
|
||
["개발 시", "Paperclip CLI로 에이전트 페르소나 정의 (CEO/CTO/Dev/QA)", C.purple],
|
||
["런타임", "AgentEngine (Python) 내장 — 외부 Paperclip 서버 불필요", C.blue],
|
||
["LLM", "Ollama localhost:11434 전용 — 외부 LLM API 완전 차단", C.red],
|
||
];
|
||
|
||
strategies.forEach(([label, desc, color], i) => {
|
||
const x = 0.25 + i * 3.2;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y: 4.2, w: 3.0, h: 1.05,
|
||
fill: { color: C.white }, line: { color: color, width: 2 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addText([
|
||
{ text: label, options: { bold: true, color, breakLine: true } },
|
||
{ text: desc, options: { color: "1E293B" } }
|
||
], {
|
||
x: x + 0.1, y: 4.2, w: 2.8, h: 1.05,
|
||
fontSize: 10.5, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 5: Phase 1 섹션 표지
|
||
// ═══════════════════════════════════════════════════════
|
||
addSectionSlide("1", "Paperclip 개발 도구 설정", "조직도 구성 · 에이전트 페르소나 · 거버넌스 규칙");
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 6: Phase 1 상세
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("03 Phase 1 — Paperclip 개발 도구 설정");
|
||
|
||
// 조직도 트리
|
||
slide.addText("에이전트 조직도", {
|
||
x: 0.25, y: 0.85, w: 4.5, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const orgNodes = [
|
||
{ label: "CEO", x: 1.9, y: 1.3, color: C.purple },
|
||
{ label: "CTO", x: 0.5, y: 2.4, color: C.blue },
|
||
{ label: "PM_AGENT", x: 3.3, y: 2.4, color: C.teal },
|
||
{ label: "DEVELOPER", x: 0.0, y: 3.5, color: C.green },
|
||
{ label: "QA", x: 1.2, y: 3.5, color: C.amber },
|
||
];
|
||
|
||
// 연결선
|
||
slide.addShape(pres.shapes.LINE, { x: 2.4, y: 1.9, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } });
|
||
slide.addShape(pres.shapes.LINE, { x: 1.5, y: 2.25, w: 1.8, h: 0, line: { color: C.gray, width: 1.5 } });
|
||
slide.addShape(pres.shapes.LINE, { x: 1.5, y: 2.25, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } });
|
||
slide.addShape(pres.shapes.LINE, { x: 3.3, y: 2.25, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } });
|
||
slide.addShape(pres.shapes.LINE, { x: 0.65, y: 3.05, w: 1.55, h: 0, line: { color: C.gray, width: 1.5 } });
|
||
slide.addShape(pres.shapes.LINE, { x: 0.65, y: 3.05, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } });
|
||
slide.addShape(pres.shapes.LINE, { x: 1.7, y: 3.05, w: 0, h: 0.5, line: { color: C.gray, width: 1.5 } });
|
||
|
||
orgNodes.forEach(node => {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: node.x, y: node.y, w: 1.2, h: 0.52,
|
||
fill: { color: node.color }, line: { color: node.color },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addText(node.label, {
|
||
x: node.x, y: node.y, w: 1.2, h: 0.52,
|
||
fontSize: 10, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
});
|
||
|
||
// 파일 구조
|
||
slide.addText("생성 파일 구조", {
|
||
x: 5.0, y: 0.85, w: 4.7, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 5.0, y: 1.25, w: 4.7, h: 3.2,
|
||
fill: { color: "1E293B" }, line: { color: "334155", width: 1 }
|
||
});
|
||
slide.addText([
|
||
{ text: "C:\\GUARDiA\\paperclip\\", options: { color: C.lightBlue, bold: true, breakLine: true } },
|
||
{ text: "├─ paperclip.config.json", options: { color: "A3E635", breakLine: true } },
|
||
{ text: "│ (조직도 · LLM 설정 · 거버넌스)", options: { color: "94A3B8", breakLine: true } },
|
||
{ text: "├─ README.md", options: { color: "A3E635", breakLine: true } },
|
||
{ text: "└─ agents/", options: { color: "FB923C", breakLine: true } },
|
||
{ text: " ├─ ceo.md", options: { color: "A3E635", breakLine: true } },
|
||
{ text: " ├─ cto.md", options: { color: "A3E635", breakLine: true } },
|
||
{ text: " ├─ developer.md", options: { color: "A3E635", breakLine: true } },
|
||
{ text: " └─ qa.md", options: { color: "A3E635" } },
|
||
], {
|
||
x: 5.15, y: 1.35, w: 4.4, h: 3.0,
|
||
fontSize: 10.5, valign: "top", fontFace: "Courier New"
|
||
});
|
||
|
||
// 거버넌스 표
|
||
slide.addText("거버넌스 규칙", {
|
||
x: 0.25, y: 4.1, w: 4.5, h: 0.32,
|
||
fontSize: 11, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const govRows = [
|
||
[{ text: "액션", options: { bold: true, fill: { color: C.navy }, color: C.white } },
|
||
{ text: "승인 방식", options: { bold: true, fill: { color: C.navy }, color: C.white } }],
|
||
["code_commit / deploy", "사람 승인 필수"],
|
||
["delete_data", "CEO 승인 필수"],
|
||
["CRITICAL 장애 분류", "담당자 승인 필수"],
|
||
["KB 등록 / SSL SR", "자동 승인"],
|
||
];
|
||
|
||
slide.addTable(govRows, {
|
||
x: 0.25, y: 4.45, w: 4.5, h: 1.0,
|
||
colW: [2.5, 2.0],
|
||
border: { pt: 1, color: C.lightGray },
|
||
fontSize: 10, fontFace: "Calibri",
|
||
fill: { color: C.white }
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 7: Phase 2 섹션 표지
|
||
// ═══════════════════════════════════════════════════════
|
||
addSectionSlide("2", "Ollama 로컬 LLM 설정", "guardia-agent 커스텀 모델 · 보안 온프레미스 추론");
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 8: Phase 2 상세
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("04 Phase 2 — Ollama 로컬 LLM");
|
||
|
||
// 왼쪽: 아키텍처
|
||
slide.addText("추론 아키텍처", {
|
||
x: 0.25, y: 0.85, w: 4.8, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const archItems = [
|
||
{ label: "GUARDiA AgentEngine", color: C.blue, y: 1.3 },
|
||
{ label: "OllamaClient (HTTP)", color: C.teal, y: 2.0 },
|
||
{ label: "Ollama Server :11434", color: C.green, y: 2.7 },
|
||
{ label: "guardia-agent 모델", color: C.purple, y: 3.4 },
|
||
];
|
||
|
||
archItems.forEach((item, i) => {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.5, y: item.y, w: 3.8, h: 0.55,
|
||
fill: { color: item.color }, line: { color: item.color },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addText(item.label, {
|
||
x: 0.5, y: item.y, w: 3.8, h: 0.55,
|
||
fontSize: 12, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
if (i < archItems.length - 1) {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 2.25, y: item.y + 0.55, w: 0.3, h: 0.15,
|
||
fill: { color: C.gray }, line: { color: C.gray }
|
||
});
|
||
}
|
||
});
|
||
|
||
// 보안 배지
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.25, y: 4.1, w: 4.5, h: 0.9,
|
||
fill: { color: "FEF2F2" }, line: { color: C.red, width: 2 }
|
||
});
|
||
slide.addText([
|
||
{ text: "🔒 외부 LLM API 완전 차단", options: { bold: true, color: C.red, breakLine: true } },
|
||
{ text: "모든 AI 추론은 localhost:11434 (Ollama) 만 허용", options: { color: "7F1D1D" } }
|
||
], {
|
||
x: 0.4, y: 4.1, w: 4.3, h: 0.9,
|
||
fontSize: 12, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
|
||
// 오른쪽: 모델 설정
|
||
slide.addText("guardia-agent 모델 파라미터", {
|
||
x: 5.05, y: 0.85, w: 4.7, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 5.05, y: 1.25, w: 4.7, h: 2.0,
|
||
fill: { color: "1E293B" }, line: { color: "334155", width: 1 }
|
||
});
|
||
slide.addText([
|
||
{ text: "FROM llama3.1:8b", options: { color: "A3E635", breakLine: true } },
|
||
{ text: "SYSTEM \"\"\"", options: { color: "FB923C", breakLine: true } },
|
||
{ text: " GUARDiA ITSM AI 운영 에이전트", options: { color: "94A3B8", breakLine: true } },
|
||
{ text: " 한국어 응답 · ITSM 전문화", options: { color: "94A3B8", breakLine: true } },
|
||
{ text: " 외부 API 호출 금지", options: { color: C.red, breakLine: true } },
|
||
{ text: "\"\"\"", options: { color: "FB923C", breakLine: true } },
|
||
{ text: "PARAMETER temperature 0.2", options: { color: "38BDF8", breakLine: true } },
|
||
{ text: "PARAMETER num_predict 2048", options: { color: "38BDF8" } },
|
||
], {
|
||
x: 5.2, y: 1.35, w: 4.4, h: 1.8,
|
||
fontSize: 10.5, valign: "top", fontFace: "Courier New"
|
||
});
|
||
|
||
// OllamaClient API
|
||
slide.addText("OllamaClient API", {
|
||
x: 5.05, y: 3.35, w: 4.7, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const methods = [
|
||
["health_check()", "서버 상태 확인"],
|
||
["resolve_model(preferred)", "fallback 모델 선택"],
|
||
["json_generate(prompt)", "JSON 추출 (코드블록 자동 제거)"],
|
||
["pull_model(model)", "모델 다운로드"],
|
||
];
|
||
|
||
methods.forEach(([method, desc], i) => {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 5.05, y: 3.75 + i * 0.43, w: 4.7, h: 0.38,
|
||
fill: { color: i % 2 === 0 ? C.offWhite : C.white },
|
||
line: { color: C.lightGray, width: 1 }
|
||
});
|
||
slide.addText([
|
||
{ text: method, options: { bold: true, color: C.blue } },
|
||
{ text: " → " + desc, options: { color: C.gray } }
|
||
], {
|
||
x: 5.15, y: 3.75 + i * 0.43, w: 4.5, h: 0.38,
|
||
fontSize: 10, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 9: Phase 3 섹션 표지
|
||
// ═══════════════════════════════════════════════════════
|
||
addSectionSlide("3", "GUARDiA 에이전트 엔진", "6종 자율 에이전트 · 하트비트 사이클 · 승인 게이트");
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 10: Phase 3 — 에이전트 종류
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("05 Phase 3 — 6종 에이전트 역할");
|
||
|
||
const agents = [
|
||
{
|
||
role: "INCIDENT_TRIAGE",
|
||
schedule: "매 15분",
|
||
color: C.orange,
|
||
lines: ["미배정 장애 자동 분류", "severity / category / reason", "CRITICAL → 사람 승인"]
|
||
},
|
||
{
|
||
role: "KB_CURATOR",
|
||
schedule: "매시간 정각",
|
||
color: C.teal,
|
||
lines: ["완료 SR → KB 자동 생성", "증상·원인·해결책 초안", "published=False (검토 대기)"]
|
||
},
|
||
{
|
||
role: "SSL_WATCHER",
|
||
schedule: "매일 08:30",
|
||
color: C.red,
|
||
lines: ["SSL 만료 0~30일 서버 감시", "자동 갱신 SR 생성", "7일↓=CRITICAL, 30일↓=HIGH"]
|
||
},
|
||
{
|
||
role: "WBS_MONITOR",
|
||
schedule: "매일 08:00",
|
||
color: C.blue,
|
||
lines: ["SI WBS 지연 감지", "3일+ 주의, 10일+ CRITICAL", "리스크 자동 등록"]
|
||
},
|
||
{
|
||
role: "PM_SUGGESTER",
|
||
schedule: "매일 09:00",
|
||
color: C.green,
|
||
lines: ["PM 미등록 서버 탐지", "권장 점검 일정 제안", "AgentTask 기록"]
|
||
},
|
||
{
|
||
role: "DEVELOPER",
|
||
schedule: "수동 트리거",
|
||
color: C.purple,
|
||
lines: ["PENDING 태스크 처리", "LLM 코드 생성", "CODE_CHANGE → 사람 승인"]
|
||
},
|
||
];
|
||
|
||
agents.forEach((a, i) => {
|
||
const col = i % 3;
|
||
const row = Math.floor(i / 3);
|
||
const x = 0.25 + col * 3.25;
|
||
const y = 0.88 + row * 2.35;
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 3.1, h: 2.18,
|
||
fill: { color: C.white }, line: { color: C.lightGray, width: 1 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 3.1, h: 0.5,
|
||
fill: { color: a.color }, line: { color: a.color }
|
||
});
|
||
slide.addText(a.role, {
|
||
x, y, w: 2.3, h: 0.5,
|
||
fontSize: 11, color: C.white, bold: true,
|
||
align: "left", valign: "middle", fontFace: "Calibri",
|
||
indent: 0.1, margin: 0
|
||
});
|
||
slide.addText(a.schedule, {
|
||
x: x + 2.3, y, w: 0.8, h: 0.5,
|
||
fontSize: 9, color: "FFEAA7",
|
||
align: "center", valign: "middle", fontFace: "Calibri", margin: 0
|
||
});
|
||
|
||
a.lines.forEach((line, li) => {
|
||
slide.addText([
|
||
{ text: "▸ ", options: { color: a.color, bold: true } },
|
||
{ text: line, options: { color: "1E293B" } }
|
||
], {
|
||
x: x + 0.1, y: y + 0.55 + li * 0.52, w: 2.9, h: 0.48,
|
||
fontSize: 10.5, valign: "top", fontFace: "Calibri"
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 11: Phase 3 — 하트비트 & 승인 게이트
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("05 Phase 3 — 하트비트 & 승인 게이트");
|
||
|
||
// 하트비트 플로우
|
||
slide.addText("하트비트 사이클", {
|
||
x: 0.25, y: 0.85, w: 4.7, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const flowSteps = [
|
||
{ label: "APScheduler\nCron 트리거", color: C.blue, y: 1.3 },
|
||
{ label: "status = ACTIVE", color: C.teal, y: 2.05 },
|
||
{ label: "Ollama\nhealth_check()", color: C.green, y: 2.8 },
|
||
{ label: "_handler() 실행", color: C.orange, y: 3.55 },
|
||
{ label: "status = IDLE\n(완료)", color: C.purple, y: 4.3 },
|
||
];
|
||
|
||
flowSteps.forEach((step, i) => {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.5, y: step.y, w: 3.0, h: 0.6,
|
||
fill: { color: step.color }, line: { color: step.color },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addText(step.label, {
|
||
x: 0.5, y: step.y, w: 3.0, h: 0.6,
|
||
fontSize: 10.5, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
if (i < flowSteps.length - 1) {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 1.85, y: step.y + 0.6, w: 0.3, h: 0.15,
|
||
fill: { color: C.gray }, line: { color: C.gray }
|
||
});
|
||
}
|
||
});
|
||
|
||
// 오른쪽: 승인 게이트
|
||
slide.addText("승인 게이트 설계", {
|
||
x: 4.0, y: 0.85, w: 5.7, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
// AUTO_APPROVED
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 4.0, y: 1.25, w: 5.7, h: 1.3,
|
||
fill: { color: "F0FDF4" }, line: { color: C.green, width: 2 }
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 4.0, y: 1.25, w: 0.1, h: 1.3,
|
||
fill: { color: C.green }, line: { color: C.green }
|
||
});
|
||
slide.addText([
|
||
{ text: "✅ AUTO_APPROVED ", options: { bold: true, color: C.green } },
|
||
{ text: "— 사람 개입 없이 즉시 실행", options: { color: "166534" } }
|
||
], {
|
||
x: 4.18, y: 1.3, w: 5.4, h: 0.35,
|
||
fontSize: 12, fontFace: "Calibri"
|
||
});
|
||
slide.addText([
|
||
{ text: "• KB 등록 초안 생성", options: { breakLine: true } },
|
||
{ text: "• SSL 갱신 SR 생성 ", options: { breakLine: true } },
|
||
{ text: "• PM 일정 제안 ", options: { breakLine: true } },
|
||
{ text: "• WBS 리스크 등록 (일반)", options: {} },
|
||
], {
|
||
x: 4.18, y: 1.68, w: 5.4, h: 0.82,
|
||
fontSize: 10.5, color: "166534", valign: "top", fontFace: "Calibri"
|
||
});
|
||
|
||
// PENDING (사람 승인)
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 4.0, y: 2.7, w: 5.7, h: 1.5,
|
||
fill: { color: "FFF7ED" }, line: { color: C.red, width: 2 }
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 4.0, y: 2.7, w: 0.1, h: 1.5,
|
||
fill: { color: C.red }, line: { color: C.red }
|
||
});
|
||
slide.addText([
|
||
{ text: "⏳ PENDING ", options: { bold: true, color: C.red } },
|
||
{ text: "— 담당자 승인 후 실행", options: { color: "7C2D12" } }
|
||
], {
|
||
x: 4.18, y: 2.75, w: 5.4, h: 0.35,
|
||
fontSize: 12, fontFace: "Calibri"
|
||
});
|
||
slide.addText([
|
||
{ text: "• CRITICAL 장애 분류 적용", options: { breakLine: true } },
|
||
{ text: "• CODE_CHANGE (개발자 에이전트)", options: { breakLine: true } },
|
||
{ text: "• CRITICAL 리스크 등록 (WBS 10일+ 지연)", options: { breakLine: true } },
|
||
{ text: "• code_commit / deploy / delete_data", options: {} }
|
||
], {
|
||
x: 4.18, y: 3.12, w: 5.4, h: 1.0,
|
||
fontSize: 10.5, color: "7C2D12", valign: "top", fontFace: "Calibri"
|
||
});
|
||
|
||
// 데이터 모델 요약
|
||
slide.addText("DB 테이블 구조", {
|
||
x: 4.0, y: 4.35, w: 5.7, h: 0.32,
|
||
fontSize: 11, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const tables = [
|
||
["tb_agent_config", "에이전트 설정 · 상태 · 통계"],
|
||
["tb_agent_task", "실행된 태스크 · LLM 입출력"],
|
||
["tb_agent_approval", "승인 대기·완료 기록"],
|
||
];
|
||
tables.forEach(([t, d], i) => {
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 4.0 + i * 1.9, y: 4.7, w: 1.8, h: 0.7,
|
||
fill: { color: C.navy }, line: { color: C.navy },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addText([
|
||
{ text: t, options: { bold: true, color: C.ice, breakLine: true } },
|
||
{ text: d, options: { color: "8FAADC" } }
|
||
], {
|
||
x: 4.05 + i * 1.9, y: 4.72, w: 1.7, h: 0.66,
|
||
fontSize: 9, align: "center", valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 12: Phase 4 섹션 표지
|
||
// ═══════════════════════════════════════════════════════
|
||
addSectionSlide("4", "자율 운영 대시보드", "agents.html SPA · 실시간 상태 · 승인 워크플로우");
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 13: Phase 4 상세
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("06 Phase 4 — 자율 운영 대시보드");
|
||
|
||
// 접근 경로 배너
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.25, y: 0.85, w: 9.5, h: 0.5,
|
||
fill: { color: C.navy }, line: { color: C.navy }
|
||
});
|
||
slide.addText([
|
||
{ text: "접근: ", options: { color: C.ice } },
|
||
{ text: "http://localhost:8001/agents", options: { bold: true, color: "A3E635" } },
|
||
{ text: " | 자동 갱신: 30초 간격", options: { color: C.ice } }
|
||
], {
|
||
x: 0.35, y: 0.85, w: 9.3, h: 0.5,
|
||
fontSize: 12, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
|
||
// 대시보드 구성 카드
|
||
const panels = [
|
||
{ title: "LLM 상태 배너", desc: "Ollama 온라인/오프라인\n실시간 연결 상태 표시", color: C.teal },
|
||
{ title: "통계 카드 (5종)", desc: "총 에이전트·활성·오늘\n태스크·토큰·승인 대기", color: C.blue },
|
||
{ title: "에이전트 탭", desc: "역할 배지 + 상태 펄스\n하트비트/정지/재개 버튼", color: C.purple },
|
||
{ title: "조직도 탭", desc: "계층적 트리 렌더링\n에이전트 관계 시각화", color: C.orange },
|
||
{ title: "승인 대기 탭", desc: "PENDING 승인 목록\nCRITICAL 강조 표시", color: C.red },
|
||
{ title: "태스크 피드 탭", desc: "전체 에이전트 태스크\n실시간 피드 통합 뷰", color: C.green },
|
||
];
|
||
|
||
panels.forEach((p, i) => {
|
||
const col = i % 3;
|
||
const row = Math.floor(i / 3);
|
||
const x = 0.25 + col * 3.25;
|
||
const y = 1.48 + row * 1.85;
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 3.1, h: 1.65,
|
||
fill: { color: C.white }, line: { color: C.lightGray, width: 1 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 3.1, h: 0.4,
|
||
fill: { color: p.color }, line: { color: p.color }
|
||
});
|
||
slide.addText(p.title, {
|
||
x, y, w: 3.1, h: 0.4,
|
||
fontSize: 11, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
slide.addText(p.desc, {
|
||
x: x + 0.1, y: y + 0.45, w: 2.9, h: 1.15,
|
||
fontSize: 11, color: "1E293B", align: "center", valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
|
||
// API 엔드포인트 수
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.25, y: 5.15, w: 9.5, h: 0.35,
|
||
fill: { color: "EEF2FF" }, line: { color: C.lightBlue, width: 1 }
|
||
});
|
||
slide.addText("REST API: 16개 엔드포인트 | GET·POST·PATCH·DELETE | CUSTOMER 역할 전체 차단 | RBAC 기반 권한 제어", {
|
||
x: 0.35, y: 5.15, w: 9.3, h: 0.35,
|
||
fontSize: 10.5, color: C.blue, align: "center", valign: "middle", fontFace: "Calibri"
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 14: 테스트 결과
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("07 테스트 결과 요약");
|
||
|
||
// 구문 검사 결과
|
||
slide.addText("구문 검사 (Python Syntax Check)", {
|
||
x: 0.25, y: 0.85, w: 9.5, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const syntaxFiles = [
|
||
"models.py", "main.py", "core/llm_client.py",
|
||
"core/agents.py", "core/scheduler.py", "routers/agents.py"
|
||
];
|
||
|
||
syntaxFiles.forEach((f, i) => {
|
||
const x = 0.25 + (i % 3) * 3.25;
|
||
const y = 1.25 + Math.floor(i / 3) * 0.6;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 3.1, h: 0.5,
|
||
fill: { color: "F0FDF4" }, line: { color: C.green, width: 1 }
|
||
});
|
||
slide.addText([
|
||
{ text: "✅ ", options: { color: C.green, bold: true } },
|
||
{ text: f, options: { color: "1E293B" } }
|
||
], {
|
||
x: x + 0.1, y, w: 2.9, h: 0.5,
|
||
fontSize: 11, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
|
||
// 심볼 검증
|
||
slide.addText("심볼 검증", {
|
||
x: 0.25, y: 2.55, w: 4.7, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const symbols = [
|
||
["AgentRole enum", "10가지 역할", C.purple],
|
||
["AgentEngine 핸들러", "6개 구현", C.orange],
|
||
["OllamaClient 메서드", "7개 구현", C.teal],
|
||
["API 엔드포인트", "16개 라우터", C.blue],
|
||
["스케줄러 잡", "9개 (기존4+신규5)", C.green],
|
||
["main.py 라우터", "34개 등록", C.navy],
|
||
];
|
||
|
||
symbols.forEach((s, i) => {
|
||
const x = 0.25 + (i % 2) * 2.45;
|
||
const y = 2.95 + Math.floor(i / 2) * 0.6;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 2.3, h: 0.52,
|
||
fill: { color: C.white }, line: { color: s[2], width: 2 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addText([
|
||
{ text: s[0] + "\n", options: { bold: true, color: "1E293B", fontSize: 10 } },
|
||
{ text: s[1], options: { color: s[2], bold: true, fontSize: 11 } }
|
||
], {
|
||
x: x + 0.08, y, w: 2.14, h: 0.52,
|
||
valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
|
||
// 스케줄러 잡 목록
|
||
slide.addText("스케줄러 잡 (총 9개)", {
|
||
x: 5.2, y: 2.55, w: 4.5, h: 0.35,
|
||
fontSize: 13, color: C.navy, bold: true, fontFace: "Calibri"
|
||
});
|
||
|
||
const jobs = [
|
||
["cert_check", "매일 09:00", C.gray],
|
||
["pm_check", "매일 09:05", C.gray],
|
||
["on_call_notify", "매일 08:55", C.gray],
|
||
["batch_cleanup", "매일 02:00", C.gray],
|
||
["agent_incident_triage", "매 15분", C.orange],
|
||
["agent_kb_curator", "매시간", C.teal],
|
||
["agent_ssl_watcher", "매일 08:30", C.red],
|
||
["agent_wbs_monitor", "매일 08:00", C.blue],
|
||
["agent_pm_suggester", "매일 09:00", C.green],
|
||
];
|
||
|
||
jobs.forEach((j, i) => {
|
||
const y = 2.95 + i * 0.3;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 5.2, y, w: 4.5, h: 0.27,
|
||
fill: { color: i < 4 ? C.offWhite : "F0FDF4" },
|
||
line: { color: C.lightGray, width: 1 }
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 5.2, y, w: 0.07, h: 0.27,
|
||
fill: { color: j[2] }, line: { color: j[2] }
|
||
});
|
||
slide.addText([
|
||
{ text: j[0], options: { bold: i >= 4, color: i >= 4 ? "1E293B" : C.gray } },
|
||
{ text: " " + j[1], options: { color: j[2], bold: i >= 4 } }
|
||
], {
|
||
x: 5.32, y, w: 4.3, h: 0.27,
|
||
fontSize: 9.5, valign: "middle", fontFace: "Calibri"
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 15: 보안 제약사항
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("08 보안 제약사항");
|
||
|
||
const secRules = [
|
||
{
|
||
cat: "LLM 보안",
|
||
color: C.red,
|
||
rules: [
|
||
"외부 LLM API 완전 차단 (OpenAI/Claude 등)",
|
||
"Ollama localhost:11434 전용 사용",
|
||
"temperature 0.2 고정 (결정론적 응답)",
|
||
"HTTP 요청 30초 타임아웃"
|
||
]
|
||
},
|
||
{
|
||
cat: "서버 정보 보호",
|
||
color: C.orange,
|
||
rules: [
|
||
"ip_addr — ServerOut 스키마 미포함",
|
||
"ssh_user — API 응답 절대 미노출",
|
||
"os_pw_enc — AES-256-GCM 암호화 필수",
|
||
"file_path — API 응답 완전 제거"
|
||
]
|
||
},
|
||
{
|
||
cat: "에이전트 액션 제어",
|
||
color: C.purple,
|
||
rules: [
|
||
"CRITICAL 액션 → 사람 승인 후 실행",
|
||
"위험 명령어 차단: rm -rf /, shutdown, mkfs",
|
||
"fork bomb 패턴 감지 & 차단",
|
||
"경로 순회 방지 (resolve().relative_to)"
|
||
]
|
||
},
|
||
{
|
||
cat: "접근 제어 (RBAC)",
|
||
color: C.blue,
|
||
rules: [
|
||
"CUSTOMER 역할 — 에이전트 API 전체 차단",
|
||
"에이전트 생성/수정/삭제 — ADMIN만 허용",
|
||
"승인 처리 — USER 이상 허용",
|
||
"스택트레이스 — API 응답 노출 금지"
|
||
]
|
||
}
|
||
];
|
||
|
||
secRules.forEach((sec, i) => {
|
||
const x = 0.25 + (i % 2) * 4.9;
|
||
const y = 0.88 + Math.floor(i / 2) * 2.35;
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 4.65, h: 2.2,
|
||
fill: { color: C.white }, line: { color: C.lightGray, width: 1 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y, w: 4.65, h: 0.42,
|
||
fill: { color: sec.color }, line: { color: sec.color }
|
||
});
|
||
slide.addText(sec.cat, {
|
||
x, y, w: 4.65, h: 0.42,
|
||
fontSize: 12, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
|
||
const textArr = sec.rules.map((r, ri) => ({
|
||
text: "▸ " + r,
|
||
options: {
|
||
color: ri === 0 ? "0F172A" : "1E293B",
|
||
bold: ri === 0,
|
||
fontSize: 10.5,
|
||
breakLine: ri < sec.rules.length - 1
|
||
}
|
||
}));
|
||
slide.addText(textArr, {
|
||
x: x + 0.12, y: y + 0.48, w: 4.4, h: 1.65,
|
||
valign: "top", fontFace: "Calibri"
|
||
});
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 16: 향후 로드맵
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addContentSlide("08 향후 로드맵");
|
||
|
||
const roadmap = [
|
||
{ ver: "v1.1", period: "2026 Q3", title: "에이전트 간 메시지 전달", desc: "CEO→CTO 위임 워크플로우\n태스크 자동 위임 체계", color: C.blue },
|
||
{ ver: "v1.2", period: "2026 Q3", title: "에이전트 학습", desc: "이전 태스크 기반 파인튜닝\n조직 맞춤형 응답 최적화", color: C.teal },
|
||
{ ver: "v1.3", period: "2026 Q4", title: "멀티모달 지원", desc: "스크린샷·로그 이미지 분석\n시각 장애 자동 탐지", color: C.green },
|
||
{ ver: "v2.0", period: "2027 Q1", title: "에이전트 마켓플레이스", desc: "커뮤니티 에이전트 공유\nPlug-in 방식 등록/사용", color: C.purple },
|
||
{ ver: "v2.1", period: "2027 Q1", title: "엣지 배포", desc: "경량 모델 (llama3.2:1b)\n저사양 서버 지원", color: C.orange },
|
||
];
|
||
|
||
roadmap.forEach((r, i) => {
|
||
const x = 0.25 + i * 1.92;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y: 0.88, w: 1.75, h: 4.5,
|
||
fill: { color: C.white }, line: { color: C.lightGray, width: 1 },
|
||
shadow: makeShadow()
|
||
});
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y: 0.88, w: 1.75, h: 0.55,
|
||
fill: { color: r.color }, line: { color: r.color }
|
||
});
|
||
slide.addText(r.ver, {
|
||
x, y: 0.88, w: 1.75, h: 0.55,
|
||
fontSize: 16, color: C.white, bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: x + 0.1, y: 1.5, w: 1.55, h: 0.38,
|
||
fill: { color: r.color, transparency: 80 }, line: { color: r.color, width: 1 }
|
||
});
|
||
slide.addText(r.period, {
|
||
x: x + 0.1, y: 1.5, w: 1.55, h: 0.38,
|
||
fontSize: 10, color: "1E293B", bold: true,
|
||
align: "center", valign: "middle", margin: 0, fontFace: "Calibri"
|
||
});
|
||
|
||
slide.addText(r.title, {
|
||
x: x + 0.1, y: 2.0, w: 1.55, h: 0.65,
|
||
fontSize: 11, color: "0F172A", bold: true,
|
||
align: "center", fontFace: "Calibri"
|
||
});
|
||
|
||
slide.addShape(pres.shapes.LINE, {
|
||
x: x + 0.2, y: 2.7, w: 1.35, h: 0,
|
||
line: { color: C.lightGray, width: 1 }
|
||
});
|
||
|
||
slide.addText(r.desc, {
|
||
x: x + 0.1, y: 2.75, w: 1.55, h: 1.55,
|
||
fontSize: 10.5, color: C.gray,
|
||
align: "center", valign: "top", fontFace: "Calibri"
|
||
});
|
||
});
|
||
|
||
// 하단 현재 완료 배너
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x: 0.25, y: 5.1, w: 9.5, h: 0.38,
|
||
fill: { color: C.green }, line: { color: C.green }
|
||
});
|
||
slide.addText("현재 완료: Phase 1~4 | 6종 에이전트 | 16개 API | 9개 스케줄러 잡 | 자율 운영 대시보드", {
|
||
x: 0.35, y: 5.1, w: 9.3, h: 0.38,
|
||
fontSize: 11, color: C.white, bold: true,
|
||
align: "center", valign: "middle", fontFace: "Calibri"
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Slide 17: 마무리
|
||
// ═══════════════════════════════════════════════════════
|
||
{
|
||
const slide = addTitleSlide(
|
||
C.navyDark,
|
||
"GUARDiA × Paperclip\n구현 완료",
|
||
"Phase 1~4 완료 | 온프레미스 AI 자율 운영 체계 구축",
|
||
"완료"
|
||
);
|
||
|
||
// 요약 통계 카드
|
||
const stats = [
|
||
{ num: "6", label: "자율 에이전트", color: C.orange },
|
||
{ num: "16", label: "REST API 엔드포인트", color: C.blue },
|
||
{ num: "9", label: "스케줄러 잡", color: C.green },
|
||
{ num: "100%", label: "구문 검사 통과", color: C.teal },
|
||
];
|
||
|
||
stats.forEach((s, i) => {
|
||
const x = 0.4 + i * 2.3;
|
||
slide.addShape(pres.shapes.RECTANGLE, {
|
||
x, y: 4.05, w: 2.1, h: 1.3,
|
||
fill: { color: s.color, transparency: 15 },
|
||
line: { color: s.color, width: 2 }
|
||
});
|
||
slide.addText(s.num, {
|
||
x, y: 4.08, w: 2.1, h: 0.7,
|
||
fontSize: 28, color: C.white, bold: true,
|
||
align: "center", valign: "middle", fontFace: "Calibri", margin: 0
|
||
});
|
||
slide.addText(s.label, {
|
||
x, y: 4.75, w: 2.1, h: 0.55,
|
||
fontSize: 10, color: C.ice,
|
||
align: "center", valign: "top", fontFace: "Calibri", margin: 0
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── 파일 저장 ───────────────────────────────────────
|
||
pres.writeFile({ fileName: "C:/GUARDiA/manual/GUARDiA_Paperclip_AI에이전트_구현보고서.pptx" })
|
||
.then(() => console.log("PPT 생성 완료: GUARDiA_Paperclip_AI에이전트_구현보고서.pptx"))
|
||
.catch(err => { console.error("PPT 생성 실패:", err); process.exit(1); });
|