const path = require('path'); module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); const PptxGenJS = require('pptxgenjs'); async function main() { const pres = new PptxGenJS(); pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); pres.layout = 'CUSTOM_16x9'; // === 컬러 팔레트 === const C = { navy: '0f172a', navyLight: '1e293b', navyMid: '334155', blue: '3b82f6', blueLight: '60a5fa', blueDark: '1d4ed8', blueBg: '1e3a5f', white: 'FFFFFF', gray100: 'f1f5f9', gray200: 'e2e8f0', gray300: 'cbd5e1', gray400: '94a3b8', gray500: '64748b', gray600: '475569', gray700: '334155', red: 'ef4444', redBg: 'fef2f2', redLight: 'fca5a5', green: '22c55e', greenBg: 'f0fdf4', greenLight: '86efac', amber: 'f59e0b', amberBg: 'fffbeb', purple: 'a855f7', purpleBg: 'faf5ff', cyan: '06b6d4', cyanBg: 'ecfeff', }; const biLogoPath = '/home/aweso/sam/docs/assets/bi/sam_bi_white.png'; // === 공통 푸터 함수 === function addFooter(slide, pageNum, totalPages) { // 하단 라인 slide.addShape(pres.ShapeType.rect, { x: 0.5, y: 5.15, w: 9, h: 0.01, fill: { color: C.navyMid } }); slide.addText('SAM 이메일 정책 | (주)코드브릿지엑스', { x: 0.5, y: 5.2, w: 7, h: 0.3, fontSize: 7, color: C.gray500, fontFace: 'Arial' }); slide.addText(`${pageNum} / ${totalPages}`, { x: 7.5, y: 5.2, w: 2, h: 0.3, fontSize: 7, color: C.gray500, align: 'right', fontFace: 'Arial' }); } // === 공통 페이지 헤더 === function addPageHeader(slide, title, subtitle) { slide.background = { fill: C.white }; // 상단 네이비 바 slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.blue } }); // 제목 액센트 바 slide.addShape(pres.ShapeType.rect, { x: 0.5, y: 0.4, w: 0.06, h: 0.4, fill: { color: C.blue } }); slide.addText(title, { x: 0.72, y: 0.35, w: 6, h: 0.5, fontSize: 20, bold: true, color: C.navy, fontFace: 'Arial' }); if (subtitle) { slide.addText(subtitle, { x: 0.72, y: 0.8, w: 8, h: 0.3, fontSize: 10, color: C.gray500, fontFace: 'Arial' }); } } // === 넘버 서클 === function addNumberCircle(slide, num, x, y, color) { slide.addShape(pres.ShapeType.ellipse, { x, y, w: 0.35, h: 0.35, fill: { color: color || C.blue } }); slide.addText(String(num), { x, y, w: 0.35, h: 0.35, fontSize: 12, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); } const TOTAL = 12; // ============================== // 슬라이드 1: 표지 // ============================== const s1 = pres.addSlide(); s1.background = { fill: C.navy }; // 상단 그라데이션 효과 (가상) s1.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 2, fill: { color: C.navyLight } }); s1.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.08, fill: { color: C.blue } }); // BI 로고 s1.addImage({ path: biLogoPath, x: 0.7, y: 0.4, w: 1.6, h: 0.7 }); // 메인 제목 s1.addText('멀티테넌시 이메일 정책', { x: 0.7, y: 2.2, w: 8.5, h: 0.8, fontSize: 36, bold: true, color: C.white, fontFace: 'Arial' }); // 서브 제목 s1.addText('SAM Email System Architecture & Policy', { x: 0.7, y: 2.95, w: 8.5, h: 0.5, fontSize: 16, color: C.blueLight, fontFace: 'Arial' }); // 구분선 s1.addShape(pres.ShapeType.rect, { x: 0.7, y: 3.6, w: 2.5, h: 0.03, fill: { color: C.blue } }); // 메타 정보 s1.addText([ { text: '2026-03-11', options: { fontSize: 11, color: C.gray400 } }, { text: ' | ', options: { fontSize: 11, color: C.navyMid } }, { text: '개발팀 내부 공유', options: { fontSize: 11, color: C.gray400 } }, ], { x: 0.7, y: 3.85, w: 6, h: 0.4, fontFace: 'Arial' }); // 하단 s1.addText('(주)코드브릿지엑스', { x: 0.7, y: 5.0, w: 5, h: 0.3, fontSize: 9, color: C.gray500, fontFace: 'Arial' }); // ============================== // 슬라이드 2: 현재 문제점 (AS-IS) // ============================== const s2 = pres.addSlide(); addPageHeader(s2, '현재 문제점 (AS-IS)', '기존 이메일 시스템의 한계'); const problems = [ { icon: '1', title: '단일 SMTP 설정', desc: '.env에 고정된 Gmail SMTP\nGmail 일일 500건 제한에 전체 영향', color: C.red, bg: C.redBg }, { icon: '2', title: '단일 발신 주소', desc: 'develop@codebridge-x.com 하나로\n모든 테넌트 메일 발송', color: C.red, bg: C.redBg }, { icon: '3', title: '발송 기록 없음', desc: '메일 발송 이력 추적 불가\n장애 시 원인 파악 어려움', color: C.amber, bg: C.amberBg }, { icon: '4', title: 'Mailable 중복', desc: 'EsignRequestMail이\nAPI + MNG 양쪽에 존재', color: C.amber, bg: C.amberBg }, { icon: '5', title: '템플릿 하드코딩', desc: '테넌트별 로고/서명/컬러\n커스터마이징 불가', color: C.amber, bg: C.amberBg }, { icon: '6', title: 'Mail::to() 직접 호출', desc: 'TenantMailService 없이\n각 컨트롤러에서 직접 발송', color: C.purple, bg: C.purpleBg }, ]; problems.forEach((p, i) => { const col = i % 3; const row = Math.floor(i / 3); const x = 0.5 + col * 3.1; const y = 1.3 + row * 1.95; // 카드 배경 s2.addShape(pres.ShapeType.roundRect, { x, y, w: 2.9, h: 1.75, rectRadius: 0.1, fill: { color: p.bg }, line: { color: p.color, width: 0.5, dashType: 'solid' } }); // 넘버 원 addNumberCircle(s2, p.icon, x + 0.15, y + 0.15, p.color); // 제목 s2.addText(p.title, { x: x + 0.6, y: y + 0.15, w: 2.1, h: 0.35, fontSize: 11, bold: true, color: p.color, fontFace: 'Arial' }); // 설명 s2.addText(p.desc, { x: x + 0.2, y: y + 0.6, w: 2.5, h: 0.9, fontSize: 9, color: C.gray600, fontFace: 'Arial', lineSpacingMultiple: 1.3 }); }); addFooter(s2, 2, TOTAL); // ============================== // 슬라이드 3: 목표 아키텍처 (TO-BE) // ============================== const s3 = pres.addSlide(); addPageHeader(s3, '목표 아키텍처 (TO-BE)', '3-Layer 구조로 테넌트 격리, 브랜딩, 추적을 구현'); // Layer 1 s3.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 1.4, w: 9, h: 1.0, rectRadius: 0.12, fill: { color: 'eff6ff' }, line: { color: C.blue, width: 1.5 } }); s3.addText('Layer 1', { x: 0.7, y: 1.45, w: 1.2, h: 0.3, fontSize: 8, bold: true, color: C.white, fontFace: 'Arial' }); s3.addShape(pres.ShapeType.roundRect, { x: 0.7, y: 1.45, w: 0.7, h: 0.25, rectRadius: 0.04, fill: { color: C.blue } }); s3.addText('Layer 1', { x: 0.7, y: 1.45, w: 0.7, h: 0.25, fontSize: 8, bold: true, color: C.white, align: 'center', fontFace: 'Arial' }); s3.addText('테넌트 메일 설정 (tenant_mail_configs)', { x: 1.55, y: 1.45, w: 5, h: 0.3, fontSize: 13, bold: true, color: C.blueDark, fontFace: 'Arial' }); s3.addText('SMTP 설정 | 발신자 주소/이름 | 브랜딩 정보 (로고, 컬러, 서명) | Provider 선택', { x: 0.9, y: 1.85, w: 8.2, h: 0.35, fontSize: 9, color: C.gray600, fontFace: 'Arial' }); // 화살표 1 s3.addText('▼', { x: 4.5, y: 2.4, w: 1, h: 0.3, fontSize: 16, color: C.blue, align: 'center', fontFace: 'Arial' }); // Layer 2 s3.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 2.7, w: 9, h: 1.0, rectRadius: 0.12, fill: { color: C.greenBg }, line: { color: C.green, width: 1.5 } }); s3.addShape(pres.ShapeType.roundRect, { x: 0.7, y: 2.75, w: 0.7, h: 0.25, rectRadius: 0.04, fill: { color: C.green } }); s3.addText('Layer 2', { x: 0.7, y: 2.75, w: 0.7, h: 0.25, fontSize: 8, bold: true, color: C.white, align: 'center', fontFace: 'Arial' }); s3.addText('메일 발송 서비스 (TenantMailService)', { x: 1.55, y: 2.75, w: 5, h: 0.3, fontSize: 13, bold: true, color: '166534', fontFace: 'Arial' }); s3.addText('테넌트 설정 자동 적용 | 큐/즉시 발송 | Fallback 전략 | 쿼터 확인', { x: 0.9, y: 3.15, w: 8.2, h: 0.35, fontSize: 9, color: C.gray600, fontFace: 'Arial' }); // 화살표 2 s3.addText('▼', { x: 4.5, y: 3.7, w: 1, h: 0.3, fontSize: 16, color: C.green, align: 'center', fontFace: 'Arial' }); // Layer 3 s3.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 4.0, w: 9, h: 1.0, rectRadius: 0.12, fill: { color: C.purpleBg }, line: { color: C.purple, width: 1.5 } }); s3.addShape(pres.ShapeType.roundRect, { x: 0.7, y: 4.05, w: 0.7, h: 0.25, rectRadius: 0.04, fill: { color: C.purple } }); s3.addText('Layer 3', { x: 0.7, y: 4.05, w: 0.7, h: 0.25, fontSize: 8, bold: true, color: C.white, align: 'center', fontFace: 'Arial' }); s3.addText('발송 기록/추적 (mail_logs)', { x: 1.55, y: 4.05, w: 5, h: 0.3, fontSize: 13, bold: true, color: '7c3aed', fontFace: 'Arial' }); s3.addText('발송 이력 | 상태 추적 (queued/sent/failed/bounced) | 일일 쿼터 관리 | 감사 로그', { x: 0.9, y: 4.45, w: 8.2, h: 0.35, fontSize: 9, color: C.gray600, fontFace: 'Arial' }); addFooter(s3, 3, TOTAL); // ============================== // 슬라이드 4: 테넌트별 메일 설정 // ============================== const s4 = pres.addSlide(); addPageHeader(s4, '테넌트별 메일 설정', 'tenant_mail_configs 테이블 구조'); // 왼쪽: 테이블 구조 s4.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 1.3, w: 4.5, h: 3.7, rectRadius: 0.1, fill: { color: C.navy } }); s4.addText('tenant_mail_configs', { x: 0.7, y: 1.4, w: 4, h: 0.35, fontSize: 12, bold: true, color: C.blueLight, fontFace: 'Courier New' }); // 구분선 s4.addShape(pres.ShapeType.rect, { x: 0.7, y: 1.8, w: 4.1, h: 0.01, fill: { color: C.navyMid } }); const fields = [ ['tenant_id', 'FK', '테넌트 ID'], ['provider', 'ENUM', 'platform / smtp / ses / mailgun'], ['from_address', 'VARCHAR', '발신 이메일 주소'], ['from_name', 'VARCHAR', '발신자 표시명'], ['reply_to', 'VARCHAR', '회신 주소 (선택)'], ['is_verified', 'BOOL', '도메인 검증 여부'], ['daily_limit', 'INT', '일일 발송 한도 (기본 500)'], ['options', 'JSON', 'SMTP 설정, 브랜딩 정보'], ]; fields.forEach((f, i) => { const fy = 1.9 + i * 0.38; s4.addText(f[0], { x: 0.7, y: fy, w: 1.6, h: 0.3, fontSize: 8, color: C.blueLight, fontFace: 'Courier New' }); s4.addText(f[1], { x: 2.35, y: fy, w: 0.75, h: 0.3, fontSize: 7, color: C.gray400, fontFace: 'Arial' }); s4.addText(f[2], { x: 3.15, y: fy, w: 1.8, h: 0.3, fontSize: 7, color: C.gray300, fontFace: 'Arial' }); }); // 오른쪽: Provider 카드 const providers = [ { name: 'Platform', desc: 'SAM 기본 SMTP\n(설정 불필요)', color: C.blue, bg: 'eff6ff' }, { name: 'Custom SMTP', desc: '테넌트 자체\nSMTP 서버', color: C.green, bg: C.greenBg }, { name: 'Amazon SES', desc: 'AWS 대량 발송\n서비스', color: C.amber, bg: C.amberBg }, { name: 'Mailgun', desc: '이메일 API\n서비스', color: C.purple, bg: C.purpleBg }, ]; s4.addText('Provider 선택', { x: 5.3, y: 1.3, w: 4, h: 0.35, fontSize: 12, bold: true, color: C.navy, fontFace: 'Arial' }); providers.forEach((p, i) => { const py = 1.8 + i * 0.85; s4.addShape(pres.ShapeType.roundRect, { x: 5.3, y: py, w: 4.2, h: 0.7, rectRadius: 0.08, fill: { color: p.bg }, line: { color: p.color, width: 0.5 } }); s4.addShape(pres.ShapeType.ellipse, { x: 5.5, y: py + 0.15, w: 0.4, h: 0.4, fill: { color: p.color } }); s4.addText(p.name.charAt(0), { x: 5.5, y: py + 0.15, w: 0.4, h: 0.4, fontSize: 12, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); s4.addText(p.name, { x: 6.1, y: py + 0.05, w: 2, h: 0.3, fontSize: 10, bold: true, color: p.color, fontFace: 'Arial' }); s4.addText(p.desc, { x: 6.1, y: py + 0.3, w: 3.2, h: 0.35, fontSize: 8, color: C.gray600, fontFace: 'Arial' }); }); addFooter(s4, 4, TOTAL); // ============================== // 슬라이드 5: 발송 흐름 // ============================== const s5 = pres.addSlide(); addPageHeader(s5, '발송 흐름', 'TenantMailService를 경유한 중앙 집중 발송'); // 흐름 다이어그램 - 세로 플로우 const flowSteps = [ { label: 'Controller / Service', sub: '메일 발송 요청', color: C.gray600, bg: C.gray100 }, { label: 'TenantMailService::send()', sub: '중앙 발송 서비스', color: C.blue, bg: 'eff6ff' }, { label: '테넌트 설정 조회', sub: 'tenant_mail_configs', color: C.blueDark, bg: 'dbeafe' }, { label: '쿼터 확인', sub: '일일 500건 한도 체크', color: C.amber, bg: C.amberBg }, { label: 'Mailer 동적 구성', sub: 'SMTP/SES/Mailgun 설정 적용', color: C.green, bg: C.greenBg }, { label: '발송 (sync / queue)', sub: 'OTP=즉시, 나머지=큐', color: C.purple, bg: C.purpleBg }, { label: 'mail_logs 기록', sub: 'status: queued → sent', color: C.cyan, bg: C.cyanBg }, ]; flowSteps.forEach((step, i) => { const sy = 1.25 + i * 0.55; // 연결 화살표 if (i > 0) { s5.addText('▼', { x: 2.1, y: sy - 0.2, w: 0.5, h: 0.2, fontSize: 8, color: C.gray400, align: 'center', fontFace: 'Arial' }); } // 넘버 addNumberCircle(s5, i + 1, 0.5, sy + 0.04, step.color); // 박스 s5.addShape(pres.ShapeType.roundRect, { x: 1.0, y: sy, w: 3.7, h: 0.42, rectRadius: 0.06, fill: { color: step.bg }, line: { color: step.color, width: 0.5 } }); s5.addText(step.label, { x: 1.15, y: sy, w: 2.2, h: 0.42, fontSize: 9, bold: true, color: step.color, valign: 'middle', fontFace: 'Arial' }); s5.addText(step.sub, { x: 3.1, y: sy, w: 1.5, h: 0.42, fontSize: 7, color: C.gray500, valign: 'middle', fontFace: 'Arial' }); }); // 오른쪽: Fallback 전략 s5.addShape(pres.ShapeType.roundRect, { x: 5.3, y: 1.25, w: 4.2, h: 3.6, rectRadius: 0.12, fill: { color: C.navy } }); s5.addText('Fallback 전략', { x: 5.5, y: 1.35, w: 3.5, h: 0.35, fontSize: 13, bold: true, color: C.white, fontFace: 'Arial' }); const fallbackItems = [ { step: '1차', desc: '테넌트 자체 SMTP로 발송', status: '시도', color: C.blue }, { step: '성공', desc: 'mail_logs (status: sent)', status: '', color: C.green }, { step: '실패', desc: '플랫폼 기본 SMTP로 재시도', status: 'Fallback', color: C.amber }, { step: '성공', desc: 'mail_logs (fallback_used: true)', status: '', color: C.green }, { step: '실패', desc: 'mail_logs (status: failed)', status: '3회 재시도', color: C.red }, ]; fallbackItems.forEach((item, i) => { const fy = 1.85 + i * 0.6; // 연결선 if (i > 0) { s5.addShape(pres.ShapeType.rect, { x: 6.0, y: fy - 0.15, w: 0.01, h: 0.15, fill: { color: C.navyMid } }); } s5.addShape(pres.ShapeType.roundRect, { x: 5.6, y: fy, w: 0.6, h: 0.35, rectRadius: 0.04, fill: { color: item.color } }); s5.addText(item.step, { x: 5.6, y: fy, w: 0.6, h: 0.35, fontSize: 7, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); s5.addText(item.desc, { x: 6.35, y: fy, w: 2.5, h: 0.35, fontSize: 8, color: C.gray300, valign: 'middle', fontFace: 'Arial' }); if (item.status) { s5.addText(item.status, { x: 8.7, y: fy, w: 0.7, h: 0.35, fontSize: 6, color: item.color, valign: 'middle', fontFace: 'Arial' }); } }); addFooter(s5, 5, TOTAL); // ============================== // 슬라이드 6: 발송 기록 (mail_logs) // ============================== const s6 = pres.addSlide(); addPageHeader(s6, '발송 기록 (mail_logs)', '모든 발송을 기록하여 추적/감사/통계 지원'); // 왼쪽: 테이블 구조 s6.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 1.3, w: 4.5, h: 3.7, rectRadius: 0.1, fill: { color: C.navy } }); s6.addText('mail_logs', { x: 0.7, y: 1.4, w: 3, h: 0.35, fontSize: 12, bold: true, color: C.blueLight, fontFace: 'Courier New' }); s6.addShape(pres.ShapeType.rect, { x: 0.7, y: 1.8, w: 4.1, h: 0.01, fill: { color: C.navyMid } }); const logFields = [ ['tenant_id', '테넌트 ID'], ['mailable_type', 'Mailable 클래스명'], ['to_address', '수신자 이메일'], ['from_address', '실제 발신 주소'], ['subject', '메일 제목'], ['status', 'queued / sent / failed / bounced'], ['sent_at', '발송 시각'], ['options', '에러메시지, 재시도횟수, 관련모델'], ]; logFields.forEach((f, i) => { const fy = 1.9 + i * 0.38; s6.addText(f[0], { x: 0.7, y: fy, w: 1.6, h: 0.3, fontSize: 8, color: C.blueLight, fontFace: 'Courier New' }); s6.addText(f[1], { x: 2.4, y: fy, w: 2.4, h: 0.3, fontSize: 8, color: C.gray300, fontFace: 'Arial' }); }); // 오른쪽: 상태 흐름 + 보안 s6.addText('상태 흐름', { x: 5.3, y: 1.3, w: 3, h: 0.35, fontSize: 12, bold: true, color: C.navy, fontFace: 'Arial' }); const statuses = [ { name: 'queued', desc: '큐에 등록됨', color: C.blue, bg: 'eff6ff' }, { name: 'sent', desc: '발송 완료', color: C.green, bg: C.greenBg }, { name: 'failed', desc: '발송 실패', color: C.red, bg: C.redBg }, { name: 'bounced', desc: '수신 거부/반송', color: C.amber, bg: C.amberBg }, ]; statuses.forEach((st, i) => { const sy = 1.8 + i * 0.55; s6.addShape(pres.ShapeType.roundRect, { x: 5.3, y: sy, w: 4.2, h: 0.42, rectRadius: 0.06, fill: { color: st.bg }, line: { color: st.color, width: 0.5 } }); s6.addShape(pres.ShapeType.ellipse, { x: 5.45, y: sy + 0.08, w: 0.26, h: 0.26, fill: { color: st.color } }); s6.addText(st.name, { x: 5.85, y: sy, w: 1.2, h: 0.42, fontSize: 9, bold: true, color: st.color, valign: 'middle', fontFace: 'Courier New' }); s6.addText(st.desc, { x: 7.2, y: sy, w: 2, h: 0.42, fontSize: 9, color: C.gray600, valign: 'middle', fontFace: 'Arial' }); }); // 보안 주의사항 s6.addShape(pres.ShapeType.roundRect, { x: 5.3, y: 4.15, w: 4.2, h: 0.8, rectRadius: 0.08, fill: { color: C.redBg }, line: { color: C.red, width: 0.5 } }); s6.addText('개인정보 보호', { x: 5.5, y: 4.2, w: 3, h: 0.25, fontSize: 9, bold: true, color: C.red, fontFace: 'Arial' }); s6.addText('mail_logs에 메일 본문(body) 저장 금지\n메타데이터(제목, 수신자, 상태)만 기록', { x: 5.5, y: 4.45, w: 3.8, h: 0.45, fontSize: 8, color: C.gray600, fontFace: 'Arial', lineSpacingMultiple: 1.3 }); addFooter(s6, 6, TOTAL); // ============================== // 슬라이드 7: 메일 타입 정리 // ============================== const s7 = pres.addSlide(); addPageHeader(s7, '메일 타입 정리', '5개 Mailable + sync/queue 구분'); const mailTypes = [ { name: 'EsignRequestMail', cat: '전자계약', trigger: '서명 요청', mode: 'queue', color: C.blue, modeColor: C.green }, { name: 'EsignOtpMail', cat: '전자계약', trigger: 'OTP 인증', mode: 'sync', color: C.blue, modeColor: C.red }, { name: 'EsignCompletedMail', cat: '전자계약', trigger: '서명 완료', mode: 'queue', color: C.blue, modeColor: C.green }, { name: 'UserPasswordMail', cat: '인증', trigger: '계정/비번 초기화', mode: 'sync', color: C.purple, modeColor: C.red }, { name: 'PayslipMail', cat: '급여', trigger: '급여명세서 발송', mode: 'queue', color: C.amber, modeColor: C.green }, ]; // 헤더 행 s7.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 1.3, w: 9, h: 0.45, rectRadius: 0.06, fill: { color: C.navy } }); const headers = [ { text: 'Mailable', x: 0.7, w: 2.5 }, { text: '카테고리', x: 3.3, w: 1.2 }, { text: '트리거', x: 4.6, w: 1.8 }, { text: '발송 모드', x: 6.5, w: 1.0 }, { text: '사유', x: 7.6, w: 1.8 }, ]; headers.forEach(h => { s7.addText(h.text, { x: h.x, y: 1.3, w: h.w, h: 0.45, fontSize: 9, bold: true, color: C.white, valign: 'middle', fontFace: 'Arial' }); }); mailTypes.forEach((mt, i) => { const ry = 1.85 + i * 0.55; // 행 배경 (짝수 행 약간 다르게) s7.addShape(pres.ShapeType.roundRect, { x: 0.5, y: ry, w: 9, h: 0.45, rectRadius: 0.06, fill: { color: i % 2 === 0 ? C.gray100 : C.white }, line: { color: C.gray200, width: 0.3 } }); s7.addText(mt.name, { x: 0.7, y: ry, w: 2.5, h: 0.45, fontSize: 9, bold: true, color: C.navy, valign: 'middle', fontFace: 'Courier New' }); // 카테고리 배지 s7.addShape(pres.ShapeType.roundRect, { x: 3.3, y: ry + 0.08, w: 0.9, h: 0.28, rectRadius: 0.04, fill: { color: mt.color } }); s7.addText(mt.cat, { x: 3.3, y: ry + 0.08, w: 0.9, h: 0.28, fontSize: 7, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); s7.addText(mt.trigger, { x: 4.6, y: ry, w: 1.8, h: 0.45, fontSize: 9, color: C.gray600, valign: 'middle', fontFace: 'Arial' }); // 모드 배지 s7.addShape(pres.ShapeType.roundRect, { x: 6.55, y: ry + 0.08, w: 0.7, h: 0.28, rectRadius: 0.04, fill: { color: mt.modeColor } }); s7.addText(mt.mode, { x: 6.55, y: ry + 0.08, w: 0.7, h: 0.28, fontSize: 7, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); s7.addText(mt.mode === 'sync' ? '시간 민감 (즉시)' : '비동기 발송 가능', { x: 7.6, y: ry, w: 1.8, h: 0.45, fontSize: 8, color: C.gray500, valign: 'middle', fontFace: 'Arial' }); }); // 하단 설명 s7.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 4.5, w: 4.3, h: 0.55, rectRadius: 0.06, fill: { color: C.redBg }, line: { color: C.red, width: 0.5 } }); s7.addText([ { text: 'sync', options: { bold: true, color: C.red, fontSize: 9 } }, { text: ' = Mail::send() 즉시 발송 (OTP, 비밀번호)', options: { color: C.gray600, fontSize: 8 } } ], { x: 0.7, y: 4.5, w: 4, h: 0.55, valign: 'middle', fontFace: 'Arial' }); s7.addShape(pres.ShapeType.roundRect, { x: 5.1, y: 4.5, w: 4.4, h: 0.55, rectRadius: 0.06, fill: { color: C.greenBg }, line: { color: C.green, width: 0.5 } }); s7.addText([ { text: 'queue', options: { bold: true, color: C.green, fontSize: 9 } }, { text: ' = Mail::queue() Supervisor 큐 워커 처리', options: { color: C.gray600, fontSize: 8 } } ], { x: 5.3, y: 4.5, w: 4, h: 0.55, valign: 'middle', fontFace: 'Arial' }); addFooter(s7, 7, TOTAL); // ============================== // 슬라이드 8: 템플릿 브랜딩 // ============================== const s8 = pres.addSlide(); addPageHeader(s8, '템플릿 브랜딩', '테넌트별 로고, 컬러, 서명 커스터마이징'); // 이메일 레이아웃 시각화 // 외곽 (이메일 프레임) s8.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 1.3, w: 4.5, h: 3.8, rectRadius: 0.12, fill: { color: C.white }, line: { color: C.gray300, width: 1 } }); // 헤더 영역 s8.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.3, w: 4.5, h: 0.8, fill: { color: 'eff6ff' } }); s8.addShape(pres.ShapeType.roundRect, { x: 0.8, y: 1.45, w: 0.8, h: 0.5, rectRadius: 0.05, fill: { color: C.blue } }); s8.addText('LOGO', { x: 0.8, y: 1.45, w: 0.8, h: 0.5, fontSize: 8, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); s8.addText('{{ 테넌트명 }}', { x: 1.8, y: 1.45, w: 2.5, h: 0.5, fontSize: 11, bold: true, color: C.navy, valign: 'middle', fontFace: 'Arial' }); // 본문 영역 s8.addText('@yield(\'content\')', { x: 0.8, y: 2.3, w: 3.9, h: 0.35, fontSize: 9, color: C.blue, fontFace: 'Courier New' }); s8.addShape(pres.ShapeType.rect, { x: 0.8, y: 2.7, w: 3.9, h: 0.15, fill: { color: C.gray200 } }); s8.addShape(pres.ShapeType.rect, { x: 0.8, y: 2.95, w: 3.0, h: 0.12, fill: { color: C.gray200 } }); s8.addShape(pres.ShapeType.rect, { x: 0.8, y: 3.15, w: 3.5, h: 0.12, fill: { color: C.gray200 } }); s8.addText('(각 Mailable에서 제공하는 본문)', { x: 0.8, y: 3.4, w: 3.9, h: 0.3, fontSize: 7, italic: true, color: C.gray400, fontFace: 'Arial' }); // 푸터 영역 s8.addShape(pres.ShapeType.rect, { x: 0.5, y: 4.2, w: 4.5, h: 0.01, fill: { color: C.gray300 } }); s8.addText('{{ 회사명 }} | {{ 주소 }} | {{ 연락처 }}', { x: 0.8, y: 4.3, w: 3.9, h: 0.25, fontSize: 7, color: C.gray500, align: 'center', fontFace: 'Arial' }); s8.addText('"SAM 시스템에서 발송된 메일입니다"', { x: 0.8, y: 4.55, w: 3.9, h: 0.25, fontSize: 7, italic: true, color: C.gray400, align: 'center', fontFace: 'Arial' }); // 라벨 // 화살표 + 라벨 s8.addText('헤더 ─', { x: 4.1, y: 1.55, w: 1, h: 0.3, fontSize: 8, color: C.blue, align: 'right', fontFace: 'Arial' }); s8.addText('본문 ─', { x: 4.1, y: 2.9, w: 1, h: 0.3, fontSize: 8, color: C.blue, align: 'right', fontFace: 'Arial' }); s8.addText('푸터 ─', { x: 4.1, y: 4.35, w: 1, h: 0.3, fontSize: 8, color: C.blue, align: 'right', fontFace: 'Arial' }); // 오른쪽: 브랜딩 요소 카드 s8.addText('커스터마이징 요소', { x: 5.3, y: 1.3, w: 4, h: 0.35, fontSize: 12, bold: true, color: C.navy, fontFace: 'Arial' }); const brandItems = [ { icon: 'IMG', label: '로고 이미지', key: 'options.branding.logo_url', def: 'SAM BI 로고' }, { icon: 'Co.', label: '회사명', key: 'options.branding.company_name', def: '(주)코드브릿지엑스' }, { icon: 'Adr', label: '주소', key: 'options.branding.company_address', def: '(없음)' }, { icon: 'Tel', label: '연락처', key: 'options.branding.company_phone', def: '(없음)' }, { icon: 'CLR', label: '테마 컬러', key: 'options.branding.primary_color', def: '#1a56db' }, { icon: 'Txt', label: '푸터 문구', key: 'options.branding.footer_text', def: 'SAM 시스템에서...' }, ]; brandItems.forEach((bi, i) => { const by = 1.8 + i * 0.52; s8.addShape(pres.ShapeType.roundRect, { x: 5.3, y: by, w: 4.2, h: 0.42, rectRadius: 0.06, fill: { color: C.gray100 } }); s8.addShape(pres.ShapeType.roundRect, { x: 5.4, y: by + 0.06, w: 0.45, h: 0.3, rectRadius: 0.04, fill: { color: C.blue } }); s8.addText(bi.icon, { x: 5.4, y: by + 0.06, w: 0.45, h: 0.3, fontSize: 7, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); s8.addText(bi.label, { x: 5.95, y: by, w: 1.2, h: 0.42, fontSize: 9, bold: true, color: C.navy, valign: 'middle', fontFace: 'Arial' }); s8.addText(bi.def, { x: 7.3, y: by, w: 2, h: 0.42, fontSize: 7, color: C.gray500, valign: 'middle', fontFace: 'Arial' }); }); addFooter(s8, 8, TOTAL); // ============================== // 슬라이드 9: 코드 전환 예시 // ============================== const s9 = pres.addSlide(); addPageHeader(s9, '코드 전환 예시', 'Before (직접 발송) → After (서비스 경유)'); // Before 코드 s9.addText('BEFORE (현재)', { x: 0.5, y: 1.3, w: 4.3, h: 0.35, fontSize: 11, bold: true, color: C.red, fontFace: 'Arial' }); s9.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 1.7, w: 4.3, h: 1.6, rectRadius: 0.1, fill: { color: C.navy } }); s9.addText([ { text: '// 컨트롤러에서 직접 발송\n', options: { color: C.gray500, fontSize: 8 } }, { text: 'Mail', options: { color: C.blueLight, fontSize: 9, bold: true } }, { text: '::', options: { color: C.gray400, fontSize: 9 } }, { text: 'to', options: { color: C.green, fontSize: 9 } }, { text: '($signer->email)\n', options: { color: C.gray300, fontSize: 9 } }, { text: ' ->', options: { color: C.gray400, fontSize: 9 } }, { text: 'send', options: { color: C.green, fontSize: 9 } }, { text: '(\n ', options: { color: C.gray400, fontSize: 9 } }, { text: 'new ', options: { color: C.purple, fontSize: 9 } }, { text: 'EsignRequestMail', options: { color: C.amber, fontSize: 9 } }, { text: '(...)\n );', options: { color: C.gray300, fontSize: 9 } }, ], { x: 0.7, y: 1.8, w: 3.9, h: 1.4, fontFace: 'Courier New', lineSpacingMultiple: 1.4 }); // 문제점 s9.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 3.45, w: 4.3, h: 0.7, rectRadius: 0.06, fill: { color: C.redBg }, line: { color: C.red, width: 0.5 } }); s9.addText([ { text: 'X ', options: { bold: true, color: C.red, fontSize: 8 } }, { text: '항상 .env SMTP 사용\n', options: { color: C.gray600, fontSize: 8 } }, { text: 'X ', options: { bold: true, color: C.red, fontSize: 8 } }, { text: '발송 기록 없음 / 쿼터 확인 없음', options: { color: C.gray600, fontSize: 8 } }, ], { x: 0.7, y: 3.5, w: 3.9, h: 0.6, fontFace: 'Arial', lineSpacingMultiple: 1.3 }); // 화살표 s9.addText('>>>', { x: 4.5, y: 2.2, w: 1, h: 0.5, fontSize: 20, bold: true, color: C.blue, align: 'center', fontFace: 'Arial' }); // After 코드 s9.addText('AFTER (전환 후)', { x: 5.2, y: 1.3, w: 4.3, h: 0.35, fontSize: 11, bold: true, color: C.green, fontFace: 'Arial' }); s9.addShape(pres.ShapeType.roundRect, { x: 5.2, y: 1.7, w: 4.3, h: 1.6, rectRadius: 0.1, fill: { color: C.navy } }); s9.addText([ { text: '// TenantMailService 경유\n', options: { color: C.gray500, fontSize: 8 } }, { text: 'app', options: { color: C.blueLight, fontSize: 9 } }, { text: '(TenantMailService', options: { color: C.amber, fontSize: 9 } }, { text: '::class)\n', options: { color: C.gray300, fontSize: 9 } }, { text: ' ->', options: { color: C.gray400, fontSize: 9 } }, { text: 'send', options: { color: C.green, fontSize: 9, bold: true } }, { text: '(\n', options: { color: C.gray400, fontSize: 9 } }, { text: ' mailable: ', options: { color: C.purple, fontSize: 8 } }, { text: 'new EsignRequestMail(...),\n', options: { color: C.gray300, fontSize: 8 } }, { text: ' to: ', options: { color: C.purple, fontSize: 8 } }, { text: '$signer->email\n );', options: { color: C.gray300, fontSize: 8 } }, ], { x: 5.4, y: 1.8, w: 3.9, h: 1.4, fontFace: 'Courier New', lineSpacingMultiple: 1.4 }); // 장점 s9.addShape(pres.ShapeType.roundRect, { x: 5.2, y: 3.45, w: 4.3, h: 0.7, rectRadius: 0.06, fill: { color: C.greenBg }, line: { color: C.green, width: 0.5 } }); s9.addText([ { text: 'O ', options: { bold: true, color: C.green, fontSize: 8 } }, { text: '테넌트 SMTP 자동 적용 + 브랜딩 주입\n', options: { color: C.gray600, fontSize: 8 } }, { text: 'O ', options: { bold: true, color: C.green, fontSize: 8 } }, { text: 'mail_logs 자동 기록 + 쿼터 확인', options: { color: C.gray600, fontSize: 8 } }, ], { x: 5.4, y: 3.5, w: 3.9, h: 0.6, fontFace: 'Arial', lineSpacingMultiple: 1.3 }); // 하단 주의사항 s9.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 4.35, w: 9, h: 0.65, rectRadius: 0.08, fill: { color: C.amberBg }, line: { color: C.amber, width: 0.5 } }); s9.addText([ { text: 'tenantId 자동 감지: ', options: { bold: true, color: C.amber, fontSize: 9 } }, { text: 'TenantScope와 동일하게 세션/헤더에서 tenant_id를 자동 추출합니다. 명시적 전달도 가능합니다.', options: { color: C.gray600, fontSize: 9 } }, ], { x: 0.7, y: 4.4, w: 8.6, h: 0.55, valign: 'middle', fontFace: 'Arial' }); addFooter(s9, 9, TOTAL); // ============================== // 슬라이드 10: 보안 규칙 // ============================== const s10 = pres.addSlide(); addPageHeader(s10, '보안 규칙', 'SMTP 자격증명, 개인정보, 테넌트 격리'); // 필수 준수 s10.addText('필수 준수 사항', { x: 0.5, y: 1.25, w: 4.3, h: 0.35, fontSize: 12, bold: true, color: C.green, fontFace: 'Arial' }); const mustDo = [ 'SMTP 비밀번호는 encrypt()로 암호화 저장', 'mail_logs에 메일 본문 저장 금지 (메타데이터만)', '급여명세서 등 민감 메일은 related_model/related_id만 기록', 'tenant_mail_configs 조회 시 TenantScope 자동 적용', 'API 응답에 SMTP 비밀번호 노출 금지 ($hidden 처리)', ]; mustDo.forEach((item, i) => { const iy = 1.7 + i * 0.48; s10.addShape(pres.ShapeType.roundRect, { x: 0.5, y: iy, w: 4.3, h: 0.38, rectRadius: 0.06, fill: { color: C.greenBg } }); s10.addText([ { text: 'O ', options: { bold: true, color: C.green, fontSize: 10 } }, { text: item, options: { color: C.gray700, fontSize: 8 } }, ], { x: 0.7, y: iy, w: 3.9, h: 0.38, valign: 'middle', fontFace: 'Arial' }); }); // 금지 사항 s10.addText('금지 사항', { x: 5.2, y: 1.25, w: 4.3, h: 0.35, fontSize: 12, bold: true, color: C.red, fontFace: 'Arial' }); const mustNot = [ 'Mail::to() 직접 호출 (TenantMailService 사용)', '.env SMTP에 운영 크리덴셜 하드코딩', '타 테넌트 mail_logs 조회 (TenantScope로 방지)', '이메일 본문에 비밀번호 평문 포함 (임시비번 제외)', ]; mustNot.forEach((item, i) => { const iy = 1.7 + i * 0.48; s10.addShape(pres.ShapeType.roundRect, { x: 5.2, y: iy, w: 4.3, h: 0.38, rectRadius: 0.06, fill: { color: C.redBg } }); s10.addText([ { text: 'X ', options: { bold: true, color: C.red, fontSize: 10 } }, { text: item, options: { color: C.gray700, fontSize: 8 } }, ], { x: 5.4, y: iy, w: 3.9, h: 0.38, valign: 'middle', fontFace: 'Arial' }); }); // 하단: 암호화 흐름 s10.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 4.1, w: 9, h: 0.9, rectRadius: 0.1, fill: { color: C.navy } }); s10.addText('SMTP 자격증명 암호화 흐름', { x: 0.7, y: 4.15, w: 5, h: 0.3, fontSize: 10, bold: true, color: C.blueLight, fontFace: 'Arial' }); s10.addText([ { text: '저장: ', options: { color: C.gray400, fontSize: 9 } }, { text: 'encrypt($password)', options: { color: C.green, fontSize: 9, bold: true } }, { text: ' → DB에 암호화된 문자열 저장 ', options: { color: C.gray400, fontSize: 9 } }, { text: '조회: ', options: { color: C.gray400, fontSize: 9 } }, { text: 'decrypt($encrypted)', options: { color: C.amber, fontSize: 9, bold: true } }, { text: ' → SMTP 설정 시 복호화 사용', options: { color: C.gray400, fontSize: 9 } }, ], { x: 0.7, y: 4.5, w: 8.6, h: 0.4, fontFace: 'Courier New' }); addFooter(s10, 10, TOTAL); // ============================== // 슬라이드 11: 구현 로드맵 // ============================== const s11 = pres.addSlide(); addPageHeader(s11, '구현 로드맵', '4단계 점진적 구현 계획'); const phases = [ { num: '1', title: '기반 구축', priority: '필수', color: C.blue, bg: 'eff6ff', prColor: C.red, items: [ 'tenant_mail_configs 마이그레이션', 'mail_logs 마이그레이션', 'TenantMailService 생성', 'TenantMailConfig / MailLog 모델', ] }, { num: '2', title: '기존 전환', priority: '필수', color: C.green, bg: C.greenBg, prColor: C.red, items: [ '5개 Mailable → TenantMailService 경유', 'API EsignRequestMail 중복 제거', 'sync/queue 모드 적용', 'Fallback 전략 구현', ] }, { num: '3', title: '브랜딩', priority: '중요', color: C.amber, bg: C.amberBg, prColor: C.amber, items: [ '공통 레이아웃 Blade 생성', '테넌트별 로고/컬러/서명 적용', 'MNG 메일 설정 관리 화면', '도메인 검증 가이드', ] }, { num: '4', title: '고급 기능', priority: '권장', color: C.purple, bg: C.purpleBg, prColor: C.green, items: [ '실패 재시도 (queue retry)', '바운스 처리', '발송 통계 대시보드', 'Amazon SES / Mailgun 연동', ] }, ]; phases.forEach((phase, i) => { const px = 0.5 + i * 2.3; // 카드 배경 s11.addShape(pres.ShapeType.roundRect, { x: px, y: 1.3, w: 2.1, h: 3.6, rectRadius: 0.1, fill: { color: phase.bg }, line: { color: phase.color, width: 1 } }); // Phase 넘버 s11.addShape(pres.ShapeType.ellipse, { x: px + 0.15, y: 1.4, w: 0.45, h: 0.45, fill: { color: phase.color } }); s11.addText(phase.num, { x: px + 0.15, y: 1.4, w: 0.45, h: 0.45, fontSize: 16, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); // 제목 s11.addText(phase.title, { x: px + 0.7, y: 1.42, w: 1.2, h: 0.4, fontSize: 13, bold: true, color: phase.color, fontFace: 'Arial' }); // 우선순위 배지 s11.addShape(pres.ShapeType.roundRect, { x: px + 0.15, y: 1.95, w: 0.65, h: 0.22, rectRadius: 0.04, fill: { color: phase.prColor } }); s11.addText(phase.priority, { x: px + 0.15, y: 1.95, w: 0.65, h: 0.22, fontSize: 7, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Arial' }); // 항목들 phase.items.forEach((item, j) => { const iy = 2.35 + j * 0.58; s11.addShape(pres.ShapeType.roundRect, { x: px + 0.1, y: iy, w: 1.9, h: 0.48, rectRadius: 0.06, fill: { color: C.white } }); s11.addText(item, { x: px + 0.2, y: iy, w: 1.7, h: 0.48, fontSize: 7.5, color: C.gray700, valign: 'middle', fontFace: 'Arial' }); }); // 화살표 (마지막 제외) if (i < 3) { s11.addText('>', { x: px + 2.1, y: 2.8, w: 0.2, h: 0.4, fontSize: 16, bold: true, color: C.gray400, align: 'center', valign: 'middle', fontFace: 'Arial' }); } }); addFooter(s11, 11, TOTAL); // ============================== // 슬라이드 12: Q&A // ============================== const s12 = pres.addSlide(); s12.background = { fill: C.navy }; s12.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.08, fill: { color: C.blue } }); // BI 로고 s12.addImage({ path: biLogoPath, x: 4.0, y: 1.2, w: 2.0, h: 0.85 }); s12.addText('Q & A', { x: 1, y: 2.3, w: 8, h: 0.8, fontSize: 42, bold: true, color: C.white, align: 'center', fontFace: 'Arial' }); s12.addText('질문 및 논의 사항을 공유해 주세요', { x: 1, y: 3.2, w: 8, h: 0.5, fontSize: 14, color: C.gray400, align: 'center', fontFace: 'Arial' }); // 구분선 s12.addShape(pres.ShapeType.rect, { x: 3.5, y: 3.9, w: 3, h: 0.03, fill: { color: C.navyMid } }); // 참조 문서 s12.addText([ { text: '기술 문서: ', options: { color: C.gray500, fontSize: 9 } }, { text: 'docs/dev/standards/email-policy.md', options: { color: C.blueLight, fontSize: 9 } }, ], { x: 1, y: 4.2, w: 8, h: 0.3, align: 'center', fontFace: 'Arial' }); s12.addText('(주)코드브릿지엑스', { x: 1, y: 4.8, w: 8, h: 0.3, fontSize: 9, color: C.gray500, align: 'center', fontFace: 'Arial' }); // === 저장 === const outputPath = '/home/aweso/sam/docs/presentations/sam-email-policy.pptx'; await pres.writeFile({ fileName: outputPath }); console.log('PPTX created:', outputPath); } main().catch(console.error);