@simplysm/solid 13.0.62 → 13.0.65

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 (157) hide show
  1. package/README.md +6 -0
  2. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  3. package/dist/components/data/sheet/DataSheet.js +3 -2
  4. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  5. package/dist/components/features/address/AddressSearch.d.ts +8 -0
  6. package/dist/components/features/address/AddressSearch.d.ts.map +1 -0
  7. package/dist/components/features/address/AddressSearch.js +72 -0
  8. package/dist/components/features/address/AddressSearch.js.map +6 -0
  9. package/dist/components/features/crud-detail/CrudDetail.d.ts.map +1 -0
  10. package/dist/components/{data → features}/crud-detail/CrudDetail.js +62 -41
  11. package/dist/components/features/crud-detail/CrudDetail.js.map +6 -0
  12. package/dist/components/features/crud-detail/CrudDetailAfter.d.ts.map +1 -0
  13. package/dist/components/{data → features}/crud-detail/CrudDetailAfter.js.map +1 -1
  14. package/dist/components/features/crud-detail/CrudDetailBefore.d.ts.map +1 -0
  15. package/dist/components/{data → features}/crud-detail/CrudDetailBefore.js.map +1 -1
  16. package/dist/components/features/crud-detail/CrudDetailTools.d.ts.map +1 -0
  17. package/dist/components/{data → features}/crud-detail/CrudDetailTools.js.map +1 -1
  18. package/dist/components/features/crud-detail/types.d.ts.map +1 -0
  19. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -0
  20. package/dist/components/{data → features}/crud-sheet/CrudSheet.js +166 -21
  21. package/dist/components/features/crud-sheet/CrudSheet.js.map +6 -0
  22. package/dist/components/features/crud-sheet/CrudSheetColumn.d.ts.map +1 -0
  23. package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.js +1 -1
  24. package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.js.map +1 -1
  25. package/dist/components/features/crud-sheet/CrudSheetFilter.d.ts.map +1 -0
  26. package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.js.map +1 -1
  27. package/dist/components/features/crud-sheet/CrudSheetHeader.d.ts.map +1 -0
  28. package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.js.map +1 -1
  29. package/dist/components/features/crud-sheet/CrudSheetTools.d.ts.map +1 -0
  30. package/dist/components/{data → features}/crud-sheet/CrudSheetTools.js.map +1 -1
  31. package/dist/components/{data → features}/crud-sheet/types.d.ts +10 -4
  32. package/dist/components/features/crud-sheet/types.d.ts.map +1 -0
  33. package/dist/components/features/data-select-button/DataSelectButton.d.ts +38 -0
  34. package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -0
  35. package/dist/components/features/data-select-button/DataSelectButton.js +184 -0
  36. package/dist/components/features/data-select-button/DataSelectButton.js.map +6 -0
  37. package/dist/components/features/permission-table/PermissionTable.d.ts.map +1 -0
  38. package/dist/components/{data → features}/permission-table/PermissionTable.js +1 -1
  39. package/dist/components/{data → features}/permission-table/PermissionTable.js.map +1 -1
  40. package/dist/components/features/shared-data/SharedDataSelect.d.ts +32 -0
  41. package/dist/components/features/shared-data/SharedDataSelect.d.ts.map +1 -0
  42. package/dist/components/features/shared-data/SharedDataSelect.js +74 -0
  43. package/dist/components/features/shared-data/SharedDataSelect.js.map +6 -0
  44. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts +29 -0
  45. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts.map +1 -0
  46. package/dist/components/features/shared-data/SharedDataSelectButton.js +17 -0
  47. package/dist/components/features/shared-data/SharedDataSelectButton.js.map +6 -0
  48. package/dist/components/features/shared-data/SharedDataSelectList.d.ts +29 -0
  49. package/dist/components/features/shared-data/SharedDataSelectList.d.ts.map +1 -0
  50. package/dist/components/features/shared-data/SharedDataSelectList.js +80 -0
  51. package/dist/components/features/shared-data/SharedDataSelectList.js.map +6 -0
  52. package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
  53. package/dist/components/form-control/checkbox/Checkbox.js +10 -10
  54. package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
  55. package/dist/components/form-control/checkbox/Checkbox.styles.d.ts.map +1 -1
  56. package/dist/components/form-control/checkbox/Checkbox.styles.js +2 -2
  57. package/dist/components/form-control/checkbox/Checkbox.styles.js.map +1 -1
  58. package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
  59. package/dist/components/form-control/checkbox/Radio.js +13 -13
  60. package/dist/components/form-control/checkbox/Radio.js.map +2 -2
  61. package/dist/components/form-control/select/Select.d.ts +7 -3
  62. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  63. package/dist/components/form-control/select/Select.js +146 -45
  64. package/dist/components/form-control/select/Select.js.map +2 -2
  65. package/dist/components/form-control/select-list/SelectList.d.ts +54 -0
  66. package/dist/components/form-control/select-list/SelectList.d.ts.map +1 -0
  67. package/dist/components/form-control/select-list/SelectList.js +280 -0
  68. package/dist/components/form-control/select-list/SelectList.js.map +6 -0
  69. package/dist/components/form-control/select-list/SelectListContext.d.ts +13 -0
  70. package/dist/components/form-control/select-list/SelectListContext.d.ts.map +1 -0
  71. package/dist/components/form-control/select-list/SelectListContext.js +14 -0
  72. package/dist/components/form-control/select-list/SelectListContext.js.map +6 -0
  73. package/dist/index.d.ts +11 -5
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +11 -5
  76. package/dist/index.js.map +1 -1
  77. package/dist/providers/ServiceClientContext.d.ts +5 -5
  78. package/dist/providers/ServiceClientContext.d.ts.map +1 -1
  79. package/dist/providers/ServiceClientProvider.d.ts.map +1 -1
  80. package/dist/providers/ServiceClientProvider.js +12 -8
  81. package/dist/providers/ServiceClientProvider.js.map +2 -2
  82. package/dist/providers/shared-data/SharedDataContext.d.ts +16 -2
  83. package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
  84. package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
  85. package/dist/providers/shared-data/SharedDataProvider.d.ts +1 -2
  86. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  87. package/dist/providers/shared-data/SharedDataProvider.js +27 -13
  88. package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
  89. package/docs/data-components.md +15 -4
  90. package/docs/form-controls.md +257 -0
  91. package/docs/hooks.md +30 -0
  92. package/docs/providers.md +7 -0
  93. package/package.json +5 -3
  94. package/src/components/data/sheet/DataSheet.tsx +6 -7
  95. package/src/components/features/address/AddressSearch.tsx +75 -0
  96. package/src/components/{data → features}/crud-detail/CrudDetail.tsx +51 -26
  97. package/src/components/{data → features}/crud-sheet/CrudSheet.tsx +160 -23
  98. package/src/components/{data → features}/crud-sheet/CrudSheetColumn.tsx +1 -1
  99. package/src/components/{data → features}/crud-sheet/types.ts +14 -4
  100. package/src/components/features/data-select-button/DataSelectButton.tsx +279 -0
  101. package/src/components/{data → features}/permission-table/PermissionTable.tsx +1 -1
  102. package/src/components/features/shared-data/SharedDataSelect.tsx +101 -0
  103. package/src/components/features/shared-data/SharedDataSelectButton.tsx +47 -0
  104. package/src/components/features/shared-data/SharedDataSelectList.tsx +85 -0
  105. package/src/components/form-control/checkbox/Checkbox.styles.ts +2 -2
  106. package/src/components/form-control/checkbox/Checkbox.tsx +18 -20
  107. package/src/components/form-control/checkbox/Radio.tsx +18 -20
  108. package/src/components/form-control/select/Select.tsx +192 -36
  109. package/src/components/form-control/select-list/SelectList.tsx +385 -0
  110. package/src/components/form-control/select-list/SelectListContext.ts +23 -0
  111. package/src/index.ts +29 -5
  112. package/src/providers/ServiceClientContext.ts +5 -5
  113. package/src/providers/ServiceClientProvider.tsx +17 -12
  114. package/src/providers/shared-data/SharedDataContext.ts +16 -2
  115. package/src/providers/shared-data/SharedDataProvider.tsx +33 -17
  116. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +0 -1
  117. package/dist/components/data/crud-detail/CrudDetail.js.map +0 -6
  118. package/dist/components/data/crud-detail/CrudDetailAfter.d.ts.map +0 -1
  119. package/dist/components/data/crud-detail/CrudDetailBefore.d.ts.map +0 -1
  120. package/dist/components/data/crud-detail/CrudDetailTools.d.ts.map +0 -1
  121. package/dist/components/data/crud-detail/types.d.ts.map +0 -1
  122. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +0 -1
  123. package/dist/components/data/crud-sheet/CrudSheet.js.map +0 -6
  124. package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts.map +0 -1
  125. package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts.map +0 -1
  126. package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts.map +0 -1
  127. package/dist/components/data/crud-sheet/CrudSheetTools.d.ts.map +0 -1
  128. package/dist/components/data/crud-sheet/types.d.ts.map +0 -1
  129. package/dist/components/data/permission-table/PermissionTable.d.ts.map +0 -1
  130. /package/dist/components/{data → features}/crud-detail/CrudDetail.d.ts +0 -0
  131. /package/dist/components/{data → features}/crud-detail/CrudDetailAfter.d.ts +0 -0
  132. /package/dist/components/{data → features}/crud-detail/CrudDetailAfter.js +0 -0
  133. /package/dist/components/{data → features}/crud-detail/CrudDetailBefore.d.ts +0 -0
  134. /package/dist/components/{data → features}/crud-detail/CrudDetailBefore.js +0 -0
  135. /package/dist/components/{data → features}/crud-detail/CrudDetailTools.d.ts +0 -0
  136. /package/dist/components/{data → features}/crud-detail/CrudDetailTools.js +0 -0
  137. /package/dist/components/{data → features}/crud-detail/types.d.ts +0 -0
  138. /package/dist/components/{data → features}/crud-detail/types.js +0 -0
  139. /package/dist/components/{data → features}/crud-detail/types.js.map +0 -0
  140. /package/dist/components/{data → features}/crud-sheet/CrudSheet.d.ts +0 -0
  141. /package/dist/components/{data → features}/crud-sheet/CrudSheetColumn.d.ts +0 -0
  142. /package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.d.ts +0 -0
  143. /package/dist/components/{data → features}/crud-sheet/CrudSheetFilter.js +0 -0
  144. /package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.d.ts +0 -0
  145. /package/dist/components/{data → features}/crud-sheet/CrudSheetHeader.js +0 -0
  146. /package/dist/components/{data → features}/crud-sheet/CrudSheetTools.d.ts +0 -0
  147. /package/dist/components/{data → features}/crud-sheet/CrudSheetTools.js +0 -0
  148. /package/dist/components/{data → features}/crud-sheet/types.js +0 -0
  149. /package/dist/components/{data → features}/crud-sheet/types.js.map +0 -0
  150. /package/dist/components/{data → features}/permission-table/PermissionTable.d.ts +0 -0
  151. /package/src/components/{data → features}/crud-detail/CrudDetailAfter.tsx +0 -0
  152. /package/src/components/{data → features}/crud-detail/CrudDetailBefore.tsx +0 -0
  153. /package/src/components/{data → features}/crud-detail/CrudDetailTools.tsx +0 -0
  154. /package/src/components/{data → features}/crud-detail/types.ts +0 -0
  155. /package/src/components/{data → features}/crud-sheet/CrudSheetFilter.tsx +0 -0
  156. /package/src/components/{data → features}/crud-sheet/CrudSheetHeader.tsx +0 -0
  157. /package/src/components/{data → features}/crud-sheet/CrudSheetTools.tsx +0 -0
@@ -0,0 +1,385 @@
1
+ import {
2
+ children,
3
+ createEffect,
4
+ createMemo,
5
+ createSignal,
6
+ For,
7
+ type JSX,
8
+ onCleanup,
9
+ type ParentComponent,
10
+ Show,
11
+ splitProps,
12
+ } from "solid-js";
13
+ import clsx from "clsx";
14
+ import { twMerge } from "tailwind-merge";
15
+ import { List } from "../../data/list/List";
16
+ import { Pagination } from "../../data/Pagination";
17
+ import { TextInput } from "../field/TextInput";
18
+ import { createSlotSignal } from "../../../hooks/createSlotSignal";
19
+ import { SelectListContext, type SelectListContextValue } from "./SelectListContext";
20
+ import { useSelectListContext } from "./SelectListContext";
21
+ import {
22
+ listItemBaseClass,
23
+ listItemSelectedClass,
24
+ listItemContentClass,
25
+ listItemDisabledClass,
26
+ } from "../../data/list/ListItem.styles";
27
+ import { ripple } from "../../../directives/ripple";
28
+ import { textMuted } from "../../../styles/tokens.styles";
29
+
30
+ void ripple;
31
+
32
+ // ─── 서브 컴포넌트 ──────────────────────────────────────
33
+
34
+ /**
35
+ * Header 슬롯 서브 컴포넌트
36
+ */
37
+ const SelectListHeader: ParentComponent = (props) => {
38
+ const ctx = useSelectListContext();
39
+ // eslint-disable-next-line solid/reactivity -- 슬롯 accessor로 저장, JSX tracked scope에서 호출됨
40
+ ctx.setHeader(() => props.children);
41
+ onCleanup(() => ctx.setHeader(undefined));
42
+ return null;
43
+ };
44
+
45
+ /**
46
+ * Filter 슬롯 서브 컴포넌트
47
+ */
48
+ const SelectListFilter: ParentComponent = (props) => {
49
+ const ctx = useSelectListContext();
50
+ // eslint-disable-next-line solid/reactivity -- 슬롯 accessor로 저장, JSX tracked scope에서 호출됨
51
+ ctx.setFilter(() => props.children);
52
+ onCleanup(() => ctx.setFilter(undefined));
53
+ return null;
54
+ };
55
+
56
+ /**
57
+ * ItemTemplate 서브 컴포넌트
58
+ */
59
+ const SelectListItemTemplate = <TArgs extends unknown[]>(props: {
60
+ children: (...args: TArgs) => JSX.Element;
61
+ }) => {
62
+ const ctx = useSelectListContext();
63
+ // eslint-disable-next-line solid/reactivity -- 렌더 함수를 signal에 저장, JSX tracked scope에서 호출됨
64
+ ctx.setItemTemplate(props.children as (...args: unknown[]) => JSX.Element);
65
+ onCleanup(() => ctx.setItemTemplate(undefined));
66
+ return null;
67
+ };
68
+
69
+ // ─── Props ──────────────────────────────────────────────
70
+
71
+ export interface SelectListProps<TValue> {
72
+ /** 목록 아이템 배열 */
73
+ items: TValue[];
74
+
75
+ /** 현재 선택된 값 */
76
+ value?: TValue;
77
+
78
+ /** 값 변경 콜백 */
79
+ onValueChange?: (value: TValue | undefined) => void;
80
+
81
+ /** 필수 선택 여부 (true이면 미지정 항목 숨김) */
82
+ required?: boolean;
83
+
84
+ /** 비활성화 */
85
+ disabled?: boolean;
86
+
87
+ /** 검색 텍스트 추출 함수 (있으면 검색 TextInput 자동 표시) */
88
+ getSearchText?: (item: TValue) => string;
89
+
90
+ /** 숨김 여부 필터 */
91
+ getIsHidden?: (item: TValue) => boolean;
92
+
93
+ /** 커스텀 필터 함수 */
94
+ filterFn?: (item: TValue, index: number) => boolean;
95
+
96
+ /** 값 변경 가드 (false 반환 시 변경 차단) */
97
+ canChange?: (item: TValue | undefined) => boolean | Promise<boolean>;
98
+
99
+ /** 페이지 크기 (있으면 Pagination 자동 표시) */
100
+ pageSize?: number;
101
+
102
+ /** 헤더 텍스트 (Header 슬롯보다 우선순위 낮음) */
103
+ header?: string;
104
+
105
+ /** 커스텀 class */
106
+ class?: string;
107
+
108
+ /** 커스텀 style */
109
+ style?: JSX.CSSProperties;
110
+
111
+ /** 서브 컴포넌트용 children */
112
+ children?: JSX.Element;
113
+ }
114
+
115
+ // ─── 스타일 ──────────────────────────────────────────────
116
+
117
+ const containerClass = clsx("inline-flex flex-col gap-1");
118
+
119
+ const headerClass = clsx("px-2 py-1 text-sm font-semibold");
120
+
121
+ // ─── 컴포넌트 ───────────────────────────────────────────
122
+
123
+ interface SelectListComponent {
124
+ <TValue = unknown>(props: SelectListProps<TValue>): JSX.Element;
125
+ Header: typeof SelectListHeader;
126
+ Filter: typeof SelectListFilter;
127
+ ItemTemplate: typeof SelectListItemTemplate;
128
+ }
129
+
130
+ export const SelectList: SelectListComponent = <TValue,>(props: SelectListProps<TValue>) => {
131
+ const [local, rest] = splitProps(props as SelectListProps<TValue> & { children?: JSX.Element }, [
132
+ "children",
133
+ "class",
134
+ "style",
135
+ "items",
136
+ "value",
137
+ "onValueChange",
138
+ "required",
139
+ "disabled",
140
+ "getSearchText",
141
+ "getIsHidden",
142
+ "filterFn",
143
+ "canChange",
144
+ "pageSize",
145
+ "header",
146
+ ]);
147
+
148
+ // ─── 내부 상태 ─────────────────────────────────────────
149
+
150
+ const [searchText, setSearchText] = createSignal("");
151
+ const [page, setPage] = createSignal(1);
152
+
153
+ // ─── 슬롯 signals ────────────────────────────────────
154
+
155
+ const [headerSlot, setHeader] = createSlotSignal();
156
+ const [filterSlot, setFilter] = createSlotSignal();
157
+ const [itemTemplate, _setItemTemplate] = createSignal<
158
+ ((...args: unknown[]) => JSX.Element) | undefined
159
+ >();
160
+ const setItemTemplate = (fn: ((...args: unknown[]) => JSX.Element) | undefined) =>
161
+ _setItemTemplate(() => fn);
162
+
163
+ // Context 값
164
+ const contextValue: SelectListContextValue = {
165
+ setHeader,
166
+ setFilter,
167
+ setItemTemplate,
168
+ };
169
+
170
+ // ─── items 변경 시 value 자동 재매칭 ──────────────────
171
+
172
+ createEffect(() => {
173
+ const currentItems = local.items;
174
+ const currentValue = local.value;
175
+ if (currentValue === undefined) return;
176
+
177
+ // 새 items에서 현재 value를 참조로 찾기
178
+ const found = currentItems.find((item) => item === currentValue);
179
+ if (found !== undefined) {
180
+ // 이미 같은 참조면 아무 것도 하지 않음
181
+ return;
182
+ }
183
+
184
+ // 참조가 없으면 현재 value를 유지 (호출하지 않음)
185
+ });
186
+
187
+ // ─── 필터링 파이프라인 ─────────────────────────────────
188
+
189
+ // getIsHidden 필터 → 검색 필터 → filterFn
190
+ const filteredItems = createMemo(() => {
191
+ let result = local.items;
192
+
193
+ // getIsHidden 필터
194
+ if (local.getIsHidden) {
195
+ const fn = local.getIsHidden;
196
+ result = result.filter((item) => !fn(item));
197
+ }
198
+
199
+ // 검색 필터
200
+ const search = searchText().trim().toLowerCase();
201
+ if (search && local.getSearchText) {
202
+ const getText = local.getSearchText;
203
+ result = result.filter((item) => getText(item).toLowerCase().includes(search));
204
+ }
205
+
206
+ // filterFn
207
+ if (local.filterFn) {
208
+ const fn = local.filterFn;
209
+ result = result.filter((item, index) => fn(item, index));
210
+ }
211
+
212
+ return result;
213
+ });
214
+
215
+ // 페이지 수 계산
216
+ const totalPageCount = createMemo(() => {
217
+ if (local.pageSize == null) return 1;
218
+ return Math.max(1, Math.ceil(filteredItems().length / local.pageSize));
219
+ });
220
+
221
+ // 검색이나 필터 변경 시 페이지 리셋
222
+ createEffect(() => {
223
+ // filteredItems에 의존
224
+ void filteredItems();
225
+ setPage(1);
226
+ });
227
+
228
+ // 페이지 슬라이스
229
+ const displayItems = createMemo(() => {
230
+ const items = filteredItems();
231
+ if (local.pageSize == null) return items;
232
+
233
+ const start = (page() - 1) * local.pageSize;
234
+ const end = start + local.pageSize;
235
+ return items.slice(start, end);
236
+ });
237
+
238
+ // ─── 선택/토글 핸들러 ─────────────────────────────────
239
+
240
+ const handleSelect = async (item: TValue | undefined) => {
241
+ if (local.disabled) return;
242
+
243
+ // canChange 가드
244
+ if (local.canChange) {
245
+ const allowed = await local.canChange(item);
246
+ if (!allowed) return;
247
+ }
248
+
249
+ // 토글: 이미 선택된 값을 다시 클릭하면 선택 해제 (required가 아닐 때만)
250
+ if (item !== undefined && item === local.value && !local.required) {
251
+ local.onValueChange?.(undefined);
252
+ } else {
253
+ local.onValueChange?.(item);
254
+ }
255
+ };
256
+
257
+ // ─── 아이템 렌더링 ────────────────────────────────────
258
+
259
+ const getItemTemplate = (): ((item: TValue, index: number) => JSX.Element) | undefined => {
260
+ return itemTemplate() as ((item: TValue, index: number) => JSX.Element) | undefined;
261
+ };
262
+
263
+ const renderItem = (item: TValue, index: number): JSX.Element => {
264
+ const tpl = getItemTemplate();
265
+ if (tpl) {
266
+ return tpl(item, index);
267
+ }
268
+ return <>{String(item)}</>;
269
+ };
270
+
271
+ // ─── 내부 렌더링 ──────────────────────────────────────
272
+
273
+ const SelectListInner: ParentComponent = (innerProps) => {
274
+ // children() resolve로 서브 컴포넌트 등록 트리거
275
+ const resolved = children(() => innerProps.children);
276
+ // resolved는 사용하지 않지만 서브 컴포넌트가 등록되도록 evaluate 필요
277
+ void resolved;
278
+
279
+ return (
280
+ <div
281
+ {...rest}
282
+ data-select-list
283
+ class={twMerge(containerClass, local.class)}
284
+ style={local.style}
285
+ >
286
+ {/* Header: 슬롯 우선, 없으면 props.header 텍스트 */}
287
+ <Show
288
+ when={headerSlot()}
289
+ fallback={
290
+ <Show when={local.header}>
291
+ <div class={headerClass}>{local.header}</div>
292
+ </Show>
293
+ }
294
+ >
295
+ {headerSlot()!()}
296
+ </Show>
297
+
298
+ {/* Filter: 슬롯 우선, 없으면 getSearchText 있을 때 TextInput */}
299
+ <Show
300
+ when={filterSlot()}
301
+ fallback={
302
+ <Show when={local.getSearchText}>
303
+ <TextInput
304
+ value={searchText()}
305
+ onValueChange={setSearchText}
306
+ placeholder="검색..."
307
+ inset
308
+ disabled={local.disabled}
309
+ />
310
+ </Show>
311
+ }
312
+ >
313
+ {filterSlot()!()}
314
+ </Show>
315
+
316
+ {/* Pagination */}
317
+ <Show when={local.pageSize != null && totalPageCount() > 1}>
318
+ <Pagination
319
+ page={page()}
320
+ onPageChange={setPage}
321
+ totalPageCount={totalPageCount()}
322
+ size="sm"
323
+ />
324
+ </Show>
325
+
326
+ {/* List */}
327
+ <List inset>
328
+ {/* 미지정 항목 (required가 아닐 때) */}
329
+ <Show when={!local.required}>
330
+ <button
331
+ type="button"
332
+ use:ripple={!local.disabled}
333
+ class={twMerge(
334
+ listItemBaseClass,
335
+ local.value === undefined && listItemSelectedClass,
336
+ local.disabled && listItemDisabledClass,
337
+ )}
338
+ data-list-item
339
+ role="treeitem"
340
+ aria-selected={local.value === undefined || undefined}
341
+ aria-disabled={local.disabled || undefined}
342
+ tabIndex={local.disabled ? -1 : 0}
343
+ onClick={() => handleSelect(undefined)}
344
+ >
345
+ <span class={clsx(listItemContentClass, textMuted)}>미지정</span>
346
+ </button>
347
+ </Show>
348
+
349
+ {/* 아이템 목록 */}
350
+ <For each={displayItems()}>
351
+ {(item, index) => (
352
+ <button
353
+ type="button"
354
+ use:ripple={!local.disabled}
355
+ class={twMerge(
356
+ listItemBaseClass,
357
+ item === local.value && listItemSelectedClass,
358
+ local.disabled && listItemDisabledClass,
359
+ )}
360
+ data-list-item
361
+ role="treeitem"
362
+ aria-selected={item === local.value || undefined}
363
+ aria-disabled={local.disabled || undefined}
364
+ tabIndex={local.disabled ? -1 : 0}
365
+ onClick={() => handleSelect(item)}
366
+ >
367
+ <span class={listItemContentClass}>{renderItem(item, index())}</span>
368
+ </button>
369
+ )}
370
+ </For>
371
+ </List>
372
+ </div>
373
+ );
374
+ };
375
+
376
+ return (
377
+ <SelectListContext.Provider value={contextValue}>
378
+ <SelectListInner>{local.children}</SelectListInner>
379
+ </SelectListContext.Provider>
380
+ );
381
+ };
382
+
383
+ SelectList.Header = SelectListHeader;
384
+ SelectList.Filter = SelectListFilter;
385
+ SelectList.ItemTemplate = SelectListItemTemplate;
@@ -0,0 +1,23 @@
1
+ import { createContext, useContext, type JSX } from "solid-js";
2
+ import type { SlotAccessor } from "../../../hooks/createSlotSignal";
3
+
4
+ export interface SelectListContextValue {
5
+ /** Header 슬롯 등록 */
6
+ setHeader: (content: SlotAccessor) => void;
7
+
8
+ /** Filter 슬롯 등록 */
9
+ setFilter: (content: SlotAccessor) => void;
10
+
11
+ /** ItemTemplate 등록 */
12
+ setItemTemplate: (fn: ((...args: unknown[]) => JSX.Element) | undefined) => void;
13
+ }
14
+
15
+ export const SelectListContext = createContext<SelectListContextValue>();
16
+
17
+ export function useSelectListContext(): SelectListContextValue {
18
+ const context = useContext(SelectListContext);
19
+ if (!context) {
20
+ throw new Error("useSelectListContext는 SelectList 컴포넌트 내부에서만 사용할 수 있습니다");
21
+ }
22
+ return context;
23
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,9 @@ export * from "./components/form-control/Button";
6
6
  // Select
7
7
  export * from "./components/form-control/select/Select";
8
8
 
9
+ // SelectList
10
+ export * from "./components/form-control/select-list/SelectList";
11
+
9
12
  // Combobox
10
13
  export * from "./components/form-control/combobox/Combobox";
11
14
 
@@ -57,12 +60,7 @@ export * from "./components/data/Pagination";
57
60
  export * from "./components/data/sheet/DataSheet";
58
61
  export * from "./components/data/sheet/DataSheet.styles";
59
62
  export * from "./components/data/sheet/types";
60
- export * from "./components/data/crud-sheet/CrudSheet";
61
- export * from "./components/data/crud-sheet/types";
62
- export * from "./components/data/crud-detail/CrudDetail";
63
- export * from "./components/data/crud-detail/types";
64
63
  export * from "./components/data/calendar/Calendar";
65
- export * from "./components/data/permission-table/PermissionTable";
66
64
  export * from "./components/data/kanban/Kanban";
67
65
  export * from "./components/data/kanban/KanbanContext";
68
66
 
@@ -171,3 +169,29 @@ export * from "./helpers/mergeStyles";
171
169
  export * from "./helpers/createAppStructure";
172
170
 
173
171
  //#endregion
172
+
173
+ //#region ========== Features ==========
174
+
175
+ // Address
176
+ export * from "./components/features/address/AddressSearch";
177
+
178
+ // SharedData wrappers
179
+ export * from "./components/features/shared-data/SharedDataSelect";
180
+ export * from "./components/features/shared-data/SharedDataSelectButton";
181
+ export * from "./components/features/shared-data/SharedDataSelectList";
182
+
183
+ // DataSelectButton
184
+ export * from "./components/features/data-select-button/DataSelectButton";
185
+
186
+ // CrudSheet
187
+ export * from "./components/features/crud-sheet/CrudSheet";
188
+ export * from "./components/features/crud-sheet/types";
189
+
190
+ // CrudDetail
191
+ export * from "./components/features/crud-detail/CrudDetail";
192
+ export * from "./components/features/crud-detail/types";
193
+
194
+ // PermissionTable
195
+ export * from "./components/features/permission-table/PermissionTable";
196
+
197
+ //#endregion
@@ -5,14 +5,14 @@ import type { ServiceClient, ServiceConnectionConfig } from "@simplysm/service-c
5
5
  * WebSocket 서비스 클라이언트 Context 값
6
6
  */
7
7
  export interface ServiceClientContextValue {
8
- /** WebSocket 연결 열기 (key 다중 연결 관리) */
9
- connect: (key: string, options?: Partial<ServiceConnectionConfig>) => Promise<void>;
8
+ /** WebSocket 연결 열기 (key 생략 "default") */
9
+ connect: (key?: string, options?: Partial<ServiceConnectionConfig>) => Promise<void>;
10
10
  /** 연결 닫기 */
11
- close: (key: string) => Promise<void>;
11
+ close: (key?: string) => Promise<void>;
12
12
  /** 연결된 클라이언트 인스턴스 가져오기 (연결되지 않은 key면 에러 발생) */
13
- get: (key: string) => ServiceClient;
13
+ get: (key?: string) => ServiceClient;
14
14
  /** 연결 상태 확인 */
15
- isConnected: (key: string) => boolean;
15
+ isConnected: (key?: string) => boolean;
16
16
  }
17
17
 
18
18
  /** WebSocket 서비스 클라이언트 Context */
@@ -45,11 +45,13 @@ export const ServiceClientProvider: ParentComponent = (props) => {
45
45
  });
46
46
 
47
47
  const connect = async (
48
- key: string,
48
+ key?: string,
49
49
  options?: Partial<ServiceConnectionConfig>,
50
50
  ): Promise<void> => {
51
- if (clientMap.has(key)) {
52
- const existing = clientMap.get(key)!;
51
+ const resolvedKey = key ?? "default";
52
+
53
+ if (clientMap.has(resolvedKey)) {
54
+ const existing = clientMap.get(resolvedKey)!;
53
55
  if (!existing.connected) {
54
56
  throw new Error("이미 연결이 끊긴 클라이언트와 같은 키로 연결을 시도하였습니다.");
55
57
  } else {
@@ -125,27 +127,30 @@ export const ServiceClientProvider: ParentComponent = (props) => {
125
127
  });
126
128
 
127
129
  await client.connect();
128
- clientMap.set(key, client);
130
+ clientMap.set(resolvedKey, client);
129
131
  };
130
132
 
131
- const close = async (key: string): Promise<void> => {
132
- const client = clientMap.get(key);
133
+ const close = async (key?: string): Promise<void> => {
134
+ const resolvedKey = key ?? "default";
135
+ const client = clientMap.get(resolvedKey);
133
136
  if (client) {
134
137
  await client.close();
135
- clientMap.delete(key);
138
+ clientMap.delete(resolvedKey);
136
139
  }
137
140
  };
138
141
 
139
- const get = (key: string): ServiceClient => {
140
- const client = clientMap.get(key);
142
+ const get = (key?: string): ServiceClient => {
143
+ const resolvedKey = key ?? "default";
144
+ const client = clientMap.get(resolvedKey);
141
145
  if (!client) {
142
- throw new Error(`연결하지 않은 클라이언트 키입니다. ${key}`);
146
+ throw new Error(`연결하지 않은 클라이언트 키입니다. ${resolvedKey}`);
143
147
  }
144
148
  return client;
145
149
  };
146
150
 
147
- const isConnected = (key: string): boolean => {
148
- const client = clientMap.get(key);
151
+ const isConnected = (key?: string): boolean => {
152
+ const resolvedKey = key ?? "default";
153
+ const client = clientMap.get(resolvedKey);
149
154
  return client?.connected ?? false;
150
155
  };
151
156
 
@@ -7,8 +7,8 @@ import { type Accessor, createContext, useContext } from "solid-js";
7
7
  * SharedDataProvider에 전달하여 서버 데이터 구독을 설정한다.
8
8
  */
9
9
  export interface SharedDataDefinition<TData> {
10
- /** 서비스 연결 key (useServiceClient의 connect key와 동일) */
11
- serviceKey: string;
10
+ /** 서비스 연결 key (생략 "default") */
11
+ serviceKey?: string;
12
12
  /** 데이터 조회 함수 (changeKeys가 있으면 해당 항목만 부분 갱신) */
13
13
  fetch: (changeKeys?: Array<string | number>) => Promise<TData[]>;
14
14
  /** 항목의 고유 key 추출 함수 */
@@ -17,6 +17,12 @@ export interface SharedDataDefinition<TData> {
17
17
  orderBy: [(item: TData) => unknown, "asc" | "desc"][];
18
18
  /** 서버 이벤트 필터 (같은 name의 이벤트 중 filter가 일치하는 것만 수신) */
19
19
  filter?: unknown;
20
+ /** 항목에서 검색 텍스트를 추출하는 함수 */
21
+ getSearchText?: (item: TData) => string;
22
+ /** 항목이 숨김 상태인지 판별하는 함수 */
23
+ getIsHidden?: (item: TData) => boolean;
24
+ /** 항목의 부모 key를 추출하는 함수 (트리 구조 지원) */
25
+ getParentKey?: (item: TData) => string | number | undefined;
20
26
  }
21
27
 
22
28
  /**
@@ -32,6 +38,14 @@ export interface SharedDataAccessor<TData> {
32
38
  get: (key: string | number | undefined) => TData | undefined;
33
39
  /** 서버에 변경 이벤트 전파 (모든 구독자에게 refetch 트리거) */
34
40
  emit: (changeKeys?: Array<string | number>) => Promise<void>;
41
+ /** 항목의 고유 key 추출 함수 */
42
+ getKey: (item: TData) => string | number;
43
+ /** 항목에서 검색 텍스트를 추출하는 함수 */
44
+ getSearchText?: (item: TData) => string;
45
+ /** 항목이 숨김 상태인지 판별하는 함수 */
46
+ getIsHidden?: (item: TData) => boolean;
47
+ /** 항목의 부모 key를 추출하는 함수 (트리 구조 지원) */
48
+ getParentKey?: (item: TData) => string | number | undefined;
35
49
  }
36
50
 
37
51
  /**
@@ -18,7 +18,7 @@ import { useLogger } from "../../hooks/useLogger";
18
18
  * - ServiceClientProvider와 NotificationProvider 내부에서 사용해야 함
19
19
  * - LoggerProvider가 있으면 fetch 실패를 로거에도 기록
20
20
  * - configure() 호출 전: wait, busy, configure만 접근 가능. 데이터 접근 시 throw
21
- * - configure() 호출 후: definitions 각 key마다 서버 이벤트 리스너를 등록하여 실시간 동기화
21
+ * - configure() 호출 후: definitions 등록. 각 key items()/get() 첫 접근 시 서버 이벤트 리스너 등록 + fetch (lazy)
22
22
  * - 동시 fetch 호출 시 version counter로 데이터 역전 방지
23
23
  * - fetch 실패 시 사용자에게 danger 알림 표시
24
24
  * - cleanup 시 모든 이벤트 리스너 자동 해제
@@ -32,7 +32,6 @@ import { useLogger } from "../../hooks/useLogger";
32
32
  * // 자식 컴포넌트에서 나중에 설정:
33
33
  * useSharedData().configure(() => ({
34
34
  * users: {
35
- * serviceKey: "main",
36
35
  * fetch: async (changeKeys) => fetchUsers(changeKeys),
37
36
  * getKey: (item) => item.id,
38
37
  * orderBy: [[(item) => item.name, "asc"]],
@@ -143,24 +142,37 @@ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Elemen
143
142
  // eslint-disable-next-line solid/reactivity -- memo 참조를 Map에 저장하는 것은 반응성 접근이 아님
144
143
  memoMap.set(name, itemMap);
145
144
 
146
- const client = serviceClient.get(def.serviceKey);
147
- void client
148
- .addEventListener(
149
- SharedDataChangeEvent,
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
- });
145
+ const client = serviceClient.get(def.serviceKey ?? "default");
146
+
147
+ let initialized = false;
148
+
149
+ function ensureInitialized() {
150
+ if (initialized) return;
151
+ initialized = true;
158
152
 
159
- void loadData(name, def);
153
+ // TODO: addEventListener가 resolve 전에 unmount되면 listener orphan 가능
154
+ void client
155
+ .addEventListener(
156
+ SharedDataChangeEvent,
157
+ { name, filter: def.filter },
158
+ async (changeKeys) => {
159
+ await loadData(name, def, changeKeys);
160
+ },
161
+ )
162
+ .then((key) => {
163
+ listenerKeyMap.set(name, key);
164
+ });
165
+
166
+ void loadData(name, def);
167
+ }
160
168
 
161
169
  accessors[name] = {
162
- items,
170
+ items: () => {
171
+ ensureInitialized();
172
+ return items();
173
+ },
163
174
  get: (key: string | number | undefined) => {
175
+ ensureInitialized();
164
176
  if (key === undefined) return undefined;
165
177
  return itemMap().get(key);
166
178
  },
@@ -171,6 +183,10 @@ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Elemen
171
183
  changeKeys,
172
184
  );
173
185
  },
186
+ getKey: def.getKey,
187
+ getSearchText: def.getSearchText,
188
+ getIsHidden: def.getIsHidden,
189
+ getParentKey: def.getParentKey,
174
190
  };
175
191
  }
176
192
  }
@@ -181,7 +197,7 @@ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Elemen
181
197
  const listenerKey = listenerKeyMap.get(name);
182
198
  if (listenerKey != null) {
183
199
  const def = currentDefinitions[name];
184
- const client = serviceClient.get(def.serviceKey);
200
+ const client = serviceClient.get(def.serviceKey ?? "default");
185
201
  void client.removeEventListener(listenerKey);
186
202
  }
187
203
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"CrudDetail.d.ts","sourceRoot":"","sources":["..\\..\\..\\..\\src\\components\\data\\crud-detail\\CrudDetail.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,GAAG,EAKT,MAAM,UAAU,CAAC;AAoBlB,OAAO,EAAE,eAAe,EAAwB,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAyB,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAwB,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAKV,eAAe,EAEhB,MAAM,SAAS,CAAC;AAEjB,UAAU,mBAAmB;IAC3B,CAAC,KAAK,SAAS,MAAM,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC;IACnE,KAAK,EAAE,OAAO,eAAe,CAAC;IAC9B,MAAM,EAAE,OAAO,gBAAgB,CAAC;IAChC,KAAK,EAAE,OAAO,eAAe,CAAC;CAC/B;AA6SD,eAAO,MAAM,UAAU,EAAgC,mBAAmB,CAAC"}