@bstage-sdk/core 0.8.7 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +61 -1
- package/dist/index.d.cts +50 -3
- package/dist/index.d.ts +50 -3
- package/dist/index.js +59 -1
- package/docs/BUILD_SYSTEM.md +45 -1
- package/docs/DEV_SERVER.md +1 -1
- package/docs/GETTING_STARTED.md +18 -18
- package/docs/I18N.md +1 -1
- package/docs/LIQUID.md +26 -26
- package/docs/SDK_ARCHITECTURE.md +20 -11
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
dirNameToSlotId: () => dirNameToSlotId,
|
|
39
39
|
fetchTranslations: () => fetchTranslations,
|
|
40
40
|
getAdminEventsByDomain: () => getAdminEventsByDomain,
|
|
41
|
+
getFrameBridge: () => getFrameBridge,
|
|
41
42
|
getSlotsByTarget: () => getSlotsByTarget,
|
|
42
43
|
getSlotsByTargetV2: () => getSlotsByTargetV2,
|
|
43
44
|
interpolateTranslation: () => interpolateTranslation,
|
|
@@ -45,6 +46,7 @@ __export(index_exports, {
|
|
|
45
46
|
lookupElementName: () => lookupElementName,
|
|
46
47
|
normalizeLanguage: () => normalizeLanguage,
|
|
47
48
|
observeLocale: () => observeLocale,
|
|
49
|
+
observeTranslations: () => observeTranslations,
|
|
48
50
|
parseRichText: () => parseRichText,
|
|
49
51
|
pickTranslation: () => pickTranslation,
|
|
50
52
|
readLocale: () => readLocale,
|
|
@@ -174,10 +176,26 @@ var HttpTimeoutError = class extends Error {
|
|
|
174
176
|
};
|
|
175
177
|
|
|
176
178
|
// src/version.ts
|
|
177
|
-
var VERSION = "0.
|
|
179
|
+
var VERSION = "0.9.1";
|
|
180
|
+
|
|
181
|
+
// src/frameBridge.ts
|
|
182
|
+
function getFrameBridge() {
|
|
183
|
+
const candidate = globalThis.__bstage_bridge__;
|
|
184
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
185
|
+
const b = candidate;
|
|
186
|
+
if (typeof b.hostOrigin !== "string" || typeof b.getInit !== "function" || typeof b.request !== "function" || typeof b.on !== "function" || typeof b.fetch !== "function") {
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
return candidate;
|
|
190
|
+
}
|
|
178
191
|
|
|
179
192
|
// src/client/BstageClient.ts
|
|
180
193
|
function resolveBaseUrl() {
|
|
194
|
+
const bridge = getFrameBridge();
|
|
195
|
+
if (bridge) {
|
|
196
|
+
const gatewayBase = bridge.getInit()?.gatewayBase;
|
|
197
|
+
return typeof gatewayBase === "string" ? gatewayBase : `${bridge.hostOrigin}/gw`;
|
|
198
|
+
}
|
|
181
199
|
if (typeof globalThis.location !== "undefined") {
|
|
182
200
|
return `${globalThis.location.origin}/gw`;
|
|
183
201
|
}
|
|
@@ -192,6 +210,8 @@ function resolvePath(template, params) {
|
|
|
192
210
|
});
|
|
193
211
|
}
|
|
194
212
|
var resolveBstageFetch = (input, init) => {
|
|
213
|
+
const bridge = getFrameBridge();
|
|
214
|
+
if (bridge) return bridge.fetch(input, init);
|
|
195
215
|
const hook = globalThis.__bstage_fetch__;
|
|
196
216
|
return hook ? hook(input, init) : globalThis.fetch(input, init);
|
|
197
217
|
};
|
|
@@ -290,7 +310,12 @@ function readLanguageCookie(target) {
|
|
|
290
310
|
}
|
|
291
311
|
return void 0;
|
|
292
312
|
}
|
|
313
|
+
function readBridgeLocale(bridge) {
|
|
314
|
+
return normalizeLanguage(bridge.getInit()?.locale) ?? DEFAULT_LANGUAGE;
|
|
315
|
+
}
|
|
293
316
|
function readLocale(target = "user") {
|
|
317
|
+
const bridge = getFrameBridge();
|
|
318
|
+
if (bridge) return readBridgeLocale(bridge);
|
|
294
319
|
if (typeof document === "undefined") return DEFAULT_LANGUAGE;
|
|
295
320
|
const htmlLang = normalizeLanguage(document.documentElement.lang);
|
|
296
321
|
if (target === "admin") {
|
|
@@ -299,6 +324,17 @@ function readLocale(target = "user") {
|
|
|
299
324
|
return htmlLang ?? readLanguageCookie("user") ?? DEFAULT_LANGUAGE;
|
|
300
325
|
}
|
|
301
326
|
function observeLocale(onChange, target = "user") {
|
|
327
|
+
const bridge = getFrameBridge();
|
|
328
|
+
if (bridge) {
|
|
329
|
+
let current2 = readBridgeLocale(bridge);
|
|
330
|
+
return bridge.on("locale", () => {
|
|
331
|
+
const next = readBridgeLocale(bridge);
|
|
332
|
+
if (next !== current2) {
|
|
333
|
+
current2 = next;
|
|
334
|
+
onChange(next);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
302
338
|
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
|
303
339
|
return () => {
|
|
304
340
|
};
|
|
@@ -434,6 +470,8 @@ function fetchVersionBase(source, fetchFn) {
|
|
|
434
470
|
return promise;
|
|
435
471
|
}
|
|
436
472
|
function fetchTranslations(locale, system) {
|
|
473
|
+
const bridge = getFrameBridge();
|
|
474
|
+
if (bridge) return Promise.resolve(readBridgeTranslations(bridge, locale));
|
|
437
475
|
const source = resolveSource(system);
|
|
438
476
|
const key = `${sourceKey(source)}|${locale}`;
|
|
439
477
|
const cached = translationCache.get(key);
|
|
@@ -450,6 +488,26 @@ function fetchTranslations(locale, system) {
|
|
|
450
488
|
translationCache.set(key, promise);
|
|
451
489
|
return promise;
|
|
452
490
|
}
|
|
491
|
+
function observeTranslations(onChange) {
|
|
492
|
+
const bridge = getFrameBridge();
|
|
493
|
+
if (!bridge) return () => {
|
|
494
|
+
};
|
|
495
|
+
let last = bridge.getInit()?.translations;
|
|
496
|
+
return bridge.on("locale", () => {
|
|
497
|
+
const next = bridge.getInit()?.translations;
|
|
498
|
+
if (next !== last) {
|
|
499
|
+
last = next;
|
|
500
|
+
onChange();
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
function readBridgeTranslations(bridge, locale) {
|
|
505
|
+
const init = bridge.getInit();
|
|
506
|
+
if (!init || normalizeLanguage(init.locale) !== locale) return {};
|
|
507
|
+
const dict = init.translations;
|
|
508
|
+
if (typeof dict !== "object" || dict === null) return {};
|
|
509
|
+
return Object.fromEntries(Object.entries(dict).filter(([, v]) => typeof v === "string"));
|
|
510
|
+
}
|
|
453
511
|
var DEFAULT_FALLBACK_CHAIN = ["en", "ko"];
|
|
454
512
|
function resolveTranslationChain(current) {
|
|
455
513
|
const tail = i18nConfig.fallbackChain ?? DEFAULT_FALLBACK_CHAIN;
|
|
@@ -927,6 +985,7 @@ function __resetElementRegistry__() {
|
|
|
927
985
|
dirNameToSlotId,
|
|
928
986
|
fetchTranslations,
|
|
929
987
|
getAdminEventsByDomain,
|
|
988
|
+
getFrameBridge,
|
|
930
989
|
getSlotsByTarget,
|
|
931
990
|
getSlotsByTargetV2,
|
|
932
991
|
interpolateTranslation,
|
|
@@ -934,6 +993,7 @@ function __resetElementRegistry__() {
|
|
|
934
993
|
lookupElementName,
|
|
935
994
|
normalizeLanguage,
|
|
936
995
|
observeLocale,
|
|
996
|
+
observeTranslations,
|
|
937
997
|
parseRichText,
|
|
938
998
|
pickTranslation,
|
|
939
999
|
readLocale,
|
package/dist/index.d.cts
CHANGED
|
@@ -116,11 +116,14 @@ interface BstageConfig {
|
|
|
116
116
|
* 특정 게이트웨이를 직접 지정하려면 `baseUrl`을 쓴다. (예약 필드)
|
|
117
117
|
*/
|
|
118
118
|
phase?: 'dev' | 'qa' | 'real' | 'sandbox';
|
|
119
|
-
/** base URL 직접 지정. 미지정 시
|
|
119
|
+
/** base URL 직접 지정. 미지정 시 `location.origin + /gw`(격리 프레임이면 init의 `gatewayBase`). */
|
|
120
120
|
baseUrl?: string;
|
|
121
121
|
/** 요청 타임아웃 (ms). */
|
|
122
122
|
timeout?: number;
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* 커스텀 fetch 함수. 미지정 시 격리 프레임이면 브리지 fetch, 아니면 `globalThis.__bstage_fetch__` →
|
|
125
|
+
* `globalThis.fetch` 순으로 사용.
|
|
126
|
+
*/
|
|
124
127
|
fetch?: FetchFunction;
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -303,6 +306,9 @@ declare function normalizeLanguage(value: string | null | undefined): LanguageCo
|
|
|
303
306
|
* 어드민은 `<html lang>`이 SSR 기본값으로 고정(런타임 미갱신)이라 쿠키를 1차로 신뢰한다.
|
|
304
307
|
*
|
|
305
308
|
* DOM이 없는 환경(SSR 등)에서는 기본값을 돌려준다.
|
|
309
|
+
*
|
|
310
|
+
* 격리 프레임(브리지 있음)에서는 대상과 무관하게 호스트가 init·`locale` 이벤트로 준 값을 쓴다
|
|
311
|
+
* (프레임 문서의 쿠키·`<html lang>`은 호스트 것이 아니다).
|
|
306
312
|
*/
|
|
307
313
|
declare function readLocale(target?: LocaleTarget): LanguageCode;
|
|
308
314
|
/**
|
|
@@ -317,6 +323,8 @@ declare function readLocale(target?: LocaleTarget): LanguageCode;
|
|
|
317
323
|
* 그 변경을 감지해 `readLocale('admin')`이 쿠키를 다시 읽는다.
|
|
318
324
|
*
|
|
319
325
|
* DOM/MutationObserver가 없는 환경에서는 아무 동작도 하지 않는 해제 함수를 반환한다.
|
|
326
|
+
*
|
|
327
|
+
* 격리 프레임에서는 브리지 `locale` 이벤트를 구독한다.
|
|
320
328
|
*/
|
|
321
329
|
declare function observeLocale(onChange: (locale: LanguageCode) => void, target?: LocaleTarget): () => void;
|
|
322
330
|
/** 플랫폼 번역 사전 — 로케일별 `Bxxxxx` 키 → 문자열(flat). */
|
|
@@ -390,10 +398,19 @@ declare function configureBstageI18n(config: BstageI18nConfig): void;
|
|
|
390
398
|
* fetch는 `config.fetch` → `globalThis.fetch` 순으로 해석한다(공개 CDN이라 인증 fetch를 쓰지 않는다).
|
|
391
399
|
* 실패 시 캐시를 비워 재시도를 허용한다.
|
|
392
400
|
*
|
|
401
|
+
* 격리 프레임에서는 CDN을 부르지 않는다(프레임은 직접 네트워크가 막혀 있다). 호스트가 init·`locale`
|
|
402
|
+
* 이벤트에 실어 준 **현재 locale 사전**만 돌려주고, 다른 locale·사전이 없으면 빈 사전이다.
|
|
403
|
+
*
|
|
393
404
|
* @param locale 받아올 로케일
|
|
394
405
|
* @param system 번역 시스템(`user`\|`admin`). React에서는 `BstageLocaleProvider`의 `target`이 넘긴다.
|
|
395
406
|
*/
|
|
396
407
|
declare function fetchTranslations(locale: LanguageCode, system?: LocaleTarget): Promise<TranslationDict>;
|
|
408
|
+
/**
|
|
409
|
+
* @internal 격리 프레임에서 호스트가 번역 사전을 새로 주면 알린다 — locale이 같고 사전만 늦게 와도.
|
|
410
|
+
* (`observeLocale`은 locale 값이 바뀔 때만 알리므로, 사전이 비동기로 뒤늦게 오는 흐름을 놓친다.)
|
|
411
|
+
* 브리지가 없으면 아무 동작도 하지 않는 해제 함수를 반환한다.
|
|
412
|
+
*/
|
|
413
|
+
declare function observeTranslations(onChange: () => void): () => void;
|
|
397
414
|
/**
|
|
398
415
|
* 현재 로케일부터 시작하는 조회 순서를 만든다. 중복은 제거한다.
|
|
399
416
|
*
|
|
@@ -1152,10 +1169,40 @@ declare function lookupElementName(url: string): string | undefined;
|
|
|
1152
1169
|
*/
|
|
1153
1170
|
declare function __resetElementRegistry__(): void;
|
|
1154
1171
|
|
|
1172
|
+
/**
|
|
1173
|
+
* 격리 프레임 브리지의 init 중 core가 읽는 부분.
|
|
1174
|
+
* 전체 모양은 `@bstage-sdk/bridge`의 `BridgeInit`이다 — core는 그 패키지에 의존하지 않고
|
|
1175
|
+
* 필요한 필드만 구조적으로 읽는다.
|
|
1176
|
+
*/
|
|
1177
|
+
interface FrameBridgeInit {
|
|
1178
|
+
readonly gatewayBase?: string;
|
|
1179
|
+
readonly locale?: string;
|
|
1180
|
+
readonly translations?: Readonly<Record<string, string>>;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* 격리 프레임 안에서 브리지 클라이언트가 설치하는 `globalThis.__bstage_bridge__`의 최소 모양.
|
|
1184
|
+
*
|
|
1185
|
+
* 템플릿이 sandbox iframe에서 돌 때만 존재한다. 없으면 모든 어댑터는 기존 경로(inline)로 동작한다.
|
|
1186
|
+
*/
|
|
1187
|
+
interface FrameBridgeLike {
|
|
1188
|
+
readonly hostOrigin: string;
|
|
1189
|
+
getInit(): FrameBridgeInit | undefined;
|
|
1190
|
+
request(type: string, payload: unknown): Promise<unknown>;
|
|
1191
|
+
on(type: string, fn: (payload: unknown) => void): () => void;
|
|
1192
|
+
readonly fetch: FetchFunction;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* @internal 현재 실행 환경의 프레임 브리지를 돌려준다. 격리 프레임이 아니면 `undefined`.
|
|
1196
|
+
*
|
|
1197
|
+
* 모양이 맞을 때만 인정한다 — 템플릿이나 다른 스크립트가 같은 이름에 다른 값을 둬도
|
|
1198
|
+
* 기존 경로가 깨지지 않게 한다. 요청마다 호출해 늦은 설치도 반영한다.
|
|
1199
|
+
*/
|
|
1200
|
+
declare function getFrameBridge(): FrameBridgeLike | undefined;
|
|
1201
|
+
|
|
1155
1202
|
/**
|
|
1156
1203
|
* 번들에 심긴 core 버전. `BstageClient`가 모든 요청에 `x-bmf-sdk-version` 헤더로 실어
|
|
1157
1204
|
* 게이트웨이 로그에서 어느 SDK 버전이 쓰이는지 볼 수 있게 한다.
|
|
1158
1205
|
*/
|
|
1159
1206
|
declare const VERSION: string;
|
|
1160
1207
|
|
|
1161
|
-
export { ADMIN_EVENT_CATALOG, type AdminConnectPayload, type AdminDomainEventMap, type AdminDomainEventType, type BoardInfo, type BoardPostInfo, type BodyRequestOptions, BstageClient, type BstageConfig, type BstageI18nConfig, type Content, type ContentsSectionInfo, type CustomSlots, type CustomSlotsV2, DEFAULT_LANGUAGE, type DigitalTicket, type FetchFunction, type FrameworkAdapter, HttpClient, type HttpClientConfig, HttpError, type HttpMethod, type HttpResponse, HttpTimeoutError, LANGUAGES, type LanguageCode, type LocaleTarget, type PathParams, PlatformBridge, type PlatformEventHandler, type PlatformEventMap, type PlatformEventType, type Product, type QueryRequestOptions, type RequestConfig, type RequestInterceptor, type RequestOptions, type ResponseInterceptor, type RichTextToken, SLOT_CATALOG, SLOT_CATALOG_V2, type SlotCatalogEntryV2, type SlotContextMap, type SlotContextOf, type SlotContextOverrides, type SlotContextPayload, type SlotIdV2, type TemplateEventMap, type TemplateEventType, type TemplateOptions, type TemplateSlot, type TicketCreateAfterPayload, type TranslateParams, type TranslationDict, type TranslationKey, type TranslationKeyRegistry, type TranslationPhase, VERSION, __resetElementRegistry__, announceElementName, configureBstageI18n, createWebComponent, dirNameToSlotId, fetchTranslations, getAdminEventsByDomain, getSlotsByTarget, getSlotsByTargetV2, interpolateTranslation, isSlotIdV2, lookupElementName, normalizeLanguage, observeLocale, parseRichText, pickTranslation, readLocale, resolveTranslationChain, slotIdToDirName };
|
|
1208
|
+
export { ADMIN_EVENT_CATALOG, type AdminConnectPayload, type AdminDomainEventMap, type AdminDomainEventType, type BoardInfo, type BoardPostInfo, type BodyRequestOptions, BstageClient, type BstageConfig, type BstageI18nConfig, type Content, type ContentsSectionInfo, type CustomSlots, type CustomSlotsV2, DEFAULT_LANGUAGE, type DigitalTicket, type FetchFunction, type FrameBridgeInit, type FrameBridgeLike, type FrameworkAdapter, HttpClient, type HttpClientConfig, HttpError, type HttpMethod, type HttpResponse, HttpTimeoutError, LANGUAGES, type LanguageCode, type LocaleTarget, type PathParams, PlatformBridge, type PlatformEventHandler, type PlatformEventMap, type PlatformEventType, type Product, type QueryRequestOptions, type RequestConfig, type RequestInterceptor, type RequestOptions, type ResponseInterceptor, type RichTextToken, SLOT_CATALOG, SLOT_CATALOG_V2, type SlotCatalogEntryV2, type SlotContextMap, type SlotContextOf, type SlotContextOverrides, type SlotContextPayload, type SlotIdV2, type TemplateEventMap, type TemplateEventType, type TemplateOptions, type TemplateSlot, type TicketCreateAfterPayload, type TranslateParams, type TranslationDict, type TranslationKey, type TranslationKeyRegistry, type TranslationPhase, VERSION, __resetElementRegistry__, announceElementName, configureBstageI18n, createWebComponent, dirNameToSlotId, fetchTranslations, getAdminEventsByDomain, getFrameBridge, getSlotsByTarget, getSlotsByTargetV2, interpolateTranslation, isSlotIdV2, lookupElementName, normalizeLanguage, observeLocale, observeTranslations, parseRichText, pickTranslation, readLocale, resolveTranslationChain, slotIdToDirName };
|
package/dist/index.d.ts
CHANGED
|
@@ -116,11 +116,14 @@ interface BstageConfig {
|
|
|
116
116
|
* 특정 게이트웨이를 직접 지정하려면 `baseUrl`을 쓴다. (예약 필드)
|
|
117
117
|
*/
|
|
118
118
|
phase?: 'dev' | 'qa' | 'real' | 'sandbox';
|
|
119
|
-
/** base URL 직접 지정. 미지정 시
|
|
119
|
+
/** base URL 직접 지정. 미지정 시 `location.origin + /gw`(격리 프레임이면 init의 `gatewayBase`). */
|
|
120
120
|
baseUrl?: string;
|
|
121
121
|
/** 요청 타임아웃 (ms). */
|
|
122
122
|
timeout?: number;
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* 커스텀 fetch 함수. 미지정 시 격리 프레임이면 브리지 fetch, 아니면 `globalThis.__bstage_fetch__` →
|
|
125
|
+
* `globalThis.fetch` 순으로 사용.
|
|
126
|
+
*/
|
|
124
127
|
fetch?: FetchFunction;
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -303,6 +306,9 @@ declare function normalizeLanguage(value: string | null | undefined): LanguageCo
|
|
|
303
306
|
* 어드민은 `<html lang>`이 SSR 기본값으로 고정(런타임 미갱신)이라 쿠키를 1차로 신뢰한다.
|
|
304
307
|
*
|
|
305
308
|
* DOM이 없는 환경(SSR 등)에서는 기본값을 돌려준다.
|
|
309
|
+
*
|
|
310
|
+
* 격리 프레임(브리지 있음)에서는 대상과 무관하게 호스트가 init·`locale` 이벤트로 준 값을 쓴다
|
|
311
|
+
* (프레임 문서의 쿠키·`<html lang>`은 호스트 것이 아니다).
|
|
306
312
|
*/
|
|
307
313
|
declare function readLocale(target?: LocaleTarget): LanguageCode;
|
|
308
314
|
/**
|
|
@@ -317,6 +323,8 @@ declare function readLocale(target?: LocaleTarget): LanguageCode;
|
|
|
317
323
|
* 그 변경을 감지해 `readLocale('admin')`이 쿠키를 다시 읽는다.
|
|
318
324
|
*
|
|
319
325
|
* DOM/MutationObserver가 없는 환경에서는 아무 동작도 하지 않는 해제 함수를 반환한다.
|
|
326
|
+
*
|
|
327
|
+
* 격리 프레임에서는 브리지 `locale` 이벤트를 구독한다.
|
|
320
328
|
*/
|
|
321
329
|
declare function observeLocale(onChange: (locale: LanguageCode) => void, target?: LocaleTarget): () => void;
|
|
322
330
|
/** 플랫폼 번역 사전 — 로케일별 `Bxxxxx` 키 → 문자열(flat). */
|
|
@@ -390,10 +398,19 @@ declare function configureBstageI18n(config: BstageI18nConfig): void;
|
|
|
390
398
|
* fetch는 `config.fetch` → `globalThis.fetch` 순으로 해석한다(공개 CDN이라 인증 fetch를 쓰지 않는다).
|
|
391
399
|
* 실패 시 캐시를 비워 재시도를 허용한다.
|
|
392
400
|
*
|
|
401
|
+
* 격리 프레임에서는 CDN을 부르지 않는다(프레임은 직접 네트워크가 막혀 있다). 호스트가 init·`locale`
|
|
402
|
+
* 이벤트에 실어 준 **현재 locale 사전**만 돌려주고, 다른 locale·사전이 없으면 빈 사전이다.
|
|
403
|
+
*
|
|
393
404
|
* @param locale 받아올 로케일
|
|
394
405
|
* @param system 번역 시스템(`user`\|`admin`). React에서는 `BstageLocaleProvider`의 `target`이 넘긴다.
|
|
395
406
|
*/
|
|
396
407
|
declare function fetchTranslations(locale: LanguageCode, system?: LocaleTarget): Promise<TranslationDict>;
|
|
408
|
+
/**
|
|
409
|
+
* @internal 격리 프레임에서 호스트가 번역 사전을 새로 주면 알린다 — locale이 같고 사전만 늦게 와도.
|
|
410
|
+
* (`observeLocale`은 locale 값이 바뀔 때만 알리므로, 사전이 비동기로 뒤늦게 오는 흐름을 놓친다.)
|
|
411
|
+
* 브리지가 없으면 아무 동작도 하지 않는 해제 함수를 반환한다.
|
|
412
|
+
*/
|
|
413
|
+
declare function observeTranslations(onChange: () => void): () => void;
|
|
397
414
|
/**
|
|
398
415
|
* 현재 로케일부터 시작하는 조회 순서를 만든다. 중복은 제거한다.
|
|
399
416
|
*
|
|
@@ -1152,10 +1169,40 @@ declare function lookupElementName(url: string): string | undefined;
|
|
|
1152
1169
|
*/
|
|
1153
1170
|
declare function __resetElementRegistry__(): void;
|
|
1154
1171
|
|
|
1172
|
+
/**
|
|
1173
|
+
* 격리 프레임 브리지의 init 중 core가 읽는 부분.
|
|
1174
|
+
* 전체 모양은 `@bstage-sdk/bridge`의 `BridgeInit`이다 — core는 그 패키지에 의존하지 않고
|
|
1175
|
+
* 필요한 필드만 구조적으로 읽는다.
|
|
1176
|
+
*/
|
|
1177
|
+
interface FrameBridgeInit {
|
|
1178
|
+
readonly gatewayBase?: string;
|
|
1179
|
+
readonly locale?: string;
|
|
1180
|
+
readonly translations?: Readonly<Record<string, string>>;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* 격리 프레임 안에서 브리지 클라이언트가 설치하는 `globalThis.__bstage_bridge__`의 최소 모양.
|
|
1184
|
+
*
|
|
1185
|
+
* 템플릿이 sandbox iframe에서 돌 때만 존재한다. 없으면 모든 어댑터는 기존 경로(inline)로 동작한다.
|
|
1186
|
+
*/
|
|
1187
|
+
interface FrameBridgeLike {
|
|
1188
|
+
readonly hostOrigin: string;
|
|
1189
|
+
getInit(): FrameBridgeInit | undefined;
|
|
1190
|
+
request(type: string, payload: unknown): Promise<unknown>;
|
|
1191
|
+
on(type: string, fn: (payload: unknown) => void): () => void;
|
|
1192
|
+
readonly fetch: FetchFunction;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* @internal 현재 실행 환경의 프레임 브리지를 돌려준다. 격리 프레임이 아니면 `undefined`.
|
|
1196
|
+
*
|
|
1197
|
+
* 모양이 맞을 때만 인정한다 — 템플릿이나 다른 스크립트가 같은 이름에 다른 값을 둬도
|
|
1198
|
+
* 기존 경로가 깨지지 않게 한다. 요청마다 호출해 늦은 설치도 반영한다.
|
|
1199
|
+
*/
|
|
1200
|
+
declare function getFrameBridge(): FrameBridgeLike | undefined;
|
|
1201
|
+
|
|
1155
1202
|
/**
|
|
1156
1203
|
* 번들에 심긴 core 버전. `BstageClient`가 모든 요청에 `x-bmf-sdk-version` 헤더로 실어
|
|
1157
1204
|
* 게이트웨이 로그에서 어느 SDK 버전이 쓰이는지 볼 수 있게 한다.
|
|
1158
1205
|
*/
|
|
1159
1206
|
declare const VERSION: string;
|
|
1160
1207
|
|
|
1161
|
-
export { ADMIN_EVENT_CATALOG, type AdminConnectPayload, type AdminDomainEventMap, type AdminDomainEventType, type BoardInfo, type BoardPostInfo, type BodyRequestOptions, BstageClient, type BstageConfig, type BstageI18nConfig, type Content, type ContentsSectionInfo, type CustomSlots, type CustomSlotsV2, DEFAULT_LANGUAGE, type DigitalTicket, type FetchFunction, type FrameworkAdapter, HttpClient, type HttpClientConfig, HttpError, type HttpMethod, type HttpResponse, HttpTimeoutError, LANGUAGES, type LanguageCode, type LocaleTarget, type PathParams, PlatformBridge, type PlatformEventHandler, type PlatformEventMap, type PlatformEventType, type Product, type QueryRequestOptions, type RequestConfig, type RequestInterceptor, type RequestOptions, type ResponseInterceptor, type RichTextToken, SLOT_CATALOG, SLOT_CATALOG_V2, type SlotCatalogEntryV2, type SlotContextMap, type SlotContextOf, type SlotContextOverrides, type SlotContextPayload, type SlotIdV2, type TemplateEventMap, type TemplateEventType, type TemplateOptions, type TemplateSlot, type TicketCreateAfterPayload, type TranslateParams, type TranslationDict, type TranslationKey, type TranslationKeyRegistry, type TranslationPhase, VERSION, __resetElementRegistry__, announceElementName, configureBstageI18n, createWebComponent, dirNameToSlotId, fetchTranslations, getAdminEventsByDomain, getSlotsByTarget, getSlotsByTargetV2, interpolateTranslation, isSlotIdV2, lookupElementName, normalizeLanguage, observeLocale, parseRichText, pickTranslation, readLocale, resolveTranslationChain, slotIdToDirName };
|
|
1208
|
+
export { ADMIN_EVENT_CATALOG, type AdminConnectPayload, type AdminDomainEventMap, type AdminDomainEventType, type BoardInfo, type BoardPostInfo, type BodyRequestOptions, BstageClient, type BstageConfig, type BstageI18nConfig, type Content, type ContentsSectionInfo, type CustomSlots, type CustomSlotsV2, DEFAULT_LANGUAGE, type DigitalTicket, type FetchFunction, type FrameBridgeInit, type FrameBridgeLike, type FrameworkAdapter, HttpClient, type HttpClientConfig, HttpError, type HttpMethod, type HttpResponse, HttpTimeoutError, LANGUAGES, type LanguageCode, type LocaleTarget, type PathParams, PlatformBridge, type PlatformEventHandler, type PlatformEventMap, type PlatformEventType, type Product, type QueryRequestOptions, type RequestConfig, type RequestInterceptor, type RequestOptions, type ResponseInterceptor, type RichTextToken, SLOT_CATALOG, SLOT_CATALOG_V2, type SlotCatalogEntryV2, type SlotContextMap, type SlotContextOf, type SlotContextOverrides, type SlotContextPayload, type SlotIdV2, type TemplateEventMap, type TemplateEventType, type TemplateOptions, type TemplateSlot, type TicketCreateAfterPayload, type TranslateParams, type TranslationDict, type TranslationKey, type TranslationKeyRegistry, type TranslationPhase, VERSION, __resetElementRegistry__, announceElementName, configureBstageI18n, createWebComponent, dirNameToSlotId, fetchTranslations, getAdminEventsByDomain, getFrameBridge, getSlotsByTarget, getSlotsByTargetV2, interpolateTranslation, isSlotIdV2, lookupElementName, normalizeLanguage, observeLocale, observeTranslations, parseRichText, pickTranslation, readLocale, resolveTranslationChain, slotIdToDirName };
|
package/dist/index.js
CHANGED
|
@@ -119,10 +119,26 @@ var HttpTimeoutError = class extends Error {
|
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
// src/version.ts
|
|
122
|
-
var VERSION = "0.
|
|
122
|
+
var VERSION = "0.9.1";
|
|
123
|
+
|
|
124
|
+
// src/frameBridge.ts
|
|
125
|
+
function getFrameBridge() {
|
|
126
|
+
const candidate = globalThis.__bstage_bridge__;
|
|
127
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
128
|
+
const b = candidate;
|
|
129
|
+
if (typeof b.hostOrigin !== "string" || typeof b.getInit !== "function" || typeof b.request !== "function" || typeof b.on !== "function" || typeof b.fetch !== "function") {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
return candidate;
|
|
133
|
+
}
|
|
123
134
|
|
|
124
135
|
// src/client/BstageClient.ts
|
|
125
136
|
function resolveBaseUrl() {
|
|
137
|
+
const bridge = getFrameBridge();
|
|
138
|
+
if (bridge) {
|
|
139
|
+
const gatewayBase = bridge.getInit()?.gatewayBase;
|
|
140
|
+
return typeof gatewayBase === "string" ? gatewayBase : `${bridge.hostOrigin}/gw`;
|
|
141
|
+
}
|
|
126
142
|
if (typeof globalThis.location !== "undefined") {
|
|
127
143
|
return `${globalThis.location.origin}/gw`;
|
|
128
144
|
}
|
|
@@ -137,6 +153,8 @@ function resolvePath(template, params) {
|
|
|
137
153
|
});
|
|
138
154
|
}
|
|
139
155
|
var resolveBstageFetch = (input, init) => {
|
|
156
|
+
const bridge = getFrameBridge();
|
|
157
|
+
if (bridge) return bridge.fetch(input, init);
|
|
140
158
|
const hook = globalThis.__bstage_fetch__;
|
|
141
159
|
return hook ? hook(input, init) : globalThis.fetch(input, init);
|
|
142
160
|
};
|
|
@@ -235,7 +253,12 @@ function readLanguageCookie(target) {
|
|
|
235
253
|
}
|
|
236
254
|
return void 0;
|
|
237
255
|
}
|
|
256
|
+
function readBridgeLocale(bridge) {
|
|
257
|
+
return normalizeLanguage(bridge.getInit()?.locale) ?? DEFAULT_LANGUAGE;
|
|
258
|
+
}
|
|
238
259
|
function readLocale(target = "user") {
|
|
260
|
+
const bridge = getFrameBridge();
|
|
261
|
+
if (bridge) return readBridgeLocale(bridge);
|
|
239
262
|
if (typeof document === "undefined") return DEFAULT_LANGUAGE;
|
|
240
263
|
const htmlLang = normalizeLanguage(document.documentElement.lang);
|
|
241
264
|
if (target === "admin") {
|
|
@@ -244,6 +267,17 @@ function readLocale(target = "user") {
|
|
|
244
267
|
return htmlLang ?? readLanguageCookie("user") ?? DEFAULT_LANGUAGE;
|
|
245
268
|
}
|
|
246
269
|
function observeLocale(onChange, target = "user") {
|
|
270
|
+
const bridge = getFrameBridge();
|
|
271
|
+
if (bridge) {
|
|
272
|
+
let current2 = readBridgeLocale(bridge);
|
|
273
|
+
return bridge.on("locale", () => {
|
|
274
|
+
const next = readBridgeLocale(bridge);
|
|
275
|
+
if (next !== current2) {
|
|
276
|
+
current2 = next;
|
|
277
|
+
onChange(next);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
247
281
|
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
|
248
282
|
return () => {
|
|
249
283
|
};
|
|
@@ -379,6 +413,8 @@ function fetchVersionBase(source, fetchFn) {
|
|
|
379
413
|
return promise;
|
|
380
414
|
}
|
|
381
415
|
function fetchTranslations(locale, system) {
|
|
416
|
+
const bridge = getFrameBridge();
|
|
417
|
+
if (bridge) return Promise.resolve(readBridgeTranslations(bridge, locale));
|
|
382
418
|
const source = resolveSource(system);
|
|
383
419
|
const key = `${sourceKey(source)}|${locale}`;
|
|
384
420
|
const cached = translationCache.get(key);
|
|
@@ -395,6 +431,26 @@ function fetchTranslations(locale, system) {
|
|
|
395
431
|
translationCache.set(key, promise);
|
|
396
432
|
return promise;
|
|
397
433
|
}
|
|
434
|
+
function observeTranslations(onChange) {
|
|
435
|
+
const bridge = getFrameBridge();
|
|
436
|
+
if (!bridge) return () => {
|
|
437
|
+
};
|
|
438
|
+
let last = bridge.getInit()?.translations;
|
|
439
|
+
return bridge.on("locale", () => {
|
|
440
|
+
const next = bridge.getInit()?.translations;
|
|
441
|
+
if (next !== last) {
|
|
442
|
+
last = next;
|
|
443
|
+
onChange();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function readBridgeTranslations(bridge, locale) {
|
|
448
|
+
const init = bridge.getInit();
|
|
449
|
+
if (!init || normalizeLanguage(init.locale) !== locale) return {};
|
|
450
|
+
const dict = init.translations;
|
|
451
|
+
if (typeof dict !== "object" || dict === null) return {};
|
|
452
|
+
return Object.fromEntries(Object.entries(dict).filter(([, v]) => typeof v === "string"));
|
|
453
|
+
}
|
|
398
454
|
var DEFAULT_FALLBACK_CHAIN = ["en", "ko"];
|
|
399
455
|
function resolveTranslationChain(current) {
|
|
400
456
|
const tail = i18nConfig.fallbackChain ?? DEFAULT_FALLBACK_CHAIN;
|
|
@@ -871,6 +927,7 @@ export {
|
|
|
871
927
|
dirNameToSlotId,
|
|
872
928
|
fetchTranslations,
|
|
873
929
|
getAdminEventsByDomain,
|
|
930
|
+
getFrameBridge,
|
|
874
931
|
getSlotsByTarget,
|
|
875
932
|
getSlotsByTargetV2,
|
|
876
933
|
interpolateTranslation,
|
|
@@ -878,6 +935,7 @@ export {
|
|
|
878
935
|
lookupElementName,
|
|
879
936
|
normalizeLanguage,
|
|
880
937
|
observeLocale,
|
|
938
|
+
observeTranslations,
|
|
881
939
|
parseRichText,
|
|
882
940
|
pickTranslation,
|
|
883
941
|
readLocale,
|
package/docs/BUILD_SYSTEM.md
CHANGED
|
@@ -124,7 +124,7 @@ createTemplate(MyTemplate, { name: 'my-space-hello', styles: css })
|
|
|
124
124
|
- `loadTemplate`이 로드 직후 이 값을 조회합니다.
|
|
125
125
|
- 조회에 실패하면 **옛 번들 폴백**을 탑니다. 옛 산출물은 디렉토리 이름이 곧 태그명이었으므로(`dist/{name}/`) URL의 디렉토리 세그먼트를 후보로 삼되, 추측으로 쓰지 않고 `customElements.get()`으로 실제 등록됐는지 확인합니다. 확인까지 실패하면 던집니다. 옛 번들이 전부 재배포되면 걷어낼 수 있습니다.
|
|
126
126
|
|
|
127
|
-
> 예전에는 `manifest.json`이 이 값을 날랐습니다.
|
|
127
|
+
> 예전에는 `manifest.json`이 이 값을 날랐습니다. 호스트 로더는 지금도 manifest를 읽지 않습니다 — 태그명은 위 전역으로 충분합니다. 다만 **포털**이 iframe 격리 판단에 manifest를 쓰므로 0.9.0부터 다시 생성합니다(아래 "manifest.json").
|
|
128
128
|
|
|
129
129
|
`spaceId` / `type`(user·admin) / `tag`(버전) 같은 상위 경로 세그먼트는 관리도구가 CDN 업로드 단계에서 주입하며, SDK는 관여하지 않습니다. 슬롯 배치와 풀페이지 레이아웃은 관리도구가 소유합니다.
|
|
130
130
|
|
|
@@ -136,15 +136,59 @@ createTemplate(MyTemplate, { name: 'my-space-hello', styles: css })
|
|
|
136
136
|
dist/
|
|
137
137
|
landing/
|
|
138
138
|
template.js ← 페이지 /landing
|
|
139
|
+
manifest.json
|
|
139
140
|
settings/
|
|
140
141
|
custom/
|
|
141
142
|
template.js ← 페이지 /settings/custom
|
|
143
|
+
manifest.json
|
|
142
144
|
user.contents-home.curation--after/
|
|
143
145
|
template.js ← 위젯
|
|
146
|
+
manifest.json
|
|
144
147
|
```
|
|
145
148
|
|
|
146
149
|
`template.js`는 IIFE 번들 하나입니다(React 런타임 + 컴포넌트 + CSS 인라인).
|
|
147
150
|
|
|
151
|
+
### manifest.json
|
|
152
|
+
|
|
153
|
+
`template.js` 옆에 놓입니다. 키 순서·들여쓰기가 고정이라 같은 입력이면 같은 바이트가 나옵니다(앞 네 필드는 예전 CLI가 만들던 것과 같은 모양, `type`만 빠짐. `bridgeProtocol`이 뒤에 붙음).
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"elementName": "my-space-hello",
|
|
158
|
+
"entry": "template.js",
|
|
159
|
+
"sdkVersion": "0.9.0",
|
|
160
|
+
"framework": "react",
|
|
161
|
+
"bridgeProtocol": 1
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| 필드 | 값 |
|
|
166
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
167
|
+
| `elementName` | `createTemplate`의 `name` — 등록되는 Custom Element 태그 |
|
|
168
|
+
| `entry` | manifest 디렉토리 기준 번들 경로. 항상 `template.js` |
|
|
169
|
+
| `sdkVersion` | **번들에 실제로 실린** `@bstage-sdk/core` 버전(CLI 버전 아님). 두 벌 실렸으면 낮은 쪽. 정보용 |
|
|
170
|
+
| `framework` | `react` 등. 소스에서 못 찾으면 번들에 bstage-react가 실렸는지로 정함 |
|
|
171
|
+
| `bridgeProtocol` | 번들에 실린 core가 구현한 격리 브리지 프로토콜 버전. core `package.json`의 `bstage.bridgeProtocol`에서 읽음. 없을 수 있음 |
|
|
172
|
+
|
|
173
|
+
읽는 쪽은 포털입니다. 게시 빌드를 전개할 때 산출물 디렉토리마다 `bridgeProtocol`·`elementName`을 기록하고, 둘 다 있고 `bridgeProtocol`이 1 이상일 때만 그 템플릿의 iframe 격리를 허용합니다. `sdkVersion`은 게이트에 쓰지 않습니다(정보용). 포털 빌더는 manifest가 있으면 앞 네 필드를 검증해 틀리면 빌드를 실패시킵니다.
|
|
174
|
+
|
|
175
|
+
> manifest는 **파트너 빌드가 스스로 적는 값**입니다. `bridgeProtocol`은 "이 번들은 격리 프레임에서 돌 수 있다"는 신고일 뿐 보안 근거가 아닙니다 — 격리 자체는 호스트(sandbox iframe·CSP·브리지 정책)가 강제하고, 브리지 호스트 정책은 manifest를 읽지 않습니다. 값이 틀리면 그 템플릿이 프레임 안에서 동작하지 않을 뿐입니다. 반대로 manifest가 없거나 값이 없으면 inline(호스트 origin에서 실행)으로만 배치되므로, 격리를 요구해야 하는 배치는 포털이 fail closed로 다룹니다(배치 거부).
|
|
176
|
+
|
|
177
|
+
그래서 CLI는 **검증을 통과할 수 없는 manifest는 쓰지 않고 경고만** 냅니다 — 빌드는 성공하고 그 템플릿은 inline으로만 배치됩니다.
|
|
178
|
+
|
|
179
|
+
- `name`이 포털 규칙 `^[a-z][a-z0-9]*(-[a-z0-9]+)+$`(100자 이하)에 맞지 않을 때 — CLI의 `name` 검증보다 좁습니다(`a--b`, `a-`는 빌드는 되지만 격리 불가)
|
|
180
|
+
- 번들에서 bstage-core 버전을 찾지 못했을 때
|
|
181
|
+
|
|
182
|
+
`bridgeProtocol`은 core가 package.json에 선언합니다.
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{ "name": "@bstage-sdk/core", "bstage": { "bridgeProtocol": 1 } }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- core의 `bridgeProtocol`은 SDK 런타임 묶음(core·react 어댑터) 전체를 대표합니다. react는 core 버전을 고정하므로 옛 react가 새 core와 실리면 core가 두 벌이 되어 아래 최솟값 규칙이 덮습니다.
|
|
189
|
+
- 번들에 core가 여러 벌 실렸으면 **가장 작은 값**을 적습니다.
|
|
190
|
+
- 한 벌이라도 값을 모르면(필드가 없는 옛 core, 양의 정수가 아닌 값, package.json으로 귀속하지 못한 사본. 다른 패키지 dist에 미리 번들돼 들어온 core는 구분하지 못합니다) 필드를 **쓰지 않습니다** — 모르는 사본보다 높은 값을 주장하지 않기 위해서입니다. 이때 빌드는 경고만 내고 그 템플릿은 inline으로만 배치됩니다.
|
|
191
|
+
|
|
148
192
|
페이지 산출물은 경로만큼 중첩됩니다 — 단일 계층이 아닙니다. `spaceId` / `type`(user·admin) / `tag`(버전) 등 상위 경로 세그먼트는 관리도구가 CDN 업로드 시점에 주입하며, SDK는 관여하지 않습니다.
|
|
149
193
|
|
|
150
194
|
---
|
package/docs/DEV_SERVER.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`bstage dev`는 Vite 개발 서버에 인증 프록시를 내장하여, 로컬에서도 b.stage API를 인증된 상태로 호출할 수 있게 합니다. 별도 프록시 서버 없이 Vite 플러그인 미들웨어 하나로 동작합니다.
|
|
6
6
|
|
|
7
|
-
**왜 프록시가 필요한가:** b.stage
|
|
7
|
+
**왜 프록시가 필요한가:** b.stage 로그인 토큰은 쿠키로 관리됩니다(리프레시 토큰은 HttpOnly, 액세스 토큰 쿠키는 현재 HttpOnly가 아닙니다 — 전환 계획). 프로덕션에서는 동일 도메인이므로 쿠키가 자동 첨부되고 호스트가 `/gw` 호출에 Bearer 헤더를 붙이지만, localhost에서는 도메인이 달라 쿠키가 전달되지 않습니다. 인증 프록시가 로그인 쿠키를 읽어 `/gw` 호출에 `Authorization: Bearer` 헤더를 붙여 이 문제를 해결합니다.
|
|
8
8
|
|
|
9
9
|
> **liquid 템플릿 레포는 다릅니다.** `public/{user|admin}/{이름}/template.liquid`로 만든 레포에서는 같은 명령이 Vite·인증 프록시 없이 liquidjs 프리뷰를 띄웁니다 — [7절](#7-liquid-프리뷰-liquid-레포) 참조. 1~6절은 React(SDK) 레포 이야기입니다.
|
|
10
10
|
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -30,7 +30,7 @@ b.stage 3rd-party 템플릿은 **Web Component** 기반으로 동작합니다. S
|
|
|
30
30
|
| `sdk` | React 컴포넌트를 Web Component로 빌드해 페이지·위젯 자리에 올립니다 (이 문서 전체) |
|
|
31
31
|
| `liquid` | 플랫폼이 서버에서 데이터를 넣어 렌더하는 템플릿을 페이지 자리에만 올립니다 |
|
|
32
32
|
|
|
33
|
-
이 문서는 `sdk` 방식을 기준으로
|
|
33
|
+
이 문서는 `sdk` 방식을 기준으로 설명합니다. liquid 방식은 [LIQUID.md](./LIQUID.md)에 따로 정리되어 있습니다.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -165,19 +165,19 @@ createTemplate(BmfHelloTemplate, {
|
|
|
165
165
|
| `type` | `string` | | 관리도구에서 사용하는 템플릿 타입 식별자 |
|
|
166
166
|
| `shadow` | `boolean` | | Shadow DOM 사용 여부 (기본값: `true`) |
|
|
167
167
|
|
|
168
|
-
>
|
|
168
|
+
> 배치 위치는 소스 위치로 정해집니다(페이지는 `src/pages/` 폴더 구조, 위젯은 `slot` 옵션). 상단·하단 바(topBar/bottomBar) 같은 페이지 레이아웃은 관리도구에서 설정하므로, 템플릿 코드에서 선언하지 않습니다. 자세한 내용은 [BUILD_SYSTEM.md](./BUILD_SYSTEM.md)·[SLOT_CATALOG_V2.md](./SLOT_CATALOG_V2.md)를 참고하세요.
|
|
169
169
|
|
|
170
170
|
> - 컴포넌트는 반드시 `export default`로 내보내야 합니다
|
|
171
171
|
> - `createTemplate()`은 반드시 모듈 최상위 레벨에서 호출해야 합니다. 함수 내부에서 호출하면 빌드 시 메타데이터를 추출할 수 없습니다.
|
|
172
|
-
> - `name
|
|
172
|
+
> - `name`에는 Custom Element 규격에 따라 하이픈이 반드시 들어가야 하며, 없으면 런타임 에러가 발생합니다. 관례적으로 `{space}-{suffix}` 형태를 권장합니다.
|
|
173
173
|
|
|
174
174
|
호출 시 `<name>` 그대로가 Custom Element 태그로 등록됩니다. 예를 들어 위 코드는 `<bmf-hello>` 요소를 등록합니다.
|
|
175
175
|
|
|
176
176
|
### 새 템플릿 추가
|
|
177
177
|
|
|
178
|
-
폴더를 만들고 `template.tsx`를 작성하면 자동으로 인식됩니다. **폴더 이름의
|
|
178
|
+
폴더를 만들고 `template.tsx`를 작성하면 자동으로 인식됩니다. **폴더 이름의 역할이 페이지와 위젯에서 다릅니다.**
|
|
179
179
|
|
|
180
|
-
페이지는 폴더 구조가
|
|
180
|
+
페이지는 폴더 구조가 그대로 배포 경로가 됩니다. 예를 들어 `/profile` 경로에 배포할 페이지는 다음과 같이 만듭니다.
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
183
|
mkdir -p src/pages/profile
|
|
@@ -196,7 +196,7 @@ createTemplate(BmfProfileTemplate, {
|
|
|
196
196
|
})
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
위젯 폴더에는 위젯 이름을 붙이면 됩니다. 위젯이 들어갈 자리는 폴더 이름이 아니라 `slot` 옵션으로만 정해집니다.
|
|
200
200
|
|
|
201
201
|
```bash
|
|
202
202
|
mkdir -p src/slots/curation-banner
|
|
@@ -216,7 +216,7 @@ createTemplate(CurationBanner, {
|
|
|
216
216
|
})
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
`slot`에 넣을 수 있는 값은 편집기 자동완성으로 볼 수 있고 전체 목록은 [SLOT_CATALOG_V2.md](./SLOT_CATALOG_V2.md)에 있습니다. 오타가 있으면 `bstage build`가
|
|
219
|
+
`slot`에 넣을 수 있는 값은 편집기 자동완성으로 볼 수 있고 전체 목록은 [SLOT_CATALOG_V2.md](./SLOT_CATALOG_V2.md)에 있습니다. 값에 오타가 있으면 `bstage build`가 사용할 수 있는 후보를 보여 주고 빌드를 중단합니다.
|
|
220
220
|
|
|
221
221
|
> 동적 경로(`src/pages/artist/[id]/`)는 아직 지원하지 않습니다.
|
|
222
222
|
|
|
@@ -556,7 +556,7 @@ npx @bstage-sdk/cli@latest init --target admin
|
|
|
556
556
|
|
|
557
557
|
### 한 레포는 유저 아니면 어드민 하나입니다
|
|
558
558
|
|
|
559
|
-
`target
|
|
559
|
+
유저 템플릿과 어드민 템플릿은 **레포를 나눠서** 개발합니다. `target`을 **프로젝트 단위**로 정하는 것도 이 때문입니다. 페이지의 배포 경로는 `src/pages/` 폴더 구조로 정해지므로, 유저 `/settings`와 어드민 `/settings`를 한 레포에 두면 두 페이지가 같은 파일(`src/pages/settings/template.tsx`)을 써야 해서 충돌합니다.
|
|
560
560
|
|
|
561
561
|
### `target`이 정하는 것
|
|
562
562
|
|
|
@@ -575,18 +575,18 @@ npx @bstage-sdk/cli@latest init --target admin
|
|
|
575
575
|
|
|
576
576
|
### API 호출
|
|
577
577
|
|
|
578
|
-
어드민 템플릿도 **같은 `BstageClient`로 호출합니다.** `baseUrl`을 주지 않으면 `location.origin + '/gw'`가 되므로 어드민 도메인에 임베드된 화면은 자동으로 **어드민 게이트웨이**를
|
|
578
|
+
어드민 템플릿도 **같은 `BstageClient`로 호출합니다.** `baseUrl`을 주지 않으면 `location.origin + '/gw'`가 되므로 어드민 도메인에 임베드된 화면은 자동으로 **어드민 게이트웨이**를 호출합니다. 코드는 [6.1 BstageClient 설정](#61-bstageclient-설정)과 같고, 다른 점은 게이트웨이가 **두 가지를 함께 확인**한다는 것뿐입니다.
|
|
579
579
|
|
|
580
|
-
| 확인하는 것 |
|
|
580
|
+
| 확인하는 것 | 전달 방식 | 템플릿이 할 일 |
|
|
581
581
|
| --- | --- | --- |
|
|
582
|
-
|
|
|
583
|
-
| 허가된
|
|
584
|
-
|
|
585
|
-
- **키 발급**: 포털 `조직 › API 키`에서 키 타입 **MANAGEMENT**, 스테이지는 이 화면이 들어갈 **하나**, 허용 Origin은 **어드민 콘솔 도메인**(예: sandbox는 `https://{스테이지ID}.mysandstage.in`)으로 발급합니다. 허용 Origin에 자동으로 채워지는 값은 유저
|
|
586
|
-
- **Surface
|
|
587
|
-
- **MANAGEMENT 키도 빌드 결과물에 평문으로
|
|
588
|
-
- **호출할 수 있는 API**는 포털 개발자 문서의 **API 레퍼런스 › 운영자 탭**에 있는 것뿐입니다.
|
|
589
|
-
- **포털은 빌드 환경변수 저장 시점에 거부합니다**(400): 키 이름이 `^VITE_[A-Z0-9_]+$`가 아니거나 32개 초과·이름 128자 초과·값 2048자 초과, 앞뒤 공백을 뺀 값이 `bss_`로 시작(모든 레포)하거나 `bsm_`로 시작(
|
|
582
|
+
| 운영자 로그인 여부 | 어드민 로그인 쿠키(같은 도메인이므로 브라우저가 자동으로 보냅니다) | 없음 |
|
|
583
|
+
| 허가된 앱 여부 | `X-BSTAGE-APP-ID`·`X-BSTAGE-APP-KEY` 헤더(**관리자 콘솔용(MANAGEMENT) 키**(`bsm_`)만 통과합니다) | `.env`와 포털 빌드 환경변수에 MANAGEMENT 키를 넣습니다 |
|
|
584
|
+
|
|
585
|
+
- **키 발급**: 포털 `조직 › API 키`에서 키 타입 **MANAGEMENT**, 스테이지는 이 화면이 들어갈 **하나**, 허용 Origin은 **어드민 콘솔 도메인**(예: sandbox는 `https://{스테이지ID}.mysandstage.in`)으로 발급합니다. 허용 Origin에 자동으로 채워지는 값은 유저 도메인이므로, 그대로 두면 어드민에서 보낸 호출이 거부됩니다. 변수 이름은 유저 템플릿과 같고 값만 다릅니다(`VITE_BSTAGE_APP_KEY=bsm_…`).
|
|
586
|
+
- **Surface마다 사용할 키 타입이 정해져 있습니다.** USER에는 PUBLIC(`bsp_`), ADMIN에는 MANAGEMENT(`bsm_`) 키를 사용합니다. PUBLIC 키로 어드민 게이트웨이를 호출하면 `401 KEY_TYPE_NOT_ALLOWED`, 어드민에 로그인하지 않았거나 어드민 도메인 밖에서 호출하면 `401 MISSING_ADMIN_ACCESS_TOKEN`, 허용 Origin이 유저 도메인으로 남아 있으면 `403`입니다. 반대로 MANAGEMENT 키를 유저 템플릿에 넣으면 유저 게이트웨이가 거부하고 키만 노출됩니다. 그래서 포털은 유저(USER) 레포의 빌드 환경변수에 `bsm_` 값을 저장하지 않습니다(400). 게시한 뒤에는 `bstage doctor`가 키 타입 불일치(`appkey.type_surface`)를 알려 줍니다.
|
|
587
|
+
- **MANAGEMENT 키도 빌드 결과물에 평문으로 포함됩니다.** 어드민 화면을 열 수 있는 사람은 개발자 도구로 키를 볼 수 있으므로, 허용 Origin과 스테이지 범위를 좁히는 발급 조건이 키를 보호하는 유일한 수단입니다. 유출이 의심되면 포털에서 키를 회전(새 키 발급 + 옛 키 즉시 만료)한 뒤 빌드 환경변수를 바꾸고 다시 빌드합니다. 회전하면 APP KEY뿐 아니라 **APP ID도 새 값**이므로 둘 다 바꿉니다. 서버용(SIGNING) 키는 어떤 템플릿에도 넣지 않습니다.
|
|
588
|
+
- **호출할 수 있는 API**는 포털 개발자 문서의 **API 레퍼런스 › 운영자 탭**에 있는 것뿐입니다. 목록에 없는 경로를 임의로 만들어 호출하지 말고, 필요한 API가 목록에 없으면 b.stage에 요청하세요.
|
|
589
|
+
- **포털은 빌드 환경변수 저장 시점에 거부합니다**(400): 키 이름이 `^VITE_[A-Z0-9_]+$`가 아니거나 32개 초과·이름 128자 초과·값 2048자 초과, 앞뒤 공백을 뺀 값이 `bss_`로 시작(모든 레포)하거나 `bsm_`로 시작(유저 레포), APP ID·APP KEY가 그 스테이지 키와 맞지 않음(공백 포함·없는 키·만료/폐기·스테이지 범위 밖·짝이 아닌 KEY. 파트너 조회가 실패하면 이 검사는 건너뜁니다).
|
|
590
590
|
- **로컬·미리보기에서는 응답이 오지 않습니다.** `bstage dev`는 어드민 게이트웨이를 프록시하지 않고(어드민 모드에서 어드민 호스트로 가는 것은 로그인뿐), 포털 미리보기는 템플릿의 직접 게이트웨이 호출을 막습니다. 어드민 API 응답은 게시한 뒤 실제 어드민 화면에서 확인하세요.
|
|
591
591
|
|
|
592
592
|
---
|
package/docs/I18N.md
CHANGED
|
@@ -154,7 +154,7 @@ configureBstageI18n({ tier: 'my-tier' }) // origin·phase·system·tier·fetch
|
|
|
154
154
|
|
|
155
155
|
> **CDN이 죽어도 문구가 뜬다** — 성공한 응답을 Cache Storage에 남겨두고, 실패 시 그것으로 렌더한다. **번들 크기는 늘지 않는다.** 그 오리진에서 한 번도 성공한 적 없는 첫 방문이 장애와 겹치면 키가 노출된다.
|
|
156
156
|
|
|
157
|
-
> **인증 fetch를 쓰지 않는다** — 번역 CDN은 공개 호스트다. 플랫폼이 주입하는 `__bstage_fetch__`는
|
|
157
|
+
> **인증 fetch를 쓰지 않는다** — 번역 CDN은 공개 호스트다. 플랫폼이 주입하는 `__bstage_fetch__`는 허용된 목적지(같은 origin의 `/gw`, 플랫폼 허용 목록)에만 `Authorization`(+ dev/qa는 CF Access) 헤더를 붙이는데, 번역 CDN은 그 밖이고 인증 헤더가 붙으면 크로스 오리진 요청이 CORS preflight 대상이 되어 이 CDN은 `OPTIONS`에 403을 준다. 그래서 사전은 기본 `fetch`로 받는다(`BstageClient`는 게이트웨이를 부르므로 그쪽은 그대로 인증 fetch를 쓴다).
|
|
158
158
|
|
|
159
159
|
> **알려진 오차** — `tier`는 런타임에 알 수 있는 신호가 없다(플랫폼은 서버에서 HTTP 헤더로 받는다). 그래서 기본 `inhouse`이며, `inhouse`가 아닌 tier를 쓰는 스페이스에 올라간 위젯은 위처럼 지정하지 않으면 inhouse 사전을 본다. 그 스페이스의 tier 값은 운영자에게 확인한다.
|
|
160
160
|
|
package/docs/LIQUID.md
CHANGED
|
@@ -4,18 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## 1. liquid 방식이란
|
|
6
6
|
|
|
7
|
-
플랫폼(서버)이 데이터를
|
|
7
|
+
liquid 페이지는 플랫폼(서버)이 데이터를 채워 렌더링하는 [liquidjs](https://liquidjs.com/) 템플릿으로, **목록을 보여 주기만 하는 정적 페이지**에 알맞습니다. 브라우저에서 번들을 실행하지 않으므로 **빌드 산출물이 없습니다.** 포털은 push된 커밋의 템플릿 파일을 그대로 패키징합니다.
|
|
8
8
|
|
|
9
9
|
| 항목 | sdk | liquid |
|
|
10
10
|
| ----------- | ------------------------------------------- | ----------------------------------------- |
|
|
11
11
|
| 작성 대상 | React 컴포넌트 (`template.tsx`) | `.liquid` 템플릿 (`template.liquid`) |
|
|
12
|
-
|
|
|
12
|
+
| 렌더링 위치 | 브라우저 (Web Component) | 서버 (플랫폼이 데이터를 넣어 렌더) |
|
|
13
13
|
| 배치 가능 | 페이지(PAGE) · 위젯(SLOT) | 페이지(PAGE)만 |
|
|
14
14
|
| 로컬 산출물 | `dist/{경로}/template.js` | 없음 (검증만) |
|
|
15
15
|
| 의존성 | core · react · design + Vite · 타입스크립트 | 저작 도구 `@bstage-sdk/cli` 하나 |
|
|
16
16
|
|
|
17
|
-
- **페이지 단위로만** 배치합니다. 슬롯(위젯) 자리에는 쓸 수
|
|
18
|
-
- **한 레포에 sdk와 섞지 않습니다.** `src/**/template.tsx`와 liquid 템플릿이 함께 있으면 포털 빌더가 패키징하지 못합니다.
|
|
17
|
+
- **페이지 단위로만** 배치합니다. 슬롯(위젯) 자리에는 쓸 수 없습니다. 위젯 배치가 있으면 `bstage deploy`가 빌드를 만들기 전에 사전 조건 오류(종료 코드 2)로 중단합니다.
|
|
19
18
|
|
|
20
19
|
## 2. 시작
|
|
21
20
|
|
|
@@ -33,9 +32,9 @@ public/
|
|
|
33
32
|
└── data.json # 로컬 미리보기용 샘플 값 (배포에 나가지 않음)
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
- **이름 규칙**: 소문자로 시작하고 소문자·숫자·하이픈만 씁니다(`^[a-z][a-z0-9-]*$`). CDN 경로 세그먼트가 되므로 대문자를 쓰면 대소문자를 구분하지 않는 파일시스템에서 로컬과
|
|
37
|
-
- **한 세그먼트**여야 합니다. `public/user/events/summer/`처럼 한 단계 더 깊으면 포털이
|
|
38
|
-
- `data.json`은 로컬 전용입니다. 실제 값은 플랫폼이 서버에서 넣습니다. **키 이름은
|
|
35
|
+
- **이름 규칙**: 소문자로 시작하고 소문자·숫자·하이픈만 씁니다(`^[a-z][a-z0-9-]*$`). CDN 경로 세그먼트가 되므로 대문자를 쓰면 대소문자를 구분하지 않는 파일시스템에서 로컬과 배포 결과가 달라집니다.
|
|
36
|
+
- **한 세그먼트**여야 합니다. `public/user/events/summer/`처럼 한 단계 더 깊으면 포털이 별도 경고 없이 무시합니다.
|
|
37
|
+
- `data.json`은 로컬 전용입니다. 실제 값은 플랫폼이 서버에서 넣습니다. **키 이름은 플랫폼이 정한 이름을 따라야 합니다.** 유저 화면 최상위는 `lounges`·`stories`·`contentSections`·`latestContents`·`shopCategories` 다섯 개입니다. 다른 이름을 쓰면 미리보기에서만 보이고, 배포하면 빈 값이 됩니다. 각 항목의 필드는 계약이 아니라 모양을 보기 위한 예시이므로 실제 렌더 결과로 확인하세요. 어드민 화면이 받는 값은 확인되지 않았습니다.
|
|
39
38
|
- 스캐폴드는 Vite·TypeScript·ESLint·`.env`를 만들지 않습니다. 번들링도 타입 검사도 하지 않기 때문입니다.
|
|
40
39
|
|
|
41
40
|
## 3. 로컬 미리보기 — `bstage dev`
|
|
@@ -45,7 +44,7 @@ npm run dev # = bstage dev
|
|
|
45
44
|
bstage dev -p 3000 # 포트 지정 (기본 5173)
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
레포 종류는 파일 구조로 자동
|
|
47
|
+
레포 종류는 파일 구조로 자동 판정하므로, liquid 전용 레포는 옵션 없이 실행합니다(혼합 레포는 `bstage dev --kind liquid`). liquid 레포면 Vite 대신 Node `http` + liquidjs 미리보기 서버가 실행되고, 항상 루프백(`127.0.0.1`)에만 바인딩합니다.
|
|
49
48
|
|
|
50
49
|
| 경로 | 화면 |
|
|
51
50
|
| ------------------- | -------------------------------------------- |
|
|
@@ -55,11 +54,11 @@ bstage dev -p 3000 # 포트 지정 (기본 5173)
|
|
|
55
54
|
| `/{surface}/{이름}` | 같은 폴더의 `data.json`으로 렌더한 결과 |
|
|
56
55
|
| 그 밖의 경로 | `public/` 아래 정적 파일 (없으면 404) |
|
|
57
56
|
|
|
58
|
-
- 파일을 고치면 **자동으로 새로고침**됩니다(
|
|
59
|
-
- 렌더가 실패하면 파일·줄 번호·메시지를 보여 주는 **오류 페이지**가 200
|
|
57
|
+
- 파일을 고치면 **자동으로 새로고침**됩니다(미리보기 서버가 넣는 SSE 스크립트로, 배포물과는 무관합니다).
|
|
58
|
+
- 렌더가 실패하면 파일·줄 번호·메시지를 보여 주는 **오류 페이지**가 HTTP 200 상태로 표시됩니다. 파일을 고치면 자동으로 새로고침되어 바로 정상 화면으로 돌아갑니다.
|
|
60
59
|
- 규약을 벗어난 `template.liquid`도 목록에 보여 주되 "포털이 무시합니다"라고 표시합니다.
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
**미리보기에 없는 기능**: 인증 프록시 · API 프록시 · phase · 실데이터. `--phase`를 지정해도 무시되며, 그렇다는 안내만 출력됩니다. 상세는 [DEV_SERVER.md §7](./DEV_SERVER.md#7-liquid-프리뷰-liquid-레포)을 참고하세요.
|
|
63
62
|
|
|
64
63
|
## 4. 검증 — `bstage build` · `bstage doctor`
|
|
65
64
|
|
|
@@ -72,27 +71,28 @@ bstage build --json # { "kind": "liquid", "issues": [...] } 한 덩어리
|
|
|
72
71
|
|
|
73
72
|
| 등급 | 검사 |
|
|
74
73
|
| ----- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
75
|
-
| error | `template.liquid` 문법 오류(줄 번호 표시) · `data.json` JSON 파싱 실패 · 폴더 이름 규칙 위반 · sdk
|
|
74
|
+
| error | `template.liquid` 문법 오류(줄 번호 표시) · `data.json` JSON 파싱 실패 · 폴더 이름 규칙 위반 · (혼합 레포) sdk 페이지 이름 `user`·`admin` · liquid 템플릿 0개 |
|
|
76
75
|
| warn | 규약 밖 위치의 `template.liquid`(포털이 무시) · `layout.json` 존재(포털이 지움) · 폴더에 `template.liquid`가 아닌 파일만 있음 |
|
|
77
76
|
|
|
78
|
-
같은 결과를 `bstage doctor`가 "liquid 검증" 절에서 보여 줍니다(`--json`에서는 `liquid.issues`). doctor의 liquid 표는 sdk와
|
|
77
|
+
같은 결과를 `bstage doctor`가 "liquid 검증" 절에서 보여 줍니다(`--json`에서는 `liquid.issues`). doctor의 liquid 표는 sdk와 다릅니다. 기대 파일은 템플릿 루트(`public/user` 또는 `public/admin`)와 `AGENTS.md`, 기대 스크립트는 `dev`·`build`이고, 권장 의존성 절은 나오지 않습니다. 버전 표도 `@bstage-sdk/cli` 한 줄뿐입니다.
|
|
79
78
|
|
|
80
79
|
## 5. 배포
|
|
81
80
|
|
|
82
|
-
포털이 push된 커밋의 `public/{user|admin}/{이름}/`을 **그대로 패키징합니다**(`data.json`·`layout.json`은
|
|
81
|
+
포털이 push된 커밋의 `public/{user|admin}/{이름}/`을 **그대로 패키징합니다**(`data.json`·`layout.json`은 제외합니다. 배치는 관리도구에서 설정합니다). 로컬 산출물은 업로드되지 않으므로, 먼저 커밋하고 push해야 합니다.
|
|
83
82
|
|
|
84
83
|
1. **레포 연결만 포털 화면에서** 합니다. 이어서 `bstage login`·`bstage link` 뒤 `bstage placement create --template <이름>`으로 페이지 배치를 만듭니다(`<이름>`은 `public/{user|admin}/<이름>/`, 성공한 빌드가 없으면 빌드부터 합니다).
|
|
85
|
-
2. 이후에는 터미널에서 `bstage deploy`(빌드 → 라이브 적용) · `bstage rollback`(이전 성공 빌드로 되돌리기) · `bstage publish on|off`(게시 토글)를 씁니다. 로그인·링크·종료 코드 등 공통 절차는 sdk와
|
|
84
|
+
2. 이후에는 터미널에서 `bstage deploy`(빌드 → 라이브 적용) · `bstage rollback`(이전 성공 빌드로 되돌리기) · `bstage publish on|off`(게시 토글)를 씁니다. 로그인·링크·종료 코드 등 공통 절차는 sdk와 같습니다. 자세한 내용은 [GETTING_STARTED.md §9](./GETTING_STARTED.md#9-배포)를 참고하세요.
|
|
85
|
+
3. `bstage list`는 최근 빌드의 산출물 종류(`sdk`·`liquid`) 열을 함께 보여 줍니다. 옛 빌드에는 이 값이 없어 `-`로 나옵니다.
|
|
86
86
|
|
|
87
|
-
> liquid 레포에는 `.env`가 없어 `bstage login`·`bstage link`가 phase를 읽을 곳이 없습니다. sandbox가 아닌 포털이면 두 명령 모두에 `--phase <phase>`를 주세요(생략하면 sandbox).
|
|
87
|
+
> liquid 레포에는 `.env`가 없어 `bstage login`·`bstage link`가 phase를 읽을 곳이 없습니다. sandbox가 아닌 포털이면 두 명령 모두에 `--phase <phase>`를 주세요(생략하면 sandbox).
|
|
88
88
|
|
|
89
89
|
## 6. 이미지·영상 넣기
|
|
90
90
|
|
|
91
91
|
**포털 화면의 스테이지 > 미디어에 올리고, 받은 URL을 템플릿에 절대 주소로 넣습니다.** 이미지와 영상만 올릴 수 있고, 허용 형식·용량 한도는 업로드할 때 화면이 알려 줍니다.
|
|
92
92
|
|
|
93
|
-
레포에 파일을 두고 상대 경로로 참조하면 **배포 후 깨집니다.** 포털은 템플릿 폴더를 그대로 복사하지만, 렌더된 HTML이 스페이스 도메인 페이지 안으로 들어가므로 상대 경로는 그 도메인 기준으로
|
|
93
|
+
레포에 파일을 두고 상대 경로로 참조하면 **배포 후 깨집니다.** 포털은 템플릿 폴더를 그대로 복사하지만, 렌더된 HTML이 스페이스 도메인 페이지 안으로 들어가므로 상대 경로는 그 도메인 기준으로 해석되어 산출물이 놓인 주소를 가리키지 않습니다.
|
|
94
94
|
|
|
95
|
-
로컬
|
|
95
|
+
로컬 미리보기는 이 점에서 배포와 **반대로** 동작합니다. 미리보기는 템플릿 폴더의 파일을 함께 서빙하므로 상대 경로 이미지가 잘 보입니다. 미리보기에서 보인다고 해서 배포 후에도 보인다고 판단하지 마세요.
|
|
96
96
|
|
|
97
97
|
## 7. liquidjs 문법 요점
|
|
98
98
|
|
|
@@ -107,16 +107,16 @@ bstage build --json # { "kind": "liquid", "issues": [...] } 한 덩어리
|
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
- 없는 값은 오류가 아니라 **빈 문자열**로 렌더됩니다(플랫폼과 같은 설정). 화면이 비어 보이면 값 이름을 먼저 의심하세요.
|
|
110
|
-
- **`{% render %}`·`{% include %}`(파셜)는 배포에서 동작하지 않습니다.** 배포 뷰어의 엔진은 레포의 파일을 찾지 못해 그 자리를 빈 문자열로 렌더합니다. 로컬
|
|
110
|
+
- **`{% render %}`·`{% include %}`(파셜)는 배포에서 동작하지 않습니다.** 배포 뷰어의 엔진은 레포의 파일을 찾지 못해 그 자리를 빈 문자열로 렌더합니다. 로컬 미리보기(탐색 루트 `public/`)에서만 동작하므로, 미리보기에서 보인다고 해서 배포 후에도 보인다고 판단하지 마세요. 템플릿은 `template.liquid` **한 파일**에 담고, `bstage build`는 파셜 태그가 있으면 경고를 표시합니다.
|
|
111
111
|
- 전체 문법은 [liquidjs 공식 문서](https://liquidjs.com/)를 참고하세요.
|
|
112
112
|
|
|
113
|
-
## 8.
|
|
113
|
+
## 8. 주의할 점
|
|
114
114
|
|
|
115
|
-
- **sdk와 함께 둘 수 있습니다(혼합 레포).** `bstage build`는 sdk 템플릿만 번들하고 liquid는 검증만 하며(`dist/`에 liquid를 넣지 않습니다), 포털은 sdk를 빌드한 뒤 liquid를 함께 패키징합니다. 단 `src/pages/user`·`src/pages/admin` 페이지는 산출물이 liquid 자리(`dist/{user|admin}/`)와 겹쳐 `bstage build`가 종료 코드 2로
|
|
116
|
-
-
|
|
117
|
-
- **페이지(PAGE) 배치만** 됩니다. 위젯 자리에
|
|
115
|
+
- **sdk와 함께 둘 수 있습니다(혼합 레포).** `bstage build`는 sdk 템플릿만 번들하고 liquid는 검증만 하며(`dist/`에 liquid를 넣지 않습니다), 포털은 sdk를 빌드한 뒤 liquid를 함께 패키징합니다. 단 `src/pages/user`·`src/pages/admin` 페이지는 산출물이 liquid 자리(`dist/{user|admin}/`)와 겹쳐 `bstage build`가 종료 코드 2로 멈춥니다. 폴더 이름을 바꾸세요. `bstage dev`는 혼합 레포에서 기본으로 sdk 미리보기를 실행하고, `--kind liquid`를 주면 liquid 미리보기를 실행합니다.
|
|
116
|
+
- **폴더 깊이가 정해져 있습니다.** `public/{user|admin}/{이름}/template.liquid`, 정확히 이 자리만 인정됩니다. 어긋나면 빌드는 성공하는데 화면에 나오지 않습니다.
|
|
117
|
+
- **페이지(PAGE) 배치만** 됩니다. 위젯 자리에 배치하면 포털이 400 오류를 반환합니다.
|
|
118
118
|
- **`data.json`·`layout.json`은 로컬 전용입니다.** 포털이 패키징에서 지우므로 배포에 필요한 값을 여기에 담지 마세요.
|
|
119
119
|
- **파일명은 `template.liquid` 고정입니다.** `index.html`처럼 다른 이름이면 포털이 그 폴더를 템플릿으로 보지 않습니다.
|
|
120
|
-
- **파셜(`{% render %}`·`{% include %}`)을 쓰지 않습니다.** 배포에서 빈 문자열이
|
|
121
|
-
- **에셋을 레포에 두고 상대 경로로 참조하지 않습니다.** 배포 후
|
|
122
|
-
- **플랫폼이
|
|
120
|
+
- **파셜(`{% render %}`·`{% include %}`)을 쓰지 않습니다.** 배포에서 빈 문자열이 됩니다. 7절을 참고하세요.
|
|
121
|
+
- **에셋을 레포에 두고 상대 경로로 참조하지 않습니다.** 배포 후 깨집니다. 6절을 참고하세요. 로컬 미리보기에서는 보이므로, 미리보기 결과만으로 판단하지 마세요.
|
|
122
|
+
- **플랫폼이 제공하는 값의 이름을 임의로 만들지 마세요.** 유저 화면 최상위는 `lounges`·`stories`·`contentSections`·`latestContents`·`shopCategories` 다섯 개뿐이고, 그 밖의 이름은 빈 문자열이 됩니다(어드민 화면이 받는 값은 확인되지 않았습니다). `data.json` 샘플도 같은 이름을 써야 미리보기가 배포와 같은 모양이 됩니다.
|
package/docs/SDK_ARCHITECTURE.md
CHANGED
|
@@ -35,7 +35,7 @@ Shadow DOM으로 서드파티 CSS가 플랫폼 UI에 영향을 주거나 플랫
|
|
|
35
35
|
|
|
36
36
|
### 토큰 비노출
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
API 호출은 BstageClient가 `appId`/`appKey`로 처리하므로 템플릿 코드가 플랫폼 사용자의 인증 토큰을 직접 다룰 필요가 없습니다. 다만 이는 설계 원칙이지 격리 보장이 아닙니다 — 템플릿은 호스트 페이지와 같은 JS 실행 환경에서 돌아가므로, 쿠키를 읽거나 회원 토큰을 제3자에게 전달해서는 안 됩니다([7. 보안 모델](#7-보안-모델) 참조).
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -145,11 +145,11 @@ Custom Element 등록(`customElements.define`)은 BstageElement 자체가 하지
|
|
|
145
145
|
|
|
146
146
|
파트너에게 공개된 API에 접근하는 HTTP 클라이언트입니다.
|
|
147
147
|
|
|
148
|
-
**설계 의도:** 서드파티가 플랫폼 API를 호출할 때, 플랫폼 사용자의 인증 토큰 대신 `appId`/`appKey`/`tenantId`를 사용합니다. 이로써 서드파티 코드가 사용자
|
|
148
|
+
**설계 의도:** 서드파티가 플랫폼 API를 호출할 때, 플랫폼 사용자의 인증 토큰 대신 `appId`/`appKey`/`tenantId`를 사용합니다. 이로써 서드파티 코드가 사용자 토큰을 직접 다루지 않아도 되며 API 접근 범위를 앱 단위로 제어할 수 있습니다. 다만 템플릿은 호스트와 같은 JS 실행 환경에서 돌아가므로 이것이 토큰 접근을 기술적으로 막는 것은 아닙니다([7. 보안 모델](#7-보안-모델) 참조).
|
|
149
149
|
|
|
150
150
|
**base URL 결정:** `resolveBaseUrl()` 함수가 `location.origin/gw`를 반환하여 현재 호스트 기반으로 요청합니다. 로컬 개발 시에는 devVitePlugin이 이 함수를 치환해 localhost 프록시로 라우팅합니다.
|
|
151
151
|
|
|
152
|
-
**커스텀 fetch 주입:** 플랫폼은 `globalThis.__bstage_fetch__`를 통해 인증
|
|
152
|
+
**커스텀 fetch 주입:** 플랫폼은 `globalThis.__bstage_fetch__`를 통해 인증 fetch를 주입할 수 있습니다. 이 fetch는 허용된 목적지(같은 origin의 `/gw`, 플랫폼 허용 목록)에만 Authorization 헤더(real 외 phase에서는 CF Access 헤더도)를 붙이고, 그 밖의 요청은 해당 헤더 없이 그대로 통과시킵니다. 그러면 템플릿이 플랫폼과 동일한 인증 컨텍스트로 게이트웨이 API를 호출합니다.
|
|
153
153
|
|
|
154
154
|
> API 시그니처와 사용 예시는 [API_REFERENCE.md](./API_REFERENCE.md#14-bstageclient)를 참고하세요.
|
|
155
155
|
|
|
@@ -233,16 +233,25 @@ flowchart TD
|
|
|
233
233
|
|
|
234
234
|
## 7. 보안 모델
|
|
235
235
|
|
|
236
|
-
Shadow DOM은 **스타일 격리**이지 **보안 격리**가 아닙니다. JavaScript 실행
|
|
236
|
+
Shadow DOM은 **스타일 격리**이지 **보안 격리**가 아닙니다. 템플릿은 호스트 페이지와 같은 JavaScript 실행 환경(Web Components, open Shadow DOM)에서 돌아갑니다.
|
|
237
237
|
|
|
238
|
-
| 위협 | 대응
|
|
239
|
-
| --------------- |
|
|
240
|
-
| 악성 코드 포함 | 번들 제출 시 정적 분석 (금지 API 패턴 탐지) |
|
|
241
|
-
| 플랫폼 DOM 조작 | CSP 헤더로 제한
|
|
242
|
-
| 토큰/쿠키 탈취 |
|
|
243
|
-
|
|
|
238
|
+
| 위협 | 대응 |
|
|
239
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
240
|
+
| 악성 코드 포함 | 번들 제출 시 정적 분석 (금지 API 패턴 탐지) — **계획**, 아직 미구현 |
|
|
241
|
+
| 플랫폼 DOM 조작 | CSP 헤더로 제한 |
|
|
242
|
+
| 토큰/쿠키 탈취 | 리프레시 토큰은 HttpOnly 쿠키. 액세스 토큰 쿠키는 현재 HttpOnly가 아님 — HttpOnly 전환 **계획** |
|
|
243
|
+
| 인증 헤더 유출 | `__bstage_fetch__`는 허용된 목적지(같은 origin의 `/gw`, 플랫폼 허용 목록)에만 Authorization(real 외 phase는 CF Access 포함) 헤더를 붙이고, 그 밖의 요청은 헤더 없이 통과 |
|
|
244
|
+
| API 남용 | API Gateway에서 rate limiting |
|
|
244
245
|
|
|
245
|
-
서드파티는 BstageClient를 통해 `appId`/`appKey`로 API에
|
|
246
|
+
서드파티는 BstageClient를 통해 `appId`/`appKey`로 API에 접근합니다. 같은 실행 환경을 공유하므로 토큰 접근이 기술적으로 차단되지는 않습니다 — 템플릿은 쿠키를 읽거나 회원 토큰을 제3자에게 전달해서는 안 됩니다.
|
|
247
|
+
|
|
248
|
+
### iframe 격리 (placement 단위 선택)
|
|
249
|
+
|
|
250
|
+
위 한계를 닫기 위해 템플릿을 **sandbox iframe**(`sandbox="allow-scripts"`, opaque origin)에서 돌리는 경로가 있습니다(`@bstage-sdk/bridge`). **기본은 inline**이고, 격리는 placement마다 켜는 선택 기능입니다. 켜지 않은 placement는 지금과 같은 inline 경로로 동작합니다.
|
|
251
|
+
|
|
252
|
+
- 프레임 문서는 CSP로 직접 네트워크를 막습니다(`connect-src 'none'`). API 요청은 브리지 메시지로 호스트에 가고, 호스트가 목적지(같은 origin `/gw`·승인된 파트너 origin)·메서드·헤더·본문 크기·분당 요청 수를 검사한 뒤 플랫폼 fetch로 대신 보냅니다.
|
|
253
|
+
- 토큰(쿠키·Authorization)은 프레임에 들어가지 않습니다. 프레임은 응답의 상태·content-type·본문만 받습니다. 쿠키·`localStorage`·호스트 DOM에도 접근할 수 없습니다.
|
|
254
|
+
- 템플릿 코드는 바꿀 필요 없이 브리지를 지원하는 SDK로 다시 빌드하면 됩니다(옛 번들은 inline으로만 돕니다). core·react가 프레임 안인지 스스로 판별해 `BstageClient`·로케일·`useNavigation`을 브리지로 보냅니다. 단, 쿠키·`localStorage`·`location`을 직접 쓰는 템플릿은 격리 환경에서 동작이 달라집니다.
|
|
246
255
|
|
|
247
256
|
---
|
|
248
257
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bstage-sdk/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "bstage SDK core — HTTP client for b.stage API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"main": "./dist/index.cjs",
|
|
27
27
|
"module": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
|
+
"bstage": {
|
|
30
|
+
"bridgeProtocol": 1
|
|
31
|
+
},
|
|
29
32
|
"files": [
|
|
30
33
|
"dist",
|
|
31
34
|
"docs"
|