@saeroon/cli 0.2.2 → 0.2.4
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 +1371 -258
- package/package.json +1 -1
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])\./,
|
|
@@ -388,7 +394,7 @@ var SaeroonApiClient = class _SaeroonApiClient {
|
|
|
388
394
|
await this.request(
|
|
389
395
|
"PUT",
|
|
390
396
|
`/api/v1/hosting/developer/sites/${encodeURIComponent(siteId)}/schema`,
|
|
391
|
-
{ schema },
|
|
397
|
+
{ schemaJson: JSON.stringify(schema), isDraft: true },
|
|
392
398
|
true
|
|
393
399
|
);
|
|
394
400
|
}
|
|
@@ -2095,7 +2101,7 @@ var PreviewClient = class {
|
|
|
2095
2101
|
* POST /api/v1/developer/preview 호출하여 Preview 세션 생성.
|
|
2096
2102
|
*/
|
|
2097
2103
|
async createSession(schema) {
|
|
2098
|
-
const url = `${this.options.apiBaseUrl}/api/v1/developer/preview`;
|
|
2104
|
+
const url = `${this.options.apiBaseUrl}/api/v1/hosting/developer/preview`;
|
|
2099
2105
|
const response = await fetch(url, {
|
|
2100
2106
|
method: "POST",
|
|
2101
2107
|
headers: {
|
|
@@ -2103,7 +2109,7 @@ var PreviewClient = class {
|
|
|
2103
2109
|
Authorization: `Bearer ${this.options.apiKey}`
|
|
2104
2110
|
},
|
|
2105
2111
|
body: JSON.stringify({
|
|
2106
|
-
schema,
|
|
2112
|
+
schemaJson: JSON.stringify(schema),
|
|
2107
2113
|
device: this.options.device
|
|
2108
2114
|
}),
|
|
2109
2115
|
signal: AbortSignal.timeout(3e4)
|
|
@@ -2122,7 +2128,7 @@ var PreviewClient = class {
|
|
|
2122
2128
|
* WebSocket 연결 수립 및 이벤트 핸들러 등록.
|
|
2123
2129
|
*/
|
|
2124
2130
|
connectWebSocket() {
|
|
2125
|
-
return new Promise((
|
|
2131
|
+
return new Promise((resolve15, reject) => {
|
|
2126
2132
|
if (!this.session) {
|
|
2127
2133
|
reject(new Error("\uC138\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2128
2134
|
return;
|
|
@@ -2136,7 +2142,7 @@ var PreviewClient = class {
|
|
|
2136
2142
|
this.retryCount = 0;
|
|
2137
2143
|
this.options.onStatusChange?.(true);
|
|
2138
2144
|
this.startPingInterval();
|
|
2139
|
-
|
|
2145
|
+
resolve15();
|
|
2140
2146
|
});
|
|
2141
2147
|
ws.on("message", (data) => {
|
|
2142
2148
|
this.handleMessage(data);
|
|
@@ -2468,88 +2474,63 @@ 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)
|
|
2524
|
+
"container",
|
|
2473
2525
|
"text-block",
|
|
2474
2526
|
"heading-block",
|
|
2475
2527
|
"button-block",
|
|
2476
2528
|
"image-block",
|
|
2477
|
-
"
|
|
2529
|
+
"embed-block",
|
|
2530
|
+
"icon-block",
|
|
2531
|
+
"input-block",
|
|
2478
2532
|
"divider",
|
|
2479
2533
|
"spacer",
|
|
2480
|
-
"icon-block",
|
|
2481
|
-
"container",
|
|
2482
|
-
// Feature (70)
|
|
2483
|
-
"image-slider",
|
|
2484
|
-
"map-block",
|
|
2485
|
-
"contact-form",
|
|
2486
|
-
"demolition-calculator",
|
|
2487
|
-
"floating-social-widget",
|
|
2488
|
-
"modal-block",
|
|
2489
|
-
"sticky-cta-bar",
|
|
2490
|
-
"product-grid",
|
|
2491
|
-
"cart-widget",
|
|
2492
|
-
"product-gallery",
|
|
2493
|
-
"product-price",
|
|
2494
|
-
"stock-badge",
|
|
2495
|
-
"variant-selector",
|
|
2496
|
-
"product-filter",
|
|
2497
|
-
"product-search",
|
|
2498
|
-
"related-products",
|
|
2499
|
-
"add-to-cart",
|
|
2500
|
-
"cart-contents",
|
|
2501
|
-
"cart-summary",
|
|
2502
|
-
"coupon-input",
|
|
2503
|
-
"checkout-form",
|
|
2504
|
-
"order-confirmation",
|
|
2505
|
-
"order-history",
|
|
2506
|
-
"order-lookup",
|
|
2507
|
-
"review-list",
|
|
2508
|
-
"cart-drawer",
|
|
2509
|
-
"wishlist",
|
|
2510
|
-
"recently-viewed",
|
|
2511
|
-
"booking-calendar",
|
|
2512
|
-
"medical-booking-block",
|
|
2513
|
-
"booking-button",
|
|
2514
|
-
"service-detail-block",
|
|
2515
|
-
"booking-service-list",
|
|
2516
|
-
"booking-service-detail",
|
|
2517
|
-
"booking-checkout",
|
|
2518
|
-
"booking-confirmation",
|
|
2519
|
-
"booking-my-bookings",
|
|
2520
|
-
"booking-staff-list",
|
|
2521
|
-
"booking-guest-cancel",
|
|
2522
|
-
"booking-class-schedule",
|
|
2523
|
-
"booking-course-detail",
|
|
2524
|
-
"booking-course-progress",
|
|
2525
|
-
"booking-resource-calendar",
|
|
2526
|
-
"booking-resource-list",
|
|
2527
|
-
"auth-block",
|
|
2528
|
-
"member-profile-block",
|
|
2529
|
-
"member-only-section",
|
|
2530
|
-
"board-block",
|
|
2531
|
-
"board-detail-block",
|
|
2532
|
-
"faq-accordion",
|
|
2533
|
-
"gallery-block",
|
|
2534
|
-
"before-after-slider",
|
|
2535
|
-
"testimonials-section",
|
|
2536
|
-
"tabs-section",
|
|
2537
|
-
"countdown-timer",
|
|
2538
|
-
"newsletter",
|
|
2539
|
-
"marquee-block",
|
|
2540
|
-
"before-after-gallery",
|
|
2541
|
-
"content-showcase",
|
|
2542
|
-
"staff-showcase",
|
|
2543
|
-
"site-menu",
|
|
2544
|
-
"stats-counter",
|
|
2545
|
-
"scroll-to-top",
|
|
2546
|
-
"anchor-nav",
|
|
2547
|
-
"trademark-search-block",
|
|
2548
|
-
"trademark-detail-block",
|
|
2549
|
-
"nice-class-browser-block",
|
|
2550
|
-
"lottie-block",
|
|
2551
|
-
"model-block",
|
|
2552
|
-
"image-sequence-block",
|
|
2553
2534
|
// Structure (6)
|
|
2554
2535
|
"header-block",
|
|
2555
2536
|
"footer-block",
|
|
@@ -2557,20 +2538,12 @@ var VALID_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
|
2557
2538
|
"main-block",
|
|
2558
2539
|
"aside-block",
|
|
2559
2540
|
"article-block",
|
|
2560
|
-
//
|
|
2561
|
-
"
|
|
2562
|
-
"
|
|
2563
|
-
"
|
|
2564
|
-
"
|
|
2565
|
-
"
|
|
2566
|
-
"logo-cloud",
|
|
2567
|
-
"social-proof",
|
|
2568
|
-
"timeline",
|
|
2569
|
-
"announcement-list",
|
|
2570
|
-
"service-card-grid",
|
|
2571
|
-
"business-hours",
|
|
2572
|
-
"split-landing-hero",
|
|
2573
|
-
"event-banner"
|
|
2541
|
+
// Specialized (5)
|
|
2542
|
+
"site-menu",
|
|
2543
|
+
"floating-action-widget",
|
|
2544
|
+
"sticky-cta-bar",
|
|
2545
|
+
"cookie-consent-bar",
|
|
2546
|
+
"announcement-bar"
|
|
2574
2547
|
]);
|
|
2575
2548
|
function validateSchemaLocal(schema) {
|
|
2576
2549
|
const errors = [];
|
|
@@ -2579,37 +2552,34 @@ function validateSchemaLocal(schema) {
|
|
|
2579
2552
|
return errors;
|
|
2580
2553
|
}
|
|
2581
2554
|
const s = schema;
|
|
2582
|
-
if (!s.
|
|
2583
|
-
errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "
|
|
2555
|
+
if (!s.version) {
|
|
2556
|
+
errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "version"', path: "version", step: 1 });
|
|
2584
2557
|
}
|
|
2585
|
-
if (!s.
|
|
2586
|
-
errors.push({ severity: "
|
|
2558
|
+
if (!s.name) {
|
|
2559
|
+
errors.push({ severity: "warning", message: '\uD544\uB4DC \uB204\uB77D: "name" (\uC0AC\uC774\uD2B8\uBA85)', path: "name", step: 1 });
|
|
2587
2560
|
}
|
|
2588
2561
|
if (!s.pages) {
|
|
2589
2562
|
errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "pages"', path: "pages", step: 1 });
|
|
2590
2563
|
}
|
|
2591
|
-
const version2 = s.
|
|
2564
|
+
const version2 = s.version;
|
|
2592
2565
|
if (version2 && typeof version2 === "string") {
|
|
2593
2566
|
const parts = version2.split(".").map(Number);
|
|
2594
2567
|
if (parts[0] !== 1 || parts[1] !== void 0 && parts[1] < 15) {
|
|
2595
2568
|
errors.push({
|
|
2596
2569
|
severity: "warning",
|
|
2597
|
-
message: `\uC2A4\uD0A4\uB9C8 \uBC84\uC804 "${version2}"\uC774 \uC624\uB798\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uCD5C\uC2E0: 1.
|
|
2570
|
+
message: `\uC2A4\uD0A4\uB9C8 \uBC84\uC804 "${version2}"\uC774 \uC624\uB798\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uCD5C\uC2E0: 1.21.0`,
|
|
2598
2571
|
path: "schemaVersion",
|
|
2599
2572
|
step: 1
|
|
2600
2573
|
});
|
|
2601
2574
|
}
|
|
2602
2575
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
step: 2
|
|
2611
|
-
});
|
|
2612
|
-
}
|
|
2576
|
+
if (s.pages && !Array.isArray(s.pages)) {
|
|
2577
|
+
errors.push({
|
|
2578
|
+
severity: "error",
|
|
2579
|
+
message: "pages\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4. (object map \uD615\uC2DD\uC740 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4)",
|
|
2580
|
+
path: "pages",
|
|
2581
|
+
step: 1
|
|
2582
|
+
});
|
|
2613
2583
|
}
|
|
2614
2584
|
if (!s.pages) return errors;
|
|
2615
2585
|
const pages = s.pages;
|
|
@@ -2674,6 +2644,42 @@ function validateSchemaLocal(schema) {
|
|
|
2674
2644
|
step: 2
|
|
2675
2645
|
});
|
|
2676
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
|
+
}
|
|
2677
2683
|
if (block.children && Array.isArray(block.children)) {
|
|
2678
2684
|
for (const childId of block.children) {
|
|
2679
2685
|
if (typeof childId === "string" && !blocks[childId]) {
|
|
@@ -2923,11 +2929,11 @@ async function computeFileHashes(localRefs) {
|
|
|
2923
2929
|
return results;
|
|
2924
2930
|
}
|
|
2925
2931
|
async function computeSha256(filePath) {
|
|
2926
|
-
return new Promise((
|
|
2932
|
+
return new Promise((resolve15, reject) => {
|
|
2927
2933
|
const hash = createHash("sha256");
|
|
2928
2934
|
const stream = createReadStream(filePath);
|
|
2929
2935
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
2930
|
-
stream.on("end", () =>
|
|
2936
|
+
stream.on("end", () => resolve15(hash.digest("hex")));
|
|
2931
2937
|
stream.on("error", (err) => reject(err));
|
|
2932
2938
|
});
|
|
2933
2939
|
}
|
|
@@ -2976,14 +2982,14 @@ function createConcurrencyLimiter(concurrency) {
|
|
|
2976
2982
|
function next() {
|
|
2977
2983
|
if (queue.length > 0 && active < concurrency) {
|
|
2978
2984
|
active++;
|
|
2979
|
-
const
|
|
2980
|
-
|
|
2985
|
+
const resolve15 = queue.shift();
|
|
2986
|
+
resolve15?.();
|
|
2981
2987
|
}
|
|
2982
2988
|
}
|
|
2983
2989
|
return async function limit(fn) {
|
|
2984
2990
|
if (active >= concurrency) {
|
|
2985
|
-
await new Promise((
|
|
2986
|
-
queue.push(
|
|
2991
|
+
await new Promise((resolve15) => {
|
|
2992
|
+
queue.push(resolve15);
|
|
2987
2993
|
});
|
|
2988
2994
|
} else {
|
|
2989
2995
|
active++;
|
|
@@ -3905,77 +3911,1183 @@ async function commandGenerate(options) {
|
|
|
3905
3911
|
}
|
|
3906
3912
|
}
|
|
3907
3913
|
|
|
3908
|
-
// src/commands/
|
|
3914
|
+
// src/commands/analyze.ts
|
|
3909
3915
|
import chalk15 from "chalk";
|
|
3910
|
-
import { resolve as
|
|
3911
|
-
import {
|
|
3916
|
+
import { resolve as resolve13, join as join2 } from "path";
|
|
3917
|
+
import { mkdir as mkdir4, writeFile as writeFile8 } from "fs/promises";
|
|
3918
|
+
|
|
3919
|
+
// src/scripts/analyze-reference.ts
|
|
3920
|
+
import { spawnSync } from "child_process";
|
|
3921
|
+
import { writeFile as writeFile7, mkdir as mkdir3 } from "fs/promises";
|
|
3922
|
+
import { join } from "path";
|
|
3923
|
+
var VIEWPORTS = [
|
|
3924
|
+
{ name: "mobile", width: 375, height: 812 },
|
|
3925
|
+
{ name: "tablet", width: 768, height: 1024 },
|
|
3926
|
+
{ name: "laptop", width: 1280, height: 800 },
|
|
3927
|
+
{ name: "desktop", width: 1536, height: 900 }
|
|
3928
|
+
];
|
|
3929
|
+
var EXTRACTION_SCRIPT = `
|
|
3930
|
+
(() => {
|
|
3931
|
+
// \u2500\u2500 Helpers \u2500\u2500
|
|
3932
|
+
function getComputedProp(el, prop) {
|
|
3933
|
+
return window.getComputedStyle(el).getPropertyValue(prop).trim();
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
function parsePixel(val) {
|
|
3937
|
+
return Math.round(parseFloat(val) || 0);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
function rgbToHex(rgb) {
|
|
3941
|
+
if (!rgb || rgb === 'transparent') return 'transparent';
|
|
3942
|
+
if (rgb.startsWith('#')) return rgb;
|
|
3943
|
+
const match = rgb.match(/\\d+/g);
|
|
3944
|
+
if (!match || match.length < 3) return rgb;
|
|
3945
|
+
const [r, g, b] = match.map(Number);
|
|
3946
|
+
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
function detectLayout(el) {
|
|
3950
|
+
const display = getComputedProp(el, 'display');
|
|
3951
|
+
const flexDir = getComputedProp(el, 'flex-direction');
|
|
3952
|
+
const position = getComputedProp(el, 'position');
|
|
3953
|
+
if (display.includes('grid')) return 'grid';
|
|
3954
|
+
if (position === 'absolute' || position === 'fixed') return 'absolute';
|
|
3955
|
+
if (display.includes('flex')) {
|
|
3956
|
+
return flexDir === 'row' ? 'flex-row' : 'flex-column';
|
|
3957
|
+
}
|
|
3958
|
+
return 'block';
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
function inferSectionRole(el) {
|
|
3962
|
+
const tag = el.tagName.toLowerCase();
|
|
3963
|
+
const cls = (el.className || '').toString().toLowerCase();
|
|
3964
|
+
const id = (el.id || '').toLowerCase();
|
|
3965
|
+
const text = (el.textContent || '').slice(0, 200).toLowerCase();
|
|
3966
|
+
const combined = tag + ' ' + cls + ' ' + id + ' ' + text;
|
|
3967
|
+
|
|
3968
|
+
if (tag === 'header' || tag === 'nav') return 'header';
|
|
3969
|
+
if (tag === 'footer') return 'footer';
|
|
3970
|
+
if (/hero|banner|jumbotron|splash|main-?visual/.test(combined)) return 'hero';
|
|
3971
|
+
if (/feature|service|benefit|what-?we/.test(combined)) return 'features';
|
|
3972
|
+
if (/testimonial|review|feedback|client|customer/.test(combined)) return 'testimonials';
|
|
3973
|
+
if (/faq|accordion|question|q-?and-?a/.test(combined)) return 'faq';
|
|
3974
|
+
if (/team|staff|member|about-?us|who-?we/.test(combined)) return 'team';
|
|
3975
|
+
if (/pricing|plan|package/.test(combined)) return 'pricing';
|
|
3976
|
+
if (/contact|inquiry|form|cta|call-?to-?action|get-?started/.test(combined)) return 'cta';
|
|
3977
|
+
if (/gallery|portfolio|work|project|showcase/.test(combined)) return 'gallery';
|
|
3978
|
+
if (/blog|news|article|post/.test(combined)) return 'blog';
|
|
3979
|
+
if (/partner|client|logo|brand|trust/.test(combined)) return 'partners';
|
|
3980
|
+
if (/map|location|address|direction/.test(combined)) return 'map';
|
|
3981
|
+
if (/stat|counter|number|achievement/.test(combined)) return 'stats';
|
|
3982
|
+
return 'section';
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
function guessAspectRatio(w, h) {
|
|
3986
|
+
if (!w || !h) return 'unknown';
|
|
3987
|
+
const r = w / h;
|
|
3988
|
+
if (Math.abs(r - 16/9) < 0.15) return '16:9';
|
|
3989
|
+
if (Math.abs(r - 4/3) < 0.15) return '4:3';
|
|
3990
|
+
if (Math.abs(r - 3/2) < 0.15) return '3:2';
|
|
3991
|
+
if (Math.abs(r - 1) < 0.15) return '1:1';
|
|
3992
|
+
if (Math.abs(r - 9/16) < 0.15) return '9:16';
|
|
3993
|
+
if (Math.abs(r - 21/9) < 0.15) return '21:9';
|
|
3994
|
+
return w + ':' + h;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
function inferImageRole(img, parentRole) {
|
|
3998
|
+
const src = (img.src || '').toLowerCase();
|
|
3999
|
+
const alt = (img.alt || '').toLowerCase();
|
|
4000
|
+
const cls = (img.className || '').toString().toLowerCase();
|
|
4001
|
+
const parent = img.closest('section, header, footer, [class*=hero], [class*=banner]');
|
|
4002
|
+
const pRole = parentRole || inferSectionRole(parent || img.parentElement);
|
|
4003
|
+
|
|
4004
|
+
if (/logo/.test(cls + ' ' + alt + ' ' + src)) return 'logo';
|
|
4005
|
+
if (/icon|svg/.test(cls)) return 'icon';
|
|
4006
|
+
if (pRole === 'hero') return 'hero-bg';
|
|
4007
|
+
if (pRole === 'team') return 'team-photo';
|
|
4008
|
+
if (pRole === 'gallery') return 'gallery-item';
|
|
4009
|
+
if (pRole === 'partners') return 'partner-logo';
|
|
4010
|
+
if (pRole === 'testimonials') return 'avatar';
|
|
4011
|
+
|
|
4012
|
+
const rect = img.getBoundingClientRect();
|
|
4013
|
+
if (rect.width > window.innerWidth * 0.8 && rect.height > 300) return 'hero-bg';
|
|
4014
|
+
if (rect.width < 80 && rect.height < 80) return 'icon';
|
|
4015
|
+
return 'card-thumbnail';
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
// \u2500\u2500 1. Structure \u2500\u2500
|
|
4019
|
+
const sectionSelectors = 'body > header, body > footer, body > nav, body > main, body > section, body > div, body > article, body > aside';
|
|
4020
|
+
const topLevelEls = document.querySelectorAll(sectionSelectors);
|
|
4021
|
+
const sections = [];
|
|
4022
|
+
let maxDepth = 0;
|
|
4023
|
+
|
|
4024
|
+
topLevelEls.forEach(el => {
|
|
4025
|
+
// \uC228\uACA8\uC9C4 \uC694\uC18C \uB610\uB294 \uB108\uBE44 0\uC778 \uC694\uC18C \uC81C\uC678
|
|
4026
|
+
const rect = el.getBoundingClientRect();
|
|
4027
|
+
if (rect.height < 10) return;
|
|
4028
|
+
|
|
4029
|
+
const tag = el.tagName.toLowerCase();
|
|
4030
|
+
const layout = detectLayout(el);
|
|
4031
|
+
const bgImg = getComputedProp(el, 'background-image');
|
|
4032
|
+
|
|
4033
|
+
sections.push({
|
|
4034
|
+
tag,
|
|
4035
|
+
role: inferSectionRole(el),
|
|
4036
|
+
childCount: el.children.length,
|
|
4037
|
+
layout,
|
|
4038
|
+
gridColumns: layout === 'grid' ? getComputedProp(el, 'grid-template-columns') : undefined,
|
|
4039
|
+
hasBackgroundImage: bgImg !== 'none' && bgImg !== '',
|
|
4040
|
+
});
|
|
4041
|
+
|
|
4042
|
+
// nesting depth
|
|
4043
|
+
let depth = 0;
|
|
4044
|
+
let cursor = el;
|
|
4045
|
+
while (cursor.firstElementChild) {
|
|
4046
|
+
depth++;
|
|
4047
|
+
cursor = cursor.firstElementChild;
|
|
4048
|
+
}
|
|
4049
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
4050
|
+
});
|
|
4051
|
+
|
|
4052
|
+
// heading hierarchy
|
|
4053
|
+
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
4054
|
+
const headingHierarchy = headings.map(h => h.tagName.toLowerCase() + ': ' + (h.textContent || '').trim().slice(0, 80));
|
|
4055
|
+
|
|
4056
|
+
// \u2500\u2500 2. Design Tokens \u2500\u2500
|
|
4057
|
+
|
|
4058
|
+
// 2a. Colors \u2014 \uBAA8\uB4E0 \uC694\uC18C\uC758 color, backgroundColor \uC218\uC9D1
|
|
4059
|
+
const colorMap = {};
|
|
4060
|
+
const bgColorMap = {};
|
|
4061
|
+
const allEls = document.querySelectorAll('*');
|
|
4062
|
+
const sampleEls = allEls.length > 500
|
|
4063
|
+
? Array.from(allEls).filter((_, i) => i % Math.ceil(allEls.length / 500) === 0)
|
|
4064
|
+
: Array.from(allEls);
|
|
4065
|
+
|
|
4066
|
+
sampleEls.forEach(el => {
|
|
4067
|
+
const color = rgbToHex(getComputedProp(el, 'color'));
|
|
4068
|
+
const bg = rgbToHex(getComputedProp(el, 'background-color'));
|
|
4069
|
+
if (color && color !== 'transparent') colorMap[color] = (colorMap[color] || 0) + 1;
|
|
4070
|
+
if (bg && bg !== 'transparent' && bg !== '#000000') bgColorMap[bg] = (bgColorMap[bg] || 0) + 1;
|
|
4071
|
+
});
|
|
4072
|
+
|
|
4073
|
+
const sortedColors = Object.entries(colorMap).sort((a, b) => b[1] - a[1]);
|
|
4074
|
+
const sortedBgs = Object.entries(bgColorMap).sort((a, b) => b[1] - a[1]);
|
|
4075
|
+
|
|
4076
|
+
// primary = body\uB098 heading\uC758 \uAC00\uC7A5 \uBE48\uBC88\uD55C \uD14D\uC2A4\uD2B8 \uC0C9\uC0C1\uC774 \uC544\uB2CC \uC0C9 \uC911 1\uC704
|
|
4077
|
+
const bodyColor = rgbToHex(getComputedProp(document.body, 'color'));
|
|
4078
|
+
const bodyBg = rgbToHex(getComputedProp(document.body, 'background-color'));
|
|
4079
|
+
const nonBodyColors = sortedColors.filter(([c]) => c !== bodyColor && c !== '#ffffff' && c !== '#000000');
|
|
4080
|
+
const allPalette = [...new Set([...sortedColors.map(c => c[0]), ...sortedBgs.map(c => c[0])])].slice(0, 20);
|
|
4081
|
+
|
|
4082
|
+
// 2b. Typography
|
|
4083
|
+
const typographyScale = {};
|
|
4084
|
+
['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'button', 'span', 'li'].forEach(tag => {
|
|
4085
|
+
const el = document.querySelector(tag);
|
|
4086
|
+
if (el) {
|
|
4087
|
+
typographyScale[tag] = {
|
|
4088
|
+
size: getComputedProp(el, 'font-size'),
|
|
4089
|
+
weight: getComputedProp(el, 'font-weight'),
|
|
4090
|
+
lineHeight: getComputedProp(el, 'line-height'),
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
|
|
4095
|
+
const fontFamilySet = new Set();
|
|
4096
|
+
sampleEls.forEach(el => {
|
|
4097
|
+
const ff = getComputedProp(el, 'font-family');
|
|
4098
|
+
if (ff) fontFamilySet.add(ff.split(',')[0].trim().replace(/['"]/g, ''));
|
|
4099
|
+
});
|
|
4100
|
+
|
|
4101
|
+
// 2c. Spacing
|
|
4102
|
+
const sectionGaps = [];
|
|
4103
|
+
for (let i = 1; i < sections.length; i++) {
|
|
4104
|
+
const prev = topLevelEls[i - 1];
|
|
4105
|
+
const curr = topLevelEls[i];
|
|
4106
|
+
if (prev && curr) {
|
|
4107
|
+
const prevRect = prev.getBoundingClientRect();
|
|
4108
|
+
const currRect = curr.getBoundingClientRect();
|
|
4109
|
+
const gap = currRect.top - prevRect.bottom;
|
|
4110
|
+
if (gap > 0 && gap < 500) sectionGaps.push(gap);
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
const firstContent = document.querySelector('main, [class*=container], [class*=wrapper], body > div > div');
|
|
4115
|
+
const contentPadding = firstContent ? parsePixel(getComputedProp(firstContent, 'padding-left')) : 16;
|
|
4116
|
+
|
|
4117
|
+
// card gap \uCD94\uC815: \uCCAB \uBC88\uC9F8 grid/flex \uCEE8\uD14C\uC774\uB108\uC758 gap
|
|
4118
|
+
let cardGap = 0;
|
|
4119
|
+
const gridContainers = document.querySelectorAll('[style*="grid"], [class*="grid"], [style*="flex"]');
|
|
4120
|
+
for (const gc of gridContainers) {
|
|
4121
|
+
const gap = parsePixel(getComputedProp(gc, 'gap') || getComputedProp(gc, 'column-gap'));
|
|
4122
|
+
if (gap > 0) { cardGap = gap; break; }
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
// base unit \uCD94\uC815 (\uAC00\uC7A5 \uD754\uD55C \uAC04\uACA9\uAC12\uC758 \uCD5C\uB300\uACF5\uC57D\uC218)
|
|
4126
|
+
const allGaps = [...sectionGaps, contentPadding, cardGap].filter(v => v > 0);
|
|
4127
|
+
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
|
|
4128
|
+
const baseUnit = allGaps.length > 1 ? allGaps.reduce((a, b) => gcd(a, b)) : (allGaps[0] || 8);
|
|
4129
|
+
|
|
4130
|
+
// 2d. BorderRadius
|
|
4131
|
+
const radiusValues = [];
|
|
4132
|
+
sampleEls.forEach(el => {
|
|
4133
|
+
const br = parsePixel(getComputedProp(el, 'border-radius'));
|
|
4134
|
+
if (br > 0) radiusValues.push(br);
|
|
4135
|
+
});
|
|
4136
|
+
const uniqueRadii = [...new Set(radiusValues)].sort((a, b) => a - b);
|
|
4137
|
+
|
|
4138
|
+
// \u2500\u2500 3. Interactions \u2500\u2500
|
|
4139
|
+
const styleSheets = Array.from(document.styleSheets);
|
|
4140
|
+
const animationNames = new Set();
|
|
4141
|
+
const transitionProps = new Set();
|
|
4142
|
+
|
|
4143
|
+
try {
|
|
4144
|
+
styleSheets.forEach(ss => {
|
|
4145
|
+
try {
|
|
4146
|
+
const rules = Array.from(ss.cssRules || []);
|
|
4147
|
+
rules.forEach(rule => {
|
|
4148
|
+
if (rule instanceof CSSKeyframesRule) {
|
|
4149
|
+
animationNames.add(rule.name);
|
|
4150
|
+
}
|
|
4151
|
+
if (rule instanceof CSSStyleRule) {
|
|
4152
|
+
const style = rule.style;
|
|
4153
|
+
if (style.animationName && style.animationName !== 'none') {
|
|
4154
|
+
animationNames.add(style.animationName);
|
|
4155
|
+
}
|
|
4156
|
+
if (style.transition && style.transition !== 'none' && style.transition !== 'all 0s ease 0s') {
|
|
4157
|
+
transitionProps.add(style.transition);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
});
|
|
4161
|
+
} catch { /* cross-origin sheets */ }
|
|
4162
|
+
});
|
|
4163
|
+
} catch { /* stylesheet access error */ }
|
|
4164
|
+
|
|
4165
|
+
const hasCarousel = !!(
|
|
4166
|
+
document.querySelector('[class*=carousel], [class*=slider], [class*=swiper], [class*=slick]') ||
|
|
4167
|
+
document.querySelector('[data-slick], [data-swiper]')
|
|
4168
|
+
);
|
|
4169
|
+
const hasAccordion = !!(
|
|
4170
|
+
document.querySelector('details, [class*=accordion], [class*=collapse], [data-toggle=collapse]')
|
|
4171
|
+
);
|
|
4172
|
+
const hasModal = !!(
|
|
4173
|
+
document.querySelector('dialog, [class*=modal], [class*=lightbox], [role=dialog]')
|
|
4174
|
+
);
|
|
4175
|
+
const hasStickyHeader = (() => {
|
|
4176
|
+
const header = document.querySelector('header, [class*=header], nav');
|
|
4177
|
+
if (!header) return false;
|
|
4178
|
+
const pos = getComputedProp(header, 'position');
|
|
4179
|
+
return pos === 'sticky' || pos === 'fixed';
|
|
4180
|
+
})();
|
|
4181
|
+
const hasScrollAnimations = !!(
|
|
4182
|
+
document.querySelector('[class*=aos], [data-aos], [class*=wow], [class*=scroll-animate], [class*=animate-on-scroll]') ||
|
|
4183
|
+
animationNames.size > 2
|
|
4184
|
+
);
|
|
4185
|
+
const hasParallax = !!(
|
|
4186
|
+
document.querySelector('[class*=parallax], [data-parallax], [class*=jarallax]') ||
|
|
4187
|
+
(() => {
|
|
4188
|
+
let found = false;
|
|
4189
|
+
sampleEls.forEach(el => {
|
|
4190
|
+
const ba = getComputedProp(el, 'background-attachment');
|
|
4191
|
+
if (ba === 'fixed') found = true;
|
|
4192
|
+
});
|
|
4193
|
+
return found;
|
|
4194
|
+
})()
|
|
4195
|
+
);
|
|
4196
|
+
const hasHoverEffects = transitionProps.size > 0;
|
|
4197
|
+
|
|
4198
|
+
// \u2500\u2500 4. Images \u2500\u2500
|
|
4199
|
+
const imgEls = document.querySelectorAll('img, picture source, [style*="background-image"]');
|
|
4200
|
+
const images = [];
|
|
4201
|
+
|
|
4202
|
+
imgEls.forEach(el => {
|
|
4203
|
+
let src = '';
|
|
4204
|
+
let alt = '';
|
|
4205
|
+
let width = 0;
|
|
4206
|
+
let height = 0;
|
|
4207
|
+
|
|
4208
|
+
if (el.tagName === 'IMG') {
|
|
4209
|
+
src = el.src || el.dataset.src || '';
|
|
4210
|
+
alt = el.alt || '';
|
|
4211
|
+
width = el.naturalWidth || el.width;
|
|
4212
|
+
height = el.naturalHeight || el.height;
|
|
4213
|
+
} else if (el.tagName === 'SOURCE') {
|
|
4214
|
+
src = el.srcset ? el.srcset.split(',')[0].trim().split(' ')[0] : '';
|
|
4215
|
+
} else {
|
|
4216
|
+
const bg = getComputedProp(el, 'background-image');
|
|
4217
|
+
const match = bg.match(/url\\(["']?(.+?)["']?\\)/);
|
|
4218
|
+
if (match) src = match[1];
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
if (!src || src.startsWith('data:image/svg') || src.includes('.svg')) return;
|
|
4222
|
+
|
|
4223
|
+
const rect = el.getBoundingClientRect();
|
|
4224
|
+
if (!width) width = Math.round(rect.width);
|
|
4225
|
+
if (!height) height = Math.round(rect.height);
|
|
4226
|
+
if (width < 20 || height < 20) return;
|
|
4227
|
+
|
|
4228
|
+
const parentSection = el.closest('section, header, footer, main, [class*=hero], [class*=banner]');
|
|
4229
|
+
const parentRole = parentSection ? inferSectionRole(parentSection) : 'unknown';
|
|
4230
|
+
|
|
4231
|
+
images.push({
|
|
4232
|
+
src: src.slice(0, 500),
|
|
4233
|
+
alt: (alt || '').slice(0, 200),
|
|
4234
|
+
width,
|
|
4235
|
+
height,
|
|
4236
|
+
aspectRatio: guessAspectRatio(width, height),
|
|
4237
|
+
role: inferImageRole(el, parentRole),
|
|
4238
|
+
dominantColor: '', // \uC11C\uBC84 \uC0AC\uC774\uB4DC\uC5D0\uC11C \uCD94\uCD9C \uB610\uB294 Vision \uBD84\uC11D
|
|
4239
|
+
position: parentRole,
|
|
4240
|
+
});
|
|
4241
|
+
});
|
|
4242
|
+
|
|
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
|
|
4325
|
+
const gaps = [];
|
|
4326
|
+
|
|
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)');
|
|
4331
|
+
}
|
|
4332
|
+
// canvas / WebGL
|
|
4333
|
+
if (document.querySelector('canvas')) {
|
|
4334
|
+
gaps.push('canvas-webgl: Canvas \uB610\uB294 WebGL \uAE30\uBC18 \uC778\uD130\uB799\uC158');
|
|
4335
|
+
}
|
|
4336
|
+
// custom cursor
|
|
4337
|
+
const bodyCursor = getComputedProp(document.body, 'cursor');
|
|
4338
|
+
if (bodyCursor !== 'auto' && bodyCursor !== 'default') {
|
|
4339
|
+
gaps.push('custom-cursor: \uCEE4\uC2A4\uD140 \uB9C8\uC6B0\uC2A4 \uCEE4\uC11C');
|
|
4340
|
+
}
|
|
4341
|
+
// 3D transforms
|
|
4342
|
+
let has3d = false;
|
|
4343
|
+
sampleEls.slice(0, 100).forEach(el => {
|
|
4344
|
+
const tf = getComputedProp(el, 'transform');
|
|
4345
|
+
if (tf.includes('matrix3d') || tf.includes('perspective')) has3d = true;
|
|
4346
|
+
});
|
|
4347
|
+
if (has3d) {
|
|
4348
|
+
gaps.push('3d-transforms: CSS 3D \uBCC0\uD658 \uC0AC\uC6A9');
|
|
4349
|
+
}
|
|
4350
|
+
// SVG animation
|
|
4351
|
+
if (document.querySelector('svg animate, svg animateTransform, svg animateMotion')) {
|
|
4352
|
+
gaps.push('svg-animation: SVG \uC778\uB77C\uC778 \uC560\uB2C8\uBA54\uC774\uC158');
|
|
4353
|
+
}
|
|
4354
|
+
// marquee / ticker
|
|
4355
|
+
if (document.querySelector('marquee, [class*=marquee], [class*=ticker]')) {
|
|
4356
|
+
gaps.push('marquee-ticker: \uC218\uD3C9 \uC2A4\uD06C\uB864 \uD14D\uC2A4\uD2B8/\uC774\uBBF8\uC9C0');
|
|
4357
|
+
}
|
|
4358
|
+
// infinite scroll
|
|
4359
|
+
if (document.querySelector('[class*=infinite], [data-infinite]')) {
|
|
4360
|
+
gaps.push('infinite-scroll: \uBB34\uD55C \uC2A4\uD06C\uB864 \uB85C\uB529');
|
|
4361
|
+
}
|
|
4362
|
+
// chat widget (\uC678\uBD80 \uC5F0\uB3D9\uC740 embed\uC73C\uB85C \uAC00\uB2A5\uD558\uC9C0\uB9CC \uAE30\uB85D)
|
|
4363
|
+
if (document.querySelector('[class*=chat-widget], [id*=chat], #ch-plugin, .channel-talk')) {
|
|
4364
|
+
gaps.push('chat-widget: \uC678\uBD80 \uCC44\uD305 \uC704\uC82F (embed-block integration \uD544\uC694)');
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
// \u2500\u2500 Return \u2500\u2500
|
|
4368
|
+
return {
|
|
4369
|
+
structure: {
|
|
4370
|
+
sections,
|
|
4371
|
+
headingHierarchy,
|
|
4372
|
+
totalSections: sections.length,
|
|
4373
|
+
nestingDepth: maxDepth,
|
|
4374
|
+
},
|
|
4375
|
+
designTokens: {
|
|
4376
|
+
colors: {
|
|
4377
|
+
primary: (nonBodyColors[0] || sortedColors[0] || ['#000000'])[0],
|
|
4378
|
+
secondary: (nonBodyColors[1] || sortedColors[1] || ['#666666'])[0],
|
|
4379
|
+
background: bodyBg || '#ffffff',
|
|
4380
|
+
text: bodyColor || '#000000',
|
|
4381
|
+
accent: (nonBodyColors[2] || sortedColors[2] || ['#0066ff'])[0],
|
|
4382
|
+
palette: allPalette,
|
|
4383
|
+
},
|
|
4384
|
+
typography: {
|
|
4385
|
+
fontFamilies: [...fontFamilySet].slice(0, 5),
|
|
4386
|
+
scale: typographyScale,
|
|
4387
|
+
},
|
|
4388
|
+
spacing: {
|
|
4389
|
+
sectionGap: sectionGaps.length > 0 ? Math.round(sectionGaps.reduce((a, b) => a + b, 0) / sectionGaps.length) : 80,
|
|
4390
|
+
contentPadding,
|
|
4391
|
+
cardGap: cardGap || 16,
|
|
4392
|
+
baseUnit: Math.max(baseUnit, 4),
|
|
4393
|
+
},
|
|
4394
|
+
borderRadius: {
|
|
4395
|
+
small: uniqueRadii[0] || 0,
|
|
4396
|
+
medium: uniqueRadii[Math.floor(uniqueRadii.length / 2)] || 0,
|
|
4397
|
+
large: uniqueRadii[uniqueRadii.length - 1] || 0,
|
|
4398
|
+
},
|
|
4399
|
+
},
|
|
4400
|
+
interactions: {
|
|
4401
|
+
hasScrollAnimations,
|
|
4402
|
+
hasHoverEffects,
|
|
4403
|
+
hasCarousel,
|
|
4404
|
+
hasAccordion,
|
|
4405
|
+
hasModal,
|
|
4406
|
+
hasStickyHeader,
|
|
4407
|
+
hasParallax,
|
|
4408
|
+
detectedAnimations: [...animationNames].slice(0, 20),
|
|
4409
|
+
detectedTransitions: [...transitionProps].slice(0, 20),
|
|
4410
|
+
},
|
|
4411
|
+
images,
|
|
4412
|
+
videos,
|
|
4413
|
+
gaps,
|
|
4414
|
+
};
|
|
4415
|
+
})()
|
|
4416
|
+
`;
|
|
4417
|
+
async function analyzeReference(options) {
|
|
4418
|
+
const { url, outputDir, timeout = 3e4 } = options;
|
|
4419
|
+
const screenshotDir = join(outputDir, "screenshots");
|
|
4420
|
+
await mkdir3(screenshotDir, { recursive: true });
|
|
4421
|
+
const screenshots = {};
|
|
4422
|
+
for (const vp of VIEWPORTS) {
|
|
4423
|
+
const filePath = join(screenshotDir, `${vp.name}-${vp.width}px.png`);
|
|
4424
|
+
captureScreenshot(url, filePath, vp.width, vp.height, timeout);
|
|
4425
|
+
screenshots[vp.name] = filePath;
|
|
4426
|
+
}
|
|
4427
|
+
const extractedData = await extractPageData(url, timeout);
|
|
4428
|
+
const analysis = {
|
|
4429
|
+
url,
|
|
4430
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4431
|
+
screenshots: {
|
|
4432
|
+
mobile: screenshots.mobile,
|
|
4433
|
+
tablet: screenshots.tablet,
|
|
4434
|
+
laptop: screenshots.laptop,
|
|
4435
|
+
desktop: screenshots.desktop
|
|
4436
|
+
},
|
|
4437
|
+
...extractedData
|
|
4438
|
+
};
|
|
4439
|
+
const analysisPath = join(outputDir, "analysis.json");
|
|
4440
|
+
await writeFile7(analysisPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
4441
|
+
return analysis;
|
|
4442
|
+
}
|
|
4443
|
+
function captureScreenshot(url, outputPath, width, height, timeout) {
|
|
4444
|
+
const result = spawnSync("npx", [
|
|
4445
|
+
"playwright",
|
|
4446
|
+
"screenshot",
|
|
4447
|
+
"--browser",
|
|
4448
|
+
"chromium",
|
|
4449
|
+
"--viewport-size",
|
|
4450
|
+
`${width},${height}`,
|
|
4451
|
+
"--wait-for-timeout",
|
|
4452
|
+
"3000",
|
|
4453
|
+
"--full-page",
|
|
4454
|
+
url,
|
|
4455
|
+
outputPath
|
|
4456
|
+
], {
|
|
4457
|
+
stdio: "pipe",
|
|
4458
|
+
timeout: timeout + 15e3
|
|
4459
|
+
});
|
|
4460
|
+
if (result.status !== 0) {
|
|
4461
|
+
const stderr = result.stderr?.toString() ?? "";
|
|
4462
|
+
throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328 (${width}px): ${stderr || "unknown error"}`);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
async function extractPageData(url, timeout) {
|
|
4466
|
+
const scriptContent = `
|
|
4467
|
+
const { chromium } = require('playwright');
|
|
4468
|
+
(async () => {
|
|
4469
|
+
const browser = await chromium.launch({ headless: true });
|
|
4470
|
+
const context = await browser.newContext({
|
|
4471
|
+
viewport: { width: 1280, height: 800 },
|
|
4472
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
4473
|
+
});
|
|
4474
|
+
const page = await context.newPage();
|
|
4475
|
+
await page.goto(${JSON.stringify(url)}, { waitUntil: 'networkidle', timeout: ${timeout} });
|
|
4476
|
+
await page.waitForTimeout(2000);
|
|
4477
|
+
const data = await page.evaluate(${JSON.stringify(EXTRACTION_SCRIPT)});
|
|
4478
|
+
await browser.close();
|
|
4479
|
+
process.stdout.write(JSON.stringify(data));
|
|
4480
|
+
})().catch(e => {
|
|
4481
|
+
process.stderr.write(e.message);
|
|
4482
|
+
process.exit(1);
|
|
4483
|
+
});
|
|
4484
|
+
`;
|
|
4485
|
+
const result = spawnSync("node", ["-e", scriptContent], {
|
|
4486
|
+
stdio: "pipe",
|
|
4487
|
+
timeout: timeout + 3e4,
|
|
4488
|
+
env: { ...process.env }
|
|
4489
|
+
});
|
|
4490
|
+
if (result.status !== 0) {
|
|
4491
|
+
const stderr = result.stderr?.toString() ?? "";
|
|
4492
|
+
console.error(`DOM \uCD94\uCD9C \uC2E4\uD328 (fallback \uC0AC\uC6A9): ${stderr}`);
|
|
4493
|
+
return createFallbackData();
|
|
4494
|
+
}
|
|
4495
|
+
try {
|
|
4496
|
+
const output = result.stdout.toString();
|
|
4497
|
+
return JSON.parse(output);
|
|
4498
|
+
} catch {
|
|
4499
|
+
console.error("DOM \uCD94\uCD9C \uACB0\uACFC \uD30C\uC2F1 \uC2E4\uD328 (fallback \uC0AC\uC6A9)");
|
|
4500
|
+
return createFallbackData();
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
function createFallbackData() {
|
|
4504
|
+
return {
|
|
4505
|
+
structure: {
|
|
4506
|
+
sections: [],
|
|
4507
|
+
headingHierarchy: [],
|
|
4508
|
+
totalSections: 0,
|
|
4509
|
+
nestingDepth: 0
|
|
4510
|
+
},
|
|
4511
|
+
designTokens: {
|
|
4512
|
+
colors: {
|
|
4513
|
+
primary: "#000000",
|
|
4514
|
+
secondary: "#666666",
|
|
4515
|
+
background: "#ffffff",
|
|
4516
|
+
text: "#000000",
|
|
4517
|
+
accent: "#0066ff",
|
|
4518
|
+
palette: []
|
|
4519
|
+
},
|
|
4520
|
+
typography: {
|
|
4521
|
+
fontFamilies: [],
|
|
4522
|
+
scale: {}
|
|
4523
|
+
},
|
|
4524
|
+
spacing: {
|
|
4525
|
+
sectionGap: 80,
|
|
4526
|
+
contentPadding: 16,
|
|
4527
|
+
cardGap: 16,
|
|
4528
|
+
baseUnit: 8
|
|
4529
|
+
},
|
|
4530
|
+
borderRadius: { small: 0, medium: 0, large: 0 }
|
|
4531
|
+
},
|
|
4532
|
+
interactions: {
|
|
4533
|
+
hasScrollAnimations: false,
|
|
4534
|
+
hasHoverEffects: false,
|
|
4535
|
+
hasCarousel: false,
|
|
4536
|
+
hasAccordion: false,
|
|
4537
|
+
hasModal: false,
|
|
4538
|
+
hasStickyHeader: false,
|
|
4539
|
+
hasParallax: false,
|
|
4540
|
+
detectedAnimations: [],
|
|
4541
|
+
detectedTransitions: []
|
|
4542
|
+
},
|
|
4543
|
+
images: [],
|
|
4544
|
+
videos: [],
|
|
4545
|
+
gaps: []
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
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
|
+
|
|
4682
|
+
// src/commands/analyze.ts
|
|
4683
|
+
async function commandAnalyze(url, options) {
|
|
4684
|
+
console.log(chalk15.bold("\n\uB808\uD37C\uB7F0\uC2A4 \uBD84\uC11D (Reference Analysis)\n"));
|
|
4685
|
+
if (!url) {
|
|
4686
|
+
console.error(chalk15.red("URL\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."));
|
|
4687
|
+
console.error(chalk15.dim(" \uC608: npx @saeroon/cli analyze https://example.com"));
|
|
4688
|
+
process.exit(1);
|
|
4689
|
+
}
|
|
4690
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
4691
|
+
url = "https://" + url;
|
|
4692
|
+
}
|
|
4693
|
+
const domain = new URL(url).hostname.replace(/^www\./, "");
|
|
4694
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4695
|
+
const defaultDir = join2(".saeroon", "analysis", `${domain}-${timestamp2}`);
|
|
4696
|
+
const outputDir = resolve13(process.cwd(), options.outputDir ?? defaultDir);
|
|
4697
|
+
const timeout = parseInt(options.timeout ?? "30000", 10);
|
|
4698
|
+
await mkdir4(outputDir, { recursive: true });
|
|
4699
|
+
console.log(chalk15.dim(` URL: ${url}`));
|
|
4700
|
+
console.log(chalk15.dim(` \uCD9C\uB825: ${outputDir}`));
|
|
4701
|
+
console.log(chalk15.dim(` \uD0C0\uC784\uC544\uC6C3: ${timeout}ms`));
|
|
4702
|
+
console.log("");
|
|
4703
|
+
const analysisSpinner = spinner("\uB808\uD37C\uB7F0\uC2A4 \uBD84\uC11D \uC911... (\uC2A4\uD06C\uB9B0\uC0F7 4\uC7A5 + DOM/CSS \uCD94\uCD9C)");
|
|
4704
|
+
try {
|
|
4705
|
+
const analysis = await analyzeReference({ url, outputDir, timeout });
|
|
4706
|
+
analysisSpinner.stop(chalk15.green(" \uBD84\uC11D \uC644\uB8CC!"));
|
|
4707
|
+
console.log("");
|
|
4708
|
+
console.log(chalk15.bold("\uAD6C\uC870"));
|
|
4709
|
+
console.log(chalk15.dim(` \uC139\uC158: ${analysis.structure.totalSections}\uAC1C`));
|
|
4710
|
+
console.log(chalk15.dim(` \uCD5C\uB300 \uC911\uCCA9: ${analysis.structure.nestingDepth}`));
|
|
4711
|
+
if (analysis.structure.headingHierarchy.length > 0) {
|
|
4712
|
+
console.log(chalk15.dim(" Heading hierarchy:"));
|
|
4713
|
+
analysis.structure.headingHierarchy.slice(0, 10).forEach((h) => {
|
|
4714
|
+
console.log(chalk15.dim(` ${h}`));
|
|
4715
|
+
});
|
|
4716
|
+
if (analysis.structure.headingHierarchy.length > 10) {
|
|
4717
|
+
console.log(chalk15.dim(` ... +${analysis.structure.headingHierarchy.length - 10}\uAC1C`));
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
console.log("");
|
|
4721
|
+
console.log(chalk15.bold("\uB514\uC790\uC778 \uD1A0\uD070"));
|
|
4722
|
+
const { colors, typography, spacing } = analysis.designTokens;
|
|
4723
|
+
console.log(chalk15.dim(` Primary: ${colors.primary}`));
|
|
4724
|
+
console.log(chalk15.dim(` Secondary: ${colors.secondary}`));
|
|
4725
|
+
console.log(chalk15.dim(` Background: ${colors.background}`));
|
|
4726
|
+
console.log(chalk15.dim(` Text: ${colors.text}`));
|
|
4727
|
+
console.log(chalk15.dim(` Accent: ${colors.accent}`));
|
|
4728
|
+
console.log(chalk15.dim(` \uD3F0\uD2B8: ${typography.fontFamilies.join(", ") || "(\uCD94\uCD9C \uC2E4\uD328)"}`));
|
|
4729
|
+
console.log(chalk15.dim(` \uC139\uC158 \uAC04\uACA9: ${spacing.sectionGap}px`));
|
|
4730
|
+
console.log(chalk15.dim(` \uAE30\uBCF8 \uB2E8\uC704: ${spacing.baseUnit}px`));
|
|
4731
|
+
console.log("");
|
|
4732
|
+
console.log(chalk15.bold("\uC778\uD130\uB799\uC158"));
|
|
4733
|
+
const { interactions } = analysis;
|
|
4734
|
+
const detected = [
|
|
4735
|
+
interactions.hasStickyHeader && "Sticky Header",
|
|
4736
|
+
interactions.hasScrollAnimations && "Scroll Animations",
|
|
4737
|
+
interactions.hasCarousel && "Carousel",
|
|
4738
|
+
interactions.hasAccordion && "Accordion",
|
|
4739
|
+
interactions.hasModal && "Modal",
|
|
4740
|
+
interactions.hasParallax && "Parallax",
|
|
4741
|
+
interactions.hasHoverEffects && "Hover Effects"
|
|
4742
|
+
].filter(Boolean);
|
|
4743
|
+
console.log(chalk15.dim(` \uAC10\uC9C0: ${detected.length > 0 ? detected.join(", ") : "(\uC5C6\uC74C)"}`));
|
|
4744
|
+
console.log("");
|
|
4745
|
+
console.log(chalk15.bold("\uC774\uBBF8\uC9C0"));
|
|
4746
|
+
console.log(chalk15.dim(` \uCD1D ${analysis.images.length}\uAC1C \uAC10\uC9C0`));
|
|
4747
|
+
const roleCount = {};
|
|
4748
|
+
analysis.images.forEach((img) => {
|
|
4749
|
+
roleCount[img.role] = (roleCount[img.role] || 0) + 1;
|
|
4750
|
+
});
|
|
4751
|
+
Object.entries(roleCount).forEach(([role, count]) => {
|
|
4752
|
+
console.log(chalk15.dim(` ${role}: ${count}\uAC1C`));
|
|
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
|
+
}
|
|
4790
|
+
if (analysis.gaps.length > 0) {
|
|
4791
|
+
console.log("");
|
|
4792
|
+
console.log(chalk15.bold(chalk15.yellow("Gap \uAC10\uC9C0")));
|
|
4793
|
+
analysis.gaps.forEach((gap) => {
|
|
4794
|
+
console.log(chalk15.yellow(` \u26A0 ${gap}`));
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
console.log("");
|
|
4798
|
+
console.log(chalk15.bold("\uC2A4\uD06C\uB9B0\uC0F7"));
|
|
4799
|
+
console.log(chalk15.dim(` mobile: ${analysis.screenshots.mobile}`));
|
|
4800
|
+
console.log(chalk15.dim(` tablet: ${analysis.screenshots.tablet}`));
|
|
4801
|
+
console.log(chalk15.dim(` laptop: ${analysis.screenshots.laptop}`));
|
|
4802
|
+
console.log(chalk15.dim(` desktop: ${analysis.screenshots.desktop}`));
|
|
4803
|
+
console.log("");
|
|
4804
|
+
console.log(chalk15.green.bold("\uBD84\uC11D \uC644\uB8CC!"));
|
|
4805
|
+
console.log(chalk15.dim(` \uACB0\uACFC: ${join2(outputDir, "analysis.json")}`));
|
|
4806
|
+
console.log("");
|
|
4807
|
+
} catch (error) {
|
|
4808
|
+
analysisSpinner.stop(chalk15.red(" \uBD84\uC11D \uC2E4\uD328"));
|
|
4809
|
+
console.error(chalk15.red(`
|
|
4810
|
+
${error instanceof Error ? error.message : String(error)}`));
|
|
4811
|
+
console.log("");
|
|
4812
|
+
console.log(chalk15.dim("Playwright\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694:"));
|
|
4813
|
+
console.log(chalk15.cyan(" npx playwright install chromium\n"));
|
|
4814
|
+
process.exit(1);
|
|
4815
|
+
}
|
|
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
|
+
}
|
|
4866
|
+
|
|
4867
|
+
// src/commands/compare.ts
|
|
4868
|
+
import chalk16 from "chalk";
|
|
4869
|
+
import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
|
|
4870
|
+
import { resolve as resolve14, join as join3 } from "path";
|
|
4871
|
+
import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
|
|
3912
4872
|
async function commandCompare(options) {
|
|
3913
|
-
console.log(
|
|
4873
|
+
console.log(chalk16.bold("\n\uC2DC\uAC01 \uBE44\uAD50 (Visual Diff)\n"));
|
|
3914
4874
|
if (!options.ref || !options.preview) {
|
|
3915
|
-
console.error(
|
|
3916
|
-
console.error(
|
|
4875
|
+
console.error(chalk16.red("--ref <url> \uACFC --preview <url> \uC740 \uBAA8\uB450 \uD544\uC218\uC785\uB2C8\uB2E4."));
|
|
4876
|
+
console.error(chalk16.dim(" \uC608: npx @saeroon/cli compare --ref https://example.com --preview https://preview.saeroon.com/abc"));
|
|
3917
4877
|
process.exit(1);
|
|
3918
4878
|
}
|
|
3919
4879
|
const playwrightAvailable = checkPlaywright();
|
|
3920
4880
|
if (!playwrightAvailable) {
|
|
3921
|
-
console.log(
|
|
3922
|
-
console.log(
|
|
3923
|
-
console.log(
|
|
4881
|
+
console.log(chalk16.yellow("Playwright\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
|
|
4882
|
+
console.log(chalk16.dim(" \uB2E4\uC74C \uBA85\uB839\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694:"));
|
|
4883
|
+
console.log(chalk16.cyan(" npx playwright install chromium\n"));
|
|
3924
4884
|
process.exit(1);
|
|
3925
4885
|
}
|
|
4886
|
+
if (options.viewports) {
|
|
4887
|
+
await runMultiViewportCompare(options);
|
|
4888
|
+
} else {
|
|
4889
|
+
await runSingleViewportCompare(options);
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
var ALL_VIEWPORTS = [
|
|
4893
|
+
{ name: "mobile", width: 375, height: 812 },
|
|
4894
|
+
{ name: "tablet", width: 768, height: 1024 },
|
|
4895
|
+
{ name: "laptop", width: 1280, height: 800 },
|
|
4896
|
+
{ name: "desktop", width: 1536, height: 900 }
|
|
4897
|
+
];
|
|
4898
|
+
async function runMultiViewportCompare(options) {
|
|
4899
|
+
const viewports = parseViewports(options.viewports);
|
|
4900
|
+
const outputDir = resolve14(process.cwd(), options.outputDir ?? ".saeroon/compare");
|
|
4901
|
+
await mkdir5(outputDir, { recursive: true });
|
|
4902
|
+
console.log(chalk16.dim(` \uB808\uD37C\uB7F0\uC2A4: ${options.ref}`));
|
|
4903
|
+
console.log(chalk16.dim(` \uD504\uB9AC\uBDF0: ${options.preview}`));
|
|
4904
|
+
console.log(chalk16.dim(` \uBDF0\uD3EC\uD2B8: ${viewports.map((v) => `${v.name}(${v.width}px)`).join(", ")}`));
|
|
4905
|
+
console.log(chalk16.dim(` \uCD9C\uB825: ${outputDir}`));
|
|
4906
|
+
console.log("");
|
|
4907
|
+
const hasMagick = checkCommand("magick");
|
|
4908
|
+
const results = [];
|
|
4909
|
+
for (const vp of viewports) {
|
|
4910
|
+
const vpSpinner = spinner(`${vp.name} (${vp.width}px) \uBE44\uAD50 \uC911...`);
|
|
4911
|
+
const refPath = join3(outputDir, `ref-${vp.name}.png`);
|
|
4912
|
+
const previewPath = join3(outputDir, `preview-${vp.name}.png`);
|
|
4913
|
+
const diffPath = join3(outputDir, `diff-${vp.name}.png`);
|
|
4914
|
+
try {
|
|
4915
|
+
captureScreenshot2(options.ref, refPath, vp.width, vp.height);
|
|
4916
|
+
captureScreenshot2(options.preview, previewPath, vp.width, vp.height);
|
|
4917
|
+
let diffPercentage = 0;
|
|
4918
|
+
if (hasMagick) {
|
|
4919
|
+
diffPercentage = generateDiffWithMagick(refPath, previewPath, diffPath);
|
|
4920
|
+
} else {
|
|
4921
|
+
generateSideBySide(refPath, previewPath, diffPath);
|
|
4922
|
+
diffPercentage = -1;
|
|
4923
|
+
}
|
|
4924
|
+
results.push({
|
|
4925
|
+
name: vp.name,
|
|
4926
|
+
width: vp.width,
|
|
4927
|
+
referenceScreenshot: refPath,
|
|
4928
|
+
previewScreenshot: previewPath,
|
|
4929
|
+
diffScreenshot: diffPath,
|
|
4930
|
+
diffPercentage
|
|
4931
|
+
});
|
|
4932
|
+
const pctText = diffPercentage >= 0 ? `${diffPercentage.toFixed(1)}%` : "(\uACC4\uC0B0 \uBD88\uAC00)";
|
|
4933
|
+
const pctColor = diffPercentage <= 10 ? chalk16.green : diffPercentage <= 25 ? chalk16.yellow : chalk16.red;
|
|
4934
|
+
vpSpinner.stop(` ${vp.name}: diff ${pctColor(pctText)}`);
|
|
4935
|
+
} catch (error) {
|
|
4936
|
+
vpSpinner.stop(chalk16.red(` ${vp.name}: \uC2E4\uD328 \u2014 ${error instanceof Error ? error.message : String(error)}`));
|
|
4937
|
+
results.push({
|
|
4938
|
+
name: vp.name,
|
|
4939
|
+
width: vp.width,
|
|
4940
|
+
referenceScreenshot: refPath,
|
|
4941
|
+
previewScreenshot: previewPath,
|
|
4942
|
+
diffScreenshot: diffPath,
|
|
4943
|
+
diffPercentage: -1
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
const validResults = results.filter((r) => r.diffPercentage >= 0);
|
|
4948
|
+
const overallDiff = validResults.length > 0 ? validResults.reduce((sum, r) => sum + r.diffPercentage, 0) / validResults.length : -1;
|
|
4949
|
+
const report = {
|
|
4950
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4951
|
+
referenceUrl: options.ref,
|
|
4952
|
+
previewUrl: options.preview,
|
|
4953
|
+
viewports: results,
|
|
4954
|
+
overallDiffPercentage: Math.round(overallDiff * 10) / 10
|
|
4955
|
+
};
|
|
4956
|
+
const reportPath = join3(outputDir, "comparison-report.json");
|
|
4957
|
+
await writeFile9(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
4958
|
+
console.log("");
|
|
4959
|
+
console.log(chalk16.bold("\uBE44\uAD50 \uC694\uC57D"));
|
|
4960
|
+
for (const r of results) {
|
|
4961
|
+
const pct = r.diffPercentage >= 0 ? `${r.diffPercentage.toFixed(1)}%` : "N/A";
|
|
4962
|
+
const icon = r.diffPercentage <= 10 ? chalk16.green("\u25CF") : r.diffPercentage <= 25 ? chalk16.yellow("\u25CF") : chalk16.red("\u25CF");
|
|
4963
|
+
console.log(` ${icon} ${r.name.padEnd(8)} ${pct.padStart(6)} ${chalk16.dim(r.diffScreenshot)}`);
|
|
4964
|
+
}
|
|
4965
|
+
console.log("");
|
|
4966
|
+
if (overallDiff >= 0) {
|
|
4967
|
+
const overallColor = overallDiff <= 10 ? chalk16.green : overallDiff <= 25 ? chalk16.yellow : chalk16.red;
|
|
4968
|
+
console.log(chalk16.bold(`\uC804\uCCB4 \uD3C9\uADE0 diff: ${overallColor(`${overallDiff.toFixed(1)}%`)}`));
|
|
4969
|
+
if (overallDiff <= 10) {
|
|
4970
|
+
console.log(chalk16.green(" \u2192 \uBE44\uAD50 \uB8E8\uD504 \uC644\uB8CC! CEO \uCD5C\uC885 \uD655\uC778 \uC694\uCCAD \uAC00\uB2A5"));
|
|
4971
|
+
} else {
|
|
4972
|
+
console.log(chalk16.yellow(" \u2192 diff > 10%: Vision \uBE44\uAD50 \u2192 \uC2A4\uD0A4\uB9C8 \uC218\uC815 \uD544\uC694"));
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
console.log(chalk16.dim(`
|
|
4976
|
+
\uB9AC\uD3EC\uD2B8: ${reportPath}`));
|
|
4977
|
+
console.log("");
|
|
4978
|
+
}
|
|
4979
|
+
function parseViewports(input) {
|
|
4980
|
+
if (input === "all") return ALL_VIEWPORTS;
|
|
4981
|
+
const names = input.split(",").map((s) => s.trim().toLowerCase());
|
|
4982
|
+
return names.map((name) => ALL_VIEWPORTS.find((v) => v.name === name)).filter((v) => v !== void 0);
|
|
4983
|
+
}
|
|
4984
|
+
function generateDiffWithMagick(refPath, previewPath, diffPath) {
|
|
4985
|
+
try {
|
|
4986
|
+
execSync2(
|
|
4987
|
+
`magick "${previewPath}" -resize "$(magick identify -format '%wx%h' "${refPath}")" -extent "$(magick identify -format '%wx%h' "${refPath}")" "${previewPath}"`,
|
|
4988
|
+
{ stdio: "pipe" }
|
|
4989
|
+
);
|
|
4990
|
+
} catch {
|
|
4991
|
+
}
|
|
4992
|
+
try {
|
|
4993
|
+
const result = spawnSync2("magick", [
|
|
4994
|
+
"compare",
|
|
4995
|
+
"-metric",
|
|
4996
|
+
"AE",
|
|
4997
|
+
refPath,
|
|
4998
|
+
previewPath,
|
|
4999
|
+
diffPath
|
|
5000
|
+
], { stdio: "pipe", timeout: 3e4 });
|
|
5001
|
+
const stderr = result.stderr?.toString().trim() ?? "";
|
|
5002
|
+
const diffPixels = parseFloat(stderr) || 0;
|
|
5003
|
+
try {
|
|
5004
|
+
const sizeStr = execSync2(`magick identify -format '%w %h' "${refPath}"`, { stdio: "pipe" }).toString().trim();
|
|
5005
|
+
const [w, h] = sizeStr.split(" ").map(Number);
|
|
5006
|
+
const totalPixels = w * h;
|
|
5007
|
+
if (totalPixels > 0) {
|
|
5008
|
+
return Math.round(diffPixels / totalPixels * 1e3) / 10;
|
|
5009
|
+
}
|
|
5010
|
+
} catch {
|
|
5011
|
+
}
|
|
5012
|
+
return diffPixels > 0 ? 50 : 0;
|
|
5013
|
+
} catch {
|
|
5014
|
+
try {
|
|
5015
|
+
execSync2(
|
|
5016
|
+
`magick composite -blend 50x50 "${refPath}" "${previewPath}" "${diffPath}"`,
|
|
5017
|
+
{ stdio: "pipe" }
|
|
5018
|
+
);
|
|
5019
|
+
} catch {
|
|
5020
|
+
}
|
|
5021
|
+
return -1;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
function generateSideBySide(refPath, previewPath, diffPath) {
|
|
5025
|
+
try {
|
|
5026
|
+
execSync2(
|
|
5027
|
+
`magick montage "${refPath}" "${previewPath}" -geometry +2+2 -tile 2x1 "${diffPath}"`,
|
|
5028
|
+
{ stdio: "pipe" }
|
|
5029
|
+
);
|
|
5030
|
+
} catch {
|
|
5031
|
+
try {
|
|
5032
|
+
execSync2(`cp "${refPath}" "${diffPath}"`, { stdio: "pipe" });
|
|
5033
|
+
} catch {
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
async function runSingleViewportCompare(options) {
|
|
3926
5038
|
const width = parseInt(options.width ?? "1280", 10);
|
|
3927
5039
|
const height = parseInt(options.height ?? "800", 10);
|
|
3928
|
-
const outputPath =
|
|
5040
|
+
const outputPath = resolve14(process.cwd(), options.output ?? "compare-result.png");
|
|
3929
5041
|
const captureSpinner = spinner("\uB808\uD37C\uB7F0\uC2A4 \uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC911...");
|
|
3930
5042
|
try {
|
|
3931
|
-
const refScreenshot =
|
|
3932
|
-
const previewScreenshot =
|
|
3933
|
-
|
|
3934
|
-
captureSpinner.stop(
|
|
5043
|
+
const refScreenshot = resolve14(process.cwd(), ".compare-ref.png");
|
|
5044
|
+
const previewScreenshot = resolve14(process.cwd(), ".compare-preview.png");
|
|
5045
|
+
captureScreenshot2(options.ref, refScreenshot, width, height);
|
|
5046
|
+
captureSpinner.stop(chalk16.green(" \uB808\uD37C\uB7F0\uC2A4 \uCEA1\uCC98 \uC644\uB8CC"));
|
|
3935
5047
|
const previewSpinner = spinner("\uD504\uB9AC\uBDF0 \uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC911...");
|
|
3936
|
-
|
|
3937
|
-
previewSpinner.stop(
|
|
5048
|
+
captureScreenshot2(options.preview, previewScreenshot, width, height);
|
|
5049
|
+
previewSpinner.stop(chalk16.green(" \uD504\uB9AC\uBDF0 \uCEA1\uCC98 \uC644\uB8CC"));
|
|
3938
5050
|
const diffSpinner = spinner("\uC2DC\uAC01 \uBE44\uAD50 \uC0DD\uC131 \uC911...");
|
|
3939
5051
|
const hasMagick = checkCommand("magick");
|
|
3940
5052
|
if (hasMagick) {
|
|
3941
5053
|
try {
|
|
3942
|
-
|
|
5054
|
+
execSync2(
|
|
3943
5055
|
`magick compare -metric AE "${refScreenshot}" "${previewScreenshot}" "${outputPath}" 2>&1`,
|
|
3944
5056
|
{ stdio: "pipe" }
|
|
3945
5057
|
);
|
|
3946
5058
|
} catch {
|
|
3947
|
-
|
|
5059
|
+
execSync2(
|
|
3948
5060
|
`magick composite -blend 50x50 "${refScreenshot}" "${previewScreenshot}" "${outputPath}"`,
|
|
3949
5061
|
{ stdio: "pipe" }
|
|
3950
5062
|
);
|
|
3951
5063
|
}
|
|
3952
|
-
diffSpinner.stop(
|
|
5064
|
+
diffSpinner.stop(chalk16.green(" Diff \uC774\uBBF8\uC9C0 \uC0DD\uC131 \uC644\uB8CC (ImageMagick)"));
|
|
3953
5065
|
} else {
|
|
3954
|
-
|
|
5066
|
+
execSync2(
|
|
3955
5067
|
`magick montage "${refScreenshot}" "${previewScreenshot}" -geometry +2+2 -tile 2x1 "${outputPath}" 2>/dev/null || cp "${refScreenshot}" "${outputPath}"`,
|
|
3956
5068
|
{ stdio: "pipe" }
|
|
3957
5069
|
);
|
|
3958
|
-
diffSpinner.stop(
|
|
5070
|
+
diffSpinner.stop(chalk16.yellow(" \uB098\uB780\uD788 \uBE44\uAD50 \uC774\uBBF8\uC9C0 \uC0DD\uC131 (ImageMagick \uC5C6\uC74C \u2014 \uC624\uBC84\uB808\uC774 \uBE44\uAD50\uB294 magick \uC124\uCE58 \uD544\uC694)"));
|
|
3959
5071
|
}
|
|
3960
5072
|
try {
|
|
3961
|
-
|
|
5073
|
+
execSync2(`rm -f "${refScreenshot}" "${previewScreenshot}"`, { stdio: "pipe" });
|
|
3962
5074
|
} catch {
|
|
3963
5075
|
}
|
|
3964
5076
|
console.log("");
|
|
3965
|
-
console.log(
|
|
3966
|
-
console.log(
|
|
3967
|
-
console.log(
|
|
3968
|
-
console.log(
|
|
3969
|
-
console.log(
|
|
5077
|
+
console.log(chalk16.green.bold("\uBE44\uAD50 \uC644\uB8CC!"));
|
|
5078
|
+
console.log(chalk16.dim(` \uCD9C\uB825: ${outputPath}`));
|
|
5079
|
+
console.log(chalk16.dim(` \uD574\uC0C1\uB3C4: ${width}\xD7${height}`));
|
|
5080
|
+
console.log(chalk16.dim(` \uB808\uD37C\uB7F0\uC2A4: ${options.ref}`));
|
|
5081
|
+
console.log(chalk16.dim(` \uD504\uB9AC\uBDF0: ${options.preview}`));
|
|
3970
5082
|
console.log("");
|
|
3971
5083
|
} catch (error) {
|
|
3972
|
-
console.error(
|
|
5084
|
+
console.error(chalk16.red(`\uBE44\uAD50 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`));
|
|
3973
5085
|
process.exit(1);
|
|
3974
5086
|
}
|
|
3975
5087
|
}
|
|
3976
5088
|
function checkPlaywright() {
|
|
3977
5089
|
try {
|
|
3978
|
-
|
|
5090
|
+
execSync2("npx playwright --version", { stdio: "pipe" });
|
|
3979
5091
|
return true;
|
|
3980
5092
|
} catch {
|
|
3981
5093
|
return false;
|
|
@@ -3983,14 +5095,14 @@ function checkPlaywright() {
|
|
|
3983
5095
|
}
|
|
3984
5096
|
function checkCommand(cmd) {
|
|
3985
5097
|
try {
|
|
3986
|
-
|
|
5098
|
+
execSync2(`which ${cmd}`, { stdio: "pipe" });
|
|
3987
5099
|
return true;
|
|
3988
5100
|
} catch {
|
|
3989
5101
|
return false;
|
|
3990
5102
|
}
|
|
3991
5103
|
}
|
|
3992
|
-
function
|
|
3993
|
-
const result =
|
|
5104
|
+
function captureScreenshot2(url, outputPath, width, height) {
|
|
5105
|
+
const result = spawnSync2("npx", [
|
|
3994
5106
|
"playwright",
|
|
3995
5107
|
"screenshot",
|
|
3996
5108
|
"--browser",
|
|
@@ -4013,7 +5125,7 @@ function captureScreenshot(url, outputPath, width, height) {
|
|
|
4013
5125
|
}
|
|
4014
5126
|
|
|
4015
5127
|
// src/commands/diff.ts
|
|
4016
|
-
import
|
|
5128
|
+
import chalk17 from "chalk";
|
|
4017
5129
|
function compareSchemas(draft, published) {
|
|
4018
5130
|
const draftObj = draft;
|
|
4019
5131
|
const pubObj = published;
|
|
@@ -4137,53 +5249,53 @@ function compareSettings(draftSettings, pubSettings, result) {
|
|
|
4137
5249
|
}
|
|
4138
5250
|
function printDiffResult(result) {
|
|
4139
5251
|
if (!result.hasDifferences) {
|
|
4140
|
-
console.log(
|
|
5252
|
+
console.log(chalk17.green("\nDraft\uC640 Published \uC2A4\uD0A4\uB9C8\uAC00 \uB3D9\uC77C\uD569\uB2C8\uB2E4. \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C.\n"));
|
|
4141
5253
|
return;
|
|
4142
5254
|
}
|
|
4143
|
-
console.log(
|
|
5255
|
+
console.log(chalk17.bold("\nStaging vs Production \uC2A4\uD0A4\uB9C8 \uBE44\uAD50:\n"));
|
|
4144
5256
|
if (result.pages.added.length > 0 || result.pages.removed.length > 0 || result.pages.modified.length > 0) {
|
|
4145
|
-
console.log(
|
|
5257
|
+
console.log(chalk17.bold.underline(" Pages"));
|
|
4146
5258
|
for (const page of result.pages.added) {
|
|
4147
|
-
console.log(` ${
|
|
5259
|
+
console.log(` ${chalk17.green("+")} ${page}`);
|
|
4148
5260
|
}
|
|
4149
5261
|
for (const page of result.pages.removed) {
|
|
4150
|
-
console.log(` ${
|
|
5262
|
+
console.log(` ${chalk17.red("-")} ${page}`);
|
|
4151
5263
|
}
|
|
4152
5264
|
for (const page of result.pages.modified) {
|
|
4153
|
-
console.log(` ${
|
|
5265
|
+
console.log(` ${chalk17.yellow("~")} ${page}`);
|
|
4154
5266
|
}
|
|
4155
5267
|
console.log("");
|
|
4156
5268
|
}
|
|
4157
5269
|
if (result.blocks.added.length > 0 || result.blocks.removed.length > 0 || result.blocks.modified.length > 0) {
|
|
4158
|
-
console.log(
|
|
5270
|
+
console.log(chalk17.bold.underline(" Blocks"));
|
|
4159
5271
|
for (const block of result.blocks.added) {
|
|
4160
|
-
console.log(` ${
|
|
5272
|
+
console.log(` ${chalk17.green("+")} ${block}`);
|
|
4161
5273
|
}
|
|
4162
5274
|
for (const block of result.blocks.removed) {
|
|
4163
|
-
console.log(` ${
|
|
5275
|
+
console.log(` ${chalk17.red("-")} ${block}`);
|
|
4164
5276
|
}
|
|
4165
5277
|
for (const block of result.blocks.modified) {
|
|
4166
|
-
console.log(` ${
|
|
5278
|
+
console.log(` ${chalk17.yellow("~")} ${block}`);
|
|
4167
5279
|
}
|
|
4168
5280
|
console.log("");
|
|
4169
5281
|
}
|
|
4170
5282
|
if (result.settings.changed.length > 0) {
|
|
4171
|
-
console.log(
|
|
5283
|
+
console.log(chalk17.bold.underline(" Settings"));
|
|
4172
5284
|
for (const { key, draft, published } of result.settings.changed) {
|
|
4173
5285
|
if (published === void 0) {
|
|
4174
|
-
console.log(` ${
|
|
5286
|
+
console.log(` ${chalk17.green("+")} ${key}: ${chalk17.green(formatValue(draft))}`);
|
|
4175
5287
|
} else if (draft === void 0) {
|
|
4176
|
-
console.log(` ${
|
|
5288
|
+
console.log(` ${chalk17.red("-")} ${key}: ${chalk17.red(formatValue(published))}`);
|
|
4177
5289
|
} else {
|
|
4178
|
-
console.log(` ${
|
|
4179
|
-
console.log(` ${
|
|
4180
|
-
console.log(` ${
|
|
5290
|
+
console.log(` ${chalk17.yellow("~")} ${key}:`);
|
|
5291
|
+
console.log(` ${chalk17.red(`- ${formatValue(published)}`)}`);
|
|
5292
|
+
console.log(` ${chalk17.green(`+ ${formatValue(draft)}`)}`);
|
|
4181
5293
|
}
|
|
4182
5294
|
}
|
|
4183
5295
|
console.log("");
|
|
4184
5296
|
}
|
|
4185
5297
|
const totalChanges = result.pages.added.length + result.pages.removed.length + result.pages.modified.length + result.blocks.added.length + result.blocks.removed.length + result.blocks.modified.length + result.settings.changed.length;
|
|
4186
|
-
console.log(
|
|
5298
|
+
console.log(chalk17.dim(` \uCD1D ${totalChanges}\uAC74\uC758 \uBCC0\uACBD \uC0AC\uD56D
|
|
4187
5299
|
`));
|
|
4188
5300
|
}
|
|
4189
5301
|
function formatValue(value) {
|
|
@@ -4198,10 +5310,10 @@ function formatValue(value) {
|
|
|
4198
5310
|
async function commandDiff(options) {
|
|
4199
5311
|
const projectConfig = await loadProjectConfig();
|
|
4200
5312
|
if (!projectConfig?.siteId) {
|
|
4201
|
-
console.error(
|
|
4202
|
-
console.error(
|
|
4203
|
-
console.error(
|
|
4204
|
-
console.error(
|
|
5313
|
+
console.error(chalk17.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
|
|
5314
|
+
console.error(chalk17.yellow("\uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC2E4\uD589\uD558\uC138\uC694:"));
|
|
5315
|
+
console.error(chalk17.dim(" 1. npx @saeroon/cli init --from-site <siteId> \uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654"));
|
|
5316
|
+
console.error(chalk17.dim(' 2. saeroon.config.json\uC5D0 "siteId" \uD544\uB4DC\uB97C \uC9C1\uC811 \uCD94\uAC00'));
|
|
4205
5317
|
console.error("");
|
|
4206
5318
|
process.exit(1);
|
|
4207
5319
|
}
|
|
@@ -4209,8 +5321,8 @@ async function commandDiff(options) {
|
|
|
4209
5321
|
const apiKey = await resolveApiKey(options.apiKey);
|
|
4210
5322
|
const apiBaseUrl = await getApiBaseUrl();
|
|
4211
5323
|
const client = new SaeroonApiClient(apiKey, apiBaseUrl);
|
|
4212
|
-
console.log(
|
|
4213
|
-
console.log(
|
|
5324
|
+
console.log(chalk17.bold("\nStaging vs Production \uC2A4\uD0A4\uB9C8 \uBE44\uAD50\n"));
|
|
5325
|
+
console.log(chalk17.dim(` \uC0AC\uC774\uD2B8 ID: ${siteId}`));
|
|
4214
5326
|
console.log("");
|
|
4215
5327
|
const fetchSpinner = spinner("Draft \uBC0F Published \uC2A4\uD0A4\uB9C8\uB97C \uAC00\uC838\uC624\uB294 \uC911...");
|
|
4216
5328
|
let draftSchema;
|
|
@@ -4223,16 +5335,16 @@ async function commandDiff(options) {
|
|
|
4223
5335
|
draftSchema = safeJsonParse(draftResult.schemaJson);
|
|
4224
5336
|
publishedSchema = safeJsonParse(publishedResult.schemaJson);
|
|
4225
5337
|
fetchSpinner.stop("");
|
|
4226
|
-
console.log(
|
|
4227
|
-
console.log(
|
|
5338
|
+
console.log(chalk17.dim(` Draft: ${draftResult.isDraft ? "Draft" : "Published"} (editVersion: ${draftResult.editVersion})`));
|
|
5339
|
+
console.log(chalk17.dim(` Published: ${publishedResult.isDraft ? "Draft" : "Published"} (editVersion: ${publishedResult.editVersion})`));
|
|
4228
5340
|
} catch (error) {
|
|
4229
5341
|
if (error instanceof ApiError) {
|
|
4230
5342
|
fetchSpinner.stop(
|
|
4231
|
-
|
|
5343
|
+
chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
|
|
4232
5344
|
);
|
|
4233
5345
|
} else {
|
|
4234
5346
|
fetchSpinner.stop(
|
|
4235
|
-
|
|
5347
|
+
chalk17.red(
|
|
4236
5348
|
`\uC2A4\uD0A4\uB9C8 \uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
|
|
4237
5349
|
)
|
|
4238
5350
|
);
|
|
@@ -4244,7 +5356,7 @@ async function commandDiff(options) {
|
|
|
4244
5356
|
}
|
|
4245
5357
|
|
|
4246
5358
|
// src/commands/template.ts
|
|
4247
|
-
import
|
|
5359
|
+
import chalk18 from "chalk";
|
|
4248
5360
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
4249
5361
|
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
4250
5362
|
var TEMPLATE_CATEGORIES2 = [
|
|
@@ -4265,26 +5377,26 @@ var TEMPLATE_CATEGORIES2 = [
|
|
|
4265
5377
|
async function commandTemplateRegister(options) {
|
|
4266
5378
|
const projectConfig = await loadProjectConfig();
|
|
4267
5379
|
if (!projectConfig?.siteId) {
|
|
4268
|
-
console.error(
|
|
4269
|
-
console.error(
|
|
4270
|
-
console.error(
|
|
4271
|
-
console.error(
|
|
5380
|
+
console.error(chalk18.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
|
|
5381
|
+
console.error(chalk18.yellow("\uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC2E4\uD589\uD558\uC138\uC694:"));
|
|
5382
|
+
console.error(chalk18.dim(" 1. npx @saeroon/cli init --from-site <siteId> \uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654"));
|
|
5383
|
+
console.error(chalk18.dim(' 2. saeroon.config.json\uC5D0 "siteId" \uD544\uB4DC\uB97C \uC9C1\uC811 \uCD94\uAC00'));
|
|
4272
5384
|
console.error("");
|
|
4273
5385
|
process.exit(1);
|
|
4274
5386
|
}
|
|
4275
5387
|
if (projectConfig.templateId) {
|
|
4276
|
-
console.error(
|
|
4277
|
-
console.error(
|
|
4278
|
-
console.error(
|
|
4279
|
-
console.error(
|
|
5388
|
+
console.error(chalk18.yellow("\n\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uB294 \uC774\uBBF8 \uD15C\uD50C\uB9BF\uC774 \uB4F1\uB85D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4."));
|
|
5389
|
+
console.error(chalk18.dim(` templateId: ${projectConfig.templateId}`));
|
|
5390
|
+
console.error(chalk18.dim(" \uBA54\uD0C0\uB370\uC774\uD130 \uC218\uC815: saeroon template update"));
|
|
5391
|
+
console.error(chalk18.dim(" \uBC84\uC804 \uB3D9\uAE30\uD654: saeroon template sync"));
|
|
4280
5392
|
console.error("");
|
|
4281
5393
|
process.exit(1);
|
|
4282
5394
|
}
|
|
4283
5395
|
const apiKey = await resolveApiKey(options.apiKey);
|
|
4284
5396
|
const apiBaseUrl = await getApiBaseUrl();
|
|
4285
5397
|
const client = new SaeroonApiClient(apiKey, apiBaseUrl);
|
|
4286
|
-
console.log(
|
|
4287
|
-
console.log(
|
|
5398
|
+
console.log(chalk18.bold("\n\uD15C\uD50C\uB9BF \uB4F1\uB85D\n"));
|
|
5399
|
+
console.log(chalk18.dim(` \uC18C\uC2A4 \uC0AC\uC774\uD2B8: ${projectConfig.siteId}`));
|
|
4288
5400
|
console.log("");
|
|
4289
5401
|
let name = options.name ?? "";
|
|
4290
5402
|
let category = options.category ?? "";
|
|
@@ -4298,15 +5410,15 @@ async function commandTemplateRegister(options) {
|
|
|
4298
5410
|
while (!name) {
|
|
4299
5411
|
name = (await rl.question(" \uC774\uB984: ")).trim();
|
|
4300
5412
|
if (!name) {
|
|
4301
|
-
console.log(
|
|
5413
|
+
console.log(chalk18.red(" \uC774\uB984\uC740 \uD544\uC218\uC785\uB2C8\uB2E4."));
|
|
4302
5414
|
}
|
|
4303
5415
|
}
|
|
4304
5416
|
}
|
|
4305
5417
|
if (!category) {
|
|
4306
5418
|
console.log("");
|
|
4307
|
-
console.log(
|
|
5419
|
+
console.log(chalk18.dim(" \uCE74\uD14C\uACE0\uB9AC \uBAA9\uB85D:"));
|
|
4308
5420
|
TEMPLATE_CATEGORIES2.forEach((cat, i) => {
|
|
4309
|
-
console.log(
|
|
5421
|
+
console.log(chalk18.dim(` ${i + 1}. ${cat}`));
|
|
4310
5422
|
});
|
|
4311
5423
|
console.log("");
|
|
4312
5424
|
while (!category) {
|
|
@@ -4323,7 +5435,7 @@ async function commandTemplateRegister(options) {
|
|
|
4323
5435
|
} else if (input) {
|
|
4324
5436
|
category = input;
|
|
4325
5437
|
} else {
|
|
4326
|
-
console.log(
|
|
5438
|
+
console.log(chalk18.red(" \uCE74\uD14C\uACE0\uB9AC\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694."));
|
|
4327
5439
|
}
|
|
4328
5440
|
}
|
|
4329
5441
|
}
|
|
@@ -4350,22 +5462,22 @@ async function commandTemplateRegister(options) {
|
|
|
4350
5462
|
sourceSiteId: projectConfig.siteId
|
|
4351
5463
|
};
|
|
4352
5464
|
const result = await client.registerTemplate(request);
|
|
4353
|
-
registerSpinner.stop(
|
|
5465
|
+
registerSpinner.stop(chalk18.green("\uD15C\uD50C\uB9BF \uB4F1\uB85D \uC644\uB8CC!"));
|
|
4354
5466
|
await saveProjectConfig({ templateId: result.id });
|
|
4355
5467
|
if (options.json) {
|
|
4356
5468
|
console.log(JSON.stringify(result, null, 2));
|
|
4357
5469
|
} else {
|
|
4358
5470
|
formatTemplateDetail(result);
|
|
4359
|
-
console.log(
|
|
4360
|
-
console.log(
|
|
5471
|
+
console.log(chalk18.dim(" templateId\uAC00 saeroon.config.json\uC5D0 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
5472
|
+
console.log(chalk18.dim(" \uBC84\uC804 \uB3D9\uAE30\uD654: saeroon template sync"));
|
|
4361
5473
|
console.log("");
|
|
4362
5474
|
}
|
|
4363
5475
|
} catch (error) {
|
|
4364
5476
|
if (error instanceof ApiError) {
|
|
4365
|
-
registerSpinner.stop(
|
|
5477
|
+
registerSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
|
|
4366
5478
|
} else {
|
|
4367
5479
|
registerSpinner.stop(
|
|
4368
|
-
|
|
5480
|
+
chalk18.red(`\uB4F1\uB85D \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
|
|
4369
5481
|
);
|
|
4370
5482
|
}
|
|
4371
5483
|
process.exit(1);
|
|
@@ -4374,22 +5486,22 @@ async function commandTemplateRegister(options) {
|
|
|
4374
5486
|
async function commandTemplateSync(options) {
|
|
4375
5487
|
const projectConfig = await loadProjectConfig();
|
|
4376
5488
|
if (!projectConfig?.templateId) {
|
|
4377
|
-
console.error(
|
|
4378
|
-
console.error(
|
|
4379
|
-
console.error(
|
|
5489
|
+
console.error(chalk18.red("\ntemplateId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
|
|
5490
|
+
console.error(chalk18.yellow("\uBA3C\uC800 \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD558\uC138\uC694:"));
|
|
5491
|
+
console.error(chalk18.dim(" saeroon template register"));
|
|
4380
5492
|
console.error("");
|
|
4381
5493
|
process.exit(1);
|
|
4382
5494
|
}
|
|
4383
5495
|
if (!projectConfig.siteId) {
|
|
4384
|
-
console.error(
|
|
5496
|
+
console.error(chalk18.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
|
|
4385
5497
|
process.exit(1);
|
|
4386
5498
|
}
|
|
4387
5499
|
const apiKey = await resolveApiKey(options.apiKey);
|
|
4388
5500
|
const apiBaseUrl = await getApiBaseUrl();
|
|
4389
5501
|
const client = new SaeroonApiClient(apiKey, apiBaseUrl);
|
|
4390
|
-
console.log(
|
|
4391
|
-
console.log(
|
|
4392
|
-
console.log(
|
|
5502
|
+
console.log(chalk18.bold("\n\uD15C\uD50C\uB9BF \uBC84\uC804 \uB3D9\uAE30\uD654\n"));
|
|
5503
|
+
console.log(chalk18.dim(` \uC18C\uC2A4 \uC0AC\uC774\uD2B8: ${projectConfig.siteId}`));
|
|
5504
|
+
console.log(chalk18.dim(` \uD15C\uD50C\uB9BF ID: ${projectConfig.templateId}`));
|
|
4393
5505
|
console.log("");
|
|
4394
5506
|
if (!options.force) {
|
|
4395
5507
|
const diffSpinner = spinner("\uC2A4\uD0A4\uB9C8 \uBCC0\uACBD \uC0AC\uD56D \uD655\uC778 \uC911...");
|
|
@@ -4401,17 +5513,17 @@ async function commandTemplateSync(options) {
|
|
|
4401
5513
|
diffSpinner.stop("");
|
|
4402
5514
|
const diff = compareSchemas(safeJsonParse(draftResult.schemaJson), safeJsonParse(publishedResult.schemaJson));
|
|
4403
5515
|
if (!diff.hasDifferences) {
|
|
4404
|
-
console.log(
|
|
5516
|
+
console.log(chalk18.green(" \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C. \uC774\uBBF8 \uCD5C\uC2E0 \uC0C1\uD0DC\uC785\uB2C8\uB2E4.\n"));
|
|
4405
5517
|
return;
|
|
4406
5518
|
}
|
|
4407
5519
|
printSyncDiffSummary(diff);
|
|
4408
5520
|
const rl = createInterface5({ input: stdin5, output: stdout5 });
|
|
4409
5521
|
try {
|
|
4410
5522
|
const answer = await rl.question(
|
|
4411
|
-
|
|
5523
|
+
chalk18.yellow(" \uC774 \uBCC0\uACBD \uC0AC\uD56D\uC73C\uB85C \uC0C8 \uBC84\uC804\uC744 \uBC1C\uD589\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? (y/N): ")
|
|
4412
5524
|
);
|
|
4413
5525
|
if (answer.trim().toLowerCase() !== "y") {
|
|
4414
|
-
console.log(
|
|
5526
|
+
console.log(chalk18.dim("\n \uB3D9\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
|
|
4415
5527
|
return;
|
|
4416
5528
|
}
|
|
4417
5529
|
} finally {
|
|
@@ -4419,10 +5531,10 @@ async function commandTemplateSync(options) {
|
|
|
4419
5531
|
}
|
|
4420
5532
|
} catch (error) {
|
|
4421
5533
|
if (error instanceof ApiError) {
|
|
4422
|
-
diffSpinner.stop(
|
|
5534
|
+
diffSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
|
|
4423
5535
|
} else {
|
|
4424
5536
|
diffSpinner.stop(
|
|
4425
|
-
|
|
5537
|
+
chalk18.red(`\uC2A4\uD0A4\uB9C8 \uBE44\uAD50 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
|
|
4426
5538
|
);
|
|
4427
5539
|
}
|
|
4428
5540
|
process.exit(1);
|
|
@@ -4432,22 +5544,22 @@ async function commandTemplateSync(options) {
|
|
|
4432
5544
|
const syncSpinner = spinner("\uC0C8 \uBC84\uC804 \uBC1C\uD589 \uC911...");
|
|
4433
5545
|
try {
|
|
4434
5546
|
const result = await client.syncTemplateVersion(projectConfig.templateId);
|
|
4435
|
-
syncSpinner.stop(
|
|
5547
|
+
syncSpinner.stop(chalk18.green(`\uD15C\uD50C\uB9BF v${result.version} \uBC1C\uD589 \uC644\uB8CC!`));
|
|
4436
5548
|
if (options.json) {
|
|
4437
5549
|
console.log(JSON.stringify(result, null, 2));
|
|
4438
5550
|
} else {
|
|
4439
5551
|
console.log("");
|
|
4440
5552
|
console.log(` \uBC84\uC804: v${result.version}`);
|
|
4441
5553
|
console.log(` \uC774\uB984: ${result.name}`);
|
|
4442
|
-
console.log(` \uBC1C\uD589: ${
|
|
5554
|
+
console.log(` \uBC1C\uD589: ${chalk18.dim(result.updatedAt ?? "")}`);
|
|
4443
5555
|
console.log("");
|
|
4444
5556
|
}
|
|
4445
5557
|
} catch (error) {
|
|
4446
5558
|
if (error instanceof ApiError) {
|
|
4447
|
-
syncSpinner.stop(
|
|
5559
|
+
syncSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
|
|
4448
5560
|
} else {
|
|
4449
5561
|
syncSpinner.stop(
|
|
4450
|
-
|
|
5562
|
+
chalk18.red(`\uB3D9\uAE30\uD654 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
|
|
4451
5563
|
);
|
|
4452
5564
|
}
|
|
4453
5565
|
process.exit(1);
|
|
@@ -4468,10 +5580,10 @@ async function commandTemplateStatus(options) {
|
|
|
4468
5580
|
}
|
|
4469
5581
|
} catch (error) {
|
|
4470
5582
|
if (error instanceof ApiError) {
|
|
4471
|
-
fetchSpinner.stop(
|
|
5583
|
+
fetchSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
|
|
4472
5584
|
} else {
|
|
4473
5585
|
fetchSpinner.stop(
|
|
4474
|
-
|
|
5586
|
+
chalk18.red(`\uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
|
|
4475
5587
|
);
|
|
4476
5588
|
}
|
|
4477
5589
|
process.exit(1);
|
|
@@ -4480,9 +5592,9 @@ async function commandTemplateStatus(options) {
|
|
|
4480
5592
|
async function commandTemplateUpdate(options) {
|
|
4481
5593
|
const projectConfig = await loadProjectConfig();
|
|
4482
5594
|
if (!projectConfig?.templateId) {
|
|
4483
|
-
console.error(
|
|
4484
|
-
console.error(
|
|
4485
|
-
console.error(
|
|
5595
|
+
console.error(chalk18.red("\ntemplateId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
|
|
5596
|
+
console.error(chalk18.yellow("\uBA3C\uC800 \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD558\uC138\uC694:"));
|
|
5597
|
+
console.error(chalk18.dim(" saeroon template register"));
|
|
4486
5598
|
console.error("");
|
|
4487
5599
|
process.exit(1);
|
|
4488
5600
|
}
|
|
@@ -4521,22 +5633,22 @@ async function commandTemplateUpdate(options) {
|
|
|
4521
5633
|
currentSpinner.stop("");
|
|
4522
5634
|
} catch (error) {
|
|
4523
5635
|
if (error instanceof ApiError) {
|
|
4524
|
-
currentSpinner.stop(
|
|
5636
|
+
currentSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
|
|
4525
5637
|
} else {
|
|
4526
|
-
currentSpinner.stop(
|
|
5638
|
+
currentSpinner.stop(chalk18.red("\uC870\uD68C \uC2E4\uD328"));
|
|
4527
5639
|
}
|
|
4528
5640
|
process.exit(1);
|
|
4529
5641
|
}
|
|
4530
5642
|
const current = templates.find((t) => t.id === projectConfig.templateId);
|
|
4531
5643
|
if (!current) {
|
|
4532
|
-
console.error(
|
|
5644
|
+
console.error(chalk18.red(`
|
|
4533
5645
|
\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${projectConfig.templateId}
|
|
4534
5646
|
`));
|
|
4535
5647
|
process.exit(1);
|
|
4536
5648
|
}
|
|
4537
|
-
console.log(
|
|
5649
|
+
console.log(chalk18.bold("\n\uD604\uC7AC \uD15C\uD50C\uB9BF \uC815\uBCF4:"));
|
|
4538
5650
|
formatTemplateDetail(current);
|
|
4539
|
-
console.log(
|
|
5651
|
+
console.log(chalk18.bold("\uC218\uC815\uD560 \uD56D\uBAA9\uC744 \uC785\uB825\uD558\uC138\uC694 (\uBE48 \uAC12 = \uBCC0\uACBD \uC5C6\uC74C):\n"));
|
|
4540
5652
|
const newName = (await rl.question(` \uC774\uB984 [${current.name}]: `)).trim();
|
|
4541
5653
|
if (newName) {
|
|
4542
5654
|
request.name = newName;
|
|
@@ -4562,14 +5674,14 @@ async function commandTemplateUpdate(options) {
|
|
|
4562
5674
|
}
|
|
4563
5675
|
}
|
|
4564
5676
|
if (!hasChanges) {
|
|
4565
|
-
console.log(
|
|
5677
|
+
console.log(chalk18.dim("\n \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C.\n"));
|
|
4566
5678
|
return;
|
|
4567
5679
|
}
|
|
4568
5680
|
console.log("");
|
|
4569
5681
|
const updateSpinner = spinner("\uD15C\uD50C\uB9BF \uBA54\uD0C0\uB370\uC774\uD130 \uC218\uC815 \uC911...");
|
|
4570
5682
|
try {
|
|
4571
5683
|
const result = await client.updateTemplate(projectConfig.templateId, request);
|
|
4572
|
-
updateSpinner.stop(
|
|
5684
|
+
updateSpinner.stop(chalk18.green("\uD15C\uD50C\uB9BF \uC218\uC815 \uC644\uB8CC!"));
|
|
4573
5685
|
if (options.json) {
|
|
4574
5686
|
console.log(JSON.stringify(result, null, 2));
|
|
4575
5687
|
} else {
|
|
@@ -4577,10 +5689,10 @@ async function commandTemplateUpdate(options) {
|
|
|
4577
5689
|
}
|
|
4578
5690
|
} catch (error) {
|
|
4579
5691
|
if (error instanceof ApiError) {
|
|
4580
|
-
updateSpinner.stop(
|
|
5692
|
+
updateSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
|
|
4581
5693
|
} else {
|
|
4582
5694
|
updateSpinner.stop(
|
|
4583
|
-
|
|
5695
|
+
chalk18.red(`\uC218\uC815 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
|
|
4584
5696
|
);
|
|
4585
5697
|
}
|
|
4586
5698
|
process.exit(1);
|
|
@@ -4593,27 +5705,27 @@ function printSyncDiffSummary(diff) {
|
|
|
4593
5705
|
const settingChanges = diff.settings.changed.length;
|
|
4594
5706
|
if (pageChanges > 0) {
|
|
4595
5707
|
const detail = [];
|
|
4596
|
-
if (diff.pages.added.length > 0) detail.push(
|
|
4597
|
-
if (diff.pages.removed.length > 0) detail.push(
|
|
4598
|
-
if (diff.pages.modified.length > 0) detail.push(
|
|
5708
|
+
if (diff.pages.added.length > 0) detail.push(chalk18.green(`+${diff.pages.added.length}`));
|
|
5709
|
+
if (diff.pages.removed.length > 0) detail.push(chalk18.red(`-${diff.pages.removed.length}`));
|
|
5710
|
+
if (diff.pages.modified.length > 0) detail.push(chalk18.yellow(`~${diff.pages.modified.length}`));
|
|
4599
5711
|
parts.push(`\uD398\uC774\uC9C0 ${detail.join(" ")}`);
|
|
4600
5712
|
}
|
|
4601
5713
|
if (blockChanges > 0) {
|
|
4602
5714
|
const detail = [];
|
|
4603
|
-
if (diff.blocks.added.length > 0) detail.push(
|
|
4604
|
-
if (diff.blocks.removed.length > 0) detail.push(
|
|
4605
|
-
if (diff.blocks.modified.length > 0) detail.push(
|
|
5715
|
+
if (diff.blocks.added.length > 0) detail.push(chalk18.green(`+${diff.blocks.added.length}`));
|
|
5716
|
+
if (diff.blocks.removed.length > 0) detail.push(chalk18.red(`-${diff.blocks.removed.length}`));
|
|
5717
|
+
if (diff.blocks.modified.length > 0) detail.push(chalk18.yellow(`~${diff.blocks.modified.length}`));
|
|
4606
5718
|
parts.push(`\uBE14\uB85D ${detail.join(" ")}`);
|
|
4607
5719
|
}
|
|
4608
5720
|
if (settingChanges > 0) {
|
|
4609
|
-
parts.push(`\uC124\uC815 ${
|
|
5721
|
+
parts.push(`\uC124\uC815 ${chalk18.yellow(`~${settingChanges}`)}`);
|
|
4610
5722
|
}
|
|
4611
5723
|
console.log(` \uBCC0\uACBD \uAC10\uC9C0: ${parts.join(", ")}`);
|
|
4612
5724
|
console.log("");
|
|
4613
5725
|
}
|
|
4614
5726
|
|
|
4615
5727
|
// src/commands/pattern.ts
|
|
4616
|
-
import
|
|
5728
|
+
import chalk19 from "chalk";
|
|
4617
5729
|
var V2_PATTERNS = [
|
|
4618
5730
|
{ id: "v2-faq-accordion", name: "FAQ \uC544\uCF54\uB514\uC5B8", nameEn: "FAQ Accordion", category: "interactive", description: "\uB124\uC774\uD2F0\uBE0C details/summary \uAE30\uBC18 \uC811\uAE30/\uD3BC\uCE58\uAE30" },
|
|
4619
5731
|
{ id: "v2-card-grid", name: "\uCE74\uB4DC \uADF8\uB9AC\uB4DC", nameEn: "Card Grid", category: "content", description: "repeat \uAE30\uBC18 \uCE74\uB4DC \uB808\uC774\uC544\uC6C3" },
|
|
@@ -4646,64 +5758,64 @@ async function commandPatterns(options) {
|
|
|
4646
5758
|
sort: "popular",
|
|
4647
5759
|
pageSize: 50
|
|
4648
5760
|
});
|
|
4649
|
-
fetchSpinner.stop(
|
|
5761
|
+
fetchSpinner.stop(chalk19.green(`${result.total}\uAC1C\uC758 \uACF5\uAC1C \uD328\uD134\uC744 \uC870\uD68C\uD588\uC2B5\uB2C8\uB2E4.`));
|
|
4650
5762
|
if (result.data.length === 0) {
|
|
4651
|
-
console.log(
|
|
5763
|
+
console.log(chalk19.gray("\n \uACF5\uAC1C \uD328\uD134\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
|
|
4652
5764
|
return;
|
|
4653
5765
|
}
|
|
4654
5766
|
console.log("");
|
|
4655
5767
|
console.log(
|
|
4656
|
-
` ${
|
|
5768
|
+
` ${chalk19.gray("ID".padEnd(10))} ${"\uC774\uB984".padEnd(18)} ${chalk19.gray("\uCE74\uD14C\uACE0\uB9AC".padEnd(12))} ${chalk19.gray("\uC0AC\uC6A9")} ${chalk19.gray("\uC791\uC131\uC790")}`
|
|
4657
5769
|
);
|
|
4658
|
-
console.log(
|
|
5770
|
+
console.log(chalk19.gray(" " + "-".repeat(70)));
|
|
4659
5771
|
for (const p of result.data) {
|
|
4660
5772
|
const shortId = p.id.substring(0, 8);
|
|
4661
5773
|
console.log(
|
|
4662
|
-
` ${
|
|
5774
|
+
` ${chalk19.cyan(shortId.padEnd(10))} ${p.name.padEnd(18)} ${chalk19.gray(p.category.padEnd(12))} ${chalk19.yellow(String(p.usageCount).padEnd(4))} ${chalk19.gray(p.authorName ?? "")}`
|
|
4663
5775
|
);
|
|
4664
5776
|
}
|
|
4665
|
-
console.log(
|
|
5777
|
+
console.log(chalk19.gray(`
|
|
4666
5778
|
Fork: npx @saeroon/cli fork-pattern <pattern-id> --site-id <site-id>
|
|
4667
5779
|
`));
|
|
4668
5780
|
} catch (error) {
|
|
4669
5781
|
fetchSpinner.stop(
|
|
4670
|
-
|
|
5782
|
+
chalk19.red(`\uACF5\uAC1C \uD328\uD134 \uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
|
|
4671
5783
|
);
|
|
4672
5784
|
process.exit(1);
|
|
4673
5785
|
}
|
|
4674
5786
|
return;
|
|
4675
5787
|
}
|
|
4676
|
-
console.log(
|
|
5788
|
+
console.log(chalk19.bold("\n V2 Patterns:\n"));
|
|
4677
5789
|
const patterns = options?.role ? V2_PATTERNS.filter((p) => p.category === options.role) : V2_PATTERNS;
|
|
4678
5790
|
for (const p of patterns) {
|
|
4679
|
-
console.log(` ${
|
|
5791
|
+
console.log(` ${chalk19.cyan(p.id.padEnd(24))} ${p.name.padEnd(12)} ${chalk19.gray(p.description)}`);
|
|
4680
5792
|
}
|
|
4681
|
-
console.log(
|
|
5793
|
+
console.log(chalk19.bold("\n V1 Patterns (legacy):\n"));
|
|
4682
5794
|
for (const p of V1_PATTERNS) {
|
|
4683
|
-
console.log(` ${
|
|
5795
|
+
console.log(` ${chalk19.gray(p.id.padEnd(24))} ${p.name.padEnd(12)} ${chalk19.gray(p.description)}`);
|
|
4684
5796
|
}
|
|
4685
|
-
console.log(
|
|
5797
|
+
console.log(chalk19.gray(`
|
|
4686
5798
|
\uCD1D ${V2_PATTERNS.length + V1_PATTERNS.length}\uAC1C \uB0B4\uC7A5 \uD328\uD134`));
|
|
4687
|
-
console.log(
|
|
5799
|
+
console.log(chalk19.gray(` \uACF5\uAC1C \uD328\uD134 \uC870\uD68C: npx @saeroon/cli patterns --public
|
|
4688
5800
|
`));
|
|
4689
5801
|
}
|
|
4690
5802
|
async function commandAddPattern(patternId) {
|
|
4691
5803
|
const pattern = V2_PATTERNS.find((p) => p.id === patternId) || V1_PATTERNS.find((p) => p.id === patternId);
|
|
4692
5804
|
if (!pattern) {
|
|
4693
|
-
console.error(
|
|
4694
|
-
console.log(
|
|
5805
|
+
console.error(chalk19.red(`Pattern "${patternId}" not found.`));
|
|
5806
|
+
console.log(chalk19.gray("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD328\uD134: " + [...V2_PATTERNS, ...V1_PATTERNS].map((p) => p.id).join(", ")));
|
|
4695
5807
|
process.exit(1);
|
|
4696
5808
|
}
|
|
4697
|
-
console.log(
|
|
5809
|
+
console.log(chalk19.cyan(`
|
|
4698
5810
|
Pattern: ${pattern.name} (${pattern.nameEn})`));
|
|
4699
|
-
console.log(
|
|
4700
|
-
console.log(
|
|
4701
|
-
console.log(
|
|
4702
|
-
console.log(
|
|
5811
|
+
console.log(chalk19.gray(` Category: ${pattern.category}`));
|
|
5812
|
+
console.log(chalk19.gray(` ${pattern.description}`));
|
|
5813
|
+
console.log(chalk19.yellow("\n \uD328\uD134 \uC0BD\uC785\uC740 \uC5D0\uB514\uD130 \uB610\uB294 MCP\uC5D0\uC11C \uC2E4\uD589\uD558\uC138\uC694."));
|
|
5814
|
+
console.log(chalk19.gray(" MCP: get_pattern \u2192 \uC2A4\uD0A4\uB9C8 JSON \uBC18\uD658\n"));
|
|
4703
5815
|
}
|
|
4704
5816
|
async function commandForkPattern(patternId, options) {
|
|
4705
5817
|
if (!options.siteId) {
|
|
4706
|
-
console.error(
|
|
5818
|
+
console.error(chalk19.red("--site-id \uC635\uC158\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."));
|
|
4707
5819
|
process.exit(1);
|
|
4708
5820
|
}
|
|
4709
5821
|
const apiBaseUrl = await getApiBaseUrl();
|
|
@@ -4712,10 +5824,10 @@ async function commandForkPattern(patternId, options) {
|
|
|
4712
5824
|
const forkSpinner = spinner("\uACF5\uAC1C \uD328\uD134\uC744 Fork\uD558\uB294 \uC911...");
|
|
4713
5825
|
try {
|
|
4714
5826
|
await client.forkPublicPattern(patternId, options.siteId);
|
|
4715
|
-
forkSpinner.stop(
|
|
5827
|
+
forkSpinner.stop(chalk19.green(`\uD328\uD134\uC774 \uC0AC\uC774\uD2B8 ${options.siteId}\uB85C Fork\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`));
|
|
4716
5828
|
} catch (error) {
|
|
4717
5829
|
forkSpinner.stop(
|
|
4718
|
-
|
|
5830
|
+
chalk19.red(`Fork \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
|
|
4719
5831
|
);
|
|
4720
5832
|
process.exit(1);
|
|
4721
5833
|
}
|
|
@@ -4737,7 +5849,8 @@ program.command("add-pattern").description("\uC2A4\uD0A4\uB9C8\uC5D0 \uD328\uD13
|
|
|
4737
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);
|
|
4738
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);
|
|
4739
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);
|
|
4740
|
-
program.command("
|
|
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);
|
|
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);
|
|
4741
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);
|
|
4742
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);
|
|
4743
5856
|
program.command("publish").description("[deprecated] \uB9C8\uCF13\uD50C\uB808\uC774\uC2A4\uC5D0 \uD15C\uD50C\uB9BF \uB4F1\uB85D \u2192 saeroon template register \uC0AC\uC6A9").argument("[schema-path]", "\uC81C\uCD9C\uD560 \uC2A4\uD0A4\uB9C8 JSON \uD30C\uC77C \uACBD\uB85C", "schema.json").option("--api-key <key>", "API Key").action(commandPublish);
|