@saeroon/cli 0.2.3 → 0.2.5
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 +21 -3
- package/dist/index.js +397 -8
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ npx @saeroon/cli deploy --target production
|
|
|
54
54
|
| `add` | schema.json에 블록 추가 (기본 props + 부모 children 자동 연결) |
|
|
55
55
|
| `upload` | 이미지를 Saeroon CDN에 업로드. `--replace-in`으로 경로 자동 교체 |
|
|
56
56
|
| `generate` | AI로 사이트 스키마 생성 (`--ref` URL 분석 / `--prompt` 텍스트) |
|
|
57
|
+
| `analyze` | 레퍼런스 URL 분석 (스크린샷 + DOM/CSS + 비디오 감지 + Pexels 스톡 매칭) |
|
|
57
58
|
| `compare` | 레퍼런스 ↔ 프리뷰 시각 비교 (Playwright 스크린샷 diff) |
|
|
58
59
|
|
|
59
60
|
### Deployment
|
|
@@ -89,9 +90,9 @@ init ──→ preview ──→ validate ──→ deploy --target staging
|
|
|
89
90
|
### AI-Assisted Creation
|
|
90
91
|
|
|
91
92
|
```
|
|
92
|
-
generate --ref <url> ──→ validate --local ──→
|
|
93
|
-
|
|
94
|
-
generate --prompt <text>
|
|
93
|
+
analyze <url> ──→ generate --ref <url> ──→ validate --local ──→ preview
|
|
94
|
+
(스크린샷 + or │
|
|
95
|
+
비디오 매칭) generate --prompt <text> upload ./assets/
|
|
95
96
|
--replace-in schema.json
|
|
96
97
|
│
|
|
97
98
|
compare --ref ──→ deploy
|
|
@@ -197,6 +198,10 @@ npx @saeroon/cli deploy --dry-run
|
|
|
197
198
|
~/.saeroon/config.json ← API 키, 기본 설정 (login으로 생성)
|
|
198
199
|
./saeroon.config.json ← 프로젝트별 siteId, templateId (init으로 생성)
|
|
199
200
|
./schema.json ← 사이트 스키마 정의
|
|
201
|
+
|
|
202
|
+
# 환경변수
|
|
203
|
+
SAEROON_API_KEY ← Saeroon API Key
|
|
204
|
+
PEXELS_API_KEY ← Pexels API Key (비디오 스톡 검색, 무료 — pexels.com/api)
|
|
200
205
|
```
|
|
201
206
|
|
|
202
207
|
## Block Scaffolding
|
|
@@ -214,6 +219,19 @@ npx @saeroon/cli add image-block --parent root --after hero-1
|
|
|
214
219
|
npx @saeroon/cli add content-showcase --id insights --parent main --page about
|
|
215
220
|
```
|
|
216
221
|
|
|
222
|
+
## Reference Analysis
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# 레퍼런스 URL 분석 (스크린샷 4장 + DOM/CSS + 비디오 감지)
|
|
226
|
+
npx @saeroon/cli analyze https://example.com
|
|
227
|
+
|
|
228
|
+
# 업종 지정 (비디오 검색 키워드에 사용)
|
|
229
|
+
npx @saeroon/cli analyze https://example.com --industry cafe
|
|
230
|
+
|
|
231
|
+
# Pexels API Key 설정 시 비디오 자동 매칭 (video-stock-map.json 생성)
|
|
232
|
+
# 설정: PEXELS_API_KEY 환경변수 또는 .saeroon/config.json의 pexelsApiKey
|
|
233
|
+
```
|
|
234
|
+
|
|
217
235
|
## AI Generation
|
|
218
236
|
|
|
219
237
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -98,6 +98,12 @@ async function getApiBaseUrl() {
|
|
|
98
98
|
const url = config.apiBaseUrl || DEFAULT_API_BASE_URL;
|
|
99
99
|
return validateApiBaseUrl(url);
|
|
100
100
|
}
|
|
101
|
+
async function resolvePexelsApiKey() {
|
|
102
|
+
const envKey = process.env.PEXELS_API_KEY;
|
|
103
|
+
if (envKey) return envKey;
|
|
104
|
+
const config = await loadConfig();
|
|
105
|
+
return config.pexelsApiKey ?? null;
|
|
106
|
+
}
|
|
101
107
|
var PRIVATE_IP_PATTERNS = [
|
|
102
108
|
/^10\./,
|
|
103
109
|
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
@@ -2468,6 +2474,51 @@ async function commandPreview(schemaPath, options) {
|
|
|
2468
2474
|
import chalk8 from "chalk";
|
|
2469
2475
|
|
|
2470
2476
|
// src/lib/local-validator.ts
|
|
2477
|
+
var ELEMENT_TYPE_PROPS = {
|
|
2478
|
+
"heading-block.level": {
|
|
2479
|
+
validValues: /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]),
|
|
2480
|
+
coerce: (v) => typeof v === "number" && v >= 1 && v <= 6 ? `h${v}` : null
|
|
2481
|
+
},
|
|
2482
|
+
"container.semanticTag": {
|
|
2483
|
+
validValues: /* @__PURE__ */ new Set([
|
|
2484
|
+
"section",
|
|
2485
|
+
"article",
|
|
2486
|
+
"aside",
|
|
2487
|
+
"figure",
|
|
2488
|
+
"figcaption",
|
|
2489
|
+
"ul",
|
|
2490
|
+
"ol",
|
|
2491
|
+
"li",
|
|
2492
|
+
"dl",
|
|
2493
|
+
"dt",
|
|
2494
|
+
"dd",
|
|
2495
|
+
"table",
|
|
2496
|
+
"thead",
|
|
2497
|
+
"tbody",
|
|
2498
|
+
"tfoot",
|
|
2499
|
+
"tr",
|
|
2500
|
+
"td",
|
|
2501
|
+
"th",
|
|
2502
|
+
"details",
|
|
2503
|
+
"summary",
|
|
2504
|
+
"dialog",
|
|
2505
|
+
"form",
|
|
2506
|
+
"fieldset",
|
|
2507
|
+
"legend",
|
|
2508
|
+
"nav",
|
|
2509
|
+
"main",
|
|
2510
|
+
"header",
|
|
2511
|
+
"footer",
|
|
2512
|
+
"address",
|
|
2513
|
+
"blockquote",
|
|
2514
|
+
"pre",
|
|
2515
|
+
"code",
|
|
2516
|
+
"progress",
|
|
2517
|
+
"meter"
|
|
2518
|
+
]),
|
|
2519
|
+
coerce: () => null
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2471
2522
|
var VALID_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
2472
2523
|
// Primitives (10)
|
|
2473
2524
|
"container",
|
|
@@ -2593,6 +2644,42 @@ function validateSchemaLocal(schema) {
|
|
|
2593
2644
|
step: 2
|
|
2594
2645
|
});
|
|
2595
2646
|
}
|
|
2647
|
+
const props = block.props;
|
|
2648
|
+
if (props) {
|
|
2649
|
+
for (const [key, rule] of Object.entries(ELEMENT_TYPE_PROPS)) {
|
|
2650
|
+
const [targetType, propName] = key.split(".");
|
|
2651
|
+
if (blockType !== targetType) continue;
|
|
2652
|
+
const value = props[propName];
|
|
2653
|
+
if (value == null) continue;
|
|
2654
|
+
if (typeof value === "string") {
|
|
2655
|
+
if (!rule.validValues.has(value)) {
|
|
2656
|
+
errors.push({
|
|
2657
|
+
severity: "error",
|
|
2658
|
+
message: `\uBE14\uB85D "${blockId}": ${propName} \uAC12 "${value}"\uC774(\uAC00) \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD5C8\uC6A9: ${[...rule.validValues].join(", ")}`,
|
|
2659
|
+
path: `${blockPath}.props.${propName}`,
|
|
2660
|
+
step: 2
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
} else {
|
|
2664
|
+
const coerced = rule.coerce?.(value);
|
|
2665
|
+
if (coerced && rule.validValues.has(coerced)) {
|
|
2666
|
+
errors.push({
|
|
2667
|
+
severity: "warning",
|
|
2668
|
+
message: `\uBE14\uB85D "${blockId}": ${propName} \uAC12 ${JSON.stringify(value)}\uC740(\uB294) \uBB38\uC790\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4. "${coerced}"\uB85C \uBCC0\uD658\uD558\uC138\uC694. (\uB80C\uB354\uB9C1 \uD06C\uB798\uC2DC \uC704\uD5D8)`,
|
|
2669
|
+
path: `${blockPath}.props.${propName}`,
|
|
2670
|
+
step: 2
|
|
2671
|
+
});
|
|
2672
|
+
} else {
|
|
2673
|
+
errors.push({
|
|
2674
|
+
severity: "error",
|
|
2675
|
+
message: `\uBE14\uB85D "${blockId}": ${propName} \uAC12 ${JSON.stringify(value)}\uC740(\uB294) \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uBB38\uC790\uC5F4\uC774\uC5B4\uC57C \uD558\uBA70 \uD5C8\uC6A9: ${[...rule.validValues].join(", ")}`,
|
|
2676
|
+
path: `${blockPath}.props.${propName}`,
|
|
2677
|
+
step: 2
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2596
2683
|
if (block.children && Array.isArray(block.children)) {
|
|
2597
2684
|
for (const childId of block.children) {
|
|
2598
2685
|
if (typeof childId === "string" && !blocks[childId]) {
|
|
@@ -3827,7 +3914,7 @@ async function commandGenerate(options) {
|
|
|
3827
3914
|
// src/commands/analyze.ts
|
|
3828
3915
|
import chalk15 from "chalk";
|
|
3829
3916
|
import { resolve as resolve13, join as join2 } from "path";
|
|
3830
|
-
import { mkdir as mkdir4 } from "fs/promises";
|
|
3917
|
+
import { mkdir as mkdir4, writeFile as writeFile8 } from "fs/promises";
|
|
3831
3918
|
|
|
3832
3919
|
// src/scripts/analyze-reference.ts
|
|
3833
3920
|
import { spawnSync } from "child_process";
|
|
@@ -4153,12 +4240,94 @@ var EXTRACTION_SCRIPT = `
|
|
|
4153
4240
|
});
|
|
4154
4241
|
});
|
|
4155
4242
|
|
|
4156
|
-
// \u2500\u2500 5.
|
|
4243
|
+
// \u2500\u2500 5. Videos \u2500\u2500
|
|
4244
|
+
const videoEls = document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="vimeo"], iframe[data-src*="youtube"], iframe[data-src*="vimeo"]');
|
|
4245
|
+
const videos = [];
|
|
4246
|
+
|
|
4247
|
+
videoEls.forEach(el => {
|
|
4248
|
+
const tag = el.tagName.toLowerCase();
|
|
4249
|
+
let src = '';
|
|
4250
|
+
let type = 'inline';
|
|
4251
|
+
let platform = 'unknown';
|
|
4252
|
+
let autoplay = false;
|
|
4253
|
+
let loop = false;
|
|
4254
|
+
let muted = false;
|
|
4255
|
+
let posterSrc = '';
|
|
4256
|
+
|
|
4257
|
+
if (tag === 'video') {
|
|
4258
|
+
src = el.src || el.querySelector('source')?.src || el.dataset.src || '';
|
|
4259
|
+
posterSrc = el.poster || '';
|
|
4260
|
+
autoplay = el.hasAttribute('autoplay');
|
|
4261
|
+
loop = el.hasAttribute('loop');
|
|
4262
|
+
muted = el.hasAttribute('muted');
|
|
4263
|
+
platform = 'direct';
|
|
4264
|
+
|
|
4265
|
+
// background \uBE44\uB514\uC624 \uD310\uC815: autoplay+muted+loop \uB610\uB294 CSS\uB85C \uBC30\uACBD \uCC98\uB9AC\uB41C \uACBD\uC6B0
|
|
4266
|
+
if ((autoplay && muted) || el.closest('[class*=hero], [class*=banner], [class*=bg-video], [class*=video-bg], [class*=background]')) {
|
|
4267
|
+
type = 'background';
|
|
4268
|
+
}
|
|
4269
|
+
} else if (tag === 'iframe') {
|
|
4270
|
+
src = el.src || el.dataset.src || '';
|
|
4271
|
+
if (/youtube.com|youtu.be/.test(src)) {
|
|
4272
|
+
platform = 'youtube';
|
|
4273
|
+
type = 'embed';
|
|
4274
|
+
// YouTube autoplay \uD30C\uB77C\uBBF8\uD130 \uAC10\uC9C0
|
|
4275
|
+
autoplay = /autoplay=1/.test(src);
|
|
4276
|
+
muted = /mute=1/.test(src);
|
|
4277
|
+
loop = /loop=1/.test(src);
|
|
4278
|
+
// YouTube thumbnail \uCD94\uCD9C
|
|
4279
|
+
const ytMatch = src.match(/(?:embed\\/|v=|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/);
|
|
4280
|
+
if (ytMatch) posterSrc = 'https://img.youtube.com/vi/' + ytMatch[1] + '/hqdefault.jpg';
|
|
4281
|
+
} else if (/vimeo.com/.test(src)) {
|
|
4282
|
+
platform = 'vimeo';
|
|
4283
|
+
type = 'embed';
|
|
4284
|
+
autoplay = /autoplay=1/.test(src);
|
|
4285
|
+
muted = /muted=1/.test(src);
|
|
4286
|
+
loop = /loop=1/.test(src);
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
if (!src) return;
|
|
4291
|
+
|
|
4292
|
+
const rect = el.getBoundingClientRect();
|
|
4293
|
+
const width = Math.round(rect.width) || 0;
|
|
4294
|
+
const height = Math.round(rect.height) || 0;
|
|
4295
|
+
|
|
4296
|
+
// \uBD80\uBAA8 \uC139\uC158 \uC5ED\uD560 \uCD94\uB860
|
|
4297
|
+
const parentSection = el.closest('section, header, footer, main, [class*=hero], [class*=banner]');
|
|
4298
|
+
const position = parentSection ? inferSectionRole(parentSection) : 'unknown';
|
|
4299
|
+
|
|
4300
|
+
// \uC8FC\uBCC0 \uD14D\uC2A4\uD2B8\uC5D0\uC11C \uB9E5\uB77D \uCD94\uCD9C
|
|
4301
|
+
const nearby = (el.parentElement?.textContent || '').trim().slice(0, 150);
|
|
4302
|
+
const title = el.getAttribute('title') || el.getAttribute('aria-label') || '';
|
|
4303
|
+
const context = (title || nearby).slice(0, 150);
|
|
4304
|
+
|
|
4305
|
+
// background \uBE44\uB514\uC624 \uC911 full-width\uC778 \uACBD\uC6B0 \uBCF4\uAC15
|
|
4306
|
+
if (type === 'inline' && width > window.innerWidth * 0.9 && height > 300) {
|
|
4307
|
+
type = 'background';
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
videos.push({
|
|
4311
|
+
src: src.slice(0, 500),
|
|
4312
|
+
type,
|
|
4313
|
+
platform,
|
|
4314
|
+
aspectRatio: guessAspectRatio(width, height),
|
|
4315
|
+
posterSrc: posterSrc.slice(0, 500),
|
|
4316
|
+
position,
|
|
4317
|
+
context: context.slice(0, 150),
|
|
4318
|
+
autoplay,
|
|
4319
|
+
loop,
|
|
4320
|
+
muted,
|
|
4321
|
+
});
|
|
4322
|
+
});
|
|
4323
|
+
|
|
4324
|
+
// \u2500\u2500 6. Gaps (V2 \uBE14\uB85D\uC73C\uB85C \uB9E4\uD551 \uBD88\uAC00\uB2A5\uD55C \uAE30\uB2A5) \u2500\u2500
|
|
4157
4325
|
const gaps = [];
|
|
4158
4326
|
|
|
4159
|
-
// video background
|
|
4160
|
-
|
|
4161
|
-
|
|
4327
|
+
// video background (\uBE44\uB514\uC624 \uAC10\uC9C0\uB294 \uC139\uC158 5\uC5D0\uC11C \uCC98\uB9AC, gap\uC5D0\uB294 \uBBF8\uC9C0\uC6D0 \uD328\uD134\uB9CC \uAE30\uB85D)
|
|
4328
|
+
const hasScrollScrubVideo = document.querySelector('video[data-scroll-scrub], [class*=scroll-video]');
|
|
4329
|
+
if (hasScrollScrubVideo) {
|
|
4330
|
+
gaps.push('video-scroll-scrub: \uC2A4\uD06C\uB864 \uC5F0\uB3D9 \uBE44\uB514\uC624 \uC7AC\uC0DD (scroll scrub)');
|
|
4162
4331
|
}
|
|
4163
4332
|
// canvas / WebGL
|
|
4164
4333
|
if (document.querySelector('canvas')) {
|
|
@@ -4240,6 +4409,7 @@ var EXTRACTION_SCRIPT = `
|
|
|
4240
4409
|
detectedTransitions: [...transitionProps].slice(0, 20),
|
|
4241
4410
|
},
|
|
4242
4411
|
images,
|
|
4412
|
+
videos,
|
|
4243
4413
|
gaps,
|
|
4244
4414
|
};
|
|
4245
4415
|
})()
|
|
@@ -4371,10 +4541,144 @@ function createFallbackData() {
|
|
|
4371
4541
|
detectedTransitions: []
|
|
4372
4542
|
},
|
|
4373
4543
|
images: [],
|
|
4544
|
+
videos: [],
|
|
4374
4545
|
gaps: []
|
|
4375
4546
|
};
|
|
4376
4547
|
}
|
|
4377
4548
|
|
|
4549
|
+
// src/lib/pexels-client.ts
|
|
4550
|
+
var PexelsClient = class {
|
|
4551
|
+
apiKey;
|
|
4552
|
+
baseUrl = "https://api.pexels.com/videos";
|
|
4553
|
+
constructor(apiKey) {
|
|
4554
|
+
this.apiKey = apiKey;
|
|
4555
|
+
}
|
|
4556
|
+
/**
|
|
4557
|
+
* 키워드로 비디오를 검색합니다.
|
|
4558
|
+
*/
|
|
4559
|
+
async searchVideos(options) {
|
|
4560
|
+
const params = new URLSearchParams({
|
|
4561
|
+
query: options.query,
|
|
4562
|
+
per_page: String(options.perPage ?? 1)
|
|
4563
|
+
});
|
|
4564
|
+
if (options.orientation) params.set("orientation", options.orientation);
|
|
4565
|
+
if (options.minDuration) params.set("min_duration", String(options.minDuration));
|
|
4566
|
+
if (options.maxDuration) params.set("max_duration", String(options.maxDuration));
|
|
4567
|
+
const url = `${this.baseUrl}/search?${params}`;
|
|
4568
|
+
const res = await fetch(url, {
|
|
4569
|
+
headers: { Authorization: this.apiKey }
|
|
4570
|
+
});
|
|
4571
|
+
if (!res.ok) {
|
|
4572
|
+
if (res.status === 429) {
|
|
4573
|
+
throw new Error("Pexels API rate limit \uCD08\uACFC. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.");
|
|
4574
|
+
}
|
|
4575
|
+
throw new Error(`Pexels API \uC624\uB958: ${res.status} ${res.statusText}`);
|
|
4576
|
+
}
|
|
4577
|
+
return res.json();
|
|
4578
|
+
}
|
|
4579
|
+
/**
|
|
4580
|
+
* 키워드로 비디오를 검색하여 최적의 파일 URL을 반환합니다.
|
|
4581
|
+
*
|
|
4582
|
+
* 선택 기준:
|
|
4583
|
+
* 1. HD 품질 우선 (sd < hd < uhd)
|
|
4584
|
+
* 2. mp4 파일 타입 우선
|
|
4585
|
+
* 3. 가로형(landscape) 기본 선호
|
|
4586
|
+
*/
|
|
4587
|
+
async findVideo(options) {
|
|
4588
|
+
const response = await this.searchVideos(options);
|
|
4589
|
+
if (response.videos.length === 0) return null;
|
|
4590
|
+
const video = response.videos[0];
|
|
4591
|
+
const file = pickBestFile(video.video_files);
|
|
4592
|
+
if (!file) return null;
|
|
4593
|
+
return {
|
|
4594
|
+
src: file.link,
|
|
4595
|
+
poster: video.image,
|
|
4596
|
+
pexelsUrl: video.url,
|
|
4597
|
+
width: file.width,
|
|
4598
|
+
height: file.height,
|
|
4599
|
+
duration: video.duration
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
/**
|
|
4603
|
+
* 여러 키워드를 순차적으로 시도하여 첫 번째 매칭되는 비디오를 반환합니다.
|
|
4604
|
+
* 업종 키워드 → 일반 키워드 순으로 fallback.
|
|
4605
|
+
*/
|
|
4606
|
+
async findVideoWithFallback(queries, options) {
|
|
4607
|
+
for (const query of queries) {
|
|
4608
|
+
const result = await this.findVideo({ ...options, query });
|
|
4609
|
+
if (result) return result;
|
|
4610
|
+
}
|
|
4611
|
+
return null;
|
|
4612
|
+
}
|
|
4613
|
+
};
|
|
4614
|
+
var QUALITY_RANK = { sd: 0, hd: 1, uhd: 2 };
|
|
4615
|
+
function pickBestFile(files) {
|
|
4616
|
+
const mp4Files = files.filter((f) => f.file_type === "video/mp4");
|
|
4617
|
+
const pool = mp4Files.length > 0 ? mp4Files : files;
|
|
4618
|
+
return pool.sort((a, b) => {
|
|
4619
|
+
const qualityDiff = (QUALITY_RANK[b.quality] ?? 0) - (QUALITY_RANK[a.quality] ?? 0);
|
|
4620
|
+
if (qualityDiff !== 0) return qualityDiff;
|
|
4621
|
+
if (a.quality === "uhd" && b.quality !== "uhd") return 1;
|
|
4622
|
+
if (b.quality === "uhd" && a.quality !== "uhd") return -1;
|
|
4623
|
+
return b.width * b.height - a.width * a.height;
|
|
4624
|
+
})[0];
|
|
4625
|
+
}
|
|
4626
|
+
function buildVideoSearchQueries(industry, sectionRole, videoType) {
|
|
4627
|
+
const queries = [];
|
|
4628
|
+
const industryKeywords = {
|
|
4629
|
+
// 외식
|
|
4630
|
+
cafe: ["cafe interior", "coffee shop", "barista coffee"],
|
|
4631
|
+
restaurant: ["restaurant kitchen", "dining food", "chef cooking"],
|
|
4632
|
+
bakery: ["bakery bread", "pastry baking", "fresh bread"],
|
|
4633
|
+
bar: ["cocktail bar", "bartender mixing", "bar nightlife"],
|
|
4634
|
+
// 뷰티/건강
|
|
4635
|
+
salon: ["hair salon", "beauty salon", "hairdresser styling"],
|
|
4636
|
+
spa: ["spa massage", "wellness relaxation", "spa treatment"],
|
|
4637
|
+
gym: ["fitness gym", "workout training", "gym exercise"],
|
|
4638
|
+
clinic: ["medical clinic", "healthcare professional", "doctor clinic"],
|
|
4639
|
+
dental: ["dental clinic", "dentist office", "dental care"],
|
|
4640
|
+
dermatology: ["skincare treatment", "dermatology clinic", "skin care"],
|
|
4641
|
+
// 전문 서비스
|
|
4642
|
+
law: ["law office", "legal professional", "law firm"],
|
|
4643
|
+
accounting: ["accounting office", "financial planning", "business meeting"],
|
|
4644
|
+
consulting: ["business consulting", "corporate meeting", "professional office"],
|
|
4645
|
+
realestate: ["real estate", "property tour", "house interior"],
|
|
4646
|
+
// 교육
|
|
4647
|
+
academy: ["classroom learning", "education study", "student learning"],
|
|
4648
|
+
kindergarten: ["children playing", "kids education", "nursery school"],
|
|
4649
|
+
// 소매/서비스
|
|
4650
|
+
shop: ["retail store", "shopping", "product display"],
|
|
4651
|
+
flower: ["flower arrangement", "florist shop", "floral bouquet"],
|
|
4652
|
+
pet: ["pet grooming", "pet care", "cute animals"],
|
|
4653
|
+
cleaning: ["cleaning service", "professional cleaning", "clean home"],
|
|
4654
|
+
moving: ["moving service", "packing boxes", "new home"],
|
|
4655
|
+
// 테크
|
|
4656
|
+
tech: ["technology innovation", "digital workspace", "coding computer"],
|
|
4657
|
+
saas: ["software dashboard", "digital technology", "cloud computing"],
|
|
4658
|
+
startup: ["startup team", "innovation workspace", "creative office"]
|
|
4659
|
+
};
|
|
4660
|
+
const keywords = industryKeywords[industry] ?? [`${industry} professional`, `${industry} business`];
|
|
4661
|
+
if (videoType === "background") {
|
|
4662
|
+
queries.push(...keywords);
|
|
4663
|
+
if (sectionRole === "hero") {
|
|
4664
|
+
queries.push(`${industry} aerial`, `${industry} cinematic`);
|
|
4665
|
+
}
|
|
4666
|
+
queries.push("abstract background loop", "minimal background");
|
|
4667
|
+
} else {
|
|
4668
|
+
if (sectionRole === "hero") {
|
|
4669
|
+
queries.push(...keywords);
|
|
4670
|
+
} else if (sectionRole === "features" || sectionRole === "gallery") {
|
|
4671
|
+
queries.push(...keywords.slice(0, 2));
|
|
4672
|
+
} else if (sectionRole === "testimonials") {
|
|
4673
|
+
queries.push("people talking", "customer interview");
|
|
4674
|
+
} else {
|
|
4675
|
+
queries.push(...keywords.slice(0, 1));
|
|
4676
|
+
}
|
|
4677
|
+
queries.push(`${industry} video`);
|
|
4678
|
+
}
|
|
4679
|
+
return queries;
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4378
4682
|
// src/commands/analyze.ts
|
|
4379
4683
|
async function commandAnalyze(url, options) {
|
|
4380
4684
|
console.log(chalk15.bold("\n\uB808\uD37C\uB7F0\uC2A4 \uBD84\uC11D (Reference Analysis)\n"));
|
|
@@ -4447,6 +4751,42 @@ async function commandAnalyze(url, options) {
|
|
|
4447
4751
|
Object.entries(roleCount).forEach(([role, count]) => {
|
|
4448
4752
|
console.log(chalk15.dim(` ${role}: ${count}\uAC1C`));
|
|
4449
4753
|
});
|
|
4754
|
+
console.log("");
|
|
4755
|
+
console.log(chalk15.bold("\uBE44\uB514\uC624"));
|
|
4756
|
+
if (analysis.videos.length > 0) {
|
|
4757
|
+
const typeCount = {};
|
|
4758
|
+
analysis.videos.forEach((v) => {
|
|
4759
|
+
typeCount[v.type] = (typeCount[v.type] || 0) + 1;
|
|
4760
|
+
});
|
|
4761
|
+
console.log(chalk15.dim(` \uCD1D ${analysis.videos.length}\uAC1C \uAC10\uC9C0`));
|
|
4762
|
+
Object.entries(typeCount).forEach(([type, count]) => {
|
|
4763
|
+
console.log(chalk15.dim(` ${type}: ${count}\uAC1C`));
|
|
4764
|
+
});
|
|
4765
|
+
const pexelsApiKey = await resolvePexelsApiKey();
|
|
4766
|
+
if (pexelsApiKey) {
|
|
4767
|
+
const videoSpinner = spinner("Pexels\uC5D0\uC11C \uC2A4\uD1A1 \uBE44\uB514\uC624 \uAC80\uC0C9 \uC911...");
|
|
4768
|
+
try {
|
|
4769
|
+
const pexels = new PexelsClient(pexelsApiKey);
|
|
4770
|
+
const industry = options.industry ?? inferIndustry(analysis);
|
|
4771
|
+
const resolved = await resolveVideos(pexels, analysis.videos, industry);
|
|
4772
|
+
videoSpinner.stop(chalk15.green(` ${resolved.length}/${analysis.videos.length}\uAC1C \uC2A4\uD1A1 \uBE44\uB514\uC624 \uB9E4\uCE6D \uC644\uB8CC`));
|
|
4773
|
+
resolved.forEach(({ video, stock }) => {
|
|
4774
|
+
console.log(chalk15.dim(` [${video.type}] ${video.position} \u2192 ${stock.src.slice(0, 80)}...`));
|
|
4775
|
+
});
|
|
4776
|
+
const videoMapPath = join2(outputDir, "video-stock-map.json");
|
|
4777
|
+
await writeFile8(videoMapPath, JSON.stringify(resolved, null, 2), "utf-8");
|
|
4778
|
+
console.log(chalk15.dim(` \uB9E4\uCE6D \uACB0\uACFC: ${videoMapPath}`));
|
|
4779
|
+
} catch (error) {
|
|
4780
|
+
videoSpinner.stop(chalk15.yellow(" Pexels \uAC80\uC0C9 \uC2E4\uD328 (\uBE44\uB514\uC624\uB294 \uC218\uB3D9 \uAD50\uCCB4 \uD544\uC694)"));
|
|
4781
|
+
console.log(chalk15.dim(` ${error instanceof Error ? error.message : String(error)}`));
|
|
4782
|
+
}
|
|
4783
|
+
} else {
|
|
4784
|
+
console.log(chalk15.dim(" PEXELS_API_KEY \uBBF8\uC124\uC815 \u2192 \uC2A4\uD1A1 \uBE44\uB514\uC624 \uC790\uB3D9 \uB9E4\uCE6D \uAC74\uB108\uB700"));
|
|
4785
|
+
console.log(chalk15.dim(" \uC124\uC815: PEXELS_API_KEY \uD658\uACBD\uBCC0\uC218 \uB610\uB294 .saeroon/config.json\uC758 pexelsApiKey"));
|
|
4786
|
+
}
|
|
4787
|
+
} else {
|
|
4788
|
+
console.log(chalk15.dim(" \uBE44\uB514\uC624 \uC5C6\uC74C"));
|
|
4789
|
+
}
|
|
4450
4790
|
if (analysis.gaps.length > 0) {
|
|
4451
4791
|
console.log("");
|
|
4452
4792
|
console.log(chalk15.bold(chalk15.yellow("Gap \uAC10\uC9C0")));
|
|
@@ -4474,10 +4814,59 @@ ${error instanceof Error ? error.message : String(error)}`));
|
|
|
4474
4814
|
process.exit(1);
|
|
4475
4815
|
}
|
|
4476
4816
|
}
|
|
4817
|
+
async function resolveVideos(pexels, videos, industry) {
|
|
4818
|
+
const results = [];
|
|
4819
|
+
for (const video of videos) {
|
|
4820
|
+
const queries = buildVideoSearchQueries(industry, video.position, video.type);
|
|
4821
|
+
const orientation = video.type === "background" ? "landscape" : void 0;
|
|
4822
|
+
const maxDuration = video.type === "background" ? 30 : 60;
|
|
4823
|
+
const stock = await pexels.findVideoWithFallback(queries, {
|
|
4824
|
+
orientation,
|
|
4825
|
+
maxDuration
|
|
4826
|
+
});
|
|
4827
|
+
if (stock) {
|
|
4828
|
+
results.push({ video, stock });
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
return results;
|
|
4832
|
+
}
|
|
4833
|
+
function inferIndustry(analysis) {
|
|
4834
|
+
const text = analysis.structure.headingHierarchy.join(" ").toLowerCase();
|
|
4835
|
+
const roles = analysis.structure.sections.map((s) => s.role).join(" ");
|
|
4836
|
+
const combined = text + " " + roles;
|
|
4837
|
+
const industryPatterns = [
|
|
4838
|
+
[/카페|coffee|cafe|커피/, "cafe"],
|
|
4839
|
+
[/레스토랑|restaurant|맛집|음식점|식당/, "restaurant"],
|
|
4840
|
+
[/베이커리|bakery|빵|제과/, "bakery"],
|
|
4841
|
+
[/바|bar|칵테일|cocktail|pub/, "bar"],
|
|
4842
|
+
[/미용|salon|헤어|hair|뷰티|beauty/, "salon"],
|
|
4843
|
+
[/스파|spa|마사지|massage|웰니스/, "spa"],
|
|
4844
|
+
[/피트니스|gym|헬스|fitness|운동/, "gym"],
|
|
4845
|
+
[/병원|clinic|의원|진료|medical/, "clinic"],
|
|
4846
|
+
[/치과|dental|dentist/, "dental"],
|
|
4847
|
+
[/피부|derma|skin|에스테틱/, "dermatology"],
|
|
4848
|
+
[/법률|law|변호사|attorney|법무/, "law"],
|
|
4849
|
+
[/회계|accounting|세무|tax/, "accounting"],
|
|
4850
|
+
[/컨설팅|consulting|자문/, "consulting"],
|
|
4851
|
+
[/부동산|real\s*estate|property|공인중개/, "realestate"],
|
|
4852
|
+
[/학원|academy|교육|education|학습/, "academy"],
|
|
4853
|
+
[/어린이집|유치원|kindergarten|nursery/, "kindergarten"],
|
|
4854
|
+
[/꽃|flower|florist|플라워/, "flower"],
|
|
4855
|
+
[/반려|pet|동물|animal/, "pet"],
|
|
4856
|
+
[/청소|cleaning|클리닝/, "cleaning"],
|
|
4857
|
+
[/이사|moving|포장이사/, "moving"],
|
|
4858
|
+
[/saas|software|플랫폼|platform/, "saas"],
|
|
4859
|
+
[/startup|스타트업/, "startup"]
|
|
4860
|
+
];
|
|
4861
|
+
for (const [pattern, industry] of industryPatterns) {
|
|
4862
|
+
if (pattern.test(combined)) return industry;
|
|
4863
|
+
}
|
|
4864
|
+
return "business";
|
|
4865
|
+
}
|
|
4477
4866
|
|
|
4478
4867
|
// src/commands/compare.ts
|
|
4479
4868
|
import chalk16 from "chalk";
|
|
4480
|
-
import { writeFile as
|
|
4869
|
+
import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
|
|
4481
4870
|
import { resolve as resolve14, join as join3 } from "path";
|
|
4482
4871
|
import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
|
|
4483
4872
|
async function commandCompare(options) {
|
|
@@ -4565,7 +4954,7 @@ async function runMultiViewportCompare(options) {
|
|
|
4565
4954
|
overallDiffPercentage: Math.round(overallDiff * 10) / 10
|
|
4566
4955
|
};
|
|
4567
4956
|
const reportPath = join3(outputDir, "comparison-report.json");
|
|
4568
|
-
await
|
|
4957
|
+
await writeFile9(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
4569
4958
|
console.log("");
|
|
4570
4959
|
console.log(chalk16.bold("\uBE44\uAD50 \uC694\uC57D"));
|
|
4571
4960
|
for (const r of results) {
|
|
@@ -5460,7 +5849,7 @@ program.command("add-pattern").description("\uC2A4\uD0A4\uB9C8\uC5D0 \uD328\uD13
|
|
|
5460
5849
|
program.command("fork-pattern").description("\uACF5\uAC1C \uD328\uD134\uC744 \uB0B4 \uC0AC\uC774\uD2B8\uB85C \uD3EC\uD06C").argument("<pattern-id>", "\uD3EC\uD06C\uD560 \uACF5\uAC1C \uD328\uD134 ID").requiredOption("--site-id <id>", "\uB300\uC0C1 \uC0AC\uC774\uD2B8 ID").action(commandForkPattern);
|
|
5461
5850
|
program.command("add").description("schema.json\uC5D0 \uBE14\uB85D \uCD94\uAC00 (\uAE30\uBCF8 props \uD3EC\uD568)").argument("<block-type>", "\uCD94\uAC00\uD560 \uBE14\uB85D \uD0C0\uC785 (e.g., heading-block, image-block)").option("--id <id>", "\uBE14\uB85D ID (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uC0DD\uC131)").option("--parent <parentId>", "\uBD80\uBAA8 \uBE14\uB85D ID").option("--after <siblingId>", "\uC0BD\uC785 \uC704\uCE58 (\uD615\uC81C \uBE14\uB85D \uB4A4)").option("--page <pageId>", "\uB300\uC0C1 \uD398\uC774\uC9C0 ID").action(commandAdd);
|
|
5462
5851
|
program.command("generate").description("AI\uB85C \uC0AC\uC774\uD2B8 \uC2A4\uD0A4\uB9C8 \uC0DD\uC131").option("--ref <url>", "\uB808\uD37C\uB7F0\uC2A4 URL (\uC0AC\uC774\uD2B8\uB97C \uBD84\uC11D\uD558\uC5EC \uC720\uC0AC\uD55C \uC2A4\uD0A4\uB9C8 \uC0DD\uC131)").option("--prompt <text>", "\uD504\uB86C\uD504\uD2B8 \uD14D\uC2A4\uD2B8 (\uC124\uBA85 \uAE30\uBC18 \uC0DD\uC131)").option("--output <file>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C", "schema.json").option("--api-key <key>", "API Key").action(commandGenerate);
|
|
5463
|
-
program.command("analyze").description("\uB808\uD37C\uB7F0\uC2A4 URL \uBD84\uC11D (\uC2A4\uD06C\uB9B0\uC0F7 4\uC7A5 + DOM/CSS \uCD94\uCD9C + \uC778\uD130\uB799\uC158 \uAC10\uC9C0)").argument("<url>", "\uBD84\uC11D\uD560 \uB808\uD37C\uB7F0\uC2A4 URL").option("--output-dir <dir>", "\uBD84\uC11D \uACB0\uACFC \uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC").option("--timeout <ms>", "\uD398\uC774\uC9C0 \uB85C\uB4DC \uD0C0\uC784\uC544\uC6C3 (ms)", "30000").action(commandAnalyze);
|
|
5852
|
+
program.command("analyze").description("\uB808\uD37C\uB7F0\uC2A4 URL \uBD84\uC11D (\uC2A4\uD06C\uB9B0\uC0F7 4\uC7A5 + DOM/CSS \uCD94\uCD9C + \uBE44\uB514\uC624 \uAC10\uC9C0 + \uC778\uD130\uB799\uC158 \uAC10\uC9C0)").argument("<url>", "\uBD84\uC11D\uD560 \uB808\uD37C\uB7F0\uC2A4 URL").option("--output-dir <dir>", "\uBD84\uC11D \uACB0\uACFC \uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC").option("--timeout <ms>", "\uD398\uC774\uC9C0 \uB85C\uB4DC \uD0C0\uC784\uC544\uC6C3 (ms)", "30000").option("--industry <type>", "\uC5C5\uC885 (cafe, restaurant, salon, law \uB4F1 \u2014 \uBE44\uB514\uC624 \uAC80\uC0C9 \uD0A4\uC6CC\uB4DC\uC5D0 \uC0AC\uC6A9)").action(commandAnalyze);
|
|
5464
5853
|
program.command("compare").description("\uB808\uD37C\uB7F0\uC2A4 \u2194 \uD504\uB9AC\uBDF0 \uC2DC\uAC01 \uBE44\uAD50 (Playwright \uC2A4\uD06C\uB9B0\uC0F7)").option("--ref <url>", "\uB808\uD37C\uB7F0\uC2A4 URL").option("--preview <url>", "\uD504\uB9AC\uBDF0 URL").option("--output <file>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C (\uB2E8\uC77C \uBDF0\uD3EC\uD2B8)", "compare-result.png").option("--output-dir <dir>", "\uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC (\uBA40\uD2F0 \uBDF0\uD3EC\uD2B8)").option("--width <px>", "\uBDF0\uD3EC\uD2B8 \uB108\uBE44 (\uB2E8\uC77C \uBAA8\uB4DC)", "1280").option("--height <px>", "\uBDF0\uD3EC\uD2B8 \uB192\uC774 (\uB2E8\uC77C \uBAA8\uB4DC)", "800").option("--viewports <list>", "\uBE44\uAD50 \uBDF0\uD3EC\uD2B8: all | mobile,tablet,laptop,desktop").action(commandCompare);
|
|
5465
5854
|
program.command("upload").description("\uC774\uBBF8\uC9C0\uB97C Saeroon CDN\uC5D0 \uC5C5\uB85C\uB4DC").argument("<path>", "\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB610\uB294 \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C").option("--replace-in <file>", "\uC5C5\uB85C\uB4DC \uD6C4 \uD30C\uC77C \uB0B4 \uB85C\uCEEC \uACBD\uB85C\uB97C CDN URL\uB85C \uAD50\uCCB4").option("--site-id <id>", "\uC0AC\uC774\uD2B8 ID").option("--api-key <key>", "API Key").action(commandUpload);
|
|
5466
5855
|
program.command("deploy").description("\uC0AC\uC774\uD2B8\uC5D0 \uC2A4\uD0A4\uB9C8 \uBC30\uD3EC").option("--api-key <key>", "API Key").option("--target <target>", "\uBC30\uD3EC \uB300\uC0C1: staging|production", "staging").option("--dry-run", "\uC2E4\uC81C \uC5C5\uB85C\uB4DC/\uBC30\uD3EC \uC5C6\uC774 \uC5D0\uC14B \uB9AC\uD3EC\uD2B8\uB9CC \uCD9C\uB825").option("--sync-template", "Production \uBC30\uD3EC \uD6C4 \uB9C8\uCF13\uD50C\uB808\uC774\uC2A4 \uD15C\uD50C\uB9BF \uBC84\uC804 \uC790\uB3D9 \uB3D9\uAE30\uD654").action(commandDeploy);
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saeroon/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Saeroon Hosting developer CLI — preview, validate, and deploy sites & templates",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"bin": {
|
|
10
|
-
"saeroon": "dist/index.js"
|
|
10
|
+
"saeroon": "dist/index.js",
|
|
11
|
+
"saeroon-cli": "dist/index.js"
|
|
11
12
|
},
|
|
12
13
|
"license": "MIT",
|
|
13
14
|
"publishConfig": {
|