목차(클릭하세요)
반복적 업무지침이므로 skill로 만들어 PPT를 만들때마다 호출
핵심은 단순히 프롬프트 한 번 쓰는 게 아니라, 수정 기록을 skill 파일로 축적하는 것.
쓸수록 나아지는 나만의 AI 시스템으로 업그레이드 하는 것이 진짜 목표!
#주의(상식) : 기본적으로 PPTX관련된 스킬은 로컬용 클로드를 pc에 설치하였다면 설정되어있을 가능성이 매우 높음
•
PPTX스킬 지침 확인
[참고영상]
1. Claude 채팅으로 발표자료 만들기(기획+제작)
총 2단계 작업: 기획하기
제작하기
1-1. 스크립트 준비 및 업로드
•
발표할 내용을 텍스트 스크립트로 미리 준비해서 Claude에 업로드함
•
회의 자료, 보고서 등 형식 무관
이 스크립트를 발표자료용 씬으로 나눠줘
각 씬마다 화면에 들어갈 핵심 텍스트랑 어떤 비주얼로 보여줄건지 B-roll 플랜 계획을 세워줘
한줄도 빠뜨리지 않고 씬으로 나눠주고
각 씬에 비주얼 유형을 배정해줘 하얀색 배경에 서브스탭 강조는 glow로 퍼센티지 같은 것들은 막대나 원형 그래프로 표현하는 것도 좋아
디자인은 기본적으로 글래스 모피즘이 적용된 트렌디&심플 스타일
시각화가 강조 되었으면 좋겠어
Plain Text
복사
•
참고: B-roll플랜
◦
B롤은 메인 비디오 클립을 보완하는 영상
◦
A롤은 메인 비디오클립(전체 주제와 맥락을 담은 메인 스토리)
예컨데, 음식 관련 영상 컨텐츠에서 음식점에서 음식을 먹는 스토리가 A-ROLL이라면, 음식을 주문한 후에 완성된 음식이 나왔을 때, 바로 음식만 나오고 이것을 먹는다면 스토리가 유려하게 이어지지 않으니 음식을 만드는 장면을 중간에 넣을 수 있는데, 이 음식 만드는 장면을 B-ROLL이라 할 수 있음
•
결과 확인
텍스트가 너무 많거나, 씬을 합치고 싶을 때 바로 말하면 됨.
1-2. 기획단계에서의 꿀팁 및 생성, 실시간 수정
•
원하는 디자인 요소나 스타일이 있다면 첨부해도 좋음
•
예시 디자인을 활용한 프롬프트 추가
이 기획대로 1920*1080풀스크린으로 PPTX를 작성할꺼야.
화살표 키로 서브 스텝이 순차적으로 등장하게 해줘
화이트 배경의 서브스텝, 강조는 글로우로.
가장 중요한 슬라이드에는 빨간색으로 포인트가 들어간 비쥬얼 요소 적용(예시 첨부파일 참고)
퍼센티지 같은 것들은 막대나 원형 그래프로 표현해줘.
시각화가 강조됐으면 좋겠어.
Plain Text
복사
#기획이 끝났으면 생성 프롬프트로 넘어감
이 기획대로 1920×1080 풀스크린 PPTX 발표 자료를 만들어줘.
다크 배경의 깔끔한 디자인으로, 화살표 키로 요소가 순차적으로 등장하게 해줘.
Plain Text
복사
•
기획과 제작을 분리하는 이유: 기획이 먼저 잡히면 Claude가 제작에 집중할 수 있어서 오류 확률이 확 줄어듦
•
수정도 대화하듯이 하면 됨:
텍스트가 겹치는 경우 있어. 글자 흐리게 만드는 건 빼줘.
카드랑 글자가 너무 작아. 글래스모피즘 디자인으로, 강조는 빛으로만 해줘.
Plain Text
복사
•
완성후 PPTX 다운로드
원하는 페이지 수정 가능
1-3. 토큰을 절약하고 싶다면?
•
클로드 이건 뭐..작업 2번이면 4시간을 또 기달려야 한다는 말인가?
•
작업 한번에 유료 플랜 토큰의 40%가 사용되는 마법
•
토큰을 아껴보자!
◦
완성 후 HTML 다운로드 → F11로 전체화면 프레젠테이션 가능
◦
이상한 부분을 모두 수정 후에 마지막에만 PPTX로 변환
[결과물1_애니메이션 미포함]
•
예시작품
•
여기에서도 몇번의 수정을 거쳐, 각 슬라이드간 전환효과가 적절하게 배치 되었는지, 요소간 애니메이션들이 작동되고 있는지 체크하고 수정
#애니메이션 자동화를 위한 “노가다 수정”단계
•
HTML에서는 애니메이션, 화면 전화 효과, 마우스 오버 효과 등이 잘 구현 되지만, PPTX 에는 그렇지 못함
◦
HTML은 '살아있는 브라우저'에서 돌아가고, PPTX는 '약속된 규칙(XML)의 뭉치’이기 때문
◦
HTML: 웹은 처음부터 '움직임'과 '상호작용'을 위해 만들어짐
▪
. DOM(Document Object Model)이라는 구조 덕분에, 코드로 실시간에 요소를 바꾸거나 애니메이션을 넣는 게 매우 자연스러움
◦
PPTX: 기본적으로 '종이(인쇄물)'의 디지털 버전
▪
슬라이드라는 고정된 페이지 위에 애니메이션이라는 부수적인 효과를 '선언'해 놓은 방식이라, 구조가 매우 경직되어 있음
•
근본적인 PPTX의 문제?
◦
애니메이션 하나를 넣으려면 p:timing, p:tnLst, p:childTnLst 등 수십 단계의 중첩된 XML 노드를 직접 건드려야 함
◦
마우스 오버 같은 효과는 PPTX 표준 규격(OOXML) 내에서 구현하기가 매우 까다롭고 제한적
◦
우리가 '애니메이션 1번'이라고 이름 붙여도, 저장하는 순간 파워포인트가 내부적으로 Shape 5, Text 12 등으로 이름을 멋대로 바꿔버림. 주소가 계속 바뀌니 자동화 도구가 작동하기 쉬운 환경은 아님
•
해결책?
◦
이 '깐깐한 서류 규칙(XML)'을 완벽하게 통제할 수 있는 Python 같은 도구를 활용해 자동화해야함
[전체 흐름과 이키텍쳐]
•
GPT/Claude → JSON 생성
•
Python → PPT 생성
•
XML → 애니메이션 삽입
[결과물2_애니메이션 포함]
•
아래 그림처럼 객체의 이름을 제각각으로 설정되다보니 오류수정이 쉽지 않아 보임
◦
특히 이렇게 각 오브젝트의 순서까지 제각각이라. 이걸 모두 반영하려면 상당히 프롬프트가 복잡해질 거 같아, ‘애니메이션 자동화’는 빠른 포기를 권장
•
이 작업을 하는 과정에서 엄청난 토큰을 소모하고, 시간적 낭비를 경험하여 추천하고 싶지는 않음
•
역공학 기법을 통해 , 이미 애니메이션이 적용된 PPTX 를 제공하여 보다 작업시간을 단축할 수 있었음
•
이렇게 애니메이션까지 적용된 PPTX를 열어보면 여러가지 수정해야할 상황들이 보일 것임
◦
예) 텍스트와 도형간 그룹화 문제
[최종본]
[파워포인트의 그룹화 핵심원칙]
> python-pptx로 애니메이션 그룹을 만들 때, **"텍스트는 항상 배경보다 나중에 그룹에 추가"** 가 핵심이다.
> 그러나 이것만으론 부족하다 — **그룹 자체의 z-order 위치**와 **중첩 리스트 처리**까지 세 가지가 동시에 맞아야 한다.
> 💬 시스템 프롬프트 요약: "python-pptx로 PPTX를 코드로 생성할 때, MK_GROUP 함수로 도형들을 그룹화하여 하나의 애니메이션 단위로 처리. 그룹 내 z-order와 spTree 삽입 위치가 모두 정확해야 함."
## 배경
python-pptx로 PPTX를 코드 생성할 때, 도형 여러 개를 하나의 클릭 애니메이션으로 묶으려면 OOXML `<p:grpSp>` 그룹 + `<p:bldP>` 타이밍을 직접 작성해야 함. 이 과정에서 반복적으로 발생한 버그 3가지와 각각의 수정 원칙을 기록함.
---
## 1. 버그 1 — 그룹이 spTree 맨 끝에 삽입되어 텍스트를 덮음
### 현상
```
초기 상태(애니메이션 전): 텍스트가 배경 없이 허공에 뜬다
클릭 후: 카드 배경이 나타나며 텍스트를 덮어버린다
```
### 원인
```python
# ❌ 기존 코드
grp_el = etree.fromstring(grp_xml.encode())
for s in shapes:
el = s._element
el.getparent().remove(el)
grp_el.append(el)
spTree.append(grp_el) # ← 맨 끝에 추가 = 가장 높은 z-order
```
OOXML에서 spTree 내 원소 순서 = z-order (뒤쪽 = 위). 그룹이 마지막에 삽입되면 그룹 안의 배경 rect가 그룹 밖에 남아있던 텍스트박스들 **위에** 렌더링됨.
### 수정
```python
# ✅ 수정 코드
all_sp = list(spTree)
shape_ids = {id(s._element) for s in shapes}
positions = [i for i, el in enumerate(all_sp) if id(el) in shape_ids]
insert_pos = min(positions) # 첫 번째 도형의 원래 위치
for s in shapes:
el = s._element
el.getparent().remove(el)
grp_el.append(el)
spTree.insert(insert_pos, grp_el) # ← 원래 자리에 삽입
```
**핵심 원칙:** 그룹은 반드시 **첫 번째 멤버 도형의 원래 위치**에 삽입해야 함.
그래야 그룹 뒤에 남아있는 독립 도형들이 그룹 위에 렌더링되지 않음.
---
## 2. 버그 2 — 미캡처 텍스트가 그룹 밖에 떠있음
### 현상
```
초기 상태: "p = 1", "D = |x₂−x₁| + |y₂−y₁|" 등 수식 텍스트만 배경 없이 보임
클릭 후: 카드 배경 나타남 → 텍스트는 이미 그룹 '뒤'라 가림 당함
```
### 원인
```python
# ❌ 변수에 담지 않아서 MK_GROUP에 전달 불가
pv = R(sl, x, y, w, h, fill=bg)
T(sl, pval, x, y, w, h, ...) # 반환값 무시 → 그룹 밖에 남음
gid = MK_GROUP(sl, bg_card, pv) # pval 텍스트 미포함
```
### 수정
```python
# ✅ 모든 관련 도형을 변수로 캡처
pv = R(sl, x, y, w, h, fill=bg)
pv_t = T(sl, pval, x, y, w, h, ...) # 캡처!
gid = MK_GROUP(sl, bg_card, pv, pv_t) # 함께 그룹화
```
**핵심 원칙:** MK_GROUP에 전달할 **모든 관련 도형을 변수로 저장**.
`T(sl, ...)` 반환값을 버리면 그 텍스트는 영원히 그룹 밖에 남음.
루프 내 생성 패턴:
```python
for k, (name, sub, col) in enumerate(metrics):
mc = CARD(sl, x, y, w, h, border=col)
mt1 = T(sl, name, ...) # 이름 텍스트
mt2 = T(sl, sub, ...) # 설명 텍스트
els.extend([mc, mt1, mt2]) # 셋 다 포함
```
---
## 3. 버그 3 — 중첩 리스트에서 CARD() 배경이 누락됨
### 현상
```
설명 카드 영역: 제목/본문 텍스트가 배경 없이 보임
클릭 후: 배경 rect가 나타나며 텍스트를 덮어버림
```
### 원인
```python
# CARD()는 리스트를 반환함
c = CARD(sl, x, y, w, h, border=col, accent=col)
# c = [bg_rect, accent_rect] ← 리스트
exp_shapes = [c, tl, bd]
# exp_shapes = [[bg_rect, accent_rect], tl, bd] ← 중첩 리스트!
# ❌ 단일 레벨 언팩: 중첩 리스트는 .left 없음 → 스킵
for a in args:
items = a if isinstance(a, (list, tuple)) else [a]
for x in items:
if hasattr(x, 'left'): # [bg_rect, accent_rect]는 .left 없음!
shapes.append(x) # bg_rect, accent_rect 누락!
```
`[bg_rect, accent_rect]` 자체는 list이므로 `.left` 속성이 없어 통째로 스킵됨.
bg_rect가 그룹 밖에 남아 텍스트를 덮음.
### 수정
```python
# ✅ 재귀 딥플래튼으로 어떤 깊이든 Shape 추출
def _deep_flatten(x):
result = []
if isinstance(x, (list, tuple)):
for item in x:
result.extend(_deep_flatten(item)) # 재귀
elif x is not None and hasattr(x, 'left'):
try:
_ = x.left
result.append(x)
except:
pass
return result
shapes = _deep_flatten(args)
```
**핵심 원칙:** CARD()처럼 **리스트를 반환하는 헬퍼 함수**가 있다면,
단일 레벨 언팩이 아닌 **재귀 딥플래튼**을 사용해야 함.
---
## 4. 완성된 MK_GROUP 함수
```python
def MK_GROUP(sl, *args):
"""도형들을 grpSp로 묶어 하나의 애니메이션 단위로 처리. shape_id 반환.
★ 버그 수정 3가지 모두 반영:
1. 재귀 딥플래튼 (CARD()=[bg,accent] 중첩 리스트 누락 방지)
2. spTree insert_pos = 첫 도형 원래 위치 (z-order 유지)
3. 도형을 전달 순서대로 그룹에 추가 (내부 z-order 유지)
"""
def _deep_flatten(x):
result = []
if isinstance(x, (list, tuple)):
for item in x:
result.extend(_deep_flatten(item))
elif x is not None and hasattr(x, 'left'):
try: _ = x.left; result.append(x)
except: pass
return result
shapes = _deep_flatten(args)
if not shapes: return None
spTree = sl.shapes._spTree
all_sp = list(spTree)
# 그룹 삽입 위치: 첫 번째 멤버 도형의 원래 z-order 위치
shape_ids = {id(s._element) for s in shapes}
positions = [i for i, el in enumerate(all_sp) if id(el) in shape_ids]
if not positions: return None
insert_pos = min(positions)
# 바운딩 박스 계산
lft = min(int(s.left) for s in shapes)
top_v = min(int(s.top) for s in shapes)
rgt = max(int(s.left + s.width) for s in shapes)
btm = max(int(s.top + s.height) for s in shapes)
# 고유 ID 생성
all_ids = [int(el.attrib['id']) for el in sl._element.iter()
if 'id' in el.attrib and el.attrib['id'].isdigit()]
nid = max(all_ids) + 1 if all_ids else 200
ns_p = "http://schemas.openxmlformats.org/presentationml/2006/main"
ns_a = "http://schemas.openxmlformats.org/drawingml/2006/main"
grp_xml = (
f'<p:grpSp xmlns:p="{ns_p}" xmlns:a="{ns_a}">'
f'<p:nvGrpSpPr><p:cNvPr id="{nid}" name="Grp{nid}"/>'
f'<p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
f'<p:grpSpPr><a:xfrm>'
f'<a:off x="{lft}" y="{top_v}"/>'
f'<a:ext cx="{rgt-lft}" cy="{btm-top_v}"/>'
f'<a:chOff x="{lft}" y="{top_v}"/>'
f'<a:chExt cx="{rgt-lft}" cy="{btm-top_v}"/>'
f'</a:xfrm></p:grpSpPr></p:grpSp>'
)
grp_el = etree.fromstring(grp_xml.encode())
# 도형을 전달 순서대로 그룹에 이동 (순서 = 내부 z-order)
for s in shapes:
el = s._element
p = el.getparent()
if p is not None: p.remove(el)
grp_el.append(el)
# ★ 핵심: append가 아닌 insert (원래 위치 유지)
spTree.insert(insert_pos, grp_el)
return nid
```
---
## 5. 호출 규칙 체크리스트
MK_GROUP을 호출하기 전 반드시 확인:
- **모든 관련 도형이 변수에 저장되어 있는가?** `T(sl, ...)` 반환값을 버리지 않았는가
- **배경 rect → 텍스트 순서로 전달하는가?** 배경이 먼저여야 그룹 내 z-order가 맞음
- **CARD() 반환값이 다른 리스트 안에 중첩되어 있지는 않은가?** 딥플래튼이 처리하지만, 의도적으로 확인
- **루프 안에서 생성되는 도형은 루프 밖 리스트에 `.extend([mc, t1, t2])`로 누적하는가?**
```python
# ✅ 올바른 패턴
bg = CARD(sl, x, y, w, h, border=col) # [bg_rect, accent_rect]
tl = T(sl, title, ...)
bd = T(sl, body, ...)
gid = MK_GROUP(sl, bg, tl, bd) # 배경 → 텍스트 순서
# ✅ 루프 패턴
els = []
for name, sub, col in data:
mc = CARD(sl, x, y, w, h, border=col)
mt1 = T(sl, name, ...)
mt2 = T(sl, sub, ...)
els.extend([mc, mt1, mt2]) # 3개씩 누적
gid = MK_GROUP(sl, els)
```
---
## 참고: OOXML z-order 규칙
| 위치 | 의미 |
|------|------|
| spTree 앞쪽 원소 | 슬라이드 아래 (배경) |
| spTree 뒤쪽 원소 | 슬라이드 위 (전경) |
| grpSp 내부 순서 | 동일 규칙 적용 |
| bldP animBg="1" | 그룹이 초기 숨김 상태 |
grpSp의 `chOff == off`, `chExt == ext` 이면 자식 도형의 좌표 = 슬라이드 절대 좌표 (변환 없음).
Plain Text
복사
2. Claude 스킬 파일 만들기
2-1. 스킬이란?
•
프롬프트: 일회성. 매번 처음부터 다시 설명해야 함
•
스킬 파일: 자산. 수정 기록이 축적되어 쓸수록 나아짐
구분 | 프롬프트 | 스킬 파일 |
지속성 | ||
수정량 | 매번 반복 | 점점 줄어듦 |
성격 | 도구 | 나만의 시스템 |
2-2. 스킬 파일 만드는 법
•
작업이 끝난 직후 Claude에게 아래처럼 요청
•
사용의 편의성을 위해 ‘트리거 키워드’를 등록해두자!
지금 우리가 했던 작업들을 바탕으로 Claude 스킬 파일을 만들어줘.
-스킬의 이름은 'phagos pptx'
-트리거 키워드는 '우리의 pptx스킬', '양파고의 pptx스킬', 'pptx 자동화 스킬' 등
-수정한 기록들도 다시는 그런 실수 안 하게 디테일하게 기록해.(중복되는 수정사항은 통합 처리)
Plain Text
복사
•
생성된 스킬을 저장하면, 다음부터는 스크립트만 던지고 “발표 자료 만들어줘” 한 마디면 끝
•
씬 나누는 기준, 비주얼 스타일, 서브스텝 구조 전부 스킬 안에 담겨 있어서 Claude가 알아서 적용함
◦
약 500자 이상의 마크다운으로 만들어짐!
•
스킬 저장소에 가보면 이렇게 나만의 스킬이 만들어져 있음
•
이 스킬 역시 계속 업그레이드&수정 할 수 있음
◦
업그레이드가 가능한 이유는 클로드 스킬중에 ‘skill creator’이 유료사용자에게는 이미 설치되어 있기 때문에
•
스킬지침 업데이트 예시
◦
스킬을 계속 업데이트하여 내 스타일을 정립할 수 있음(강점이긴 한데, 초기 토큰이 많이 필요)
기본적으로 하얀색 배경으로 슬라이드를 만들어가지만,
다음 상황에서는 예시이미지를 참고하여 PPT에 드라마틱한 효과를 줄수도 있어.
첫페이지, 중간에 주의전환, 강조할 페이지에는 검은색 배경의 보라색, 그라이데이션 색이 입혀진 텍스트가 반영된 디자인도 할 수 있도록 스킬지침 업데이트해
Plain Text
복사
[애니메이션 일부자동화까지 포함된 스킬]
•
스킬도 버전업을 할때 마다 뒤에 버전 넘버링이 업데이트 됨
•
예시 스킬 지침MD
---
name: phagos-pptx
description: "양파고(Yangphago)의 python-pptx 기반 PPTX 자동화 스킬. '우리의 pptx스킬', '양파고의 pptx스킬', 'pptx 자동화 스킬', 'pptx 만들어줘', '프레젠테이션 코드로 생성', 'python-pptx 애니메이션', 'PPTX 그룹화', 'PPTX 클릭 애니메이션', 'bldLst', 'bldP', 'OOXML 애니메이션' 등을 언급할 때 반드시 사용. python-pptx + lxml로 슬라이드를 코드 생성. 글래스모피즘·다크드라마틱·교육용 코드블럭 포함 완전한 디자인 시스템. ★ 이 스킬 전체를 읽고 시작할 것. 형광색(FF0000·9900FF·FFFF00) 절대 사용 금지."
---
# Phago's PPTX 자동화 스킬 (디자인 + 기술 통합)
---
## 0. 디자인 철학 — 글래스모피즘 트렌디&심플
### 기본 (화이트 베이스)
모든 슬라이드의 기본 스타일:
- **배경**: 순백(FFFFFF) 또는 극연한 그레이-블루(F0F7FF)
- **카드**: 반투명 글래스 느낌 — 연한 fill + 1px 연보더 + 소프트 섀도
- **강조**: glow 이펙트(effectLst > glow) 또는 레드(RD) 포인트
- **타이포**: Calibri 계열, 위계 명확 (제목 28~32pt / 소제목 13~16pt / 본문 11~12pt)
- **서브스텝 강조**: ADD_GLOW() 사용 (형광 번짐 효과)
- **퍼센티지·수치**: 반드시 막대 또는 원형 차트로 시각화
- **중요 슬라이드**: 빨간색(DC2626) 포인트 요소 적용
### ★ 예외 — 다크 드라마틱 슬라이드 (SCENE_DARK)
아래 3가지 상황에서는 검은 배경 + 그라디언트 텍스트 디자인 적용:
1. **첫 페이지** (오프닝 임팩트)
2. **중간 주의전환** (섹션 전환, 챕터 구분)
3. **핵심 강조 페이지** (가장 중요한 메시지 1개)
다크 드라마틱 스타일:
- 배경: 거의 검은 네이비(0D0F1A)
- 메인 텍스트: 흰색(FFFFFF), 굵고 크게
- **키워드**: 보라→블루 그라디언트(GRAD_T) + glow 효과
- 보조 텍스트: 밝은 그레이(9CA3AF) 또는 틸(14B8A6)
- 장식 요소: 반투명 원형 OV, 테두리 없는 부유 카드
- 하단 배지/배너: 반투명 다크 카드
---
## 1. 색상 팔레트
```python
W='FFFFFF'; DK='1A1D3E'; RD='DC2626'
BL='3B82F6'; GR='059669'; TE='0D9488'; AM='D97706'
PU='7C3AED'; NV='1E3A8A'; SL='475569'; CO='E55A4E'
IND='6366F1' # 인디고 — 교육/코딩 메인 컬러
# 라이트 (글래스 카드 fill)
RDLT='FEF2F2'; BLLT='DBEAFE'; GRLT='D1FAE5'
TELT='CCFBF1'; AMLT='FEF3C7'; PULT='EDE9FE'; INDLT='EEF2FF'
# 미디엄 (강조선·차트)
RDMD='FCA5A5'; BLMD='93C5FD'
# 그레이
G1='111827'; G2='374151'; G3='6B7280'; G4='D1D5DB'; G5='F3F4F6'; GB='F8FAFC'
# 글래스 전용
GLASS='F0F7FF' # 극연한 블루-화이트
GLASS_BORDER='C7D9F0' # 글래스 테두리
# ─ 다크 드라마틱 전용 ─
DARK_BG='0D0F1A'; DARK_BG2='13162A'; DARK_CARD='1E2235'
GRAD_PU1='7C3AED'; GRAD_PU2='818CF8'
GRAD_BL1='6366F1'; GRAD_BL2='60A5FA'
GRAD_TE1='14B8A6'; GRAD_TE2='6EE7B7'
# ─ 코드 블럭 전용 ─────────────────────────────────────────
# ★ 절대 형광색(FF0000·9900FF·FFFF00) 사용 금지 — 코드 색상도 팔레트 준수
CODE_BG = '0F172A' # 코드 블럭 배경 (어두운 네이비)
CODE_LINE= '1E293B' # 줄번호 영역
CODE_W = 'E2E8F0' # 기본 코드 텍스트
CODE_GR = '4ADE80' # 함수명·키워드 (그린)
CODE_BLU = '60A5FA' # 변수·속성 (블루)
CODE_YL = 'FDE68A' # 문자열 (옐로우)
CODE_PU = 'A78BFA' # 클래스·내장함수 (퍼플)
CODE_RD = 'F87171' # 에러 (레드)
CODE_CMT = '64748B' # 주석 (그레이)
```
---
## 2. 기본 환경 설정
```python
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.chart.data import ChartData, XyChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
from pptx.enum.shapes import MSO_CONNECTOR_TYPE
from lxml import etree
PPTNS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
ANS = 'http://schemas.openxmlformats.org/drawingml/2006/main'
P14NS = 'http://schemas.microsoft.com/office/powerpoint/2010/main'
P_ = '{' + PPTNS + '}'; A_ = '{' + ANS + '}'
prs = Presentation()
prs.slide_width = Inches(13.3)
prs.slide_height = Inches(7.5)
BLANK = prs.slide_layouts[6]
def NS_(): return prs.slides.add_slide(BLANK)
def i(v): return Inches(v)
def pt(v): return Pt(v)
def rgb(h):
h = h.strip('#')
return RGBColor(int(h[0:2],16), int(h[2:4],16), int(h[4:6],16))
```
---
## 3. 핵심 디자인 컴포넌트
### 3-1. 기본 도형 헬퍼
```python
def R(sl, x, y, w, h, fill=W, line=None, lw=0.5, rnd=False):
s = sl.shapes.add_shape(5 if rnd else 1, i(x), i(y), i(w), i(h))
if fill: s.fill.solid(); s.fill.fore_color.rgb = rgb(fill)
else: s.fill.background()
if line: s.line.color.rgb = rgb(line); s.line.width = pt(lw)
else: s.line.fill.background()
if rnd:
sp=s._element; spPr=sp.find(P_+'spPr')
pg=spPr.find(A_+'prstGeom') if spPr else None
if pg is not None:
al=pg.find(A_+'avLst')
if al is None: al=etree.SubElement(pg, A_+'avLst')
for g in al.findall(A_+'gd'): al.remove(g)
gd=etree.SubElement(al, A_+'gd')
gd.set('name','adj'); gd.set('fmla','val 7000')
return s
def T(sl, text, x, y, w, h, sz=14, bold=False, col=G1,
al='l', va='m', italic=False, mono=False):
"""텍스트박스. \\n 줄바꿈 지원.
★ 기본 sz=14pt — 투영 환경 기준 (최소 14pt 유지)."""
tb=sl.shapes.add_textbox(i(x),i(y),i(w),i(h))
tf=tb.text_frame; tf.word_wrap=True
tf.vertical_anchor={'t':MSO_ANCHOR.TOP,'m':MSO_ANCHOR.MIDDLE,
'b':MSO_ANCHOR.BOTTOM}.get(va,MSO_ANCHOR.TOP)
al_map={'l':PP_ALIGN.LEFT,'c':PP_ALIGN.CENTER,'r':PP_ALIGN.RIGHT}
face='Courier New' if mono else 'Noto Sans KR'
for k,line in enumerate(str(text).split('\n')):
par=tf.paragraphs[0] if k==0 else tf.add_paragraph()
par.alignment=al_map.get(al,PP_ALIGN.LEFT)
run=par.add_run(); run.text=line
run.font.name=face; run.font.size=pt(sz)
run.font.bold=bold; run.font.italic=italic
run.font.color.rgb=rgb(col)
return tb
def OV(sl,x,y,w,h,fill=W,line=None):
s=sl.shapes.add_shape(9,i(x),i(y),i(w),i(h))
if fill: s.fill.solid(); s.fill.fore_color.rgb=rgb(fill)
else: s.fill.background()
if line: s.line.color.rgb=rgb(line); s.line.width=pt(0.8)
else: s.line.fill.background()
return s
def LN(sl,x1,y1,x2,y2,col=G4,lw=0.5):
c=sl.shapes.add_connector(MSO_CONNECTOR_TYPE.STRAIGHT,i(x1),i(y1),i(x2),i(y2))
c.line.color.rgb=rgb(col); c.line.width=pt(lw)
return c
```
### 3-2. CARD / GLASS_CARD
```python
def CARD(sl, x, y, w, h, fill=W, border=G4, accent=None, rnd=True):
"""일반 컬러 카드. ★ 리스트 반환 — MK_GROUP 중첩 주의"""
shapes=[]
bg=R(sl,x,y,w,h,fill=fill,line=border,lw=0.5,rnd=rnd)
shapes.append(bg)
if accent:
acc=R(sl,x,y,0.06,h,fill=accent,rnd=False)
shapes.append(acc)
return shapes
def GLASS_CARD(sl, x, y, w, h, accent_col=None):
"""글래스모피즘 카드: F0F7FF fill + C7D9F0 테두리 + 소프트 섀도.
accent_col 지정 시 왼쪽 컬러바 추가."""
shapes=[]
bg=R(sl,x,y,w,h,fill=GLASS,line=GLASS_BORDER,lw=0.8,rnd=True)
sp=bg._element; spPr=sp.find(P_+'spPr')
if spPr is not None:
ef=spPr.find(A_+'effectLst')
if ef is None: ef=etree.SubElement(spPr,A_+'effectLst')
for s in ef.findall(A_+'outerShdw'): ef.remove(s)
shdw=etree.SubElement(ef,A_+'outerShdw')
shdw.set('blurRad','60000'); shdw.set('dist','25000')
shdw.set('dir','5400000'); shdw.set('rotWithShape','0')
sc=etree.SubElement(shdw,A_+'srgbClr'); sc.set('val','CBD5E1')
av=etree.SubElement(sc,A_+'alpha'); av.set('val','30000')
shapes.append(bg)
if accent_col:
acc=R(sl,x,y,0.05,h,fill=accent_col,rnd=False)
shapes.append(acc)
return shapes
```
### 3-3. ADD_GLOW — ★ 서브스텝 강조 핵심
```python
def ADD_GLOW(shape, color, radius_pt=8, alpha_pct=45):
"""도형에 글로우(번짐) 효과.
서브스텝 강조·중요 수치·카드 헤더에 사용.
color: hex. radius_pt: 번짐 크기(pt). alpha_pct: 투명도(%)."""
sp=shape._element; spPr=sp.find(P_+'spPr')
if spPr is None: return
ef=spPr.find(A_+'effectLst')
if ef is None: ef=etree.SubElement(spPr,A_+'effectLst')
for g in ef.findall(A_+'glow'): ef.remove(g)
gw=etree.SubElement(ef,A_+'glow')
gw.set('rad',str(int(radius_pt*12700)))
sc=etree.SubElement(gw,A_+'srgbClr'); sc.set('val',color.strip('#'))
av=etree.SubElement(sc,A_+'alpha'); av.set('val',str(alpha_pct*1000))
# 사용 예시
step_rect=R(sl,x,y,w,h,fill=BLLT,line=BL,rnd=True)
ADD_GLOW(step_rect, BL, radius_pt=10, alpha_pct=40) # 서브스텝 강조
num_text=T(sl,'87%',x,y,w,h,sz=36,bold=True,col=RD)
ADD_GLOW(num_text, RD, radius_pt=8, alpha_pct=35) # 강조 수치 글로우
```
### 3-4. 레드 포인트 슬라이드 요소 (★ 중요 슬라이드 전용)
```python
def RED_TOP(sl):
"""상단 빨간 강조 바만"""
R(sl,0,0,13.3,0.1,fill=RD)
def RED_ACCENT_SLIDE(sl):
"""상단 + 좌측 빨간 바 (중요 슬라이드 완전 세팅)"""
R(sl,0,0,13.3,0.08,fill=RD) # 상단
R(sl,0,0,0.08,7.5,fill=RD) # 좌측
def BADGE(sl, text, x, y, w=1.5, h=0.3, bg=NV, col=W):
s=R(sl,x,y,w,h,fill=bg,rnd=True)
t=T(sl,text,x,y,w,h,sz=10,bold=True,col=col,al='c',va='m')
return s,t
def HDR(sl, title, sub=''):
T(sl,title,0.5,0.22,12.3,0.72,sz=30,bold=True,col=G1)
if sub: T(sl,sub,0.5,0.95,12.3,0.28,sz=11,col=G3)
LN(sl,0.5,1.22,12.8,1.22,G4,0.5)
```
### 3-5. 다크 드라마틱 슬라이드 컴포넌트 (★ 예외 상황 전용)
```python
def DARK_SLIDE(sl, bg_col=DARK_BG):
"""슬라이드 배경을 다크 네이비로 설정."""
sl.background.fill.solid()
sl.background.fill.fore_color.rgb = rgb(bg_col)
def DARK_DECO(sl):
"""다크 슬라이드 장식 원형들 (반투명 느낌 시뮬레이션).
배경에 대형 반투명 블러 원 배치 — 보라/블루 그라디언트 느낌."""
# 대형 보라 원형 (좌측 상단)
ov1 = OV(sl, -0.8, -0.5, 4.0, 4.0, fill='1A0A3A')
# 대형 블루 원형 (우측 하단)
ov2 = OV(sl, 10.2, 4.5, 4.0, 4.0, fill='0A1A3A')
# 중간 크기 포인트 원 (장식)
ov3 = OV(sl, 0.5, 0.5, 0.35, 0.35, fill=GRAD_PU1)
ov4 = OV(sl, 12.1, 0.6, 0.22, 0.22, fill=GRAD_TE1)
return ov1, ov2, ov3, ov4
def GRAD_T(sl, text, x, y, w, h, sz=44, bold=True,
col1=GRAD_PU1, col2=GRAD_BL2, ang_deg=45,
al='c', va='m'):
"""★ 그라디언트 텍스트 (다크 슬라이드 키워드 강조 전용).
OOXML gradFill을 런 프로퍼티에 직접 주입.
col1→col2 방향 그라디언트. ang_deg: 45=대각선, 0=좌→우, 90=상→하."""
tb = sl.shapes.add_textbox(i(x), i(y), i(w), i(h))
tf = tb.text_frame; tf.word_wrap = True
tf.vertical_anchor = {'t':MSO_ANCHOR.TOP,'m':MSO_ANCHOR.MIDDLE,
'b':MSO_ANCHOR.BOTTOM}.get(va, MSO_ANCHOR.MIDDLE)
al_map = {'l':PP_ALIGN.LEFT,'c':PP_ALIGN.CENTER,'r':PP_ALIGN.RIGHT}
for k, line in enumerate(str(text).split('\n')):
par = tf.paragraphs[0] if k==0 else tf.add_paragraph()
par.alignment = al_map.get(al, PP_ALIGN.CENTER)
run = par.add_run()
run.text = line
run.font.size = pt(sz)
run.font.bold = bold
run.font.name = 'Calibri'
# 먼저 색상 설정으로 rPr 생성 유도
run.font.color.rgb = rgb(col1)
# rPr 내부 solidFill → gradFill 교체
r_el = run._r
rPr = r_el.find(A_+'rPr')
if rPr is not None:
for sf in rPr.findall(A_+'solidFill'): rPr.remove(sf)
ang = str(int(ang_deg * 60000)) # degree → EMU 각도
grad = etree.fromstring(
f'<a:gradFill xmlns:a="{ANS}">'
f'<a:gsLst>'
f'<a:gs pos="0"><a:srgbClr val="{col1.strip("#")}"/></a:gs>'
f'<a:gs pos="100000"><a:srgbClr val="{col2.strip("#")}"/></a:gs>'
f'</a:gsLst>'
f'<a:lin ang="{ang}" scaled="0"/>'
f'</a:gradFill>'
)
rPr.insert(0, grad)
return tb
def DARK_CMD_BAR(sl, cmd_text, x=0.7, y=6.55, w=11.9, h=0.65):
"""다크 슬라이드 하단 커맨드/설명 바 (터미널 스타일).
예: $ npx cerebralos init"""
bg = R(sl, x, y, w, h, fill=DARK_CARD, line='2D3252', lw=0.5, rnd=True)
prefix = T(sl, '$', x+0.25, y, 0.3, h, sz=12, bold=True,
col=GRAD_TE1, va='m', mono=True)
cmd = T(sl, ' '+cmd_text, x+0.55, y, w-1.5, h, sz=12,
col='C4CAF5', va='m', mono=True)
hint = T(sl, 'click to copy', x+w-1.4, y, 1.3, h, sz=9,
col='4B5280', al='r', va='m')
return bg, prefix, cmd, hint
```
---
## 4. B-roll 씬 플래닝 가이드 (★ 제작 전 반드시 수립)
### 4-1. 비주얼 유형 7가지
| 유형 | 설명 | 언제 |
|------|------|------|
| `SCENE_TITLE` | 다크 배경 + 대형 타이틀 + 키스탯 | 첫 슬라이드 |
| `SCENE_STEPS` | 계단형·타임라인 스텝 (glow 강조) | 단계·순서 |
| `SCENE_CARDS` | 글래스 카드 그리드 2~8개 | 개념 목록·비교 |
| `SCENE_CHART` | 막대/원형/산점도 + 설명 카드 | **수치·퍼센티지 반드시** |
| `SCENE_SPLIT` | 좌우 분할 (시각화 ↔ 설명 카드) | 이항 비교 |
| `SCENE_MATRIX` | 2×2·3×3 격자 + 설명 카드 | 혼동행렬·사분면 |
| `SCENE_SUMMARY` | 플로우차트·체계도 | 종합·요약 |
| **`SCENE_DARK`** | **검은 배경 + 그라디언트 텍스트 드라마틱** | **★ 예외: 첫 페이지·주의전환·핵심 강조** |
### 4-2. 씬 플랜 작성 형식
```
S01 | SCENE_DARK | ★ 오프닝 임팩트 — 메인 메시지 + 그라디언트 키워드 | fade 전환
S02 | SCENE_STEPS | 4단계 계단 (클릭마다 glow 강조) | push 전환
S03 | SCENE_CARDS | 동심원 계층도 + 정의 카드 3개
S04 | SCENE_CARDS | 글래스 카드 그리드 (4×2)
S05 | SCENE_SPLIT | 파이프라인 좌우 분기
S06 | SCENE_CHART | 산점도 + 설명 카드 | 차트 pulse
S07 | SCENE_CARDS | 비교 3컬럼 카드
S08 | SCENE_DARK | ★ 주의전환 — 섹션B 시작 강조 | push 전환
S09 | SCENE_CHART | 공식 카드 + 비교 막대차트 | RED + push
S10 | SCENE_CHART | 막대 시각화 + 도넛차트 | RED + push
S11 | SCENE_MATRIX | 2×2 행렬 + 설명 카드 | RED + push
S12 | SCENE_SPLIT | 비교 카드 + 도넛차트
S13 | SCENE_MATRIX | 3×3 행렬 + 막대차트 | RED + push
S14 | SCENE_DARK | ★ 핵심 메시지 강조 — 결론 1문장 | fade 전환
```
---
## 5. 씬 유형별 구현 패턴
### 5-1. SCENE_TITLE
```python
sl=NS_()
sl.background.fill.solid()
sl.background.fill.fore_color.rgb=rgb(DK)
for cx,cy,cr,cl in [(1.2,1.5,1.1,'1E2450'),(11.8,6.2,0.9,'1E2450')]:
OV(sl,cx-cr,cy-cr,cr*2,cr*2,fill=cl)
OV(sl,0.82,0.82,0.36,0.36,fill=RD) # 빨간 포인트 장식
T(sl,'타이틀',0.7,2.0,11.9,1.1,sz=52,bold=True,col=W)
R(sl,0.7,2.95,4.5,0.04,fill=RD) # 레드 언더라인
T(sl,'서브 설명',0.7,3.1,11.9,0.7,sz=30,col='93C5FD')
for v,lb,nx in [('14','항목A',2.0),('6','항목B',5.0),('3','항목C',8.0)]:
R(sl,nx,5.0,2.4,1.2,fill='252A50',rnd=True)
T(sl,v,nx,5.02,2.4,0.65,sz=34,bold=True,col=W,al='c',va='m')
T(sl,lb,nx,5.65,2.4,0.45,sz=12,col='9CA3AF',al='c')
ANIM(sl,[],'fade')
```
### 5-2. SCENE_STEPS (glow 강조 포함)
```python
steps=[('01','제목','기간','설명','한계',BL,1.4),...]
groups=[]
for k,(num,title,period,desc,limit,col,h) in enumerate(steps):
x=0.8+k*2.88; bY=7.05-h
bar=R(sl,x,bY,2.65,h,fill=col)
ADD_GLOW(bar, col, radius_pt=8, alpha_pct=30) # ★ 서브스텝 glow
tn=T(sl,num,x+0.1,bY+0.1,2.45,0.38,sz=18,bold=True,col=W)
tt=T(sl,title,x+0.1,bY+0.46,2.45,0.38,sz=12,bold=True,col=W)
elems=[bar,tn,tt]
if h>=1.8: elems.append(T(sl,period,x+0.1,bY+0.82,2.45,0.28,sz=10,col='E5E7EB'))
if h>=2.5: elems.append(T(sl,desc,x+0.1,bY+1.18,2.45,0.55,sz=11,col=W))
groups.append(ids(elems))
ANIM(sl,groups,'push')
```
### 5-3. SCENE_CARDS (글래스 카드 그리드)
```python
items=[('제목','설명',BL,BLLT),...]
groups=[]
for k,(title,sub,col,bg) in enumerate(items):
col_=k%4; row_=k//4; x=0.5+col_*3.2; y=1.38+row_*2.8
c=GLASS_CARD(sl,x,y,3.0,2.55,accent_col=col) # 글래스 카드
hd=R(sl,x,y,3.0,0.58,fill=col)
ADD_GLOW(hd,col,radius_pt=6,alpha_pct=25) # 헤더 glow
tl=T(sl,title,x,y,3.0,0.58,sz=14,bold=True,col=W,al='c',va='m')
bd=T(sl,sub,x,y+0.65,3.0,1.8,sz=11,col=G2)
gid=MK_GROUP(sl,c,hd,tl,bd)
if gid: groups.append([gid])
ANIM(sl,groups)
```
### 5-4. SCENE_CHART (수치·퍼센티지 반드시 차트)
```python
def BAR(sl,x,y,w,h,cats,series,colors=None,horizontal=True,
legend=True,show_val=True,vmin=None,vmax=None):
cd=ChartData(); cd.categories=cats
for name,vals in series: cd.add_series(name,vals)
ct=XL_CHART_TYPE.BAR_CLUSTERED if horizontal else XL_CHART_TYPE.COLUMN_CLUSTERED
cs=sl.shapes.add_chart(ct,i(x),i(y),i(w),i(h),cd); ch=cs.chart
if colors:
for s,c in zip(ch.series,colors):
s.format.fill.solid(); s.format.fill.fore_color.rgb=rgb(c)
ch.has_title=False; ch.has_legend=legend
if legend: ch.legend.position=XL_LEGEND_POSITION.BOTTOM; ch.legend.include_in_layout=False
if show_val: ch.plots[0].has_data_labels=True; ch.plots[0].data_labels.font.size=pt(9)
if vmin is not None: ch.value_axis.minimum_scale=vmin
if vmax is not None: ch.value_axis.maximum_scale=vmax
ch.chart_area.format.fill.solid(); ch.chart_area.format.fill.fore_color.rgb=rgb(W)
ch.plot_area.format.fill.solid(); ch.plot_area.format.fill.fore_color.rgb=rgb(W)
return cs
def DONUT(sl,x,y,w,h,cats,vals,colors=None):
cd=ChartData(); cd.categories=cats; cd.add_series('',vals)
cs=sl.shapes.add_chart(XL_CHART_TYPE.DOUGHNUT,i(x),i(y),i(w),i(h),cd); ch=cs.chart
if colors:
for pt_obj,c in zip(ch.series[0].points,colors):
pt_obj.format.fill.solid(); pt_obj.format.fill.fore_color.rgb=rgb(c)
ch.plots[0].has_data_labels=True; ch.plots[0].data_labels.show_percentage=True
ch.plots[0].data_labels.font.size=pt(10)
ch.has_legend=True; ch.legend.position=XL_LEGEND_POSITION.BOTTOM
ch.legend.include_in_layout=False; ch.has_title=False
ch.chart_area.format.fill.solid(); ch.chart_area.format.fill.fore_color.rgb=rgb(W)
return cs
def SCATTER(sl,x,y,w,h,series_data):
xd=XyChartData()
for name,pts in series_data:
s=xd.add_series(name)
for px,py in pts: s.add_data_point(px,py)
cs=sl.shapes.add_chart(XL_CHART_TYPE.XY_SCATTER,i(x),i(y),i(w),i(h),xd); ch=cs.chart
ch.has_title=False; ch.has_legend=True; ch.legend.position=XL_LEGEND_POSITION.BOTTOM
ch.chart_area.format.fill.solid(); ch.chart_area.format.fill.fore_color.rgb=rgb(W)
return cs
# ★ 차트 등장 시 pulse 효과
sc=SCATTER(sl,...)
groups.append([sc.shape_id])
ANIM(sl,groups,trans='push',chart_ids={sc.shape_id},pulse_ids={sc.shape_id})
```
### 5-5. SCENE_SPLIT (좌우 분할)
```python
sc=SCATTER(sl,0.5,1.28,6.3,5.55,series_data)
groups=[]; groups.append([sc.shape_id])
for k,(title,body,col) in enumerate(infos):
y=1.35+k*1.82
c=GLASS_CARD(sl,7.0,y,5.8,1.65,accent_col=col)
tl=T(sl,title,7.2,y+0.1,5.4,0.38,sz=13,bold=True,col=col)
bd=T(sl,body,7.2,y+0.5,5.4,1.02,sz=11.5,col=G2)
ADD_GLOW(tl,col,radius_pt=6,alpha_pct=30) # ★ 타이틀 glow
gid=MK_GROUP(sl,c,tl,bd)
if gid: groups.append([gid])
ANIM(sl,groups,trans=None,chart_ids={sc.shape_id},pulse_ids={sc.shape_id})
```
### 5-6. SCENE_MATRIX (중요 슬라이드 — 레드 포인트)
```python
sl=NS_()
RED_ACCENT_SLIDE(sl) # ★ 상단 + 좌측 빨간 바
HDR(sl,'제목','서브설명')
BADGE(sl,'★ 핵심',8.5,1.28,2.4,0.3,bg=RD)
positions=[(1.6,2.1),(3.92,2.1),(1.6,3.25),(3.92,3.25)]
matrix_items=[('TP','True Positive','맞게 양성',GRLT,GR),...]
groups=[]
for k,((x,y),(abbr,label,desc,fill,col)) in enumerate(zip(positions,matrix_items)):
mc=CARD(sl,x,y,2.2,1.02,fill=fill,border=col,rnd=False)
ADD_GLOW(mc[0],col,radius_pt=6,alpha_pct=25) # ★ 카드 glow
ab=T(sl,abbr,x,y,2.2,0.42,sz=20,bold=True,col=col,al='c',va='m')
lb=T(sl,label,x,y+0.44,2.2,0.22,sz=10,bold=True,col=col,al='c')
ds=T(sl,desc,x,y+0.66,2.2,0.32,sz=9.5,col=G2,al='c')
gid=MK_GROUP(sl,mc,ab,lb,ds)
if gid: groups.append([gid])
ANIM(sl,groups,'push')
```
### 5-7. SCENE_DARK ★ 예외 — 드라마틱 다크 슬라이드
**사용 상황**: 첫 페이지 / 섹션 전환 / 핵심 메시지 강조 (전체 슬라이드의 10~20% 이내)
#### 패턴 A — 오프닝 임팩트형 (참고 이미지1 스타일)
```python
sl = NS_()
DARK_SLIDE(sl) # 배경 = 0D0F1A
DARK_DECO(sl) # 장식 원형 배치 (배경에 겹쳐서)
# 상단 배지/태그 (선택)
badge_bg = R(sl, 3.2, 0.9, 3.5, 0.38, fill=DARK_CARD, line='2D3252', rnd=True)
badge_t = T(sl, '● v1.2.1 — MIT License', 3.2, 0.9, 3.5, 0.38,
sz=11, col='9CA3AF', al='c', va='m')
# 메인 텍스트 — 흰색 기본 + 키워드 그라디언트
# 방법: 같은 줄에 흰색 텍스트와 그라디언트 텍스트를 x좌표로 분리 배치
T(sl, 'Built to ', 1.2, 2.0, 4.0, 1.0, sz=56, bold=True, col=W)
gt1 = GRAD_T(sl, 'forget.', 5.0, 2.0, 5.5, 1.0, sz=56,
col1=GRAD_PU1, col2=GRAD_BL2, ang_deg=45)
ADD_GLOW(gt1, GRAD_PU1, radius_pt=14, alpha_pct=40) # ★ 키워드 glow
T(sl, 'Designed to ', 1.2, 3.1, 4.8, 1.0, sz=56, bold=True, col=W)
gt2 = GRAD_T(sl, 'dream.', 5.8, 3.1, 4.8, 1.0, sz=56,
col1=GRAD_BL1, col2=GRAD_BL2, ang_deg=45)
ADD_GLOW(gt2, GRAD_BL2, radius_pt=14, alpha_pct=35)
# 서브 설명
T(sl, '핵심 설명 문장.\n두 줄까지 허용.',
2.2, 4.3, 9.0, 0.9, sz=16, col='9CA3AF', al='c', va='m')
# 하단 커맨드 바 (선택)
DARK_CMD_BAR(sl, 'npx your-tool init')
# 버튼/링크 배지 (선택)
btn1_bg = R(sl, 4.4, 5.5, 2.0, 0.55, fill=GRAD_PU1, rnd=True)
btn1_t = T(sl, ' ⬡ GitHub', 4.4, 5.5, 2.0, 0.55, sz=13, bold=True, col=W, al='c', va='m')
btn2_bg = R(sl, 6.55, 5.5, 1.8, 0.55, fill=DARK_CARD, line='3D4270', lw=0.8, rnd=True)
btn2_t = T(sl, ' ⬡ npm', 6.55, 5.5, 1.8, 0.55, sz=13, bold=True, col='C4CAF5', al='c', va='m')
groups=[]
gid1 = MK_GROUP(sl, badge_bg, badge_t); (groups.append([gid1]) if gid1 else None)
gid2 = MK_GROUP(sl, gt1, gt2) # 키워드 그라디언트 묶음
if gid2: groups.append([gid2])
ANIM(sl, groups, 'fade')
```
#### 패턴 B — 주의전환/섹션 구분형 (참고 이미지2 스타일)
```python
sl = NS_()
DARK_SLIDE(sl)
DARK_DECO(sl)
# 소제목 태그
T(sl, "TODAY'S ANSWER", 0.5, 1.5, 12.3, 0.3, sz=10, bold=True,
col=GRAD_TE1, al='c')
# 취소선 텍스트 (이전 방식 취소 효과) — 회색 + 빨간 취소선
old_t = T(sl, '이전 방식 설명', 2.0, 2.2, 9.0, 0.9,
sz=42, bold=True, col='3D4270', al='c')
strike = R(sl, 2.0, 2.52, 9.0, 0.06, fill=RD) # 빨간 가로선 (취소선 효과)
# 없이 키워드 (그라디언트 + glow)
gt_key = GRAD_T(sl, '없이', 9.5, 2.2, 2.5, 0.9, sz=42,
col1=W, col2=W, ang_deg=0) # 흰색 강조어
# 메인 슬로건 (대형 + 틸 그라디언트)
T(sl, 'Claude 채팅에서', 0.5, 3.4, 12.3, 1.0, sz=48, bold=True, col=W, al='c')
T(sl, '만드는 방법', 0.5, 4.45, 12.3, 0.95, sz=48, bold=True, col=W, al='c')
# 틸 그라디언트 키워드 (메인과 같은 줄에 겹쳐서 강조)
gt_hl = GRAD_T(sl, '채팅', 4.7, 3.4, 2.5, 1.0, sz=48,
col1=GRAD_TE1, col2=GRAD_TE2, ang_deg=45)
ADD_GLOW(gt_hl, GRAD_TE1, radius_pt=12, alpha_pct=45)
# 하단 안내 배너
banner_bg = R(sl, 2.0, 6.0, 9.0, 0.55, fill=DARK_CARD,
line='2D3252', lw=0.5, rnd=True)
T(sl, '끝까지 따라오시면 ', 2.0, 6.0, 4.5, 0.55,
sz=12, col='9CA3AF', al='c', va='m')
T(sl, '여러분도 오늘 바로 ', 6.2, 6.0, 2.5, 0.55,
sz=12, col=W, al='l', va='m')
T(sl, '만드실 수 있어요', 8.4, 6.0, 2.6, 0.55,
sz=12, col=GRAD_TE2, al='l', va='m')
ANIM(sl, [], 'fade')
```
#### GRAD_T 그라디언트 조합 레시피
| 용도 | col1 | col2 | ang_deg |
|------|------|------|---------|
| 보라→라벤더 (키워드) | `GRAD_PU1` (7C3AED) | `GRAD_PU2` (818CF8) | 45 |
| 인디고→스카이 (서브) | `GRAD_BL1` (6366F1) | `GRAD_BL2` (60A5FA) | 45 |
| 틸→민트 (강조) | `GRAD_TE1` (14B8A6) | `GRAD_TE2` (6EE7B7) | 45 |
| 흰색→흰색 (단순 강조) | `W` (FFFFFF) | `W` (FFFFFF) | 0 |
| 레드→핑크 (경고/취소) | `RD` (DC2626) | `CO` (E55A4E) | 45 |
#### DARK 슬라이드 glow 강도 가이드
```python
# 키워드 그라디언트 텍스트 — 강하게
ADD_GLOW(grad_text, GRAD_PU1, radius_pt=14, alpha_pct=45)
# 카드 헤더/아이콘 — 중간
ADD_GLOW(hd, GRAD_TE1, radius_pt=8, alpha_pct=35)
# 배경 장식 원형 — 약하게 (있어보이는 효과)
ADD_GLOW(ov, GRAD_BL1, radius_pt=30, alpha_pct=20)
```
#### 주의사항
- DARK 슬라이드에서는 `DARK_SLIDE()` 먼저, `DARK_DECO()` 바로 다음에 호출
- `DARK_DECO()` 반환 도형들은 그룹화하지 않음 (항상 표시되는 배경 장식)
- GRAD_T는 MK_GROUP에 넣을 수 있음 (textbox 도형이므로 _deep_flatten이 처리)
- 다크 슬라이드에서 `T(sl, ...)` 기본 col은 반드시 `W` 또는 `'9CA3AF'` 사용
- 흰 배경 슬라이드와 교차 시 fade 전환 권장 (push는 너무 갑작스러움)
---
## 6. 텍스트 사이즈 위계 (반드시 준수)
| 용도 | 크기 | 굵기 | 색상 |
|------|------|------|------|
| 타이틀 슬라이드 대형 | 48~56pt | bold | W (다크배경) |
| 슬라이드 메인 타이틀 | 28~32pt | bold | G1 |
| 섹션 헤더 | 20~24pt | bold | G1 또는 주색 |
| 카드 타이틀 | 13~16pt | bold | 주색 |
| 카드 서브타이틀 | 11~12pt | normal | G2 |
| 본문·설명 | 10~12pt | normal | G2 또는 G3 |
| 수식·코드 | 11~14pt | bold | 주색, mono=True |
| 강조 수치 | 22~36pt | bold | 주색 또는 RD |
| 배지·태그 | 9~10pt | bold | W |
| 저작권 캡션 | 9pt | normal | G3 |
**핵심 원칙**:
- 한 슬라이드에 폰트 크기 3종 이내
- 콘텐츠 넘칠 경우 폰트 줄이지 말고 **슬라이드 분리**
- 텍스트박스 높이(h)는 내용보다 10~20% 여유 있게
---
## 7. 그룹화 (MK_GROUP) — ★★★ 절대 재발 금지 버그
### 7-1. 완성된 MK_GROUP
```python
def MK_GROUP(sl, *args):
"""★ 버그 수정 3가지 모두 반영. 이 함수 그대로 사용할 것."""
def _deep_flatten(x):
"""재귀 딥플래튼: CARD()=[bg,accent] 중첩도 누락 없이"""
result=[]
if isinstance(x,(list,tuple)):
for item in x: result.extend(_deep_flatten(item))
elif x is not None and hasattr(x,'left'):
try: _=x.left; result.append(x)
except: pass
return result
shapes=_deep_flatten(args)
if not shapes: return None
spTree=sl.shapes._spTree; all_sp=list(spTree)
shape_ids_set={id(s._element) for s in shapes}
positions=[i for i,el in enumerate(all_sp) if id(el) in shape_ids_set]
if not positions: return None
insert_pos=min(positions)
lft=min(int(s.left) for s in shapes); top_v=min(int(s.top) for s in shapes)
rgt=max(int(s.left+s.width) for s in shapes)
btm=max(int(s.top+s.height) for s in shapes)
all_ids=[int(el.attrib['id']) for el in sl._element.iter()
if 'id' in el.attrib and el.attrib['id'].isdigit()]
nid=max(all_ids)+1 if all_ids else 200
grp_xml=(f'<p:grpSp xmlns:p="{PPTNS}" xmlns:a="{ANS}">'
f'<p:nvGrpSpPr><p:cNvPr id="{nid}" name="Grp{nid}"/>'
f'<p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
f'<p:grpSpPr><a:xfrm>'
f'<a:off x="{lft}" y="{top_v}"/><a:ext cx="{rgt-lft}" cy="{btm-top_v}"/>'
f'<a:chOff x="{lft}" y="{top_v}"/><a:chExt cx="{rgt-lft}" cy="{btm-top_v}"/>'
f'</a:xfrm></p:grpSpPr></p:grpSp>')
grp_el=etree.fromstring(grp_xml.encode())
for s in shapes:
el=s._element; p=el.getparent()
if p is not None: p.remove(el)
grp_el.append(el)
spTree.insert(insert_pos,grp_el) # ★ append 아닌 insert
return nid
```
### 7-2. 그룹화 3대 버그
| 버그 | 원인 | 수정 |
|------|------|------|
| bg_rect 누락 (빈번) | CARD()=[bg,accent] 중첩 단일언팩 스킵 | `_deep_flatten()` 재귀 |
| 텍스트 덮임 | `spTree.append()` → 그룹 맨 위 z-order | `spTree.insert(insert_pos)` |
| 텍스트 부유 | `T(sl,...)` 반환값 버림 | 모든 도형 변수 캡처 |
### 7-3. 올바른 패턴
```python
# ✅ 기본 패턴
mc = GLASS_CARD(sl,x,y,w,h,accent_col=col)
hd = R(sl,x,y,w,0.6,fill=col)
tl = T(sl,title,...)
bd = T(sl,body,...)
ADD_GLOW(hd,col) # glow는 MK_GROUP 전에 추가
gid = MK_GROUP(sl,mc,hd,tl,bd)
# ✅ 루프 패턴
els=[]
for name,sub,col in data:
mc=GLASS_CARD(sl,x,y,w,h,accent_col=col)
mt1=T(sl,name,...); mt2=T(sl,sub,...)
els.extend([mc,mt1,mt2])
gid=MK_GROUP(sl,els)
```
---
## 8. 애니메이션 시스템
### 8-1. mk_timing / mk_trans / ANIM
```python
def mk_timing(groups, chart_ids=None, pulse_ids=None):
if chart_ids is None: chart_ids=set()
if pulse_ids is None: pulse_ids=set()
cid=3; seq=''; bld=''
for gi,gids in enumerate(groups):
inner=''
for sid in gids:
if sid in pulse_ids:
# ★ withEffect 절대 사용 금지 → clickEffect 하나로 통합
inner+=(f'<p:par><p:cTn id="{cid}" presetID="1" presetClass="entr"'
f' presetSubtype="0" fill="hold" grpId="{gi}" nodeType="clickEffect">'
f'<p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>'
f'<p:set><p:cBhvr><p:cTn id="{cid+1}" dur="1" fill="hold"/>'
f'<p:tgtEl><p:spTgt spid="{sid}"/></p:tgtEl>'
f'<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst>'
f'</p:cBhvr><p:to><p:strVal val="visible"/></p:to></p:set>'
f'<p:animScale><p:cBhvr><p:cTn id="{cid+2}" dur="200" fill="hold">'
f'<p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn>'
f'<p:tgtEl><p:spTgt spid="{sid}"/></p:tgtEl></p:cBhvr>'
f'<p:from x="100000" y="100000"/><p:to x="112000" y="112000"/></p:animScale>'
f'<p:animScale><p:cBhvr><p:cTn id="{cid+3}" dur="200" fill="hold">'
f'<p:stCondLst><p:cond delay="200"/></p:stCondLst></p:cTn>'
f'<p:tgtEl><p:spTgt spid="{sid}"/></p:tgtEl></p:cBhvr>'
f'<p:from x="112000" y="112000"/><p:to x="100000" y="100000"/></p:animScale>'
f'</p:childTnLst></p:cTn></p:par>'); cid+=4
else:
inner+=(f'<p:par><p:cTn id="{cid}" presetID="1" presetClass="entr"'
f' presetSubtype="0" fill="hold" grpId="{gi}" nodeType="clickEffect">'
f'<p:stCondLst><p:cond delay="0"/></p:stCondLst>'
f'<p:childTnLst><p:set><p:cBhvr>'
f'<p:cTn id="{cid+1}" dur="1" fill="hold"/>'
f'<p:tgtEl><p:spTgt spid="{sid}"/></p:tgtEl>'
f'<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst>'
f'</p:cBhvr><p:to><p:strVal val="visible"/></p:to>'
f'</p:set></p:childTnLst></p:cTn></p:par>'); cid+=2
seq+=(f'<p:par><p:cTn id="{cid}" fill="hold">'
f'<p:stCondLst><p:cond evt="onClick" delay="0"/></p:stCondLst>'
f'<p:childTnLst>{inner}</p:childTnLst></p:cTn></p:par>'); cid+=1
for sid in gids:
if sid in chart_ids:
bld+=(f'<p:bldGraphic spid="{sid}" grpId="{gi}">'
f'<p:bldSub><p14:bldChart xmlns:p14="{P14NS}" animBg="1"/>'
f'</p:bldSub></p:bldGraphic>')
else:
# ★ animated="1" 필수
bld+=f'<p:bldP spid="{sid}" grpId="{gi}" uiExpand="1" build="allAtOnce" animated="1"/>'
last=cid; ns=f'xmlns:p="{PPTNS}"'
return (f'<p:timing {ns}><p:tnLst><p:par>'
f'<p:cTn id="1" dur="indefinite" restart="whenNotActive" nodeType="tmRoot">'
f'<p:childTnLst><p:seq concurrent="1" nextAc="seek">'
f'<p:cTn id="2" dur="indefinite" nodeType="mainSeq">'
f'<p:childTnLst>{seq}</p:childTnLst></p:cTn>'
f'<p:prevCondLst><p:cond evt="onPrev" delay="0">'
f'<p:tn><p:prevAc><p:hlf><p:cTn id="{last}" dur="1" fill="hold"/>'
f'</p:hlf></p:prevAc></p:tn></p:cond></p:prevCondLst>'
f'</p:seq></p:childTnLst></p:cTn></p:par></p:tnLst>'
f'<p:bldLst>{bld}</p:bldLst></p:timing>')
def mk_trans(style='push'):
inner={'push':'<p:push dir="l"/>','fade':'<p:fade thruBlk="0"/>','wipe':'<p:wipe dir="l"/>'}
return f'<p:transition xmlns:p="{PPTNS}" spd="fast" advClick="1">{inner.get(style,"<p:push dir=l/>")}</p:transition>'
def ANIM(sl, groups, trans=None, chart_ids=None, pulse_ids=None):
"""★ OOXML 요구 순서: transition 먼저, timing 나중"""
el=sl._element
for t in el.findall(P_+'timing'): el.remove(t)
for t in el.findall(P_+'transition'): el.remove(t)
if trans: el.append(etree.fromstring(mk_trans(trans)))
if groups: el.append(etree.fromstring(
mk_timing(groups, chart_ids or set(), pulse_ids or set())))
```
### 8-2. 애니메이션 4대 버그
| 버그 | 원인 | 수정 |
|------|------|------|
| 초기 숨김 안 됨 | bldP에 `animated="1"` 누락 | 반드시 포함 |
| timing 전체 무시 | timing→transition 순서 역전 | transition 먼저, timing 나중 |
| 이름없음 미실행 | `nodeType="withEffect"` 사용 | 모든 효과를 clickEffect 하나로 |
| 차트 숨김 안 됨 | 차트를 bldP로 등록 | `bldGraphic + bldChart` 사용 |
### 8-3. 전환효과 적용 기준
전체 슬라이드의 30% 이하:
- `'fade'`: 타이틀, 종합 요약
- `'push'`: 빨간 강조 슬라이드 (중요 개념 전환)
- `None`: 나머지 슬라이드
---
## 9. PPTX 제작 시작 전 체크리스트
1. **씬 B-roll 플랜** 수립 (§4-2 형식으로 먼저 작성)
2. **수치·퍼센티지** → SCENE_CHART, BAR 또는 DONUT 반드시
3. **중요 슬라이드** 지정 → `RED_ACCENT_SLIDE()` + push 전환
4. **서브스텝·헤더 강조** → `ADD_GLOW()` 적용
5. **카드류** → `GLASS_CARD()` 기본, 강조 필요 시 컬러 fill
6. **★ 예외 슬라이드** 지정 → 첫 페이지·주의전환·핵심 강조 = `SCENE_DARK` 패턴
7. **MK_GROUP** 전 모든 `T()` 반환값 변수 저장 확인
8. **ANIM** 슬라이드 마지막에 1회, transition→timing 순서 확인
9. **다크 슬라이드 전환** → 반드시 `fade` (push는 어색)
10. 모든 슬라이드 `© Made By Yangphago` 우측 하단 추가
---
## 10. 저작권 표시
```python
T(sl, '© Made By Yangphago', 0.5, 7.18, 12.3, 0.25, sz=9, col=G3, al='r')
```
클릭 시 https://yangphago.oopy.io/ 연결
---
## 11. 교육용 전용 컴포넌트 (★ 코딩·강의 슬라이드)
파이썬/코딩 교육 자료 제작 시 아래 컴포넌트를 반드시 사용.
구형 슬라이드의 문제 패턴 → 트렌디한 대체 컴포넌트:
| 구형 (금지) | 새 컴포넌트 |
|------------|------------|
| 배경 이미지로 디자인 | 도형/색상으로 대체 |
| 형광 보라(9900FF)·빨강(FF0000)·노랑(FFFF00) | §1 팔레트 사용 |
| 날것 텍스트 코드 (일반 폰트) | `CODE_BLOCK()` |
| "1 차시 파이썬 소개(5분)" 단순 텍스트 | `SEC_OPENER()` |
| NO.1~NO.6 플로팅 텍스트 목차 | `STEP_LIST()` |
| 말풍선·화살표 강조 | `POINT_BOX()` |
| 상단 하늘색 직사각형 바 | `HDR_BAR()` |
### 11-1. HDR_BAR — 콘텐츠 슬라이드 표준 헤더
```python
def HDR_BAR(sl, title, section_name='', section_num=None, accent_col=IND):
"""★ 콘텐츠 슬라이드 표준 헤더. 구형 하늘색 직사각형 바 대체.
상단 얇은 인디고 라인 + 연한 배경 + 제목 + 우측 섹션 배지."""
R(sl, 0, 0, 13.3, 0.06, fill=accent_col) # 상단 포인트 바
R(sl, 0, 0.06, 13.3, 0.88, fill=INDLT) # 헤더 배경
T(sl, title, 0.4, 0.06, 10.0, 0.88, sz=24, bold=True, col=IND, va='m')
if section_name:
badge_text = f'{section_num}강 · {section_name}' if section_num else section_name
R(sl, 10.6, 0.18, 2.5, 0.56, fill=accent_col, rnd=True)
T(sl, badge_text, 10.6, 0.18, 2.5, 0.56,
sz=11, bold=True, col=W, al='c', va='m')
LN(sl, 0.4, 1.0, 12.9, 1.0, G4, 0.5)
```
### 11-2. SEC_OPENER — 섹션 오프닝 슬라이드
```python
def SEC_OPENER(sl, number, title, subtitle='', accent_col=IND):
"""★ 섹션/챕터 구분 슬라이드. 구형 '1 파이썬 소개(5분)' 대체.
좌측 컬러 패널 + 대형 번호 + 우측 제목."""
R(sl, 0, 0, 4.0, 7.5, fill=accent_col) # 좌측 컬러 패널
R(sl, 4.0, 0, 9.3, 7.5, fill=INDLT) # 우측 연배경
T(sl, str(number), 0.3, 1.2, 3.4, 3.5, sz=120, bold=True,
col=W, al='c', va='m') # 대형 번호
T(sl, 'SECTION', 0.3, 4.7, 3.4, 0.5, sz=13, bold=True,
col='C7D2FE', al='c', va='m') # 라벨
T(sl, title, 4.5, 2.2, 8.4, 1.5, sz=42, bold=True,
col=IND, al='l', va='m') # 제목
LN(sl, 4.5, 3.9, 12.8, 3.9, IND, 1.5)
if subtitle:
T(sl, subtitle, 4.5, 4.2, 8.3, 0.8, sz=18, col=G3, al='l')
OV(sl, 11.8, 5.8, 1.8, 1.8, fill='DDE1FF') # 장식 원
OV(sl, 12.5, 6.3, 0.8, 0.8, fill=accent_col)
```
### 11-3. CODE_BLOCK — 코드 블럭 (★ 필수)
```python
def CODE_BLOCK(sl, x, y, w, h, code_lines, lang='python',
show_linenum=True, title=None):
"""★ 코딩 교육 핵심. 다크 배경 + 구문 색상 코드 박스.
code_lines: 단순 문자열 리스트 또는 (text, color_hex) 튜플 리스트."""
shapes = []
title_h = 0.38 if title else 0
if title:
tbg = R(sl, x, y, w, title_h, fill='1E293B', rnd=False)
ADD_SHADOW(tbg, alpha_pct=30)
lang_colors = {'python':BL, 'javascript':AM, 'bash':GR, 'sql':TE}
lc = lang_colors.get(lang, IND)
R(sl, x+0.12, y+0.07, 0.65, 0.22, fill=lc, rnd=True)
T(sl, lang, x+0.12, y+0.07, 0.65, 0.22, sz=9, bold=True,
col=W, al='c', va='m')
T(sl, title, x+0.9, y+0.07, w-1.05, 0.24, sz=11, col='94A3B8', va='m', mono=True)
shapes.append(tbg)
cy = y + title_h
ch = h - title_h
bg = R(sl, x, cy, w, ch, fill=CODE_BG, rnd=False)
ADD_SHADOW(bg, alpha_pct=40, blur_pt=14)
shapes.append(bg)
ln_w = 0.42 if show_linenum else 0
if show_linenum:
ln_bg = R(sl, x, cy, ln_w, ch, fill=CODE_LINE, rnd=False)
shapes.append(ln_bg)
line_h = 0.33; start_y = cy + 0.15
for k, item in enumerate(code_lines):
cur_y = start_y + k * line_h
if cur_y + line_h > cy + ch - 0.1: break
if show_linenum:
T(sl, str(k+1), x+0.04, cur_y, ln_w-0.06, line_h,
sz=11, col='475569', al='r', va='m', mono=True)
code_text = item[0] if isinstance(item, tuple) else str(item)
code_col = item[1] if isinstance(item, tuple) else CODE_W
T(sl, code_text, x+ln_w+0.14, cur_y, w-ln_w-0.22, line_h,
sz=14, col=code_col, al='l', va='m', mono=True)
return shapes
# ─ 코드 색상 헬퍼 (튜플 반환) ─
def kw(t): return (t, CODE_GR) # 함수·키워드 — green
def var(t): return (t, CODE_BLU) # 변수·속성 — blue
def s_(t): return (t, CODE_YL) # 문자열 — yellow
def cls(t): return (t, CODE_PU) # 클래스·내장 — purple
def cmt(t): return (f'# {t}', CODE_CMT) # 주석 — gray
def err(t): return (t, CODE_RD) # 에러 — red
# 사용: code_lines=[kw('print'), '(', s_('"Hello"'), ')']
# 또는: code_lines=['print("Hello")'] # 단순 문자열도 OK
```
### 11-4. POINT_BOX — 포인트 강조 박스
```python
def POINT_BOX(sl, x, y, w, h, text, level='info'):
"""★ 중요 내용 강조. 구형 말풍선·화살표 도형 대체.
level: 'info'(파랑) | 'tip'(그린) | 'warn'(노랑) | 'key'(보라)"""
colors = {
'info': (BLLT, BL, '💡'),
'tip': (GRLT, GR, '✅'),
'warn': (AMLT, AM, '⚠️'),
'key': (PULT, PU, '🔑'),
}
bg_col, border_col, icon = colors.get(level, colors['info'])
shapes = []
bg = R(sl, x, y, w, h, fill=bg_col, line=border_col, lw=1.0, rnd=True)
ADD_GLOW(bg, border_col, radius_pt=4, alpha_pct=20)
bar = R(sl, x, y, 0.06, h, fill=border_col, rnd=False)
ic = T(sl, icon, x+0.12, y, 0.45, h, sz=16, al='c', va='m')
tx = T(sl, text, x+0.58, y+0.08, w-0.7, h-0.16, sz=15, col=G1, va='t')
shapes.extend([bg, bar, ic, tx])
return shapes
```
### 11-5. STEP_LIST — 단계 목록 (목차·순서)
```python
def STEP_LIST(sl, x, y, w, items, accent_col=IND, item_h=0.72):
"""★ 번호가 있는 단계 목록. 구형 NO.1~NO.6 플로팅 목차 대체.
items: [(title, desc_str), ...] 또는 ['title', ...]"""
shapes = []
for k, item in enumerate(items):
cy = y + k * (item_h + 0.12)
c = GLASS_CARD(sl, x, cy, w, item_h, accent_col=accent_col)
num = OV(sl, x+0.1, cy+0.1, item_h-0.2, item_h-0.2, fill=accent_col)
ADD_GLOW(num, accent_col, radius_pt=5, alpha_pct=30)
nt = T(sl, str(k+1), x+0.1, cy+0.1, item_h-0.2, item_h-0.2,
sz=16, bold=True, col=W, al='c', va='m')
if isinstance(item, tuple):
tl = T(sl, item[0], x+0.9, cy+0.06, w-1.1, 0.36,
sz=16, bold=True, col=G1)
ds = T(sl, item[1], x+0.9, cy+0.44, w-1.1, 0.26, sz=12, col=G3)
shapes.extend(c + [num, nt, tl, ds])
else:
tl = T(sl, item, x+0.9, cy+0.1, w-1.1, item_h-0.2,
sz=16, bold=True, col=G1, va='m')
shapes.extend(c + [num, nt, tl])
return shapes
```
### 11-6. 완전한 코드 슬라이드 예시 (SCENE_CODE)
```python
sl = NS_()
HDR_BAR(sl, 'Hello, Python! — 첫 번째 코드', section_name='코랩 실습', section_num=3)
# 좌측: 코드 블럭
cb = CODE_BLOCK(sl, 0.5, 1.15, 7.8, 4.6,
code_lines=[
cmt('화면에 출력하기'),
kw('print') + ('("Hello, Python!")', CODE_W),
'',
cmt('변수 사용'),
('name = ', CODE_W) + s_('"양석재"'),
('age = 17', CODE_BLU),
kw('print') + ('(f"이름: {name}, 나이: {age}")', CODE_W),
],
lang='python', title='기본 출력 예제')
# 우측: 개념 카드
for k, (title, desc, col) in enumerate([
('print()', '화면에 출력하는 기본 함수', GR),
('변수 (Variable)', '데이터를 저장하는 이름표', BL),
('f-string', '문자열 안에 변수 삽입', PU),
]):
y = 1.15 + k * 1.55
c = GLASS_CARD(sl, 8.5, y, 4.6, 1.38, accent_col=col)
tl = T(sl, title, 8.7, y+0.1, 4.2, 0.38, sz=15, bold=True, col=col)
bd = T(sl, desc, 8.7, y+0.52, 4.2, 0.75, sz=13, col=G2)
ADD_GLOW(tl, col, radius_pt=5, alpha_pct=25)
# 실행 결과 박스
R(sl, 0.5, 5.95, 7.8, 1.35, fill=CODE_BG, rnd=True)
T(sl, '▶ 실행 결과', 0.7, 6.0, 2.5, 0.38,
sz=11, bold=True, col=GRAD_TE1, mono=True)
T(sl, 'Hello, Python!\n이름: 양석재, 나이: 17',
0.7, 6.42, 7.2, 0.82, sz=13, col=CODE_GR, mono=True)
ANIM(sl, [], None)
T(sl, '© Made By Yangphago', 0.5, 7.18, 12.3, 0.22, sz=9, col=G3, al='r')
```
### 11-7. 텍스트 사이즈 위계 (투영 환경 기준 — AI 슬라이드보다 큼)
| 용도 | 크기 | 비고 |
|------|------|------|
| 섹션 번호 (SEC_OPENER) | 100~120pt | 흰색 |
| HDR_BAR 제목 | 22~26pt | bold, 인디고 |
| 섹션 제목 | 36~44pt | bold |
| 카드 타이틀 | 15~18pt | bold |
| 본문·설명 | **최소 14pt** | 투영 환경 기준 |
| 코드 (CODE_BLOCK) | **최소 14pt** | mono=True 필수 |
| 포인트 박스 텍스트 | 14~16pt | |
| 저작권 | 9pt | |
**핵심**: 본문과 코드 **절대 12pt 이하 금지** (스크린 가독성)
---
## 12. 제작 시작 전 체크리스트
1. **씬 플랜** 수립 (§6-2 형식)
2. **형광색(FF0000·9900FF·FFFF00) 절대 사용 금지** — §1 팔레트 사용
3. **배경 이미지로 디자인 요소 사용 금지** — 도형/색상으로 대체
4. **코드** → 반드시 `CODE_BLOCK()` (일반 텍스트박스 금지)
5. **섹션 구분** → `SEC_OPENER()`
6. **목차/스텝** → `STEP_LIST()`
7. **중요 강조** → `POINT_BOX()` (말풍선·화살표 금지)
8. **수치·퍼센티지** → 차트 시각화
9. **MK_GROUP** 전 모든 `T()` 반환값 저장
10. **ANIM** 마지막 1회, transition→timing 순서
11. **다크↔화이트 전환** → `'fade'` 필수
12. 모든 슬라이드 `© Made By Yangphago` 우측 하단
Plain Text
복사
3. Claude vs ChatGPT vs Gemini 비교
3-1. 비교 결과 요약
같은 스킬 파일(프롬프트화)을 AI삼국지에 동일하게 사용한 결과:
항목 | Claude | ChatGPT | Gemini |
내용 누락 | 없음 | 일부 디테일 누락 | 파트 통째로 누락 |
비주얼 다양성 | 씬마다 다양한 레이아웃 | 비슷한 카드 구조 반복 | : |
서브스텝 | 정상 작동 | 타이밍 어색, 요소 겹침 | : |
수정 필요량 | 거의 없음 | 꽤 많음 | 많음 |
#클로드 스킬 사용의 결과
3-2. 결론
•
Gemini: 이 작업엔 아직 많이 부족함
•
ChatGPT: 수정하면 쓸 수는 있지만, 스킬이 없으니 매번 프롬프트부터 다시 작성해야 함
•
Claude: 스킬 파일에 수정 기록이 쌓여 있어서 처음부터 고품질 결과물이 나옴
◦
하지만 거기까지 상당히 많은 시간과 money가 필요할 수 있음!
4. AI를 ’단순 도구’가 아닌 ’시스템’으로 쓰는 법
4-1. 핵심 사이클
① 일 시키기
② 결과 확인
③ 수정하기
④ 수정 기록 저장 → 스킬 파일에 반영
⑤ 반복할수록 수정량 감소
Plain Text
복사
4-2. 이 방식이 필요한 이유
•
프롬프트는 쓸 때마다 리셋되지만, 스킬은 내가 쌓아온 경험이 축적됨
•
AI를 단순 도구로 쓰는 것과, 나만의 시스템으로 설계하는 것은 완전히 다른 차원임





























