@uniai-fe/uds-primitives 0.2.4 → 0.2.6
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/styles.css +56 -0
- package/package.json +1 -1
- package/src/components/input/hooks/index.ts +3 -0
- package/src/components/input/hooks/useInputFile.ts +254 -0
- package/src/components/input/hooks/useInputFileContext.ts +244 -0
- package/src/components/input/hooks/useInputFileRHF.ts +105 -0
- package/src/components/input/index.scss +1 -0
- package/src/components/input/markup/file/DragAndDrop.tsx +59 -0
- package/src/components/input/markup/file/UploadButton.tsx +44 -0
- package/src/components/input/markup/file/UploadedChip.tsx +66 -0
- package/src/components/input/markup/file/index.tsx +25 -0
- package/src/components/input/markup/file/list/Body.tsx +55 -0
- package/src/components/input/markup/file/list/Container.tsx +97 -0
- package/src/components/input/markup/file/list/Header.tsx +86 -0
- package/src/components/input/markup/file/list/Item.tsx +35 -0
- package/src/components/input/markup/file/list/Provider.tsx +35 -0
- package/src/components/input/markup/file/list/Remove.tsx +54 -0
- package/src/components/input/markup/file/list/index.tsx +31 -0
- package/src/components/input/markup/foundation/SideSlot.tsx +0 -2
- package/src/components/input/markup/foundation/Utility.tsx +2 -0
- package/src/components/input/markup/foundation/index.tsx +0 -2
- package/src/components/input/markup/index.tsx +2 -0
- package/src/components/input/markup/text/Password.tsx +2 -0
- package/src/components/input/markup/text/Search.tsx +0 -2
- package/src/components/input/styles/file.scss +60 -0
- package/src/components/input/types/file.ts +531 -0
- package/src/components/input/types/index.ts +1 -0
- package/src/components/input/utils/file.ts +103 -0
- package/src/components/input/utils/index.tsx +1 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.input-file-drag-and-drop {
|
|
2
|
+
width: 100%;
|
|
3
|
+
border: 1px dashed var(--input-border-color);
|
|
4
|
+
border-radius: var(--input-default-radius-base);
|
|
5
|
+
background-color: var(--input-surface-color);
|
|
6
|
+
transition:
|
|
7
|
+
border-color 0.2s ease,
|
|
8
|
+
background-color 0.2s ease;
|
|
9
|
+
|
|
10
|
+
&[data-dragging="true"] {
|
|
11
|
+
border-color: var(--input-border-active-color);
|
|
12
|
+
background-color: var(--input-surface-muted-color);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&[data-disabled="true"] {
|
|
16
|
+
opacity: 0.6;
|
|
17
|
+
cursor: not-allowed;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.input-file-list-container {
|
|
22
|
+
width: 100%;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: 8px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.input-file-list-header {
|
|
29
|
+
width: 100%;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
|
|
34
|
+
&[data-trigger="true"] {
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.input-file-list-body {
|
|
40
|
+
width: 100%;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 6px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.input-file-list-item {
|
|
47
|
+
width: 100%;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
gap: 8px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.input-file-list-remove {
|
|
55
|
+
border: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: inherit;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
4
|
+
DragEvent,
|
|
5
|
+
FocusEvent,
|
|
6
|
+
MouseEvent,
|
|
7
|
+
ReactNode,
|
|
8
|
+
RefObject,
|
|
9
|
+
SyntheticEvent,
|
|
10
|
+
} from "react";
|
|
11
|
+
import type { ButtonProps } from "../../button/types";
|
|
12
|
+
import type { ChipInputProps } from "../../chip/types";
|
|
13
|
+
import type { UseFormRegisterReturn } from "react-hook-form";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Input Types; File Upload 소스 타입
|
|
17
|
+
*/
|
|
18
|
+
export type InputFileSource = "input" | "drop";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Input Types; File Upload 이벤트 타입
|
|
22
|
+
*/
|
|
23
|
+
export type InputFileEvent =
|
|
24
|
+
| ChangeEvent<HTMLInputElement>
|
|
25
|
+
| DragEvent<HTMLElement>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Input Types; File Upload 변경 핸들러
|
|
29
|
+
*/
|
|
30
|
+
export type InputFileChangeHandler = (
|
|
31
|
+
files: File[],
|
|
32
|
+
source: InputFileSource,
|
|
33
|
+
event: InputFileEvent,
|
|
34
|
+
) => void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Input Types; File Upload Hook 옵션
|
|
38
|
+
* @property {InputFileChangeHandler} onFilesChange 파일 변경 콜백
|
|
39
|
+
* @property {RefObject<HTMLInputElement | null>} [inputRef] 직접 제어할 file input ref
|
|
40
|
+
* @property {string} [selectorId] inputRef가 없을 때 조회할 input id
|
|
41
|
+
* @property {boolean} [disabled] 동작 비활성화 여부
|
|
42
|
+
* @property {boolean} [resetInputValue] 등록 후 input 값을 비울지 여부
|
|
43
|
+
*/
|
|
44
|
+
export interface UseInputFileOptions {
|
|
45
|
+
/**
|
|
46
|
+
* 파일 변경 콜백
|
|
47
|
+
*/
|
|
48
|
+
onFilesChange: InputFileChangeHandler;
|
|
49
|
+
/**
|
|
50
|
+
* 직접 제어할 file input ref
|
|
51
|
+
*/
|
|
52
|
+
inputRef?: RefObject<HTMLInputElement | null>;
|
|
53
|
+
/**
|
|
54
|
+
* inputRef가 없을 때 조회할 input id
|
|
55
|
+
*/
|
|
56
|
+
selectorId?: string;
|
|
57
|
+
/**
|
|
58
|
+
* 동작 비활성화 여부
|
|
59
|
+
*/
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* 등록 후 input 값을 비울지 여부
|
|
63
|
+
*/
|
|
64
|
+
resetInputValue?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Input Types; File Upload Hook 반환값
|
|
69
|
+
* @property {boolean} isDragging 드래그 오버 상태
|
|
70
|
+
* @property {File[]} selectedFiles 최근 선택된 파일 목록
|
|
71
|
+
* @property {boolean} hasSelectedFiles 파일 선택 여부
|
|
72
|
+
* @property {() => void} onClearSelectedFiles 선택된 파일 초기화 핸들러
|
|
73
|
+
* @property {(event?: SyntheticEvent<HTMLElement>) => void} onOpenFileDialog 파일 선택창 열기 핸들러
|
|
74
|
+
* @property {(event: ChangeEvent<HTMLInputElement>) => void} onInputFileChange file input change 핸들러
|
|
75
|
+
* @property {(event: DragEvent<HTMLElement>) => void} onDropFiles drop 핸들러
|
|
76
|
+
* @property {(event: DragEvent<HTMLElement>) => void} onDragEnter dragenter 핸들러
|
|
77
|
+
* @property {(event: DragEvent<HTMLElement>) => void} onDragOver dragover 핸들러
|
|
78
|
+
* @property {(event: DragEvent<HTMLElement>) => void} onDragLeave dragleave 핸들러
|
|
79
|
+
*/
|
|
80
|
+
export interface UseInputFileResult {
|
|
81
|
+
/**
|
|
82
|
+
* 드래그 오버 상태
|
|
83
|
+
*/
|
|
84
|
+
isDragging: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* 최근 선택된 파일 목록
|
|
87
|
+
*/
|
|
88
|
+
selectedFiles: File[];
|
|
89
|
+
/**
|
|
90
|
+
* 파일 선택 여부
|
|
91
|
+
*/
|
|
92
|
+
hasSelectedFiles: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* 선택된 파일 초기화 핸들러
|
|
95
|
+
*/
|
|
96
|
+
onClearSelectedFiles: () => void;
|
|
97
|
+
/**
|
|
98
|
+
* 파일 선택창 열기 핸들러
|
|
99
|
+
*/
|
|
100
|
+
onOpenFileDialog: (event?: SyntheticEvent<HTMLElement>) => void;
|
|
101
|
+
/**
|
|
102
|
+
* file input change 핸들러
|
|
103
|
+
*/
|
|
104
|
+
onInputFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
105
|
+
/**
|
|
106
|
+
* drop 핸들러
|
|
107
|
+
*/
|
|
108
|
+
onDropFiles: (event: DragEvent<HTMLElement>) => void;
|
|
109
|
+
/**
|
|
110
|
+
* dragenter 핸들러
|
|
111
|
+
*/
|
|
112
|
+
onDragEnter: (event: DragEvent<HTMLElement>) => void;
|
|
113
|
+
/**
|
|
114
|
+
* dragover 핸들러
|
|
115
|
+
*/
|
|
116
|
+
onDragOver: (event: DragEvent<HTMLElement>) => void;
|
|
117
|
+
/**
|
|
118
|
+
* dragleave 핸들러
|
|
119
|
+
*/
|
|
120
|
+
onDragLeave: (event: DragEvent<HTMLElement>) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Input Types; File DragAndDrop 컴포넌트 props
|
|
125
|
+
* @property {React.ReactNode} children drop zone 내부 콘텐츠
|
|
126
|
+
* @property {InputFileChangeHandler} onFilesChange 파일 변경 콜백
|
|
127
|
+
* @property {boolean} [disabled] drop 비활성화 여부
|
|
128
|
+
*/
|
|
129
|
+
export interface InputFileDragAndDropProps extends Omit<
|
|
130
|
+
ComponentPropsWithoutRef<"div">,
|
|
131
|
+
"onDrop" | "onDragEnter" | "onDragLeave" | "onDragOver"
|
|
132
|
+
> {
|
|
133
|
+
/**
|
|
134
|
+
* drop zone 내부 콘텐츠
|
|
135
|
+
*/
|
|
136
|
+
children: ReactNode;
|
|
137
|
+
/**
|
|
138
|
+
* 파일 변경 콜백
|
|
139
|
+
*/
|
|
140
|
+
onFilesChange: InputFileChangeHandler;
|
|
141
|
+
/**
|
|
142
|
+
* drop 비활성화 여부
|
|
143
|
+
*/
|
|
144
|
+
disabled?: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Input Types; File Upload dialog open 핸들러
|
|
149
|
+
*/
|
|
150
|
+
export type InputFileOpenDialogHandler = (
|
|
151
|
+
event?: SyntheticEvent<HTMLElement>,
|
|
152
|
+
) => void;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Input Types; File Upload 버튼 props
|
|
156
|
+
* @property {InputFileOpenDialogHandler} [onOpenFileDialog] file dialog open 핸들러
|
|
157
|
+
* @property {ButtonProps} ... Button.Default props 전체
|
|
158
|
+
*/
|
|
159
|
+
export interface InputFileUploadButtonProps extends ButtonProps {
|
|
160
|
+
/**
|
|
161
|
+
* file dialog open 핸들러
|
|
162
|
+
*/
|
|
163
|
+
onOpenFileDialog?: InputFileOpenDialogHandler;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Input Types; File UploadedChip 컴포넌트 props
|
|
168
|
+
* @property {string} [fileId] 업로드 엔트리 id
|
|
169
|
+
* @property {string} fileName 업로드된 파일명
|
|
170
|
+
* @property {(fileId: string) => void} [onRemoveFromInput] input file 컨텍스트 제거 핸들러
|
|
171
|
+
* @property {() => void} [onClearSelectedFiles] fileId가 없을 때 선택 파일 초기화 핸들러
|
|
172
|
+
* @property {(event: MouseEvent<HTMLButtonElement>) => void} [onRemove] 파일 제거 핸들러
|
|
173
|
+
* @property {string} [removeButtonLabel] 제거 버튼 aria-label
|
|
174
|
+
*/
|
|
175
|
+
export interface InputFileUploadedChipProps extends Omit<
|
|
176
|
+
ChipInputProps,
|
|
177
|
+
"kind" | "children" | "onRemove"
|
|
178
|
+
> {
|
|
179
|
+
/**
|
|
180
|
+
* 업로드 엔트리 id
|
|
181
|
+
*/
|
|
182
|
+
fileId?: string;
|
|
183
|
+
/**
|
|
184
|
+
* 업로드된 파일명
|
|
185
|
+
*/
|
|
186
|
+
fileName: string;
|
|
187
|
+
/**
|
|
188
|
+
* input file 컨텍스트 제거 핸들러
|
|
189
|
+
*/
|
|
190
|
+
onRemoveFromInput?: (fileId: string) => void;
|
|
191
|
+
/**
|
|
192
|
+
* fileId가 없을 때 선택 파일 초기화 핸들러
|
|
193
|
+
*/
|
|
194
|
+
onClearSelectedFiles?: () => void;
|
|
195
|
+
/**
|
|
196
|
+
* 파일 제거 핸들러
|
|
197
|
+
*/
|
|
198
|
+
onRemove?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Input Types; File Upload List Container 컴포넌트 props
|
|
203
|
+
* @property {ReactNode} children list 하위 슬롯 콘텐츠
|
|
204
|
+
* @property {boolean} [collapsible] 헤더 트리거 기반 접기/펼치기 활성화 여부
|
|
205
|
+
* @property {boolean} [defaultCollapsed] uncontrolled 초기 접힘 상태
|
|
206
|
+
* @property {boolean} [collapsed] controlled 접힘 상태
|
|
207
|
+
* @property {(collapsed: boolean) => void} [onCollapsedChange] 접힘 상태 변경 콜백
|
|
208
|
+
*/
|
|
209
|
+
export interface InputFileListContainerProps extends ComponentPropsWithoutRef<"section"> {
|
|
210
|
+
/**
|
|
211
|
+
* list 하위 슬롯 콘텐츠
|
|
212
|
+
*/
|
|
213
|
+
children: ReactNode;
|
|
214
|
+
/**
|
|
215
|
+
* 헤더 트리거 기반 접기/펼치기 활성화 여부
|
|
216
|
+
*/
|
|
217
|
+
collapsible?: boolean;
|
|
218
|
+
/**
|
|
219
|
+
* uncontrolled 초기 접힘 상태
|
|
220
|
+
*/
|
|
221
|
+
defaultCollapsed?: boolean;
|
|
222
|
+
/**
|
|
223
|
+
* controlled 접힘 상태
|
|
224
|
+
*/
|
|
225
|
+
collapsed?: boolean;
|
|
226
|
+
/**
|
|
227
|
+
* 접힘 상태 변경 콜백
|
|
228
|
+
*/
|
|
229
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Input Types; File Upload List Header 컴포넌트 props
|
|
234
|
+
* @property {ReactNode} children 헤더 콘텐츠
|
|
235
|
+
* @property {boolean} [asTrigger] 클릭/키보드 트리거로 접기/펼치기 제어 여부
|
|
236
|
+
*/
|
|
237
|
+
export interface InputFileListHeaderProps extends ComponentPropsWithoutRef<"div"> {
|
|
238
|
+
/**
|
|
239
|
+
* 헤더 콘텐츠
|
|
240
|
+
*/
|
|
241
|
+
children: ReactNode;
|
|
242
|
+
/**
|
|
243
|
+
* 클릭/키보드 트리거로 접기/펼치기 제어 여부
|
|
244
|
+
*/
|
|
245
|
+
asTrigger?: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Input Types; File Upload List Body 컴포넌트 props
|
|
250
|
+
* @property {ReactNode} children 목록 콘텐츠
|
|
251
|
+
* @property {boolean} [forceMount] 접힘 상태에서도 DOM 유지 여부
|
|
252
|
+
*/
|
|
253
|
+
export interface InputFileListBodyProps extends ComponentPropsWithoutRef<"div"> {
|
|
254
|
+
/**
|
|
255
|
+
* 목록 콘텐츠
|
|
256
|
+
*/
|
|
257
|
+
children: ReactNode;
|
|
258
|
+
/**
|
|
259
|
+
* 접힘 상태에서도 DOM 유지 여부
|
|
260
|
+
*/
|
|
261
|
+
forceMount?: boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Input Types; File Upload List Item 컴포넌트 props
|
|
266
|
+
* @property {ReactNode} children 파일 항목 콘텐츠
|
|
267
|
+
*/
|
|
268
|
+
export interface InputFileListItemProps extends ComponentPropsWithoutRef<"div"> {
|
|
269
|
+
/**
|
|
270
|
+
* 파일 항목 콘텐츠
|
|
271
|
+
*/
|
|
272
|
+
children: ReactNode;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Input Types; File Upload List Remove 컴포넌트 props
|
|
277
|
+
* @property {ReactNode} [children] 제거 버튼 콘텐츠
|
|
278
|
+
* @property {(event: MouseEvent<HTMLButtonElement>) => void} [onRemove] 제거 핸들러
|
|
279
|
+
*/
|
|
280
|
+
export interface InputFileListRemoveProps extends Omit<
|
|
281
|
+
ComponentPropsWithoutRef<"button">,
|
|
282
|
+
"onClick"
|
|
283
|
+
> {
|
|
284
|
+
/**
|
|
285
|
+
* 제거 버튼 콘텐츠
|
|
286
|
+
*/
|
|
287
|
+
children?: ReactNode;
|
|
288
|
+
/**
|
|
289
|
+
* 제거 핸들러
|
|
290
|
+
*/
|
|
291
|
+
onRemove?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Input Types; File Upload List 컨텍스트 값 타입
|
|
296
|
+
* @property {boolean} collapsible 헤더 트리거 기반 접기/펼치기 활성화 여부
|
|
297
|
+
* @property {boolean} collapsed 현재 접힘 상태
|
|
298
|
+
* @property {() => void} onToggleCollapsed 접힘 상태 토글 핸들러
|
|
299
|
+
*/
|
|
300
|
+
export interface InputFileListContextValue {
|
|
301
|
+
/**
|
|
302
|
+
* 헤더 트리거 기반 접기/펼치기 활성화 여부
|
|
303
|
+
*/
|
|
304
|
+
collapsible: boolean;
|
|
305
|
+
/**
|
|
306
|
+
* 현재 접힘 상태
|
|
307
|
+
*/
|
|
308
|
+
collapsed: boolean;
|
|
309
|
+
/**
|
|
310
|
+
* 접힘 상태 토글 핸들러
|
|
311
|
+
*/
|
|
312
|
+
onToggleCollapsed: () => void;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Input Types; File Upload 컨텍스트 merge 전략
|
|
317
|
+
*/
|
|
318
|
+
export type InputFileContextMergeMode = "replace" | "append";
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Input Types; File Upload 엔트리 타입
|
|
322
|
+
* @property {string} id 파일 엔트리 고유 id
|
|
323
|
+
* @property {File} file 원본 File 객체
|
|
324
|
+
*/
|
|
325
|
+
export interface InputFileEntry {
|
|
326
|
+
/**
|
|
327
|
+
* 파일 엔트리 고유 id
|
|
328
|
+
*/
|
|
329
|
+
id: string;
|
|
330
|
+
/**
|
|
331
|
+
* 원본 File 객체
|
|
332
|
+
*/
|
|
333
|
+
file: File;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Input Types; File Upload 메타데이터 타입
|
|
338
|
+
* @property {string} id 파일 엔트리 고유 id
|
|
339
|
+
* @property {string} title 파일 제목(확장자 제외)
|
|
340
|
+
* @property {string} name 파일명(확장자 포함)
|
|
341
|
+
* @property {number} size 파일 크기(Byte)
|
|
342
|
+
* @property {string} sizeLabel 사람이 읽기 쉬운 파일 크기 문자열
|
|
343
|
+
* @property {string} format 파일 MIME type 또는 확장자 fallback
|
|
344
|
+
* @property {string} extension 파일 확장자(소문자)
|
|
345
|
+
* @property {number} lastModified 마지막 수정 시간(ms)
|
|
346
|
+
*/
|
|
347
|
+
export interface InputFileMeta {
|
|
348
|
+
/**
|
|
349
|
+
* 파일 엔트리 고유 id
|
|
350
|
+
*/
|
|
351
|
+
id: string;
|
|
352
|
+
/**
|
|
353
|
+
* 파일 제목(확장자 제외)
|
|
354
|
+
*/
|
|
355
|
+
title: string;
|
|
356
|
+
/**
|
|
357
|
+
* 파일명(확장자 포함)
|
|
358
|
+
*/
|
|
359
|
+
name: string;
|
|
360
|
+
/**
|
|
361
|
+
* 파일 크기(Byte)
|
|
362
|
+
*/
|
|
363
|
+
size: number;
|
|
364
|
+
/**
|
|
365
|
+
* 사람이 읽기 쉬운 파일 크기 문자열
|
|
366
|
+
*/
|
|
367
|
+
sizeLabel: string;
|
|
368
|
+
/**
|
|
369
|
+
* 파일 MIME type 또는 확장자 fallback
|
|
370
|
+
*/
|
|
371
|
+
format: string;
|
|
372
|
+
/**
|
|
373
|
+
* 파일 확장자(소문자)
|
|
374
|
+
*/
|
|
375
|
+
extension: string;
|
|
376
|
+
/**
|
|
377
|
+
* 마지막 수정 시간(ms)
|
|
378
|
+
*/
|
|
379
|
+
lastModified: number;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Input Types; File Upload 컨텍스트 Hook 옵션
|
|
384
|
+
* @property {File[]} [initialFiles] 초기 파일 목록
|
|
385
|
+
* @property {InputFileContextMergeMode} [mergeMode="append"] 파일 합치기 전략
|
|
386
|
+
*/
|
|
387
|
+
export interface UseInputFileContextOptions {
|
|
388
|
+
/**
|
|
389
|
+
* 초기 파일 목록
|
|
390
|
+
*/
|
|
391
|
+
initialFiles?: File[];
|
|
392
|
+
/**
|
|
393
|
+
* 파일 합치기 전략
|
|
394
|
+
*/
|
|
395
|
+
mergeMode?: InputFileContextMergeMode;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Input Types; File Upload FormData 생성 옵션
|
|
400
|
+
* @property {string} [fieldName="files"] FormData에 append할 필드 이름
|
|
401
|
+
*/
|
|
402
|
+
export interface InputFileFormDataOptions {
|
|
403
|
+
/**
|
|
404
|
+
* FormData에 append할 필드 이름
|
|
405
|
+
*/
|
|
406
|
+
fieldName?: string;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Input Types; File Upload RHF 통합 Hook 옵션
|
|
411
|
+
* @property {(event: ChangeEvent<HTMLInputElement>) => void} onInputFileChange 파일 입력 핸들러
|
|
412
|
+
* @property {RefObject<HTMLInputElement | null>} [inputRef] file input ref
|
|
413
|
+
* @property {UseFormRegisterReturn} [register] react-hook-form register 반환값
|
|
414
|
+
*/
|
|
415
|
+
export interface UseInputFileRHFOptions {
|
|
416
|
+
/**
|
|
417
|
+
* 파일 입력 핸들러
|
|
418
|
+
*/
|
|
419
|
+
onInputFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
420
|
+
/**
|
|
421
|
+
* file input ref
|
|
422
|
+
*/
|
|
423
|
+
inputRef?: RefObject<HTMLInputElement | null>;
|
|
424
|
+
/**
|
|
425
|
+
* react-hook-form register 반환값
|
|
426
|
+
*/
|
|
427
|
+
register?: UseFormRegisterReturn;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Input Types; File Upload RHF 통합 input bind props
|
|
432
|
+
* @property {string} [name] input name
|
|
433
|
+
* @property {(node: HTMLInputElement | null) => void} ref register/ref 병합 함수
|
|
434
|
+
* @property {(event: ChangeEvent<HTMLInputElement>) => void} onChange register + file handler 병합 함수
|
|
435
|
+
* @property {(event: FocusEvent<HTMLInputElement>) => void} [onBlur] register onBlur 전달 함수
|
|
436
|
+
*/
|
|
437
|
+
export interface InputFileRHFBindProps {
|
|
438
|
+
/**
|
|
439
|
+
* input name
|
|
440
|
+
*/
|
|
441
|
+
name?: string;
|
|
442
|
+
/**
|
|
443
|
+
* register/ref 병합 함수
|
|
444
|
+
*/
|
|
445
|
+
ref: (node: HTMLInputElement | null) => void;
|
|
446
|
+
/**
|
|
447
|
+
* register + file handler 병합 함수
|
|
448
|
+
*/
|
|
449
|
+
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
450
|
+
/**
|
|
451
|
+
* register onBlur 전달 함수
|
|
452
|
+
*/
|
|
453
|
+
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Input Types; File Upload RHF 통합 Hook 반환값
|
|
458
|
+
* @property {InputFileRHFBindProps} inputBindProps file input에 spread할 bind props
|
|
459
|
+
*/
|
|
460
|
+
export interface UseInputFileRHFResult {
|
|
461
|
+
/**
|
|
462
|
+
* file input에 spread할 bind props
|
|
463
|
+
*/
|
|
464
|
+
inputBindProps: InputFileRHFBindProps;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Input Types; File Upload 컨텍스트 Hook 반환값
|
|
469
|
+
* @property {InputFileEntry[]} entries 파일 엔트리 목록
|
|
470
|
+
* @property {File[]} files 원본 파일 목록
|
|
471
|
+
* @property {InputFileMeta[]} fileMetas 파일 메타데이터 목록
|
|
472
|
+
* @property {boolean} hasFiles 파일 존재 여부
|
|
473
|
+
* @property {number} fileCount 파일 개수
|
|
474
|
+
* @property {InputFileChangeHandler} onSyncFromInputFile useInputFile onFilesChange 연결용 핸들러
|
|
475
|
+
* @property {(files: File[]) => void} onReplaceFiles 파일 전체 교체 핸들러
|
|
476
|
+
* @property {(files: File[]) => void} onAppendFiles 파일 추가 핸들러
|
|
477
|
+
* @property {(id: string) => void} onRemoveFileById id 기준 파일 제거 핸들러
|
|
478
|
+
* @property {(index: number) => void} onRemoveFileByIndex index 기준 파일 제거 핸들러
|
|
479
|
+
* @property {() => void} onClearFiles 파일 전체 제거 핸들러
|
|
480
|
+
* @property {(options?: InputFileFormDataOptions) => FormData} toFormData 컨텍스트 파일 목록을 FormData로 직렬화하는 헬퍼
|
|
481
|
+
*/
|
|
482
|
+
export interface UseInputFileContextResult {
|
|
483
|
+
/**
|
|
484
|
+
* 파일 엔트리 목록
|
|
485
|
+
*/
|
|
486
|
+
entries: InputFileEntry[];
|
|
487
|
+
/**
|
|
488
|
+
* 원본 파일 목록
|
|
489
|
+
*/
|
|
490
|
+
files: File[];
|
|
491
|
+
/**
|
|
492
|
+
* 파일 메타데이터 목록
|
|
493
|
+
*/
|
|
494
|
+
fileMetas: InputFileMeta[];
|
|
495
|
+
/**
|
|
496
|
+
* 파일 존재 여부
|
|
497
|
+
*/
|
|
498
|
+
hasFiles: boolean;
|
|
499
|
+
/**
|
|
500
|
+
* 파일 개수
|
|
501
|
+
*/
|
|
502
|
+
fileCount: number;
|
|
503
|
+
/**
|
|
504
|
+
* useInputFile onFilesChange 연결용 핸들러
|
|
505
|
+
*/
|
|
506
|
+
onSyncFromInputFile: InputFileChangeHandler;
|
|
507
|
+
/**
|
|
508
|
+
* 파일 전체 교체 핸들러
|
|
509
|
+
*/
|
|
510
|
+
onReplaceFiles: (files: File[]) => void;
|
|
511
|
+
/**
|
|
512
|
+
* 파일 추가 핸들러
|
|
513
|
+
*/
|
|
514
|
+
onAppendFiles: (files: File[]) => void;
|
|
515
|
+
/**
|
|
516
|
+
* id 기준 파일 제거 핸들러
|
|
517
|
+
*/
|
|
518
|
+
onRemoveFileById: (id: string) => void;
|
|
519
|
+
/**
|
|
520
|
+
* index 기준 파일 제거 핸들러
|
|
521
|
+
*/
|
|
522
|
+
onRemoveFileByIndex: (index: number) => void;
|
|
523
|
+
/**
|
|
524
|
+
* 파일 전체 제거 핸들러
|
|
525
|
+
*/
|
|
526
|
+
onClearFiles: () => void;
|
|
527
|
+
/**
|
|
528
|
+
* 컨텍스트 파일 목록을 FormData로 직렬화하는 헬퍼
|
|
529
|
+
*/
|
|
530
|
+
toFormData: (options?: InputFileFormDataOptions) => FormData;
|
|
531
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InputFileEntry,
|
|
3
|
+
InputFileFormDataOptions,
|
|
4
|
+
InputFileMeta,
|
|
5
|
+
} from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Input Utils; File Upload 확장자 추출 유틸
|
|
9
|
+
* @param {File} file 파일 객체
|
|
10
|
+
* @returns {string} 소문자 확장자
|
|
11
|
+
*/
|
|
12
|
+
export const resolveInputFileExtension = (file: File): string => {
|
|
13
|
+
const fileName = file.name ?? "";
|
|
14
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
15
|
+
if (dotIndex < 0 || dotIndex === fileName.length - 1) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
return fileName.slice(dotIndex + 1).toLowerCase();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Input Utils; File Upload 제목 추출 유틸
|
|
23
|
+
* @param {File} file 파일 객체
|
|
24
|
+
* @returns {string} 확장자를 제외한 파일 제목
|
|
25
|
+
*/
|
|
26
|
+
export const resolveInputFileTitle = (file: File): string => {
|
|
27
|
+
const fileName = file.name ?? "";
|
|
28
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
29
|
+
if (dotIndex <= 0) {
|
|
30
|
+
return fileName;
|
|
31
|
+
}
|
|
32
|
+
return fileName.slice(0, dotIndex);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Input Utils; File Upload 포맷 추출 유틸
|
|
37
|
+
* @param {File} file 파일 객체
|
|
38
|
+
* @returns {string} MIME type 또는 확장자 기반 fallback
|
|
39
|
+
*/
|
|
40
|
+
export const resolveInputFileFormat = (file: File): string => {
|
|
41
|
+
if (file.type.length > 0) {
|
|
42
|
+
return file.type;
|
|
43
|
+
}
|
|
44
|
+
const extension = resolveInputFileExtension(file);
|
|
45
|
+
return extension.length > 0 ? extension : "unknown";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Input Utils; File Upload 용량 포맷 유틸
|
|
50
|
+
* @param {number} size 파일 크기(Byte)
|
|
51
|
+
* @returns {string} 사람이 읽기 쉬운 용량 문자열
|
|
52
|
+
*/
|
|
53
|
+
export const formatInputFileSize = (size: number): string => {
|
|
54
|
+
if (size < 1024) {
|
|
55
|
+
return `${size} B`;
|
|
56
|
+
}
|
|
57
|
+
if (size < 1024 * 1024) {
|
|
58
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
59
|
+
}
|
|
60
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Input Utils; File Upload 메타데이터 생성 유틸
|
|
65
|
+
* @param {InputFileEntry} entry 파일 엔트리
|
|
66
|
+
* @returns {InputFileMeta} 파일 메타데이터
|
|
67
|
+
*/
|
|
68
|
+
export const createInputFileMeta = (entry: InputFileEntry): InputFileMeta => {
|
|
69
|
+
const { file, id } = entry;
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
title: resolveInputFileTitle(file),
|
|
73
|
+
name: file.name,
|
|
74
|
+
size: file.size,
|
|
75
|
+
sizeLabel: formatInputFileSize(file.size),
|
|
76
|
+
format: resolveInputFileFormat(file),
|
|
77
|
+
extension: resolveInputFileExtension(file),
|
|
78
|
+
lastModified: file.lastModified,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Input Utils; File Upload FormData 직렬화 유틸
|
|
84
|
+
* @param {File[]} files 원본 파일 목록
|
|
85
|
+
* @param {InputFileFormDataOptions} [options] FormData 직렬화 옵션
|
|
86
|
+
* @param {string} [options.fieldName="files"] FormData append 필드명
|
|
87
|
+
* @returns {FormData} 파일이 append된 FormData
|
|
88
|
+
* @example
|
|
89
|
+
* const formData = createInputFileFormData(fileContext.files, {
|
|
90
|
+
* fieldName: "attach_files",
|
|
91
|
+
* });
|
|
92
|
+
*/
|
|
93
|
+
export const createInputFileFormData = (
|
|
94
|
+
files: File[],
|
|
95
|
+
options?: InputFileFormDataOptions,
|
|
96
|
+
): FormData => {
|
|
97
|
+
const formData = new FormData();
|
|
98
|
+
const fieldName = options?.fieldName ?? "files";
|
|
99
|
+
files.forEach(file => {
|
|
100
|
+
formData.append(fieldName, file);
|
|
101
|
+
});
|
|
102
|
+
return formData;
|
|
103
|
+
};
|