sns-auto-builder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # sns-auto-builder
2
+
3
+ 카드뉴스(인스타 캐러셀 9장) / 쓰레드(Threads) 생성 도구.
4
+ **글 생성에는 API 키가 필요 없습니다** — Claude Code CLI(`claude -p`)를 호출해 구독 계정으로 돌아갑니다.
5
+ 이미지 생성만 키가 듭니다.
6
+
7
+ 구조는 [insta-carousel-builder](https://github.com/shdsjh123-cpu/insta-carousel-builder) 하네스를
8
+ **원본 그대로** 가져왔습니다 (`brand-facts.md` 의 브랜드명 한 줄만 교체). 부족한 부분은 원본을 고치지 않고 별도 파일로 추가했습니다.
9
+
10
+ ---
11
+
12
+ ## 진입점 2개 — 같은 하네스를 쓴다
13
+
14
+ **웹 UI**
15
+
16
+ ```bash
17
+ node server.mjs # → http://127.0.0.1:8787
18
+ ```
19
+
20
+ **슬래시 커맨드** (Claude Code 안에서)
21
+
22
+ ```
23
+ /carousel-new 클로드 코드 생산성 꿀팁
24
+ /carousel-quality
25
+ ```
26
+
27
+ 웹 서버는 파이프라인을 복제하지 않고 `claude -p` 로 **같은 서브에이전트를 호출**합니다.
28
+ `CLAUDE.md` 가 단일 출처입니다.
29
+
30
+ 포트 충돌 시 `PORT=8788 node server.mjs`.
31
+
32
+ ---
33
+
34
+ ## 카드뉴스 엔진 3개
35
+
36
+ 웹에서 카드뉴스를 고르면 엔진을 묻습니다. 한글 정확도와 비용이 여기서 갈립니다.
37
+
38
+ | 엔진 | 한글 | 비용 | 키 | 어울리는 곳 |
39
+ |:--|:--|:--|:--|:--|
40
+ | **HTML만** | **100% 결정적** | 0원 | 불필요 | 세법·법무·정보성. 한 글자만 고쳐 재캡처 가능 |
41
+ | **이미지 + HTML** | **100% 결정적** | 구독 | 불필요 (ChatGPT 로그인) | 분위기가 필요한 브랜드·감성 주제 |
42
+ | **이미지만** | 비결정적 ⚠️ | 구독 | 불필요 (ChatGPT 로그인) | 글자가 적은 비주얼 위주 |
43
+
44
+ **이미지만 모드는 한글이 이미지 안에 들어가 매번 달라집니다.** 스크립트가 오타를 못 잡으니
45
+ 9장을 눈으로 확인하고 깨진 장만 `--only N` 으로 다시 뽑으세요. 글자가 중요하면 다른 두 엔진을 쓰세요.
46
+
47
+ 쓰레드는 글만 뽑으므로 엔진 선택이 없습니다.
48
+
49
+ ---
50
+
51
+ ## 파이프라인
52
+
53
+ ```
54
+ 리서치 carousel-researcher → brief.json (검증 사실 / 불확실 분리)
55
+
56
+ 배경(선택) chatgpt-image-gen.js --mode bg → bg/bg-NN.png ← 이미지+HTML 만
57
+
58
+ 작성 carousel-html-writer → slides/slide-NN.html
59
+ 또는 carousel-prompt-writer → templates/slides.<topic>.json ← 이미지만
60
+
61
+ 생성 html-carousel-gen.js → slide-NN.png (2160×2700)
62
+ 또는 chatgpt-image-gen.js → slide-NN.png (2160×2700)
63
+
64
+ 검사 quality-check.js --dir → 해상도·배율·장수 (기계)
65
+ carousel-reviewer → 10항목 채점 (정성)
66
+ ```
67
+
68
+ 작성자와 검증자를 분리한 게 핵심입니다. 실제로 리뷰어가 작성자의 사실 오류를 잡아냅니다.
69
+
70
+ ---
71
+
72
+ ## 구조
73
+
74
+ ```
75
+ sns-auto-builder/
76
+ ├── CLAUDE.md # 에이전트 진입점 (원본)
77
+ ├── .claude/
78
+ │ ├── agents/ # carousel-researcher / -html-writer / -prompt-writer / -reviewer (원본)
79
+ │ └── commands/ # /carousel-new, /carousel-quality (원본)
80
+ ├── knowledge/ # ★ 프롬프트 단일 출처
81
+ │ ├── brand-facts.md # 브랜드·톤·수치·디자인 DNA (원본 + 브랜드명만 교체)
82
+ │ ├── banned-words.json # 금칙어 (원본)
83
+ │ └── patterns/
84
+ │ ├── carousel-structure.md # 9장 공식 (원본)
85
+ │ ├── carousel-output-format.md # 텍스트 출력 형식 (추가)
86
+ │ └── thread-structure.md # 스레드 공식 (추가 — 원본엔 쓰레드가 없음)
87
+ ├── scripts/
88
+ │ ├── quality-check.js # JSON 스키마 + PNG 해상도/배율 (원본 + 해상도 검사 추가)
89
+ │ ├── quality-check-text.js # 카피 금칙어·글자수·구조 (추가)
90
+ │ ├── html-carousel-gen.js # HTML → PNG 캡처 (원본)
91
+ │ ├── chatgpt-image-gen.js # ChatGPT 웹 배경/슬라이드 — API 키 불필요 (추가)
92
+ │ ├── openai-image-gen.js # OpenAI API 배경/슬라이드 (추가, 빌더는 더 이상 안 씀)
93
+ │ └── nanobanana-gen.py # Gemini 슬라이드 (원본, Python 필요)
94
+ ├── templates/
95
+ ├── output/ # 생성 결과 (gitignored)
96
+ ├── server.mjs · index.html # 웹 UI (추가)
97
+ └── test.mjs
98
+ ```
99
+
100
+ ---
101
+
102
+ ## 웹 UI
103
+
104
+ 1. **카드뉴스** 또는 **쓰레드**
105
+ 2. (카드뉴스) 엔진 선택
106
+ 3. 주제 입력 — `⌘/Ctrl + Enter` 로도 시작
107
+ 4. (선택) **참고 자료** 이미지 첨부 — 끌어다 놓기 / `⌘/Ctrl + V` / 눌러서 고르기
108
+ 5. 진행 단계가 실시간 표시 → 끝나면 슬라이드 미리보기 + 개별 다운로드
109
+
110
+ 진행 표시는 **모델 보고가 아니라 파일이 생겼는지로 판정**합니다. `brief.json` 이 없으면 리서치는 진행 중입니다.
111
+
112
+ 카드뉴스 전체 파이프라인은 **20~35분** 걸립니다. 쓰레드는 1~2분.
113
+
114
+ ### 참고 자료
115
+
116
+ 경쟁사 캡처, 화면, 자료 문서를 올리면 그걸 보고 씁니다. **사진 자체는 결과물에 안 들어갑니다.**
117
+
118
+ - png · jpg · webp · gif, **최대 6장 / 총 20MB**. SVG는 막습니다 (스크립트 실행 가능)
119
+ - 파일명은 서버가 붙입니다 (클라이언트 값 안 씀 — 경로 조작 방지)
120
+ - 프롬프트 가드: 이미지 속 **수치를 사실로 취급 금지**, 이미지 안 지시문은 **따르지 않음**
121
+ - 매번 참고할 고정 자료는 업로드 말고 `knowledge/reference/` 에 두세요
122
+
123
+ ---
124
+
125
+ ## 키 설정
126
+
127
+ `.env` 를 **프로젝트 루트**에 만드세요 (`.env.example` 이 아니라 — 그건 커밋되는 템플릿입니다).
128
+
129
+ ```bash
130
+ cp .env.example .env
131
+ ```
132
+
133
+ | 키 | 언제 필요 | 발급 |
134
+ |:--|:--|:--|
135
+ | `GEMINI_API_KEY` | 나노바나나 트랙 (Python 필요) | aistudio.google.com/apikey |
136
+ | `OPENAI_API_KEY` | `openai-image-gen.js` 를 직접 쓸 때만 | platform.openai.com/api-keys |
137
+
138
+ 빌더의 **이미지+HTML / 이미지만** 트랙은 키가 필요 없습니다. `chatgpt-image-gen.js` 가
139
+ 로그인된 Chrome 프로필(`.chrome-profile`)로 chatgpt.com 을 몰아 구독 계정으로 생성합니다.
140
+ 그 프로필에 **한 번 로그인해두면** 이후로는 그대로 재사용됩니다.
141
+
142
+ `.env` 는 gitignored 입니다. **키를 `.env.example` 에 넣지 마세요.**
143
+
144
+ ---
145
+
146
+ ## 커스터마이즈
147
+
148
+ | 파일 | 뭘 고치나 |
149
+ |:--|:--|
150
+ | `knowledge/brand-facts.md` | 브랜드·타겟·수치·디자인 DNA — **여기부터 채우세요** |
151
+ | `knowledge/banned-words.json` | 금칙어. 브랜드 금지어는 `brand_specific` 에 |
152
+ | `knowledge/patterns/*.md` | 구조 공식, 글자수, 출력 형식 |
153
+ | `scripts/html-carousel-gen.js` | `CONFIG.deviceScaleFactor` — 배율 (기본 2x) |
154
+
155
+ `brand-facts.md` 에 `<채우세요>` 가 남아 있으면 그 항목은 **언급되지 않습니다** (지어내지 않습니다).
156
+
157
+ ---
158
+
159
+ ## 검사
160
+
161
+ ```bash
162
+ node test.mjs # 조립·검증 로직
163
+ node scripts/quality-check.js --dir output/<topic> # PNG 해상도·배율·장수
164
+ node scripts/quality-check.js --prompt templates/slides.<topic>.json
165
+ node scripts/quality-check-text.js --file output/<topic>/copy.md
166
+ node scripts/chatgpt-image-gen.js --topic t --slides templates/slides.example.json --dry-run
167
+ ```
168
+
169
+ **해상도**: `1080×1350` 의 정수배만 통과합니다. HTML 트랙 기본이 2x(`2160×2700`)이고,
170
+ 인스타는 업로드 시 가로 1080으로 줄이므로 2x가 손해가 아닙니다.
171
+
172
+ ---
173
+
174
+ ## 안 하는 것
175
+
176
+ - **자동 업로드** — 의도적 배제 (알고리즘 패턴 탐지 회피)
177
+ - **한글 오타 자동 검출** — 이미지만 모드는 육안 검수가 필수입니다
178
+ - 툴 UI의 크림/Jua 배색은 **툴 자체 컨셉**입니다. 카드뉴스 디자인은 `brand-facts.md` 의 디자인 DNA를 따릅니다
@@ -0,0 +1,145 @@
1
+ # insta-carousel-builder
2
+
3
+ > **Claude Code 하네스로 돌아가는 인스타 캐러셀 자동 제작 도구.**
4
+ > **듀얼 엔진**: 나노바나나 Pro(Gemini 3.0 Pro Image) + HTML/CSS+Puppeteer.
5
+
6
+ AI 이미지 도구로 인스타 캐러셀을 만들면 한글이 깨지고 AI 티가 나서 결국 수작업/외주로 돌아간다 — 는 통념을, **두 엔진 골라 쓰기**로 깨는 레포입니다.
7
+
8
+ **👀 실제 결과물 → [`docs/sample-output/`](./docs/sample-output/) (나노바나나) / [`docs/sample-html/`](./docs/sample-html/) (HTML)**
9
+
10
+ ## 듀얼 엔진 비교
11
+
12
+ | 비교 | 🍌 나노바나나 (Gemini API) | 🎨 HTML/Puppeteer |
13
+ |:---|:---|:---|
14
+ | **명령어** | `python scripts/nanobanana-gen.py` | `node scripts/html-carousel-gen.js` |
15
+ | **한글 정확도** | 88~98% (비결정적) | **100% (결정적)** |
16
+ | **비용** | ~500~1000원 / 9장 | **0원** |
17
+ | **시간** | ~4분 30초 | ~30초 (캡처만) |
18
+ | **디자인 자유도** | AI 해석 (가끔 워터마크) | **100% 통제** |
19
+ | **수정 용이성** | 재생성 필요 | **한 글자도 즉시 Edit** |
20
+ | **디자인 다양성** | 매번 다름 | 템플릿 한정 |
21
+ | **Best for** | 빠른 실험 / A/B 시안 / 디자인 실험 | 광고 / 법무 / 브랜드 / 텍스트 정확성 |
22
+
23
+ → "**상황 따라 골라 쓴다**" 가 핵심.
24
+
25
+ ---
26
+
27
+ ## 이게 뭐예요?
28
+
29
+ 에이나우(AINOW) 내부에서 실사용 중인 인스타 캐러셀 생성 파이프라인을 오픈소스로 공개한 버전입니다.
30
+
31
+ - ✅ `.env` 에 Gemini API 키 하나 + `npx` 한 줄이면 재현 가능
32
+ - ✅ `templates/slides.example.json` 의 프롬프트 9개만 수정하면 본인 콘텐츠로 교체
33
+ - ✅ Claude Code `.claude/agents/` + `.claude/commands/` 하네스로 품질 검증까지 자동화
34
+ - ✅ 디자인 DNA(`#131316` + `#CF5C3F` + Pretendard) 유지된 채로 주제만 바뀜
35
+
36
+ ---
37
+
38
+ ## 빠른 시작 (Quickstart)
39
+
40
+ ```bash
41
+ # 1. 레포 clone
42
+ git clone https://github.com/ainow/insta-carousel-builder.git
43
+ cd insta-carousel-builder
44
+
45
+ # 2. 의존성 설치
46
+ pip install python-dotenv google-genai
47
+ npm install # (optional, 품질 체크 훅용)
48
+
49
+ # 3. API 키 세팅
50
+ cp .env.example .env
51
+ # .env 열어서 GEMINI_API_KEY 채우기 — https://aistudio.google.com/apikey
52
+
53
+ # 4. 기본 예제 생성 (에이나우 Claude Code 7가지 꿀팁)
54
+ python scripts/nanobanana-gen.py --topic claude-code-tips
55
+
56
+ # 5. 본인 주제로 수정
57
+ # templates/slides.example.json 을 복사해서 프롬프트 9장 수정
58
+ cp templates/slides.example.json templates/slides.my-topic.json
59
+ # 수정 후:
60
+ python scripts/nanobanana-gen.py --topic my-topic --slides templates/slides.my-topic.json
61
+ ```
62
+
63
+ 결과: `output/{topic}/slide-01.png ~ slide-09.png`
64
+
65
+ ---
66
+
67
+ ## 왜 이 레포인가? (Why)
68
+
69
+ 시중의 AI 이미지 캐러셀 도구 대부분은 3가지 중 하나가 부족합니다:
70
+
71
+ | 문제 | 이 레포의 해법 |
72
+ |:---|:---|
73
+ | ❌ 한글 깨짐 | ✅ Gemini 3.0 Pro Image — 한글 네이티브 렌더링 (실측 97.8%) |
74
+ | ❌ 매번 다른 디자인 | ✅ `common_style` 고정 DNA로 9장 통일 |
75
+ | ❌ 생성 후 일일이 포토샵 | ✅ 1080×1350 PNG 바로 업로드 가능 |
76
+
77
+ ---
78
+
79
+ ## 구조
80
+
81
+ ```
82
+ insta-carousel-builder/
83
+ ├── CLAUDE.md # Claude Code 지시서 (에이전트 진입점)
84
+ ├── .claude/
85
+ │ ├── agents/ # 서브에이전트 (researcher / writer / reviewer)
86
+ │ └── commands/ # /carousel-new, /carousel-quality 등
87
+ ├── knowledge/ # 브랜드 팩트 / 톤 / 패턴 (커스터마이즈 포인트)
88
+ ├── templates/
89
+ │ └── slides.example.json # 9장 프롬프트 예제 (복사해서 수정)
90
+ ├── scripts/
91
+ │ ├── nanobanana-gen.py # 이미지 생성기 (Python, Gemini SDK)
92
+ │ ├── quality-check.js # PostToolUse 훅 (Node.js)
93
+ │ └── duplicate-check.js
94
+ ├── output/ # 생성 결과 (gitignored)
95
+ └── docs/GETTING-STARTED.md
96
+ ```
97
+
98
+ ---
99
+
100
+ ---
101
+
102
+ ## 샘플 결과 (실측, 2026-04-15)
103
+
104
+ `docs/sample-output/slide-01.png ~ slide-09.png` 가 실제 생성된 9장입니다.
105
+ 주제: **"Claude Code 7가지 꿀팁"** (`templates/slides.example.json` 그대로 실행).
106
+
107
+ | 항목 | 결과 |
108
+ |:---|:---|
109
+ | 생성 성공률 | **9/9 (100%)** |
110
+ | 총 소요 시간 | 4분 28초 (24~31초/장) |
111
+ | 평균 파일 크기 | 487 KB |
112
+ | 해상도 | 1080×1350 (4:5) |
113
+ | 한글 정확도 (육안 표본) | 4장 중 3장 완벽, 1장 한글자 변형 (88%) |
114
+
115
+ ### 알려진 한계
116
+
117
+ - **한글 정확도 비결정성**: 같은 프롬프트라도 세션마다 1~2글자 변형 가능 (`통` → `튱` 등). 100% 정확 보장 안 됨 → **생성 후 육안 검수 필수**
118
+ - **가끔 가짜 인스타 핸들/워터마크 추가**: Gemini 가 학습 데이터 영향으로 본 적 없는 핸들(예: `@DevOpsPro`) 을 자체 추가하는 경우 있음 → 발견 시 해당 슬라이드만 재생성
119
+ - **한글 오타 자동 감지 미구현**: 현재 OCR 기반 오타 감지 없음. `carousel-reviewer` 서브에이전트가 LLM 비전으로 점검
120
+
121
+ ### 재생성 팁
122
+
123
+ 오타 1~2장은 아래로 해당 슬라이드만 재생성 (현재는 전체 재실행, 다음 버전에서 `--only N` 추가 예정):
124
+
125
+ ```bash
126
+ # 임시로 slides JSON 의 다른 슬라이드를 비워두고 실행
127
+ python scripts/nanobanana-gen.py --topic claude-code-tips --slides templates/slides.partial.json
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 라이선스
133
+
134
+ MIT. 자유롭게 포크/수정/재배포. 크레딧은 환영이지만 강제 아닙니다.
135
+
136
+ ---
137
+
138
+ ## 만든 곳
139
+
140
+ **에이나우(AINOW)** — AI 생산자 양성 플랫폼.
141
+ - 인스타그램: [@ai.ainow](https://instagram.com/ai.ainow)
142
+ - 이 레포가 실제로 어떻게 만들어졌는지 궁금하면: [에이나우 유튜브 채널](https://youtube.com/@신영선의AI탐구)
143
+
144
+ > *"AI 이미지는 한글이 깨진다"는 통념이 2026년 현재 깨졌습니다.
145
+ > 이 레포는 그 증거입니다.*
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 카피 빌더 실행기.
4
+ *
5
+ * npx 는 패키지를 **캐시 폴더**에서 실행한다. 그 안에 결과물·브랜드 설정·ChatGPT 로그인
6
+ * 프로필을 두면 버전을 올릴 때마다 새 캐시로 옮겨가 전부 초기화된다.
7
+ * 그래서 사용자 데이터는 패키지 밖 고정 폴더(워크스페이스)에 두고, 도구 파일만 매 실행 때
8
+ * 최신으로 덮어쓴다. → 업데이트해도 내 작업물과 로그인은 그대로 남는다.
9
+ *
10
+ * 워크스페이스: %LOCALAPPDATA%\sns-auto-builder (macOS/Linux: ~/.sns-auto-builder)
11
+ *
12
+ * 사용법: npx sns-auto-builder 또는 실행.bat 더블클릭
13
+ */
14
+ import { cpSync, existsSync, mkdirSync, symlinkSync, rmSync, readdirSync } from 'node:fs';
15
+ import { join, dirname, resolve, sep } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { spawn } from 'node:child_process';
18
+ import { createRequire } from 'node:module';
19
+
20
+ const PKG = resolve(dirname(fileURLToPath(import.meta.url)), '..');
21
+
22
+ function workspaceDir() {
23
+ if (process.env.SNS_BUILDER_HOME) return resolve(process.env.SNS_BUILDER_HOME);
24
+ const base = process.platform === 'win32'
25
+ ? (process.env.LOCALAPPDATA || process.env.APPDATA || process.env.USERPROFILE)
26
+ : process.env.HOME;
27
+ return join(base || process.cwd(), process.platform === 'win32' ? 'sns-auto-builder' : '.sns-auto-builder');
28
+ }
29
+
30
+ // 매 실행 때 최신으로 덮어쓰는 것 = 도구 자체.
31
+ const TOOL_FILES = ['server.mjs', 'index.html', 'CLAUDE.md'];
32
+ const TOOL_DIRS = ['scripts', '.claude', 'templates', 'knowledge'];
33
+
34
+ // 덮어쓰면 안 되는 것 = 사용자가 만든 것. 도구 폴더 안에 섞여 있어 이름으로 걸러낸다.
35
+ const KEEP = new Set(['brand.json', 'brand-facts.md', 'logo.svg']);
36
+ const isUserFile = (p) => {
37
+ const name = p.split(/[\\/]/).pop();
38
+ if (KEEP.has(name)) return true;
39
+ // templates/slides.<주제>.json 은 실행마다 생기는 산출물이다. example 만 도구다.
40
+ return /^(slides|backgrounds)\..+\.json$/.test(name) && !/\.example\.json$/.test(name);
41
+ };
42
+
43
+ function syncTools(ws) {
44
+ for (const f of TOOL_FILES) {
45
+ if (existsSync(join(PKG, f))) cpSync(join(PKG, f), join(ws, f));
46
+ }
47
+ for (const d of TOOL_DIRS) {
48
+ if (!existsSync(join(PKG, d))) continue;
49
+ cpSync(join(PKG, d), join(ws, d), {
50
+ recursive: true,
51
+ // 사용자 파일은 건드리지 않는다 — 있으면 그대로 둔다
52
+ filter: (src) => !(isUserFile(src) && existsSync(join(ws, src.slice(PKG.length + 1)))),
53
+ });
54
+ }
55
+ }
56
+
57
+ // scripts/ 가 워크스페이스에서 돌면서 puppeteer 를 import 한다.
58
+ // 패키지의 node_modules 를 정션으로 연결해두면 그대로 해결된다 (윈도우도 관리자 권한 불필요).
59
+ //
60
+ // 주의: `PKG/node_modules` 를 가정하면 안 된다. npm 은 의존성을 **상위로 끌어올려** 설치해서
61
+ // 실제 설치 환경에서는 그 경로가 없다. 그러면 정션이 조용히 안 만들어지고,
62
+ // 서버는 멀쩡히 뜬 뒤 **이미지 생성 때가 되어서야** import 실패로 죽는다.
63
+ // puppeteer 가 실제로 어디 있는지 물어서 그 node_modules 를 쓴다.
64
+ function modulesDir() {
65
+ try {
66
+ const p = createRequire(import.meta.url).resolve('puppeteer');
67
+ const marker = `${sep}node_modules${sep}`;
68
+ const i = p.lastIndexOf(marker);
69
+ if (i >= 0) return p.slice(0, i + marker.length - 1);
70
+ } catch { /* 아래에서 경고 */ }
71
+ return null;
72
+ }
73
+
74
+ function linkModules(ws) {
75
+ const target = modulesDir();
76
+ const link = join(ws, 'node_modules');
77
+ if (!target || !existsSync(target)) {
78
+ console.warn('(경고) puppeteer 를 찾지 못했습니다. 이미지 생성이 안 될 수 있습니다.');
79
+ return;
80
+ }
81
+ if (existsSync(link)) {
82
+ try { readdirSync(link); return; } catch { rmSync(link, { force: true, recursive: true }); } // 끊긴 링크면 다시 만든다
83
+ }
84
+ try {
85
+ symlinkSync(target, link, process.platform === 'win32' ? 'junction' : 'dir');
86
+ } catch (e) {
87
+ console.warn(`(경고) node_modules 연결 실패: ${e.message}\n 이미지 생성이 안 되면 워크스페이스에서 npm install 하세요.`);
88
+ }
89
+ }
90
+
91
+ const open = (url) => {
92
+ const cmd = process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
93
+ : process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
94
+ spawn(cmd[0], cmd[1], { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
95
+ };
96
+
97
+ const ws = workspaceDir();
98
+ mkdirSync(ws, { recursive: true });
99
+ syncTools(ws);
100
+ linkModules(ws);
101
+
102
+ const port = process.env.PORT || 8787;
103
+ console.log(`\n 카피 빌더`);
104
+ console.log(` 작업 폴더: ${ws}`);
105
+ console.log(` 주소: http://127.0.0.1:${port}\n`);
106
+ console.log(` ※ 이 창을 닫으면 빌더도 꺼집니다. 작업이 끝날 때까지 열어두세요.\n`);
107
+
108
+ // 서버는 워크스페이스에서 돈다 — 결과물·설정이 전부 거기 쌓인다.
109
+ const srv = spawn(process.execPath, [join(ws, 'server.mjs')], {
110
+ cwd: ws,
111
+ stdio: 'inherit',
112
+ env: { ...process.env, PORT: String(port) },
113
+ });
114
+ srv.on('exit', (code) => process.exit(code ?? 0));
115
+
116
+ setTimeout(() => open(`http://127.0.0.1:${port}`), 1200);