@bstage-sdk/core 0.8.0 → 0.8.2

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 (36) hide show
  1. package/dist/index.cjs +932 -0
  2. package/dist/index.d.cts +1153 -0
  3. package/dist/index.d.ts +1153 -0
  4. package/dist/index.js +876 -0
  5. package/docs/AI_TOOLKIT.md +99 -0
  6. package/docs/API_REFERENCE.md +470 -0
  7. package/docs/BUILD_SYSTEM.md +206 -0
  8. package/docs/DESIGN_TOKENS.md +109 -0
  9. package/docs/DEV_SERVER.md +397 -0
  10. package/docs/GETTING_STARTED.md +706 -0
  11. package/docs/I18N.md +235 -0
  12. package/docs/INIT.md +224 -0
  13. package/docs/LIQUID.md +122 -0
  14. package/docs/MIGRATION.md +308 -0
  15. package/docs/README.md +29 -0
  16. package/docs/SDK_ARCHITECTURE.md +255 -0
  17. package/docs/SLOT_CATALOG.md +162 -0
  18. package/docs/SLOT_CATALOG_V2.md +247 -0
  19. package/docs/SLOT_PROTOCOL.md +465 -0
  20. package/docs/SLOT_SYSTEM.md +316 -0
  21. package/docs/assets/slots/community-board.svg +69 -0
  22. package/docs/assets/slots/community-post-detail.svg +42 -0
  23. package/docs/assets/slots/contents-curation.svg +37 -0
  24. package/docs/assets/slots/contents-detail.svg +44 -0
  25. package/docs/assets/slots/contents-home.svg +92 -0
  26. package/docs/assets/slots/contents-section.svg +46 -0
  27. package/docs/assets/slots/lounge-community.svg +50 -0
  28. package/docs/assets/slots/lounge-contents.svg +43 -0
  29. package/docs/assets/slots/lounge-story.svg +44 -0
  30. package/docs/assets/slots/my-home.svg +44 -0
  31. package/docs/assets/slots/pop-home.svg +33 -0
  32. package/docs/assets/slots/product-detail.svg +29 -0
  33. package/docs/assets/slots/shop-home.svg +84 -0
  34. package/docs/assets/slots/story-feed-detail.svg +41 -0
  35. package/docs/assets/slots/story-home.svg +55 -0
  36. package/package.json +1 -1
package/dist/index.cjs ADDED
@@ -0,0 +1,932 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ADMIN_EVENT_CATALOG: () => ADMIN_EVENT_CATALOG,
24
+ BstageClient: () => BstageClient,
25
+ DEFAULT_LANGUAGE: () => DEFAULT_LANGUAGE,
26
+ HttpClient: () => HttpClient,
27
+ HttpError: () => HttpError,
28
+ HttpTimeoutError: () => HttpTimeoutError,
29
+ LANGUAGES: () => LANGUAGES,
30
+ PlatformBridge: () => PlatformBridge,
31
+ SLOT_CATALOG: () => SLOT_CATALOG,
32
+ SLOT_CATALOG_V2: () => SLOT_CATALOG_V2,
33
+ VERSION: () => VERSION,
34
+ __resetElementRegistry__: () => __resetElementRegistry__,
35
+ announceElementName: () => announceElementName,
36
+ configureBstageI18n: () => configureBstageI18n,
37
+ createWebComponent: () => createWebComponent,
38
+ dirNameToSlotId: () => dirNameToSlotId,
39
+ fetchTranslations: () => fetchTranslations,
40
+ getAdminEventsByDomain: () => getAdminEventsByDomain,
41
+ getSlotsByTarget: () => getSlotsByTarget,
42
+ getSlotsByTargetV2: () => getSlotsByTargetV2,
43
+ interpolateTranslation: () => interpolateTranslation,
44
+ isSlotIdV2: () => isSlotIdV2,
45
+ lookupElementName: () => lookupElementName,
46
+ normalizeLanguage: () => normalizeLanguage,
47
+ observeLocale: () => observeLocale,
48
+ parseRichText: () => parseRichText,
49
+ pickTranslation: () => pickTranslation,
50
+ readLocale: () => readLocale,
51
+ resolveTranslationChain: () => resolveTranslationChain,
52
+ slotIdToDirName: () => slotIdToDirName
53
+ });
54
+ module.exports = __toCommonJS(index_exports);
55
+
56
+ // src/http/HttpClient.ts
57
+ var HttpClient = class {
58
+ constructor(config) {
59
+ this.requestInterceptors = [];
60
+ this.responseInterceptors = [];
61
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
62
+ this.defaultHeaders = config.headers ?? {};
63
+ this.defaultTimeout = config.timeout ?? 3e4;
64
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
65
+ }
66
+ addRequestInterceptor(interceptor) {
67
+ this.requestInterceptors.push(interceptor);
68
+ }
69
+ addResponseInterceptor(interceptor) {
70
+ this.responseInterceptors.push(interceptor);
71
+ }
72
+ async request(config) {
73
+ let resolvedConfig = { ...config };
74
+ for (const interceptor of this.requestInterceptors) {
75
+ resolvedConfig = await interceptor(resolvedConfig);
76
+ }
77
+ const url = this.buildUrl(resolvedConfig.url, resolvedConfig.params);
78
+ const timeout = resolvedConfig.timeout ?? this.defaultTimeout;
79
+ const controller = new AbortController();
80
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
81
+ try {
82
+ const fetchResponse = await this.fetchFn(url, {
83
+ method: resolvedConfig.method,
84
+ headers: {
85
+ ...this.defaultHeaders,
86
+ ...resolvedConfig.headers
87
+ },
88
+ body: resolvedConfig.body != null ? JSON.stringify(resolvedConfig.body) : void 0,
89
+ signal: controller.signal,
90
+ credentials: "include"
91
+ });
92
+ const data = await this.parseResponse(fetchResponse);
93
+ let response = {
94
+ status: fetchResponse.status,
95
+ headers: fetchResponse.headers,
96
+ data
97
+ };
98
+ for (const interceptor of this.responseInterceptors) {
99
+ response = await interceptor(response);
100
+ }
101
+ if (!fetchResponse.ok) {
102
+ throw new HttpError(response);
103
+ }
104
+ return response;
105
+ } catch (error) {
106
+ if (error instanceof HttpError) throw error;
107
+ if (isAbortError(error)) {
108
+ throw new HttpTimeoutError(timeout, url);
109
+ }
110
+ throw error;
111
+ } finally {
112
+ clearTimeout(timeoutId);
113
+ }
114
+ }
115
+ async get(url, options) {
116
+ return this.request({ method: "GET", url, ...options });
117
+ }
118
+ async post(url, options) {
119
+ return this.request({ method: "POST", url, ...options });
120
+ }
121
+ async put(url, options) {
122
+ return this.request({ method: "PUT", url, ...options });
123
+ }
124
+ async patch(url, options) {
125
+ return this.request({ method: "PATCH", url, ...options });
126
+ }
127
+ async delete(url, options) {
128
+ return this.request({ method: "DELETE", url, ...options });
129
+ }
130
+ buildUrl(path, params) {
131
+ const full = path.startsWith("http://") || path.startsWith("https://") ? path : `${this.baseUrl}${path}`;
132
+ const url = new URL(full);
133
+ if (params) {
134
+ for (const [key, value] of Object.entries(params)) {
135
+ if (value === void 0) continue;
136
+ if (Array.isArray(value)) {
137
+ for (const item of value) {
138
+ url.searchParams.append(key, String(item));
139
+ }
140
+ } else {
141
+ url.searchParams.set(key, String(value));
142
+ }
143
+ }
144
+ }
145
+ return url.toString();
146
+ }
147
+ async parseResponse(response) {
148
+ const contentType = response.headers.get("content-type");
149
+ if (contentType?.includes("application/json")) {
150
+ return response.json();
151
+ }
152
+ return response.text();
153
+ }
154
+ };
155
+ function isAbortError(error) {
156
+ return typeof error === "object" && error !== null && error.name === "AbortError";
157
+ }
158
+ var HttpError = class extends Error {
159
+ constructor(response) {
160
+ super(`HTTP ${response.status}`);
161
+ this.name = "HttpError";
162
+ this.status = response.status;
163
+ this.headers = response.headers;
164
+ this.data = response.data;
165
+ }
166
+ };
167
+ var HttpTimeoutError = class extends Error {
168
+ constructor(timeout, url) {
169
+ super(`Request timed out after ${timeout}ms: ${url}`);
170
+ this.name = "HttpTimeoutError";
171
+ this.timeout = timeout;
172
+ this.url = url;
173
+ }
174
+ };
175
+
176
+ // src/version.ts
177
+ var VERSION = "0.8.2";
178
+
179
+ // src/client/BstageClient.ts
180
+ function resolveBaseUrl() {
181
+ if (typeof globalThis.location !== "undefined") {
182
+ return `${globalThis.location.origin}/gw`;
183
+ }
184
+ return "/gw";
185
+ }
186
+ function resolvePath(template, params) {
187
+ if (!params) return template;
188
+ return template.replace(/\{([^}]+)\}/g, (_, key) => {
189
+ const value = params[key];
190
+ if (value === void 0) return `{${key}}`;
191
+ return encodeURIComponent(String(value));
192
+ });
193
+ }
194
+ var resolveBstageFetch = (input, init) => {
195
+ const hook = globalThis.__bstage_fetch__;
196
+ return hook ? hook(input, init) : globalThis.fetch(input, init);
197
+ };
198
+ var APP_ID_PREFIX = "bsa_";
199
+ var APP_KEY_PREFIX = "bsp_";
200
+ function warnOnSwappedCredentials(appId, appKey) {
201
+ const appIdLooksLikeKey = typeof appId === "string" && appId.startsWith(APP_KEY_PREFIX);
202
+ const appKeyLooksLikeId = typeof appKey === "string" && appKey.startsWith(APP_ID_PREFIX);
203
+ if (!appIdLooksLikeKey && !appKeyLooksLikeId) return;
204
+ console.warn(
205
+ `[b.stage] appId\uC640 appKey\uAC00 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4.
206
+ appId\uB294 "${APP_ID_PREFIX}", appKey\uB294 "${APP_KEY_PREFIX}"\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.
207
+ \uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 \uAC12\uC744 \uB2E4\uC2DC \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
208
+ );
209
+ }
210
+ function resolveAppKey(config) {
211
+ return config.appKey || config.appSecret || void 0;
212
+ }
213
+ function warnOnMissingAppKey(appKey) {
214
+ if (appKey !== void 0) return;
215
+ console.warn(
216
+ `[b.stage] appKey\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 X-BSTAGE-APP-KEY \uD5E4\uB354 \uC5C6\uC774 \uD638\uCD9C\uB418\uC5B4 API\uAC00 401\uC744 \uB0C5\uB2C8\uB2E4.
217
+ .env\uC758 VITE_BSTAGE_APP_KEY(\uD30C\uD2B8\uB108 \uCF58\uC194\uC758 APP KEY)\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
218
+ );
219
+ }
220
+ var BstageClient = class {
221
+ constructor(config) {
222
+ const appKey = resolveAppKey(config);
223
+ warnOnMissingAppKey(appKey);
224
+ warnOnSwappedCredentials(config.appId, appKey);
225
+ const fetchFn = config.fetch ?? resolveBstageFetch;
226
+ this.http = new HttpClient({
227
+ baseUrl: config.baseUrl ?? resolveBaseUrl(),
228
+ timeout: config.timeout,
229
+ fetch: fetchFn,
230
+ headers: {
231
+ "Content-Type": "application/json",
232
+ "X-BSTAGE-APP-ID": config.appId,
233
+ // 값이 없으면(UI 전용 템플릿의 빈 .env) 헤더 자체를 빼서 "undefined" 문자열이 실리지 않게 한다.
234
+ ...appKey !== void 0 ? { "X-BSTAGE-APP-KEY": appKey } : {},
235
+ "X-BSTAGE-TENANT-ID": config.tenantId,
236
+ // 어느 SDK(core) 버전이 호출하는지 게이트웨이 로그에서 볼 수 있게. 값은 버전 문자열만(패키지 이름 없음).
237
+ "x-bmf-sdk-version": VERSION
238
+ }
239
+ });
240
+ }
241
+ // ── GET ──────────────────────────────────────────────────────────────
242
+ get(url, options) {
243
+ const { path, ...rest } = options ?? {};
244
+ return this.http.get(resolvePath(url, path), rest);
245
+ }
246
+ // ── POST ─────────────────────────────────────────────────────────────
247
+ post(url, options) {
248
+ const { path, ...rest } = options ?? {};
249
+ return this.http.post(resolvePath(url, path), rest);
250
+ }
251
+ // ── PUT ──────────────────────────────────────────────────────────────
252
+ put(url, options) {
253
+ const { path, ...rest } = options ?? {};
254
+ return this.http.put(resolvePath(url, path), rest);
255
+ }
256
+ // ── PATCH ────────────────────────────────────────────────────────────
257
+ patch(url, options) {
258
+ const { path, ...rest } = options ?? {};
259
+ return this.http.patch(resolvePath(url, path), rest);
260
+ }
261
+ // ── DELETE ───────────────────────────────────────────────────────────
262
+ delete(url, options) {
263
+ const { path, ...rest } = options ?? {};
264
+ return this.http.delete(resolvePath(url, path), rest);
265
+ }
266
+ };
267
+
268
+ // src/i18n.ts
269
+ var LANGUAGES = ["ko", "en", "ja", "zh-CN", "zh-TW", "es", "vi"];
270
+ var DEFAULT_LANGUAGE = "ko";
271
+ var LANGUAGE_COOKIE = {
272
+ user: "bmf_bstage_lang",
273
+ admin: "bmf_mybstage_locale"
274
+ };
275
+ var LANGUAGE_LOOKUP = new Map(
276
+ LANGUAGES.map((code) => [code.toLowerCase(), code])
277
+ );
278
+ function normalizeLanguage(value) {
279
+ if (!value) return void 0;
280
+ return LANGUAGE_LOOKUP.get(value.trim().toLowerCase());
281
+ }
282
+ function readLanguageCookie(target) {
283
+ if (typeof document === "undefined" || !document.cookie) return void 0;
284
+ const cookieName = LANGUAGE_COOKIE[target];
285
+ for (const part of document.cookie.split(";")) {
286
+ const [name, ...rest] = part.split("=");
287
+ if (name?.trim() === cookieName) {
288
+ return normalizeLanguage(decodeURIComponent(rest.join("=")));
289
+ }
290
+ }
291
+ return void 0;
292
+ }
293
+ function readLocale(target = "user") {
294
+ if (typeof document === "undefined") return DEFAULT_LANGUAGE;
295
+ const htmlLang = normalizeLanguage(document.documentElement.lang);
296
+ if (target === "admin") {
297
+ return readLanguageCookie("admin") ?? htmlLang ?? DEFAULT_LANGUAGE;
298
+ }
299
+ return htmlLang ?? readLanguageCookie("user") ?? DEFAULT_LANGUAGE;
300
+ }
301
+ function observeLocale(onChange, target = "user") {
302
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
303
+ return () => {
304
+ };
305
+ }
306
+ let current = readLocale(target);
307
+ const observer = new MutationObserver(() => {
308
+ const next = readLocale(target);
309
+ if (next !== current) {
310
+ current = next;
311
+ onChange(next);
312
+ }
313
+ });
314
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
315
+ return () => observer.disconnect();
316
+ }
317
+ var TRANSLATION_ORIGIN = "https://cdn.static.bstage.in";
318
+ var TRANSLATION_BASE_KEY = "static/i18n";
319
+ var TRANSLATION_BUCKET_PREFIX = "cdn.bstage.in/";
320
+ var DEFAULT_TIER = "inhouse";
321
+ var LATEST_TTL_MS = 5 * 60 * 1e3;
322
+ var i18nConfig = {};
323
+ function configureBstageI18n(config) {
324
+ i18nConfig = { ...i18nConfig, ...config };
325
+ }
326
+ var PHASES = ["dev", "qa", "real", "real-canary", "sandbox"];
327
+ function normalizePhase(value) {
328
+ return PHASES.find((p) => p === value);
329
+ }
330
+ function resolvePhase() {
331
+ const fromConfig = normalizePhase(i18nConfig.phase);
332
+ if (fromConfig) return fromConfig;
333
+ const injected = normalizePhase(
334
+ globalThis.__bstage_i18n_phase__
335
+ );
336
+ if (injected) return injected;
337
+ const host = typeof location === "undefined" ? "" : location.hostname;
338
+ if (host.includes(".dev.")) return "dev";
339
+ if (host.includes(".qa.")) return "qa";
340
+ if (host.includes("sandstage.in")) return "sandbox";
341
+ return "real";
342
+ }
343
+ function resolveSource(system) {
344
+ return {
345
+ origin: i18nConfig.origin ?? TRANSLATION_ORIGIN,
346
+ phase: resolvePhase(),
347
+ system: system ?? i18nConfig.system ?? "user",
348
+ tier: i18nConfig.tier ?? DEFAULT_TIER
349
+ };
350
+ }
351
+ function sourceKey(source) {
352
+ return `${source.origin}|${source.phase}|${source.system}|${source.tier}`;
353
+ }
354
+ function latestUrl(source) {
355
+ const segments = [TRANSLATION_BASE_KEY, source.phase, source.system];
356
+ if (source.tier !== DEFAULT_TIER) segments.push(source.tier);
357
+ return `${source.origin}/${segments.join("/")}/latest.json?date=${cacheStamp(/* @__PURE__ */ new Date())}`;
358
+ }
359
+ function cacheStamp(now) {
360
+ const pad = (n) => String(n).padStart(2, "0");
361
+ const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}`;
362
+ const min = now.getMinutes();
363
+ return `${date}-${pad(min - min % 5)}`;
364
+ }
365
+ var latestCache = /* @__PURE__ */ new Map();
366
+ var translationCache = /* @__PURE__ */ new Map();
367
+ function resolveFetch() {
368
+ const fn = i18nConfig.fetch ?? globalThis.fetch;
369
+ return typeof fn === "function" ? fn : void 0;
370
+ }
371
+ var TRANSLATION_CACHE_NAME = "bstage-i18n-v1";
372
+ async function openTranslationCache() {
373
+ try {
374
+ if (typeof caches === "undefined") return void 0;
375
+ return await caches.open(TRANSLATION_CACHE_NAME);
376
+ } catch {
377
+ return void 0;
378
+ }
379
+ }
380
+ async function fetchJsonWithCache(url, cacheKey, fetchFn, label) {
381
+ const cache = await openTranslationCache();
382
+ try {
383
+ const res = await fetchFn(url);
384
+ if (!res.ok) throw new Error(`${label}: HTTP ${res.status}`);
385
+ if (cache) {
386
+ await cache.put(cacheKey, res.clone()).catch(() => {
387
+ });
388
+ }
389
+ return await res.json();
390
+ } catch (err) {
391
+ const hit = await cache?.match(cacheKey).catch(() => void 0);
392
+ if (hit) return await hit.json();
393
+ throw err;
394
+ }
395
+ }
396
+ async function pruneOldVersions(source, keepBase) {
397
+ const cache = await openTranslationCache();
398
+ if (!cache) return;
399
+ const prefix = `${source.origin}/${TRANSLATION_BASE_KEY}/${source.phase}/${source.system}/`;
400
+ try {
401
+ const keys = await cache.keys();
402
+ await Promise.all(
403
+ keys.map(async (req) => {
404
+ if (!req.url.startsWith(prefix)) return;
405
+ if (req.url.endsWith("latest.json")) return;
406
+ if (req.url.startsWith(keepBase)) return;
407
+ await cache.delete(req).catch(() => {
408
+ });
409
+ })
410
+ );
411
+ } catch {
412
+ }
413
+ }
414
+ function fetchVersionBase(source, fetchFn) {
415
+ const key = sourceKey(source);
416
+ const cached = latestCache.get(key);
417
+ if (cached && Date.now() - cached.at < LATEST_TTL_MS) return cached.promise;
418
+ const url = latestUrl(source);
419
+ const promise = fetchJsonWithCache(
420
+ url,
421
+ url.split("?")[0],
422
+ fetchFn,
423
+ `i18n latest ${source.phase}/${source.system}`
424
+ ).then((latest) => {
425
+ if (!latest?.cdnPath) throw new Error("i18n latest: cdnPath \uC5C6\uC74C");
426
+ const base = `${source.origin}/${latest.cdnPath.replace(TRANSLATION_BUCKET_PREFIX, "")}`;
427
+ void pruneOldVersions(source, base);
428
+ return base;
429
+ }).catch((err) => {
430
+ latestCache.delete(key);
431
+ throw err;
432
+ });
433
+ latestCache.set(key, { at: Date.now(), promise });
434
+ return promise;
435
+ }
436
+ function fetchTranslations(locale, system) {
437
+ const source = resolveSource(system);
438
+ const key = `${sourceKey(source)}|${locale}`;
439
+ const cached = translationCache.get(key);
440
+ if (cached) return cached;
441
+ const fetchFn = resolveFetch();
442
+ if (!fetchFn) return Promise.reject(new Error("fetch unavailable"));
443
+ const promise = fetchVersionBase(source, fetchFn).then((base) => {
444
+ const url = `${base}/${locale}/translation.json`;
445
+ return fetchJsonWithCache(url, url, fetchFn, `translations ${locale}`);
446
+ }).catch((err) => {
447
+ translationCache.delete(key);
448
+ throw err;
449
+ });
450
+ translationCache.set(key, promise);
451
+ return promise;
452
+ }
453
+ var DEFAULT_FALLBACK_CHAIN = ["en", "ko"];
454
+ function resolveTranslationChain(current) {
455
+ const tail = i18nConfig.fallbackChain ?? DEFAULT_FALLBACK_CHAIN;
456
+ return [current, ...tail].filter((locale, i, all) => all.indexOf(locale) === i);
457
+ }
458
+ function pickTranslation(dicts, key) {
459
+ for (const dict of dicts) {
460
+ const value = dict[key];
461
+ if (typeof value === "string" && value.trim() !== "") return value;
462
+ }
463
+ return void 0;
464
+ }
465
+ var RICH_TAG_RE = /<(\d+)>([\s\S]*?)<\/\1>/g;
466
+ function parseRichText(template, params) {
467
+ const tokens = [];
468
+ let cursor = 0;
469
+ for (const match of template.matchAll(RICH_TAG_RE)) {
470
+ const at = match.index ?? 0;
471
+ if (at > cursor) {
472
+ tokens.push({
473
+ type: "text",
474
+ value: interpolateTranslation(template.slice(cursor, at), params)
475
+ });
476
+ }
477
+ tokens.push({
478
+ type: "tag",
479
+ index: Number(match[1]),
480
+ value: interpolateTranslation(match[2], params)
481
+ });
482
+ cursor = at + match[0].length;
483
+ }
484
+ if (cursor < template.length) {
485
+ tokens.push({ type: "text", value: interpolateTranslation(template.slice(cursor), params) });
486
+ }
487
+ return tokens;
488
+ }
489
+ function interpolateTranslation(template, params) {
490
+ if (!params) return template;
491
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
492
+ const value = params[key];
493
+ return value === void 0 ? `{{${key}}}` : String(value);
494
+ });
495
+ }
496
+
497
+ // src/core/adminEvents.ts
498
+ var ADMIN_EVENT_CATALOG = [
499
+ {
500
+ type: "ticket.create.after",
501
+ domain: "ticket",
502
+ description: "\uD2F0\uCF13 \uC0DD\uC131 \uC644\uB8CC \uD6C4 \uBC1C\uD589. \uCEE4\uC2A4\uD140 UI\uAC00 \uD6C4\uC18D \uCC98\uB9AC(QR \uC800\uC7A5 \uB4F1)\uB97C \uC218\uD589\uD560 \uC218 \uC788\uB2E4."
503
+ }
504
+ ];
505
+ function getAdminEventsByDomain(domain) {
506
+ return ADMIN_EVENT_CATALOG.filter((e) => e.domain === domain);
507
+ }
508
+
509
+ // src/core/slotCatalog.ts
510
+ var SLOT_CATALOG = [
511
+ {
512
+ key: "contents.home.contents.before",
513
+ target: "user",
514
+ page: "contents.home",
515
+ section: "contents",
516
+ position: "before",
517
+ description: "\uCF58\uD150\uCE20 \uD648 \u2014 \uCF58\uD150\uCE20 \uC139\uC158 \uC704 (Curation/Tags/Show \uC544\uB798)"
518
+ },
519
+ {
520
+ key: "contents.home.contents.after",
521
+ target: "user",
522
+ page: "contents.home",
523
+ section: "contents",
524
+ position: "after",
525
+ description: "\uCF58\uD150\uCE20 \uD648 \u2014 \uCF58\uD150\uCE20 \uC139\uC158 \uC544\uB798 (Footer \uC704)"
526
+ },
527
+ {
528
+ key: "products.digital-ticket.form-additional.after",
529
+ target: "admin",
530
+ page: "products.digital-ticket",
531
+ section: "form-additional",
532
+ position: "after",
533
+ description: "\uB514\uC9C0\uD138 \uD2F0\uCF13 \uC0DD\uC131/\uC218\uC815 \u2014 \uCD94\uAC00 \uC124\uC815 \uC2A4\uD15D \uC544\uB798"
534
+ },
535
+ {
536
+ key: "shop.order-item.ticket.after",
537
+ target: "user",
538
+ page: "shop.order-item",
539
+ section: "ticket",
540
+ position: "after",
541
+ description: "\uC8FC\uBB38 \uC544\uC774\uD15C \u2014 \uB514\uC9C0\uD138 \uD2F0\uCF13 \uC815\uBCF4 \uC544\uB798"
542
+ },
543
+ // 어드민 메뉴 슬롯 — 서브메뉴
544
+ {
545
+ key: "admin.nav.my-stage.menu.after",
546
+ target: "admin",
547
+ page: "admin.nav",
548
+ section: "my-stage",
549
+ position: "after",
550
+ description: "\uB0B4 \uD648\uD398\uC774\uC9C0 \uC11C\uBE0C\uBA54\uB274 \uD558\uB2E8"
551
+ },
552
+ {
553
+ key: "admin.nav.contents.menu.after",
554
+ target: "admin",
555
+ page: "admin.nav",
556
+ section: "contents",
557
+ position: "after",
558
+ description: "\uCF58\uD150\uCE20 \uC11C\uBE0C\uBA54\uB274 \uD558\uB2E8"
559
+ },
560
+ {
561
+ key: "admin.nav.products.menu.after",
562
+ target: "admin",
563
+ page: "admin.nav",
564
+ section: "products",
565
+ position: "after",
566
+ description: "\uC0C1\uD488 \uC11C\uBE0C\uBA54\uB274 \uD558\uB2E8"
567
+ },
568
+ {
569
+ key: "admin.nav.orders.menu.after",
570
+ target: "admin",
571
+ page: "admin.nav",
572
+ section: "orders",
573
+ position: "after",
574
+ description: "\uC8FC\uBB38 \uC11C\uBE0C\uBA54\uB274 \uD558\uB2E8"
575
+ },
576
+ {
577
+ key: "admin.nav.settlement.menu.after",
578
+ target: "admin",
579
+ page: "admin.nav",
580
+ section: "settlement",
581
+ position: "after",
582
+ description: "\uC815\uC0B0 \uC11C\uBE0C\uBA54\uB274 \uD558\uB2E8"
583
+ },
584
+ {
585
+ key: "admin.nav.operation.menu.after",
586
+ target: "admin",
587
+ page: "admin.nav",
588
+ section: "operation",
589
+ position: "after",
590
+ description: "\uD68C\uC6D0 \uC11C\uBE0C\uBA54\uB274 \uD558\uB2E8"
591
+ },
592
+ {
593
+ key: "admin.nav.settings.menu.after",
594
+ target: "admin",
595
+ page: "admin.nav",
596
+ section: "settings",
597
+ position: "after",
598
+ description: "\uC124\uC815 \uC11C\uBE0C\uBA54\uB274 \uD558\uB2E8"
599
+ },
600
+ // 어드민 메뉴 슬롯 — 대메뉴
601
+ {
602
+ key: "admin.nav.menu.after",
603
+ target: "admin",
604
+ page: "admin.nav",
605
+ section: "menu",
606
+ position: "after",
607
+ description: "\uB300\uBA54\uB274 \uBAA9\uB85D \uD558\uB2E8 (\uC0C8 \uBA54\uB274 \uADF8\uB8F9)"
608
+ }
609
+ ];
610
+ function getSlotsByTarget(target) {
611
+ return SLOT_CATALOG.filter((s) => s.target === target);
612
+ }
613
+
614
+ // src/core/slotCatalogV2.ts
615
+ var SLOT_CATALOG_V2 = {
616
+ // 어드민 — 디지털 티켓 상세 (PoC 한계가 알려진 영역. 결정-016)
617
+ "admin.digital-ticket-detail.form:after": {
618
+ description: "\uB514\uC9C0\uD138 \uD2F0\uCF13 \uC0C1\uC138 \uBCF8\uBB38 \uC544\uB798",
619
+ context: {
620
+ ticket: { type: "DigitalTicket", required: true }
621
+ }
622
+ },
623
+ // 어드민 — 콘텐츠 목록 PageTitleContainer 아래
624
+ "admin.contents-list.list:before": {
625
+ description: "\uCF58\uD150\uCE20 \uBAA9\uB85D \u2014 \uBAA9\uB85D \uC704",
626
+ context: {}
627
+ },
628
+ // 어드민 — 커뮤니티 보드 목록 PageTitleContainer 아래
629
+ "admin.community-board-list.list:before": {
630
+ description: "\uCEE4\uBBA4\uB2C8\uD2F0 \uBCF4\uB4DC \uBAA9\uB85D \u2014 \uBAA9\uB85D \uC704",
631
+ context: {}
632
+ },
633
+ // 유저 — 콘텐츠 홈 (결정-018에 1차 명시. curation은 신설, contents-section은 옛 contents.home.contents.before/after 이전)
634
+ "user.contents-home.curation:after": {
635
+ description: "\uCF58\uD150\uCE20 \uD648 \u2014 \uD050\uB808\uC774\uC158 \uC139\uC158 \uC544\uB798",
636
+ context: {}
637
+ },
638
+ "user.contents-home.contents-section:before": {
639
+ description: "\uCF58\uD150\uCE20 \uD648 \u2014 \uCF58\uD150\uCE20 \uC139\uC158 \uBB36\uC74C \uC704",
640
+ context: {}
641
+ },
642
+ "user.contents-home.contents-section:after": {
643
+ description: "\uCF58\uD150\uCE20 \uD648 \u2014 \uCF58\uD150\uCE20 \uC139\uC158 \uBB36\uC74C \uC544\uB798",
644
+ context: {}
645
+ },
646
+ // 유저 — 콘텐츠 상세 (포스트·미디어 두 렌더러가 같은 자리 키를 공유)
647
+ "user.contents-detail.body:before": {
648
+ description: "\uCF58\uD150\uCE20 \uC0C1\uC138 \u2014 \uBCF8\uBB38 \uC704",
649
+ context: {
650
+ content: { type: "Content", required: true }
651
+ }
652
+ },
653
+ "user.contents-detail.body:after": {
654
+ description: "\uCF58\uD150\uCE20 \uC0C1\uC138 \u2014 \uBCF8\uBB38 \uC544\uB798",
655
+ context: {
656
+ content: { type: "Content", required: true }
657
+ }
658
+ },
659
+ "user.contents-detail.comments:before": {
660
+ description: "\uCF58\uD150\uCE20 \uC0C1\uC138 \u2014 \uB313\uAE00 \uC139\uC158 \uC704",
661
+ context: {
662
+ content: { type: "Content", required: true }
663
+ }
664
+ },
665
+ // 유저 — 콘텐츠 큐레이션 목록
666
+ // list:after는 미도입 — 페이지 끝 자리가 전역 footer 영역(user.app-footer.*)과 겹침 (결정-021과 동일 사유)
667
+ "user.contents-curation.list:before": {
668
+ description: "\uCF58\uD150\uCE20 \uD050\uB808\uC774\uC158 \uBAA9\uB85D \u2014 \uBAA9\uB85D \uC704",
669
+ context: {}
670
+ },
671
+ // 유저 — 콘텐츠 섹션 목록 (list:after 미도입 — 큐레이션과 동일 사유)
672
+ "user.contents-section.list:before": {
673
+ description: "\uCF58\uD150\uCE20 \uC139\uC158 \uBAA9\uB85D \u2014 \uBAA9\uB85D \uC704",
674
+ context: {
675
+ section: { type: "ContentsSectionInfo", required: true }
676
+ }
677
+ },
678
+ // 유저 — 스토리 홈 (feed:after 미도입 — 무한스크롤 끝 + footer 인접)
679
+ "user.story-home.feed:before": {
680
+ description: "\uC2A4\uD1A0\uB9AC \uD648 \u2014 \uD53C\uB4DC \uC601\uC5ED \uC704",
681
+ context: {}
682
+ },
683
+ // 유저 — 스토리 피드 상세
684
+ "user.story-feed-detail.post:before": {
685
+ description: "\uC2A4\uD1A0\uB9AC \uD53C\uB4DC \uC0C1\uC138 \u2014 \uD3EC\uC2A4\uD2B8 \uC601\uC5ED \uC704",
686
+ context: {
687
+ content: { type: "Content", required: true }
688
+ }
689
+ },
690
+ "user.story-feed-detail.comments:before": {
691
+ description: "\uC2A4\uD1A0\uB9AC \uD53C\uB4DC \uC0C1\uC138 \u2014 \uB313\uAE00 \uC139\uC158 \uC704 (\uBCF8\uBB38 \uC544\uB798)",
692
+ context: {
693
+ content: { type: "Content", required: true }
694
+ }
695
+ },
696
+ // 유저 — POP 홈 (단일 페이지·단일 자리 — 조건부 섹션이 많아 항상 렌더되는 구독 목록 기준)
697
+ "user.pop-home.list:before": {
698
+ description: "POP \uD648 \u2014 \uAD6C\uB3C5 \uBAA9\uB85D \uC704",
699
+ context: {}
700
+ },
701
+ // 유저 — 커뮤니티 보드 (커뮤니티 홈(/community)과 보드 상세가 같은 화면이라 한 키 공유)
702
+ "user.community-board.feed:before": {
703
+ description: "\uCEE4\uBBA4\uB2C8\uD2F0 \uBCF4\uB4DC \u2014 \uD53C\uB4DC \uBAA9\uB85D \uC704",
704
+ context: {
705
+ board: { type: "BoardInfo", required: true }
706
+ }
707
+ },
708
+ // 유저 — 커뮤니티 포스트 상세 (보드 포스트는 콘텐츠 엔티티가 아니라 Content 대신 전용 약속 사용)
709
+ "user.community-post-detail.post:before": {
710
+ description: "\uCEE4\uBBA4\uB2C8\uD2F0 \uD3EC\uC2A4\uD2B8 \uC0C1\uC138 \u2014 \uD3EC\uC2A4\uD2B8 \uC601\uC5ED \uC704",
711
+ context: {
712
+ post: { type: "BoardPostInfo", required: true }
713
+ }
714
+ },
715
+ "user.community-post-detail.comments:before": {
716
+ description: "\uCEE4\uBBA4\uB2C8\uD2F0 \uD3EC\uC2A4\uD2B8 \uC0C1\uC138 \u2014 \uB313\uAE00 \uC139\uC158 \uC704 (\uBCF8\uBB38 \uC544\uB798)",
717
+ context: {
718
+ post: { type: "BoardPostInfo", required: true }
719
+ }
720
+ },
721
+ // 유저 — 라운지 (메인과 다른 전용 컴포넌트라 라운지 전용 페이지 키. 멀티 인스턴스 — 매핑은 모든 라운지 공통)
722
+ // 라운지 커뮤니티: 홈(/lounge/[h]/community)·보드 상세가 같은 컴포넌트라 한 키 공유 (메인 community 패턴)
723
+ "user.lounge-community.feed:before": {
724
+ description: "\uB77C\uC6B4\uC9C0 \uCEE4\uBBA4\uB2C8\uD2F0 \u2014 \uD53C\uB4DC \uBAA9\uB85D \uC704",
725
+ context: {
726
+ board: { type: "BoardInfo", required: true }
727
+ }
728
+ },
729
+ // 라운지 콘텐츠
730
+ "user.lounge-contents.section:before": {
731
+ description: "\uB77C\uC6B4\uC9C0 \uCF58\uD150\uCE20 \u2014 \uC139\uC158 \uBB36\uC74C \uC704",
732
+ context: {}
733
+ },
734
+ // 라운지 스토리 (홈·/story/feed 같은 컴포넌트)
735
+ "user.lounge-story.feed:before": {
736
+ description: "\uB77C\uC6B4\uC9C0 \uC2A4\uD1A0\uB9AC \u2014 \uD53C\uB4DC \uC601\uC5ED \uC704",
737
+ context: {}
738
+ },
739
+ // 유저 — 샵 홈 (SectionPage 공유 → 호스트가 주입 지점으로 격리. 콘텐츠 홈 성격이라 context 없음)
740
+ // 고정 섹션 묶음 + 실제 footer라 콘텐츠 홈처럼 before/after 둘 다 둠 (무한스크롤 아님 — list/feed의 :after 미도입 사유 해당 없음)
741
+ "user.shop-home.section:before": {
742
+ description: "\uC0F5 \uD648 \u2014 \uC139\uC158 \uBB36\uC74C \uC704",
743
+ context: {}
744
+ },
745
+ "user.shop-home.section:after": {
746
+ description: "\uC0F5 \uD648 \u2014 \uC139\uC158 \uBB36\uC74C \uC544\uB798 (footer \uC704)",
747
+ context: {}
748
+ },
749
+ // 유저 — 상품 상세 (Physical·Digital·PopPass·Ticket 4개 렌더러 공통 — 컨테이너 레벨 자리)
750
+ "user.product-detail.detail:after": {
751
+ description: "\uC0C1\uD488 \uC0C1\uC138 \u2014 \uBCF8\uBB38 \uC544\uB798",
752
+ context: {
753
+ product: { type: "Product", required: true }
754
+ }
755
+ },
756
+ // 유저 — 마이홈 (프로필 카드와 메뉴 목록 사이. 설정성 하위 페이지는 제외)
757
+ "user.my-home.menu:before": {
758
+ description: "\uB9C8\uC774\uD648 \u2014 \uBA54\uB274 \uBAA9\uB85D \uC704 (\uD504\uB85C\uD544 \uC544\uB798)",
759
+ context: {}
760
+ }
761
+ };
762
+ function getSlotsByTargetV2(target) {
763
+ const entries = Object.entries(SLOT_CATALOG_V2);
764
+ return entries.filter(([id]) => id.startsWith(`${target}.`)).map(([id, entry]) => ({ id, ...entry }));
765
+ }
766
+ var SLOT_DIR_SEPARATOR = "--";
767
+ function slotIdToDirName(id) {
768
+ return id.replace(":", SLOT_DIR_SEPARATOR);
769
+ }
770
+ function dirNameToSlotId(dirName) {
771
+ return dirName.replace(SLOT_DIR_SEPARATOR, ":");
772
+ }
773
+ function isSlotIdV2(value) {
774
+ return Object.prototype.hasOwnProperty.call(SLOT_CATALOG_V2, value);
775
+ }
776
+
777
+ // src/core/PlatformBridge.ts
778
+ var PlatformBridge = class {
779
+ constructor(host) {
780
+ this.listeners = [];
781
+ this.host = host;
782
+ }
783
+ /** @internal 프레임워크 어댑터가 host 엘리먼트에 접근하기 위한 내부 메서드. */
784
+ getHost() {
785
+ return this.host;
786
+ }
787
+ /**
788
+ * 플랫폼으로 템플릿 이벤트를 디스패치합니다.
789
+ *
790
+ * @example
791
+ * bridge.emit('navigate', { path: '/lounge/123' })
792
+ * bridge.emit('toast', { message: 'Saved!', variant: 'success' })
793
+ */
794
+ emit(type, payload) {
795
+ this.host.dispatchEvent(
796
+ new CustomEvent(`bstage:${type}`, {
797
+ bubbles: true,
798
+ composed: true,
799
+ detail: payload
800
+ })
801
+ );
802
+ }
803
+ /**
804
+ * 호스트 엘리먼트에서 디스패치된 플랫폼 이벤트를 수신합니다.
805
+ * 구독 해제 함수를 반환합니다.
806
+ *
807
+ * @example
808
+ * const off = bridge.on('some-event', (payload) => { ... })
809
+ * off() // 구독 해제
810
+ */
811
+ on(type, handler) {
812
+ const eventType = `bstage:${type}`;
813
+ const listener = (e) => {
814
+ handler(e.detail);
815
+ };
816
+ this.host.addEventListener(eventType, listener);
817
+ this.listeners.push({ type: eventType, handler: listener });
818
+ return () => {
819
+ this.host.removeEventListener(eventType, listener);
820
+ this.listeners = this.listeners.filter((l) => l.handler !== listener);
821
+ };
822
+ }
823
+ /**
824
+ * 등록된 모든 리스너를 제거합니다. disconnectedCallback에서 호출됩니다.
825
+ */
826
+ destroy() {
827
+ for (const { type, handler } of this.listeners) {
828
+ this.host.removeEventListener(type, handler);
829
+ }
830
+ this.listeners = [];
831
+ }
832
+ };
833
+
834
+ // src/core/createWebComponent.ts
835
+ function createWebComponent(createAdapter, component, options) {
836
+ const { shadow = true } = options;
837
+ class BstageElement extends HTMLElement {
838
+ constructor() {
839
+ super();
840
+ this._adapter = null;
841
+ this._bridge = null;
842
+ this._shadowRoot = null;
843
+ this._mounted = false;
844
+ if (shadow) {
845
+ this._shadowRoot = this.attachShadow({ mode: "open" });
846
+ }
847
+ }
848
+ connectedCallback() {
849
+ if (!this._mounted) {
850
+ this._mount();
851
+ }
852
+ }
853
+ disconnectedCallback() {
854
+ if (this._mounted) {
855
+ this._adapter.unmount();
856
+ this._bridge.destroy();
857
+ this._adapter = null;
858
+ this._bridge = null;
859
+ this._mounted = false;
860
+ }
861
+ }
862
+ _mount() {
863
+ const container = this._shadowRoot ?? this;
864
+ this._adapter = createAdapter();
865
+ this._bridge = new PlatformBridge(this);
866
+ this._adapter.mount(component, container, {}, this._bridge);
867
+ this._mounted = true;
868
+ }
869
+ }
870
+ return BstageElement;
871
+ }
872
+
873
+ // src/core/elementRegistry.ts
874
+ var GLOBAL_KEY = "__bstage_elements__";
875
+ function registry() {
876
+ const holder = globalThis;
877
+ return holder[GLOBAL_KEY] ?? (holder[GLOBAL_KEY] = {});
878
+ }
879
+ function announceElementName(tagName) {
880
+ if (typeof document === "undefined") return;
881
+ const src = document.currentScript?.src;
882
+ if (!src) return;
883
+ const map = registry();
884
+ if (!(src in map)) map[src] = tagName;
885
+ }
886
+ function lookupElementName(url) {
887
+ const map = registry();
888
+ if (url in map) return map[url];
889
+ try {
890
+ const absolute = new URL(url, typeof document !== "undefined" ? document.baseURI : void 0).href;
891
+ return map[absolute];
892
+ } catch {
893
+ return void 0;
894
+ }
895
+ }
896
+ function __resetElementRegistry__() {
897
+ ;
898
+ globalThis[GLOBAL_KEY] = {};
899
+ }
900
+ // Annotate the CommonJS export names for ESM import in node:
901
+ 0 && (module.exports = {
902
+ ADMIN_EVENT_CATALOG,
903
+ BstageClient,
904
+ DEFAULT_LANGUAGE,
905
+ HttpClient,
906
+ HttpError,
907
+ HttpTimeoutError,
908
+ LANGUAGES,
909
+ PlatformBridge,
910
+ SLOT_CATALOG,
911
+ SLOT_CATALOG_V2,
912
+ VERSION,
913
+ __resetElementRegistry__,
914
+ announceElementName,
915
+ configureBstageI18n,
916
+ createWebComponent,
917
+ dirNameToSlotId,
918
+ fetchTranslations,
919
+ getAdminEventsByDomain,
920
+ getSlotsByTarget,
921
+ getSlotsByTargetV2,
922
+ interpolateTranslation,
923
+ isSlotIdV2,
924
+ lookupElementName,
925
+ normalizeLanguage,
926
+ observeLocale,
927
+ parseRichText,
928
+ pickTranslation,
929
+ readLocale,
930
+ resolveTranslationChain,
931
+ slotIdToDirName
932
+ });