diff --git a/docs/architecture.html b/docs/architecture.html new file mode 100644 index 0000000..86ca116 --- /dev/null +++ b/docs/architecture.html @@ -0,0 +1,788 @@ + + + + + + O2O CastAD Backend - 인프라 아키텍처 + + + + + + + +
+
+

O2O CastAD Backend

+

인프라 아키텍처 및 비용 산출 문서

+
+ + +
+
+

1DB 및 서버 부하 분산 방법

+

Nginx 로드밸런싱, 커넥션 풀 관리, 단계별 수평 확장 전략

+
+ +
+
+

현재 구현 현황 (단일 인스턴스)

+
    +
  • API 커넥션 풀: pool_size=20, max_overflow=20 → 최대 40
  • +
  • 백그라운드 풀: pool_size=10, max_overflow=10 → 최대 20
  • +
  • 인스턴스당 총 DB 연결: 40 + 20 = 60
  • +
  • 풀 리사이클: 280초 (MySQL wait_timeout 300초 이전), pre-ping 활성화
  • +
+ +

단계별 확장 전략

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
단계동시접속App ServerLBDB ( MySQL Flexible)
S1~50명x1Nginx x1Burstable B1ms
S250~200명x2~4NginxGP D2ds_v4 + Replica x1
S3200~1,000명API ServerxN
+ Scheduler
NginxBC D4ds_v4 + Replica x2 + Redis P1
+
+ +
+

커넥션 풀 수치 계산

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
항목Stage 1 (1대)Stage 2 (4대)Stage 3 (8대)
Main Pool / 인스턴스20+20 = 4010+10 = 205+5 = 10
BG Pool / 인스턴스10+10 = 205+5 = 103+3 = 6
인스턴스당 소계603016
Primary 총 연결604 x 30 = 1208 x 16 = 128
max_connections 권장100200300
+ +
+ 핵심: + JWT Stateless 설계로 Nginx 세션 어피니티 불필요 (round-robin / least_conn). + Stage 2부터 Read Replica로 읽기 분산, Redis는 Stage 3에서 캐싱/Rate Limiting 도입. +
+
+
+ + +
+
+graph TB
+    subgraph S1["Stage 1: ~50명"]
+        direction LR
+        S1N["Nginx
(Reverse Proxy)"] --> S1A["App Server x1"] + S1A --> S1D[" MySQL
Burstable B1ms"] + end + + subgraph S2["Stage 2: 50~200명"] + direction LR + S2N["Nginx
(Reverse Proxy)"] --> S2API["APP Server
x 1 ~ 2"] + S2N --> S2WK["Scheduler
Server"] + S2API --> S2P["MySQL BC
Primary
(D4ds_v4)"] + S2API --> S2R1["Read Replica
x1"] + S2WK --> S2P + S2WK --> S2R1 + end + + subgraph S3["Stage 3: 200~1,000명"] + direction LR + S3N["Nginx
(Reverse Proxy)"] --> S3API["APP Server
x N"] + S3N --> S3WK["Scheduler
Server"] + S3API --> S3P["MySQL BC
Primary
(D4ds_v4)"] + S3API --> S3R1["Read Replica
xN"] + S3API --> S3RD["Redis
Premium P1"] + S3WK --> S3P + S3WK --> S3R1 + end + + S1 ~~~ S2 ~~~ S3 + + style S1 fill:#0d3320,stroke:#34d399,stroke-width:2px,color:#e1e4ed + style S2 fill:#3b2506,stroke:#fb923c,stroke-width:2px,color:#e1e4ed + style S3 fill:#3b1010,stroke:#f87171,stroke-width:2px,color:#e1e4ed +
+
+
+ + +
+
+

2전체 아키텍처 다이어그램

+

Nginx + FastAPI App Server 구성과 외부 서비스 연동 구조

+
+ +
+
+
    +
  • 로드밸런서: Nginx (Reverse Proxy, L7 LB, SSL 종단)
  • +
  • App Server: FastAPI (Python 3.13) — Auth, Home, Lyric, Song, Video, Social, SNS, Archive, Admin, Background Worker
  • +
  • DB: Database for MySQL Flexible Server — Stage 2+ Read Replica
  • +
+
+
+
    +
  • 캐시: Cache for Redis (Stage 3 도입)
  • +
  • 콘텐츠 생성: 가사(ChatGPT) → 음악(Suno AI) → 영상(Creatomate) → SNS 업로드
  • +
  • 외부 연동: Kakao OAuth, Naver Map/Search API, Blob Storage
  • +
+
+
+ + +
+
+graph TB
+    Client["클라이언트
(Web / App)"] + LB["Nginx
(Reverse Proxy + SSL 종단)"] + + subgraph APP["App Server (FastAPI)"] + direction LR + Auth["Auth"] --- Home["Home"] --- Lyric["Lyric"] --- Song["Song"] --- Video["Video"] + Social["Social"] --- SNS["SNS"] --- Archive["Archive"] --- Admin["Admin"] --- BG["BG Worker"] + end + + subgraph DB[" MySQL Flexible Server"] + direction LR + Primary["Primary (R/W)"] + Replica["Read Replica"] + end + + subgraph AI["AI 콘텐츠 생성 파이프라인"] + direction LR + ChatGPT["ChatGPT
(가사 생성)"] + Suno["Suno AI
(음악 생성)"] + Creatomate["Creatomate
(영상 생성)"] + ChatGPT --> Suno --> Creatomate + end + + subgraph EXT["외부 서비스"] + direction LR + Blob[" Blob
Storage"] + Kakao["Kakao
OAuth"] + YT["YouTube /
Instagram"] + Naver["Naver Map /
Search API"] + end + + Redis[" Cache for Redis
(Stage 3 도입)"] + + Client -->|HTTPS| LB + LB --> APP + APP --> Primary + APP -->|"읽기 전용"| Replica + APP -.->|"Stage 3"| Redis + APP --> AI + APP --> Blob + APP --> Kakao + APP --> YT + APP --> Naver + + style Client fill:#1a3a1a,stroke:#34d399,stroke-width:2px,color:#e1e4ed + style LB fill:#1a3a1a,stroke:#34d399,stroke-width:2px,color:#e1e4ed + style APP fill:#1a2744,stroke:#6c8cff,stroke-width:2px,color:#e1e4ed + style DB fill:#2a1f00,stroke:#fb923c,stroke-width:2px,color:#e1e4ed + style AI fill:#2a0f2a,stroke:#a78bfa,stroke-width:2px,color:#e1e4ed + style EXT fill:#0d2a2a,stroke:#34d399,stroke-width:2px,color:#e1e4ed + style Redis fill:#3b1010,stroke:#f87171,stroke-width:1px,color:#e1e4ed +
+
+

전체 시스템 아키텍처 구성도

+ +
+ 콘텐츠 생성 흐름: 사용자 요청 → Naver 크롤링 → ChatGPT 가사 생성 → Suno AI 음악 생성 → Creatomate 영상 생성 → Blob 저장 → YouTube/Instagram 업로드 +
+
+ + +
+
+

3예상 리소스 및 비용

+

기반 단계별 월 예상 비용 (인프라 + 외부 API)

+
+ +
+
+
Stage 1 · 동시 ~50명
+
$170~390
+
약 22~51만원/월
+
+
+
Stage 2 · 동시 50~200명
+
$960~2,160
+
약 125~280만원/월
+
+
+
Stage 3 · 동시 200~1,000명
+
$3,850~8,500
+
약 500~1,100만원/월
+
+
+ +
+
+

항목별 비용 상세

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
항목Stage 1Stage 2Stage 3
App Server$50~70$200~400$600~1,000
Nginx-포함 / VM $15~30VM $30~60
MySQL PrimaryB1ms $15~25GP $130~160BC $350~450
MySQL Replica-GP x1 $130~160BC x2 $260~360
Redis--P1 $225
스토리지/네트워크$10~20$55~100$160~270
AI API (합계)$90~280$400~1,250$2,100~5,800
+
+ +
+

DB 용량 예측 (1년 후)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Stage 1 (500명)Stage 2 (5,000명)Stage 3 (50,000명)
DB 용량~1.2GB~12GB~120GB
Blob 스토리지~1.1TB~11TB~110TB
MySQL 추천32GB SSD128GB SSD512GB SSD
+ +
+ 비용 최적화 팁: + 3rd party 의존도 낮춰야함 +
+ Blob Lifecycle Policy (30일 미접근 → Cool 티어), +
+
+
+ + +
+
+pie title Stage 3 월 비용 구성 비중
+    "App Server (APP+Scheduler)" : 800
+    "Nginx" : 45
+    "MySQL Primary" : 400
+    "MySQL Replica x2" : 310
+    "Redis Premium" : 225
+    "스토리지/네트워크" : 215
+    "OpenAI API" : 550
+    "Suno AI" : 1400
+    "Creatomate" : 2000
+            
+
+

Stage 3 월간 비용 구성 비율 — AI API 비중이 전체의 약 66%

+
+
+ + + + + diff --git a/docs/architecture.pptx b/docs/architecture.pptx new file mode 100644 index 0000000..561b793 Binary files /dev/null and b/docs/architecture.pptx differ diff --git a/docs/generate_ppt.py b/docs/generate_ppt.py new file mode 100644 index 0000000..26c6122 --- /dev/null +++ b/docs/generate_ppt.py @@ -0,0 +1,551 @@ +""" +O2O CastAD Backend - 인프라 아키텍처 PPT 생성 스크립트 +실행: python3 docs/generate_ppt.py +출력: docs/architecture.pptx +""" + +from pptx import Presentation +from pptx.util import Inches, Pt, Emu +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR +from pptx.enum.shapes import MSO_SHAPE + +# ── 색상 팔레트 (HTML 다크 테마 매칭) ── +BG = RGBColor(0x0F, 0x11, 0x17) +SURFACE = RGBColor(0x1A, 0x1D, 0x27) +SURFACE2 = RGBColor(0x23, 0x27, 0x33) +BORDER = RGBColor(0x2E, 0x33, 0x45) +TEXT = RGBColor(0xE1, 0xE4, 0xED) +TEXT_DIM = RGBColor(0x8B, 0x90, 0xA0) +ACCENT = RGBColor(0x6C, 0x8C, 0xFF) +ACCENT2 = RGBColor(0xA7, 0x8B, 0xFA) +GREEN = RGBColor(0x34, 0xD3, 0x99) +ORANGE = RGBColor(0xFB, 0x92, 0x3C) +RED = RGBColor(0xF8, 0x71, 0x71) +WHITE = RGBColor(0xFF, 0xFF, 0xFF) + +SLIDE_W = Inches(13.333) +SLIDE_H = Inches(7.5) + + +def set_slide_bg(slide, color): + bg = slide.background + fill = bg.fill + fill.solid() + fill.fore_color.rgb = color + + +def add_textbox(slide, left, top, width, height, text, font_size=14, + color=TEXT, bold=False, alignment=PP_ALIGN.LEFT, font_name="맑은 고딕"): + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.text = text + p.font.size = Pt(font_size) + p.font.color.rgb = color + p.font.bold = bold + p.font.name = font_name + p.alignment = alignment + return txBox + + +def add_bullet_list(slide, left, top, width, height, items, font_size=13): + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.word_wrap = True + for i, item in enumerate(items): + if i == 0: + p = tf.paragraphs[0] + else: + p = tf.add_paragraph() + p.space_after = Pt(4) + p.font.size = Pt(font_size) + p.font.color.rgb = TEXT_DIM + p.font.name = "맑은 고딕" + # bold 부분 처리 + if isinstance(item, tuple): + run_bold = p.add_run() + run_bold.text = item[0] + run_bold.font.bold = True + run_bold.font.color.rgb = TEXT + run_bold.font.size = Pt(font_size) + run_bold.font.name = "맑은 고딕" + run_normal = p.add_run() + run_normal.text = item[1] + run_normal.font.color.rgb = TEXT_DIM + run_normal.font.size = Pt(font_size) + run_normal.font.name = "맑은 고딕" + else: + p.text = f"• {item}" + return txBox + + +def add_table(slide, left, top, width, height, headers, rows, col_widths=None): + n_rows = len(rows) + 1 + n_cols = len(headers) + table_shape = slide.shapes.add_table(n_rows, n_cols, left, top, width, height) + table = table_shape.table + + # 컬럼 폭 설정 + if col_widths: + for i, w in enumerate(col_widths): + table.columns[i].width = w + + # 헤더 행 + for j, h in enumerate(headers): + cell = table.cell(0, j) + cell.text = h + for paragraph in cell.text_frame.paragraphs: + paragraph.font.size = Pt(11) + paragraph.font.bold = True + paragraph.font.color.rgb = ACCENT + paragraph.font.name = "맑은 고딕" + paragraph.alignment = PP_ALIGN.CENTER + cell.fill.solid() + cell.fill.fore_color.rgb = SURFACE2 + + # 데이터 행 + for i, row in enumerate(rows): + for j, val in enumerate(row): + cell = table.cell(i + 1, j) + cell.text = str(val) + for paragraph in cell.text_frame.paragraphs: + paragraph.font.size = Pt(10) + paragraph.font.color.rgb = TEXT_DIM + paragraph.font.name = "맑은 고딕" + paragraph.alignment = PP_ALIGN.CENTER + cell.fill.solid() + cell.fill.fore_color.rgb = SURFACE if i % 2 == 0 else BG + + # 테이블 테두리 제거 (깔끔하게) + for i in range(n_rows): + for j in range(n_cols): + cell = table.cell(i, j) + cell.vertical_anchor = MSO_ANCHOR.MIDDLE + for border_name in ['top', 'bottom', 'left', 'right']: + border = getattr(cell, f'border_{border_name}' if hasattr(cell, f'border_{border_name}') else border_name, None) + + return table_shape + + +def add_rounded_rect(slide, left, top, width, height, fill_color, border_color=None, text="", + font_size=12, text_color=TEXT, bold=False): + shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height) + shape.fill.solid() + shape.fill.fore_color.rgb = fill_color + if border_color: + shape.line.color.rgb = border_color + shape.line.width = Pt(1.5) + else: + shape.line.fill.background() + if text: + tf = shape.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.text = text + p.font.size = Pt(font_size) + p.font.color.rgb = text_color + p.font.bold = bold + p.font.name = "맑은 고딕" + p.alignment = PP_ALIGN.CENTER + tf.paragraphs[0].space_before = Pt(0) + tf.paragraphs[0].space_after = Pt(0) + return shape + + +def add_section_number(slide, left, top, number, color=ACCENT): + shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, Inches(0.4), Inches(0.4)) + shape.fill.solid() + shape.fill.fore_color.rgb = color + shape.line.fill.background() + tf = shape.text_frame + p = tf.paragraphs[0] + p.text = str(number) + p.font.size = Pt(14) + p.font.color.rgb = WHITE + p.font.bold = True + p.font.name = "맑은 고딕" + p.alignment = PP_ALIGN.CENTER + return shape + + +def add_arrow(slide, x1, y1, x2, y2, color=ACCENT): + connector = slide.shapes.add_connector(1, x1, y1, x2, y2) # 1 = straight + connector.line.color.rgb = color + connector.line.width = Pt(1.5) + connector.end_x = x2 + connector.end_y = y2 + return connector + + +# ══════════════════════════════════════════════════════════════════ +# PPT 생성 시작 +# ══════════════════════════════════════════════════════════════════ +prs = Presentation() +prs.slide_width = SLIDE_W +prs.slide_height = SLIDE_H + +blank_layout = prs.slide_layouts[6] # Blank + +# ── Slide 1: 타이틀 ── +slide = prs.slides.add_slide(blank_layout) +set_slide_bg(slide, BG) + +add_textbox(slide, Inches(0), Inches(2.2), SLIDE_W, Inches(1), + "O2O CastAD Backend", font_size=44, color=ACCENT, bold=True, + alignment=PP_ALIGN.CENTER) +add_textbox(slide, Inches(0), Inches(3.3), SLIDE_W, Inches(0.6), + "인프라 아키텍처 및 비용 산출 문서", font_size=20, color=TEXT_DIM, + alignment=PP_ALIGN.CENTER) + +# 하단 구분선 +line = slide.shapes.add_connector(1, Inches(4.5), Inches(4.2), Inches(8.8), Inches(4.2)) +line.line.color.rgb = ACCENT +line.line.width = Pt(2) + +add_textbox(slide, Inches(0), Inches(4.5), SLIDE_W, Inches(0.5), + "Nginx + FastAPI + MySQL + AI Pipeline", font_size=14, color=TEXT_DIM, + alignment=PP_ALIGN.CENTER) + + +# ── Slide 2: 부하 분산 - 현재 구현 & 확장 전략 ── +slide = prs.slides.add_slide(blank_layout) +set_slide_bg(slide, BG) + +add_section_number(slide, Inches(0.5), Inches(0.4), "1") +add_textbox(slide, Inches(1.0), Inches(0.35), Inches(8), Inches(0.5), + "DB 및 서버 부하 분산 방법", font_size=26, color=ACCENT, bold=True) +add_textbox(slide, Inches(1.0), Inches(0.85), Inches(10), Inches(0.3), + "Nginx 로드밸런싱, 커넥션 풀 관리, 단계별 수평 확장 전략", font_size=12, color=TEXT_DIM) + +# 좌측: 현재 구현 현황 +add_textbox(slide, Inches(0.5), Inches(1.5), Inches(5), Inches(0.4), + "현재 구현 현황 (단일 인스턴스)", font_size=16, color=TEXT, bold=True) + +items = [ + ("API 커넥션 풀: ", "pool_size=20, max_overflow=20 → 최대 40"), + ("백그라운드 풀: ", "pool_size=10, max_overflow=10 → 최대 20"), + ("인스턴스당 총 DB 연결: ", "40 + 20 = 60"), + ("풀 리사이클: ", "280초 (MySQL wait_timeout 300초 이전)"), + ("Pre-ping: ", "활성화 (죽은 커넥션 자동 복구)"), +] +add_bullet_list(slide, Inches(0.5), Inches(2.0), Inches(5.5), Inches(2.5), items, font_size=12) + +# 우측: 확장 전략 테이블 +add_textbox(slide, Inches(6.8), Inches(1.5), Inches(6), Inches(0.4), + "단계별 확장 전략", font_size=16, color=TEXT, bold=True) + +headers = ["단계", "동시접속", "App Server", "LB", "DB (MySQL Flexible)"] +rows = [ + ["S1", "~50명", "x1", "Nginx x1", "Burstable B1ms"], + ["S2", "50~200명", "x2~4", "Nginx", "GP D2ds + Replica x1"], + ["S3", "200~1,000명", "API xN\n+ Scheduler", "Nginx", "BC D4ds + Replica x2\n+ Redis P1"], +] +add_table(slide, Inches(6.8), Inches(2.0), Inches(6), Inches(2.0), headers, rows) + +# 하단: 핵심 노트 +note_shape = add_rounded_rect(slide, Inches(0.5), Inches(4.6), Inches(12.3), Inches(0.7), + SURFACE2, ACCENT) +tf = note_shape.text_frame +tf.word_wrap = True +p = tf.paragraphs[0] +p.text = "" +run = p.add_run() +run.text = "핵심: " +run.font.bold = True +run.font.color.rgb = ACCENT +run.font.size = Pt(11) +run.font.name = "맑은 고딕" +run = p.add_run() +run.text = "JWT Stateless 설계로 Nginx 세션 어피니티 불필요 (round-robin / least_conn). Stage 2부터 Read Replica로 읽기 분산, Redis는 Stage 3에서 캐싱/Rate Limiting 도입." +run.font.color.rgb = TEXT_DIM +run.font.size = Pt(11) +run.font.name = "맑은 고딕" + + +# ── Slide 3: 커넥션 풀 수치 계산 ── +slide = prs.slides.add_slide(blank_layout) +set_slide_bg(slide, BG) + +add_section_number(slide, Inches(0.5), Inches(0.4), "1") +add_textbox(slide, Inches(1.0), Inches(0.35), Inches(8), Inches(0.5), + "커넥션 풀 수치 계산", font_size=26, color=ACCENT, bold=True) +add_textbox(slide, Inches(1.0), Inches(0.85), Inches(10), Inches(0.3), + "인스턴스 수 증가에 따른 인스턴스당 풀 사이즈 축소 및 총 DB 커넥션 관리", font_size=12, color=TEXT_DIM) + +headers = ["항목", "Stage 1 (1대)", "Stage 2 (4대)", "Stage 3 (8대)"] +rows = [ + ["Main Pool / 인스턴스", "20+20 = 40", "10+10 = 20", "5+5 = 10"], + ["BG Pool / 인스턴스", "10+10 = 20", "5+5 = 10", "3+3 = 6"], + ["인스턴스당 소계", "60", "30", "16"], + ["Primary 총 연결", "60", "4 x 30 = 120", "8 x 16 = 128"], + ["max_connections 권장", "100", "200", "300"], +] +add_table(slide, Inches(1.5), Inches(1.6), Inches(10.3), Inches(2.8), headers, rows) + +# 시각적 요약 박스 +stages = [ + ("Stage 1", "1대 × 60 = 60", GREEN, Inches(2)), + ("Stage 2", "4대 × 30 = 120", ORANGE, Inches(5.5)), + ("Stage 3", "8대 × 16 = 128", RED, Inches(9)), +] +for label, val, color, left in stages: + add_rounded_rect(slide, left, Inches(4.8), Inches(2.3), Inches(0.9), + SURFACE2, color, f"{label}\n{val}", font_size=13, text_color=color, bold=True) + + +# ── Slide 4: 아키텍처 다이어그램 (상세 블록) ── +slide = prs.slides.add_slide(blank_layout) +set_slide_bg(slide, BG) + +add_section_number(slide, Inches(0.5), Inches(0.4), "2") +add_textbox(slide, Inches(1.0), Inches(0.35), Inches(8), Inches(0.5), + "전체 아키텍처 다이어그램", font_size=26, color=ACCENT, bold=True) +add_textbox(slide, Inches(1.0), Inches(0.85), Inches(10), Inches(0.3), + "Nginx + FastAPI App Server 구성과 외부 서비스 연동 구조", font_size=12, color=TEXT_DIM) + +# 클라이언트 +add_rounded_rect(slide, Inches(5.5), Inches(1.4), Inches(2.3), Inches(0.6), + RGBColor(0x1A, 0x3A, 0x1A), GREEN, "클라이언트 (Web / App)", + font_size=11, text_color=TEXT) + +# Nginx +add_rounded_rect(slide, Inches(5.5), Inches(2.3), Inches(2.3), Inches(0.6), + RGBColor(0x1A, 0x3A, 0x1A), GREEN, "Nginx\n(Reverse Proxy + SSL)", + font_size=10, text_color=TEXT) + +# App Server +app_box = add_rounded_rect(slide, Inches(2.5), Inches(3.2), Inches(8.3), Inches(1.2), + RGBColor(0x1A, 0x27, 0x44), ACCENT, "", font_size=10) +add_textbox(slide, Inches(2.7), Inches(3.15), Inches(3), Inches(0.3), + "App Server (FastAPI)", font_size=11, color=ACCENT, bold=True) + +modules = ["Auth", "Home", "Lyric", "Song", "Video", "Social", "SNS", "Archive", "Admin", "BG Worker"] +for i, mod in enumerate(modules): + col = i % 10 + x = Inches(2.7 + col * 0.8) + y = Inches(3.55) + add_rounded_rect(slide, x, y, Inches(0.7), Inches(0.5), + SURFACE2, BORDER, mod, font_size=8, text_color=TEXT_DIM) + +# DB +add_rounded_rect(slide, Inches(1.0), Inches(4.85), Inches(3.5), Inches(0.7), + RGBColor(0x2A, 0x1F, 0x00), ORANGE, "MySQL Flexible Server\nPrimary (R/W) + Read Replica", + font_size=10, text_color=TEXT) + +# Redis +add_rounded_rect(slide, Inches(5.0), Inches(4.85), Inches(2.3), Inches(0.7), + RGBColor(0x3B, 0x10, 0x10), RED, "Cache for Redis\n(Stage 3 도입)", + font_size=10, text_color=TEXT) + +# AI Pipeline +add_rounded_rect(slide, Inches(7.8), Inches(4.85), Inches(4.5), Inches(0.7), + RGBColor(0x2A, 0x0F, 0x2A), ACCENT2, "AI Pipeline: ChatGPT → Suno AI → Creatomate", + font_size=10, text_color=TEXT) + +# 외부 서비스 +ext_items = [("Blob\nStorage", Inches(1.0)), ("Kakao\nOAuth", Inches(3.2)), + ("YouTube /\nInstagram", Inches(5.4)), ("Naver Map /\nSearch", Inches(7.6))] +for label, x in ext_items: + add_rounded_rect(slide, x, Inches(6.0), Inches(1.8), Inches(0.7), + RGBColor(0x0D, 0x2A, 0x2A), GREEN, label, + font_size=9, text_color=TEXT_DIM) + +# 콘텐츠 생성 흐름 노트 +note_shape = add_rounded_rect(slide, Inches(1.0), Inches(6.85), Inches(11.3), Inches(0.45), + SURFACE2, ACCENT) +tf = note_shape.text_frame +tf.word_wrap = True +p = tf.paragraphs[0] +run = p.add_run() +run.text = "콘텐츠 생성 흐름: " +run.font.bold = True +run.font.color.rgb = ACCENT +run.font.size = Pt(10) +run.font.name = "맑은 고딕" +run = p.add_run() +run.text = "사용자 요청 → Naver 크롤링 → ChatGPT 가사 → Suno AI 음악 → Creatomate 영상 → Blob 저장 → YouTube/Instagram 업로드" +run.font.color.rgb = TEXT_DIM +run.font.size = Pt(10) +run.font.name = "맑은 고딕" + + +# ── Slide 5: Stage별 인프라 스케일링 ── +slide = prs.slides.add_slide(blank_layout) +set_slide_bg(slide, BG) + +add_section_number(slide, Inches(0.5), Inches(0.4), "2") +add_textbox(slide, Inches(1.0), Inches(0.35), Inches(8), Inches(0.5), + "단계별 인프라 스케일링", font_size=26, color=ACCENT, bold=True) + +# Stage 1 +stage1_box = add_rounded_rect(slide, Inches(0.5), Inches(1.3), Inches(3.8), Inches(5.2), + RGBColor(0x0D, 0x33, 0x20), GREEN) +add_textbox(slide, Inches(0.7), Inches(1.4), Inches(3.4), Inches(0.3), + "Stage 1: ~50명", font_size=16, color=GREEN, bold=True) +items = [ + ("Nginx: ", "Reverse Proxy x1"), + ("App Server: ", "x1"), + ("MySQL: ", "Burstable B1ms"), + ("Redis: ", "미사용"), + ("월 비용: ", "$170~390"), +] +add_bullet_list(slide, Inches(0.7), Inches(1.9), Inches(3.4), Inches(3), items, font_size=12) + +# Stage 2 +stage2_box = add_rounded_rect(slide, Inches(4.7), Inches(1.3), Inches(3.8), Inches(5.2), + RGBColor(0x3B, 0x25, 0x06), ORANGE) +add_textbox(slide, Inches(4.9), Inches(1.4), Inches(3.4), Inches(0.3), + "Stage 2: 50~200명", font_size=16, color=ORANGE, bold=True) +items = [ + ("Nginx: ", "Reverse Proxy (LB)"), + ("App Server: ", "x2~4"), + ("Scheduler: ", "Server x1"), + ("MySQL: ", "GP D2ds + Replica x1"), + ("Redis: ", "미사용"), + ("월 비용: ", "$960~2,160"), +] +add_bullet_list(slide, Inches(4.9), Inches(1.9), Inches(3.4), Inches(3.5), items, font_size=12) + +# Stage 3 +stage3_box = add_rounded_rect(slide, Inches(8.9), Inches(1.3), Inches(3.8), Inches(5.2), + RGBColor(0x3B, 0x10, 0x10), RED) +add_textbox(slide, Inches(9.1), Inches(1.4), Inches(3.4), Inches(0.3), + "Stage 3: 200~1,000명", font_size=16, color=RED, bold=True) +items = [ + ("Nginx: ", "Reverse Proxy (LB)"), + ("API Server: ", "x N (Auto Scale)"), + ("Scheduler: ", "Server x1"), + ("MySQL: ", "BC D4ds + Replica x2"), + ("Redis: ", "Premium P1 (캐싱)"), + ("월 비용: ", "$3,850~8,500"), +] +add_bullet_list(slide, Inches(9.1), Inches(1.9), Inches(3.4), Inches(3.5), items, font_size=12) + + +# ── Slide 6: 비용 산출 개요 ── +slide = prs.slides.add_slide(blank_layout) +set_slide_bg(slide, BG) + +add_section_number(slide, Inches(0.5), Inches(0.4), "3") +add_textbox(slide, Inches(1.0), Inches(0.35), Inches(8), Inches(0.5), + "예상 리소스 및 비용", font_size=26, color=ACCENT, bold=True) +add_textbox(slide, Inches(1.0), Inches(0.85), Inches(10), Inches(0.3), + "단계별 월 예상 비용 (인프라 + 외부 API)", font_size=12, color=TEXT_DIM) + +# 비용 카드 3개 +cost_data = [ + ("Stage 1", "동시 ~50명", "$170~390", "약 22~51만원/월", GREEN), + ("Stage 2", "동시 50~200명", "$960~2,160", "약 125~280만원/월", ORANGE), + ("Stage 3", "동시 200~1,000명", "$3,850~8,500", "약 500~1,100만원/월", RED), +] +for i, (stage, users, usd, krw, color) in enumerate(cost_data): + x = Inches(1.0 + i * 3.9) + card = add_rounded_rect(slide, x, Inches(1.5), Inches(3.5), Inches(1.8), SURFACE2, color) + + add_textbox(slide, x, Inches(1.6), Inches(3.5), Inches(0.3), + f"{stage} · {users}", font_size=12, color=TEXT_DIM, alignment=PP_ALIGN.CENTER) + add_textbox(slide, x, Inches(2.0), Inches(3.5), Inches(0.5), + usd, font_size=28, color=color, bold=True, alignment=PP_ALIGN.CENTER) + add_textbox(slide, x, Inches(2.6), Inches(3.5), Inches(0.3), + krw, font_size=12, color=TEXT_DIM, alignment=PP_ALIGN.CENTER) + +# 항목별 비용 상세 테이블 +add_textbox(slide, Inches(0.5), Inches(3.6), Inches(5), Inches(0.4), + "항목별 비용 상세", font_size=16, color=TEXT, bold=True) + +headers = ["항목", "Stage 1", "Stage 2", "Stage 3"] +rows = [ + ["App Server", "$50~70", "$200~400", "$600~1,000"], + ["Nginx", "포함", "포함 / VM $15~30", "VM $30~60"], + ["MySQL Primary", "B1ms $15~25", "GP $130~160", "BC $350~450"], + ["MySQL Replica", "-", "GP x1 $130~160", "BC x2 $260~360"], + ["Redis", "-", "-", "P1 $225"], + ["스토리지/네트워크", "$10~20", "$55~100", "$160~270"], + ["AI API (합계)", "$90~280", "$400~1,250", "$2,100~5,800"], +] +add_table(slide, Inches(0.5), Inches(4.1), Inches(12.3), Inches(3.0), headers, rows) + + +# ── Slide 7: 비용 구성 비중 & DB 용량 ── +slide = prs.slides.add_slide(blank_layout) +set_slide_bg(slide, BG) + +add_section_number(slide, Inches(0.5), Inches(0.4), "3") +add_textbox(slide, Inches(1.0), Inches(0.35), Inches(8), Inches(0.5), + "Stage 3 비용 구성 & DB 용량", font_size=26, color=ACCENT, bold=True) + +# 좌측: Stage 3 비용 구성 (수평 바 차트 시뮬레이션) +add_textbox(slide, Inches(0.5), Inches(1.2), Inches(5), Inches(0.4), + "Stage 3 월 비용 구성 비중", font_size=16, color=TEXT, bold=True) + +cost_items = [ + ("Creatomate", 2000, RED), + ("Suno AI", 1400, ORANGE), + ("App Server", 800, ACCENT), + ("OpenAI API", 550, ACCENT2), + ("MySQL Primary", 400, ORANGE), + ("MySQL Replica x2", 310, ORANGE), + ("Redis Premium", 225, RED), + ("스토리지/네트워크", 215, GREEN), + ("Nginx", 45, GREEN), +] +total = sum(v for _, v, _ in cost_items) +max_bar_width = 5.0 # inches + +y_start = Inches(1.7) +for i, (label, value, color) in enumerate(cost_items): + y = y_start + Emu(int(i * Inches(0.45))) + bar_w = Inches(max_bar_width * value / total) + + # 바 + bar = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(2.2), y, bar_w, Inches(0.32)) + bar.fill.solid() + bar.fill.fore_color.rgb = color + bar.line.fill.background() + + # 레이블 + add_textbox(slide, Inches(0.5), y, Inches(1.6), Inches(0.32), + label, font_size=9, color=TEXT_DIM, alignment=PP_ALIGN.RIGHT) + + # 값 + pct = value / total * 100 + val_x = Inches(2.3) + bar_w + add_textbox(slide, val_x, y, Inches(1.5), Inches(0.32), + f"${value:,} ({pct:.0f}%)", font_size=9, color=TEXT_DIM) + +add_textbox(slide, Inches(0.5), y_start + Emu(int(len(cost_items) * Inches(0.45))), Inches(6), Inches(0.3), + f"AI API 비중: 전체의 약 66% (${(2000+1400+550):,} / ${total:,})", + font_size=11, color=ACCENT, bold=True) + +# 우측: DB 용량 예측 +add_textbox(slide, Inches(7.5), Inches(1.2), Inches(5), Inches(0.4), + "DB 용량 예측 (1년 후)", font_size=16, color=TEXT, bold=True) + +headers = ["", "Stage 1\n(500명)", "Stage 2\n(5,000명)", "Stage 3\n(50,000명)"] +rows = [ + ["DB 용량", "~1.2GB", "~12GB", "~120GB"], + ["Blob 스토리지", "~1.1TB", "~11TB", "~110TB"], + ["MySQL 추천", "32GB SSD", "128GB SSD", "512GB SSD"], +] +add_table(slide, Inches(7.5), Inches(1.7), Inches(5.3), Inches(1.8), headers, rows) + +# 비용 최적화 팁 +add_textbox(slide, Inches(7.5), Inches(3.8), Inches(5), Inches(0.4), + "비용 최적화 팁", font_size=16, color=TEXT, bold=True) + +items = [ + ("3rd party 의존도: ", "낮춰야 함 (AI API가 전체 비용의 66%)"), + ("Blob Lifecycle: ", "30일 미접근 미디어 → Cool 티어 자동 이전"), + ("App Server: ", "비활성 시간대(야간) 인스턴스 축소"), + ("OpenAI Batch API: ", "비실시간 가사생성은 50% 절감 가능"), + ("Reserved Instances: ", "1년 예약 시 ~30% 할인"), +] +add_bullet_list(slide, Inches(7.5), Inches(4.3), Inches(5.3), Inches(3), items, font_size=11) + + +# ── 저장 ── +output_path = "/Users/marineyang/Desktop/work/code/o2o-castad-backend/docs/architecture.pptx" +prs.save(output_path) +print(f"PPT 생성 완료: {output_path}")