From 5c7685f6aa9b6fa06484fdb7cc403d3bf8956181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Wed, 11 Mar 2026 21:47:36 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[email]=20=EB=A9=80=ED=8B=B0=ED=85=8C?= =?UTF-8?q?=EB=84=8C=EC=8B=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EB=AC=B8=EC=84=9C=20=EB=B0=8F=20=EB=B0=9C=ED=91=9C?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이메일 발송 정책 기술문서 (dev/standards/email-policy.md) - 개발팀 설명용 PPTX 12슬라이드 (presentations/sam-email-policy.pptx) - INDEX.md에 이메일 정책 문서 등록 --- INDEX.md | 2 + dev/standards/email-policy.md | 411 +++++++++ presentations/email-policy-convert.cjs | 1083 ++++++++++++++++++++++++ presentations/sam-email-policy.pptx | Bin 0 -> 425843 bytes 4 files changed, 1496 insertions(+) create mode 100644 dev/standards/email-policy.md create mode 100644 presentations/email-policy-convert.cjs create mode 100644 presentations/sam-email-policy.pptx diff --git a/INDEX.md b/INDEX.md index cd6a342..cdb46a5 100644 --- a/INDEX.md +++ b/INDEX.md @@ -16,6 +16,7 @@ | Git 커밋 | `dev/standards/git-conventions.md` | 커밋 메시지, 브랜치 전략 | | 품질 검증 | `dev/standards/quality-checklist.md` | 코드 품질 체크리스트 | | Swagger | `dev/guides/swagger-guide.md` | API 문서 작성법 | +| 이메일 | `dev/standards/email-policy.md` | 멀티테넌시 이메일 정책 | | 품목관리 | `rules/item-policy.md` | 품목 정책 | | 단가관리 | `rules/pricing-policy.md` | 원가/판매가, 리비전 | | 견적관리 | `features/quotes/README.md` | 견적 시스템, BOM 계산 | @@ -108,6 +109,7 @@ DB 도메인별: | [pagination-policy.md](dev/standards/pagination-policy.md) | 페이지네이션 표준 | | [options-column-policy.md](dev/standards/options-column-policy.md) | JSON options 컬럼 정책 | | [pdf-font-policy.md](dev/standards/pdf-font-policy.md) | PDF 생성 시 폰트 정책 (DomPDF) | +| [email-policy.md](dev/standards/email-policy.md) | 멀티테넌시 이메일 발송 정책 | --- diff --git a/dev/standards/email-policy.md b/dev/standards/email-policy.md new file mode 100644 index 0000000..30a2737 --- /dev/null +++ b/dev/standards/email-policy.md @@ -0,0 +1,411 @@ +# 이메일 발송 정책 (멀티테넌시) + +> **작성일**: 2026-03-11 +> **상태**: 설계 확정 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM 멀티테넌시 환경에서 이메일 발송의 테넌트 격리, 브랜딩, 발송 추적, 쿼터 관리를 위한 표준 정책을 정의한다. + +### 1.2 핵심 원칙 + +| 원칙 | 설명 | +|------|------| +| 🔴 테넌트 격리 | 테넌트 A의 메일 설정/발송 기록이 테넌트 B에 노출되지 않는다 | +| 🔴 중앙 서비스 경유 | 모든 메일 발송은 `TenantMailService`를 경유한다 | +| 🟡 테넌트 브랜딩 | 발신자명, 로고, 서명을 테넌트별로 커스터마이징한다 | +| 🟡 발송 기록 | 모든 발송은 `mail_logs` 테이블에 기록한다 | +| 🟢 쿼터 관리 | 테넌트별 일일 발송 한도를 관리한다 | + +### 1.3 현재 상태 (AS-IS) + +``` +문제점: +├── 단일 SMTP (.env 고정) → Gmail 일일 500건 제한에 전체 영향 +├── 단일 발신 주소 (develop@codebridge-x.com) → 테넌트 구분 불가 +├── EsignRequestMail 중복 (API + MNG 양쪽에 존재) +├── 발송 기록 없음 → 추적/감사 불가 +├── Mail::to() 직접 호출 → 테넌트 설정 적용 불가 +└── 템플릿 하드코딩 → 테넌트별 브랜딩 불가 +``` + +--- + +## 2. 아키텍처 + +### 2.1 3-Layer 구조 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Layer 1: 테넌트 메일 설정 (tenant_mail_configs) │ +│ SMTP 설정, 발신자 주소, 브랜딩 정보 │ +├─────────────────────────────────────────────────────────┤ +│ Layer 2: 메일 발송 서비스 (TenantMailService) │ +│ 테넌트 설정 자동 적용, 큐 발송, Fallback │ +├─────────────────────────────────────────────────────────┤ +│ Layer 3: 발송 기록 (mail_logs) │ +│ 발송 이력, 상태 추적, 일일 쿼터 관리 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 발송 흐름 + +``` +Controller / Service + │ + ▼ +TenantMailService::send($mailable, $to, $tenantId?) + │ + ├── 1. 테넌트 설정 조회 (tenant_mail_configs) + │ └── 미설정 시 플랫폼 기본 SMTP 사용 + │ + ├── 2. 쿼터 확인 (일일 발송 한도) + │ └── 초과 시 예외 발생 + 관리자 알림 + │ + ├── 3. Mailer 동적 구성 + │ ├── SMTP host/port/user/pass 설정 + │ ├── from address/name 설정 + │ └── 브랜딩 데이터 주입 + │ + ├── 4. 발송 모드 결정 + │ ├── sync: OTP, 비밀번호 (시간 민감) + │ └── queue: 나머지 전부 + │ + ├── 5. mail_logs 기록 (status: queued/sent) + │ + └── 6. Fallback (자체 SMTP 실패 시) + └── 플랫폼 기본 SMTP로 재시도 +``` + +--- + +## 3. 테이블 설계 + +### 3.1 `tenant_mail_configs` (테넌트 메일 설정) + +> **마이그레이션 위치**: `/home/aweso/sam/api/database/migrations/` + +```sql +CREATE TABLE tenant_mail_configs ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + provider ENUM('platform', 'smtp', 'ses', 'mailgun') DEFAULT 'platform', + from_address VARCHAR(255) NOT NULL COMMENT '발신 이메일', + from_name VARCHAR(255) NOT NULL COMMENT '발신자명', + reply_to VARCHAR(255) NULL COMMENT '회신 주소', + is_verified BOOLEAN DEFAULT FALSE COMMENT '도메인 검증 여부', + daily_limit INT UNSIGNED DEFAULT 500 COMMENT '일일 발송 한도', + is_active BOOLEAN DEFAULT TRUE, + options JSON NULL COMMENT 'SMTP 설정, 브랜딩 정보', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + UNIQUE KEY uq_tenant_mail_configs (tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) COMMENT '테넌트 메일 설정'; +``` + +**`options` JSON 구조**: + +```json +{ + "smtp": { + "host": "smtp.example.com", + "port": 587, + "username": "user@example.com", + "password": "", + "encryption": "tls" + }, + "branding": { + "logo_url": "/storage/tenants/1/logo.png", + "primary_color": "#1a56db", + "company_name": "테넌트 회사명", + "company_address": "서울시 강남구...", + "company_phone": "02-1234-5678", + "footer_text": "본 메일은 SAM 시스템에서 발송되었습니다." + }, + "ses": { + "key": "", + "secret": "", + "region": "ap-northeast-2" + } +} +``` + +> **보안**: `options.smtp.password`, `options.ses.key`, `options.ses.secret`은 `encrypt()` / `decrypt()`로 저장/조회한다. + +### 3.2 `mail_logs` (발송 기록) + +```sql +CREATE TABLE mail_logs ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + mailable_type VARCHAR(100) NOT NULL COMMENT 'Mailable 클래스명', + to_address VARCHAR(255) NOT NULL COMMENT '수신자', + from_address VARCHAR(255) NOT NULL COMMENT '발신자', + subject VARCHAR(500) NOT NULL COMMENT '제목', + status ENUM('queued', 'sent', 'failed', 'bounced') DEFAULT 'queued', + sent_at TIMESTAMP NULL COMMENT '발송 시각', + options JSON NULL COMMENT '에러 메시지, 재시도 횟수, 관련 모델', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + + INDEX idx_mail_logs_tenant_status (tenant_id, status), + INDEX idx_mail_logs_tenant_date (tenant_id, created_at), + INDEX idx_mail_logs_mailable (tenant_id, mailable_type), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) COMMENT '메일 발송 기록'; +``` + +**`options` JSON 구조**: + +```json +{ + "error_message": "Connection refused", + "retry_count": 2, + "related_model": "App\\Models\\ESign\\EsignContract", + "related_id": 123, + "provider_used": "smtp", + "fallback_used": true +} +``` + +> **개인정보 보호**: `mail_logs`에 메일 본문(body)은 저장하지 않는다. 메타데이터만 기록한다. + +--- + +## 4. 서비스 설계 + +### 4.1 `TenantMailService` + +> **위치**: `/home/aweso/sam/api/app/Services/Mail/TenantMailService.php` +> MNG에서도 동일 패턴의 서비스를 생성한다. + +```php +class TenantMailService +{ + /** + * 테넌트 설정을 적용하여 메일 발송 + * + * @param Mailable $mailable 발송할 Mailable 인스턴스 + * @param string|array $to 수신자 + * @param int|null $tenantId 테넌트 ID (null이면 현재 테넌트) + * @param bool $sync 즉시 발송 여부 (기본: false = queue) + */ + public function send( + Mailable $mailable, + string|array $to, + ?int $tenantId = null, + bool $sync = false + ): MailLog; +} +``` + +### 4.2 발송 모드 기준 + +| 모드 | Mailable | 사유 | +|------|----------|------| +| **sync** (즉시) | `EsignOtpMail` | OTP 시간 제한 | +| **sync** (즉시) | `UserPasswordMail` | 즉시 로그인 필요 | +| **queue** (큐) | `EsignRequestMail` | 비동기 가능 | +| **queue** (큐) | `EsignCompletedMail` | 비동기 가능 | +| **queue** (큐) | `PayslipMail` | 대량 발송 가능 | + +### 4.3 Fallback 전략 + +``` +테넌트 자체 SMTP 발송 시도 + │ + ├── 성공 → mail_logs (status: sent) + │ + └── 실패 → 플랫폼 기본 SMTP로 재시도 + │ + ├── 성공 → mail_logs (status: sent, fallback_used: true) + │ + └── 실패 → mail_logs (status: failed) + └── 3회까지 자동 재시도 (queue retry) +``` + +--- + +## 5. 메일 타입 정리 + +### 5.1 현재 Mailable 목록 + +| Mailable | 위치 | 트리거 | 비고 | +|----------|------|--------|------| +| `EsignRequestMail` | API + MNG (중복) | 서명 요청 | 🔴 중복 제거 필요 | +| `EsignOtpMail` | MNG | OTP 인증 | | +| `EsignCompletedMail` | MNG | 서명 완료 | | +| `UserPasswordMail` | MNG | 계정 생성/비번 초기화 | | +| `PayslipMail` | MNG | 급여명세서 | | + +### 5.2 Mailable 위치 정리 방향 + +``` +❌ 현재: API와 MNG에 중복 존재 +✅ 목표: 비즈니스 로직은 API, MNG는 관리자 트리거만 + +API (app/Mail/) +├── EsignRequestMail.php ← API에서 통합 관리 +├── EsignOtpMail.php +├── EsignCompletedMail.php +├── UserPasswordMail.php +└── PayslipMail.php + +MNG → API의 Mailable을 HTTP API로 호출 + 또는 공유 패키지로 분리 +``` + +> **현실적 방향**: 현재 MNG에서 직접 DB 접근하므로, MNG의 Mailable을 유지하되 `TenantMailService` 경유로 통일한다. API의 중복 `EsignRequestMail`은 제거한다. + +--- + +## 6. 템플릿 브랜딩 + +### 6.1 공통 레이아웃 + +``` +┌─ emails.layouts.tenant ──────────────────────────┐ +│ │ +│ ┌─ 헤더 ──────────────────────────────────────┐ │ +│ │ [테넌트 로고] 테넌트명 │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 본문 ──────────────────────────────────────┐ │ +│ │ @yield('content') │ │ +│ │ (각 Mailable에서 제공) │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 푸터 ──────────────────────────────────────┐ │ +│ │ {{ 회사명 }} | {{ 주소 }} | {{ 연락처 }} │ │ +│ │ "SAM 시스템에서 발송된 메일입니다" │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────┘ +``` + +### 6.2 브랜딩 요소 + +| 요소 | 저장 위치 | 기본값 | +|------|----------|--------| +| 로고 이미지 | `options.branding.logo_url` | SAM BI 로고 | +| 회사명 | `options.branding.company_name` | (주)코드브릿지엑스 | +| 주소 | `options.branding.company_address` | — | +| 연락처 | `options.branding.company_phone` | — | +| 테마 컬러 | `options.branding.primary_color` | `#1a56db` | +| 푸터 문구 | `options.branding.footer_text` | "SAM 시스템에서 발송된 메일입니다" | + +--- + +## 7. 쿼터 관리 + +### 7.1 쿼터 정책 + +| 항목 | 기본값 | 설명 | +|------|--------|------| +| 일일 발송 한도 | 500건 | `tenant_mail_configs.daily_limit` | +| 경고 임계치 | 80% (400건) | 도달 시 관리자 알림 | +| 초과 시 동작 | 발송 차단 + 예외 | `MailQuotaExceededException` | + +### 7.2 쿼터 확인 + +```php +// mail_logs에서 오늘 발송 건수 조회 +$todayCount = MailLog::where('tenant_id', $tenantId) + ->whereDate('created_at', today()) + ->whereIn('status', ['queued', 'sent']) + ->count(); + +if ($todayCount >= $config->daily_limit) { + throw new MailQuotaExceededException($tenantId); +} +``` + +--- + +## 8. 보안 규칙 + +### 8.1 필수 준수 사항 + +``` +✅ SMTP 비밀번호는 encrypt()로 암호화 저장 +✅ mail_logs에 메일 본문 저장 금지 (메타데이터만) +✅ 급여명세서 등 민감 메일은 related_model/related_id만 기록 +✅ tenant_mail_configs 조회 시 TenantScope 자동 적용 +✅ API 응답에 SMTP 비밀번호 노출 금지 (hidden 처리) +``` + +### 8.2 금지 사항 + +``` +❌ Mail::to() 직접 호출 금지 → TenantMailService 사용 +❌ .env SMTP 설정에 운영 크리덴셜 하드코딩 금지 +❌ 타 테넌트 mail_logs 조회 금지 (TenantScope로 방지) +❌ 이메일 본문에 비밀번호 평문 포함 금지 (임시 비밀번호 제외) +``` + +--- + +## 9. 구현 단계 + +| Phase | 범위 | 주요 작업 | 우선순위 | +|-------|------|----------|---------| +| **Phase 1** | 기반 구축 | `tenant_mail_configs` + `mail_logs` 마이그레이션, `TenantMailService` 생성, 모델 생성 | 🔴 필수 | +| **Phase 2** | 기존 전환 | 현재 5개 Mailable을 `TenantMailService` 경유로 변경, API `EsignRequestMail` 중복 제거 | 🔴 필수 | +| **Phase 3** | 브랜딩 | 공통 레이아웃 생성, 테넌트별 로고/컬러/서명 적용, MNG 관리 화면 | 🟡 중요 | +| **Phase 4** | 고급 기능 | 실패 재시도, 바운스 처리, 발송 통계 대시보드, SES/Mailgun 연동 | 🟢 권장 | + +--- + +## 10. SMTP 제공자 비교 + +| 제공자 | 일일 한도 | 비용 | 적합 시점 | +|--------|---------|------|----------| +| Gmail SMTP | 500건 | 무료 | 현재 (소규모) | +| Amazon SES | 무제한 | $0.10/1,000건 | 테넌트 10개+ | +| Mailgun | 5,000건/월 무료 | $0.80/1,000건 | 중규모 | +| 자체 SMTP | 무제한 | 서버 비용 | 테넌트 자체 운영 | + +> **권장**: Phase 1~2는 Gmail SMTP 유지, Phase 4에서 Amazon SES 전환 검토 + +--- + +## 11. 기존 코드 전환 가이드 + +### 11.1 Before (현재) + +```php +// MNG 컨트롤러에서 직접 발송 +Mail::to($signer->email)->send(new EsignRequestMail($contract, $signer)); +``` + +### 11.2 After (전환 후) + +```php +// TenantMailService 경유 +app(TenantMailService::class)->send( + mailable: new EsignRequestMail($contract, $signer), + to: $signer->email, + // tenantId는 자동 감지 (현재 세션 기반) +); +``` + +--- + +## 관련 문서 + +- [테넌트 DB 구조](../../system/database/tenants.md) +- [전자서명 기능](../../features/esign/README.md) +- [급여관리 기능](../../features/finance/payroll.md) +- [API 개발 규칙](api-rules.md) +- [options JSON 컬럼 정책](options-column-policy.md) + +--- + +**최종 업데이트**: 2026-03-11 diff --git a/presentations/email-policy-convert.cjs b/presentations/email-policy-convert.cjs new file mode 100644 index 0000000..46890f9 --- /dev/null +++ b/presentations/email-policy-convert.cjs @@ -0,0 +1,1083 @@ +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); diff --git a/presentations/sam-email-policy.pptx b/presentations/sam-email-policy.pptx new file mode 100644 index 0000000000000000000000000000000000000000..78e8ac510b414a45c7c4836f83cb56ef95b78ac7 GIT binary patch literal 425843 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 literal 0 HcmV?d00001