feat: v2.2.92 → v2.2.158 — god-file 분해 + Stocks feature + 대화 연속성
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* `/youtube` slash command 의 LLM 입력 빌더 + 자막 변환 헬퍼.
|
||||
* - formatHms / fullScriptFromSegments / bucketSegments — segment list 가공
|
||||
* - YoutubeAnalysisMode — info/benchmark/both 라우팅 enum (slashRouter 가 사용)
|
||||
* - buildInfoExtractionPrompt — *영상 내용(지식)* 카드 추출 프롬프트
|
||||
* - build4LensPrompt — 영상 *제작 기법* (훅/구조/제작/CTR) 4-렌즈 분석 프롬프트
|
||||
*
|
||||
* 옛 코드: slashRouter.ts 의 320줄짜리 inline 블록. 분리해 (a) 두 프롬프트가 같은
|
||||
* segment 변환 helper 를 자연스럽게 공유, (b) 새 모드 추가 시 한 파일만 수정,
|
||||
* (c) 단위 테스트로 prompt 회귀 확인 가능.
|
||||
*/
|
||||
|
||||
export function formatHms(totalSec: number): string {
|
||||
if (!isFinite(totalSec) || totalSec <= 0) return '00:00';
|
||||
const s = Math.floor(totalSec);
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
return h > 0
|
||||
? `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
|
||||
: `${m}:${String(sec).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 영상 전체 자막을 30초 버킷으로 묶어 `[mm:ss] 문장…` 형태의 읽기 좋은 full script로 변환.
|
||||
* YouTube 자동자막은 segment가 잘게 끊겨 그대로 나열하면 가독성이 나쁘므로 묶는다.
|
||||
*/
|
||||
export function fullScriptFromSegments(segments: any[] | undefined): string {
|
||||
if (!segments || segments.length === 0) return '(자막 없음 — 자동 자막이 없는 영상일 수 있습니다.)';
|
||||
const buckets = new Map<number, string[]>();
|
||||
for (const seg of segments) {
|
||||
const b = Math.floor((seg.start || 0) / 30);
|
||||
const arr = buckets.get(b) || [];
|
||||
arr.push(String(seg.text || '').trim());
|
||||
buckets.set(b, arr);
|
||||
}
|
||||
return Array.from(buckets.entries())
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([b, texts]) => `**[${formatHms(b * 30)}]** ${texts.join(' ').replace(/\s+/g, ' ').trim()}`)
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* timestamped segments → 분 단위 버킷으로 묶은 "타임라인 뼈대" 텍스트.
|
||||
* §2 구조 분석에서 LLM 토큰을 아낀다.
|
||||
*/
|
||||
export function bucketSegments(segments: any[] | undefined, bucketSec = 30): { time: string; text: string }[] {
|
||||
if (!segments || segments.length === 0) return [];
|
||||
const buckets = new Map<number, string[]>();
|
||||
for (const seg of segments) {
|
||||
const bucket = Math.floor(seg.start / bucketSec);
|
||||
const arr = buckets.get(bucket) || [];
|
||||
arr.push(String(seg.text || '').trim());
|
||||
buckets.set(bucket, arr);
|
||||
}
|
||||
return Array.from(buckets.entries())
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([bucket, texts]) => ({
|
||||
time: formatHms(bucket * bucketSec),
|
||||
text: texts.join(' ').replace(/\s+/g, ' ').trim().slice(0, 240),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Astra `/youtube` 의 분석 모드. 사용자 입력 `mode:info|benchmark|both`. */
|
||||
export type YoutubeAnalysisMode = 'info' | 'benchmark' | 'both';
|
||||
|
||||
/**
|
||||
* 정보 추출(info) 모드 LLM 프롬프트 — 영상의 *내용·지식* 자체를 다룬다.
|
||||
*
|
||||
* 의도: build4LensPrompt 가 "이 영상을 어떻게 베껴 만들지" 의 벤치마킹 톤이라
|
||||
* 튜토리얼·강의·뉴스·인터뷰·리뷰 같은 정보형 영상에서는 가치가 낮다. 이 함수는
|
||||
* 정반대 방향 — 영상이 *말한 것* 을 사실·주장·근거 단위로 추출해서, 사용자가
|
||||
* 영상을 안 다시 봐도 의사결정·학습·인용에 바로 쓸 수 있는 지식 카드로 정리한다.
|
||||
*
|
||||
* 출력 규칙은 build4LensPrompt 와 일관 (마크다운, 한국어, 자막에 있는 것만 인용).
|
||||
*/
|
||||
export function buildInfoExtractionPrompt(video: any, userContent: string): string {
|
||||
const meta = video.metadata || {};
|
||||
const segments = video.segments || [];
|
||||
|
||||
// 자막 본문 — info 모드는 *전체* 본문을 보여줘야 사실 추출이 정확. 단,
|
||||
// LLM 컨텍스트 한도 고려해 너무 길면 trim. 12000자 = 가벼운 강의 60분 분량 정도.
|
||||
const fullText = segments.map((s: any) => String(s.text || '').trim()).join(' ').replace(/\s+/g, ' ');
|
||||
const trimmed = fullText.length > 12000 ? fullText.slice(0, 12000) + ' …[자막 일부 잘림]' : fullText;
|
||||
|
||||
const slim = {
|
||||
url: meta.webpage_url || `https://www.youtube.com/watch?v=${video.video_id}`,
|
||||
title: meta.title || video.title,
|
||||
channel: meta.channel,
|
||||
durationSec: meta.duration,
|
||||
durationHms: meta.duration_string,
|
||||
uploadDate: meta.upload_date,
|
||||
viewCount: meta.view_count,
|
||||
likeCount: meta.like_count,
|
||||
tags: (meta.tags || []).slice(0, 8),
|
||||
categories: meta.categories,
|
||||
chapters: meta.chapters,
|
||||
descriptionPreview: (meta.description || '').slice(0, 600),
|
||||
};
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const userBlock = userContent.trim()
|
||||
? `\n\n[사용자 컨텍스트 — 사용자가 어떤 관점에서 이 영상을 활용하려는지]\n${userContent.trim()}`
|
||||
: '';
|
||||
|
||||
return `당신은 영상 콘텐츠를 *지식 카드*로 변환하는 정보 큐레이터입니다. 사장님이
|
||||
이 영상을 다시 보지 않고도 핵심 정보를 그대로 활용할 수 있도록, 영상이 *말한 것*
|
||||
(주장·사실·근거·결론)을 구조화해서 정리하세요.
|
||||
|
||||
[분석 원칙 — 모두 반드시 준수]
|
||||
1. **출처 분리** — 영상 본문(자막)에 *명시된 것* 만 핵심 섹션에 넣음. 정리자의 추론·외부
|
||||
지식·자기 해석은 별도 \`## 🧩 정리자 노트\` 섹션에만. 두 줄 섞지 말 것.
|
||||
2. **빈 곳 채우지 말 것** — 자막에 없는 사실은 "본문에 명시되지 않음" 또는 "해당 사례 없음".
|
||||
3. **신뢰도 라벨 필수** — 모든 핵심 주장 앞에 다음 중 하나:
|
||||
- \`[근거 명시]\` 구체 출처·수치·인용이 본문에 있음
|
||||
- \`[화자 주장]\` 출처 없는 단정 (디노가 그렇게 말함)
|
||||
- \`[가정]\` 조건부·"~인 것 같다" 표현
|
||||
- \`[정리자 추론]\` 본문에 없지만 정리자가 추가 (이건 정리자 노트 섹션 전용)
|
||||
4. **타임스탬프 필수** — 본문 인용·구간 요약·발언 따옴표는 끝에 \`(mm:ss)\` 무조건 붙임.
|
||||
이걸 빠뜨리면 fail. "(시점 미상)" 도 허용 안 함 — 모르면 인용 자체 빼기.
|
||||
5. **화자 한 줄 비유 보존 + 방향 보존** — 영상에 비유·은유·"X 는 Y 같은 것" 식 압축 표현이
|
||||
있으면 반드시 별도 섹션 \`## 💡 화자 한 줄 비유\` 에 보존. 영상의 결정적 요약이 거기
|
||||
들어 있을 가능성 큼. 없으면 "본문에 명시된 한 줄 비유 없음" 명시.
|
||||
⚠️ **비유는 방향이 뒤집히기 쉬움** — 화자가 "Hugging Face = 자료실, Reddit = 공부방"
|
||||
이라 했으면 정확히 그 짝(어느 쪽이 자료실이고 어느 쪽이 공부방인지)을 그대로 따옴표
|
||||
인용으로 보존. 정리자가 단어 위치를 바꾸거나 뜻을 의역하면 안 됨. 고유명사·수치·
|
||||
대응 관계도 마찬가지 — 본문 그대로.
|
||||
6. **순서·단계 발명 금지** — 화자가 "A → B → C 순서로" 라고 명시하지 *않았으면* "단계적
|
||||
학습 순서" 같은 흐름을 정리자가 만들지 말 것. 굳이 필요하면 정리자 노트로.
|
||||
7. 한국어 마크다운. 표·불릿 자유롭게.
|
||||
|
||||
[영상 메타데이터]
|
||||
\`\`\`json
|
||||
${JSON.stringify(slim, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
[자막 본문]
|
||||
${trimmed}${userBlock}
|
||||
|
||||
[필수 출력 형식 — 정확히 이 구조. 아래 8개 섹션 외 추가 금지]
|
||||
|
||||
# ${slim.title || video.title} — 정보 추출 카드
|
||||
|
||||
> **영상 URL**: ${slim.url} · **분석 일자**: ${today} · **길이**: ${slim.durationHms || (slim.durationSec ? formatHms(slim.durationSec) : '?')} · **채널**: ${slim.channel || '?'}
|
||||
|
||||
## 🎯 한 줄 요약 (TL;DR)
|
||||
(영상의 핵심 메시지 한 문장. "무엇이 누구에게 왜 중요한가" 를 압축. 제목 그대로 베끼지
|
||||
말고 본문 기준으로 다시 쓸 것. 정리자의 해석은 금지 — 화자의 말 그대로 압축)
|
||||
|
||||
## 💡 화자 한 줄 비유 (Anchor Metaphor)
|
||||
영상에서 화자가 *전체 메시지를 한 줄로 압축한 비유·은유* 가 있으면 그대로 따옴표로
|
||||
보존. 영상 마무리부에 자주 등장. 예: "Hugging Face = 자료실, Reddit = 공부방,
|
||||
유튜브 = 복습실" 같은 식. 없으면 "본문에 명시된 한 줄 비유 없음".
|
||||
|
||||
## 📌 핵심 주장 3~5개
|
||||
영상이 *명시한* 주요 결론·주장만. 정리자 추론은 여기 들어오면 안 됨 (그건 🧩 섹션).
|
||||
각 항목 한 줄 + 신뢰도 라벨 + 본문 인용 (mm:ss).
|
||||
- **[근거 명시]** "주장 한 줄" — 본문 인용 (mm:ss)
|
||||
- **[화자 주장]** "주장 한 줄" — 본문 인용 (mm:ss)
|
||||
- …
|
||||
|
||||
## 📊 사실·데이터·인용
|
||||
영상에 등장한 *구체적 수치·날짜·출처·고유명사·전문 용어 정의*. 가공 없이 그대로.
|
||||
표로 정리:
|
||||
|
||||
| 항목 | 값 / 정의 | 출처 (영상 내) | 타임스탬프 |
|
||||
| --- | --- | --- | --- |
|
||||
| … | … | 화자/자료 화면/외부 출처 | mm:ss |
|
||||
|
||||
데이터가 없는 영상이면 "본문에 명시된 구체 수치·출처 없음" 한 줄.
|
||||
|
||||
## 🧭 구조 요약 (Sectioned Summary)
|
||||
영상을 chapters (메타데이터에 있으면 그것 사용) 또는 30초 버킷으로 구간 나눠 각 구간의
|
||||
*내용 요약*. 1~2문장씩. 각 항목 끝에 타임스탬프 범위 필수.
|
||||
- **[00:00–02:30]** 도입부에서 다룬 내용 한 문장 요약 (mm:ss–mm:ss)
|
||||
- **[02:30–05:00]** 본론 첫 부분… (mm:ss–mm:ss)
|
||||
- …
|
||||
|
||||
## 🔗 인용용 한 줄 카드 (Citation Snippets)
|
||||
영상의 *결정적 발언* 을 그대로 따옴표로 보존. 사장님이 글·발표·메모에 인용할 때 복붙용.
|
||||
3~5개. 길이는 한 문장. 타임스탬프 필수.
|
||||
- "직접 인용 한 문장" — ${slim.title || video.title}, ${slim.channel || '?'} (mm:ss)
|
||||
- …
|
||||
|
||||
## ❓ 더 파고들 질문 (Open Questions)
|
||||
영상이 답하지 않았거나 추가 검증 필요한 사항 2~4개. 사장님이 다음 자료를 찾을 때
|
||||
바로 검색어로 쓸 수 있게 구체적으로.
|
||||
- "본문에서 X 가 Y 라고 했지만 Z 데이터 출처는 명시 안 됨 — 원 데이터 찾아볼 것"
|
||||
- …
|
||||
|
||||
## 🧩 정리자 노트 (원본 보강) — 선택
|
||||
*본문에 없지만* 정리자가 추가로 짚고 싶은 맥락·해석·연결·경고. 위 6개 핵심 섹션과
|
||||
구조적으로 격리되어, 독자가 "이건 화자가 말한 게 아니라 LLM 이 추론한 거" 라고
|
||||
명확히 인지하도록. 모든 항목은 \`[정리자 추론]\` 라벨로 시작.
|
||||
- **[정리자 추론]** 화자가 "여러 채널을 동시 시청" 하라 했지만, 입문자 페이스를 고려하면
|
||||
먼저 한 채널을 깊게 따라가는 것도 한 가지 시작점이 될 수 있음.
|
||||
- …
|
||||
|
||||
특별히 보강할 게 없으면 이 섹션 통째로 "정리자 추가 노트 없음 — 본문 그대로가 명확함" 한 줄.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* extract된 영상 → 유튜브 4-렌즈(훅/구조/제작/CTR) 분석 LLM 프롬프트.
|
||||
* Datacollect 웹앱(YoutubePanel)의 build4LensPrompt를 그대로 이식.
|
||||
*/
|
||||
export function build4LensPrompt(video: any, userContent: string): string {
|
||||
const meta = video.metadata || {};
|
||||
const segments = video.segments || [];
|
||||
|
||||
// 초반 30초 / 60초 텍스트 — §1 훅 분석용.
|
||||
const first30s = segments.filter((s: any) => s.start < 30).map((s: any) => String(s.text || '').trim()).join(' ').slice(0, 600);
|
||||
const first60s = segments.filter((s: any) => s.start < 60).map((s: any) => String(s.text || '').trim()).join(' ').slice(0, 1200);
|
||||
|
||||
// 타임라인 버킷 (30초 단위) — §2 구조 분석용.
|
||||
const timelineBuckets = bucketSegments(segments, 30);
|
||||
const timelinePreview = timelineBuckets.slice(0, 24).map(b => `[${b.time}] ${b.text}`).join('\n');
|
||||
|
||||
// 인게이지먼트 키워드 매치 — §2 보조.
|
||||
const engagementHits = segments
|
||||
.filter((s: any) => /구독|좋아요|알림|댓글|공유|subscribe|like|comment/i.test(String(s.text || '')))
|
||||
.slice(0, 5)
|
||||
.map((s: any) => ({ t: formatHms(s.start), text: String(s.text || '').trim().slice(0, 100) }));
|
||||
|
||||
const slim = {
|
||||
url: meta.webpage_url || `https://www.youtube.com/watch?v=${video.video_id}`,
|
||||
title: meta.title || video.title,
|
||||
channel: meta.channel,
|
||||
durationSec: meta.duration,
|
||||
durationHms: meta.duration_string,
|
||||
viewCount: meta.view_count,
|
||||
likeCount: meta.like_count,
|
||||
commentCount: meta.comment_count,
|
||||
uploadDate: meta.upload_date,
|
||||
thumbnail: meta.thumbnail,
|
||||
tags: (meta.tags || []).slice(0, 12),
|
||||
categories: meta.categories,
|
||||
chapters: meta.chapters,
|
||||
descriptionPreview: (meta.description || '').slice(0, 600),
|
||||
opening30s: first30s,
|
||||
opening60s: first60s,
|
||||
engagementMoments: engagementHits,
|
||||
segmentCount: segments.length,
|
||||
timelinePreview,
|
||||
};
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const userBlock = userContent.trim()
|
||||
? userContent.trim()
|
||||
: '(미입력 — 일반 콘텐츠 제작자 컨텍스트로 작성)';
|
||||
|
||||
return `당신은 유튜브 '대본(스크립트)' 분석 전문가이자 콘텐츠 작가입니다. 사장님이
|
||||
이 영상과 비슷한 콘텐츠의 **대본을 직접 쓰려** 합니다. 영상 연출이 아니라 오직
|
||||
스크립트(텍스트)와 언어 구조만 분석해, 읽자마자 자기 대본에 복붙하듯 써먹을 수 있는
|
||||
'유저 친화적 역기획서'를 작성하세요.
|
||||
|
||||
[분석 원칙]
|
||||
1. BGM·자막·컷 전환·썸네일 등 대본만으로 알 수 없는 '영상 연출' 항목은 과감히 생략한다.
|
||||
오직 스크립트(텍스트)와 언어 구조에만 집중한다.
|
||||
2. 대사를 단순 인용하지 말고, 그 대사가 시청자 심리를 어떻게 건드렸는지 '언어적 장치'를
|
||||
태그로 라벨링한다. 아래 태그 어휘에서만 골라 일관되게 사용한다:
|
||||
#FOMO #권위부여 #호기심갭 #사회적증명 #페르소나 #약속Promise #공감후킹
|
||||
#반전 #숫자강조 #문제고발 #브릿지멘트 #쉬운비유
|
||||
3. 전문 용어가 나오면, 화자가 그것을 어떤 '쉬운 비유'나 일상어로 풀어 말했는지
|
||||
그 구어체 '말의 맛'을 반드시 분석에 포함한다.
|
||||
4. 한국어. 자막(text)·chapters·메타데이터에 있는 것만 인용(추측 금지). 타임스탬프는 mm:ss.
|
||||
|
||||
[영상 데이터]
|
||||
\`\`\`json
|
||||
${JSON.stringify(slim, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
[우리가 만들고 싶은 콘텐츠 / 채널 컨텍스트]
|
||||
${userBlock}
|
||||
|
||||
[필수 출력 형식 — 정확히 이 구조. 아래 5개 섹션 외 추가 금지]
|
||||
|
||||
# ${slim.title || video.title} — 대본 역기획서
|
||||
|
||||
> **영상 URL**: ${slim.url} · **분석 일자**: ${today} · **길이**: ${slim.durationHms || (slim.durationSec ? formatHms(slim.durationSec) : '?')} · **채널**: ${slim.channel || '?'}
|
||||
|
||||
## 🎬 한 줄 인상 (One-line Read)
|
||||
(이 영상 스크립트의 핵심 성격과 설득 전략을 한 줄로. 예: "전문 지식을 친구에게
|
||||
설명하듯 풀어내고, 호기심 갭으로 끝까지 끌고 가는 정보형 대본")
|
||||
|
||||
## 1. 스크립트 뼈대 구조도 (Script Architecture)
|
||||
구간별 마크다운 표 1개. '레퍼런스 실제 대사'는 자막에서 1문장 이내로 짧게 따옴표 인용.
|
||||
'스크립트 기능'에는 위 태그 어휘를 1~2개 붙인다. 비중 %는 durationSec 기준,
|
||||
chapters가 있으면 그것을, 없으면 timelinePreview로 구간을 추정.
|
||||
|
||||
| 구간 (비중) | 스크립트 기능 (태그) | 레퍼런스 실제 대사 | 벤치마킹 핵심 기술 |
|
||||
| --- | --- | --- | --- |
|
||||
| 오프닝 Hook (0:00~?, ?%) | #호기심갭 #약속Promise | "첫 대사…" | 결과를 미리 흘려 이탈 차단 |
|
||||
| 도입부 (?~?, ?%) | … | … | … |
|
||||
| 본론 (?~?, ?%) | … | … | … |
|
||||
| 아웃트로·CTA (?~?, ?%) | … | … | … |
|
||||
|
||||
## 2. 말의 맛 & 톤앤매너 (Tone & Manner)
|
||||
- **문장 길이 특징**: 단문/장문, 호흡, 리듬 — 실제 자막 예시 1개를 따옴표로.
|
||||
- **어조 페르소나**: 예) 친근한 전문가체 / 단정적 신뢰체 — 근거 대사 1개.
|
||||
- **핵심 대사 장치**: 시청자 중간 이탈을 막으려 대본 사이에 심은 미끼 문장·브릿지 멘트를
|
||||
타임스탬프와 함께 2~3개 추출, 각각 태그 라벨을 붙인다.
|
||||
- **전문용어 → 쉬운 비유**: 어려운 개념을 화자가 어떤 비유·일상어로 풀었는지
|
||||
\`용어 → "화자의 실제 표현"\` 형태로 2~3개. 사례가 없으면 "해당 사례 없음"이라 명시.
|
||||
|
||||
## 3. 내 대본에 바로 쓰는 액션 체크리스트 (Action Items)
|
||||
다음 대본을 쓸 때 무조건 적용할 행동 지침 3~4개. 반드시 체크박스로, 구체적 수치를 포함.
|
||||
- [ ] (예: 오프닝 15초 안에 '내가 누구인지' 페르소나 한 문장 박기)
|
||||
- [ ] …
|
||||
- [ ] …
|
||||
|
||||
## ✂️ 빈칸 채우기식 대본 템플릿 (Fill-in-the-Blank)
|
||||
레퍼런스의 말하기 구조·접속사·리듬은 그대로 살리고, 내 콘텐츠 내용만 [ ]에 채우면
|
||||
대본이 완성되는 형태. 각 [ ] 안에는 무엇을 넣을지 짧은 힌트를 적는다.
|
||||
|
||||
\`\`\`
|
||||
[오프닝 — Hook]
|
||||
"여러분, 혹시 [시청자의 흔한 고민]… 해보신 적 있으세요?
|
||||
오늘은 [이 영상이 줄 핵심 결과]를 [숫자]분 만에 끝내 드릴게요."
|
||||
|
||||
[도입부 — 공감 + 권위]
|
||||
…
|
||||
|
||||
[본론 — 단계별 설명]
|
||||
…
|
||||
|
||||
[아웃트로 — CTA]
|
||||
…
|
||||
\`\`\`
|
||||
|
||||
> ⚠️ 본 분석은 스크립트의 언어·구조 패턴 학습용입니다. 대사·자료는 직접 창작/라이선스 확보.`;
|
||||
}
|
||||
Reference in New Issue
Block a user