o2o-castad-backend/docs/generate_ppt.py

552 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
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}")