@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.
Files changed (3) hide show
  1. package/README.md +21 -3
  2. package/dist/index.js +1371 -258
  3. 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((resolve13, reject) => {
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
- resolve13();
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
- // Element (9)
2523
+ // Primitives (10)
2524
+ "container",
2473
2525
  "text-block",
2474
2526
  "heading-block",
2475
2527
  "button-block",
2476
2528
  "image-block",
2477
- "video-block",
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
- // Pattern (13, legacy but valid)
2561
- "hero",
2562
- "cta-banner",
2563
- "feature-grid",
2564
- "pricing-table",
2565
- "team-profile-section",
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.schemaVersion && !s.version) {
2583
- errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "schemaVersion"', path: "schemaVersion", step: 1 });
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.global && !s.settings) {
2586
- errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "global"', path: "global", step: 1 });
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.schemaVersion ?? s.version;
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.20.0`,
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
- const global = s.global ?? s.settings;
2604
- if (global && typeof global === "object") {
2605
- if (!global.name && !global.title) {
2606
- errors.push({
2607
- severity: "error",
2608
- message: "global.name (\uB610\uB294 settings.title) \uC740 \uD544\uC218\uC785\uB2C8\uB2E4.",
2609
- path: "global.name",
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((resolve13, reject) => {
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", () => resolve13(hash.digest("hex")));
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 resolve13 = queue.shift();
2980
- resolve13?.();
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((resolve13) => {
2986
- queue.push(resolve13);
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/compare.ts
3914
+ // src/commands/analyze.ts
3909
3915
  import chalk15 from "chalk";
3910
- import { resolve as resolve12 } from "path";
3911
- import { execSync, spawnSync } from "child_process";
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(chalk15.bold("\n\uC2DC\uAC01 \uBE44\uAD50 (Visual Diff)\n"));
4873
+ console.log(chalk16.bold("\n\uC2DC\uAC01 \uBE44\uAD50 (Visual Diff)\n"));
3914
4874
  if (!options.ref || !options.preview) {
3915
- console.error(chalk15.red("--ref <url> \uACFC --preview <url> \uC740 \uBAA8\uB450 \uD544\uC218\uC785\uB2C8\uB2E4."));
3916
- console.error(chalk15.dim(" \uC608: npx @saeroon/cli compare --ref https://example.com --preview https://preview.saeroon.com/abc"));
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(chalk15.yellow("Playwright\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
3922
- console.log(chalk15.dim(" \uB2E4\uC74C \uBA85\uB839\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694:"));
3923
- console.log(chalk15.cyan(" npx playwright install chromium\n"));
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 = resolve12(process.cwd(), options.output ?? "compare-result.png");
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 = resolve12(process.cwd(), ".compare-ref.png");
3932
- const previewScreenshot = resolve12(process.cwd(), ".compare-preview.png");
3933
- captureScreenshot(options.ref, refScreenshot, width, height);
3934
- captureSpinner.stop(chalk15.green(" \uB808\uD37C\uB7F0\uC2A4 \uCEA1\uCC98 \uC644\uB8CC"));
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
- captureScreenshot(options.preview, previewScreenshot, width, height);
3937
- previewSpinner.stop(chalk15.green(" \uD504\uB9AC\uBDF0 \uCEA1\uCC98 \uC644\uB8CC"));
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
- execSync(
5054
+ execSync2(
3943
5055
  `magick compare -metric AE "${refScreenshot}" "${previewScreenshot}" "${outputPath}" 2>&1`,
3944
5056
  { stdio: "pipe" }
3945
5057
  );
3946
5058
  } catch {
3947
- execSync(
5059
+ execSync2(
3948
5060
  `magick composite -blend 50x50 "${refScreenshot}" "${previewScreenshot}" "${outputPath}"`,
3949
5061
  { stdio: "pipe" }
3950
5062
  );
3951
5063
  }
3952
- diffSpinner.stop(chalk15.green(" Diff \uC774\uBBF8\uC9C0 \uC0DD\uC131 \uC644\uB8CC (ImageMagick)"));
5064
+ diffSpinner.stop(chalk16.green(" Diff \uC774\uBBF8\uC9C0 \uC0DD\uC131 \uC644\uB8CC (ImageMagick)"));
3953
5065
  } else {
3954
- execSync(
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(chalk15.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)"));
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
- execSync(`rm -f "${refScreenshot}" "${previewScreenshot}"`, { stdio: "pipe" });
5073
+ execSync2(`rm -f "${refScreenshot}" "${previewScreenshot}"`, { stdio: "pipe" });
3962
5074
  } catch {
3963
5075
  }
3964
5076
  console.log("");
3965
- console.log(chalk15.green.bold("\uBE44\uAD50 \uC644\uB8CC!"));
3966
- console.log(chalk15.dim(` \uCD9C\uB825: ${outputPath}`));
3967
- console.log(chalk15.dim(` \uD574\uC0C1\uB3C4: ${width}\xD7${height}`));
3968
- console.log(chalk15.dim(` \uB808\uD37C\uB7F0\uC2A4: ${options.ref}`));
3969
- console.log(chalk15.dim(` \uD504\uB9AC\uBDF0: ${options.preview}`));
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(chalk15.red(`\uBE44\uAD50 \uC2E4\uD328: ${error instanceof Error ? error.message : String(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
- execSync("npx playwright --version", { stdio: "pipe" });
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
- execSync(`which ${cmd}`, { stdio: "pipe" });
5098
+ execSync2(`which ${cmd}`, { stdio: "pipe" });
3987
5099
  return true;
3988
5100
  } catch {
3989
5101
  return false;
3990
5102
  }
3991
5103
  }
3992
- function captureScreenshot(url, outputPath, width, height) {
3993
- const result = spawnSync("npx", [
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 chalk16 from "chalk";
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(chalk16.green("\nDraft\uC640 Published \uC2A4\uD0A4\uB9C8\uAC00 \uB3D9\uC77C\uD569\uB2C8\uB2E4. \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C.\n"));
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(chalk16.bold("\nStaging vs Production \uC2A4\uD0A4\uB9C8 \uBE44\uAD50:\n"));
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(chalk16.bold.underline(" Pages"));
5257
+ console.log(chalk17.bold.underline(" Pages"));
4146
5258
  for (const page of result.pages.added) {
4147
- console.log(` ${chalk16.green("+")} ${page}`);
5259
+ console.log(` ${chalk17.green("+")} ${page}`);
4148
5260
  }
4149
5261
  for (const page of result.pages.removed) {
4150
- console.log(` ${chalk16.red("-")} ${page}`);
5262
+ console.log(` ${chalk17.red("-")} ${page}`);
4151
5263
  }
4152
5264
  for (const page of result.pages.modified) {
4153
- console.log(` ${chalk16.yellow("~")} ${page}`);
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(chalk16.bold.underline(" Blocks"));
5270
+ console.log(chalk17.bold.underline(" Blocks"));
4159
5271
  for (const block of result.blocks.added) {
4160
- console.log(` ${chalk16.green("+")} ${block}`);
5272
+ console.log(` ${chalk17.green("+")} ${block}`);
4161
5273
  }
4162
5274
  for (const block of result.blocks.removed) {
4163
- console.log(` ${chalk16.red("-")} ${block}`);
5275
+ console.log(` ${chalk17.red("-")} ${block}`);
4164
5276
  }
4165
5277
  for (const block of result.blocks.modified) {
4166
- console.log(` ${chalk16.yellow("~")} ${block}`);
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(chalk16.bold.underline(" Settings"));
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(` ${chalk16.green("+")} ${key}: ${chalk16.green(formatValue(draft))}`);
5286
+ console.log(` ${chalk17.green("+")} ${key}: ${chalk17.green(formatValue(draft))}`);
4175
5287
  } else if (draft === void 0) {
4176
- console.log(` ${chalk16.red("-")} ${key}: ${chalk16.red(formatValue(published))}`);
5288
+ console.log(` ${chalk17.red("-")} ${key}: ${chalk17.red(formatValue(published))}`);
4177
5289
  } else {
4178
- console.log(` ${chalk16.yellow("~")} ${key}:`);
4179
- console.log(` ${chalk16.red(`- ${formatValue(published)}`)}`);
4180
- console.log(` ${chalk16.green(`+ ${formatValue(draft)}`)}`);
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(chalk16.dim(` \uCD1D ${totalChanges}\uAC74\uC758 \uBCC0\uACBD \uC0AC\uD56D
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(chalk16.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4202
- console.error(chalk16.yellow("\uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC2E4\uD589\uD558\uC138\uC694:"));
4203
- console.error(chalk16.dim(" 1. npx @saeroon/cli init --from-site <siteId> \uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654"));
4204
- console.error(chalk16.dim(' 2. saeroon.config.json\uC5D0 "siteId" \uD544\uB4DC\uB97C \uC9C1\uC811 \uCD94\uAC00'));
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(chalk16.bold("\nStaging vs Production \uC2A4\uD0A4\uB9C8 \uBE44\uAD50\n"));
4213
- console.log(chalk16.dim(` \uC0AC\uC774\uD2B8 ID: ${siteId}`));
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(chalk16.dim(` Draft: ${draftResult.isDraft ? "Draft" : "Published"} (editVersion: ${draftResult.editVersion})`));
4227
- console.log(chalk16.dim(` Published: ${publishedResult.isDraft ? "Draft" : "Published"} (editVersion: ${publishedResult.editVersion})`));
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
- chalk16.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
5343
+ chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
4232
5344
  );
4233
5345
  } else {
4234
5346
  fetchSpinner.stop(
4235
- chalk16.red(
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 chalk17 from "chalk";
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(chalk17.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4269
- console.error(chalk17.yellow("\uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC2E4\uD589\uD558\uC138\uC694:"));
4270
- console.error(chalk17.dim(" 1. npx @saeroon/cli init --from-site <siteId> \uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654"));
4271
- console.error(chalk17.dim(' 2. saeroon.config.json\uC5D0 "siteId" \uD544\uB4DC\uB97C \uC9C1\uC811 \uCD94\uAC00'));
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(chalk17.yellow("\n\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uB294 \uC774\uBBF8 \uD15C\uD50C\uB9BF\uC774 \uB4F1\uB85D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4."));
4277
- console.error(chalk17.dim(` templateId: ${projectConfig.templateId}`));
4278
- console.error(chalk17.dim(" \uBA54\uD0C0\uB370\uC774\uD130 \uC218\uC815: saeroon template update"));
4279
- console.error(chalk17.dim(" \uBC84\uC804 \uB3D9\uAE30\uD654: saeroon template sync"));
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(chalk17.bold("\n\uD15C\uD50C\uB9BF \uB4F1\uB85D\n"));
4287
- console.log(chalk17.dim(` \uC18C\uC2A4 \uC0AC\uC774\uD2B8: ${projectConfig.siteId}`));
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(chalk17.red(" \uC774\uB984\uC740 \uD544\uC218\uC785\uB2C8\uB2E4."));
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(chalk17.dim(" \uCE74\uD14C\uACE0\uB9AC \uBAA9\uB85D:"));
5419
+ console.log(chalk18.dim(" \uCE74\uD14C\uACE0\uB9AC \uBAA9\uB85D:"));
4308
5420
  TEMPLATE_CATEGORIES2.forEach((cat, i) => {
4309
- console.log(chalk17.dim(` ${i + 1}. ${cat}`));
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(chalk17.red(" \uCE74\uD14C\uACE0\uB9AC\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694."));
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(chalk17.green("\uD15C\uD50C\uB9BF \uB4F1\uB85D \uC644\uB8CC!"));
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(chalk17.dim(" templateId\uAC00 saeroon.config.json\uC5D0 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
4360
- console.log(chalk17.dim(" \uBC84\uC804 \uB3D9\uAE30\uD654: saeroon template sync"));
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(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
5477
+ registerSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4366
5478
  } else {
4367
5479
  registerSpinner.stop(
4368
- chalk17.red(`\uB4F1\uB85D \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
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(chalk17.red("\ntemplateId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4378
- console.error(chalk17.yellow("\uBA3C\uC800 \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD558\uC138\uC694:"));
4379
- console.error(chalk17.dim(" saeroon template register"));
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(chalk17.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
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(chalk17.bold("\n\uD15C\uD50C\uB9BF \uBC84\uC804 \uB3D9\uAE30\uD654\n"));
4391
- console.log(chalk17.dim(` \uC18C\uC2A4 \uC0AC\uC774\uD2B8: ${projectConfig.siteId}`));
4392
- console.log(chalk17.dim(` \uD15C\uD50C\uB9BF ID: ${projectConfig.templateId}`));
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(chalk17.green(" \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C. \uC774\uBBF8 \uCD5C\uC2E0 \uC0C1\uD0DC\uC785\uB2C8\uB2E4.\n"));
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
- chalk17.yellow(" \uC774 \uBCC0\uACBD \uC0AC\uD56D\uC73C\uB85C \uC0C8 \uBC84\uC804\uC744 \uBC1C\uD589\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? (y/N): ")
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(chalk17.dim("\n \uB3D9\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
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(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
5534
+ diffSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4423
5535
  } else {
4424
5536
  diffSpinner.stop(
4425
- chalk17.red(`\uC2A4\uD0A4\uB9C8 \uBE44\uAD50 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
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(chalk17.green(`\uD15C\uD50C\uB9BF v${result.version} \uBC1C\uD589 \uC644\uB8CC!`));
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: ${chalk17.dim(result.updatedAt ?? "")}`);
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(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
5559
+ syncSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4448
5560
  } else {
4449
5561
  syncSpinner.stop(
4450
- chalk17.red(`\uB3D9\uAE30\uD654 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
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(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
5583
+ fetchSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4472
5584
  } else {
4473
5585
  fetchSpinner.stop(
4474
- chalk17.red(`\uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
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(chalk17.red("\ntemplateId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4484
- console.error(chalk17.yellow("\uBA3C\uC800 \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD558\uC138\uC694:"));
4485
- console.error(chalk17.dim(" saeroon template register"));
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(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
5636
+ currentSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4525
5637
  } else {
4526
- currentSpinner.stop(chalk17.red("\uC870\uD68C \uC2E4\uD328"));
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(chalk17.red(`
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(chalk17.bold("\n\uD604\uC7AC \uD15C\uD50C\uB9BF \uC815\uBCF4:"));
5649
+ console.log(chalk18.bold("\n\uD604\uC7AC \uD15C\uD50C\uB9BF \uC815\uBCF4:"));
4538
5650
  formatTemplateDetail(current);
4539
- console.log(chalk17.bold("\uC218\uC815\uD560 \uD56D\uBAA9\uC744 \uC785\uB825\uD558\uC138\uC694 (\uBE48 \uAC12 = \uBCC0\uACBD \uC5C6\uC74C):\n"));
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(chalk17.dim("\n \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C.\n"));
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(chalk17.green("\uD15C\uD50C\uB9BF \uC218\uC815 \uC644\uB8CC!"));
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(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
5692
+ updateSpinner.stop(chalk18.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4581
5693
  } else {
4582
5694
  updateSpinner.stop(
4583
- chalk17.red(`\uC218\uC815 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
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(chalk17.green(`+${diff.pages.added.length}`));
4597
- if (diff.pages.removed.length > 0) detail.push(chalk17.red(`-${diff.pages.removed.length}`));
4598
- if (diff.pages.modified.length > 0) detail.push(chalk17.yellow(`~${diff.pages.modified.length}`));
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(chalk17.green(`+${diff.blocks.added.length}`));
4604
- if (diff.blocks.removed.length > 0) detail.push(chalk17.red(`-${diff.blocks.removed.length}`));
4605
- if (diff.blocks.modified.length > 0) detail.push(chalk17.yellow(`~${diff.blocks.modified.length}`));
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 ${chalk17.yellow(`~${settingChanges}`)}`);
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 chalk18 from "chalk";
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(chalk18.green(`${result.total}\uAC1C\uC758 \uACF5\uAC1C \uD328\uD134\uC744 \uC870\uD68C\uD588\uC2B5\uB2C8\uB2E4.`));
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(chalk18.gray("\n \uACF5\uAC1C \uD328\uD134\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
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
- ` ${chalk18.gray("ID".padEnd(10))} ${"\uC774\uB984".padEnd(18)} ${chalk18.gray("\uCE74\uD14C\uACE0\uB9AC".padEnd(12))} ${chalk18.gray("\uC0AC\uC6A9")} ${chalk18.gray("\uC791\uC131\uC790")}`
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(chalk18.gray(" " + "-".repeat(70)));
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
- ` ${chalk18.cyan(shortId.padEnd(10))} ${p.name.padEnd(18)} ${chalk18.gray(p.category.padEnd(12))} ${chalk18.yellow(String(p.usageCount).padEnd(4))} ${chalk18.gray(p.authorName ?? "")}`
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(chalk18.gray(`
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
- chalk18.red(`\uACF5\uAC1C \uD328\uD134 \uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
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(chalk18.bold("\n V2 Patterns:\n"));
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(` ${chalk18.cyan(p.id.padEnd(24))} ${p.name.padEnd(12)} ${chalk18.gray(p.description)}`);
5791
+ console.log(` ${chalk19.cyan(p.id.padEnd(24))} ${p.name.padEnd(12)} ${chalk19.gray(p.description)}`);
4680
5792
  }
4681
- console.log(chalk18.bold("\n V1 Patterns (legacy):\n"));
5793
+ console.log(chalk19.bold("\n V1 Patterns (legacy):\n"));
4682
5794
  for (const p of V1_PATTERNS) {
4683
- console.log(` ${chalk18.gray(p.id.padEnd(24))} ${p.name.padEnd(12)} ${chalk18.gray(p.description)}`);
5795
+ console.log(` ${chalk19.gray(p.id.padEnd(24))} ${p.name.padEnd(12)} ${chalk19.gray(p.description)}`);
4684
5796
  }
4685
- console.log(chalk18.gray(`
5797
+ console.log(chalk19.gray(`
4686
5798
  \uCD1D ${V2_PATTERNS.length + V1_PATTERNS.length}\uAC1C \uB0B4\uC7A5 \uD328\uD134`));
4687
- console.log(chalk18.gray(` \uACF5\uAC1C \uD328\uD134 \uC870\uD68C: npx @saeroon/cli patterns --public
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(chalk18.red(`Pattern "${patternId}" not found.`));
4694
- console.log(chalk18.gray("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD328\uD134: " + [...V2_PATTERNS, ...V1_PATTERNS].map((p) => p.id).join(", ")));
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(chalk18.cyan(`
5809
+ console.log(chalk19.cyan(`
4698
5810
  Pattern: ${pattern.name} (${pattern.nameEn})`));
4699
- console.log(chalk18.gray(` Category: ${pattern.category}`));
4700
- console.log(chalk18.gray(` ${pattern.description}`));
4701
- console.log(chalk18.yellow("\n \uD328\uD134 \uC0BD\uC785\uC740 \uC5D0\uB514\uD130 \uB610\uB294 MCP\uC5D0\uC11C \uC2E4\uD589\uD558\uC138\uC694."));
4702
- console.log(chalk18.gray(" MCP: get_pattern \u2192 \uC2A4\uD0A4\uB9C8 JSON \uBC18\uD658\n"));
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(chalk18.red("--site-id \uC635\uC158\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."));
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(chalk18.green(`\uD328\uD134\uC774 \uC0AC\uC774\uD2B8 ${options.siteId}\uB85C Fork\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`));
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
- chalk18.red(`Fork \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
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("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", "compare-result.png").option("--width <px>", "\uBDF0\uD3EC\uD2B8 \uB108\uBE44", "1280").option("--height <px>", "\uBDF0\uD3EC\uD2B8 \uB192\uC774", "800").action(commandCompare);
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);