@simplysm/solid 13.0.34 → 13.0.36

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 (80) hide show
  1. package/README.md +22 -42
  2. package/dist/components/disclosure/DialogContext.d.ts +29 -0
  3. package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
  4. package/dist/components/disclosure/DialogContext.js.map +1 -1
  5. package/dist/components/disclosure/DialogInstanceContext.d.ts +14 -0
  6. package/dist/components/disclosure/DialogInstanceContext.d.ts.map +1 -1
  7. package/dist/components/disclosure/DialogInstanceContext.js.map +1 -1
  8. package/dist/components/feedback/busy/BusyContext.d.ts +18 -0
  9. package/dist/components/feedback/busy/BusyContext.d.ts.map +1 -1
  10. package/dist/components/feedback/busy/BusyContext.js.map +1 -1
  11. package/dist/components/feedback/busy/BusyProvider.d.ts +10 -0
  12. package/dist/components/feedback/busy/BusyProvider.d.ts.map +1 -1
  13. package/dist/components/feedback/busy/BusyProvider.js.map +1 -1
  14. package/dist/components/feedback/notification/NotificationContext.d.ts +29 -0
  15. package/dist/components/feedback/notification/NotificationContext.d.ts.map +1 -1
  16. package/dist/components/feedback/notification/NotificationContext.js.map +1 -1
  17. package/dist/components/feedback/notification/NotificationProvider.d.ts +9 -0
  18. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  19. package/dist/components/feedback/notification/NotificationProvider.js.map +1 -1
  20. package/dist/hooks/useLogger.d.ts +4 -2
  21. package/dist/hooks/useLogger.d.ts.map +1 -1
  22. package/dist/hooks/useLogger.js +11 -4
  23. package/dist/hooks/useLogger.js.map +1 -1
  24. package/dist/hooks/useSyncConfig.d.ts +2 -0
  25. package/dist/hooks/useSyncConfig.d.ts.map +1 -1
  26. package/dist/hooks/useSyncConfig.js +30 -26
  27. package/dist/hooks/useSyncConfig.js.map +1 -1
  28. package/dist/index.d.ts +8 -14
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +21 -15
  31. package/dist/index.js.map +1 -1
  32. package/dist/providers/InitializeProvider.d.ts +33 -0
  33. package/dist/providers/InitializeProvider.d.ts.map +1 -0
  34. package/dist/providers/InitializeProvider.js +75 -0
  35. package/dist/providers/InitializeProvider.js.map +6 -0
  36. package/dist/providers/LoggerContext.d.ts +31 -9
  37. package/dist/providers/LoggerContext.d.ts.map +1 -1
  38. package/dist/providers/LoggerContext.js +17 -13
  39. package/dist/providers/LoggerContext.js.map +2 -2
  40. package/dist/providers/ServiceClientContext.d.ts +13 -0
  41. package/dist/providers/ServiceClientContext.d.ts.map +1 -1
  42. package/dist/providers/ServiceClientContext.js.map +1 -1
  43. package/dist/providers/ServiceClientProvider.d.ts +21 -0
  44. package/dist/providers/ServiceClientProvider.d.ts.map +1 -1
  45. package/dist/providers/ServiceClientProvider.js.map +1 -1
  46. package/dist/providers/SyncStorageContext.d.ts +29 -11
  47. package/dist/providers/SyncStorageContext.d.ts.map +1 -1
  48. package/dist/providers/SyncStorageContext.js +18 -13
  49. package/dist/providers/SyncStorageContext.js.map +2 -2
  50. package/dist/providers/shared-data/SharedDataChangeEvent.d.ts +8 -0
  51. package/dist/providers/shared-data/SharedDataChangeEvent.d.ts.map +1 -1
  52. package/dist/providers/shared-data/SharedDataChangeEvent.js.map +1 -1
  53. package/dist/providers/shared-data/SharedDataContext.d.ts +41 -0
  54. package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
  55. package/dist/providers/shared-data/SharedDataContext.js +1 -3
  56. package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
  57. package/dist/providers/shared-data/SharedDataProvider.d.ts +30 -5
  58. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  59. package/dist/providers/shared-data/SharedDataProvider.js +60 -38
  60. package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
  61. package/docs/hooks.md +15 -0
  62. package/docs/providers.md +70 -195
  63. package/package.json +3 -3
  64. package/src/components/disclosure/DialogContext.ts +29 -0
  65. package/src/components/disclosure/DialogInstanceContext.ts +14 -0
  66. package/src/components/feedback/busy/BusyContext.ts +18 -0
  67. package/src/components/feedback/busy/BusyProvider.tsx +10 -0
  68. package/src/components/feedback/notification/NotificationContext.ts +29 -0
  69. package/src/components/feedback/notification/NotificationProvider.tsx +9 -0
  70. package/src/hooks/useLogger.ts +14 -4
  71. package/src/hooks/useSyncConfig.ts +42 -35
  72. package/src/index.ts +34 -14
  73. package/src/providers/InitializeProvider.tsx +74 -0
  74. package/src/providers/LoggerContext.tsx +51 -11
  75. package/src/providers/ServiceClientContext.ts +13 -0
  76. package/src/providers/ServiceClientProvider.tsx +21 -0
  77. package/src/providers/SyncStorageContext.tsx +53 -15
  78. package/src/providers/shared-data/SharedDataChangeEvent.ts +8 -0
  79. package/src/providers/shared-data/SharedDataContext.ts +44 -3
  80. package/src/providers/shared-data/SharedDataProvider.tsx +108 -54
@@ -1,15 +1,28 @@
1
1
  import { createContext, useContext } from "solid-js";
2
2
  import type { ServiceClient, ServiceConnectionConfig } from "@simplysm/service-client";
3
3
 
4
+ /**
5
+ * WebSocket 서비스 클라이언트 Context 값
6
+ */
4
7
  export interface ServiceClientContextValue {
8
+ /** WebSocket 연결 열기 (key로 다중 연결 관리) */
5
9
  connect: (key: string, options?: Partial<ServiceConnectionConfig>) => Promise<void>;
10
+ /** 연결 닫기 */
6
11
  close: (key: string) => Promise<void>;
12
+ /** 연결된 클라이언트 인스턴스 가져오기 (연결되지 않은 key면 에러 발생) */
7
13
  get: (key: string) => ServiceClient;
14
+ /** 연결 상태 확인 */
8
15
  isConnected: (key: string) => boolean;
9
16
  }
10
17
 
18
+ /** WebSocket 서비스 클라이언트 Context */
11
19
  export const ServiceClientContext = createContext<ServiceClientContextValue>();
12
20
 
21
+ /**
22
+ * WebSocket 서비스 클라이언트에 접근하는 훅
23
+ *
24
+ * @throws ServiceClientProvider가 없으면 에러 발생
25
+ */
13
26
  export function useServiceClient(): ServiceClientContextValue {
14
27
  const context = useContext(ServiceClientContext);
15
28
  if (!context) {
@@ -8,6 +8,27 @@ import { ServiceClientContext, type ServiceClientContextValue } from "./ServiceC
8
8
  import { useConfig } from "./ConfigContext";
9
9
  import { useNotification } from "../components/feedback/notification/NotificationContext";
10
10
 
11
+ /**
12
+ * WebSocket 서비스 클라이언트 Provider
13
+ *
14
+ * @remarks
15
+ * - ConfigProvider와 NotificationProvider 내부에서 사용해야 함
16
+ * - key 기반 다중 연결 관리
17
+ * - 요청/응답 진행률을 NotificationProvider 알림으로 표시
18
+ * - host, port, ssl 미지정 시 window.location에서 자동 추론
19
+ * - cleanup 시 모든 연결 자동 종료
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <ConfigProvider clientName="my-app">
24
+ * <NotificationProvider>
25
+ * <ServiceClientProvider>
26
+ * <App />
27
+ * </ServiceClientProvider>
28
+ * </NotificationProvider>
29
+ * </ConfigProvider>
30
+ * ```
31
+ */
11
32
  export const ServiceClientProvider: ParentComponent = (props) => {
12
33
  const config = useConfig();
13
34
  const notification = useNotification();
@@ -1,4 +1,10 @@
1
- import { createContext, useContext, type ParentComponent } from "solid-js";
1
+ import {
2
+ type Accessor,
3
+ createContext,
4
+ createSignal,
5
+ useContext,
6
+ type ParentComponent,
7
+ } from "solid-js";
2
8
 
3
9
  /**
4
10
  * 커스텀 동기화 저장소 어댑터 인터페이스
@@ -13,40 +19,72 @@ export interface StorageAdapter {
13
19
  removeItem(key: string): void | Promise<void>;
14
20
  }
15
21
 
22
+ /**
23
+ * 기본 localStorage 기반 어댑터
24
+ */
25
+ const defaultStorageAdapter: StorageAdapter = {
26
+ getItem: (key) => localStorage.getItem(key),
27
+ setItem: (key, value) => localStorage.setItem(key, value),
28
+ removeItem: (key) => localStorage.removeItem(key),
29
+ };
30
+
31
+ /**
32
+ * 동기화 저장소 Context 값
33
+ *
34
+ * @remarks
35
+ * - `adapter`: 현재 설정된 StorageAdapter (signal). 기본값은 localStorage 기반 어댑터
36
+ * - `configure`: decorator 함수를 받아 기존 adapter를 감싸서 새 adapter를 설정하는 함수
37
+ */
38
+ export interface SyncStorageContextValue {
39
+ adapter: Accessor<StorageAdapter>;
40
+ configure: (fn: (origin: StorageAdapter) => StorageAdapter) => void;
41
+ }
42
+
16
43
  /**
17
44
  * 동기화 저장소 Context
18
45
  *
19
46
  * @remarks
20
47
  * Provider가 없으면 `undefined` (useSyncConfig에서 localStorage로 fallback)
21
48
  */
22
- export const SyncStorageContext = createContext<StorageAdapter | undefined>(undefined);
49
+ export const SyncStorageContext = createContext<SyncStorageContextValue>();
23
50
 
24
51
  /**
25
52
  * 동기화 저장소 Context에 접근하는 훅
26
53
  *
27
- * @returns StorageAdapter 또는 undefined (Provider가 없으면)
54
+ * @returns SyncStorageContextValue 또는 undefined (Provider가 없으면)
28
55
  */
29
- export function useSyncStorage(): StorageAdapter | undefined {
56
+ export function useSyncStorage(): SyncStorageContextValue | undefined {
30
57
  return useContext(SyncStorageContext);
31
58
  }
32
59
 
33
60
  /**
34
61
  * 동기화 저장소 Provider
35
62
  *
63
+ * @remarks
64
+ * - prop 없이 사용. 기본적으로 localStorage 기반 어댑터가 설정됨
65
+ * - `configure()`로 decorator 함수를 전달하여 기존 adapter를 감싸거나 교체 가능
66
+ *
36
67
  * @example
37
68
  * ```tsx
38
- * <SyncStorageProvider storage={myStorageAdapter}>
39
- * <ThemeProvider>
40
- * <App />
41
- * </ThemeProvider>
69
+ * <SyncStorageProvider>
70
+ * <App />
42
71
  * </SyncStorageProvider>
72
+ *
73
+ * // 자식 컴포넌트에서 decorator 패턴으로 adapter 커스터마이징:
74
+ * useSyncStorage()!.configure((origin) => ({
75
+ * getItem: (key) => myCustomGetItem(key),
76
+ * setItem: origin.setItem,
77
+ * removeItem: origin.removeItem,
78
+ * }));
43
79
  * ```
44
80
  */
45
- export const SyncStorageProvider: ParentComponent<{ storage: StorageAdapter }> = (props) => {
46
- return (
47
- // eslint-disable-next-line solid/reactivity -- storage는 초기 설정값으로 변경되지 않음
48
- <SyncStorageContext.Provider value={props.storage}>
49
- {props.children}
50
- </SyncStorageContext.Provider>
51
- );
81
+ export const SyncStorageProvider: ParentComponent = (props) => {
82
+ const [adapter, setAdapter] = createSignal<StorageAdapter>(defaultStorageAdapter);
83
+
84
+ const value: SyncStorageContextValue = {
85
+ adapter,
86
+ configure: (fn: (origin: StorageAdapter) => StorageAdapter) => setAdapter((prev) => fn(prev)),
87
+ };
88
+
89
+ return <SyncStorageContext.Provider value={value}>{props.children}</SyncStorageContext.Provider>;
52
90
  };
@@ -1,5 +1,13 @@
1
1
  import { defineEvent } from "@simplysm/service-common";
2
2
 
3
+ /**
4
+ * SharedData 변경 이벤트 정의
5
+ *
6
+ * @remarks
7
+ * 서버-클라이언트 간 공유 데이터 변경을 알리는 이벤트.
8
+ * - 이벤트 정보: `{ name: string; filter: unknown }` — 데이터 이름과 필터
9
+ * - 이벤트 데이터: `(string | number)[] | undefined` — 변경된 항목의 key 배열 (undefined면 전체 갱신)
10
+ */
3
11
  export const SharedDataChangeEvent = defineEvent<
4
12
  { name: string; filter: unknown },
5
13
  (string | number)[] | undefined
@@ -1,36 +1,77 @@
1
1
  import { type Accessor, createContext, useContext } from "solid-js";
2
2
 
3
+ /**
4
+ * 공유 데이터 정의
5
+ *
6
+ * @remarks
7
+ * SharedDataProvider에 전달하여 서버 데이터 구독을 설정한다.
8
+ */
3
9
  export interface SharedDataDefinition<TData> {
10
+ /** 서비스 연결 key (useServiceClient의 connect key와 동일) */
4
11
  serviceKey: string;
12
+ /** 데이터 조회 함수 (changeKeys가 있으면 해당 항목만 부분 갱신) */
5
13
  fetch: (changeKeys?: Array<string | number>) => Promise<TData[]>;
14
+ /** 항목의 고유 key 추출 함수 */
6
15
  getKey: (item: TData) => string | number;
16
+ /** 정렬 기준 배열 (여러 기준 적용 가능) */
7
17
  orderBy: [(item: TData) => unknown, "asc" | "desc"][];
18
+ /** 서버 이벤트 필터 (같은 name의 이벤트 중 filter가 일치하는 것만 수신) */
8
19
  filter?: unknown;
9
20
  }
10
21
 
22
+ /**
23
+ * 공유 데이터 접근자
24
+ *
25
+ * @remarks
26
+ * 각 데이터 key에 대한 반응형 접근 및 변경 알림을 제공한다.
27
+ */
11
28
  export interface SharedDataAccessor<TData> {
29
+ /** 반응형 항목 배열 */
12
30
  items: Accessor<TData[]>;
31
+ /** key로 단일 항목 조회 */
13
32
  get: (key: string | number | undefined) => TData | undefined;
33
+ /** 서버에 변경 이벤트 전파 (모든 구독자에게 refetch 트리거) */
14
34
  emit: (changeKeys?: Array<string | number>) => Promise<void>;
15
35
  }
16
36
 
37
+ /**
38
+ * 공유 데이터 Context 값
39
+ *
40
+ * @remarks
41
+ * - configure 호출 전: wait, busy, configure만 접근 가능. 데이터 접근 시 throw
42
+ * - configure 호출 후: 각 데이터 key별 SharedDataAccessor와 전체 상태 관리 메서드 포함
43
+ */
17
44
  export type SharedDataValue<TSharedData extends Record<string, unknown>> = {
18
45
  [K in keyof TSharedData]: SharedDataAccessor<TSharedData[K]>;
19
46
  } & {
47
+ /** 모든 초기 fetch 완료까지 대기 */
20
48
  wait: () => Promise<void>;
49
+ /** fetch 진행 중 여부 */
21
50
  busy: Accessor<boolean>;
51
+ /** definitions를 설정하여 데이터 구독 시작 (decorator 패턴) */
52
+ configure: (
53
+ fn: (origin: {
54
+ [K in keyof TSharedData]: SharedDataDefinition<TSharedData[K]>;
55
+ }) => {
56
+ [K in keyof TSharedData]: SharedDataDefinition<TSharedData[K]>;
57
+ },
58
+ ) => void;
22
59
  };
23
60
 
61
+ /** 공유 데이터 Context */
24
62
  export const SharedDataContext = createContext<SharedDataValue<Record<string, unknown>>>();
25
63
 
64
+ /**
65
+ * 공유 데이터에 접근하는 훅
66
+ *
67
+ * @throws SharedDataProvider가 없으면 에러 발생
68
+ */
26
69
  export function useSharedData<
27
70
  TSharedData extends Record<string, unknown> = Record<string, unknown>,
28
71
  >(): SharedDataValue<TSharedData> {
29
72
  const context = useContext(SharedDataContext);
30
73
  if (!context) {
31
- throw new Error(
32
- "useSharedData는 SharedDataProvider 내부에서만 사용할 수 있습니다. SharedDataProvider는 ServiceClientProvider 아래에 위치해야 합니다",
33
- );
74
+ throw new Error("useSharedData는 SharedDataProvider 내부에서만 사용할 수 있습니다");
34
75
  }
35
76
  return context as unknown as SharedDataValue<TSharedData>;
36
77
  }
@@ -11,14 +11,41 @@ import { useServiceClient } from "../ServiceClientContext";
11
11
  import { useNotification } from "../../components/feedback/notification/NotificationContext";
12
12
  import { useLogger } from "../../hooks/useLogger";
13
13
 
14
- export function SharedDataProvider<TSharedData extends Record<string, unknown>>(props: {
15
- definitions: { [K in keyof TSharedData]: SharedDataDefinition<TSharedData[K]> };
16
- children: JSX.Element;
17
- }): JSX.Element {
14
+ /**
15
+ * 공유 데이터 Provider
16
+ *
17
+ * @remarks
18
+ * - ServiceClientProvider와 NotificationProvider 내부에서 사용해야 함
19
+ * - LoggerProvider가 있으면 fetch 실패를 로거에도 기록
20
+ * - configure() 호출 전: wait, busy, configure만 접근 가능. 데이터 접근 시 throw
21
+ * - configure() 호출 후: definitions의 각 key마다 서버 이벤트 리스너를 등록하여 실시간 동기화
22
+ * - 동시 fetch 호출 시 version counter로 데이터 역전 방지
23
+ * - fetch 실패 시 사용자에게 danger 알림 표시
24
+ * - cleanup 시 모든 이벤트 리스너 자동 해제
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <SharedDataProvider>
29
+ * <App />
30
+ * </SharedDataProvider>
31
+ *
32
+ * // 자식 컴포넌트에서 나중에 설정:
33
+ * useSharedData().configure(() => ({
34
+ * users: {
35
+ * serviceKey: "main",
36
+ * fetch: async (changeKeys) => fetchUsers(changeKeys),
37
+ * getKey: (item) => item.id,
38
+ * orderBy: [[(item) => item.name, "asc"]],
39
+ * },
40
+ * }));
41
+ * ```
42
+ */
43
+ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Element {
18
44
  const serviceClient = useServiceClient();
19
45
  const notification = useNotification();
20
46
  const logger = useLogger();
21
47
 
48
+ let configured = false;
22
49
  const [busyCount, setBusyCount] = createSignal(0);
23
50
  const busy: Accessor<boolean> = () => busyCount() > 0;
24
51
 
@@ -26,6 +53,8 @@ export function SharedDataProvider<TSharedData extends Record<string, unknown>>(
26
53
  const memoMap = new Map<string, Accessor<Map<string | number, unknown>>>();
27
54
  const listenerKeyMap = new Map<string, string>();
28
55
  const versionMap = new Map<string, number>();
56
+ const accessors: Record<string, SharedDataAccessor<unknown>> = {};
57
+ let currentDefinitions: Record<string, SharedDataDefinition<unknown>> | undefined;
29
58
 
30
59
  function ordering<TT>(data: TT[], orderByList: [(item: TT) => unknown, "asc" | "desc"][]): TT[] {
31
60
  let result = [...data];
@@ -86,70 +115,95 @@ export function SharedDataProvider<TSharedData extends Record<string, unknown>>(
86
115
  await waitUntil(() => busyCount() <= 0);
87
116
  }
88
117
 
89
- const accessors: Record<string, SharedDataAccessor<unknown>> = {};
90
-
91
- // eslint-disable-next-line solid/reactivity -- definitions는 초기 설정용으로 마운트 시 1회만 읽음
92
- for (const [name, def] of Object.entries(props.definitions) as [
93
- string,
94
- SharedDataDefinition<unknown>,
95
- ][]) {
96
- const [items, setItems] = createSignal<unknown[]>([]);
97
- // eslint-disable-next-line solid/reactivity -- signal 참조를 Map에 저장하는 것은 반응성 접근이 아님
98
- signalMap.set(name, [items, setItems]);
99
-
100
- const itemMap = createMemo(() => {
101
- const map = new Map<string | number, unknown>();
102
- for (const item of items()) {
103
- map.set(def.getKey(item as never), item);
104
- }
105
- return map;
106
- });
107
- // eslint-disable-next-line solid/reactivity -- memo 참조를 Map에 저장하는 것은 반응성 접근이 아님
108
- memoMap.set(name, itemMap);
109
-
110
- const client = serviceClient.get(def.serviceKey);
111
- void client
112
- .addEventListener(SharedDataChangeEvent, { name, filter: def.filter }, async (changeKeys) => {
113
- await loadData(name, def, changeKeys);
114
- })
115
- .then((key) => {
116
- listenerKeyMap.set(name, key);
118
+ function configure(
119
+ fn: (
120
+ origin: Record<string, SharedDataDefinition<unknown>>,
121
+ ) => Record<string, SharedDataDefinition<unknown>>,
122
+ ): void {
123
+ if (configured) {
124
+ throw new Error("SharedDataProvider: configure() 1회만 호출할 수 있습니다");
125
+ }
126
+ configured = true;
127
+
128
+ const definitions = fn({});
129
+ currentDefinitions = definitions;
130
+
131
+ for (const [name, def] of Object.entries(definitions)) {
132
+ const [items, setItems] = createSignal<unknown[]>([]);
133
+ // eslint-disable-next-line solid/reactivity -- signal 참조를 Map에 저장하는 것은 반응성 접근이 아님
134
+ signalMap.set(name, [items, setItems]);
135
+
136
+ const itemMap = createMemo(() => {
137
+ const map = new Map<string | number, unknown>();
138
+ for (const item of items()) {
139
+ map.set(def.getKey(item as never), item);
140
+ }
141
+ return map;
117
142
  });
143
+ // eslint-disable-next-line solid/reactivity -- memo 참조를 Map에 저장하는 것은 반응성 접근이 아님
144
+ memoMap.set(name, itemMap);
118
145
 
119
- void loadData(name, def);
120
-
121
- accessors[name] = {
122
- items,
123
- get: (key: string | number | undefined) => {
124
- if (key === undefined) return undefined;
125
- return itemMap().get(key);
126
- },
127
- emit: async (changeKeys?: Array<string | number>) => {
128
- await client.emitToServer(
146
+ const client = serviceClient.get(def.serviceKey);
147
+ void client
148
+ .addEventListener(
129
149
  SharedDataChangeEvent,
130
- (info) => info.name === name && objEqual(info.filter, def.filter),
131
- changeKeys,
132
- );
133
- },
134
- };
150
+ { name, filter: def.filter },
151
+ async (changeKeys) => {
152
+ await loadData(name, def, changeKeys);
153
+ },
154
+ )
155
+ .then((key) => {
156
+ listenerKeyMap.set(name, key);
157
+ });
158
+
159
+ void loadData(name, def);
160
+
161
+ accessors[name] = {
162
+ items,
163
+ get: (key: string | number | undefined) => {
164
+ if (key === undefined) return undefined;
165
+ return itemMap().get(key);
166
+ },
167
+ emit: async (changeKeys?: Array<string | number>) => {
168
+ await client.emitToServer(
169
+ SharedDataChangeEvent,
170
+ (info) => info.name === name && objEqual(info.filter, def.filter),
171
+ changeKeys,
172
+ );
173
+ },
174
+ };
175
+ }
135
176
  }
136
177
 
137
178
  onCleanup(() => {
138
- for (const [name] of Object.entries(props.definitions)) {
179
+ if (!currentDefinitions) return;
180
+ for (const [name] of Object.entries(currentDefinitions)) {
139
181
  const listenerKey = listenerKeyMap.get(name);
140
182
  if (listenerKey != null) {
141
- const def = (props.definitions as Record<string, SharedDataDefinition<unknown>>)[name];
183
+ const def = currentDefinitions[name];
142
184
  const client = serviceClient.get(def.serviceKey);
143
185
  void client.removeEventListener(listenerKey);
144
186
  }
145
187
  }
146
188
  });
147
189
 
148
- const contextValue = {
149
- ...accessors,
150
- wait,
151
- busy,
152
- } as SharedDataValue<Record<string, unknown>>;
190
+ const KNOWN_KEYS = new Set(["wait", "busy", "configure"]);
191
+
192
+ // Proxy: configure 전 데이터 접근 시 throw
193
+ const contextValue = new Proxy(
194
+ { wait, busy, configure } as SharedDataValue<Record<string, unknown>>,
195
+ {
196
+ get(target, prop: string) {
197
+ if (KNOWN_KEYS.has(prop)) {
198
+ return target[prop];
199
+ }
200
+ if (!configured) {
201
+ throw new Error("SharedDataProvider: configure()를 먼저 호출해야 합니다");
202
+ }
203
+ return accessors[prop];
204
+ },
205
+ },
206
+ );
153
207
 
154
208
  return (
155
209
  <SharedDataContext.Provider value={contextValue}>{props.children}</SharedDataContext.Provider>