sns-auto-builder 1.0.4 → 1.0.6
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 +27 -8
- package/bin/sns-auto-builder.mjs +13 -1
- package/index.html +20 -0
- package/package.json +1 -1
- package/scripts/build-dist-zip.mjs +97 -0
- package/scripts/chatgpt-image-gen.js +7 -2
- package/server.mjs +16 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ ChatGPT 웹을 몰아 만듭니다 — 둘 다 **본인 구독 계정**으로
|
|
|
16
16
|
|
|
17
17
|
`dist/` 의 파일 **4개를 같은 폴더에** 두고 씁니다:
|
|
18
18
|
|
|
19
|
-
1. **설치.bat** — 처음 한 번. Node · Claude Code 설치
|
|
19
|
+
1. **설치.bat** — 처음 한 번. 저장 폴더 선택 → Node · Claude Code 설치 → 로그인 2번(Claude, ChatGPT)
|
|
20
20
|
2. **실행.bat** — 매번. 더블클릭하면 브라우저가 열립니다
|
|
21
21
|
3. **config.bat** — 설정. **두 bat 이 이것을 읽습니다**
|
|
22
22
|
4. **사용법.txt** — 안내
|
|
@@ -48,15 +48,24 @@ Claude Code 안에서는 슬래시 커맨드도 됩니다:
|
|
|
48
48
|
브랜드 설정 · ChatGPT 로그인 · 결과물이 그대로 남습니다.
|
|
49
49
|
|
|
50
50
|
```
|
|
51
|
-
%LOCALAPPDATA%\sns-auto-builder
|
|
51
|
+
<설치.bat 에서 고른 폴더>\ ← 기본값은 %LOCALAPPDATA%\sns-auto-builder
|
|
52
52
|
├── workspace 파일들 (output, knowledge, .chrome-profile, logs …)
|
|
53
53
|
├── browser\ puppeteer 크로미움 (약 2.6GB)
|
|
54
54
|
└── npm-cache\ npx 캐시 (수 GB)
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
`설치.bat` 1단계가 폴더 선택 창을 띄우고, 고른 경로를 `config.bat` 의
|
|
58
|
+
`set "DATA_PATH="` 줄에 써넣습니다. 바꾸려면 `설치.bat` 을 다시 실행하면 됩니다.
|
|
59
|
+
기존 워크스페이스는 최초 1회 자동으로 옮겨집니다.
|
|
60
|
+
|
|
61
|
+
`config.bat` 을 손으로 고치지 마세요. 이 파일은 **UTF-8(BOM 없음) + CRLF** 여야 하고,
|
|
62
|
+
둘 중 하나만 어긋나면 cmd 가 한글 섞인 줄을 잘못 읽어 **설정이 조용히 무시**됩니다
|
|
63
|
+
(LF 로 저장된 적이 있었고, 그때 한글 경로가 통째로 날아갔습니다).
|
|
64
|
+
`설치.bat` 은 PowerShell `WriteAllLines` 로 써서 이 조합을 보장합니다.
|
|
65
|
+
잘못돼도 빌더 첫 화면에 **실제 사용 중인 폴더가 항상 표시**되므로 눈에 띕니다.
|
|
66
|
+
|
|
67
|
+
**USB·외장 드라이브, 다운로드 폴더, OneDrive 동기화 폴더는 피하세요** —
|
|
68
|
+
각각 분리 시 로그인 소실, 정리 중 삭제, 2.6GB 크로미움 무한 동기화 문제가 있습니다.
|
|
60
69
|
|
|
61
70
|
> **경로는 한 곳에서만 계산합니다.** 예전에는 `실행.bat` · `설치.bat` · 스크립트가
|
|
62
71
|
> 각자 경로를 계산했고, 한쪽만 바뀌자 **ChatGPT 로그인이 빌더가 안 보는 폴더에 저장**됐습니다.
|
|
@@ -213,9 +222,19 @@ npm publish
|
|
|
213
222
|
|
|
214
223
|
`.bat` 은 패키지에 없어서 **자동 업데이트가 안 됩니다.** 고치면 팀원에게 4개를 다시 보내야 합니다.
|
|
215
224
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
225
|
+
```bash
|
|
226
|
+
node scripts/build-dist-zip.mjs # dist/snsauto-builder.zip 재생성
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
이 스크립트가 묶기 전에 **CRLF · BOM 없음 · 명령부 ASCII** 를 검사하고, 어긋나면
|
|
230
|
+
멈춥니다. zip 은 한글 파일명에 UTF-8 플래그(0x800)를 직접 세웁니다
|
|
231
|
+
(`Compress-Archive` 는 이걸 안 세워서 푸는 쪽에 따라 이름이 깨집니다).
|
|
232
|
+
|
|
233
|
+
> `.bat` 의 **명령과 주석은 ASCII 로만** 쓰세요. cmd 는 콘솔 코드페이지로 파일을 읽는데
|
|
234
|
+
> 그 값이 PC 마다 달라서, 한글 바이트가 명령어를 갉아먹습니다
|
|
235
|
+
> (`echo.` 가 `ech` + `o.` 로 쪼개진 적 있음). 한글 안내는 `사용법.txt` 와 Node 출력이 담당합니다.
|
|
236
|
+
> 예외는 `config.bat` 의 `set "DATA_PATH=..."` 값 한 줄뿐이고,
|
|
237
|
+
> 그건 파일이 UTF-8 + CRLF 일 때만 안전합니다.
|
|
219
238
|
|
|
220
239
|
---
|
|
221
240
|
|
package/bin/sns-auto-builder.mjs
CHANGED
|
@@ -28,6 +28,13 @@ function workspaceDir() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// 매 실행 때 최신으로 덮어쓰는 것 = 도구 자체.
|
|
31
|
+
// 빌더 전용 CDP 포트. 9222 는 크롬 디버깅의 관례 포트라 사용자 본인의 크롬,
|
|
32
|
+
// chrome-devtools MCP, 다른 자동화 도구가 이미 쓰고 있기 쉽다. 겹치면 한쪽이
|
|
33
|
+
// ::1 을, 다른 쪽이 127.0.0.1 을 잡아서 **엉뚱한 브라우저에 붙는다** — 조용히
|
|
34
|
+
// 사용자 개인 크롬을 조종하게 되므로 관례 포트를 피한다.
|
|
35
|
+
// scripts/chatgpt-image-gen.js 가 같은 기본값과 같은 환경변수를 쓴다.
|
|
36
|
+
const CDP_PORT = process.env.SNS_BUILDER_CDP_PORT || '19222';
|
|
37
|
+
|
|
31
38
|
const TOOL_FILES = ['server.mjs', 'index.html', 'CLAUDE.md'];
|
|
32
39
|
const TOOL_DIRS = ['scripts', '.claude', 'templates', 'knowledge'];
|
|
33
40
|
|
|
@@ -131,8 +138,13 @@ if (argv.includes('--login')) {
|
|
|
131
138
|
console.log(` 로그인만 하고 창은 그대로 두거나 닫으면 됩니다.\n`);
|
|
132
139
|
// 포트를 반드시 붙인다. Chrome 은 user-data-dir 당 인스턴스 하나만 띄우므로,
|
|
133
140
|
// 포트 없이 뜬 창이 남으면 빌더가 그 프로필로 포트를 못 연다.
|
|
141
|
+
//
|
|
142
|
+
// 9222 가 아니라 CDP_PORT 인 이유: 9222 는 크롬 디버깅의 관례 포트라 사용자
|
|
143
|
+
// 본인의 크롬이나 다른 도구가 이미 쓰고 있는 경우가 있다. 실제로 한쪽이
|
|
144
|
+
// ::1:9222 를, 다른 한쪽이 127.0.0.1:9222 를 잡아서, 빌더가 사용자 개인
|
|
145
|
+
// 크롬에 붙을 뻔했다. chatgpt-image-gen.js 와 같은 값을 써야 한다.
|
|
134
146
|
spawn(exe, [
|
|
135
|
-
|
|
147
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
136
148
|
`--user-data-dir=${profile}`,
|
|
137
149
|
'--no-first-run', '--no-default-browser-check',
|
|
138
150
|
'https://chatgpt.com',
|
package/index.html
CHANGED
|
@@ -382,10 +382,13 @@ button.go:focus-visible, button.quiet:focus-visible, .copy:focus-visible { outli
|
|
|
382
382
|
</div>
|
|
383
383
|
<div class="bar">
|
|
384
384
|
<button class="quiet" id="brandOpen">⚙ 브랜드 컨셉 설정</button>
|
|
385
|
+
<button class="quiet" id="openHome" title="결과물이 저장되는 폴더를 엽니다">📂 작업 폴더 열기</button>
|
|
385
386
|
<a class="quiet dl" id="logDl" href="/logs" download
|
|
386
387
|
title="문제가 생기면 이 파일을 담당자에게 보내세요">🛟 문제 신고용 로그</a>
|
|
387
388
|
<span class="fine" id="brandNow"></span>
|
|
388
389
|
</div>
|
|
390
|
+
<!-- 설정이 조용히 무시돼도 여기서 바로 드러나게 실제 경로를 보여준다 -->
|
|
391
|
+
<p class="fine" id="homePath"></p>
|
|
389
392
|
</section>
|
|
390
393
|
|
|
391
394
|
<!-- 1.2 브랜드 설정 — 여기 값이 모든 카피·디자인의 기준이 된다 -->
|
|
@@ -1554,6 +1557,23 @@ async function openOutput(it) {
|
|
|
1554
1557
|
$('outsOpen').onclick = () => { loadOutputs(); show('outs'); };
|
|
1555
1558
|
$('outsBack').onclick = () => show('engine');
|
|
1556
1559
|
|
|
1560
|
+
// 데이터는 bat 옆이 아니라 안정된 위치에 쌓인다 (새 zip 을 다른 곳에 풀어도 살아남게).
|
|
1561
|
+
// 대신 어디인지 보이고, 눌러서 열 수 있어야 한다.
|
|
1562
|
+
$('openHome').onclick = async () => {
|
|
1563
|
+
try {
|
|
1564
|
+
const r = await fetch('/home/open', { method: 'POST' });
|
|
1565
|
+
if (!r.ok) throw new Error('열지 못했습니다');
|
|
1566
|
+
} catch (e) { alert(e.message); }
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
(async () => {
|
|
1570
|
+
try {
|
|
1571
|
+
const { output } = await (await fetch('/home')).json();
|
|
1572
|
+
$('openHome').title = `결과물 폴더: ${output}`;
|
|
1573
|
+
$('homePath').textContent = `저장 위치: ${output}`;
|
|
1574
|
+
} catch { /* 경로 표시는 없어도 동작에 지장 없다 */ }
|
|
1575
|
+
})();
|
|
1576
|
+
|
|
1557
1577
|
loadBrand();
|
|
1558
1578
|
|
|
1559
1579
|
$('go').onclick = generate;
|
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// 팀원에게 보내는 4파일 묶음을 만든다.
|
|
2
|
+
// node scripts/build-dist-zip.mjs
|
|
3
|
+
//
|
|
4
|
+
// store 모드(무압축) zip 을 직접 쓴다. 의존성을 늘리지 않으려는 것도 있지만,
|
|
5
|
+
// Compress-Archive 가 한글 파일명에 UTF-8 플래그를 안 세워서 압축을 푸는
|
|
6
|
+
// 쪽에 따라 이름이 깨지기 때문이다. 여기서는 0x800 을 직접 세운다.
|
|
7
|
+
//
|
|
8
|
+
// bat 은 UTF-8(BOM 없음) + CRLF 여야 한다. 둘 중 하나만 어긋나도 cmd 가
|
|
9
|
+
// 한글이 섞인 줄을 조용히 잘못 읽어서 저장 폴더 설정이 무시된다.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { join, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
|
16
|
+
const OUT = 'snsauto-builder.zip';
|
|
17
|
+
|
|
18
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
19
|
+
let c = n;
|
|
20
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
21
|
+
return c >>> 0;
|
|
22
|
+
});
|
|
23
|
+
const crc32 = (buf) => {
|
|
24
|
+
let c = 0xffffffff;
|
|
25
|
+
for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
26
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function checkBat(name, buf) {
|
|
30
|
+
if (buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) throw new Error(`${name}: BOM 이 있다`);
|
|
31
|
+
const text = buf.toString('utf8');
|
|
32
|
+
if (/(?<!\r)\n/.test(text)) throw new Error(`${name}: LF 줄바꿈이 남아 있다 (CRLF 여야 함)`);
|
|
33
|
+
for (const [i, line] of text.split('\r\n').entries()) {
|
|
34
|
+
// 경로 값 줄만 한글 허용. 명령·주석은 ASCII 여야 콘솔 코드페이지와 무관하게 읽힌다.
|
|
35
|
+
if (/[^\x00-\x7F]/.test(line) && !/^set "DATA_PATH=/.test(line)) {
|
|
36
|
+
throw new Error(`${name}:${i + 1}: bat 명령부에 비ASCII 문자 — ${line.trim()}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 확장자 화이트리스트. dist 안에서 claude 를 한 번 실행했더니 .claude/.omc 가
|
|
42
|
+
// 생겨서 zip 빌드가 깨진 적이 있다. 팀원에게 갈 것만 명시적으로 고른다.
|
|
43
|
+
const files = readdirSync(DIST, { withFileTypes: true })
|
|
44
|
+
.filter((e) => e.isFile() && e.name !== OUT && /\.(bat|txt)$/.test(e.name))
|
|
45
|
+
.map((e) => e.name)
|
|
46
|
+
.sort();
|
|
47
|
+
|
|
48
|
+
if (files.length !== 4) throw new Error(`4개여야 하는데 ${files.length}개: ${files.join(', ')}`);
|
|
49
|
+
const locals = [];
|
|
50
|
+
const central = [];
|
|
51
|
+
let offset = 0;
|
|
52
|
+
|
|
53
|
+
for (const name of files) {
|
|
54
|
+
const body = readFileSync(join(DIST, name));
|
|
55
|
+
if (name.endsWith('.bat')) checkBat(name, body);
|
|
56
|
+
|
|
57
|
+
const nameBuf = Buffer.from(name, 'utf8');
|
|
58
|
+
const crc = crc32(body);
|
|
59
|
+
const flags = 0x0800; // 파일명이 UTF-8 임을 표시
|
|
60
|
+
|
|
61
|
+
const local = Buffer.alloc(30);
|
|
62
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
63
|
+
local.writeUInt16LE(20, 4);
|
|
64
|
+
local.writeUInt16LE(flags, 6);
|
|
65
|
+
local.writeUInt32LE(crc, 14);
|
|
66
|
+
local.writeUInt32LE(body.length, 18);
|
|
67
|
+
local.writeUInt32LE(body.length, 22);
|
|
68
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
69
|
+
locals.push(local, nameBuf, body);
|
|
70
|
+
|
|
71
|
+
const dir = Buffer.alloc(46);
|
|
72
|
+
dir.writeUInt32LE(0x02014b50, 0);
|
|
73
|
+
dir.writeUInt16LE(20, 4);
|
|
74
|
+
dir.writeUInt16LE(20, 6);
|
|
75
|
+
dir.writeUInt16LE(flags, 8);
|
|
76
|
+
dir.writeUInt32LE(crc, 16);
|
|
77
|
+
dir.writeUInt32LE(body.length, 20);
|
|
78
|
+
dir.writeUInt32LE(body.length, 24);
|
|
79
|
+
dir.writeUInt16LE(nameBuf.length, 28);
|
|
80
|
+
dir.writeUInt32LE(offset, 42);
|
|
81
|
+
central.push(dir, nameBuf);
|
|
82
|
+
|
|
83
|
+
offset += 30 + nameBuf.length + body.length;
|
|
84
|
+
console.log(` ${name.padEnd(14)} ${String(body.length).padStart(6)} bytes`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const centralBuf = Buffer.concat(central);
|
|
88
|
+
const end = Buffer.alloc(22);
|
|
89
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
90
|
+
end.writeUInt16LE(files.length, 8);
|
|
91
|
+
end.writeUInt16LE(files.length, 10);
|
|
92
|
+
end.writeUInt32LE(centralBuf.length, 12);
|
|
93
|
+
end.writeUInt32LE(offset, 16);
|
|
94
|
+
|
|
95
|
+
const zip = Buffer.concat([...locals, centralBuf, end]);
|
|
96
|
+
writeFileSync(join(DIST, OUT), zip);
|
|
97
|
+
console.log(`\n${OUT}: ${files.length}개 파일, ${zip.length} bytes`);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* 브라우저:
|
|
25
25
|
* 기본 — .chrome-profile 로 Chrome 을 직접 띄운다. 그 프로필로 이미 Chrome 이
|
|
26
26
|
* 떠 있으면(예: chrome-devtools MCP) 프로필이 잠겨 실패한다. 그때는
|
|
27
|
-
* --browser-url http://127.0.0.1:
|
|
27
|
+
* --browser-url http://127.0.0.1:19222 로 떠 있는 Chrome 에 붙어라.
|
|
28
28
|
*/
|
|
29
29
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'node:fs';
|
|
30
30
|
import { join, resolve, dirname } from 'node:path';
|
|
@@ -35,7 +35,12 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
35
35
|
const BASE_W = 1080, BASE_H = 1350, SCALE = 2; // openai-image-gen.js / html-carousel-gen.js 와 동일
|
|
36
36
|
const CHATGPT = 'https://chatgpt.com/';
|
|
37
37
|
const PROFILE = join(REPO_ROOT, '.chrome-profile');
|
|
38
|
-
|
|
38
|
+
// 빌더 전용 CDP 포트. bin/sns-auto-builder.mjs 의 CDP_PORT 와 같은 값이어야 한다.
|
|
39
|
+
// 9222(크롬 디버깅 관례 포트)를 쓰다가, 사용자 본인 크롬이 127.0.0.1:9222 를 잡고
|
|
40
|
+
// 빌더 크롬은 ::1:9222 만 잡은 적이 있다. 그 상태에서 여기가 127.0.0.1 로 붙으면
|
|
41
|
+
// **사용자 개인 크롬을 조종하게 된다.** 관례 포트를 피하는 이유다.
|
|
42
|
+
const CDP_PORT = process.env.SNS_BUILDER_CDP_PORT || '19222';
|
|
43
|
+
const DEBUG_URL = `http://127.0.0.1:${CDP_PORT}`;
|
|
39
44
|
|
|
40
45
|
// 이미지 안에 한글이 들어가므로 정확도 가드를 붙인다 (openai-image-gen.js 의 KOREAN 과 같은 역할).
|
|
41
46
|
const KOREAN = '한글은 오타 없이 정확히 그대로 렌더링할 것. 지정하지 않은 문구는 넣지 말 것.';
|
package/server.mjs
CHANGED
|
@@ -902,6 +902,22 @@ ${context || '(없음)'}
|
|
|
902
902
|
.end(readFileSync(LOGO_SVG));
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
+
// 작업 폴더 위치를 알려주고, 탐색기로 열어준다.
|
|
906
|
+
// 데이터를 bat 옆이 아니라 안정된 위치에 두는 대신, 찾기는 쉬워야 한다.
|
|
907
|
+
if (req.method === 'GET' && req.url === '/home') {
|
|
908
|
+
return json(200, { root: ROOT, output: join(ROOT, 'output') });
|
|
909
|
+
}
|
|
910
|
+
if (req.method === 'POST' && req.url === '/home/open') {
|
|
911
|
+
const target = join(ROOT, 'output');
|
|
912
|
+
try { mkdirSync(target, { recursive: true }); } catch { /* 이미 있으면 그만 */ }
|
|
913
|
+
const cmd = process.platform === 'win32' ? ['explorer', [target]]
|
|
914
|
+
: process.platform === 'darwin' ? ['open', [target]] : ['xdg-open', [target]];
|
|
915
|
+
// explorer 는 성공해도 0 이 아닌 코드를 내는 일이 있어 종료코드를 보지 않는다.
|
|
916
|
+
spawn(cmd[0], cmd[1], { detached: true, stdio: 'ignore' }).on('error', () => {}).unref();
|
|
917
|
+
log('작업 폴더 열기', target);
|
|
918
|
+
return json(200, { ok: true, path: target });
|
|
919
|
+
}
|
|
920
|
+
|
|
905
921
|
// 브랜드 설정 — 스키마와 현재 값을 같이 준다 (화면이 스키마로 폼을 그린다)
|
|
906
922
|
if (req.method === 'GET' && req.url === '/brand') {
|
|
907
923
|
return json(200, { schema: JSON.parse(read('brand.schema.json')), values: readBrand(), logo: hasLogo() });
|