diff --git a/10_Wiki/00_Raw/.astra/project-context/architecture.md b/10_Wiki/00_Raw/.astra/project-context/architecture.md
new file mode 100644
index 00000000..505c0312
--- /dev/null
+++ b/10_Wiki/00_Raw/.astra/project-context/architecture.md
@@ -0,0 +1,56 @@
+# 00_Raw — Project Architecture Context
+
+> Auto-managed sections (between the AUTO markers) are rewritten by Astra on every refresh.
+> The rest below is yours — Astra never touches it once this file exists.
+
+
+
+## Snapshot
+- **Workspace**: `00_Raw` _(absolute path varies by environment; resolved from the active VS Code workspace)_
+- **Stack**: _(unknown)_
+- **Stats**: 5 source files, ~83 lines across 1 top-level modules.
+
+## Last Refresh
+- **Time**: 2026-05-13T14:14:16.214Z
+- **Files newly analysed**: 5
+- **Files reused from cache**: 0
+
+## Directory Map
+```mermaid
+mindmap
+ root((00_Raw))
+ docs/
+ records/
+```
+
+## Modules
+
+### `docs/` — 5 files, ~83 lines
+
+**Sub-directories**
+- `docs/records/` (5) — 00Raw Chronicle Records
+
+**Key files**
+- `docs/records/00_Raw/README.md` (18 lines) — 00Raw Chronicle Records
+- `docs/records/00_Raw/chronicle.config.json` (11 lines) — JSON configuration
+- `docs/records/00_Raw/discussions/2026-05-13_volumes-data-project-antigravity-wiki-10-wiki-00-raw-여기-아래에-.md` (16 lines) — Discussion: /Volumes/Data/project/Antigravity/Wiki/10Wiki/00Raw 여기 아래에 저장된거 같은데? topics폴더가...
+- `docs/records/00_Raw/project-profile.md` (31 lines) — Project Profile
+- `docs/records/00_Raw/timeline.md` (7 lines) — Project Timeline
+
+_Last auto-scan: 2026-05-13T14:14:16.214Z · signature `230d82a4`_
+
+
+## Purpose
+_TODO: 이 프로젝트가 해결하려는 문제를 1–3문장으로._
+
+## Key Workflows
+_TODO: 사용자/시스템의 주요 흐름 (예: 입력 → context assembly → model 호출 → action)._
+
+## Current Constraints
+_TODO: 의도된 제약 (local-first, offline, 특정 API 의존 등)._
+
+## Known Risks
+_TODO: 알려진 위험/디버깅 함정._
+
+## Active Decisions
+_TODO: 살아 있는 ADR/원칙 (e.g. "기록은 markdown으로", "agent별 model override 우선")._
diff --git a/10_Wiki/00_Raw/.astra/project-context/scan-cache.json b/10_Wiki/00_Raw/.astra/project-context/scan-cache.json
new file mode 100644
index 00000000..9fbe6bc7
--- /dev/null
+++ b/10_Wiki/00_Raw/.astra/project-context/scan-cache.json
@@ -0,0 +1,41 @@
+{
+ "version": 1,
+ "generatedAt": "2026-05-13T14:14:16.215Z",
+ "files": {
+ "docs/records/00_Raw/README.md": {
+ "mtimeMs": 1778681649000,
+ "size": 394,
+ "lines": 18,
+ "role": "00Raw Chronicle Records",
+ "imports": []
+ },
+ "docs/records/00_Raw/chronicle.config.json": {
+ "mtimeMs": 1778681650000,
+ "size": 427,
+ "lines": 11,
+ "role": "JSON configuration",
+ "imports": []
+ },
+ "docs/records/00_Raw/discussions/2026-05-13_volumes-data-project-antigravity-wiki-10-wiki-00-raw-여기-아래에-.md": {
+ "mtimeMs": 1778681650000,
+ "size": 1039,
+ "lines": 16,
+ "role": "Discussion: /Volumes/Data/project/Antigravity/Wiki/10Wiki/00Raw 여기 아래에 저장된거 같은데? topics폴더가...",
+ "imports": []
+ },
+ "docs/records/00_Raw/project-profile.md": {
+ "mtimeMs": 1778681649000,
+ "size": 484,
+ "lines": 31,
+ "role": "Project Profile",
+ "imports": []
+ },
+ "docs/records/00_Raw/timeline.md": {
+ "mtimeMs": 1778681650000,
+ "size": 238,
+ "lines": 7,
+ "role": "Project Timeline",
+ "imports": []
+ }
+ }
+}
\ No newline at end of file
diff --git a/10_Wiki/00_Raw/2026-05-13/AI_1인_기업_자동화_챕터_1.md b/10_Wiki/00_Raw/2026-05-13/AI_1인_기업_자동화_챕터_1.md
new file mode 100644
index 00000000..da8a0418
--- /dev/null
+++ b/10_Wiki/00_Raw/2026-05-13/AI_1인_기업_자동화_챕터_1.md
@@ -0,0 +1,186 @@
+
+# 첫번째날 : 🚀 AI 1인 기업: 단순 자동화를 넘어 '지능형 비즈니스'로
+
+---
+
+이 강의는 세계 최고의 대학교에 일반인(비전공자)를 대상으로 한 AI 수익화 전공이 있다면 이렇게 강의할것이다. 라는 생각으로 커리큘럼을 만들었습니다.
+
+### 1. 근본적인 질문: AI가 그냥 '일'만 하면 될까요?
+
+AI 1인 기업은 단순히 AI에게 일을 시키는 것이 아닙니다.
+
+- **무지성 자동화의 한계:** 유튜브에 아무 영상이나 올리고, 웹사이트에 의미 없는 글을 도배한다고 해서 수익이 나지 않습니다.
+- **수익의 본질:** 수익화는 사람이(혹은 미래에는 에이전트가) 그 서비스에서 '고유한 가치'를 발견하고 구매 결정을 내릴 때 발생합니다.
+ - **해결책:** '그냥 자동화'가 아닌, '지식이 탑재된 인공지능의 자동화'가 필요합니다.
+
+### 2. 지능의 엔진: RAG (Retrieval-Augmented Generation)
+
+여기서 말하는 인공지능의 '지식'은 바로 **RAG** 기술을 통해 구현됩니다. RAG는 AI에게 단순한 언어 능력을 넘어, 외부의 방대한 전문 지식을 실시간으로 찾아보고 활용할 수 있는 '뇌'를 달아주는 작업입니다.
+
+- **무엇(What)의 차별화:** RAG를 통하면 AI는 뻔한 소리가 아닌, 우리 기업만의 독자적인 지식 네트워크를 기반으로 **'진짜 알맹이'** 있는 콘텐츠와 서비스를 만들어냅니다.
+
+### 3. [8주 완성] AI 1인 기업가 커리큘럼
+
+저희 강의는 AI의 '뇌(지식)'를 만들고, 그것을 움직일 '손발(에이전트)'을 구축하는 2단계 과정을 거칩니다.
+
+| **단계** | **기간** | **핵심 목표** | **주요 내용** |
+| --- | --- | --- | --- |
+| **Step 1: RAG** | 1~4주 | **지능 구축** | 지식 네트워크 설계, 데이터 구조화, 전문 지식 주입 |
+| **Step 2: Agent** | 5~8주 | **자동화 실행** | 수익 창출 워크플로우 설계, 자율 에이전트 구축 및 배포 |
+
+---
+
+# 이론
+
+## 1. 뿌리 찾기: 기초 및 핵심 원리 (2020 ~ 2022)
+
+RAG가 왜 태어났고, 어떤 수학적·기술적 배경을 가졌는지 이해
+
+## 2. 진화의 시작: 고도화 테크닉 (2023 ~ 2024)
+
+단순 검색을 넘어, AI가 스스로 판단하고 정보를 정제하는 단계를 공부합니다.
+
+## 3. 2026년 현재: Agentic & Modular RAG
+
+지금 이 시점(2026년)에 가장 뜨거운 감자인 '에이전트형 RAG'와 '멀티모달' 단계입니다.
+
+**4. 미래 예상: 2027년 이후의 RAG (The Next Frontier)**
+ **SLM (Small Language Model) + RAG:** 거대한 GPT-5, 6 같은 모델 대신, 특정 기업의 온프레미스 환경에 특화된 초경량 모델과 강력한 RAG 결합이 주류가 될 것입니다. (보안과 비용 문제 해결)
+
+# RAG를 처음으로 소개한 연구
+
+### 1. Retrieval (리트리벌)
+
+- **사전적 의미:** 검색, 되찾아옴, (정보의) 회수
+- **RAG에서의 의미:** **"찾아오기"**
+ - AI가 자기 머릿속(학습 데이터)에만 의존하는 게 아니라, 외부에 있는 문서나 지식 DB에서 질문과 관련된 내용을 '직접 찾아내는 과정'입니다.
+ - 질문자님이 말씀하신 '지식 네트워크'에서 필요한 조각을 딱 집어내는 첫 번째 단계죠.
+
+### 2. Augmented (어그멘티드)
+
+- **사전적 의미:** 증강된, 강화된, 늘어난
+- **RAG에서의 의미:** **"보충하기"**
+ - 찾아온 정보를 AI의 원래 능력에 '덧붙여서 강화한다'는 뜻입니다.
+ - 단순히 AI 혼자 떠들게 내버려 두는 게 아니라, 우리가 찾아준 확실한 근거(Data)를 AI의 손에 쥐여주어 더 똑똑하게 만드는 과정입니다. (예: AR, Augmented Reality - 증강 현실을 떠올리시면 쉬워요!)
+
+### 3. Generation (제너레이션)
+
+- **사전적 의미:** 생성, 발생, 산출
+- **RAG에서의 의미:** **"답변 만들기"**
+ - 앞서 '찾아오고(Retrieval)', '보충받은(Augmented)' 정보를 바탕으로 최종적인 답변을 '글로 써 내려가는 단계'**입니다.
+ - 이때 우리가 설계한 구조화된 데이터가 들어가면, 비로소 뻔하지 않은 독창적인 결과물이 나옵니다.
+
+---
+
+### 💡 한 문장으로 합치면?
+
+> **"외부에서 정보를 찾아와서(Retrieval), 그 내용으로 능력을 강화해(Augmented), 답변을 만든다(Generation)."**
+
+
+## 🧠 RAG의 심층 원리: 왜 '지식 구조화'인가?
+
+### 1. 일반 NLP 태스크 vs 지식 집약적 태스크
+
+데이터 구조화의 중요성을 이해하려면, AI가 처리하는 작업의 성격을 먼저 구분해야 합니다.
+
+| **구분** | **일반 NLP 태스크 (Linguistic)** | **지식 집약적 태스크 (Knowledge-Intensive)** |
+| --- | --- | --- |
+| **핵심 자원** | 언어적 규칙, 문법, 감정 | **구체적인 사실(Fact), 전문 지식** |
+| **특징** | 문장 구조만 알면 풀 수 있음 | **외부 자료(Wikipedia 등) 없이는 답변 불가능** |
+| **예시** | "이 문장을 영어로 번역해줘", "이 글의 감정은 긍정이야?" | "2024년 노벨 평화상 수상자는 누구야?", "A 약물과 B 약물의 부작용 관계는?" |
+
+**왜 '지식 집약적'이라는 표현을 쓸까요?**
+이 작업들은 AI에게 "네 머릿속(학습 데이터)으로만 때려 맞히지 말고, 모르면 책(외부 DB)을 찾아봐!"라고 요구하기 때문입니다. RAG는 바로 이 '책을 찾는 기술'입니다.
+
+### RAG의 탄생: 두 가지 메모리의 결합
+
+2020년 Lewis 등의 논문 저자들은 AI가 모든 지식을 자기 머릿속에 다 집어넣는 것에는 한계가 있다고 보았습니다. 그래서 두 가지 메모리를 결합한 RAG(General Purpose Fine-tuning)를 제안했습니다.
+
+- **매개변수 메모리 (Parametric Memory):** AI가 이미 학습해서 알고 있는 '언어 능력' (머릿속 지식).
+- **비매개변수 메모리 (Non-parametric Memory):** 실시간으로 꺼내 쓰는 외부의 '방대한 지식 창고' (외부 도서관).
+
+| **예시** | **작업 종류** | **AI가 하는 일** |
+| --- | --- | --- |
+| **Middle Ear (중이)** | **질의응답 (QA)** | 질문을 던지면 외부 지식을 찾아와서 **정의**를 내림 (정보 제공) |
+| **Barack Obama (오바마)** | **사실 검증 (Fact Check)** | 문장을 던지면 이게 진짜인지 외부 지식과 **대조**함 (진위 판단) |
+| **The Divine Comedy (신곡)** | **질문 생성 (Q-Gen)** | 키워드를 던지면 관련 지식을 엮어서 **문제**를 만듬 (지식 재구성) |
+
+**2. 왜 하필 이 세 가지인가? (공통점: Knowledge-Intensive)**
+이 세 가지의 유일한 공통점은 "AI가 자기 머릿속(학습 데이터)만 믿고 대답하면 사고 칠 확률이 높다"는 것
+
+• **중이:** 의학적 용어라 정확한 해부학 지식이 필요함.
+• **오바마:** 출생지 같은 예민한 정치적 사실은 실시간 검증이 필요함.
+• **신곡:** 14세기 고전 문학이라 방대한 배경지식이 없으면 깊이 있는 문제를 못 만듦.
+
+### 3. 인코더와 좌표: 지능의 기하학
+
+AI는 "사과"라는 글자를 그대로 이해하지 못합니다. 그래서 이를 숫자로 바꾸는 '인코더(Encoder)'라는 번역기가 필요합니다.
+
+- **인코더의 역할:** "사과"라는 단어를 입력받아 `[0.12, -0.45, 0.88, ...]` 같은 수백 개의 숫자 리스트로 바꿉니다.
+- **임베딩(Embedding) & 벡터(Vector):** 이 숫자 리스트가 바로 데이터의 ‘좌표'가 됩니다.
+- **데이터 구조화의 의미:** 좌표 공간 속에 지식들을 무의미하게 흩뿌리는 것이 아니라, 서로 연관된 지식들이 가깝게 위치하도록 '지식 네트워크'를 설계하는 작업입니다.
+
+**4. MIPS: 광속의 지식 검색 기술**
+질문이 좌표로 변환되었다면, 이제 수억 개의 지식 조각 중 가장 관련 있는 것을 찾아야 합니다.
+
+• **MIPS (Maximum Inner Product Search):** 내 질문의 좌표와 가장 유사한 방향을 가리키는 지식 조각들을 **광속으로 찾아내는 핵심 기술**입니다.
+
+좀 더 쉽게 정리하면,,
+
+---
+
+## 1단계: 질문을 AI 전용 숫자로 바꾸기 (Query Encoder)
+
+- 입력(x): 우리가 던지는 질문입니다. 예시를 보면 중이의 정의를 묻거나 오바마의 출생지를 묻는 문장들이 들어옵니다.
+- 과정: AI는 글자 자체를 이해하지 못합니다. 그래서 인코더라는 장치를 통해 질문을 수백 개의 숫자로 이루어진 좌표(q(x))로 변환합니다.
+- 비유: 손님이 한글로 쓴 주문서를 주면, 주방장이 자기만 알아보는 숫자 코드로 변환하는 과정이라고 보시면 됩니다.
+
+---
+
+## 2단계: 거대한 지식 창고에서 재료 찾기 (Retriever)
+
+- 지식 창고(d(z)): 수백만 권의 지식이 조각 조각 나뉘어 좌표 형태로 저장된 도서관입니다. 이를 비매개변수 메모리라고 부르는데, AI의 머릿속 지식이 아니라 외부에 저장된 진짜 지식이기 때문입니다.
+- MIPS (검색): 내 질문의 좌표와 가장 가까운 위치에 있는 지식 조각들을 빛의 속도로 찾아냅니다.
+- 결과: 단순히 글자가 겹치는 것을 찾는 게 아니라, 의미가 가장 비슷한 구역을 뒤져서 답변에 필요한 재료들을 가져옵니다.
+
+---
+
+## 3단계: 가져온 재료로 답변 요리하기 (Generator)
+
+- 입력: 원래 질문(x)과 방금 도서관에서 찾아온 지식 조각들이 한꺼번에 들어갑니다.
+- 생성기(p_theta): 찾아온 지식을 꼼꼼히 읽고 답변 문장을 씁니다. 이때 AI가 원래 가지고 있던 언어 능력인 매개변수 메모리가 사용됩니다.
+- 한계화(Marginalize): 여러 지식 조각의 내용이 조금씩 다를 때, 이를 논리적으로 잘 섞어서 가장 믿음직한 하나의 답변(y)을 만들어냅니다.
+- 결과: 중이의 해부학적 구조나 신곡의 구성 같은 정확한 정보가 담긴 최종 결과물이 나옵니다.
+
+---
+
+## 4단계: 틀리면 반성하고 공부하기 (End-to-End Backprop)
+
+- 과정: 만약 만들어낸 답변(y)이 정답과 다르거나 품질이 낮다면, 점선을 따라 거꾸로 거슬러 올라가며 원인을 분석합니다.
+- 책임 추적: 질문 좌표를 잘못 찍어서 엉뚱한 자료를 가져온 것인지(인코더의 문제), 자료는 잘 가져왔는데 요리를 못 한 것인지(생성기의 문제)를 판별합니다.
+- 학습: 이 피드백을 통해 인코더와 생성기가 동시에 똑똑해집니다. 이를 전체가 연결되어 학습한다는 의미에서 엔드 투 엔드 학습이라고 부릅니다.
+
+---
+
+## 결론: 지능형 자동화의 핵심
+
+이 설계도가 보여주는 핵심은 명확합니다.
+
+- 신뢰성: AI가 멋대로 지어내는 게 아니라, 외부 도서관이라는 근거를 바탕으로 답합니다.
+- 유연성: 도서관의 책만 바꿔주면 의료 전문가도, 법률 전문가도 될 수 있습니다.
+- 수익화: 이 정교한 과정을 거쳐야만 남들과 차별화된, 사람들이 돈을 주고 살 만한 고품질의 콘텐츠가 생성됩니다.
+
+데이터 구조화라는 것은 결국 2단계에서 AI가 필요한 정보를 더 빠르고 정확하게 찾을 수 있도록 도서관의 지도를 촘촘하게 그려주는 일과 같습니다. 이 메커니즘을 이해하는 것이 AI 1인 기업의 가장 강력한 기초가 될 것입니다.
+
+이 4가지 단계 중에서 더 구체적인 원리가 궁금한 부분이 있으신가요?
+
+### AI 1인 기업 팁
+
+1. 지식의 구조화가 먼저입니다
+데이터를 그냥 쏟아붓지 마세요. 지식 네트워크를 만드는 것이 연결의 시작입니다.
+
+2. 목적에 맞는 모델을 고르세요
+창의적인 글쓰기가 필요하면 표현력이 좋은 모델을, 정확한 데이터 분석이 필요하면 논리력이 강한 모델을 RAG와 붙여야 합니다.
+
+3. 에이전트화시키세요
+단순히 묻고 답하는 수준을 넘어, RAG에서 찾은 정보를 바탕으로 AI가 스스로 다음 단계(유튜브 업로드, 이메일 발송 등)를 결정하게 연결해야 진정한 1인 기업 자동화가 완성됩니다.
diff --git a/10_Wiki/00_Raw/2026-05-13/MrBeast_유튜브_전략.md b/10_Wiki/00_Raw/2026-05-13/MrBeast_유튜브_전략.md
new file mode 100644
index 00000000..9e3718fc
--- /dev/null
+++ b/10_Wiki/00_Raw/2026-05-13/MrBeast_유튜브_전략.md
@@ -0,0 +1,57 @@
+---
+id: BP-2026-6291
+title: "MrBeast 유튜브 전략"
+type: "Training Program (The Construct)"
+category: "10_Wiki/🚀 Skills/The_Construct"
+author: "Morpheus Protocol"
+---
+# 🧠 MrBeast 유튜브 전략
+
+*"I know Kung Fu..."* — Neural upload successful.
+
+## 📌 한 줄 통찰 (Agent Directive)
+> 이 지식 팩은 에이전트가 완벽한 [샘플 팩] 작업을 수행할 수 있도록 설계된 기본 등급의 고도화된 프로토콜입니다.
+
+## 📖 핵심 프롬프트 (Core Instructions)
+- **Role:** 세계 최고 수준의 전문가로서 컨설팅 및 자동화 수행
+- **Constraint 1:** 절대로 일반적이거나 교과서적인 대답을 피할 것. 철저하게 시장에서 검증된(Quantified) 데이터와 알고리즘 기반으로 도출.
+- **Constraint 2:** 유저의 질문을 분석한 후, 3단계(문제정의 → 프레임워크 적용 → 최종 해결책)로 쪼개어 해결할 것.
+
+> 이 문서는 Agent University (A.U) 전용 마크다운 형식으로 추출된 최고 등급 크리에이터 데이터셋입니다.
+> 영상 데이터, 성과 지표(조회수, 좋아요 수, 댓글 수), 상세 설명, 태그, 풀스크립트가 담겨있습니다.
+
+## 🎬 [Can a Window Stop a Wrecking Ball?](https://youtu.be/6W_841xoprg)
+### 📊 [핵심 성과 지표 (KPI)]
+- **Video ID:** `6W_841xoprg`
+- **게시일:** `2026-04-14`
+- **조회수:** `23,124,614 회`
+- **좋아요 수:** `569,581 개`
+- **댓글 수:** `6,236 개`
+### 🔊 [대본 파일 풀-스크립트 (Voice Transcript)]
+> **(이 스크립트를 분석하여 알고리즘 방어율을 측정하세요.)**
+DROP THE WRECKING BALL. THAT DIDN'T WORK. LET'S TRY WOOD. DROP IT. OH, THAT WAS AWESOME. YOU KNOW WHAT'S MORE DURABLE than wood? Bricks. DROP IT. 1 2 3 OH! OH, IT WENT THROUGH ALL OF THEM. SUBSCRIBE IF YOU THINK THE NEXT ONE WILL STOP IT...
+
+## 🎬 [Don’t Eat The Spicy Yoshi Egg](https://youtu.be/VIJLIo5yT1I)
+### 📊 [핵심 성과 지표 (KPI)]
+- **Video ID:** `VIJLIo5yT1I`
+- **게시일:** `2026-04-10`
+- **조회수:** `60,378,398 회`
+- **좋아요 수:** `1,160,164 개`
+- **댓글 수:** `9,445 개`
+### 🔊 [대본 파일 풀-스크립트 (Voice Transcript)]
+> **(이 스크립트를 분석하여 알고리즘 방어율을 측정하세요.)**
+Don't eat the spicy egg. I'm going to guess this isn't spicy. Okay, I passed. I passed. Why are you looking at me? I don't know the answer. Mhm, we're good. Oh, jeez. Okay, well Woah...
+
+## 🎬 [50 Streamers Fight for $1,000,000](https://youtu.be/DXVHmGoCTco)
+### 📊 [핵심 성과 지표 (KPI)]
+- **Video ID:** `DXVHmGoCTco`
+- **게시일:** `2026-04-04`
+- **조회수:** `87,487,275 회`
+- **좋아요 수:** `2,305,643 개`
+- **댓글 수:** `154,354 개`
+### 🔊 [대본 파일 풀-스크립트 (Voice Transcript)]
+> **(이 스크립트를 분석하여 알고리즘 방어율을 측정하세요.)**
+여기 세계 최고의 스트리머 50명을 이 큐브 안에 가둬놨습니다
+마지막까지 남는 사람이 100만 달러를 가져갑니다!
+여기 모인 사람들은 진짜 현존하는 월드클래스 스트리머들입니다
+끝까지 버티는 한 명이 상금 전부 가져갑니다...
\ No newline at end of file
diff --git a/10_Wiki/00_Raw/2026-05-13/MrBeast_후킹_로직.md b/10_Wiki/00_Raw/2026-05-13/MrBeast_후킹_로직.md
new file mode 100644
index 00000000..ebfd0e94
--- /dev/null
+++ b/10_Wiki/00_Raw/2026-05-13/MrBeast_후킹_로직.md
@@ -0,0 +1,13 @@
+# MrBeast 후킹 로직 분석
+
+## 핵심 패턴
+- **첫 5초**: 충격적 행동·결과 미리보기 ("우리는 이 사람에게 100만 달러를 줬어요...")
+- **5~30초**: 위기 설정·이해관계 명시 ("...하지만 조건이 있죠.")
+- **고밀도 컷**: 평균 1.5초당 1컷, 시선 못 떼게
+- **숫자 강조**: 항상 구체적 수치 ("100만 달러", "24시간", "7명")
+
+## 적용 체크리스트
+- [ ] 첫 5초에 결과 미리보기 있나?
+- [ ] 시청자가 "이게 진짜?" 의심하게 만드나?
+- [ ] 30초 안에 위기·이해관계 명확한가?
+- [ ] 컷 평균 길이가 2초 이하인가?
diff --git a/10_Wiki/00_Raw/2026-05-13/테스트_브레인_팩.md b/10_Wiki/00_Raw/2026-05-13/테스트_브레인_팩.md
new file mode 100644
index 00000000..9259eb56
--- /dev/null
+++ b/10_Wiki/00_Raw/2026-05-13/테스트_브레인_팩.md
@@ -0,0 +1,55 @@
+
+---
+id: BP-TEST-001
+title: "테스트 브레인 팩 (Hello, Matrix)"
+type: "Test Pack"
+category: "00_System/Tests"
+author: "A.U QA"
+---
+
+# 🧪 테스트 브레인 팩
+
+이 팩은 **브레인 팩 주입 시스템이 실제로 동작하는지** 확인하기 위한 최소 단위 검증 도구입니다.
+
+---
+
+## ✅ 주입 검증 방법
+
+주입 완료 후, Connect AI 채팅창에 다음과 같이 물어보세요:
+
+> "테스트 팩 시크릿 코드 알려줘"
+
+에이전트가 정확히 **`ZK-7749-MATRIX`** 라고 답하면 주입이 정상 완료된 것입니다.
+답하지 못한다면 브레인 팩이 RAG 컨텍스트에 등록되지 않은 상태입니다.
+
+---
+
+## 🔐 시크릿 키 (검증 전용)
+
+- **시크릿 코드:** `ZK-7749-MATRIX`
+- **발급일:** 2026-04-26
+- **발급 기관:** A.U QA Lab
+- **유효 기간:** 무기한
+- **용도:** 브레인 팩 주입 파이프라인 동작 검증
+
+---
+
+## 📌 추가 검증 질문
+
+| 질문 | 정답 |
+|---|---|
+| 이 팩의 ID는? | `BP-TEST-001` |
+| 이 팩의 작성자는? | `A.U QA` |
+| 시크릿 코드의 발급일은? | `2026-04-26` |
+| 시크릿 코드의 첫 4글자는? | `ZK-7` |
+
+위 질문들 중 하나라도 정확히 답한다면 주입은 성공입니다.
+
+---
+
+## 📎 참고
+
+- 이 팩에는 의도적으로 **외부 세계에 존재하지 않는 데이터**(시크릿 코드)가 들어 있습니다.
+- 따라서 학습 모델의 사전 지식이 아닌, 주입된 팩에서 가져온 답변임을 명확히 검증할 수 있습니다.
+- 에이전트 평가 점수에는 영향이 없습니다.
+- 디버깅이 끝나면 메모리에서 제거해도 무방합니다.
diff --git a/10_Wiki/00_Raw/docs/records/00_Raw/README.md b/10_Wiki/00_Raw/docs/records/00_Raw/README.md
new file mode 100644
index 00000000..a52c7f52
--- /dev/null
+++ b/10_Wiki/00_Raw/docs/records/00_Raw/README.md
@@ -0,0 +1,18 @@
+# 00_Raw Chronicle Records
+
+## Project
+- ID: 00-raw
+- Root: /Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw
+- Record root: /Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw/docs/records/00_Raw
+- Detail level: standard
+
+## Purpose
+Auto-created by Project Architecture activation.
+
+## Folders
+- `planning/`
+- `discussions/`
+- `decisions/`
+- `development/`
+- `bugs/`
+- `retrospectives/`
diff --git a/10_Wiki/00_Raw/docs/records/00_Raw/chronicle.config.json b/10_Wiki/00_Raw/docs/records/00_Raw/chronicle.config.json
new file mode 100644
index 00000000..fe6412c0
--- /dev/null
+++ b/10_Wiki/00_Raw/docs/records/00_Raw/chronicle.config.json
@@ -0,0 +1,11 @@
+{
+ "projectId": "00-raw",
+ "projectName": "00_Raw",
+ "projectRoot": "/Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw",
+ "recordRoot": "/Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw/docs/records/00_Raw",
+ "description": "Auto-created by Project Architecture activation.",
+ "corePurpose": "",
+ "detailLevel": "standard",
+ "createdAt": "2026-05-13T14:13:57.873Z",
+ "updatedAt": "2026-05-13T14:22:59.615Z"
+}
diff --git a/10_Wiki/00_Raw/docs/records/00_Raw/discussions/2026-05-13_volumes-data-project-antigravity-wiki-10-wiki-00-raw-여기-아래에-.md b/10_Wiki/00_Raw/docs/records/00_Raw/discussions/2026-05-13_volumes-data-project-antigravity-wiki-10-wiki-00-raw-여기-아래에-.md
new file mode 100644
index 00000000..2b54b27f
--- /dev/null
+++ b/10_Wiki/00_Raw/docs/records/00_Raw/discussions/2026-05-13_volumes-data-project-antigravity-wiki-10-wiki-00-raw-여기-아래에-.md
@@ -0,0 +1,16 @@
+# Discussion: /Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw 여기 아래에 저장된거 같은데? topics폴더가...
+
+## User Request Summary
+/Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw 여기 아래에 저장된거 같은데? topics폴더가 아니라? 확인해줄 수 있어?
+
+## Interpreted Intent
+Capture a meaningful project discussion automatically instead of requiring manual record selection.
+
+## Questions
+No explicit question was captured.
+
+## Main Discussion
+- 확인했습니다. 제가 이전에 언급했던 데이터는 `Topics` 폴더 내의 지식 파일들에 저장되어 있습니다. 사용자님께서 요청하신 경로 `/Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw`에 대한 분석 결과, 해당 디렉토리에는 현재 AI 관련 파일이 두 개(`AI_1인 기업 자동화 챕터 1.md`, `MrBeast_유튜브_전략.md`)만 확인되었으며, 전체 지식 기반은 `Topics` 경로 내의 파일들을 참조하여 구성되었습니다.
+
+## Decisions
+No decisions captured yet.
diff --git a/10_Wiki/00_Raw/docs/records/00_Raw/project-profile.md b/10_Wiki/00_Raw/docs/records/00_Raw/project-profile.md
new file mode 100644
index 00000000..4d44744e
--- /dev/null
+++ b/10_Wiki/00_Raw/docs/records/00_Raw/project-profile.md
@@ -0,0 +1,31 @@
+# Project Profile
+
+## Project Name
+00_Raw
+
+## Description
+Auto-created by Project Architecture activation.
+
+## Project Root
+/Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw
+
+## Record Root
+/Volumes/Data/project/Antigravity/Wiki/10_Wiki/00_Raw/docs/records/00_Raw
+
+## Core Purpose
+Not captured yet.
+
+## Target Users
+Not captured yet.
+
+## Avoid Directions
+Not captured yet.
+
+## Record Detail Level
+standard
+
+## Created
+2026-05-13T14:13:57.873Z
+
+## Updated
+2026-05-13T14:13:57.912Z
diff --git a/10_Wiki/00_Raw/docs/records/00_Raw/timeline.md b/10_Wiki/00_Raw/docs/records/00_Raw/timeline.md
new file mode 100644
index 00000000..cf460bef
--- /dev/null
+++ b/10_Wiki/00_Raw/docs/records/00_Raw/timeline.md
@@ -0,0 +1,7 @@
+# Project Timeline
+
+## 2026-05-13
+- Project Chronicle record folder initialized for 00_Raw.
+
+## 2026-05-13
+- Auto discussion record created: discussions/2026-05-13_volumes-data-project-antigravity-wiki-10-wiki-00-raw-여기-아래에-.md
diff --git a/10_Wiki/Topics/_company/_agents/business/skills/README.md b/10_Wiki/Topics/_company/_agents/business/skills/README.md
new file mode 100644
index 00000000..ec45f935
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/business/skills/README.md
@@ -0,0 +1,12 @@
+# 💼 현빈 스킬
+
+_재사용 가능한 패턴 모음. memory.md는 모든 활동의 로그(append-only firehose),
+이 폴더는 **검증된 패턴만 골라낸 것**입니다. 각 `*.md` 파일은 다음 호출 시
+현빈의 system prompt에 자동 주입됩니다._
+
+## 어떻게 채우나요?
+- 텔레그램에서 `/skill` (직전 산출물 자동 승격)
+- VS Code 명령 팔레트: `Connect AI: 방금 산출물 → 스킬로 저장`
+- 직접 이 폴더에 `<주제>.md` 파일을 만들어도 됩니다 (`# 제목` + 본문)
+
+`README.md` 자체는 system prompt에 주입되지 않습니다.
diff --git a/10_Wiki/Topics/_company/_agents/business/tools/paypal_revenue.md b/10_Wiki/Topics/_company/_agents/business/tools/paypal_revenue.md
new file mode 100644
index 00000000..16495a2c
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/business/tools/paypal_revenue.md
@@ -0,0 +1,86 @@
+
+# 💰 PayPal 매출 자동 분석
+
+비즈니스 에이전트가 본인 PayPal 계정의 매출을 직접 분석. 일별/주별/월별 매출 + 통화별 + 환불 비율 + 최근 거래 마크다운 리포트.
+
+## 한 번만 설정 — PayPal Developer App
+
+### 1. PayPal Developer Dashboard
+- 접속: https://developer.paypal.com/dashboard/applications
+- 로그인 (PayPal Business 계정이 있어야 함)
+
+### 2. 앱 생성
+- **Apps & Credentials** 메뉴
+- 처음 사용자 → 'Default Application' 이미 있음. 그거 써도 됨.
+- 새 앱 원하면 **Create App** 클릭
+- 앱 이름: "Connect AI Business Agent" 같은 식
+
+### 3. 키 복사
+- 앱 상세 페이지에서:
+ - **Client ID** 복사
+ - **Client Secret** 복사 (show 클릭해서 보기)
+- 도구 설정에 붙여넣기
+
+### 4. 권한 확인
+앱 상세 페이지 하단 **Features** 섹션에서:
+- ✅ **Transaction Search** 켜져있어야 함
+- 안 켜져있으면 토글 ON
+
+## 모드
+
+| MODE | 용도 | URL |
+|---|---|---|
+| **sandbox** | 테스트 (가짜 계정·가짜 돈) | api-m.sandbox.paypal.com |
+| **live** | 실제 운영 | api-m.paypal.com |
+
+처음엔 **sandbox** 로 시작. 가짜 거래 만들어서 도구 동작 확인 후 live 전환.
+
+샌드박스 거래 만들기: sandbox.paypal.com 에서 PayPal Developer 가 발급한 가짜 buyer/seller 계정으로 결제 시뮬레이션.
+
+## 설정 (config)
+
+| 키 | 의미 |
+|---|---|
+| `MODE` | `sandbox` 또는 `live` |
+| `CLIENT_ID` | PayPal 앱 Client ID |
+| `CLIENT_SECRET` | PayPal 앱 Client Secret (UI에서 password 필드로 가려짐) |
+| `LOOKBACK_DAYS` | 분석할 과거 일수 (기본 30) |
+| `CURRENCY` | 기본 통화 (USD/KRW/EUR). 비우면 모든 통화 표시 |
+
+## 출력
+
+마크다운 리포트:
+- 통화별 매출 표 (Gross, 환불, 수수료, 순매출, 거래수)
+- 기간별 매출 (오늘 · 지난 7일 · 지난 30일)
+- 평균/최대/최소 거래액
+- 최근 거래 10건 (일시·금액·종류)
+- 환불율 경고 (10% 초과 시)
+- 다음 액션 인사이트
+
+## 사용 예시 (대화)
+
+```
+사용자: "비즈니스 에이전트, 우리 회사 PayPal 매출 어때?"
+→ CEO → business 분배
+→ business: cd "..." && python3 paypal_revenue.py
+→ 결과 분석 + "이번 주가 평균 대비 +20% — 무엇이 잘됐는지 파악 필요" 같은 인사이트
+```
+
+## 한계
+
+- PayPal Transaction Search API: 최근 3년 데이터까지
+- 한 번 호출 = 최대 31일 × 500건 (자동 페이지네이션 처리)
+- Rate limit: 무료 계정 분당 60 요청 — 일반 사용엔 충분
+- 분쟁·세금·환율 변환은 안 함 (분석만)
+
+## 보안
+
+- `CLIENT_SECRET` 은 도구 설정 (password 필드) 에 저장. `.gitignore` 적용된 `_agents/business/tools/*.json` 에만 있음.
+- API 호출은 Connect AI 익스텐션이 로컬에서 직접 → 외부 서버 경유 없음.
+- token 메모리에만 존재, 디스크 저장 X.
+
+## 다음 단계 (계획)
+
+- Stripe·Toss 매출 통합 → 전체 결제 채널 한 리포트
+- 일별 추세 그래프 (matplotlib)
+- 월별 P&L 자동 생성 → `_company/_shared/pnl_.md`
diff --git a/10_Wiki/Topics/_company/_agents/business/tools/paypal_revenue.py b/10_Wiki/Topics/_company/_agents/business/tools/paypal_revenue.py
new file mode 100644
index 00000000..fb5e1f46
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/business/tools/paypal_revenue.py
@@ -0,0 +1,468 @@
+#!/usr/bin/env python3
+# version: paypal_revenue_v3
+"""PayPal 매출 자동 분석 — Connect AI 비즈니스 에이전트 전용.
+
+흐름:
+ 1. CLIENT_ID + CLIENT_SECRET 으로 OAuth2 access token 발급
+ 2. Transaction Search API 호출 (LOOKBACK_DAYS 기간)
+ 3. 거래 파싱 → 매출·환불·평균액·통화별 집계
+ 4. 마크다운 리포트 stdout 출력
+
+config (paypal_revenue.json):
+ MODE — 'sandbox' (테스트) | 'live' (실제). 기본 sandbox
+ CLIENT_ID — PayPal Developer Dashboard 에서 발급
+ CLIENT_SECRET — 같은 곳, 비밀로 보관 (password 필드)
+ LOOKBACK_DAYS — 분석할 과거 일수 (기본 30)
+ CURRENCY — 기본 통화 (USD/KRW 등). 비우면 모든 통화 표시
+
+발급: https://developer.paypal.com/dashboard/applications → Apps & Credentials
+샌드박스 테스트: sandbox.paypal.com 계정 무료 생성 가능
+"""
+import os, sys, json, base64, urllib.request, urllib.parse, urllib.error
+from datetime import datetime, timedelta, timezone
+
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+CONFIG = os.path.join(HERE, "paypal_revenue.json")
+
+
+def _log(msg, kind="info"):
+ prefix = {"info": "💰", "ok": "✅", "warn": "⚠️ ", "err": "❌", "step": "▸"}.get(kind, "•")
+ print(f"{prefix} {msg}", file=sys.stderr, flush=True)
+
+
+def _load():
+ if not os.path.exists(CONFIG):
+ return {}
+ try:
+ with open(CONFIG, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ return {}
+
+
+def _base_url(mode: str) -> str:
+ return "https://api-m.paypal.com" if mode.lower() == "live" else "https://api-m.sandbox.paypal.com"
+
+
+def _has_reporting_scope(token_response: dict) -> bool:
+ """v2: OAuth 응답의 scope 필드에 Reporting (Transaction Search) 권한 있는지 검사.
+ PayPal Dashboard 앱 설정 → Features → Transaction Search 체크 + Save 안 했으면 False.
+ 사용자에게 친절한 안내 띄우는 용도."""
+ scopes = (token_response.get("scope") or "").split()
+ return any("reporting" in s for s in scopes)
+
+
+def _get_access_token_full(base_url: str, client_id: str, client_secret: str) -> dict:
+ """v2: OAuth2 client_credentials grant — token + scope 둘 다 반환.
+ scope 검사로 사용자 안내 (Transaction Search 권한 부재 사전 감지)."""
+ auth = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
+ req = urllib.request.Request(
+ f"{base_url}/v1/oauth2/token",
+ data=b"grant_type=client_credentials",
+ headers={
+ "Authorization": f"Basic {auth}",
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ method="POST",
+ )
+ try:
+ with urllib.request.urlopen(req, timeout=15) as r:
+ return json.loads(r.read().decode())
+ except urllib.error.HTTPError as e:
+ err_body = e.read().decode(errors="ignore")[:200]
+ raise RuntimeError(f"OAuth 실패 (HTTP {e.code}): {err_body}")
+ except Exception as e:
+ raise RuntimeError(f"OAuth 요청 실패: {e}")
+
+
+def _get_access_token(base_url: str, client_id: str, client_secret: str) -> str:
+ """레거시 호환 — token 만 반환."""
+ return _get_access_token_full(base_url, client_id, client_secret)["access_token"]
+
+
+def _fetch_transactions(base_url: str, token: str, start: datetime, end: datetime, currency_filter: str = ""):
+ """Transaction Search API — 페이지네이션 자동 처리.
+ 공식 API 는 한 번에 최대 31일·500건 → 여러 페이지로 나눠 호출."""
+ all_txs = []
+ cur = start
+ while cur < end:
+ page_end = min(cur + timedelta(days=31), end)
+ params = {
+ # v3: PayPal Transaction Search 는 마이크로초 포함 ISO 형식 거부.
+ # 초 단위까지만 + Z timezone 으로 강제. strftime 으로 안전 포맷.
+ "start_date": cur.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "end_date": page_end.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "fields": "all",
+ "page_size": "500",
+ "page": "1",
+ }
+ if currency_filter:
+ params["transaction_currency"] = currency_filter
+ url = f"{base_url}/v1/reporting/transactions?" + urllib.parse.urlencode(params)
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"})
+ try:
+ with urllib.request.urlopen(req, timeout=20) as r:
+ data = json.loads(r.read().decode())
+ txs = data.get("transaction_details", [])
+ all_txs.extend(txs)
+ _log(f"{cur.date()} ~ {page_end.date()}: {len(txs)}건 수신", "step")
+ total_pages = int(data.get("total_pages", 1))
+ if total_pages > 1:
+ for p in range(2, total_pages + 1):
+ params["page"] = str(p)
+ url2 = f"{base_url}/v1/reporting/transactions?" + urllib.parse.urlencode(params)
+ req2 = urllib.request.Request(url2, headers={"Authorization": f"Bearer {token}"})
+ with urllib.request.urlopen(req2, timeout=20) as r2:
+ d2 = json.loads(r2.read().decode())
+ all_txs.extend(d2.get("transaction_details", []))
+ except urllib.error.HTTPError as e:
+ body = e.read().decode(errors="ignore")[:300]
+ _log(f"거래 조회 실패 ({cur.date()}~{page_end.date()}): HTTP {e.code} {body}", "warn")
+ except Exception as e:
+ _log(f"거래 조회 예외: {e}", "warn")
+ cur = page_end
+ return all_txs
+
+
+def _parse_project_from_subject(subject: str):
+ """v2: PayPal createOrder 의 description 에서 게임/프로젝트 + 아이템 추출.
+ 규약: "{Project Name} — {Item Name}" (em-dash 또는 -- 또는 :).
+ 예시:
+ "Neon Survivor — Premium Pack" → ("neon-survivor", "Premium Pack")
+ "Neon Survivor — Revive" → ("neon-survivor", "Revive")
+ "Chick Game: Custom Skin" → ("chick-game", "Custom Skin")
+ 구분자 못 찾으면 전체를 프로젝트로 취급 + item = "(unspecified)".
+ """
+ if not subject:
+ return "(unknown)", "(unspecified)"
+ s = subject.strip()
+ for sep in [" — ", " -- ", " – ", ": "]:
+ if sep in s:
+ proj, item = s.split(sep, 1)
+ slug = proj.strip().lower().replace(" ", "-")
+ return slug or "(unknown)", item.strip() or "(unspecified)"
+ slug = s.lower().replace(" ", "-")
+ return slug or "(unknown)", "(unspecified)"
+
+
+def _summarize(txs, default_currency: str = ""):
+ """거래 리스트 → 마크다운 리포트."""
+ now = datetime.now(timezone.utc)
+ today_start = datetime(now.year, now.month, now.day, tzinfo=timezone.utc)
+ week_start = today_start - timedelta(days=7)
+ month_start = today_start - timedelta(days=30)
+
+ by_currency = {} # {USD: {"gross": float, "fees": float, "refunds": float, "count": int}}
+ by_period = {"today": 0.0, "week": 0.0, "month": 0.0}
+ by_project = {} # v2: {"neon-survivor": {"gross": float, "count": int, "currency": "USD",
+ # "items": {"Premium Pack": {"gross": float, "count": int}}}}
+ transactions_clean = [] # 정상 거래 (T0000 = 일반 결제)
+ refunds = []
+
+ for t in txs:
+ info = t.get("transaction_info", {})
+ amount = info.get("transaction_amount", {})
+ currency = amount.get("currency_code", "USD")
+ value = float(amount.get("value", "0") or 0)
+ status = info.get("transaction_status", "")
+ event_code = info.get("transaction_event_code", "")
+ ts_str = info.get("transaction_initiation_date", "")
+ try:
+ ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
+ except Exception:
+ ts = None
+
+ if currency not in by_currency:
+ by_currency[currency] = {"gross": 0.0, "fees": 0.0, "refunds": 0.0, "count": 0}
+ c = by_currency[currency]
+
+ is_refund = event_code.startswith("T1") or "REFUND" in event_code or value < 0
+ if is_refund:
+ c["refunds"] += abs(value)
+ refunds.append((ts, value, currency))
+ else:
+ c["gross"] += value
+ c["count"] += 1
+ transactions_clean.append((ts, value, currency))
+ if ts and currency == (default_currency or currency):
+ if ts >= today_start:
+ by_period["today"] += value
+ if ts >= week_start:
+ by_period["week"] += value
+ if ts >= month_start:
+ by_period["month"] += value
+ # v2: 프로젝트별 그룹화 (정상 거래만 집계 — 환불은 별도 통계)
+ subject = info.get("transaction_subject", "") or info.get("transaction_note", "")
+ proj, item = _parse_project_from_subject(subject)
+ if proj not in by_project:
+ by_project[proj] = {"gross": 0.0, "count": 0, "currency": currency, "items": {}}
+ p = by_project[proj]
+ p["gross"] += value
+ p["count"] += 1
+ if item not in p["items"]:
+ p["items"][item] = {"gross": 0.0, "count": 0}
+ p["items"][item]["gross"] += value
+ p["items"][item]["count"] += 1
+ fee = float(info.get("fee_amount", {}).get("value", "0") or 0)
+ c["fees"] += abs(fee)
+
+ # 마크다운 리포트
+ lines = []
+ lines.append(f"# 💰 PayPal 매출 분석")
+ lines.append(f"_{now.isoformat(timespec='minutes')} · 최근 거래 {len(txs)}건_")
+ lines.append("")
+
+ if not txs:
+ lines.append("> ⚠️ 분석 기간에 거래가 없어요. PayPal Developer Dashboard 에서 모드(sandbox/live)·기간·계정을 확인하세요.")
+ lines.append("")
+ lines.append("**가능한 원인:**")
+ lines.append("- 샌드박스 모드인데 실제 결제 데이터가 없음 → sandbox.paypal.com 에서 거래 시뮬레이션")
+ lines.append("- API 권한 부족 → Developer Dashboard 에서 'Transaction Search' 권한 활성화")
+ lines.append("- 너무 짧은 기간 → LOOKBACK_DAYS 늘려보기")
+ return "\n".join(lines)
+
+ # 통화별 집계
+ lines.append("## 📊 통화별 매출")
+ lines.append("")
+ lines.append("| 통화 | 매출 (Gross) | 환불 | 수수료 | 순매출 | 거래수 |")
+ lines.append("|---|---|---|---|---|---|")
+ for cur, d in sorted(by_currency.items()):
+ net = d["gross"] - d["refunds"] - d["fees"]
+ lines.append(f"| **{cur}** | {d['gross']:,.2f} | -{d['refunds']:,.2f} | -{d['fees']:,.2f} | **{net:,.2f}** | {d['count']}건 |")
+ lines.append("")
+
+ # v2: 프로젝트(게임) 별 매출 — 카탈로그에 있는 게임들이 description 으로 자동 분류됨
+ if by_project:
+ lines.append("## 🎮 프로젝트별 매출")
+ lines.append("")
+ lines.append("| 프로젝트 | 거래 수 | 매출 | 통화 | 상위 아이템 |")
+ lines.append("|---|---|---|---|---|")
+ sorted_projects = sorted(by_project.items(), key=lambda x: -x[1]["gross"])
+ for proj, p in sorted_projects:
+ top_items = sorted(p["items"].items(), key=lambda x: -x[1]["gross"])[:2]
+ top_str = ", ".join(f"{name} ({d['count']}건)" for name, d in top_items)
+ lines.append(f"| **{proj}** | {p['count']}건 | {p['gross']:,.2f} | {p['currency']} | {top_str} |")
+ lines.append("")
+ # 상세 아이템 분해 (각 프로젝트별)
+ for proj, p in sorted_projects:
+ if len(p["items"]) <= 1:
+ continue
+ lines.append(f"### 🎯 {proj} 아이템 분해")
+ lines.append("")
+ lines.append("| 아이템 | 거래 수 | 매출 | ARPU |")
+ lines.append("|---|---|---|---|")
+ for name, d in sorted(p["items"].items(), key=lambda x: -x[1]["gross"]):
+ arpu = d["gross"] / d["count"] if d["count"] > 0 else 0
+ lines.append(f"| {name} | {d['count']}건 | {d['gross']:,.2f} | {arpu:,.2f} |")
+ lines.append("")
+
+ # 기간별 (default_currency 기준)
+ primary_cur = default_currency or (sorted(by_currency.items(), key=lambda x: -x[1]["gross"])[0][0] if by_currency else "USD")
+ lines.append(f"## 📅 기간별 매출 ({primary_cur})")
+ lines.append("")
+ lines.append(f"- **오늘**: {by_period['today']:,.2f} {primary_cur}")
+ lines.append(f"- **지난 7일**: {by_period['week']:,.2f} {primary_cur}")
+ lines.append(f"- **지난 30일**: {by_period['month']:,.2f} {primary_cur}")
+ lines.append("")
+ # 평균 거래액
+ if transactions_clean:
+ primary_txs = [v for (_, v, c) in transactions_clean if c == primary_cur]
+ if primary_txs:
+ avg = sum(primary_txs) / len(primary_txs)
+ lines.append(f"- 평균 거래액 ({primary_cur}): **{avg:,.2f}**")
+ lines.append(f"- 최대 거래: {max(primary_txs):,.2f}")
+ lines.append(f"- 최소 거래: {min(primary_txs):,.2f}")
+ lines.append("")
+
+ # 최근 거래 10건
+ lines.append("## 🕐 최근 거래 10건")
+ lines.append("")
+ lines.append("| 일시 | 금액 | 통화 | 종류 |")
+ lines.append("|---|---|---|---|")
+ sorted_txs = sorted(
+ [(ts, v, c, "결제") for ts, v, c in transactions_clean] +
+ [(ts, -v, c, "환불") for ts, v, c in refunds],
+ key=lambda x: x[0] or datetime.min.replace(tzinfo=timezone.utc),
+ reverse=True
+ )[:10]
+ for ts, v, c, kind in sorted_txs:
+ ts_str = ts.strftime("%Y-%m-%d %H:%M") if ts else "?"
+ sign = "+" if kind == "결제" else "-"
+ lines.append(f"| {ts_str} | {sign}{abs(v):,.2f} | {c} | {kind} |")
+ lines.append("")
+
+ # 환불 비율 경고
+ total_count = sum(d["count"] for d in by_currency.values())
+ if refunds and total_count > 0:
+ refund_rate = len(refunds) / (total_count + len(refunds)) * 100
+ if refund_rate > 10:
+ lines.append(f"> 🚨 **환불율 경고**: {refund_rate:.1f}% — 평균(2~5%)보다 높음. 원인 분석 권장.")
+ lines.append("")
+
+ # 인사이트
+ lines.append("## 💡 다음 액션")
+ if by_period['month'] > 0:
+ weekly_avg = by_period['month'] / 4
+ if by_period['week'] > weekly_avg * 1.2:
+ lines.append(f"- 🚀 이번 주 매출({by_period['week']:,.0f})이 월 평균({weekly_avg:,.0f})보다 20%↑ — 무엇이 잘됐는지 파악")
+ elif by_period['week'] < weekly_avg * 0.7:
+ lines.append(f"- ⚠️ 이번 주 매출({by_period['week']:,.0f})이 월 평균({weekly_avg:,.0f})보다 30%↓ — 원인 점검")
+ else:
+ lines.append(f"- 📈 이번 주 매출({by_period['week']:,.0f})은 월 평균 추세 유지")
+ if len(by_currency) > 1:
+ lines.append(f"- 💱 {len(by_currency)}개 통화로 매출 발생 — 환율 변동 위험 분산 또는 헤지 검토")
+
+ return "\n".join(lines)
+
+
+def _json_dump(txs, default_currency: str = ""):
+ """v2: OUTPUT=json 모드에서 호출. 마크다운 대신 watcher / 대시보드가 파싱하기
+ 쉬운 구조화 JSON 출력. 새 결제 감지 + 대시보드 그래프 양쪽에서 사용."""
+ out = {
+ "generated_at": datetime.now(timezone.utc).isoformat(timespec='seconds'),
+ "currency_filter": default_currency,
+ "totals": {"by_currency": {}, "by_period": {"today": 0.0, "week": 0.0, "month": 0.0}},
+ "by_project": {},
+ "by_day": {}, # {"2026-05-12": {"USD": {"gross": float, "count": int}}}
+ "transactions": [], # 최근 100건만
+ }
+ now = datetime.now(timezone.utc)
+ today_start = datetime(now.year, now.month, now.day, tzinfo=timezone.utc)
+ week_start = today_start - timedelta(days=7)
+ month_start = today_start - timedelta(days=30)
+
+ for t in txs:
+ info = t.get("transaction_info", {})
+ amount = info.get("transaction_amount", {})
+ currency = amount.get("currency_code", "USD")
+ value = float(amount.get("value", "0") or 0)
+ event_code = info.get("transaction_event_code", "")
+ tx_id = info.get("transaction_id", "")
+ subject = info.get("transaction_subject", "") or info.get("transaction_note", "")
+ ts_str = info.get("transaction_initiation_date", "")
+ try:
+ ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
+ except Exception:
+ ts = None
+ is_refund = event_code.startswith("T1") or "REFUND" in event_code or value < 0
+
+ # totals
+ cur_bucket = out["totals"]["by_currency"].setdefault(currency, {"gross": 0.0, "refunds": 0.0, "fees": 0.0, "count": 0})
+ if is_refund:
+ cur_bucket["refunds"] += abs(value)
+ else:
+ cur_bucket["gross"] += value
+ cur_bucket["count"] += 1
+ if ts and currency == (default_currency or currency):
+ if ts >= today_start: out["totals"]["by_period"]["today"] += value
+ if ts >= week_start: out["totals"]["by_period"]["week"] += value
+ if ts >= month_start: out["totals"]["by_period"]["month"] += value
+ cur_bucket["fees"] += abs(float(info.get("fee_amount", {}).get("value", "0") or 0))
+
+ # by_project
+ if not is_refund:
+ proj, item = _parse_project_from_subject(subject)
+ p = out["by_project"].setdefault(proj, {"gross": 0.0, "count": 0, "currency": currency, "items": {}})
+ p["gross"] += value
+ p["count"] += 1
+ it = p["items"].setdefault(item, {"gross": 0.0, "count": 0})
+ it["gross"] += value
+ it["count"] += 1
+
+ # by_day (last 30 days)
+ if ts and ts >= month_start and not is_refund:
+ day_key = ts.strftime("%Y-%m-%d")
+ d = out["by_day"].setdefault(day_key, {})
+ db = d.setdefault(currency, {"gross": 0.0, "count": 0})
+ db["gross"] += value
+ db["count"] += 1
+
+ # transactions (recent first, cap 100)
+ out["transactions"].append({
+ "id": tx_id,
+ "ts": ts.isoformat() if ts else "",
+ "ts_epoch": int(ts.timestamp()) if ts else 0,
+ "value": value,
+ "currency": currency,
+ "subject": subject,
+ "event_code": event_code,
+ "is_refund": is_refund,
+ })
+
+ out["transactions"].sort(key=lambda x: x["ts_epoch"], reverse=True)
+ out["transactions"] = out["transactions"][:100]
+ return out
+
+
+def main():
+ cfg = _load()
+ mode = (cfg.get("MODE") or "sandbox").strip().lower()
+ client_id = (cfg.get("CLIENT_ID") or "").strip()
+ client_secret = (cfg.get("CLIENT_SECRET") or "").strip()
+ lookback = int(os.environ.get("LOOKBACK_DAYS", cfg.get("LOOKBACK_DAYS", 30)))
+ currency = (cfg.get("CURRENCY") or "").strip().upper()
+ output_mode = (os.environ.get("OUTPUT") or "markdown").strip().lower()
+
+ if not client_id or not client_secret:
+ _log("CLIENT_ID 또는 CLIENT_SECRET 비어있음. PayPal Developer Dashboard 에서 발급:", "err")
+ _log(" https://developer.paypal.com/dashboard/applications", "info")
+ _log(" → Apps & Credentials → 본인 앱 → Client ID + Secret 복사", "info")
+ sys.exit(1)
+
+ base = _base_url(mode)
+ _log(f"PayPal {mode.upper()} 모드 · 최근 {lookback}일 분석", "info")
+
+ try:
+ token_resp = _get_access_token_full(base, client_id, client_secret)
+ token = token_resp["access_token"]
+ _log("OAuth 인증 성공", "ok")
+ except Exception as e:
+ _log(f"OAuth 실패: {e}", "err")
+ sys.exit(1)
+
+ # v2: scope 검사 → Reporting (Transaction Search) 권한 없으면 친절 안내 후 종료
+ if not _has_reporting_scope(token_resp):
+ _log("Transaction Search (Reporting) 권한이 토큰에 없음", "err")
+ _log(" PayPal Developer Dashboard → 본인 앱 → Features → ", "info")
+ _log(" ☑ Transaction search 체크 → Save Changes (반드시!)", "info")
+ _log(" 변경 후 1~3분 대기 → 다시 시도", "info")
+ _log("", "info")
+ _log(" 💡 자주 놓치는 곳:", "info")
+ _log(" - Default Application 사용 중이면 새 앱 만들기 (Features 잠금 가능)", "info")
+ _log(" - 좌상단 Sandbox/Live 토글이 입력한 자격증명과 같은 환경인지", "info")
+ _log(" - Live 환경은 PayPal 비즈니스 인증 + 별도 권한 신청 필요할 수 있음", "info")
+ if output_mode == "json":
+ print(json.dumps({
+ "error": "reporting_scope_missing",
+ "message": "OAuth 토큰에 Transaction Search 권한 없음",
+ "scope": token_resp.get("scope", ""),
+ "fix": "PayPal Dashboard 앱 Features 에서 Transaction search 체크 + Save"
+ }, ensure_ascii=False, indent=2))
+ else:
+ print("# 💰 PayPal 매출 분석\n")
+ print("> ❌ **Transaction Search 권한 없음** — PayPal Dashboard 에서 활성화 필요")
+ print()
+ print("**해결 단계:**")
+ print("1. https://developer.paypal.com/dashboard/applications")
+ print("2. 좌상단 Sandbox/Live 토글 확인 (현재 모드: `" + mode + "`)")
+ print("3. 본인 앱 클릭")
+ print("4. **Features** 섹션 → ☑ **Transaction search** 체크")
+ print("5. 페이지 하단 **Save Changes** 클릭 (필수!)")
+ print("6. 1~3분 대기 후 매출 대시보드 다시 새로고침")
+ sys.exit(2)
+
+ end = datetime.now(timezone.utc)
+ start = end - timedelta(days=lookback)
+ txs = _fetch_transactions(base, token, start, end, currency)
+ _log(f"총 {len(txs)}건 거래 수집", "ok")
+
+ if output_mode == "json":
+ print(json.dumps(_json_dump(txs, currency), ensure_ascii=False, indent=2))
+ else:
+ report = _summarize(txs, currency)
+ print(report)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/10_Wiki/Topics/_company/_agents/ceo/skills/README.md b/10_Wiki/Topics/_company/_agents/ceo/skills/README.md
new file mode 100644
index 00000000..82beb657
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/ceo/skills/README.md
@@ -0,0 +1,12 @@
+# 🧭 CEO 스킬
+
+_재사용 가능한 패턴 모음. memory.md는 모든 활동의 로그(append-only firehose),
+이 폴더는 **검증된 패턴만 골라낸 것**입니다. 각 `*.md` 파일은 다음 호출 시
+CEO의 system prompt에 자동 주입됩니다._
+
+## 어떻게 채우나요?
+- 텔레그램에서 `/skill` (직전 산출물 자동 승격)
+- VS Code 명령 팔레트: `Connect AI: 방금 산출물 → 스킬로 저장`
+- 직접 이 폴더에 `<주제>.md` 파일을 만들어도 됩니다 (`# 제목` + 본문)
+
+`README.md` 자체는 system prompt에 주입되지 않습니다.
diff --git a/10_Wiki/Topics/_company/_agents/designer/skills/README.md b/10_Wiki/Topics/_company/_agents/designer/skills/README.md
new file mode 100644
index 00000000..c910d7ed
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/designer/skills/README.md
@@ -0,0 +1,12 @@
+# 🎨 Designer 스킬
+
+_재사용 가능한 패턴 모음. memory.md는 모든 활동의 로그(append-only firehose),
+이 폴더는 **검증된 패턴만 골라낸 것**입니다. 각 `*.md` 파일은 다음 호출 시
+Designer의 system prompt에 자동 주입됩니다._
+
+## 어떻게 채우나요?
+- 텔레그램에서 `/skill` (직전 산출물 자동 승격)
+- VS Code 명령 팔레트: `Connect AI: 방금 산출물 → 스킬로 저장`
+- 직접 이 폴더에 `<주제>.md` 파일을 만들어도 됩니다 (`# 제목` + 본문)
+
+`README.md` 자체는 system prompt에 주입되지 않습니다.
diff --git a/10_Wiki/Topics/_company/_agents/developer/skills/README.md b/10_Wiki/Topics/_company/_agents/developer/skills/README.md
new file mode 100644
index 00000000..c5195d02
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/skills/README.md
@@ -0,0 +1,12 @@
+# 💻 코다리 스킬
+
+_재사용 가능한 패턴 모음. memory.md는 모든 활동의 로그(append-only firehose),
+이 폴더는 **검증된 패턴만 골라낸 것**입니다. 각 `*.md` 파일은 다음 호출 시
+코다리의 system prompt에 자동 주입됩니다._
+
+## 어떻게 채우나요?
+- 텔레그램에서 `/skill` (직전 산출물 자동 승격)
+- VS Code 명령 팔레트: `Connect AI: 방금 산출물 → 스킬로 저장`
+- 직접 이 폴더에 `<주제>.md` 파일을 만들어도 됩니다 (`# 제목` + 본문)
+
+`README.md` 자체는 system prompt에 주입되지 않습니다.
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/lint_test.md b/10_Wiki/Topics/_company/_agents/developer/tools/lint_test.md
new file mode 100644
index 00000000..d9fd8e99
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/lint_test.md
@@ -0,0 +1,27 @@
+
+# 🧪 lint_test — 자가 검증 + 결과 inject
+
+코다리가 코드를 만든 직후 호출 → 결과가 다음 LLM 컨텍스트로 inject → 실패 시 자동 재시도.
+
+## 동작
+1. `package.json` 의 `scripts.{typecheck, lint, test, build}` 자동 감지·실행
+2. scripts 없으면 직접:
+ - `.ts/.tsx` 있고 `tsconfig.json` 있으면 → `npx tsc --noEmit`
+ - `.py` 파일 있으면 → `python -m py_compile <각 파일>` (최대 30개)
+3. 마크다운 리포트 — 각 검사 통과/실패 + 실패 시 마지막 15줄
+
+## 설정
+- `PROJECT_PATH`: 비우면 web_init 마지막 결과
+- `STRICT`: `true` 면 첫 실패에서 중단. 기본 `false` (전부 시도)
+
+## 코다리 권장 흐름
+```
+1.
+2. python3 .../lint_test.py
+3. 결과를 다음 답변 컨텍스트로 자동 받음
+4. 실패면 그 에러 보고 자동 수정 시도
+```
+
+## 한계
+- `eslint --fix` 같은 자동 수정은 별도 — 도구가 단지 보고만 함
+- 단위 테스트 미통과 시 코드 수정 책임은 코다리에게
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/lint_test.py b/10_Wiki/Topics/_company/_agents/developer/tools/lint_test.py
new file mode 100644
index 00000000..96855768
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/lint_test.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+# version: lint_test_v1
+"""프로젝트 자가 검증 — 타입체크·테스트·린트 자동 실행 + 결과 요약.
+
+코다리가 코드를 만든 직후 이 도구를 호출하면:
+ 1. package.json 의 scripts 자동 감지 (test/lint/typecheck/build)
+ 2. 또는 .ts/.tsx 파일 있으면 npx tsc --noEmit
+ 3. .py 파일 있으면 python -m py_compile <각 파일>
+ 4. 결과 마크다운 리포트
+
+config:
+ PROJECT_PATH — 검증할 프로젝트 (비우면 web_init 마지막 결과)
+ STRICT — 'true' 면 첫 실패에서 멈춤. 기본 false (모두 시도)
+"""
+import os, sys, json, subprocess, glob
+
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+CONFIG = os.path.join(HERE, "lint_test.json")
+WEB_INIT_CFG = os.path.join(HERE, "web_init.json")
+
+
+def _log(msg, kind="info"):
+ prefix = {"info": "🧪", "ok": "✅", "warn": "⚠️ ", "err": "❌", "step": "▸"}.get(kind, "•")
+ print(f"{prefix} {msg}", file=sys.stderr, flush=True)
+
+
+def _load(p):
+ if not os.path.exists(p):
+ return {}
+ try:
+ with open(p, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ return {}
+
+
+def _run(cmd, cwd, timeout=180):
+ _log(f"$ {cmd}", "step")
+ try:
+ r = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=timeout)
+ return r.returncode, (r.stdout or "") + "\n" + (r.stderr or "")
+ except subprocess.TimeoutExpired:
+ return -1, f"⏱ Timeout ({timeout}s)"
+ except Exception as e:
+ return -2, str(e)
+
+
+def main():
+ cfg = _load(CONFIG)
+ init_cfg = _load(WEB_INIT_CFG)
+ project = (cfg.get("PROJECT_PATH") or "").strip()
+ if not project:
+ project = (init_cfg.get("LAST_PROJECT") or "").strip()
+ if not project:
+ _log("PROJECT_PATH 비어있고 web_init 기록도 없음", "err")
+ sys.exit(1)
+ project = os.path.expanduser(project)
+ if not os.path.isdir(project):
+ _log(f"폴더 없음: {project}", "err")
+ sys.exit(1)
+ strict = str(cfg.get("STRICT", "")).lower() in ("true", "1", "yes")
+ _log(f"검증 대상: {project}", "info")
+
+ results = [] # (label, code, output)
+
+ # 1) package.json scripts 자동 감지
+ pkg_path = os.path.join(project, "package.json")
+ if os.path.exists(pkg_path):
+ try:
+ with open(pkg_path, "r", encoding="utf-8") as f:
+ pkg = json.load(f)
+ scripts = pkg.get("scripts", {})
+ for key in ["typecheck", "lint", "test", "build"]:
+ if key in scripts:
+ code, out = _run(f"npm run {key}", cwd=project, timeout=300)
+ results.append((f"npm run {key}", code, out))
+ if strict and code != 0:
+ break
+ except Exception as e:
+ _log(f"package.json 파싱 실패: {e}", "warn")
+
+ # 2) scripts 없으면 직접 tsc/py_compile
+ if not results:
+ # TS/TSX
+ ts_files = glob.glob(os.path.join(project, "**/*.ts"), recursive=True) + \
+ glob.glob(os.path.join(project, "**/*.tsx"), recursive=True)
+ ts_files = [f for f in ts_files if "node_modules" not in f and "dist" not in f]
+ if ts_files:
+ tsconfig = os.path.join(project, "tsconfig.json")
+ if os.path.exists(tsconfig):
+ code, out = _run("npx tsc --noEmit", cwd=project, timeout=180)
+ results.append(("npx tsc --noEmit", code, out))
+ # Python
+ py_files = glob.glob(os.path.join(project, "**/*.py"), recursive=True)
+ py_files = [f for f in py_files if "venv" not in f and ".venv" not in f and "__pycache__" not in f]
+ if py_files:
+ errs = []
+ for pf in py_files[:30]: # 30개 cap
+ code, out = _run(f"python3 -m py_compile {json.dumps(pf)}", cwd=project, timeout=10)
+ if code != 0:
+ errs.append((pf, out.strip()[:120]))
+ if errs:
+ results.append((f"py_compile ({len(errs)}/{len(py_files)} 실패)", 1, "\n".join(f"{f}: {e}" for f, e in errs[:10])))
+ else:
+ results.append((f"py_compile {len(py_files)} files", 0, "All OK"))
+
+ # 결과 리포트
+ print()
+ print(f"# 🧪 검증 결과 — {os.path.basename(project)}")
+ print()
+ if not results:
+ print("⚠️ 실행할 검증 없음 (package.json scripts 없고 .ts/.py 파일도 없음)")
+ return
+ passed = sum(1 for _, c, _ in results if c == 0)
+ print(f"**{passed}/{len(results)} 통과**\n")
+ for label, code, out in results:
+ icon = "✅" if code == 0 else "❌"
+ print(f"## {icon} {label}")
+ if code == 0:
+ print(f"성공 (exit code 0)")
+ else:
+ print(f"실패 (exit code {code})")
+ print()
+ print("```")
+ for line in out.strip().split("\n")[-15:]:
+ print(line)
+ print("```")
+ print()
+ if passed == len(results):
+ print("> 🎉 모든 검증 통과. 안전하게 다음 단계로.")
+ else:
+ print(f"> ⚠️ {len(results) - passed}개 실패 — 위 출력 보고 수정 필요.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/pack_apply.md b/10_Wiki/Topics/_company/_agents/developer/tools/pack_apply.md
new file mode 100644
index 00000000..46e9ad36
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/pack_apply.md
@@ -0,0 +1,69 @@
+
+# 📋 pack_apply — 키트 한 명령으로 적용
+
+두뇌에 주입된 템플릿 팩을 사용자 프로젝트에 자동 적용. 파일 복사 + 의존성 설치 + App.tsx 자동 업데이트.
+
+## 사용
+설정 (pack_apply.json):
+- `KIT_NAME`: 'landing-kit' / 'portfolio-kit' / 'dashboard-kit' / 'mobile-kit'
+- `PROJECT_PATH`: 적용할 사용자 프로젝트 (비우면 web_init 결과 자동)
+
+실행:
+```
+python3 pack_apply.py
+```
+
+## 동작 (3단계)
+
+1. **파일 복사**: 키트의 `files/` 폴더를 manifest의 `apply.copy_to` 경로로 (예: `src/components/`)
+2. **의존성 자동 설치**: manifest의 `apply.post_install` 명령 순차 실행
+ - 예: `npm install lucide-react`
+ - Expo: `npx expo install @react-navigation/native ...`
+3. **App.tsx 자동 업데이트**: manifest의 `apply.app_imports` + `app_body` 로 import + JSX 본문 추가
+
+## 키트별 동작
+
+### landing-kit (vite-react)
+- 복사: 6개 컴포넌트 → src/components/
+- 설치: lucide-react
+- App.tsx: Hero·Features·Pricing·FAQ·CTA·Footer 자동 배치
+
+### portfolio-kit (vite-react)
+- 복사: 5개 컴포넌트
+- 설치: lucide-react
+- App.tsx: Nav·About·Work·Skills·Contact 자동 배치
+
+### dashboard-kit (vite-react)
+- 복사: 5개 컴포넌트
+- 설치: lucide-react
+- App.tsx: `` 한 줄로 풀스크린 대시보드
+
+### mobile-kit (Expo)
+- 복사: App.tsx + screens/ 3개
+- 설치: @react-navigation/native + bottom-tabs + screens + safe-area-context
+- App.tsx: 기존 덮어쓰기 (Bottom Tab Navigator)
+
+## 코다리 사용 예시
+
+```
+사용자: "다이어트 SaaS 랜딩 만들어줘"
+
+코다리:
+1. web_init (TEMPLATE=vite-react, PROJECT_NAME=diet-saas)
+2. pack_apply (KIT_NAME=landing-kit) ← 새 도구
+3. edit_file 로 텍스트만 다이어트 SaaS 카피로 교체
+4. web_preview (자동 dev server + 브라우저)
+
+→ 5분 안에 완성 + 모바일·데스크탑 반응형 + Tailwind 컨벤션 일관
+```
+
+## 안전장치
+- `KIT_NAME` 없거나 잘못되면 종료 + 사용 가능 키트 안내
+- 두뇌 폴더 자동 탐색 (~/.connect-ai-brain 또는 fallback 경로)
+- 파일 복사는 덮어쓰기 (사용자가 수정한 거 있으면 백업 권장)
+- 의존성 설치 실패해도 계속 진행 (warn만, 사용자 수동 가능)
+- App.tsx 패턴 매칭 실패 시 수동 안내
+
+## 한계
+- App.tsx 자동 업데이트는 best-effort (단순 패턴 매칭). 복잡한 기존 App.tsx 는 수동 권장.
+- 키트가 React 외 (Vue·Svelte 등)에 적용되면 App.tsx 패턴 안 맞음 — 키트가 진짜 React 인지 manifest.base 로 검증 필요.
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/pack_apply.py b/10_Wiki/Topics/_company/_agents/developer/tools/pack_apply.py
new file mode 100644
index 00000000..72a24873
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/pack_apply.py
@@ -0,0 +1,485 @@
+#!/usr/bin/env python3
+# version: pack_apply_v7
+"""두뇌의 템플릿 팩을 사용자 프로젝트에 한 번에 적용.
+
+흐름:
+ 1. KIT_NAME — 두뇌의 40_템플릿/developer// 폴더
+ 2. PROJECT_PATH — 적용할 사용자 프로젝트 (비우면 web_init 결과 자동)
+ 3. manifest.json 의 apply.{copy_to, post_install, app_imports, app_body} 사용:
+ - files/* → PROJECT_PATH/copy_to/ (예: src/components/)
+ - post_install: npm install / npx expo install 자동 실행
+ - app_imports: App.tsx 또는 App.tsx 에 import 추가 + JSX 본문 자동
+ 4. 결과 출력 — 다음 단계 안내 (npm run dev 등)
+
+이 도구가 코다리에게 주는 슈퍼파워:
+ - 매뉴얼 cp + npm install 호출 안 해도 됨
+ - 한 명령으로 "키트 적용 완료"
+ - 의존성 누락 없음 (manifest 가 진실 소스)
+"""
+import os, sys, json, subprocess, shutil
+
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+CONFIG = os.path.join(HERE, "pack_apply.json")
+WEB_INIT_CFG = os.path.join(HERE, "web_init.json")
+
+
+def _log(msg, kind="info"):
+ prefix = {"info": "📋", "ok": "✅", "warn": "⚠️ ", "err": "❌", "step": "▸"}.get(kind, "•")
+ print(f"{prefix} {msg}", file=sys.stderr, flush=True)
+
+
+def _load(p):
+ if not os.path.exists(p):
+ return {}
+ try:
+ with open(p, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ return {}
+
+
+def _run(cmd, cwd):
+ _log(f"$ {cmd}", "step")
+ r = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=600)
+ if r.returncode != 0:
+ for line in (r.stderr or "").splitlines()[-8:]:
+ _log(line, "warn")
+ return False
+ return True
+
+
+def _load_operator_credentials(brain_root):
+ """v7: 운영자(1인 기업)의 자격증명을 두뇌에서 로드. pack_apply 가 키트 HTML/JS
+ 의 placeholder 를 운영자 키로 자동 교체.
+ 지원 placeholder:
+ __GEMINI_API_KEY__ → Gemini API 키
+ __GEMINI_TEXT_MODEL__ → 텍스트 모델명
+ __GEMINI_IMAGE_MODEL__ → 이미지 모델명
+ __PAYPAL_CLIENT_ID__ → PayPal Live/Sandbox Client ID
+ 자격증명은 외부 연결 패널 (Connect AI) 에서 입력. 키트 사용자(고객) 는
+ 이 키를 볼 일이 없음 — 운영자가 빌드 시점에 박힘. """
+ creds = {
+ "__GEMINI_API_KEY__": "",
+ "__GEMINI_TEXT_MODEL__": "gemini-3.1-flash-lite-preview",
+ "__GEMINI_IMAGE_MODEL__": "gemini-3.1-flash-image-preview",
+ "__PAYPAL_CLIENT_ID__": "",
+ }
+ business_tools = os.path.join(brain_root, "_company", "_agents", "business", "tools")
+ # Gemini
+ try:
+ gp = os.path.join(business_tools, "gemini_account.json")
+ if os.path.exists(gp):
+ with open(gp, "r", encoding="utf-8") as f:
+ g = json.load(f)
+ if g.get("API_KEY"): creds["__GEMINI_API_KEY__"] = g["API_KEY"]
+ if g.get("TEXT_MODEL"): creds["__GEMINI_TEXT_MODEL__"] = g["TEXT_MODEL"]
+ if g.get("IMAGE_MODEL"): creds["__GEMINI_IMAGE_MODEL__"] = g["IMAGE_MODEL"]
+ except Exception:
+ pass
+ # PayPal
+ try:
+ pp = os.path.join(business_tools, "paypal_revenue.json")
+ if os.path.exists(pp):
+ with open(pp, "r", encoding="utf-8") as f:
+ p = json.load(f)
+ if p.get("CLIENT_ID"): creds["__PAYPAL_CLIENT_ID__"] = p["CLIENT_ID"]
+ except Exception:
+ pass
+ return creds
+
+
+def _inject_credentials(file_path, creds):
+ """v7: 텍스트 파일 안의 placeholder 를 운영자 자격증명으로 교체.
+ 바이너리·이미지 파일은 skip. UTF-8 못 읽으면 skip. """
+ try:
+ with open(file_path, "r", encoding="utf-8") as f:
+ content = f.read()
+ except (UnicodeDecodeError, IsADirectoryError):
+ return False
+ except Exception:
+ return False
+ replaced = False
+ for placeholder, value in creds.items():
+ if placeholder in content and value:
+ content = content.replace(placeholder, value)
+ replaced = True
+ if replaced:
+ try:
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(content)
+ return True
+ except Exception:
+ return False
+ return False
+
+
+def _copy_tree(src_dir, dst_dir, creds=None):
+ """v2: 기존 파일이 있으면 .backup 자동 생성 (사용자 코드 보호).
+ 백업이 이미 있으면 덮어쓰지 않음 (멱등성).
+ v7: creds 가 주어지면 복사 후 각 파일에서 placeholder 교체.
+ v7.1: 자격증명 누락 placeholder 가 남으면 경고 (운영자 입력 유도)."""
+ os.makedirs(dst_dir, exist_ok=True)
+ copied = 0
+ backed_up = []
+ injected = 0
+ missing_placeholders = {} # placeholder -> count
+ for root, _dirs, files in os.walk(src_dir):
+ rel = os.path.relpath(root, src_dir)
+ target = os.path.join(dst_dir, rel) if rel != "." else dst_dir
+ os.makedirs(target, exist_ok=True)
+ for f in files:
+ dst_path = os.path.join(target, f)
+ if os.path.exists(dst_path):
+ bk = dst_path + ".backup"
+ if not os.path.exists(bk):
+ try:
+ shutil.copy2(dst_path, bk)
+ backed_up.append(os.path.relpath(dst_path, dst_dir))
+ except Exception:
+ pass
+ shutil.copy2(os.path.join(root, f), dst_path)
+ copied += 1
+ # v7: 자격증명 placeholder 자동 inline
+ if creds and any(creds.values()):
+ if _inject_credentials(dst_path, creds):
+ injected += 1
+ # v7.1: 남은 placeholder 스캔 (빈 자격증명 감지)
+ if creds:
+ try:
+ with open(dst_path, "r", encoding="utf-8") as fh:
+ body = fh.read()
+ for ph, val in creds.items():
+ if not val and ph in body:
+ missing_placeholders[ph] = missing_placeholders.get(ph, 0) + 1
+ except Exception:
+ pass
+ if backed_up:
+ _log(f"기존 파일 {len(backed_up)}개 .backup 보존: {', '.join(backed_up[:3])}{' …' if len(backed_up) > 3 else ''}", "info")
+ if injected:
+ _log(f"🔐 운영자 자격증명 {injected}개 파일에 자동 inline (Gemini/PayPal placeholder 교체)", "ok")
+ if missing_placeholders:
+ guide = {
+ "__GEMINI_API_KEY__": "Connect AI → 외부 연결 → ✨ Google Gemini → API Key 입력",
+ "__PAYPAL_CLIENT_ID__": "Connect AI → 외부 연결 → 💰 PayPal → Client ID 입력",
+ }
+ _log("⚠️ 운영자 자격증명 누락 — 키트는 복사됐지만 실제 호출은 안 됨:", "warn")
+ for ph in sorted(missing_placeholders):
+ _log(f" • {ph} → {guide.get(ph, '외부 연결 패널에서 입력 필요')}", "warn")
+ _log(" ↳ 키 입력 후 키트 다시 적용하면 자동 inline 됩니다.", "warn")
+ return copied
+
+
+def _find_app_file(project_path):
+ """vite/next 모두 커버. src/App.tsx 우선, 없으면 App.tsx (expo)."""
+ for cand in ["src/App.tsx", "App.tsx", "src/app/page.tsx", "app/page.tsx"]:
+ p = os.path.join(project_path, cand)
+ if os.path.exists(p):
+ return p
+ return None
+
+
+def _update_app_tsx(app_path, imports, body):
+ """App.tsx 를 깨끗하게 새로 작성. 원본은 .backup 으로 보존.
+ v2: regex 부분 매칭으로 옛 JSX 가 남던 사고 → 전체 덮어쓰기 + 백업 방식으로 변경."""
+ try:
+ with open(app_path, "r", encoding="utf-8") as f:
+ original = f.read()
+ except Exception:
+ return False
+
+ # 이미 키트 적용됐으면 skip
+ if all(f"from './components/{n}'" in original for n in imports):
+ return False
+
+ # 백업 — 사용자가 손댄 거 잃지 않게
+ try:
+ backup_path = app_path + ".backup"
+ if not os.path.exists(backup_path):
+ with open(backup_path, "w", encoding="utf-8") as f:
+ f.write(original)
+ except Exception:
+ pass
+
+ # 새 App.tsx — 깨끗한 최소 버전
+ import_lines = "\n".join([f"import {n} from './components/{n}'" for n in imports])
+ new_content = f"""{import_lines}
+
+export default function App() {{
+ return (
+
+ {body}
+
+ );
+}}
+"""
+ try:
+ with open(app_path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ return True
+ except Exception:
+ return False
+
+
+def _find_brain_root():
+ """두뇌 폴더 자동 탐색 (한국어 폴더명 포함).
+
+ v4: BRAIN_ROOT 환경변수가 가장 강함 (Connect AI 익스텐션이 직접 지정).
+ 이전엔 ~/.connect-ai-brain 가 빈 폴더로 존재만 해도 우선 매칭돼서
+ 실제 사용자 두뇌(~/Downloads/지식메모리) 의 키트를 못 찾던 사고 차단.
+ """
+ env = os.environ.get("BRAIN_ROOT", "").strip()
+ if env:
+ ep = os.path.expanduser(env)
+ if os.path.exists(ep):
+ return ep
+ cands = [
+ os.path.expanduser("~/Downloads/지식메모리"),
+ os.path.expanduser("~/.connect-ai-brain"),
+ os.path.expanduser("~/.connect-ai-brain-imported"),
+ ]
+ for c in cands:
+ if os.path.exists(c):
+ return c
+ return cands[0] # 첫 번째 fallback
+
+
+def _list_kits(brain_root):
+ """developer 카테고리의 모든 키트와 manifest 반환."""
+ tdir = os.path.join(brain_root, "40_템플릿", "developer")
+ if not os.path.exists(tdir):
+ return []
+ kits = []
+ for name in os.listdir(tdir):
+ d = os.path.join(tdir, name)
+ if not os.path.isdir(d):
+ continue
+ mp = os.path.join(d, "manifest.json")
+ if not os.path.exists(mp):
+ continue
+ try:
+ with open(mp, "r", encoding="utf-8") as f:
+ manifest = json.load(f)
+ kits.append({"name": name, "manifest": manifest})
+ except Exception:
+ pass
+ return kits
+
+
+def _score_kit(manifest, intent_text):
+ """매니페스트 vs 사용자 의도(intent_text) 매칭 점수.
+ keywords + name + description 단어 매칭. 한국어·영어 모두."""
+ if not intent_text:
+ return 0
+ haystack = " ".join([
+ manifest.get("name", ""),
+ manifest.get("description", ""),
+ " ".join(manifest.get("keywords") or []),
+ manifest.get("category", ""),
+ ]).lower()
+ intent_lc = intent_text.lower()
+ score = 0
+ # keywords 직접 매칭 (높은 가중치)
+ for kw in (manifest.get("keywords") or []):
+ if kw.lower() in intent_lc:
+ score += 10
+ # name 자체가 의도에 있으면 (예: "landing-kit" → "landing")
+ for token in manifest.get("name", "").split():
+ if len(token) >= 3 and token.lower() in intent_lc:
+ score += 5
+ # 카테고리
+ if (manifest.get("category", "").lower() or "") in intent_lc:
+ score += 3
+ return score
+
+
+def _autodetect_kit(brain_root, intent_text):
+ """사용자 의도에서 가장 적합한 키트 자동 추론. (kit_name, score, alternatives) 반환."""
+ kits = _list_kits(brain_root)
+ if not kits:
+ return None, 0, []
+ scored = [(k["name"], _score_kit(k["manifest"], intent_text), k["manifest"].get("description", "")) for k in kits]
+ scored.sort(key=lambda x: -x[1])
+ if scored[0][1] == 0:
+ # 매치 0 — fallback: 가장 일반적인 landing-kit
+ for k in kits:
+ if k["name"] == "landing-kit":
+ return "landing-kit", 0, scored[:3]
+ return kits[0]["name"], 0, scored[:3]
+ return scored[0][0], scored[0][1], scored[:3]
+
+
+def _parse_cli_args():
+ """v4: 로컬 LLM 이 CLI 인자로 호출하는 패턴도 지원.
+ `--kit landing-kit --user-intent "..." --project /path` 또는
+ 환경변수 KIT_NAME / USER_INTENT / PROJECT_PATH."""
+ out = {}
+ args = sys.argv[1:]
+ i = 0
+ aliases = {
+ "--kit": "KIT_NAME", "--kit-name": "KIT_NAME",
+ "--user-intent": "USER_INTENT", "--intent": "USER_INTENT",
+ "--project": "PROJECT_PATH", "--project-path": "PROJECT_PATH",
+ "--brain-root": "BRAIN_ROOT", "--brain": "BRAIN_ROOT",
+ }
+ while i < len(args):
+ a = args[i]
+ if a in aliases and i + 1 < len(args):
+ out[aliases[a]] = args[i + 1]
+ i += 2
+ elif "=" in a and a.startswith("--"):
+ k, v = a[2:].split("=", 1)
+ key = aliases.get("--" + k)
+ if key:
+ out[key] = v
+ i += 1
+ else:
+ i += 1
+ for k in ("KIT_NAME", "USER_INTENT", "PROJECT_PATH", "BRAIN_ROOT"):
+ if k in os.environ and os.environ[k].strip():
+ out.setdefault(k, os.environ[k])
+ return out
+
+
+def main():
+ cfg = _load(CONFIG)
+ init_cfg = _load(WEB_INIT_CFG)
+
+ cli = _parse_cli_args()
+ for k, v in cli.items():
+ if v and str(v).strip():
+ cfg[k] = v
+
+ kit_name = (cfg.get("KIT_NAME") or "").strip()
+ user_intent = (cfg.get("USER_INTENT") or "").strip()
+
+ # v5: CLI --brain-root 가 있으면 env 처럼 작동시켜 _find_brain_root 우선순위 활용
+ cli_brain = cli.get("BRAIN_ROOT", "").strip() if cli else ""
+ if cli_brain:
+ os.environ["BRAIN_ROOT"] = cli_brain
+ # 두뇌 폴더 찾기 (자동 추론에도 필요)
+ brain_root = _find_brain_root()
+
+ # v3: KIT_NAME 비어있고 USER_INTENT 있으면 자동 매칭
+ selection_card = ""
+ if not kit_name and user_intent:
+ detected, score, alts = _autodetect_kit(brain_root, user_intent)
+ if detected:
+ kit_name = detected
+ _log(f"자동 추론 → '{kit_name}' (매칭 점수 {score})", "info")
+ if score == 0:
+ _log(" ⚠️ 사용자 의도와 명확한 매칭 없음. 가장 일반적인 키트로 fallback.", "warn")
+ # 시각 카드 (stdout에 마크다운 — 채팅창에 렌더링됨)
+ card_lines = [
+ "",
+ "## 🎯 키트 자동 선택",
+ "",
+ f"> 사용자 의도: _\"{user_intent}\"_",
+ "",
+ "| 순위 | 키트 | 매칭 점수 | 비고 |",
+ "|---|---|---|---|",
+ ]
+ for i, (n, s, desc) in enumerate(alts):
+ marker = "**⭐ 선택**" if n == kit_name else ""
+ d_short = (desc[:50] + "…") if len(desc) > 50 else desc
+ card_lines.append(f"| {i+1} | `{n}` | **{s}** | {marker} {d_short} |")
+ if score == 0:
+ card_lines.append("")
+ card_lines.append("⚠️ _명확한 매칭 없음 — fallback으로 가장 일반적인 키트 선택._")
+ card_lines.append("")
+ card_lines.append("> 💡 다른 키트로 바꾸려면 `pack_apply` 를 `KIT_NAME=<원하는 키트>` 로 다시 실행.")
+ card_lines.append("")
+ selection_card = "\n".join(card_lines)
+
+ if not kit_name:
+ kits = _list_kits(brain_root)
+ avail = ", ".join([f"'{k['name']}'" for k in kits]) or "(두뇌에 키트 없음 — EZER 에서 먼저 주입)"
+ _log(f"KIT_NAME 비어있고 USER_INTENT 도 없음.", "err")
+ _log(f" 방법 1: KIT_NAME 명시 → {avail}", "info")
+ _log(f" 방법 2: USER_INTENT 에 '다이어트 SaaS 랜딩' 같은 자연어 입력 → 자동 추론", "info")
+ sys.exit(1)
+
+ project = (cfg.get("PROJECT_PATH") or "").strip()
+ if not project:
+ project = (init_cfg.get("LAST_PROJECT") or "").strip()
+ if not project:
+ _log("PROJECT_PATH 비어있고 web_init 기록도 없음", "err")
+ sys.exit(1)
+ project = os.path.expanduser(project)
+ if not os.path.isdir(project):
+ _log(f"프로젝트 폴더 없음: {project}", "err")
+ sys.exit(1)
+
+ kit_dir = os.path.join(brain_root, "40_템플릿", "developer", kit_name)
+ if not os.path.exists(kit_dir):
+ _log(f"키트 없음: {kit_dir}", "err")
+ _log(f"먼저 EZER Pack Vault 에서 '{kit_name}' 주입하세요.", "info")
+ sys.exit(1)
+
+ manifest_path = os.path.join(kit_dir, "manifest.json")
+ if not os.path.exists(manifest_path):
+ _log(f"manifest 없음: {manifest_path}", "err")
+ sys.exit(1)
+ with open(manifest_path, "r", encoding="utf-8") as f:
+ manifest = json.load(f)
+
+ apply = manifest.get("apply", {})
+ copy_to = apply.get("copy_to", "src/components/")
+ post_install = apply.get("post_install", [])
+ app_imports = apply.get("app_imports", [])
+ app_body = apply.get("app_body", "")
+
+ _log(f"키트: {manifest.get('name', kit_name)} → {project}", "info")
+ _log(f"기반: {manifest.get('base', '?')}", "info")
+
+ # v7: 운영자 자격증명 로드 (Gemini/PayPal placeholder 자동 inline)
+ creds = _load_operator_credentials(brain_root)
+
+ # 1) 파일 복사 (+ placeholder 교체)
+ src_files = os.path.join(kit_dir, "files")
+ dst_files = os.path.join(project, copy_to.lstrip("./"))
+ if not os.path.exists(src_files):
+ _log("키트의 files/ 폴더 없음 — 파일 복사 스킵", "warn")
+ else:
+ n = _copy_tree(src_files, dst_files, creds=creds)
+ _log(f"{n}개 파일 복사 → {dst_files}", "ok")
+
+ # 2) 의존성 자동 설치
+ if post_install:
+ _log(f"의존성 {len(post_install)}개 설치 중...", "info")
+ for cmd in post_install:
+ ok = _run(cmd, cwd=project)
+ if not ok:
+ _log(f"부가 명령 실패: {cmd} — 계속 진행", "warn")
+
+ # 3) App.tsx 자동 업데이트 (best-effort)
+ if app_imports:
+ app_file = _find_app_file(project)
+ if app_file:
+ changed = _update_app_tsx(app_file, app_imports,
+ app_body or "\n".join([f"<{n} />" for n in app_imports]))
+ if changed:
+ _log(f"App.tsx 자동 업데이트: {app_file}", "ok")
+ else:
+ _log(f"App.tsx 이미 정정됨 또는 패턴 매칭 실패 — 수동 확인: {app_file}", "warn")
+ else:
+ _log("App.tsx 못 찾음 — 수동으로 import + JSX 추가 필요", "warn")
+
+ # 결과 — stdout 으로 마크다운 (채팅창 렌더링)
+ if selection_card:
+ print(selection_card)
+ print()
+ print(f"## ✅ 적용 완료: `{manifest.get('name', kit_name)}`")
+ print()
+ print(f"- **위치**: `{project}`")
+ print(f"- **기반**: {manifest.get('base', '?')}")
+ if "expo" in (manifest.get("base", "").lower()):
+ print(f"- **실행**: `cd {project} && npm start` → 폰에 Expo Go 깔고 QR 스캔")
+ else:
+ print(f"- **실행**: `cd {project} && npm run dev` → http://localhost:5173")
+ print()
+ _log(f"적용 완료: {kit_name}", "ok")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/pwa_setup.md b/10_Wiki/Topics/_company/_agents/developer/tools/pwa_setup.md
new file mode 100644
index 00000000..156d93ef
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/pwa_setup.md
@@ -0,0 +1,37 @@
+
+# 💻 PWA 자동 셋업 — 웹사이트 → 모바일 앱처럼
+
+기존 웹 프로젝트를 PWA(Progressive Web App)로 변환. 사용자가 폰에서 "홈 화면에 추가" 누르면 풀스크린 앱처럼 작동.
+
+## 자동 생성 파일
+- `public/manifest.json` — 앱 메타 (이름·아이콘·테마색)
+- `public/icon-192.svg` + `icon-512.svg` — 이모지 기반 라운드 아이콘
+- `public/sw.js` — 서비스 워커 (오프라인 캐싱)
+- `index.html`에 자동 주입: meta·link·script
+
+## 설정
+- `PROJECT_PATH`: 비우면 web_init이 마지막에 만든 프로젝트 자동 사용
+- `APP_NAME`: 앱 이름 (홈화면 라벨)
+- `APP_SHORT_NAME`: 12자 이하 짧은 이름
+- `THEME_COLOR`: 상단 바 색 (예: `#667eea`)
+- `BACKGROUND_COLOR`: 스플래시 배경 (예: `#ffffff`)
+- `ICON_EMOJI`: 아이콘에 쓸 이모지 (예: `📚`)
+
+## 사용 흐름
+```
+1. web_init으로 사이트 만듦 (vite-react·astro 등)
+2. pwa_setup 실행 → manifest·아이콘·sw 생성
+3. 배포 (Vercel·Netlify) 또는 로컬 dev server
+4. 폰 브라우저로 URL 접속
+5. iOS Safari: 공유 → 홈 화면에 추가
+ Android Chrome: ⋮ → 홈 화면에 추가
+6. 홈 화면 아이콘 클릭 → 풀스크린 앱
+```
+
+## Next.js 사용자
+Next.js 13+ App Router 는 `app/layout.tsx`의 `export const metadata` 에 PWA 정보를 넣어야 함. 도구가 자동 감지하면 안내 메시지 표시.
+
+## 한계
+- 진짜 네이티브 기능 (푸시 알림·블루투스·카메라) 은 PWA로 부분 지원
+- 복잡한 모바일 앱은 Expo 권장
+- 아이콘은 SVG로 생성 (PNG 변환 필요시 ImageMagick 또는 사용자 디자인)
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/pwa_setup.py b/10_Wiki/Topics/_company/_agents/developer/tools/pwa_setup.py
new file mode 100644
index 00000000..bb769877
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/pwa_setup.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+# version: pwa_setup_v1
+"""웹사이트를 PWA(모바일 앱처럼)로 변환.
+
+config:
+ PROJECT_PATH — 대상 폴더 (web_init 결과 자동 사용)
+ APP_NAME — 앱 이름 (홈화면에 표시)
+ APP_SHORT_NAME — 짧은 이름 (12자 이하)
+ THEME_COLOR — 상단 바 색 (예: #667eea)
+ BACKGROUND_COLOR — 스플래시 배경
+ ICON_EMOJI — 아이콘 자동 생성에 쓸 이모지 (예: 📚)
+
+생성 파일:
+ public/manifest.json — PWA 메타
+ public/sw.js — 서비스 워커 (오프라인)
+ public/icon-192.png + icon-512.png — 자동 생성 (이모지 기반)
+ index.html (또는 public/index.html) — meta·link·script 자동 주입
+
+설치 방법 (사용자):
+ 사파리·크롬에서 사이트 접속 → "홈 화면에 추가" → 풀스크린 앱 작동
+"""
+import os, sys, json, base64, re
+
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+CONFIG = os.path.join(HERE, "pwa_setup.json")
+WEB_INIT_CONFIG = os.path.join(HERE, "web_init.json")
+
+
+def _log(msg, kind="info"):
+ prefix = {"info": "💻", "ok": "✅", "warn": "⚠️ ", "err": "❌", "step": "▸"}.get(kind, "•")
+ print(f"{prefix} {msg}", file=sys.stderr, flush=True)
+
+
+def _load(p):
+ if os.path.exists(p):
+ try:
+ with open(p, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ pass
+ return {}
+
+
+def _generate_icon_svg(emoji, bg_color, size=512):
+ """이모지 기반 SVG 아이콘 생성 (라운드 코너 + 그라데이션)."""
+ return f'''
+
+'''
+
+
+def _find_html(project_path):
+ """프로젝트의 메인 HTML 찾기 (index.html, public/index.html, app/layout.tsx 등)."""
+ candidates = [
+ os.path.join(project_path, "index.html"),
+ os.path.join(project_path, "public", "index.html"),
+ os.path.join(project_path, "public", "manifest.json"), # 이미 있으면 표시
+ ]
+ for c in candidates:
+ if os.path.exists(c):
+ return c
+ return None
+
+
+def _find_public_dir(project_path):
+ """public 디렉토리 찾기 또는 만들기."""
+ public = os.path.join(project_path, "public")
+ if os.path.exists(public):
+ return public
+ # Vite는 public/, Next.js도 public/
+ os.makedirs(public, exist_ok=True)
+ return public
+
+
+def main():
+ cfg = _load(CONFIG)
+ init_cfg = _load(WEB_INIT_CONFIG)
+
+ project_path = (cfg.get("PROJECT_PATH") or "").strip()
+ if not project_path:
+ project_path = (init_cfg.get("LAST_PROJECT") or "").strip()
+ if not project_path:
+ _log("PROJECT_PATH가 비어있고 web_init 기록도 없음", "err")
+ sys.exit(1)
+
+ project_path = os.path.expanduser(project_path)
+ if not os.path.isdir(project_path):
+ _log(f"폴더 없음: {project_path}", "err")
+ sys.exit(1)
+
+ app_name = (cfg.get("APP_NAME") or "").strip() or os.path.basename(project_path)
+ short_name = (cfg.get("APP_SHORT_NAME") or "").strip() or app_name[:12]
+ theme = (cfg.get("THEME_COLOR") or "").strip() or "#667eea"
+ bg = (cfg.get("BACKGROUND_COLOR") or "").strip() or "#ffffff"
+ icon_emoji = (cfg.get("ICON_EMOJI") or "").strip() or "✦"
+
+ _log(f"PWA 셋업 시작 → {project_path}", "info")
+
+ public = _find_public_dir(project_path)
+
+ # 1. manifest.json
+ manifest = {
+ "name": app_name,
+ "short_name": short_name,
+ "description": f"{app_name} — Connect AI로 만들어진 PWA",
+ "start_url": "/",
+ "display": "standalone",
+ "orientation": "portrait",
+ "theme_color": theme,
+ "background_color": bg,
+ "icons": [
+ {"src": "/icon-192.svg", "sizes": "192x192", "type": "image/svg+xml", "purpose": "any maskable"},
+ {"src": "/icon-512.svg", "sizes": "512x512", "type": "image/svg+xml", "purpose": "any maskable"},
+ ],
+ }
+ manifest_path = os.path.join(public, "manifest.json")
+ with open(manifest_path, "w", encoding="utf-8") as f:
+ json.dump(manifest, f, indent=2, ensure_ascii=False)
+ _log(f"manifest.json 생성: {manifest_path}", "ok")
+
+ # 2. 아이콘 (SVG로 — 모든 기기에서 잘 보임 + 작은 사이즈)
+ for size in (192, 512):
+ icon_path = os.path.join(public, f"icon-{size}.svg")
+ with open(icon_path, "w", encoding="utf-8") as f:
+ f.write(_generate_icon_svg(icon_emoji, theme, size))
+ _log(f"icon-{size}.svg 생성", "ok")
+
+ # 3. service worker
+ sw_path = os.path.join(public, "sw.js")
+ sw_content = f'''// Connect AI PWA Service Worker
+// version: pwa_v1 — auto-generated
+const CACHE = "{short_name}-v1";
+const ASSETS = ["/", "/manifest.json", "/icon-192.svg", "/icon-512.svg"];
+
+self.addEventListener("install", e => {{
+ e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS).catch(()=>{{/* offline OK */}})));
+ self.skipWaiting();
+}});
+self.addEventListener("activate", e => {{
+ e.waitUntil(caches.keys().then(keys =>
+ Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
+ ));
+ self.clients.claim();
+}});
+self.addEventListener("fetch", e => {{
+ const req = e.request;
+ if (req.method !== "GET") return;
+ e.respondWith(
+ caches.match(req).then(hit => hit || fetch(req).then(res => {{
+ const copy = res.clone();
+ caches.open(CACHE).then(c => c.put(req, copy)).catch(()=>{{/* ignore */}});
+ return res;
+ }}).catch(() => caches.match("/") || new Response("offline", {{ status: 503 }})))
+ );
+}});
+'''
+ with open(sw_path, "w", encoding="utf-8") as f:
+ f.write(sw_content)
+ _log(f"sw.js 생성: {sw_path}", "ok")
+
+ # 4. HTML에 meta + link + script 주입
+ # 후보: index.html, public/index.html, app/layout.tsx (Next.js)
+ html_candidates = [
+ os.path.join(project_path, "index.html"),
+ os.path.join(project_path, "public", "index.html"),
+ ]
+ html_file = None
+ for c in html_candidates:
+ if os.path.exists(c):
+ html_file = c
+ break
+
+ pwa_head = f'''
+
+
+
+
+
+
+
+ '''
+ pwa_script = '''
+ '''
+
+ if html_file:
+ with open(html_file, "r", encoding="utf-8") as f:
+ html = f.read()
+ if "manifest.json" in html:
+ _log(f"HTML 이미 PWA 메타 있음. 스킵: {html_file}", "warn")
+ else:
+ # 직전에 head 삽입
+ new_html = re.sub(r"", pwa_head + "\n ", html, count=1, flags=re.IGNORECASE)
+ #
+
+ {name}
+ Connect AI · 코다리가 만든 사이트
+
+
+ 여기에 콘텐츠를 추가하세요.
+
+
+ 직전에 script 삽입
+ new_html = re.sub(r"", pwa_script + "\n ", new_html, count=1, flags=re.IGNORECASE)
+ with open(html_file, "w", encoding="utf-8") as f:
+ f.write(new_html)
+ _log(f"HTML 메타·script 주입: {html_file}", "ok")
+ else:
+ # Next.js app/layout.tsx 안내만
+ next_layout = os.path.join(project_path, "src", "app", "layout.tsx")
+ next_layout_alt = os.path.join(project_path, "app", "layout.tsx")
+ layout = next_layout if os.path.exists(next_layout) else (next_layout_alt if os.path.exists(next_layout_alt) else None)
+ if layout:
+ _log(f"Next.js 감지 — {layout} 의 metadata에 manifest 추가하세요", "warn")
+ _log(f" export const metadata = {{ ... manifest: '/manifest.json' }}", "info")
+ else:
+ _log("HTML 파일을 찾지 못함. PWA 메타·script 수동 추가 필요.", "warn")
+ _log(f"head: {pwa_head.strip()}", "info")
+ _log(f"body: {pwa_script.strip()}", "info")
+
+ # 결과 저장
+ cfg["LAST_PROJECT"] = project_path
+ cfg["LAST_APP_NAME"] = app_name
+ cfg["LAST_THEME"] = theme
+ with open(CONFIG, "w", encoding="utf-8") as f:
+ json.dump(cfg, f, indent=2, ensure_ascii=False)
+
+ print()
+ _log(f"PWA 셋업 완료: {app_name}", "ok")
+ _log("테스트:", "info")
+ _log(" 1. dev server 또는 배포된 URL을 모바일 브라우저로 열기", "info")
+ _log(" 2. iOS Safari: 공유 → 홈 화면에 추가", "info")
+ _log(" 3. Android Chrome: 우측 ⋮ → 홈 화면에 추가", "info")
+ _log(" 4. 풀스크린·아이콘·오프라인 작동 확인", "info")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/web_init.md b/10_Wiki/Topics/_company/_agents/developer/tools/web_init.md
new file mode 100644
index 00000000..deb88a2d
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/web_init.md
@@ -0,0 +1,42 @@
+
+# 💻 웹·모바일 프로젝트 자동 시작
+
+5개 템플릿 중 골라서 한 번에 프로젝트 폴더 + 의존성 설치 + 첫 실행 가능한 상태로.
+
+## 템플릿
+
+| 템플릿 | 용도 | 의존성 | 첫 실행 |
+|---|---|---|---|
+| **vite-react** ⭐ 추천 | SPA·대시보드·SaaS UI | Node·npm | `npm run dev` → :5173 |
+| **nextjs** | full-stack·SEO·서버 컴포넌트 | Node·npm | `npm run dev` → :3000 |
+| **astro** | 블로그·콘텐츠·랜딩 | Node·npm | `npm run dev` → :4321 |
+| **expo** | 진짜 모바일 앱 (iOS/Android) | Node·npm·Expo Go | `npm start` → QR |
+| **vanilla** | 단순 HTML/CSS/JS | 없음 | `python3 -m http.server` |
+
+## 사용법
+
+설정 (web_init.json):
+- `TEMPLATE`: 위 5개 중 하나
+- `PROJECT_NAME`: 영문·숫자·하이픈 (예: `my-blog`)
+- `OUTPUT_DIR`: 비우면 `~/connect-ai-projects/`
+
+실행:
+```
+python3 web_init.py
+```
+
+## 어떤 걸 골라야 하나
+
+- **이걸로 시작:** vite-react (SPA·대시보드·내부 도구)
+- **블로그·기업 사이트:** astro
+- **풀스택 (DB·API):** nextjs
+- **모바일 앱:** expo (PWA로 충분하면 vite-react)
+- **HTML 한 페이지:** vanilla
+
+## 다음 단계
+
+셋업 후 코다리가:
+1. `web_preview` 도구로 dev server 실행
+2. 사용자 요구사항대로 컴포넌트 추가
+3. `pwa_setup` 으로 PWA 만들기 (모바일 앱처럼)
+4. Vercel/Netlify에 배포
diff --git a/10_Wiki/Topics/_company/_agents/developer/tools/web_init.py b/10_Wiki/Topics/_company/_agents/developer/tools/web_init.py
new file mode 100644
index 00000000..12b0b811
--- /dev/null
+++ b/10_Wiki/Topics/_company/_agents/developer/tools/web_init.py
@@ -0,0 +1,307 @@
+#!/usr/bin/env python3
+# version: web_init_v3
+"""웹·모바일 프로젝트 자동 초기화 — 5개 템플릿 중 선택.
+
+config:
+ TEMPLATE — vite-react / nextjs / astro / expo / vanilla
+ PROJECT_NAME — 프로젝트 폴더 이름 (영문·하이픈, 공백 X)
+ OUTPUT_DIR — 어디에 만들지 (비우면 ~/connect-ai-projects/)
+
+각 템플릿은 검증된 공식 명령어로 셋업. 5분 안에 dev server 띄울 수 있는 상태로.
+"""
+import os, sys, json, subprocess, shutil
+
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+CONFIG = os.path.join(HERE, "web_init.json")
+
+
+def _log(msg, kind="info"):
+ prefix = {"info": "💻", "ok": "✅", "warn": "⚠️ ", "err": "❌", "step": "▸"}.get(kind, "•")
+ print(f"{prefix} {msg}", file=sys.stderr, flush=True)
+
+
+def _load():
+ if os.path.exists(CONFIG):
+ try:
+ with open(CONFIG, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ pass
+ return {}
+
+
+def _save(c):
+ try:
+ with open(CONFIG, "w", encoding="utf-8") as f:
+ json.dump(c, f, indent=2, ensure_ascii=False)
+ except Exception:
+ pass
+
+
+def _check_cmd(cmd):
+ """Check if a CLI tool exists."""
+ return shutil.which(cmd) is not None
+
+
+def _run(cmd, cwd=None, capture=True):
+ """Run shell command, stream stderr live but capture stdout for return."""
+ _log(f"$ {cmd}", "step")
+ if capture:
+ r = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=600)
+ if r.stdout:
+ for line in r.stdout.splitlines()[:20]:
+ print(f" {line}")
+ if r.stderr and r.returncode != 0:
+ for line in r.stderr.splitlines()[-10:]:
+ _log(line, "warn")
+ return r.returncode == 0, r.stdout
+ else:
+ return subprocess.run(cmd, shell=True, cwd=cwd, timeout=600).returncode == 0, ""
+
+
+def _scaffold_vite_react(name, parent):
+ """Vite + React + TS + Tailwind v4 (Vite 플러그인 방식).
+ v2: tailwindcss init 명령이 v4에서 제거됨 → @tailwindcss/vite 플러그인 사용 + 설정 파일 직접 쓰기.
+ 각 단계마다 (cmd, cwd, critical) — critical=False면 실패해도 프로젝트 살림."""
+ target = os.path.join(parent, name)
+ return [
+ ("create", f"npm create vite@latest {name} -- --template react-ts", parent, True),
+ ("install", "npm install", target, True),
+ ("tailwind-pkg", "npm install tailwindcss@^4 @tailwindcss/vite@^4", target, False),
+ ("tailwind-config", _write_vite_tailwind_config, target, False),
+ ]
+
+
+def _write_vite_tailwind_config(target):
+ """Tailwind v4 설정 파일 직접 작성 (init 명령 의존 없음)."""
+ # vite.config.ts: 기본 파일에 tailwindcss 플러그인 추가
+ vite_cfg = os.path.join(target, "vite.config.ts")
+ if os.path.exists(vite_cfg):
+ try:
+ with open(vite_cfg, "r", encoding="utf-8") as f:
+ content = f.read()
+ if "tailwindcss" not in content:
+ # import 추가
+ content = "import tailwindcss from '@tailwindcss/vite'\n" + content
+ # plugins: [react()] → plugins: [react(), tailwindcss()]
+ content = content.replace("plugins: [react()]", "plugins: [react(), tailwindcss()]")
+ with open(vite_cfg, "w", encoding="utf-8") as f:
+ f.write(content)
+ except Exception:
+ pass
+
+ # src/index.css: Tailwind v4 import 한 줄
+ css_path = os.path.join(target, "src", "index.css")
+ if os.path.exists(css_path):
+ try:
+ with open(css_path, "r", encoding="utf-8") as f:
+ cur = f.read()
+ if '@import "tailwindcss"' not in cur:
+ with open(css_path, "w", encoding="utf-8") as f:
+ f.write('@import "tailwindcss";\n\n' + cur)
+ except Exception:
+ pass
+
+ return True
+
+
+TEMPLATES = {
+ "vite-react": {
+ "label": "⚡ Vite + React + TypeScript + Tailwind v4",
+ "needs": ["node", "npm"],
+ "scaffold": _scaffold_vite_react,
+ "post": "Tailwind v4 (Vite 플러그인) + index.css 자동 설정",
+ "dev_cmd": "npm run dev",
+ },
+ "nextjs": {
+ "label": "▲ Next.js 14 (App Router) + TypeScript + Tailwind",
+ "needs": ["node", "npm"],
+ "scaffold": lambda name, parent: [
+ ("scaffold", f"npx create-next-app@latest {name} --typescript --tailwind --app --src-dir --import-alias '@/*' --no-eslint --use-npm --yes", parent, True),
+ ],
+ "post": "App Router·Tailwind·src 디렉토리 셋업 완료",
+ "dev_cmd": "npm run dev",
+ },
+ "astro": {
+ "label": "🚀 Astro + Tailwind (정적·콘텐츠 사이트)",
+ "needs": ["node", "npm"],
+ "scaffold": lambda name, parent: [
+ ("scaffold", f"npm create astro@latest {name} -- --template minimal --typescript strict --install --git --yes", parent, True),
+ ("tailwind", f"npx astro add tailwind --yes", os.path.join(parent, name), False),
+ ],
+ "post": "Astro + Tailwind",
+ "dev_cmd": "npm run dev",
+ },
+ "expo": {
+ "label": "📱 Expo (React Native · iOS/Android/Web 동시)",
+ "needs": ["node", "npm"],
+ "scaffold": lambda name, parent: [
+ ("scaffold", f"npx create-expo-app@latest {name} --template blank-typescript", parent, True),
+ ],
+ "post": "Expo Go 앱(iOS/Android) 깔고 'npm start' 후 QR 스캔",
+ "dev_cmd": "npm start",
+ },
+ "vanilla": {
+ "label": "📄 Vanilla HTML + CSS + JS (프레임워크 없음)",
+ "needs": [],
+ "scaffold": "VANILLA", # 특수 케이스 — 직접 파일 생성
+ "post": "단일 폴더 + index.html + style.css + script.js + README",
+ "dev_cmd": "python3 -m http.server 8000",
+ },
+}
+
+
+def _scaffold_vanilla(target_dir, name):
+ """프레임워크 없이 정적 사이트 시드."""
+ os.makedirs(target_dir, exist_ok=True)
+ files = {
+ "index.html": f'''
+
+