@ehfuse/mui-virtual-data-table 1.0.8 → 1.0.10
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.esm.js +7 -2014
- package/dist/index.esm.js.map +7 -1
- package/dist/index.js +7 -2016
- package/dist/index.js.map +7 -1
- package/package.json +6 -9
package/dist/index.esm.js
CHANGED
|
@@ -1,1160 +1,4 @@
|
|
|
1
|
-
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { useRef, useState, useEffect, forwardRef, useMemo, useCallback, useImperativeHandle, useLayoutEffect, memo } from 'react';
|
|
3
|
-
import { Box, CircularProgress, TableContainer, TableRow, TableCell, TableSortLabel, TableBody, TableHead, Table, Typography, Paper } from '@mui/material';
|
|
4
|
-
import { TableVirtuoso } from 'react-virtuoso';
|
|
5
|
-
|
|
6
|
-
/******************************************************************************
|
|
7
|
-
Copyright (c) Microsoft Corporation.
|
|
8
|
-
|
|
9
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
10
|
-
purpose with or without fee is hereby granted.
|
|
11
|
-
|
|
12
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
13
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
14
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
15
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
16
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
17
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
18
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
19
|
-
***************************************************************************** */
|
|
20
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
var __assign = function() {
|
|
24
|
-
__assign = Object.assign || function __assign(t) {
|
|
25
|
-
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
26
|
-
s = arguments[i];
|
|
27
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
|
|
28
|
-
}
|
|
29
|
-
return t;
|
|
30
|
-
};
|
|
31
|
-
return __assign.apply(this, arguments);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
35
|
-
var e = new Error(message);
|
|
36
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function LoadingProgress(_a) {
|
|
40
|
-
var visible = _a.visible, onComplete = _a.onComplete, _b = _a.fadeoutDuration, fadeoutDuration = _b === void 0 ? 200 : _b, _c = _a.exitDelay, exitDelay = _c === void 0 ? 0 : _c, _d = _a.size, size = _d === void 0 ? 40 : _d, className = _a.className, style = _a.style, sx = _a.sx, indicator = _a.indicator, _e = _a.background, background = _e === void 0 ? {
|
|
41
|
-
show: true,
|
|
42
|
-
color: "255, 255, 255",
|
|
43
|
-
opacity: 0.9,
|
|
44
|
-
} : _e;
|
|
45
|
-
var prevVisibleRef = useRef(undefined);
|
|
46
|
-
var boxRef = useRef(null);
|
|
47
|
-
var _f = useState(visible), shouldRender = _f[0], setShouldRender = _f[1];
|
|
48
|
-
// 배경 설정 기본값 처리
|
|
49
|
-
var _g = background.show, showBackground = _g === void 0 ? true : _g, _h = background.color, backgroundColor = _h === void 0 ? "255, 255, 255" : _h, _j = background.opacity, backgroundOpacity = _j === void 0 ? 0.9 : _j;
|
|
50
|
-
useEffect(function () {
|
|
51
|
-
var prevVisible = prevVisibleRef.current;
|
|
52
|
-
// visible이 true에서 false로 변경될 때 페이드아웃 후 onComplete 호출
|
|
53
|
-
if (prevVisible === true && visible === false && boxRef.current) {
|
|
54
|
-
// CSS transition으로 페이드아웃 시작
|
|
55
|
-
var box = boxRef.current;
|
|
56
|
-
box.style.opacity = "0";
|
|
57
|
-
// fadeoutDuration + exitDelay 후 onComplete 호출 및 언마운트
|
|
58
|
-
setTimeout(function () {
|
|
59
|
-
setShouldRender(false); // 페이드아웃 완료 후 언마운트
|
|
60
|
-
onComplete === null || onComplete === void 0 ? void 0 : onComplete();
|
|
61
|
-
}, fadeoutDuration + exitDelay);
|
|
62
|
-
}
|
|
63
|
-
else if (visible === true) {
|
|
64
|
-
// visible이 true가 되면 다시 렌더링하고 opacity 복원
|
|
65
|
-
setShouldRender(true);
|
|
66
|
-
if (boxRef.current) {
|
|
67
|
-
boxRef.current.style.opacity = "1";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// 현재 visible 값을 ref에 저장
|
|
71
|
-
prevVisibleRef.current = visible;
|
|
72
|
-
}, [visible, onComplete, fadeoutDuration, exitDelay]);
|
|
73
|
-
if (!shouldRender) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
// indicator 렌더링 로직
|
|
77
|
-
var renderIndicator = function () {
|
|
78
|
-
// string이면 이미지 URL로 간주
|
|
79
|
-
if (typeof indicator === "string") {
|
|
80
|
-
return (jsx("img", { src: indicator, alt: "Loading", style: { width: size, height: size } }));
|
|
81
|
-
}
|
|
82
|
-
// ReactNode가 제공되면 그대로 렌더링
|
|
83
|
-
if (indicator) {
|
|
84
|
-
return indicator;
|
|
85
|
-
}
|
|
86
|
-
// 기본값: MUI CircularProgress
|
|
87
|
-
return jsx(CircularProgress, { size: size });
|
|
88
|
-
};
|
|
89
|
-
// 배경색 처리: 다양한 색상 형식 지원
|
|
90
|
-
var getBackgroundStyle = function () {
|
|
91
|
-
if (!showBackground) {
|
|
92
|
-
return { backgroundColor: "transparent" };
|
|
93
|
-
}
|
|
94
|
-
// 이미 rgba 형식이면 그대로 사용
|
|
95
|
-
if (backgroundColor.startsWith("rgba")) {
|
|
96
|
-
return { backgroundColor: backgroundColor };
|
|
97
|
-
}
|
|
98
|
-
// RGB 문자열 형식 (예: "255, 255, 255")
|
|
99
|
-
if (/^\d+,\s*\d+,\s*\d+$/.test(backgroundColor)) {
|
|
100
|
-
return {
|
|
101
|
-
backgroundColor: "rgba(".concat(backgroundColor, ", ").concat(backgroundOpacity, ")"),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
// hex, rgb(), named color 등의 경우
|
|
105
|
-
// CSS에서 color와 opacity를 함께 사용
|
|
106
|
-
if (backgroundOpacity !== 1) {
|
|
107
|
-
return {
|
|
108
|
-
backgroundColor: backgroundColor,
|
|
109
|
-
// 배경에만 opacity를 적용하기 위해 before pseudo-element 사용하거나
|
|
110
|
-
// 직접 rgba로 변환하는 대신 CSS custom property 사용
|
|
111
|
-
"&::before": {
|
|
112
|
-
content: '""',
|
|
113
|
-
position: "absolute",
|
|
114
|
-
top: 0,
|
|
115
|
-
left: 0,
|
|
116
|
-
right: 0,
|
|
117
|
-
bottom: 0,
|
|
118
|
-
backgroundColor: backgroundColor,
|
|
119
|
-
opacity: backgroundOpacity,
|
|
120
|
-
zIndex: -1,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
// opacity가 1이면 그냥 색상만
|
|
125
|
-
return { backgroundColor: backgroundColor };
|
|
126
|
-
};
|
|
127
|
-
return (jsx(Box, { ref: boxRef, className: className, style: __assign({}, style), sx: __assign(__assign({ position: "absolute", top: 0, left: 0, right: 0, bottom: 0, display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, opacity: visible ? 1 : 0, transition: "opacity ".concat(fadeoutDuration, "ms ease-out") }, getBackgroundStyle()), sx), children: renderIndicator() }));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* MIT License
|
|
132
|
-
*
|
|
133
|
-
* Copyright (c) 2025 KIM YOUNG JIN (ehfuse@gmail.com)
|
|
134
|
-
*
|
|
135
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
136
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
137
|
-
* in the Software without restriction, including without limitation the rights
|
|
138
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
139
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
140
|
-
* furnished to do so, subject to the following conditions:
|
|
141
|
-
*
|
|
142
|
-
* The above copyright notice and this permission notice shall be included in all
|
|
143
|
-
* copies or substantial portions of the Software.
|
|
144
|
-
*
|
|
145
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
146
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
147
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
148
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
149
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
150
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
151
|
-
* SOFTWARE.
|
|
152
|
-
*/
|
|
153
|
-
// 드래그 스크롤을 제외할 클래스들 (자신 또는 부모 요소에서 확인)
|
|
154
|
-
const DEFAULT_EXCLUDE_CLASSES = [
|
|
155
|
-
// 기본 입력 요소들
|
|
156
|
-
"editor",
|
|
157
|
-
"textarea",
|
|
158
|
-
"input",
|
|
159
|
-
"select",
|
|
160
|
-
"textfield",
|
|
161
|
-
"form-control",
|
|
162
|
-
"contenteditable",
|
|
163
|
-
// Material-UI 컴포넌트들
|
|
164
|
-
"MuiInputBase-input",
|
|
165
|
-
"MuiSelect-select",
|
|
166
|
-
"MuiOutlinedInput-input",
|
|
167
|
-
"MuiFilledInput-input",
|
|
168
|
-
"MuiInput-input",
|
|
169
|
-
"MuiFormControl-root",
|
|
170
|
-
"MuiTextField-root",
|
|
171
|
-
"MuiSelect-root",
|
|
172
|
-
"MuiOutlinedInput-root",
|
|
173
|
-
"MuiFilledInput-root",
|
|
174
|
-
"MuiInput-root",
|
|
175
|
-
"MuiAutocomplete-input",
|
|
176
|
-
"MuiDatePicker-input",
|
|
177
|
-
"MuiSlider-thumb",
|
|
178
|
-
"MuiSlider-rail",
|
|
179
|
-
"MuiSlider-track",
|
|
180
|
-
"MuiSlider-mark",
|
|
181
|
-
"MuiSlider-markLabel",
|
|
182
|
-
"MuiSlider-root",
|
|
183
|
-
"MuiSlider-colorPrimary",
|
|
184
|
-
"MuiSlider-sizeMedium",
|
|
185
|
-
"MuiIconButton-root",
|
|
186
|
-
"MuiButton-root",
|
|
187
|
-
"MuiButtonBase-root",
|
|
188
|
-
"MuiTouchRipple-root",
|
|
189
|
-
"MuiCheckbox-root",
|
|
190
|
-
"MuiRadio-root",
|
|
191
|
-
"MuiSwitch-root",
|
|
192
|
-
"PrivateSwitchBase-root",
|
|
193
|
-
// Ant Design 컴포넌트들
|
|
194
|
-
"ant-input",
|
|
195
|
-
"ant-input-affix-wrapper",
|
|
196
|
-
"ant-input-group-addon",
|
|
197
|
-
"ant-input-number",
|
|
198
|
-
"ant-input-number-handler",
|
|
199
|
-
"ant-select",
|
|
200
|
-
"ant-select-selector",
|
|
201
|
-
"ant-select-selection-search",
|
|
202
|
-
"ant-select-dropdown",
|
|
203
|
-
"ant-cascader",
|
|
204
|
-
"ant-cascader-input",
|
|
205
|
-
"ant-picker",
|
|
206
|
-
"ant-picker-input",
|
|
207
|
-
"ant-time-picker",
|
|
208
|
-
"ant-calendar-picker",
|
|
209
|
-
"ant-slider",
|
|
210
|
-
"ant-slider-track",
|
|
211
|
-
"ant-slider-handle",
|
|
212
|
-
"ant-switch",
|
|
213
|
-
"ant-checkbox",
|
|
214
|
-
"ant-checkbox-wrapper",
|
|
215
|
-
"ant-radio",
|
|
216
|
-
"ant-radio-wrapper",
|
|
217
|
-
"ant-rate",
|
|
218
|
-
"ant-upload",
|
|
219
|
-
"ant-upload-drag",
|
|
220
|
-
"ant-form-item",
|
|
221
|
-
"ant-form-item-control",
|
|
222
|
-
"ant-btn",
|
|
223
|
-
"ant-dropdown",
|
|
224
|
-
"ant-dropdown-trigger",
|
|
225
|
-
"ant-menu",
|
|
226
|
-
"ant-menu-item",
|
|
227
|
-
"ant-tooltip",
|
|
228
|
-
"ant-popover",
|
|
229
|
-
"ant-modal",
|
|
230
|
-
"ant-drawer",
|
|
231
|
-
"ant-tree-select",
|
|
232
|
-
"ant-auto-complete",
|
|
233
|
-
"ant-mentions",
|
|
234
|
-
"ant-transfer",
|
|
235
|
-
// Shadcn/ui 컴포넌트들
|
|
236
|
-
"ui-input",
|
|
237
|
-
"ui-textarea",
|
|
238
|
-
"ui-select",
|
|
239
|
-
"ui-select-trigger",
|
|
240
|
-
"ui-select-content",
|
|
241
|
-
"ui-select-item",
|
|
242
|
-
"ui-button",
|
|
243
|
-
"ui-checkbox",
|
|
244
|
-
"ui-radio-group",
|
|
245
|
-
"ui-switch",
|
|
246
|
-
"ui-slider",
|
|
247
|
-
"ui-range-slider",
|
|
248
|
-
"ui-calendar",
|
|
249
|
-
"ui-date-picker",
|
|
250
|
-
"ui-combobox",
|
|
251
|
-
"ui-command",
|
|
252
|
-
"ui-command-input",
|
|
253
|
-
"ui-popover",
|
|
254
|
-
"ui-dialog",
|
|
255
|
-
"ui-sheet",
|
|
256
|
-
"ui-dropdown-menu",
|
|
257
|
-
"ui-context-menu",
|
|
258
|
-
"ui-menubar",
|
|
259
|
-
"ui-navigation-menu",
|
|
260
|
-
"ui-form",
|
|
261
|
-
"ui-form-control",
|
|
262
|
-
"ui-form-item",
|
|
263
|
-
"ui-form-field",
|
|
264
|
-
"ui-label",
|
|
265
|
-
// Radix UI 기본 클래스들 (Shadcn 기반)
|
|
266
|
-
"radix-ui",
|
|
267
|
-
"radix-select",
|
|
268
|
-
"radix-dropdown",
|
|
269
|
-
"radix-dialog",
|
|
270
|
-
"radix-popover",
|
|
271
|
-
"radix-accordion",
|
|
272
|
-
"radix-tabs",
|
|
273
|
-
"radix-slider",
|
|
274
|
-
"radix-switch",
|
|
275
|
-
"radix-checkbox",
|
|
276
|
-
"radix-radio",
|
|
277
|
-
// Quill Editor
|
|
278
|
-
"ql-editor",
|
|
279
|
-
"ql-container",
|
|
280
|
-
"ql-toolbar",
|
|
281
|
-
"ql-picker",
|
|
282
|
-
"ql-picker-label",
|
|
283
|
-
"ql-picker-options",
|
|
284
|
-
"ql-formats",
|
|
285
|
-
"ql-snow",
|
|
286
|
-
"ql-bubble",
|
|
287
|
-
"quill",
|
|
288
|
-
"quilleditor",
|
|
289
|
-
// Monaco Editor
|
|
290
|
-
"monaco-editor",
|
|
291
|
-
"monaco-editor-background",
|
|
292
|
-
"view-lines",
|
|
293
|
-
"decorationsOverviewRuler",
|
|
294
|
-
"monaco-scrollable-element",
|
|
295
|
-
// CodeMirror
|
|
296
|
-
"CodeMirror",
|
|
297
|
-
"CodeMirror-code",
|
|
298
|
-
"CodeMirror-lines",
|
|
299
|
-
"CodeMirror-scroll",
|
|
300
|
-
"CodeMirror-sizer",
|
|
301
|
-
"cm-editor",
|
|
302
|
-
"cm-focused",
|
|
303
|
-
"cm-content",
|
|
304
|
-
// TinyMCE
|
|
305
|
-
"tox-editor-container",
|
|
306
|
-
"tox-editor-header",
|
|
307
|
-
"tox-edit-area",
|
|
308
|
-
"tox-tinymce",
|
|
309
|
-
"mce-content-body",
|
|
310
|
-
// CKEditor
|
|
311
|
-
"ck-editor",
|
|
312
|
-
"ck-content",
|
|
313
|
-
"ck-toolbar",
|
|
314
|
-
"ck-editor__editable",
|
|
315
|
-
"ck-widget",
|
|
316
|
-
// Slate.js
|
|
317
|
-
"slate-editor",
|
|
318
|
-
"slate-content",
|
|
319
|
-
// Draft.js
|
|
320
|
-
"DraftEditor-root",
|
|
321
|
-
"DraftEditor-editorContainer",
|
|
322
|
-
"public-DraftEditor-content",
|
|
323
|
-
// EhfuseEditor
|
|
324
|
-
"ehfuse-editor",
|
|
325
|
-
"ehfuse-editor-wrapper",
|
|
326
|
-
"ehfuse-editor-content",
|
|
327
|
-
"ehfuse-toolbar",
|
|
328
|
-
"ehfuse-toolbar-group",
|
|
329
|
-
"ehfuse-cursor",
|
|
330
|
-
// 기타 에디터들
|
|
331
|
-
"text-editor",
|
|
332
|
-
"rich-text-editor",
|
|
333
|
-
"wysiwyg",
|
|
334
|
-
"ace_editor",
|
|
335
|
-
"ace_content",
|
|
336
|
-
];
|
|
337
|
-
/**
|
|
338
|
-
* 드래그 스크롤이 허용되지 않는 요소들인지 확인
|
|
339
|
-
*/
|
|
340
|
-
/**
|
|
341
|
-
* 드래그 스크롤이 허용되지 않는 요소들인지 확인
|
|
342
|
-
*/
|
|
343
|
-
const isTextInputElement = (element, config) => {
|
|
344
|
-
const tagName = element.tagName.toLowerCase();
|
|
345
|
-
const inputTypes = [
|
|
346
|
-
"text",
|
|
347
|
-
"password",
|
|
348
|
-
"email",
|
|
349
|
-
"number",
|
|
350
|
-
"search",
|
|
351
|
-
"tel",
|
|
352
|
-
"url",
|
|
353
|
-
"checkbox",
|
|
354
|
-
"radio",
|
|
355
|
-
];
|
|
356
|
-
// input 태그이면서 텍스트 입력 타입이나 체크박스/라디오인 경우
|
|
357
|
-
if (tagName === "input") {
|
|
358
|
-
const type = element.type;
|
|
359
|
-
return inputTypes.includes(type);
|
|
360
|
-
}
|
|
361
|
-
// textarea, select, 편집 가능한 요소들
|
|
362
|
-
if (["textarea", "select", "button"].includes(tagName)) {
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
// SVG 요소들 (아이콘들)
|
|
366
|
-
if ([
|
|
367
|
-
"svg",
|
|
368
|
-
"path",
|
|
369
|
-
"circle",
|
|
370
|
-
"rect",
|
|
371
|
-
"line",
|
|
372
|
-
"polygon",
|
|
373
|
-
"polyline",
|
|
374
|
-
].includes(tagName)) {
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
// contenteditable 속성이 있는 요소
|
|
378
|
-
if (element.getAttribute("contenteditable") === "true") {
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
// 추가 셀렉터 체크
|
|
382
|
-
if (config === null || config === void 0 ? void 0 : config.excludeSelectors) {
|
|
383
|
-
for (const selector of config.excludeSelectors) {
|
|
384
|
-
if (element.matches(selector)) {
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return checkElementAndParents(element, config);
|
|
390
|
-
};
|
|
391
|
-
/**
|
|
392
|
-
* 자신 또는 부모 요소들을 확인하여 드래그 스크롤을 제외할 요소인지 판단
|
|
393
|
-
*/
|
|
394
|
-
const checkElementAndParents = (element, config) => {
|
|
395
|
-
// 모든 제외 클래스들 합치기 (기본 클래스 + 사용자 추가 클래스)
|
|
396
|
-
const allExcludeClasses = [
|
|
397
|
-
...DEFAULT_EXCLUDE_CLASSES,
|
|
398
|
-
...((config === null || config === void 0 ? void 0 : config.excludeClasses) || []),
|
|
399
|
-
];
|
|
400
|
-
let currentElement = element;
|
|
401
|
-
let depth = 0;
|
|
402
|
-
const maxDepth = 5; // 최대 5단계까지 부모 요소 확인
|
|
403
|
-
while (currentElement && depth <= maxDepth) {
|
|
404
|
-
// 현재 요소가 제외 클래스를 가지고 있는지 확인
|
|
405
|
-
if (allExcludeClasses.some((cls) => currentElement.classList.contains(cls))) {
|
|
406
|
-
return true;
|
|
407
|
-
}
|
|
408
|
-
// 다이얼로그 루트에 도달하면 중단
|
|
409
|
-
if (currentElement.classList.contains("MuiDialogContent-root")) {
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
currentElement = currentElement.parentElement;
|
|
413
|
-
depth++;
|
|
414
|
-
}
|
|
415
|
-
return false;
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
// 기본 설정 객체들을 컴포넌트 외부에 상수로 선언 (재렌더링 시 동일한 참조 유지)
|
|
419
|
-
const DEFAULT_THUMB_CONFIG = {};
|
|
420
|
-
const DEFAULT_TRACK_CONFIG = {};
|
|
421
|
-
const DEFAULT_ARROWS_CONFIG = {};
|
|
422
|
-
const DEFAULT_DRAG_SCROLL_CONFIG = {};
|
|
423
|
-
const DEFAULT_AUTO_HIDE_CONFIG = {};
|
|
424
|
-
const OverlayScrollbar = forwardRef(({ className = "", style = {}, containerStyle = {}, contentStyle = {}, children, onScroll,
|
|
425
|
-
// 그룹화된 설정 객체들
|
|
426
|
-
thumb = DEFAULT_THUMB_CONFIG, track = DEFAULT_TRACK_CONFIG, arrows = DEFAULT_ARROWS_CONFIG, dragScroll = DEFAULT_DRAG_SCROLL_CONFIG, autoHide = DEFAULT_AUTO_HIDE_CONFIG,
|
|
427
|
-
// 기타 설정들
|
|
428
|
-
showScrollbar = true, detectInnerScroll = false, }, ref) => {
|
|
429
|
-
// props 변경 추적용 ref
|
|
430
|
-
const prevPropsRef = useRef({});
|
|
431
|
-
// 렌더링 시 어떤 prop이 변경되었는지 체크
|
|
432
|
-
useEffect(() => {
|
|
433
|
-
// 현재 props 저장
|
|
434
|
-
prevPropsRef.current = {
|
|
435
|
-
children,
|
|
436
|
-
onScroll,
|
|
437
|
-
showScrollbar,
|
|
438
|
-
thumb,
|
|
439
|
-
track,
|
|
440
|
-
arrows,
|
|
441
|
-
dragScroll,
|
|
442
|
-
autoHide,
|
|
443
|
-
};
|
|
444
|
-
});
|
|
445
|
-
const wrapperRef = useRef(null);
|
|
446
|
-
const containerRef = useRef(null);
|
|
447
|
-
const contentRef = useRef(null);
|
|
448
|
-
const scrollbarRef = useRef(null);
|
|
449
|
-
const thumbRef = useRef(null);
|
|
450
|
-
// 스크롤 컨테이너 캐싱용 ref (성능 최적화)
|
|
451
|
-
const cachedScrollContainerRef = useRef(null);
|
|
452
|
-
// 기본 상태들
|
|
453
|
-
const [scrollbarVisible, setScrollbarVisible] = useState(false);
|
|
454
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
455
|
-
const [isThumbHovered, setIsThumbHovered] = useState(false);
|
|
456
|
-
const [dragStart, setDragStart] = useState({ y: 0, scrollTop: 0 });
|
|
457
|
-
const [thumbHeight, setThumbHeight] = useState(0);
|
|
458
|
-
const [thumbTop, setThumbTop] = useState(0);
|
|
459
|
-
const [hasScrollableContent, setHasScrollableContent] = useState(false);
|
|
460
|
-
// 드래그 스크롤 상태
|
|
461
|
-
const [isDragScrolling, setIsDragScrolling] = useState(false);
|
|
462
|
-
const [dragScrollStart, setDragScrollStart] = useState({
|
|
463
|
-
x: 0,
|
|
464
|
-
y: 0,
|
|
465
|
-
scrollTop: 0,
|
|
466
|
-
scrollLeft: 0,
|
|
467
|
-
});
|
|
468
|
-
const [activeArrow, setActiveArrow] = useState(null);
|
|
469
|
-
const [hoveredArrow, setHoveredArrow] = useState(null);
|
|
470
|
-
// 초기 마운트 시 hover 방지용
|
|
471
|
-
const [isInitialized, setIsInitialized] = useState(false);
|
|
472
|
-
// 휠 스크롤 감지용
|
|
473
|
-
const wheelTimeoutRef = useRef(null);
|
|
474
|
-
const [isWheelScrolling, setIsWheelScrolling] = useState(false);
|
|
475
|
-
// 숨김 타이머
|
|
476
|
-
const hideTimeoutRef = useRef(null);
|
|
477
|
-
// 그룹화된 설정 객체들에 기본값 설정
|
|
478
|
-
const finalThumbConfig = useMemo(() => {
|
|
479
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
480
|
-
const baseColor = (_a = thumb.color) !== null && _a !== void 0 ? _a : "#606060";
|
|
481
|
-
return {
|
|
482
|
-
width: (_b = thumb.width) !== null && _b !== void 0 ? _b : 8,
|
|
483
|
-
minHeight: (_c = thumb.minHeight) !== null && _c !== void 0 ? _c : 50,
|
|
484
|
-
radius: (_d = thumb.radius) !== null && _d !== void 0 ? _d : ((_e = thumb.width) !== null && _e !== void 0 ? _e : 8) / 2,
|
|
485
|
-
color: baseColor,
|
|
486
|
-
opacity: (_f = thumb.opacity) !== null && _f !== void 0 ? _f : 0.6,
|
|
487
|
-
hoverColor: (_g = thumb.hoverColor) !== null && _g !== void 0 ? _g : baseColor,
|
|
488
|
-
hoverOpacity: (_h = thumb.hoverOpacity) !== null && _h !== void 0 ? _h : 1.0,
|
|
489
|
-
};
|
|
490
|
-
}, [thumb]);
|
|
491
|
-
const finalTrackConfig = useMemo(() => {
|
|
492
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
493
|
-
return ({
|
|
494
|
-
width: (_a = track.width) !== null && _a !== void 0 ? _a : 16,
|
|
495
|
-
color: (_b = track.color) !== null && _b !== void 0 ? _b : "rgba(128, 128, 128, 0.1)",
|
|
496
|
-
visible: (_c = track.visible) !== null && _c !== void 0 ? _c : true,
|
|
497
|
-
alignment: (_d = track.alignment) !== null && _d !== void 0 ? _d : "center",
|
|
498
|
-
radius: (_f = (_e = track.radius) !== null && _e !== void 0 ? _e : finalThumbConfig.radius) !== null && _f !== void 0 ? _f : 4,
|
|
499
|
-
margin: (_g = track.margin) !== null && _g !== void 0 ? _g : 4,
|
|
500
|
-
});
|
|
501
|
-
}, [track, finalThumbConfig.radius]);
|
|
502
|
-
const finalArrowsConfig = useMemo(() => {
|
|
503
|
-
var _a, _b, _c, _d, _e, _f;
|
|
504
|
-
const baseColor = (_a = arrows.color) !== null && _a !== void 0 ? _a : "#808080";
|
|
505
|
-
return {
|
|
506
|
-
visible: (_b = arrows.visible) !== null && _b !== void 0 ? _b : false,
|
|
507
|
-
step: (_c = arrows.step) !== null && _c !== void 0 ? _c : 50,
|
|
508
|
-
color: baseColor,
|
|
509
|
-
opacity: (_d = arrows.opacity) !== null && _d !== void 0 ? _d : 0.6,
|
|
510
|
-
hoverColor: (_e = arrows.hoverColor) !== null && _e !== void 0 ? _e : baseColor,
|
|
511
|
-
hoverOpacity: (_f = arrows.hoverOpacity) !== null && _f !== void 0 ? _f : 1.0,
|
|
512
|
-
};
|
|
513
|
-
}, [arrows]);
|
|
514
|
-
const finalDragScrollConfig = useMemo(() => {
|
|
515
|
-
var _a, _b, _c;
|
|
516
|
-
return ({
|
|
517
|
-
enabled: (_a = dragScroll.enabled) !== null && _a !== void 0 ? _a : true,
|
|
518
|
-
excludeClasses: (_b = dragScroll.excludeClasses) !== null && _b !== void 0 ? _b : [],
|
|
519
|
-
excludeSelectors: (_c = dragScroll.excludeSelectors) !== null && _c !== void 0 ? _c : [],
|
|
520
|
-
});
|
|
521
|
-
}, [dragScroll]);
|
|
522
|
-
const finalAutoHideConfig = useMemo(() => {
|
|
523
|
-
var _a, _b, _c;
|
|
524
|
-
return ({
|
|
525
|
-
enabled: (_a = autoHide.enabled) !== null && _a !== void 0 ? _a : true,
|
|
526
|
-
delay: (_b = autoHide.delay) !== null && _b !== void 0 ? _b : 1500,
|
|
527
|
-
delayOnWheel: (_c = autoHide.delayOnWheel) !== null && _c !== void 0 ? _c : 700,
|
|
528
|
-
});
|
|
529
|
-
}, [autoHide]);
|
|
530
|
-
// 호환성을 위한 변수들 (자주 사용되는 변수들만 유지)
|
|
531
|
-
const finalThumbWidth = finalThumbConfig.width;
|
|
532
|
-
const finalTrackWidth = finalTrackConfig.width;
|
|
533
|
-
const thumbMinHeight = finalThumbConfig.minHeight;
|
|
534
|
-
const showArrows = finalArrowsConfig.visible;
|
|
535
|
-
const arrowStep = finalArrowsConfig.step;
|
|
536
|
-
// 포커스 유지 함수 (키보드 입력이 계속 작동하도록)
|
|
537
|
-
const maintainFocus = useCallback(() => {
|
|
538
|
-
if (!containerRef.current)
|
|
539
|
-
return;
|
|
540
|
-
// 현재 포커스된 요소 확인
|
|
541
|
-
const activeElement = document.activeElement;
|
|
542
|
-
// 오버레이 스크롤바 내부에 이미 포커스된 요소가 있으면 스킵
|
|
543
|
-
if (activeElement &&
|
|
544
|
-
containerRef.current.contains(activeElement) &&
|
|
545
|
-
activeElement !== containerRef.current) {
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
// 포커스된 요소가 없거나 외부에 있으면 컨테이너에 포커스
|
|
549
|
-
containerRef.current.focus();
|
|
550
|
-
}, []);
|
|
551
|
-
// ref를 통해 외부에서 스크롤 컨테이너에 접근할 수 있도록 함
|
|
552
|
-
useImperativeHandle(ref, () => ({
|
|
553
|
-
getScrollContainer: () => containerRef.current,
|
|
554
|
-
scrollTo: (options) => {
|
|
555
|
-
if (containerRef.current) {
|
|
556
|
-
containerRef.current.scrollTo(options);
|
|
557
|
-
}
|
|
558
|
-
},
|
|
559
|
-
get scrollTop() {
|
|
560
|
-
var _a;
|
|
561
|
-
return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) || 0;
|
|
562
|
-
},
|
|
563
|
-
get scrollHeight() {
|
|
564
|
-
var _a;
|
|
565
|
-
return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight) || 0;
|
|
566
|
-
},
|
|
567
|
-
get clientHeight() {
|
|
568
|
-
var _a;
|
|
569
|
-
return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
|
|
570
|
-
},
|
|
571
|
-
}), []);
|
|
572
|
-
// 실제 스크롤 가능한 요소 찾기 (캐싱 최적화)
|
|
573
|
-
const findScrollableElement = useCallback(() => {
|
|
574
|
-
// 캐시된 요소가 여전히 유효한지 확인
|
|
575
|
-
if (cachedScrollContainerRef.current) {
|
|
576
|
-
const cached = cachedScrollContainerRef.current;
|
|
577
|
-
// DOM에 연결되어 있고 여전히 스크롤 가능한지 확인
|
|
578
|
-
if (document.contains(cached) &&
|
|
579
|
-
cached.scrollHeight > cached.clientHeight + 2) {
|
|
580
|
-
return cached;
|
|
581
|
-
}
|
|
582
|
-
// 캐시 무효화
|
|
583
|
-
cachedScrollContainerRef.current = null;
|
|
584
|
-
}
|
|
585
|
-
if (!containerRef.current) {
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
// 내부 컨테이너의 스크롤 가능 여부 확인
|
|
589
|
-
if (contentRef.current &&
|
|
590
|
-
contentRef.current.scrollHeight >
|
|
591
|
-
containerRef.current.clientHeight + 2) {
|
|
592
|
-
cachedScrollContainerRef.current = containerRef.current;
|
|
593
|
-
return containerRef.current;
|
|
594
|
-
}
|
|
595
|
-
// detectInnerScroll 옵션이 활성화된 경우에만 children 내부의 스크롤 요소 찾기
|
|
596
|
-
// (가상 테이블 등 내부에서 스크롤을 처리하는 경우에 사용)
|
|
597
|
-
if (!detectInnerScroll) {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
// children 요소에서 스크롤 가능한 요소 찾기
|
|
601
|
-
// 중첩된 OverlayScrollbar의 영역은 제외 (다른 OverlayScrollbar의 container는 스킵)
|
|
602
|
-
const childScrollableElements = containerRef.current.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
|
|
603
|
-
for (const child of childScrollableElements) {
|
|
604
|
-
const element = child;
|
|
605
|
-
// 이 요소가 다른 OverlayScrollbar의 container인지 확인
|
|
606
|
-
// (자신의 containerRef는 아니어야 하고, overlay-scrollbar-container 클래스를 가진 경우)
|
|
607
|
-
if (element !== containerRef.current &&
|
|
608
|
-
element.classList.contains("overlay-scrollbar-container")) {
|
|
609
|
-
// 중첩된 OverlayScrollbar의 container이므로 스킵
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
// 이 요소의 부모 중에 다른 OverlayScrollbar container가 있는지 확인
|
|
613
|
-
let parent = element.parentElement;
|
|
614
|
-
let isNestedInAnotherScrollbar = false;
|
|
615
|
-
while (parent && parent !== containerRef.current) {
|
|
616
|
-
if (parent.classList.contains("overlay-scrollbar-container") &&
|
|
617
|
-
parent !== containerRef.current) {
|
|
618
|
-
// 다른 OverlayScrollbar 내부의 요소이므로 스킵
|
|
619
|
-
isNestedInAnotherScrollbar = true;
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
parent = parent.parentElement;
|
|
623
|
-
}
|
|
624
|
-
if (isNestedInAnotherScrollbar) {
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
// 스크롤 가능한 요소인지 확인
|
|
628
|
-
if (element.scrollHeight > element.clientHeight + 2) {
|
|
629
|
-
cachedScrollContainerRef.current = element;
|
|
630
|
-
return element;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return null;
|
|
634
|
-
}, []);
|
|
635
|
-
// 스크롤 가능 여부 체크
|
|
636
|
-
const isScrollable = useCallback(() => {
|
|
637
|
-
return findScrollableElement() !== null;
|
|
638
|
-
}, [findScrollableElement]);
|
|
639
|
-
// 타이머 정리
|
|
640
|
-
const clearHideTimer = useCallback(() => {
|
|
641
|
-
if (hideTimeoutRef.current) {
|
|
642
|
-
clearTimeout(hideTimeoutRef.current);
|
|
643
|
-
hideTimeoutRef.current = null;
|
|
644
|
-
}
|
|
645
|
-
}, []);
|
|
646
|
-
// 스크롤바 숨기기 타이머
|
|
647
|
-
const setHideTimer = useCallback((delay) => {
|
|
648
|
-
// 자동 숨김이 비활성화되어 있으면 타이머를 설정하지 않음
|
|
649
|
-
if (!finalAutoHideConfig.enabled) {
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
clearHideTimer();
|
|
653
|
-
hideTimeoutRef.current = setTimeout(() => {
|
|
654
|
-
setScrollbarVisible(false);
|
|
655
|
-
hideTimeoutRef.current = null;
|
|
656
|
-
}, delay);
|
|
657
|
-
}, [clearHideTimer, finalAutoHideConfig.enabled]);
|
|
658
|
-
// 스크롤바 위치 및 크기 업데이트
|
|
659
|
-
const updateScrollbar = useCallback(() => {
|
|
660
|
-
const scrollableElement = findScrollableElement();
|
|
661
|
-
if (!scrollableElement) {
|
|
662
|
-
// 스크롤 불가능하면 숨김
|
|
663
|
-
setScrollbarVisible(false);
|
|
664
|
-
setHasScrollableContent(false);
|
|
665
|
-
clearHideTimer();
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
// 스크롤 가능한 콘텐츠가 있음을 표시
|
|
669
|
-
setHasScrollableContent(true);
|
|
670
|
-
if (!scrollbarRef.current)
|
|
671
|
-
return;
|
|
672
|
-
// 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
|
|
673
|
-
if (!finalAutoHideConfig.enabled) {
|
|
674
|
-
setScrollbarVisible(true);
|
|
675
|
-
clearHideTimer();
|
|
676
|
-
}
|
|
677
|
-
const containerHeight = scrollableElement.clientHeight;
|
|
678
|
-
const contentHeight = scrollableElement.scrollHeight;
|
|
679
|
-
const scrollTop = scrollableElement.scrollTop;
|
|
680
|
-
// wrapper의 패딩 계산 (상하 패딩만 필요)
|
|
681
|
-
let wrapperPaddingTopBottom = 0;
|
|
682
|
-
if (wrapperRef.current) {
|
|
683
|
-
const computedStyle = window.getComputedStyle(wrapperRef.current);
|
|
684
|
-
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
|
|
685
|
-
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
|
|
686
|
-
wrapperPaddingTopBottom = paddingTop + paddingBottom;
|
|
687
|
-
}
|
|
688
|
-
// 화살표와 간격 공간 계산 (화살표 + 위아래 마진, 화살표 없어도 위아래 마진)
|
|
689
|
-
const arrowSpace = showArrows
|
|
690
|
-
? finalThumbWidth * 2 + finalTrackConfig.margin * 4
|
|
691
|
-
: finalTrackConfig.margin * 2;
|
|
692
|
-
// 썸 높이 계산 (사용자 설정 최소 높이 사용, 화살표 공간 제외, wrapper 패딩 추가)
|
|
693
|
-
const availableHeight = containerHeight - arrowSpace + wrapperPaddingTopBottom;
|
|
694
|
-
const scrollRatio = containerHeight / contentHeight;
|
|
695
|
-
const calculatedThumbHeight = Math.max(availableHeight * scrollRatio, thumbMinHeight);
|
|
696
|
-
// 썸 위치 계산 (화살표와 간격 공간 제외)
|
|
697
|
-
const scrollableHeight = contentHeight - containerHeight;
|
|
698
|
-
const thumbScrollableHeight = availableHeight - calculatedThumbHeight;
|
|
699
|
-
const calculatedThumbTop = scrollableHeight > 0
|
|
700
|
-
? (scrollTop / scrollableHeight) * thumbScrollableHeight
|
|
701
|
-
: 0;
|
|
702
|
-
setThumbHeight(calculatedThumbHeight);
|
|
703
|
-
setThumbTop(calculatedThumbTop);
|
|
704
|
-
}, [
|
|
705
|
-
findScrollableElement,
|
|
706
|
-
clearHideTimer,
|
|
707
|
-
showArrows,
|
|
708
|
-
finalThumbWidth,
|
|
709
|
-
thumbMinHeight,
|
|
710
|
-
finalAutoHideConfig.enabled,
|
|
711
|
-
]);
|
|
712
|
-
// 썸 드래그 시작
|
|
713
|
-
const handleThumbMouseDown = useCallback((event) => {
|
|
714
|
-
event.preventDefault();
|
|
715
|
-
event.stopPropagation();
|
|
716
|
-
const actualScrollContainer = findScrollableElement();
|
|
717
|
-
if (!actualScrollContainer) {
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
setIsDragging(true);
|
|
721
|
-
setDragStart({
|
|
722
|
-
y: event.clientY,
|
|
723
|
-
scrollTop: actualScrollContainer.scrollTop,
|
|
724
|
-
});
|
|
725
|
-
clearHideTimer();
|
|
726
|
-
setScrollbarVisible(true);
|
|
727
|
-
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
728
|
-
maintainFocus();
|
|
729
|
-
}, [findScrollableElement, clearHideTimer, maintainFocus]);
|
|
730
|
-
// 썸 드래그 중
|
|
731
|
-
const handleMouseMove = useCallback((event) => {
|
|
732
|
-
if (!isDragging)
|
|
733
|
-
return;
|
|
734
|
-
const actualScrollContainer = findScrollableElement();
|
|
735
|
-
if (!actualScrollContainer) {
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
const containerHeight = actualScrollContainer.clientHeight;
|
|
739
|
-
const contentHeight = actualScrollContainer.scrollHeight;
|
|
740
|
-
const scrollableHeight = contentHeight - containerHeight;
|
|
741
|
-
const deltaY = event.clientY - dragStart.y;
|
|
742
|
-
const thumbScrollableHeight = containerHeight - thumbHeight;
|
|
743
|
-
const scrollDelta = (deltaY / thumbScrollableHeight) * scrollableHeight;
|
|
744
|
-
const newScrollTop = Math.max(0, Math.min(scrollableHeight, dragStart.scrollTop + scrollDelta));
|
|
745
|
-
actualScrollContainer.scrollTop = newScrollTop;
|
|
746
|
-
updateScrollbar();
|
|
747
|
-
}, [
|
|
748
|
-
isDragging,
|
|
749
|
-
dragStart,
|
|
750
|
-
thumbHeight,
|
|
751
|
-
updateScrollbar,
|
|
752
|
-
findScrollableElement,
|
|
753
|
-
]);
|
|
754
|
-
// 썸 드래그 종료
|
|
755
|
-
const handleMouseUp = useCallback(() => {
|
|
756
|
-
setIsDragging(false);
|
|
757
|
-
if (isScrollable()) {
|
|
758
|
-
setHideTimer(finalAutoHideConfig.delay); // 기본 숨김 시간 적용
|
|
759
|
-
}
|
|
760
|
-
}, [isScrollable, setHideTimer, finalAutoHideConfig.delay]);
|
|
761
|
-
// 트랙 클릭으로 스크롤 점프
|
|
762
|
-
const handleTrackClick = useCallback((event) => {
|
|
763
|
-
if (!scrollbarRef.current) {
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
const scrollbar = scrollbarRef.current;
|
|
767
|
-
const rect = scrollbar.getBoundingClientRect();
|
|
768
|
-
const clickY = event.clientY - rect.top;
|
|
769
|
-
const actualScrollContainer = findScrollableElement();
|
|
770
|
-
if (!actualScrollContainer) {
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
const containerHeight = actualScrollContainer.clientHeight;
|
|
774
|
-
const contentHeight = actualScrollContainer.scrollHeight;
|
|
775
|
-
const scrollRatio = clickY / containerHeight;
|
|
776
|
-
const newScrollTop = scrollRatio * (contentHeight - containerHeight);
|
|
777
|
-
actualScrollContainer.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
|
|
778
|
-
updateScrollbar();
|
|
779
|
-
setScrollbarVisible(true);
|
|
780
|
-
setHideTimer(finalAutoHideConfig.delay);
|
|
781
|
-
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
782
|
-
maintainFocus();
|
|
783
|
-
}, [
|
|
784
|
-
updateScrollbar,
|
|
785
|
-
setHideTimer,
|
|
786
|
-
finalAutoHideConfig.delay,
|
|
787
|
-
findScrollableElement,
|
|
788
|
-
maintainFocus,
|
|
789
|
-
]);
|
|
790
|
-
// 위쪽 화살표 클릭 핸들러
|
|
791
|
-
const handleUpArrowClick = useCallback((event) => {
|
|
792
|
-
event.preventDefault();
|
|
793
|
-
event.stopPropagation();
|
|
794
|
-
if (!containerRef.current)
|
|
795
|
-
return;
|
|
796
|
-
const newScrollTop = Math.max(0, containerRef.current.scrollTop - arrowStep);
|
|
797
|
-
containerRef.current.scrollTop = newScrollTop;
|
|
798
|
-
updateScrollbar();
|
|
799
|
-
setScrollbarVisible(true);
|
|
800
|
-
setHideTimer(finalAutoHideConfig.delay);
|
|
801
|
-
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
802
|
-
maintainFocus();
|
|
803
|
-
}, [
|
|
804
|
-
updateScrollbar,
|
|
805
|
-
setHideTimer,
|
|
806
|
-
arrowStep,
|
|
807
|
-
finalAutoHideConfig.delay,
|
|
808
|
-
maintainFocus,
|
|
809
|
-
]);
|
|
810
|
-
// 아래쪽 화살표 클릭 핸들러
|
|
811
|
-
const handleDownArrowClick = useCallback((event) => {
|
|
812
|
-
event.preventDefault();
|
|
813
|
-
event.stopPropagation();
|
|
814
|
-
if (!containerRef.current || !contentRef.current)
|
|
815
|
-
return;
|
|
816
|
-
const container = containerRef.current;
|
|
817
|
-
const content = contentRef.current;
|
|
818
|
-
const maxScrollTop = content.scrollHeight - container.clientHeight;
|
|
819
|
-
const newScrollTop = Math.min(maxScrollTop, container.scrollTop + arrowStep);
|
|
820
|
-
container.scrollTop = newScrollTop;
|
|
821
|
-
updateScrollbar();
|
|
822
|
-
setScrollbarVisible(true);
|
|
823
|
-
setHideTimer(finalAutoHideConfig.delay);
|
|
824
|
-
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
825
|
-
maintainFocus();
|
|
826
|
-
}, [
|
|
827
|
-
updateScrollbar,
|
|
828
|
-
setHideTimer,
|
|
829
|
-
arrowStep,
|
|
830
|
-
finalAutoHideConfig.delay,
|
|
831
|
-
maintainFocus,
|
|
832
|
-
]);
|
|
833
|
-
// 드래그 스크롤 시작
|
|
834
|
-
const handleDragScrollStart = useCallback((event) => {
|
|
835
|
-
// 드래그 스크롤이 비활성화된 경우
|
|
836
|
-
if (!finalDragScrollConfig.enabled)
|
|
837
|
-
return;
|
|
838
|
-
// 텍스트 입력 요소나 제외 대상이면 드래그 스크롤 하지 않음
|
|
839
|
-
const target = event.target;
|
|
840
|
-
if (isTextInputElement(target, finalDragScrollConfig)) {
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
// 오른쪽 클릭이나 휠 클릭은 제외
|
|
844
|
-
if (event.button !== 0)
|
|
845
|
-
return;
|
|
846
|
-
const scrollableElement = findScrollableElement();
|
|
847
|
-
if (!scrollableElement)
|
|
848
|
-
return;
|
|
849
|
-
// 스크롤 가능한 영역이 아니면 제외
|
|
850
|
-
if (scrollableElement.scrollHeight <=
|
|
851
|
-
scrollableElement.clientHeight)
|
|
852
|
-
return;
|
|
853
|
-
event.preventDefault();
|
|
854
|
-
setIsDragScrolling(true);
|
|
855
|
-
setDragScrollStart({
|
|
856
|
-
x: event.clientX,
|
|
857
|
-
y: event.clientY,
|
|
858
|
-
scrollTop: scrollableElement.scrollTop,
|
|
859
|
-
scrollLeft: scrollableElement.scrollLeft || 0,
|
|
860
|
-
});
|
|
861
|
-
// 스크롤바 표시
|
|
862
|
-
clearHideTimer();
|
|
863
|
-
setScrollbarVisible(true);
|
|
864
|
-
}, [
|
|
865
|
-
finalDragScrollConfig,
|
|
866
|
-
isTextInputElement,
|
|
867
|
-
findScrollableElement,
|
|
868
|
-
clearHideTimer,
|
|
869
|
-
]);
|
|
870
|
-
// 드래그 스크롤 중
|
|
871
|
-
const handleDragScrollMove = useCallback((event) => {
|
|
872
|
-
if (!isDragScrolling)
|
|
873
|
-
return;
|
|
874
|
-
const scrollableElement = findScrollableElement();
|
|
875
|
-
if (!scrollableElement)
|
|
876
|
-
return;
|
|
877
|
-
dragScrollStart.x - event.clientX;
|
|
878
|
-
const deltaY = dragScrollStart.y - event.clientY;
|
|
879
|
-
// 세로 스크롤만 처리 (가로 스크롤은 필요시 나중에 추가)
|
|
880
|
-
const newScrollTop = Math.max(0, Math.min(scrollableElement.scrollHeight -
|
|
881
|
-
scrollableElement.clientHeight, dragScrollStart.scrollTop + deltaY));
|
|
882
|
-
scrollableElement.scrollTop = newScrollTop;
|
|
883
|
-
updateScrollbar();
|
|
884
|
-
}, [
|
|
885
|
-
isDragScrolling,
|
|
886
|
-
dragScrollStart,
|
|
887
|
-
findScrollableElement,
|
|
888
|
-
updateScrollbar,
|
|
889
|
-
]);
|
|
890
|
-
// 드래그 스크롤 종료
|
|
891
|
-
const handleDragScrollEnd = useCallback(() => {
|
|
892
|
-
setIsDragScrolling(false);
|
|
893
|
-
if (isScrollable()) {
|
|
894
|
-
setHideTimer(finalAutoHideConfig.delay);
|
|
895
|
-
}
|
|
896
|
-
}, [isScrollable, setHideTimer, finalAutoHideConfig.delay]);
|
|
897
|
-
// 스크롤 이벤트 리스너 (externalScrollContainer 우선 사용)
|
|
898
|
-
useEffect(() => {
|
|
899
|
-
const handleScroll = (event) => {
|
|
900
|
-
updateScrollbar();
|
|
901
|
-
// 스크롤 중에는 스크롤바 표시
|
|
902
|
-
clearHideTimer();
|
|
903
|
-
setScrollbarVisible(true);
|
|
904
|
-
// 휠 스크롤 중이면 빠른 숨김, 아니면 기본 숨김 시간 적용
|
|
905
|
-
const delay = isWheelScrolling
|
|
906
|
-
? finalAutoHideConfig.delayOnWheel
|
|
907
|
-
: finalAutoHideConfig.delay;
|
|
908
|
-
setHideTimer(delay);
|
|
909
|
-
if (onScroll) {
|
|
910
|
-
onScroll(event);
|
|
911
|
-
}
|
|
912
|
-
};
|
|
913
|
-
const handleWheel = () => {
|
|
914
|
-
// 휠 스크롤 상태 표시
|
|
915
|
-
setIsWheelScrolling(true);
|
|
916
|
-
// 기존 휠 타이머 제거
|
|
917
|
-
if (wheelTimeoutRef.current) {
|
|
918
|
-
clearTimeout(wheelTimeoutRef.current);
|
|
919
|
-
}
|
|
920
|
-
// 300ms 후 휠 스크롤 상태 해제 (휠 스크롤이 끝났다고 간주)
|
|
921
|
-
wheelTimeoutRef.current = setTimeout(() => {
|
|
922
|
-
setIsWheelScrolling(false);
|
|
923
|
-
}, 300);
|
|
924
|
-
clearHideTimer();
|
|
925
|
-
setScrollbarVisible(true);
|
|
926
|
-
};
|
|
927
|
-
const elementsToWatch = [];
|
|
928
|
-
// 실제 스크롤 가능한 요소 찾기
|
|
929
|
-
const scrollableElement = findScrollableElement();
|
|
930
|
-
if (scrollableElement) {
|
|
931
|
-
elementsToWatch.push(scrollableElement);
|
|
932
|
-
}
|
|
933
|
-
// fallback: 내부 컨테이너와 children 요소도 감지
|
|
934
|
-
const container = containerRef.current;
|
|
935
|
-
if (container && !scrollableElement) {
|
|
936
|
-
elementsToWatch.push(container);
|
|
937
|
-
// children 요소들의 스크롤도 감지 (중첩된 OverlayScrollbar 제외)
|
|
938
|
-
const childScrollableElements = container.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
|
|
939
|
-
childScrollableElements.forEach((child) => {
|
|
940
|
-
const element = child;
|
|
941
|
-
// 다른 OverlayScrollbar의 container는 제외
|
|
942
|
-
if (element !== container &&
|
|
943
|
-
element.classList.contains("overlay-scrollbar-container")) {
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
// 부모 중에 다른 OverlayScrollbar container가 있으면 제외
|
|
947
|
-
let parent = element.parentElement;
|
|
948
|
-
while (parent && parent !== container) {
|
|
949
|
-
if (parent.classList.contains("overlay-scrollbar-container") &&
|
|
950
|
-
parent !== container) {
|
|
951
|
-
return; // 중첩된 OverlayScrollbar 내부이므로 제외
|
|
952
|
-
}
|
|
953
|
-
parent = parent.parentElement;
|
|
954
|
-
}
|
|
955
|
-
elementsToWatch.push(element);
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
// 모든 요소에 이벤트 리스너 등록
|
|
959
|
-
elementsToWatch.forEach((element) => {
|
|
960
|
-
element.addEventListener("scroll", handleScroll, {
|
|
961
|
-
passive: true,
|
|
962
|
-
});
|
|
963
|
-
element.addEventListener("wheel", handleWheel, {
|
|
964
|
-
passive: true,
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
return () => {
|
|
968
|
-
// 모든 이벤트 리스너 제거
|
|
969
|
-
elementsToWatch.forEach((element) => {
|
|
970
|
-
element.removeEventListener("scroll", handleScroll);
|
|
971
|
-
element.removeEventListener("wheel", handleWheel);
|
|
972
|
-
});
|
|
973
|
-
if (wheelTimeoutRef.current) {
|
|
974
|
-
clearTimeout(wheelTimeoutRef.current);
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
}, [
|
|
978
|
-
findScrollableElement,
|
|
979
|
-
updateScrollbar,
|
|
980
|
-
onScroll,
|
|
981
|
-
clearHideTimer,
|
|
982
|
-
setHideTimer,
|
|
983
|
-
finalAutoHideConfig,
|
|
984
|
-
isWheelScrolling,
|
|
985
|
-
]);
|
|
986
|
-
// 키보드 네비게이션 핸들러 (방향키, PageUp/PageDown/Home/End)
|
|
987
|
-
useEffect(() => {
|
|
988
|
-
const handleKeyDown = (event) => {
|
|
989
|
-
const scrollableElement = findScrollableElement();
|
|
990
|
-
if (!scrollableElement)
|
|
991
|
-
return;
|
|
992
|
-
const { key } = event;
|
|
993
|
-
const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
|
|
994
|
-
const maxScrollTop = scrollHeight - clientHeight;
|
|
995
|
-
// 한 줄 스크롤 단위 (rowHeight 또는 기본값)
|
|
996
|
-
const lineScrollStep = 50;
|
|
997
|
-
let newScrollTop = null;
|
|
998
|
-
switch (key) {
|
|
999
|
-
case "ArrowUp":
|
|
1000
|
-
event.preventDefault();
|
|
1001
|
-
newScrollTop = Math.max(0, scrollTop - lineScrollStep);
|
|
1002
|
-
break;
|
|
1003
|
-
case "ArrowDown":
|
|
1004
|
-
event.preventDefault();
|
|
1005
|
-
newScrollTop = Math.min(maxScrollTop, scrollTop + lineScrollStep);
|
|
1006
|
-
break;
|
|
1007
|
-
case "PageUp":
|
|
1008
|
-
event.preventDefault();
|
|
1009
|
-
newScrollTop = Math.max(0, scrollTop - clientHeight);
|
|
1010
|
-
break;
|
|
1011
|
-
case "PageDown":
|
|
1012
|
-
event.preventDefault();
|
|
1013
|
-
newScrollTop = Math.min(maxScrollTop, scrollTop + clientHeight);
|
|
1014
|
-
break;
|
|
1015
|
-
case "Home":
|
|
1016
|
-
event.preventDefault();
|
|
1017
|
-
newScrollTop = 0;
|
|
1018
|
-
break;
|
|
1019
|
-
case "End":
|
|
1020
|
-
event.preventDefault();
|
|
1021
|
-
newScrollTop = maxScrollTop;
|
|
1022
|
-
break;
|
|
1023
|
-
default:
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
if (newScrollTop !== null) {
|
|
1027
|
-
// 썸 위치를 먼저 업데이트
|
|
1028
|
-
const scrollRatio = newScrollTop / maxScrollTop;
|
|
1029
|
-
const arrowSpace = showArrows
|
|
1030
|
-
? finalThumbWidth * 2 + finalTrackConfig.margin * 4
|
|
1031
|
-
: finalTrackConfig.margin * 2;
|
|
1032
|
-
const availableHeight = clientHeight - arrowSpace;
|
|
1033
|
-
const scrollableThumbHeight = availableHeight - thumbHeight;
|
|
1034
|
-
const newThumbTop = scrollableThumbHeight * scrollRatio;
|
|
1035
|
-
setThumbTop(newThumbTop);
|
|
1036
|
-
// 스크롤 위치를 즉시 변경 (애니메이션 없음)
|
|
1037
|
-
scrollableElement.scrollTop = newScrollTop;
|
|
1038
|
-
// 스크롤바 표시
|
|
1039
|
-
clearHideTimer();
|
|
1040
|
-
setScrollbarVisible(true);
|
|
1041
|
-
setHideTimer(finalAutoHideConfig.delay);
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
const container = containerRef.current;
|
|
1045
|
-
if (container) {
|
|
1046
|
-
container.addEventListener("keydown", handleKeyDown);
|
|
1047
|
-
return () => {
|
|
1048
|
-
container.removeEventListener("keydown", handleKeyDown);
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
}, [
|
|
1052
|
-
findScrollableElement,
|
|
1053
|
-
showArrows,
|
|
1054
|
-
finalThumbWidth,
|
|
1055
|
-
finalTrackConfig.margin,
|
|
1056
|
-
thumbHeight,
|
|
1057
|
-
clearHideTimer,
|
|
1058
|
-
setHideTimer,
|
|
1059
|
-
finalAutoHideConfig.delay,
|
|
1060
|
-
]);
|
|
1061
|
-
// 드래그 스크롤 전역 마우스 이벤트 리스너
|
|
1062
|
-
useEffect(() => {
|
|
1063
|
-
if (isDragScrolling) {
|
|
1064
|
-
document.addEventListener("mousemove", handleDragScrollMove);
|
|
1065
|
-
document.addEventListener("mouseup", handleDragScrollEnd);
|
|
1066
|
-
return () => {
|
|
1067
|
-
document.removeEventListener("mousemove", handleDragScrollMove);
|
|
1068
|
-
document.removeEventListener("mouseup", handleDragScrollEnd);
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
}, [isDragScrolling, handleDragScrollMove, handleDragScrollEnd]);
|
|
1072
|
-
// 전역 마우스 이벤트 리스너
|
|
1073
|
-
useEffect(() => {
|
|
1074
|
-
if (isDragging) {
|
|
1075
|
-
document.addEventListener("mousemove", handleMouseMove);
|
|
1076
|
-
document.addEventListener("mouseup", handleMouseUp);
|
|
1077
|
-
return () => {
|
|
1078
|
-
document.removeEventListener("mousemove", handleMouseMove);
|
|
1079
|
-
document.removeEventListener("mouseup", handleMouseUp);
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
1083
|
-
// 초기 스크롤바 업데이트
|
|
1084
|
-
useEffect(() => {
|
|
1085
|
-
// 즉시 업데이트
|
|
1086
|
-
updateScrollbar();
|
|
1087
|
-
// 약간의 지연 후에도 업데이트 (DOM이 완전히 렌더링된 후)
|
|
1088
|
-
const timer = setTimeout(() => {
|
|
1089
|
-
updateScrollbar();
|
|
1090
|
-
}, 100);
|
|
1091
|
-
return () => clearTimeout(timer);
|
|
1092
|
-
}, [updateScrollbar]);
|
|
1093
|
-
// 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
|
|
1094
|
-
useLayoutEffect(() => {
|
|
1095
|
-
setIsInitialized(true);
|
|
1096
|
-
// 초기화 직후 스크롤바 업데이트 (썸 높이 정확하게 계산)
|
|
1097
|
-
updateScrollbar();
|
|
1098
|
-
// 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
|
|
1099
|
-
if (!finalAutoHideConfig.enabled && isScrollable()) {
|
|
1100
|
-
setScrollbarVisible(true);
|
|
1101
|
-
}
|
|
1102
|
-
}, [isScrollable, updateScrollbar, finalAutoHideConfig.enabled]);
|
|
1103
|
-
// Resize observer로 크기 변경 감지
|
|
1104
|
-
useEffect(() => {
|
|
1105
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
1106
|
-
updateScrollbar();
|
|
1107
|
-
});
|
|
1108
|
-
const elementsToObserve = [];
|
|
1109
|
-
// 내부 컨테이너들 관찰
|
|
1110
|
-
if (containerRef.current) {
|
|
1111
|
-
elementsToObserve.push(containerRef.current);
|
|
1112
|
-
}
|
|
1113
|
-
if (contentRef.current) {
|
|
1114
|
-
elementsToObserve.push(contentRef.current);
|
|
1115
|
-
}
|
|
1116
|
-
// 캐시된 스크롤 컨테이너도 관찰
|
|
1117
|
-
if (cachedScrollContainerRef.current &&
|
|
1118
|
-
document.contains(cachedScrollContainerRef.current)) {
|
|
1119
|
-
elementsToObserve.push(cachedScrollContainerRef.current);
|
|
1120
|
-
}
|
|
1121
|
-
// 모든 요소들 관찰 시작
|
|
1122
|
-
elementsToObserve.forEach((element) => {
|
|
1123
|
-
resizeObserver.observe(element);
|
|
1124
|
-
});
|
|
1125
|
-
return () => resizeObserver.disconnect();
|
|
1126
|
-
}, [updateScrollbar]);
|
|
1127
|
-
// MutationObserver로 DOM 변경 감지
|
|
1128
|
-
useEffect(() => {
|
|
1129
|
-
if (!containerRef.current) {
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
const observer = new MutationObserver(() => {
|
|
1133
|
-
// 캐시 초기화하여 새로운 스크롤 컨테이너 감지
|
|
1134
|
-
cachedScrollContainerRef.current = null;
|
|
1135
|
-
updateScrollbar();
|
|
1136
|
-
});
|
|
1137
|
-
observer.observe(containerRef.current, {
|
|
1138
|
-
childList: true,
|
|
1139
|
-
subtree: true,
|
|
1140
|
-
attributes: true,
|
|
1141
|
-
attributeFilter: ["style"],
|
|
1142
|
-
});
|
|
1143
|
-
return () => observer.disconnect();
|
|
1144
|
-
}, [updateScrollbar]);
|
|
1145
|
-
// trackWidth가 thumbWidth보다 작으면 thumbWidth와 같게 설정
|
|
1146
|
-
const adjustedTrackWidth = Math.max(finalTrackWidth, finalThumbWidth);
|
|
1147
|
-
// 웹킷 스크롤바 숨기기용 CSS 동적 주입
|
|
1148
|
-
useEffect(() => {
|
|
1149
|
-
const styleId = "overlay-scrollbar-webkit-hide";
|
|
1150
|
-
// 이미 스타일이 있으면 제거
|
|
1151
|
-
const existingStyle = document.getElementById(styleId);
|
|
1152
|
-
if (existingStyle) {
|
|
1153
|
-
existingStyle.remove();
|
|
1154
|
-
}
|
|
1155
|
-
const style = document.createElement("style");
|
|
1156
|
-
style.id = styleId;
|
|
1157
|
-
style.textContent = `
|
|
1
|
+
import{forwardRef as Ze,useCallback as me,useMemo as Qe,useRef as q,useEffect as ke,memo as Lt,useState as lt}from"react";import{Box as he,Table as Dt,TableBody as Et,TableCell as ze,TableContainer as Ht,TableHead as Rt,TableRow as Fe,TableSortLabel as et,Paper as It,Typography as $t}from"@mui/material";import{TableVirtuoso as Ot}from"react-virtuoso";import{jsx as Je}from"react/jsx-runtime";import{useRef as tt,useState as pt,useEffect as ft}from"react";import{Box as mt,CircularProgress as ht}from"@mui/material";var Ne=function(){return Ne=Object.assign||function(p){for(var h,x=1,D=arguments.length;x<D;x++){h=arguments[x];for(var k in h)Object.prototype.hasOwnProperty.call(h,k)&&(p[k]=h[k])}return p},Ne.apply(this,arguments)};function rt(s){var p=s.visible,h=s.onComplete,x=s.fadeoutDuration,D=x===void 0?200:x,k=s.exitDelay,T=k===void 0?0:k,B=s.size,L=B===void 0?40:B,N=s.className,v=s.style,j=s.sx,Z=s.indicator,Q=s.background,ge=Q===void 0?{show:!0,color:"255, 255, 255",opacity:.9}:Q,ee=tt(void 0),a=tt(null),z=pt(p),te=z[0],re=z[1],$=ge.show,oe=$===void 0?!0:$,S=ge.color,E=S===void 0?"255, 255, 255":S,ne=ge.opacity,ie=ne===void 0?.9:ne;if(ft(function(){var De=ee.current;if(De===!0&&p===!1&&a.current){var ae=a.current;ae.style.opacity="0",setTimeout(function(){re(!1),h?.()},D+T)}else p===!0&&(re(!0),a.current&&(a.current.style.opacity="1"));ee.current=p},[p,h,D,T]),!te)return null;var we=function(){return typeof Z=="string"?Je("img",{src:Z,alt:"Loading",style:{width:L,height:L}}):Z||Je(ht,{size:L})},le=function(){return oe?E.startsWith("rgba")?{backgroundColor:E}:/^\d+,\s*\d+,\s*\d+$/.test(E)?{backgroundColor:"rgba(".concat(E,", ").concat(ie,")")}:ie!==1?{backgroundColor:E,"&::before":{content:'""',position:"absolute",top:0,left:0,right:0,bottom:0,backgroundColor:E,opacity:ie,zIndex:-1}}:{backgroundColor:E}:{backgroundColor:"transparent"}};return Je(mt,{ref:a,className:N,style:Ne({},v),sx:Ne(Ne({position:"absolute",top:0,left:0,right:0,bottom:0,display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e3,opacity:p?1:0,transition:"opacity ".concat(D,"ms ease-out")},le()),j),children:we()})}import{useRef as U,useEffect as J,useState as A,useCallback as R,useMemo as je,forwardRef as gt,useImperativeHandle as bt,useLayoutEffect as yt}from"react";import{jsx as Ce,jsxs as nt}from"react/jsx-runtime";var xt=["editor","textarea","input","select","textfield","form-control","contenteditable","MuiInputBase-input","MuiSelect-select","MuiOutlinedInput-input","MuiFilledInput-input","MuiInput-input","MuiFormControl-root","MuiTextField-root","MuiSelect-root","MuiOutlinedInput-root","MuiFilledInput-root","MuiInput-root","MuiAutocomplete-input","MuiDatePicker-input","MuiSlider-thumb","MuiSlider-rail","MuiSlider-track","MuiSlider-mark","MuiSlider-markLabel","MuiSlider-root","MuiSlider-colorPrimary","MuiSlider-sizeMedium","MuiIconButton-root","MuiButton-root","MuiButtonBase-root","MuiTouchRipple-root","MuiCheckbox-root","MuiRadio-root","MuiSwitch-root","PrivateSwitchBase-root","ant-input","ant-input-affix-wrapper","ant-input-group-addon","ant-input-number","ant-input-number-handler","ant-select","ant-select-selector","ant-select-selection-search","ant-select-dropdown","ant-cascader","ant-cascader-input","ant-picker","ant-picker-input","ant-time-picker","ant-calendar-picker","ant-slider","ant-slider-track","ant-slider-handle","ant-switch","ant-checkbox","ant-checkbox-wrapper","ant-radio","ant-radio-wrapper","ant-rate","ant-upload","ant-upload-drag","ant-form-item","ant-form-item-control","ant-btn","ant-dropdown","ant-dropdown-trigger","ant-menu","ant-menu-item","ant-tooltip","ant-popover","ant-modal","ant-drawer","ant-tree-select","ant-auto-complete","ant-mentions","ant-transfer","ui-input","ui-textarea","ui-select","ui-select-trigger","ui-select-content","ui-select-item","ui-button","ui-checkbox","ui-radio-group","ui-switch","ui-slider","ui-range-slider","ui-calendar","ui-date-picker","ui-combobox","ui-command","ui-command-input","ui-popover","ui-dialog","ui-sheet","ui-dropdown-menu","ui-context-menu","ui-menubar","ui-navigation-menu","ui-form","ui-form-control","ui-form-item","ui-form-field","ui-label","radix-ui","radix-select","radix-dropdown","radix-dialog","radix-popover","radix-accordion","radix-tabs","radix-slider","radix-switch","radix-checkbox","radix-radio","ql-editor","ql-container","ql-toolbar","ql-picker","ql-picker-label","ql-picker-options","ql-formats","ql-snow","ql-bubble","quill","quilleditor","monaco-editor","monaco-editor-background","view-lines","decorationsOverviewRuler","monaco-scrollable-element","CodeMirror","CodeMirror-code","CodeMirror-lines","CodeMirror-scroll","CodeMirror-sizer","cm-editor","cm-focused","cm-content","tox-editor-container","tox-editor-header","tox-edit-area","tox-tinymce","mce-content-body","ck-editor","ck-content","ck-toolbar","ck-editor__editable","ck-widget","slate-editor","slate-content","DraftEditor-root","DraftEditor-editorContainer","public-DraftEditor-content","ehfuse-editor","ehfuse-editor-wrapper","ehfuse-editor-content","ehfuse-toolbar","ehfuse-toolbar-group","ehfuse-cursor","text-editor","rich-text-editor","wysiwyg","ace_editor","ace_content"],ot=(s,p)=>{let h=s.tagName.toLowerCase(),x=["text","password","email","number","search","tel","url","checkbox","radio"];if(h==="input"){let D=s.type;return x.includes(D)}if(["textarea","select","button"].includes(h)||["svg","path","circle","rect","line","polygon","polyline"].includes(h)||s.getAttribute("contenteditable")==="true")return!0;if(p?.excludeSelectors){for(let D of p.excludeSelectors)if(s.matches(D))return!0}return vt(s,p)},vt=(s,p)=>{let h=[...xt,...p?.excludeClasses||[]],x=s,D=0,k=5;for(;x&&D<=k;){if(h.some(T=>x.classList.contains(T)))return!0;if(x.classList.contains("MuiDialogContent-root"))break;x=x.parentElement,D++}return!1},wt={},Mt={},Tt={},St={},Ct={},kt=gt(({className:s="",style:p={},containerStyle:h={},contentStyle:x={},children:D,onScroll:k,thumb:T=wt,track:B=Mt,arrows:L=Tt,dragScroll:N=St,autoHide:v=Ct,showScrollbar:j=!0,detectInnerScroll:Z=!1},Q)=>{let ge=U({});J(()=>{ge.current={children:D,onScroll:k,showScrollbar:j,thumb:T,track:B,arrows:L,dragScroll:N,autoHide:v}});let ee=U(null),a=U(null),z=U(null),te=U(null),re=U(null),$=U(null),[oe,S]=A(!1),[E,ne]=A(!1),[ie,we]=A(!1),[le,De]=A({y:0,scrollTop:0}),[ae,Ee]=A(0),[He,Y]=A(0),[X,Re]=A(!1),[se,Ie]=A(!1),[K,Me]=A({x:0,y:0,scrollTop:0,scrollLeft:0}),[Ae,at]=A(null),[Te,ce]=A(null),[Ye,Se]=A(!1),be=U(null),[qe,We]=A(!1),ye=U(null),ue=U(null),r=U(null),n=je(()=>{let t=T.color??"#606060";return{width:T.width??8,minHeight:T.minHeight??50,radius:T.radius??(T.width??8)/2,color:t,opacity:T.opacity??.6,hoverColor:T.hoverColor??t,hoverOpacity:T.hoverOpacity??1}},[T]),l=je(()=>({width:B.width??16,color:B.color??"rgba(128, 128, 128, 0.1)",visible:B.visible??!0,alignment:B.alignment??"center",radius:B.radius??n.radius??4,margin:B.margin??4}),[B,n.radius]),m=je(()=>{let t=L.color??"#808080";return{visible:L.visible??!1,step:L.step??50,color:t,opacity:L.opacity??.6,hoverColor:L.hoverColor??t,hoverOpacity:L.hoverOpacity??1}},[L]),W=je(()=>({enabled:N.enabled??!0,excludeClasses:N.excludeClasses??[],excludeSelectors:N.excludeSelectors??[]}),[N]),e=je(()=>({enabled:v.enabled??!0,delay:v.delay??1500,delayOnWheel:v.delayOnWheel??700}),[v]),g=n.width,$e=l.width,de=n.minHeight,V=m.visible,pe=m.step,H=R(()=>{if(!a.current)return;let t=document.activeElement;t&&a.current.contains(t)&&t!==a.current||a.current.focus()},[]);bt(Q,()=>({getScrollContainer:()=>a.current,scrollTo:t=>{a.current&&a.current.scrollTo(t)},get scrollTop(){return a.current?.scrollTop||0},get scrollHeight(){return a.current?.scrollHeight||0},get clientHeight(){return a.current?.clientHeight||0}}),[]);let f=R(()=>{if($.current){let o=$.current;if(document.contains(o)&&o.scrollHeight>o.clientHeight+2)return o;$.current=null}if(!a.current)return null;if(z.current&&z.current.scrollHeight>a.current.clientHeight+2)return $.current=a.current,a.current;if(!Z)return null;let t=a.current.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');for(let o of t){let i=o;if(i!==a.current&&i.classList.contains("overlay-scrollbar-container"))continue;let c=i.parentElement,y=!1;for(;c&&c!==a.current;){if(c.classList.contains("overlay-scrollbar-container")&&c!==a.current){y=!0;break}c=c.parentElement}if(!y&&i.scrollHeight>i.clientHeight+2)return $.current=i,i}return null},[]),I=R(()=>f()!==null,[f]),b=R(()=>{ye.current&&(clearTimeout(ye.current),ye.current=null)},[]),xe=R(()=>{ue.current&&(clearTimeout(ue.current),ue.current=null)},[]),fe=R(()=>{r.current&&(clearTimeout(r.current),r.current=null)},[]),w=R(t=>{e.enabled&&(b(),ye.current=setTimeout(()=>{S(!1),ye.current=null},t))},[b,e.enabled]),u=R(()=>{let t=f();if(!t){S(!1),Re(!1),b();return}if(Re(!0),!te.current)return;e.enabled||(S(!0),b());let o=t.clientHeight,i=t.scrollHeight,c=t.scrollTop,y=0;if(ee.current){let Ve=window.getComputedStyle(ee.current),Ke=parseFloat(Ve.paddingTop)||0,dt=parseFloat(Ve.paddingBottom)||0;y=Ke+dt}let M=V?g*2+l.margin*4:l.margin*2,O=o-M+y,P=o/i,ve=Math.max(O*P,de),Be=i-o,G=O-ve,Xe=Be>0?c/Be*G:0;Ee(ve),Y(Xe)},[f,b,V,g,de,e.enabled]),Oe=R(t=>{t.preventDefault(),t.stopPropagation();let o=f();o&&(ne(!0),De({y:t.clientY,scrollTop:o.scrollTop}),b(),S(!0),H())},[f,b,H]),C=R(t=>{if(!E)return;let o=f();if(!o)return;let i=o.clientHeight,c=o.scrollHeight-i,y=t.clientY-le.y,M=i-ae,O=y/M*c,P=Math.max(0,Math.min(c,le.scrollTop+O));o.scrollTop=P,u()},[E,le,ae,u,f]),F=R(()=>{ne(!1),I()&&w(e.delay)},[I,w,e.delay]),_=R(t=>{if(!te.current)return;let o=te.current.getBoundingClientRect(),i=t.clientY-o.top,c=f();if(!c)return;let y=c.clientHeight,M=c.scrollHeight,O=i/y*(M-y);c.scrollTop=Math.max(0,Math.min(M-y,O)),u(),S(!0),w(e.delay),H()},[u,w,e.delay,f,H]),st=R(t=>{if(t.preventDefault(),t.stopPropagation(),!a.current)return;let o=Math.max(0,a.current.scrollTop-pe);a.current.scrollTop=o,u(),S(!0),w(e.delay),H()},[u,w,pe,e.delay,H]),ct=R(t=>{if(t.preventDefault(),t.stopPropagation(),!a.current||!z.current)return;let o=a.current,i=z.current.scrollHeight-o.clientHeight,c=Math.min(i,o.scrollTop+pe);o.scrollTop=c,u(),S(!0),w(e.delay),H()},[u,w,pe,e.delay,H]),ut=R(t=>{if(!W.enabled)return;let o=t.target;if(ot(o,W)||t.button!==0)return;let i=f();i&&(i.scrollHeight<=i.clientHeight||(t.preventDefault(),Ie(!0),Me({x:t.clientX,y:t.clientY,scrollTop:i.scrollTop,scrollLeft:i.scrollLeft||0}),b()))},[W,ot,f,b]),Ge=R(t=>{if(!se)return;let o=f();if(!o)return;let i=K.x-t.clientX,c=K.y-t.clientY;if(Math.abs(c)<3&&Math.abs(i)<3)return;S(!0);let y=Math.max(0,Math.min(o.scrollHeight-o.clientHeight,K.scrollTop+c));o.scrollTop=y,u()},[se,K,f,u]),Ue=R(()=>{Ie(!1),I()&&w(e.delay)},[I,w,e.delay]);J(()=>{let t=M=>{u(),b(),S(!0);let O=qe?e.delayOnWheel:e.delay;w(O),k&&k(M)},o=()=>{We(!0),be.current&&clearTimeout(be.current),be.current=setTimeout(()=>{We(!1)},300),b(),fe(),r.current=setTimeout(()=>{S(!0),r.current=null},50)},i=[],c=f();c&&i.push(c);let y=a.current;return y&&!c&&(i.push(y),y.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]').forEach(M=>{let O=M;if(O!==y&&O.classList.contains("overlay-scrollbar-container"))return;let P=O.parentElement;for(;P&&P!==y;){if(P.classList.contains("overlay-scrollbar-container")&&P!==y)return;P=P.parentElement}i.push(O)})),i.forEach(M=>{M.addEventListener("scroll",t,{passive:!0}),M.addEventListener("wheel",o,{passive:!0})}),()=>{i.forEach(M=>{M.removeEventListener("scroll",t),M.removeEventListener("wheel",o)}),be.current&&clearTimeout(be.current),r.current&&clearTimeout(r.current)}},[f,u,k,b,w,e,qe]),J(()=>{let t=i=>{let c=f();if(!c)return;let{key:y}=i,{scrollTop:M,scrollHeight:O,clientHeight:P}=c,ve=O-P,Be=50,G=null;switch(y){case"ArrowUp":i.preventDefault(),G=Math.max(0,M-Be);break;case"ArrowDown":i.preventDefault(),G=Math.min(ve,M+Be);break;case"PageUp":i.preventDefault(),G=Math.max(0,M-P);break;case"PageDown":i.preventDefault(),G=Math.min(ve,M+P);break;case"Home":i.preventDefault(),G=0;break;case"End":i.preventDefault(),G=ve;break;default:return}if(G!==null){let Xe=G/ve,Ve=V?g*2+l.margin*4:l.margin*2,Ke=(P-Ve-ae)*Xe;Y(Ke),c.scrollTop=G,b(),S(!0),w(e.delay)}},o=a.current;if(o)return o.addEventListener("keydown",t),()=>{o.removeEventListener("keydown",t)}},[f,V,g,l.margin,ae,b,w,e.delay]),J(()=>{if(se)return document.addEventListener("mousemove",Ge),document.addEventListener("mouseup",Ue),()=>{document.removeEventListener("mousemove",Ge),document.removeEventListener("mouseup",Ue)}},[se,Ge,Ue]),J(()=>{if(E)return document.addEventListener("mousemove",C),document.addEventListener("mouseup",F),()=>{document.removeEventListener("mousemove",C),document.removeEventListener("mouseup",F)}},[E,C,F]),J(()=>{u();let t=setTimeout(()=>{u()},100);return()=>clearTimeout(t)},[u]),yt(()=>{Se(!0),u(),!e.enabled&&I()&&S(!0)},[I,u,e.enabled]),J(()=>{let t=new ResizeObserver(()=>{u()}),o=[];return a.current&&o.push(a.current),z.current&&o.push(z.current),$.current&&document.contains($.current)&&o.push($.current),o.forEach(i=>{t.observe(i)}),()=>t.disconnect()},[u]),J(()=>{if(!a.current)return;let t=new MutationObserver(()=>{$.current=null,u()});return t.observe(a.current,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style"]}),()=>t.disconnect()},[u]);let Pe=Math.max($e,g);return J(()=>{let t="overlay-scrollbar-webkit-hide",o=document.getElementById(t);o&&o.remove();let i=document.createElement("style");return i.id=t,i.textContent=`
|
|
1158
2
|
.overlay-scrollbar-container::-webkit-scrollbar {
|
|
1159
3
|
display: none !important;
|
|
1160
4
|
width: 0 !important;
|
|
@@ -1174,863 +18,12 @@ showScrollbar = true, detectInnerScroll = false, }, ref) => {
|
|
|
1174
18
|
outline: 2px solid rgba(0, 123, 255, 0.5);
|
|
1175
19
|
outline-offset: -2px;
|
|
1176
20
|
}
|
|
1177
|
-
`;
|
|
1178
|
-
document.head.appendChild(style);
|
|
1179
|
-
return () => {
|
|
1180
|
-
const styleToRemove = document.getElementById(styleId);
|
|
1181
|
-
if (styleToRemove) {
|
|
1182
|
-
styleToRemove.remove();
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
1185
|
-
}, []);
|
|
1186
|
-
return (jsxs("div", { ref: wrapperRef, className: `overlay-scrollbar-wrapper ${className}`, style: Object.assign({ display: "flex", flexDirection: "column", position: "relative", minHeight: 0, height: "100%", flex: "1 1 0%" }, style), children: [jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", tabIndex: -1, onMouseDown: handleDragScrollStart, style: Object.assign({ display: "flex", width: "100%", flex: "1 1 auto", minHeight: 0, overflow: "auto",
|
|
1187
|
-
// 브라우저 기본 스크롤바만 숨기기
|
|
1188
|
-
scrollbarWidth: "none", msOverflowStyle: "none",
|
|
1189
|
-
// 키보드 포커스 스타일 (접근성)
|
|
1190
|
-
outline: "none", userSelect: isDragScrolling ? "none" : "auto" }, containerStyle), children: jsx("div", { ref: contentRef, className: "overlay-scrollbar-content", style: Object.assign({ flex: "1 1 0%", minHeight: 0, display: "flex", flexDirection: "column" }, contentStyle), children: children }) }), showScrollbar && hasScrollableContent && (jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
|
|
1191
|
-
clearHideTimer();
|
|
1192
|
-
setScrollbarVisible(true);
|
|
1193
|
-
}, onMouseLeave: () => {
|
|
1194
|
-
if (!isDragging) {
|
|
1195
|
-
setHideTimer(finalAutoHideConfig.delay);
|
|
1196
|
-
}
|
|
1197
|
-
}, style: {
|
|
1198
|
-
position: "absolute",
|
|
1199
|
-
top: 0,
|
|
1200
|
-
right: 0,
|
|
1201
|
-
width: `${adjustedTrackWidth}px`,
|
|
1202
|
-
height: "100%",
|
|
1203
|
-
opacity: scrollbarVisible ? 1 : 0,
|
|
1204
|
-
transition: "opacity 0.2s ease-in-out",
|
|
1205
|
-
cursor: "pointer",
|
|
1206
|
-
zIndex: 1000,
|
|
1207
|
-
pointerEvents: "auto",
|
|
1208
|
-
}, children: [finalTrackConfig.visible && (jsx("div", { className: "overlay-scrollbar-track-background", onClick: (e) => {
|
|
1209
|
-
e.preventDefault();
|
|
1210
|
-
e.stopPropagation();
|
|
1211
|
-
handleTrackClick(e);
|
|
1212
|
-
}, style: {
|
|
1213
|
-
position: "absolute",
|
|
1214
|
-
top: showArrows
|
|
1215
|
-
? `${finalThumbConfig.width +
|
|
1216
|
-
finalTrackConfig.margin * 2}px`
|
|
1217
|
-
: `${finalTrackConfig.margin}px`,
|
|
1218
|
-
right: finalTrackConfig.alignment === "right"
|
|
1219
|
-
? "0px"
|
|
1220
|
-
: `${(adjustedTrackWidth -
|
|
1221
|
-
finalThumbConfig.width) /
|
|
1222
|
-
2}px`, // 트랙 정렬
|
|
1223
|
-
width: `${finalThumbConfig.width}px`,
|
|
1224
|
-
height: showArrows
|
|
1225
|
-
? `calc(100% - ${finalThumbConfig.width * 2 +
|
|
1226
|
-
finalTrackConfig.margin * 4}px)`
|
|
1227
|
-
: `calc(100% - ${finalTrackConfig.margin * 2}px)`,
|
|
1228
|
-
backgroundColor: finalTrackConfig.color,
|
|
1229
|
-
borderRadius: `${finalTrackConfig.radius}px`,
|
|
1230
|
-
cursor: "pointer",
|
|
1231
|
-
} })), jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, onMouseEnter: () => setIsThumbHovered(true), onMouseLeave: () => setIsThumbHovered(false), style: {
|
|
1232
|
-
position: "absolute",
|
|
1233
|
-
top: `${(showArrows
|
|
1234
|
-
? finalThumbWidth +
|
|
1235
|
-
finalTrackConfig.margin * 2
|
|
1236
|
-
: finalTrackConfig.margin) + thumbTop}px`,
|
|
1237
|
-
right: finalTrackConfig.alignment === "right"
|
|
1238
|
-
? "0px"
|
|
1239
|
-
: `${(adjustedTrackWidth -
|
|
1240
|
-
finalThumbWidth) /
|
|
1241
|
-
2}px`, // 트랙 정렬
|
|
1242
|
-
width: `${finalThumbWidth}px`,
|
|
1243
|
-
height: `${Math.max(thumbHeight, thumbMinHeight)}px`,
|
|
1244
|
-
backgroundColor: isThumbHovered || isDragging
|
|
1245
|
-
? finalThumbConfig.hoverColor
|
|
1246
|
-
: finalThumbConfig.color,
|
|
1247
|
-
opacity: isThumbHovered || isDragging
|
|
1248
|
-
? finalThumbConfig.hoverOpacity
|
|
1249
|
-
: finalThumbConfig.opacity,
|
|
1250
|
-
borderRadius: `${finalThumbConfig.radius}px`,
|
|
1251
|
-
cursor: "pointer",
|
|
1252
|
-
transition: "background-color 0.2s ease-in-out, opacity 0.2s ease-in-out",
|
|
1253
|
-
} })] })), showScrollbar && hasScrollableContent && showArrows && (jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
1254
|
-
position: "absolute",
|
|
1255
|
-
top: `${finalTrackConfig.margin}px`,
|
|
1256
|
-
right: finalTrackConfig.alignment === "right"
|
|
1257
|
-
? "0px"
|
|
1258
|
-
: `${(adjustedTrackWidth -
|
|
1259
|
-
finalThumbWidth) /
|
|
1260
|
-
2}px`, // 트랙 정렬
|
|
1261
|
-
width: `${finalThumbWidth}px`,
|
|
1262
|
-
height: `${finalThumbWidth}px`,
|
|
1263
|
-
cursor: "pointer",
|
|
1264
|
-
display: "flex",
|
|
1265
|
-
alignItems: "center",
|
|
1266
|
-
justifyContent: "center",
|
|
1267
|
-
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
1268
|
-
color: hoveredArrow === "up"
|
|
1269
|
-
? finalArrowsConfig.hoverColor
|
|
1270
|
-
: finalArrowsConfig.color,
|
|
1271
|
-
userSelect: "none",
|
|
1272
|
-
zIndex: 1001,
|
|
1273
|
-
opacity: scrollbarVisible
|
|
1274
|
-
? hoveredArrow === "up"
|
|
1275
|
-
? finalArrowsConfig.hoverOpacity
|
|
1276
|
-
: finalArrowsConfig.opacity
|
|
1277
|
-
: 0,
|
|
1278
|
-
transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
|
|
1279
|
-
}, children: "\u25B2" })), showScrollbar && hasScrollableContent && showArrows && (jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
1280
|
-
position: "absolute",
|
|
1281
|
-
bottom: `${finalTrackConfig.margin}px`,
|
|
1282
|
-
right: finalTrackConfig.alignment === "right"
|
|
1283
|
-
? "0px"
|
|
1284
|
-
: `${(adjustedTrackWidth -
|
|
1285
|
-
finalThumbWidth) /
|
|
1286
|
-
2}px`, // 트랙 정렬
|
|
1287
|
-
width: `${finalThumbWidth}px`,
|
|
1288
|
-
height: `${finalThumbWidth}px`,
|
|
1289
|
-
cursor: "pointer",
|
|
1290
|
-
display: "flex",
|
|
1291
|
-
alignItems: "center",
|
|
1292
|
-
justifyContent: "center",
|
|
1293
|
-
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
1294
|
-
color: hoveredArrow === "down"
|
|
1295
|
-
? finalArrowsConfig.hoverColor
|
|
1296
|
-
: finalArrowsConfig.color,
|
|
1297
|
-
userSelect: "none",
|
|
1298
|
-
zIndex: 1001,
|
|
1299
|
-
opacity: scrollbarVisible
|
|
1300
|
-
? hoveredArrow === "down"
|
|
1301
|
-
? finalArrowsConfig.hoverOpacity
|
|
1302
|
-
: finalArrowsConfig.opacity
|
|
1303
|
-
: 0,
|
|
1304
|
-
transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
|
|
1305
|
-
}, children: "\u25BC" }))] }));
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
// OverlayScrollbar 설정을 컴포넌트 외부에 상수로 선언 (재렌더링 시 동일한 참조 유지)
|
|
1309
|
-
const OVERLAY_SCROLLBAR_TRACK_CONFIG = {
|
|
1310
|
-
alignment: "right",
|
|
1311
|
-
margin: 0,
|
|
1312
|
-
radius: 0,
|
|
1313
|
-
};
|
|
21
|
+
`,document.head.appendChild(i),()=>{let c=document.getElementById(t);c&&c.remove()}},[]),nt("div",{ref:ee,className:`overlay-scrollbar-wrapper ${s}`,style:{display:"flex",flexDirection:"column",position:"relative",minHeight:0,height:"100%",flex:"1 1 0%",...p},children:[Ce("div",{ref:a,className:"overlay-scrollbar-container",tabIndex:-1,onMouseDown:ut,style:{display:"flex",width:"100%",flex:"1 1 auto",minHeight:0,overflow:"auto",scrollbarWidth:"none",msOverflowStyle:"none",outline:"none",userSelect:se?"none":"auto",...h},children:Ce("div",{ref:z,className:"overlay-scrollbar-content",style:{flex:"1 1 0%",minHeight:0,display:"flex",flexDirection:"column",...x},children:D})}),j&&X&&nt("div",{ref:te,className:"overlay-scrollbar-track",onMouseEnter:()=>{b(),ue.current=setTimeout(()=>{S(!0),ue.current=null},100)},onMouseLeave:()=>{xe(),E||w(e.delay)},style:{position:"absolute",top:0,right:0,width:`${Pe}px`,height:"100%",opacity:oe?1:0,transition:"opacity 0.2s ease-in-out",cursor:"pointer",zIndex:1e3,pointerEvents:"auto"},children:[l.visible&&Ce("div",{className:"overlay-scrollbar-track-background",onClick:t=>{t.preventDefault(),t.stopPropagation(),_(t)},style:{position:"absolute",top:V?`${n.width+l.margin*2}px`:`${l.margin}px`,right:l.alignment==="right"?"0px":`${(Pe-n.width)/2}px`,width:`${n.width}px`,height:V?`calc(100% - ${n.width*2+l.margin*4}px)`:`calc(100% - ${l.margin*2}px)`,backgroundColor:l.color,borderRadius:`${l.radius}px`,cursor:"pointer"}}),Ce("div",{ref:re,className:"overlay-scrollbar-thumb",onMouseDown:Oe,onMouseEnter:()=>we(!0),onMouseLeave:()=>we(!1),style:{position:"absolute",top:`${(V?g+l.margin*2:l.margin)+He}px`,right:l.alignment==="right"?"0px":`${(Pe-g)/2}px`,width:`${g}px`,height:`${Math.max(ae,de)}px`,backgroundColor:ie||E?n.hoverColor:n.color,opacity:ie||E?n.hoverOpacity:n.opacity,borderRadius:`${n.radius}px`,cursor:"pointer",transition:"background-color 0.2s ease-in-out, opacity 0.2s ease-in-out"}})]}),j&&X&&V&&Ce("div",{className:"overlay-scrollbar-up-arrow",onClick:st,onMouseEnter:()=>ce("up"),onMouseLeave:()=>ce(null),style:{position:"absolute",top:`${l.margin}px`,right:l.alignment==="right"?"0px":`${(Pe-g)/2}px`,width:`${g}px`,height:`${g}px`,cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",fontSize:`${Math.max(g*.75,8)}px`,color:Te==="up"?m.hoverColor:m.color,userSelect:"none",zIndex:1001,opacity:oe?Te==="up"?m.hoverOpacity:m.opacity:0,transition:"opacity 0.2s ease-in-out, color 0.15s ease-in-out"},children:"\u25B2"}),j&&X&&V&&Ce("div",{className:"overlay-scrollbar-down-arrow",onClick:ct,onMouseEnter:()=>ce("down"),onMouseLeave:()=>ce(null),style:{position:"absolute",bottom:`${l.margin}px`,right:l.alignment==="right"?"0px":`${(Pe-g)/2}px`,width:`${g}px`,height:`${g}px`,cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",fontSize:`${Math.max(g*.75,8)}px`,color:Te==="down"?m.hoverColor:m.color,userSelect:"none",zIndex:1001,opacity:oe?Te==="down"?m.hoverOpacity:m.opacity:0,transition:"opacity 0.2s ease-in-out, color 0.15s ease-in-out"},children:"\u25BC"})]})}),it=kt;import{Fragment as _e,jsx as d,jsxs as Le}from"react/jsx-runtime";var Pt={alignment:"right",margin:0,radius:0};function Bt({data:s,loading:p=!1,columns:h,onRowClick:x,rowHeight:D=50,columnHeight:k=56,striped:T,rowDivider:B=!0,onSort:L,onLoadMore:N,sortBy:v,sortDirection:j,showPaper:Z=!0,paddingX:Q="1rem",paddingTop:ge=0,paddingBottom:ee=0,rowHoverColor:a,rowHoverOpacity:z,scrollbars:te,emptyMessage:re="NO DATA",LoadingComponent:$}){let oe=Qe(()=>Ze((r,n)=>{let l=q(null);return d(it,{detectInnerScroll:!0,track:Pt,...te,children:d(Ht,{component:he,...r,ref:m=>{l.current=m,typeof n=="function"?n(m):n&&(n.current=m)},sx:{userSelect:"auto",WebkitUserSelect:"auto",position:"relative",width:"100%",height:"100%",overflow:"auto",display:"flex",flexDirection:"column",scrollbarWidth:"none",msOverflowStyle:"none",transform:"translateZ(0)",backfaceVisibility:"hidden",willChange:"scroll-position","&::-webkit-scrollbar":{display:"none"},"& .MuiTable-root":{paddingRight:Q,paddingTop:ge,paddingBottom:ee}}})})}),[]),S=Qe(()=>{if(T===!0)return"#f5f5f5";if(typeof T=="string")return T},[T]),[E,ne]=lt(p),[ie,we]=lt(0);ke(()=>{p&&ne(!0)},[p]);let le=me(()=>{ne(!1)},[]),De=E,ae=p&&s.length>0,Ee=q(!1),He=q(null),Y=q(null),X=q(!1),Re=q({x:0,y:0,scrollTop:0}),se=q(!1),Ie=q(0),K=q(0),Me=q(!1),Ae=q({x:0,y:0}),at=q(null),Te=me(r=>{},[]),ce=me(r=>{if(!se.current||!Y.current)return;let n=r.clientY-Re.current.y;if(!X.current&&Math.abs(n)>5&&(X.current=!0,Y.current&&(Y.current.style.userSelect="none")),X.current){let m=n*2;K.current+=-m;let W=Y.current,e=Math.max(0,Ie.current+K.current);W.scrollTop=e,Re.current.y=r.clientY,r.preventDefault()}},[]),Ye=me(()=>{if(se.current=!1,X.current&&Y.current){let r=Math.max(0,Ie.current+K.current),n=()=>{Y.current&&(Y.current.scrollTop=r)};n(),setTimeout(n,1),setTimeout(n,5),setTimeout(n,10)}X.current=!1,K.current=0,Y.current&&(Y.current.style.userSelect="auto")},[]);ke(()=>{let r=document.querySelector('[data-testid="virtuoso-scroller"]');r&&r.querySelectorAll(".MuiTableSortLabel-root").forEach(l=>{let m=new MouseEvent("mouseleave",{bubbles:!0,cancelable:!0});l.dispatchEvent(m)})},[v]);let Se=me(r=>{if(!L)return;L(r,v===r&&j==="asc"?"desc":"asc")},[L,v,j]),be=me(r=>{if(!N||p&&s.length===0)return;let n=Date.now(),l=window.lastRangeChangeTime||0;if(n-l<100)return;window.lastRangeChangeTime=n;let m=Math.max(10,Math.floor(s.length*.1)),W=r.endIndex>=s.length-m,e=s.length>=30;if(W&&e&&N&&!Ee.current){Ee.current=!0;let g=s.length;N(g,50)}},[s.length,p,N]);ke(()=>{p||(Ee.current=!1)},[p]),ke(()=>{s.length===0&&we(r=>r+1)},[s.length]),ke(()=>{He.current&&s.length>0&&He.current.scrollToIndex({index:0,align:"start",behavior:"auto"})},[s]),ke(()=>(document.addEventListener("mousemove",ce),document.addEventListener("mouseup",Ye),()=>{document.removeEventListener("mousemove",ce),document.removeEventListener("mouseup",Ye)}),[ce,Ye]);let qe=me(()=>{let r={},n=[];if(h.forEach(e=>{e.group?(r[e.group]||(r[e.group]=[]),r[e.group].push(e)):n.push(e)}),!(Object.keys(r).length>0))return d(Fe,{children:h.map(e=>d(ze,{align:e.align||"left",style:{width:e.width,minWidth:e.width,...e.style,fontWeight:"bold",position:"sticky",top:0,zIndex:2,padding:"16px"},children:e.sortable?d("div",{style:{display:"flex",alignItems:"center",justifyContent:e.align==="center"?"center":e.align==="right"?"flex-end":"flex-start",position:"relative",width:"100%"},children:Le(he,{sx:{position:"relative",cursor:"default","&:hover .MuiTableSortLabel-root":{opacity:"1 !important","& .MuiSvgIcon-root":{color:"#000",opacity:"1 !important"}}},onClick:()=>Se(String(e.id)),children:[e.text,d(et,{active:v===e.id,direction:v===e.id?j:"desc",sx:{position:"absolute",left:"100%",top:"50%",transform:"translateY(-50%)",marginLeft:"4px",minWidth:"auto",width:"16px",height:"16px",cursor:"default",opacity:v===e.id?1:0,transition:"opacity 0.2s ease","& .MuiTableSortLabel-icon":{position:"relative",marginLeft:0,marginRight:0,opacity:1,transition:"color 0.2s ease"},"& .MuiTableSortLabel-iconDirectionAsc":{transform:"rotate(180deg)"},"& .MuiTableSortLabel-iconDirectionDesc":{transform:"rotate(0deg)"}}})]})}):e.text},String(e.id)))});let m=[...n.map(e=>d(ze,{rowSpan:2,align:e.align||"left",style:{width:e.width,minWidth:e.width,...e.style,fontWeight:"bold",position:"sticky",top:0,zIndex:2,padding:"16px"},children:e.sortable&&L?d("div",{style:{display:"flex",alignItems:"center",justifyContent:e.align==="center"?"center":e.align==="right"?"flex-end":"flex-start",position:"relative",width:"100%"},children:Le(he,{sx:{position:"relative",cursor:"default","&:hover .MuiTableSortLabel-root":{opacity:"1 !important","& .MuiSvgIcon-root":{color:"#000",opacity:"1 !important"}}},onClick:()=>Se(String(e.id)),children:[e.text,d(et,{active:v===e.id,direction:v===e.id?j:"desc",sx:{position:"absolute",left:"100%",top:"50%",transform:"translateY(-50%)",marginLeft:"4px",minWidth:"auto",width:"16px",height:"16px",cursor:"default",opacity:v===e.id?1:0,transition:"opacity 0.2s ease","&:hover":{opacity:"1 !important","& .MuiSvgIcon-root":{color:"#000",opacity:"1 !important"}},"& .MuiTableSortLabel-icon":{position:"relative",marginLeft:0,marginRight:0,opacity:1,transition:"color 0.2s ease"},"& .MuiTableSortLabel-iconDirectionAsc":{transform:"rotate(180deg)"},"& .MuiTableSortLabel-iconDirectionDesc":{transform:"rotate(0deg)"}}})]})}):e.text},String(e.id))),...Object.entries(r).map(([e,g])=>d(ze,{align:"center",colSpan:g.length,style:{fontWeight:"bold",position:"sticky",top:0,zIndex:2,padding:"16px"},children:e},e))],W=[...Object.values(r).flat().map(e=>d(ze,{align:e.align||"left",style:{width:e.width,minWidth:e.width,...e.style,fontWeight:"bold",position:"sticky",top:0,zIndex:2,padding:"16px"},children:e.sortable&&L?d("div",{style:{display:"flex",alignItems:"center",justifyContent:e.align==="center"?"center":e.align==="right"?"flex-end":"flex-start",position:"relative",width:"100%"},children:Le(he,{sx:{position:"relative",cursor:"default","&:hover .MuiTableSortLabel-root":{opacity:"1 !important","& .MuiSvgIcon-root":{color:"#000",opacity:"1 !important"}}},onClick:()=>Se(String(e.id)),children:[e.text,d(et,{active:v===e.id,direction:v===e.id?j:"desc",sx:{position:"absolute",left:"100%",top:"50%",transform:"translateY(-50%)",marginLeft:"4px",minWidth:"auto",width:"16px",height:"16px",cursor:"default",opacity:v===e.id?1:0,transition:"opacity 0.2s ease","&:hover":{opacity:"1 !important","& .MuiSvgIcon-root":{color:"#000",opacity:"1 !important"}},"& .MuiTableSortLabel-icon":{position:"relative",marginLeft:0,marginRight:0,opacity:1,transition:"color 0.2s ease"},"& .MuiTableSortLabel-iconDirectionAsc":{transform:"rotate(0deg)"},"& .MuiTableSortLabel-iconDirectionDesc":{transform:"rotate(180deg)"}}})]})}):e.text},String(e.id)))];return Le(_e,{children:[d(Fe,{children:m}),d(Fe,{children:W})]})},[h,v,j,Se,L,k]),We=me((r,n)=>n?d(_e,{children:h.map(l=>d(ze,{align:l.align||"left",style:{width:l.width,minWidth:l.width,...l.style,padding:"8px 16px"},children:l.render?l.render(n,r):String(n[l.id]||"")},String(l.id)))}):(console.log("rowContent - \uC544\uC774\uD15C \uC5C6\uC74C, \uC778\uB371\uC2A4:",r),null),[h]),ye=Qe(()=>({Scroller:oe,Table:r=>d(Dt,{...r,sx:{borderCollapse:"separate",tableLayout:"fixed",marginRight:"16px"}}),TableHead:Ze((r,n)=>d(Rt,{...r,ref:n,sx:{userSelect:"none","& tr":{height:k,"& th":{padding:"16px",position:"sticky",top:0,zIndex:2,fontWeight:"bold"}}}})),TableRow:r=>{let{item:n,...l}=r,m=l["data-index"]??0,W=m%2===1;return d(Fe,{...l,onMouseDown:e=>{Me.current=!1,Ae.current={x:e.clientX,y:e.clientY}},onMouseMove:e=>{let g=Math.abs(e.clientX-Ae.current.x),$e=Math.abs(e.clientY-Ae.current.y),de=5;(g>de||$e>de)&&(Me.current=!0)},onClick:()=>{!Me.current&&!X.current&&n&&x&&x(n,m),Me.current=!1},sx:{userSelect:"none",height:D,backgroundColor:W&&S?S:"transparent","& td":{padding:"8px 16px",borderBottom:B?"1px solid rgba(224, 224, 224, 1)":"none"},"& th":{padding:"8px 16px",borderBottom:"none"},"&:hover":x?{backgroundColor:e=>{let g=e.palette.mode==="dark",de=a??"#000000",V=z??.06,pe=de.replace("#",""),H=parseInt(pe.substring(0,2),16)/255,f=parseInt(pe.substring(2,4),16)/255,I=parseInt(pe.substring(4,6),16)/255;if(g){let b=Math.max(H,f,I),xe=Math.min(H,f,I),fe=0,w=0,u=(b+xe)/2;if(b!==xe){let C=b-xe;switch(w=u>.5?C/(2-b-xe):C/(b+xe),b){case H:fe=((f-I)/C+(f<I?6:0))/6;break;case f:fe=((I-H)/C+2)/6;break;case I:fe=((H-f)/C+4)/6;break}}u=1-u;let Oe=(C,F,_)=>(_<0&&(_+=1),_>1&&(_-=1),_<1/6?C+(F-C)*6*_:_<1/2?F:_<2/3?C+(F-C)*(2/3-_)*6:C);if(w===0)H=f=I=u;else{let C=u<.5?u*(1+w):u+w-u*w,F=2*u-C;H=Oe(F,C,fe+1/3),f=Oe(F,C,fe),I=Oe(F,C,fe-1/3)}}return`rgba(${Math.round(H*255)}, ${Math.round(f*255)}, ${Math.round(I*255)}, ${V})`},transition:"background-color 0.2s ease"}:{}}})},TableBody:Ze((r,n)=>d(Et,{...r,ref:n}))}),[x,D,Te,S,B,k,a,z,oe]),ue=Le(he,{sx:{position:"relative",height:"100%",width:"100%","& .MuiTableHead-root":{backgroundColor:r=>r.palette.mode==="dark"?"#1e1e1e !important":"#ffffff !important"}},children:[d(Ot,{ref:He,data:s,totalCount:N?s.length+1:s.length,fixedHeaderContent:qe,itemContent:We,rangeChanged:be,components:ye,style:{height:"100%"},increaseViewportBy:{top:100,bottom:300},overscan:5,followOutput:!1},ie),s.length===0&&!p&&d(he,{sx:{position:"absolute",top:0,left:0,right:0,bottom:0,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:2},children:typeof re=="string"?Le(_e,{children:[d(he,{sx:{width:48,height:48,display:"flex",alignItems:"center",justifyContent:"center",borderRadius:"50%",backgroundColor:"#f5f5f5",color:"#999"},children:"\u{1F4C4}"}),d($t,{variant:"body1",sx:{color:"text.secondary"},children:re})]}):re}),De&&d(_e,{children:$?d($,{visible:p,onComplete:le}):d(rt,{visible:p,onComplete:le,size:40,sx:{top:`${h.some(r=>r.group)?k*2:k}px`},background:{show:s.length===0,opacity:.8}})})]});return Z?d(It,{className:"grow",elevation:1,sx:{padding:0,paddingLeft:Q,height:"100%",minHeight:0,flex:1,display:"flex",flexDirection:"column"},children:ue}):d(he,{className:"grow",style:{padding:0,paddingLeft:Q,height:"100%",minHeight:0,flex:1,display:"flex",flexDirection:"column"},children:ue})}var Nt=Lt(Bt);export{Nt as VirtualDataTable};
|
|
1314
22
|
/**
|
|
1315
|
-
*
|
|
23
|
+
* Virtual Data Table - A high-performance virtual data table component for React
|
|
24
|
+
*
|
|
25
|
+
* @license MIT
|
|
26
|
+
* @copyright 2025 김영진 (Kim Young Jin)
|
|
27
|
+
* @author 김영진 (ehfuse@gmail.com)
|
|
1316
28
|
*/
|
|
1317
|
-
function VirtualDataTableComponent({ data, loading = false, columns, onRowClick, rowHeight = 50, columnHeight = 56, striped, rowDivider = true, onSort, onLoadMore, sortBy, sortDirection, showPaper = true, paddingX = "1rem", paddingTop = 0, paddingBottom = 0, rowHoverColor, rowHoverOpacity, scrollbars, emptyMessage = "NO DATA", LoadingComponent, }) {
|
|
1318
|
-
// 각 테이블 인스턴스별로 Scroller 컴포넌트 생성 (scrollbars, paddingX를 초기값으로 고정)
|
|
1319
|
-
const VirtuosoScroller = useMemo(() => forwardRef((props, ref) => {
|
|
1320
|
-
const scrollContainerRef = useRef(null);
|
|
1321
|
-
return (jsx(OverlayScrollbar, { detectInnerScroll: true, track: OVERLAY_SCROLLBAR_TRACK_CONFIG, ...scrollbars, children: jsx(TableContainer, { component: Box, ...props, ref: (node) => {
|
|
1322
|
-
scrollContainerRef.current =
|
|
1323
|
-
node;
|
|
1324
|
-
if (typeof ref === "function") {
|
|
1325
|
-
ref(node);
|
|
1326
|
-
}
|
|
1327
|
-
else if (ref) {
|
|
1328
|
-
ref.current = node;
|
|
1329
|
-
}
|
|
1330
|
-
}, sx: {
|
|
1331
|
-
userSelect: "auto",
|
|
1332
|
-
WebkitUserSelect: "auto",
|
|
1333
|
-
position: "relative",
|
|
1334
|
-
width: "100%",
|
|
1335
|
-
height: "100%",
|
|
1336
|
-
overflow: "auto",
|
|
1337
|
-
display: "flex",
|
|
1338
|
-
flexDirection: "column",
|
|
1339
|
-
scrollbarWidth: "none",
|
|
1340
|
-
msOverflowStyle: "none",
|
|
1341
|
-
// 깜박임 방지를 위한 GPU 가속
|
|
1342
|
-
transform: "translateZ(0)",
|
|
1343
|
-
backfaceVisibility: "hidden",
|
|
1344
|
-
willChange: "scroll-position",
|
|
1345
|
-
"&::-webkit-scrollbar": {
|
|
1346
|
-
display: "none",
|
|
1347
|
-
},
|
|
1348
|
-
"& .MuiTable-root": {
|
|
1349
|
-
paddingRight: paddingX,
|
|
1350
|
-
paddingTop: paddingTop,
|
|
1351
|
-
paddingBottom: paddingBottom,
|
|
1352
|
-
},
|
|
1353
|
-
} }) }));
|
|
1354
|
-
}),
|
|
1355
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1356
|
-
[] // 빈 배열: 최초 마운트 시에만 생성, scrollbars, paddingX, paddingTop, paddingBottom은 클로저로 고정
|
|
1357
|
-
);
|
|
1358
|
-
// Striped row 배경색 계산
|
|
1359
|
-
const stripedRowColor = useMemo(() => {
|
|
1360
|
-
if (striped === true) {
|
|
1361
|
-
return "#f5f5f5"; // 기본 회색
|
|
1362
|
-
}
|
|
1363
|
-
else if (typeof striped === "string") {
|
|
1364
|
-
return striped; // 사용자 지정 색상
|
|
1365
|
-
}
|
|
1366
|
-
return undefined; // 배경색 없음
|
|
1367
|
-
}, [striped]);
|
|
1368
|
-
// 로딩 상태 관리 (원본 방식)
|
|
1369
|
-
const [internalLoading, setInternalLoading] = useState(loading);
|
|
1370
|
-
// 테이블 재마운트를 위한 키 (데이터가 비워지면 재마운트)
|
|
1371
|
-
const [tableKey, setTableKey] = useState(0);
|
|
1372
|
-
// 로딩 상태 변경 감지
|
|
1373
|
-
useEffect(() => {
|
|
1374
|
-
if (loading) {
|
|
1375
|
-
// 로딩이 시작되면 즉시 표시
|
|
1376
|
-
setInternalLoading(true);
|
|
1377
|
-
}
|
|
1378
|
-
// 로딩이 끝나도 internalLoading은 handleLoadingComplete에서만 false로 설정
|
|
1379
|
-
// LoadingProgress에서는 visible={false}로 페이드아웃을 시작함
|
|
1380
|
-
}, [loading]);
|
|
1381
|
-
// 로딩 완료 핸들러 - LoadingProgress의 페이드아웃이 완료된 후 호출됨
|
|
1382
|
-
const handleLoadingComplete = useCallback(() => {
|
|
1383
|
-
setInternalLoading(false);
|
|
1384
|
-
}, []);
|
|
1385
|
-
// 로딩 오버레이 표시 조건
|
|
1386
|
-
const shouldShowLoading = internalLoading;
|
|
1387
|
-
// 더보기 로딩 여부 (데이터가 있고 로딩 중이면 더보기 로딩)
|
|
1388
|
-
loading && data.length > 0;
|
|
1389
|
-
// 무한 스크롤 로딩 상태 (기존 VirtualDataTable 방식)
|
|
1390
|
-
const isLoadingMoreRef = useRef(false);
|
|
1391
|
-
const virtuosoRef = useRef(null); // TableVirtuoso ref
|
|
1392
|
-
// 스크롤 컨테이너 참조 (OverlayScrollbar용)
|
|
1393
|
-
const scrollContainerRef = useRef(null); // 드래그 스크롤 상태 (OverlayScrollbar 사용시에는 비활성화)
|
|
1394
|
-
const isDraggingRef = useRef(false);
|
|
1395
|
-
const dragStartRef = useRef({ x: 0, y: 0, scrollTop: 0 });
|
|
1396
|
-
const isMouseDownRef = useRef(false);
|
|
1397
|
-
const initialScrollTopRef = useRef(0);
|
|
1398
|
-
const totalDragDistanceRef = useRef(0);
|
|
1399
|
-
const isScrollDraggingRef = useRef(false); // OverlayScrollbar 드래그 스크롤 감지용
|
|
1400
|
-
const mouseDownPositionRef = useRef({ x: 0, y: 0 }); // 마우스 다운 시작 위치
|
|
1401
|
-
useRef(null);
|
|
1402
|
-
/**
|
|
1403
|
-
* 마우스 버튼 누름 이벤트 핸들러
|
|
1404
|
-
* OverlayScrollbar 사용시에는 기본 드래그 스크롤을 비활성화
|
|
1405
|
-
*/
|
|
1406
|
-
const handleMouseDown = useCallback((e) => {
|
|
1407
|
-
// OverlayScrollbar를 사용하므로 기본 드래그 스크롤 비활성화
|
|
1408
|
-
// OverlayScrollbar가 자체적으로 스크롤을 처리함
|
|
1409
|
-
return;
|
|
1410
|
-
}, []);
|
|
1411
|
-
/**
|
|
1412
|
-
* 마우스 이동 이벤트 핸들러
|
|
1413
|
-
* 드래그 스크롤 기능의 핵심 로직
|
|
1414
|
-
*/
|
|
1415
|
-
const handleMouseMove = useCallback((e) => {
|
|
1416
|
-
if (!isMouseDownRef.current || !scrollContainerRef.current)
|
|
1417
|
-
return;
|
|
1418
|
-
const deltaY = e.clientY - dragStartRef.current.y;
|
|
1419
|
-
const threshold = 5; // 드래그 감지 임계값
|
|
1420
|
-
// 임계값을 넘어야 드래그로 인식
|
|
1421
|
-
if (!isDraggingRef.current && Math.abs(deltaY) > threshold) {
|
|
1422
|
-
isDraggingRef.current = true;
|
|
1423
|
-
// DOM 스타일 직접 변경 (리렌더링 방지)
|
|
1424
|
-
if (scrollContainerRef.current) {
|
|
1425
|
-
scrollContainerRef.current.style.userSelect = "none";
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
if (isDraggingRef.current) {
|
|
1429
|
-
// 드래그 거리 누적
|
|
1430
|
-
const dragDelta = deltaY * 2; // 감도 조절
|
|
1431
|
-
totalDragDistanceRef.current += -dragDelta; // 드래그 방향과 반대
|
|
1432
|
-
// 스크롤 위치 계산 (초기 위치 + 누적 드래그 거리)
|
|
1433
|
-
const scrollContainer = scrollContainerRef.current;
|
|
1434
|
-
const newScrollTop = Math.max(0, initialScrollTopRef.current + totalDragDistanceRef.current);
|
|
1435
|
-
// 스크롤 위치 설정
|
|
1436
|
-
scrollContainer.scrollTop = newScrollTop;
|
|
1437
|
-
// 드래그 시작점 업데이트 (연속적인 드래그를 위해)
|
|
1438
|
-
dragStartRef.current.y = e.clientY;
|
|
1439
|
-
e.preventDefault();
|
|
1440
|
-
}
|
|
1441
|
-
}, []);
|
|
1442
|
-
/**
|
|
1443
|
-
* 마우스 버튼 해제 이벤트 핸들러
|
|
1444
|
-
* 드래그 스크롤 종료 및 상태 초기화
|
|
1445
|
-
*/
|
|
1446
|
-
const handleMouseUp = useCallback(() => {
|
|
1447
|
-
isMouseDownRef.current = false;
|
|
1448
|
-
// 드래그가 진행되었다면 최종 스크롤 위치 계산 및 설정
|
|
1449
|
-
if (isDraggingRef.current && scrollContainerRef.current) {
|
|
1450
|
-
const finalScrollTop = Math.max(0, initialScrollTopRef.current + totalDragDistanceRef.current);
|
|
1451
|
-
// 스크롤 위치를 여러 번 강제로 설정하여 확실히 고정
|
|
1452
|
-
const setScrollPosition = () => {
|
|
1453
|
-
if (scrollContainerRef.current) {
|
|
1454
|
-
scrollContainerRef.current.scrollTop = finalScrollTop;
|
|
1455
|
-
}
|
|
1456
|
-
};
|
|
1457
|
-
// 즉시 설정 및 지연 설정 (스크롤 안정화)
|
|
1458
|
-
setScrollPosition();
|
|
1459
|
-
setTimeout(setScrollPosition, 1);
|
|
1460
|
-
setTimeout(setScrollPosition, 5);
|
|
1461
|
-
setTimeout(setScrollPosition, 10);
|
|
1462
|
-
}
|
|
1463
|
-
// 드래그 상태 초기화 (리렌더링 방지를 위해 ref만 사용)
|
|
1464
|
-
isDraggingRef.current = false;
|
|
1465
|
-
totalDragDistanceRef.current = 0;
|
|
1466
|
-
// DOM 스타일 초기화
|
|
1467
|
-
if (scrollContainerRef.current) {
|
|
1468
|
-
scrollContainerRef.current.style.userSelect = "auto";
|
|
1469
|
-
}
|
|
1470
|
-
}, []);
|
|
1471
|
-
// 정렬이 변경될 때 모든 TableSortLabel의 hover 상태 초기화
|
|
1472
|
-
useEffect(() => {
|
|
1473
|
-
// sortBy가 변경되면 모든 TableSortLabel 요소의 hover 상태를 강제로 초기화
|
|
1474
|
-
const tableContainer = document.querySelector('[data-testid="virtuoso-scroller"]');
|
|
1475
|
-
if (tableContainer) {
|
|
1476
|
-
const sortLabels = tableContainer.querySelectorAll(".MuiTableSortLabel-root");
|
|
1477
|
-
sortLabels.forEach((label) => {
|
|
1478
|
-
// 마우스 이벤트를 시뮬레이션하여 hover 상태 해제
|
|
1479
|
-
const mouseLeaveEvent = new MouseEvent("mouseleave", {
|
|
1480
|
-
bubbles: true,
|
|
1481
|
-
cancelable: true,
|
|
1482
|
-
});
|
|
1483
|
-
label.dispatchEvent(mouseLeaveEvent);
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
}, [sortBy]);
|
|
1487
|
-
// 정렬 핸들러
|
|
1488
|
-
const handleSort = useCallback((columnId) => {
|
|
1489
|
-
if (!onSort)
|
|
1490
|
-
return;
|
|
1491
|
-
const newDirection = sortBy === columnId && sortDirection === "asc" ? "desc" : "asc";
|
|
1492
|
-
onSort(columnId, newDirection);
|
|
1493
|
-
}, [onSort, sortBy, sortDirection]);
|
|
1494
|
-
// 가상화 스크롤 범위 변경 감지 핸들러 (기존 VirtualDataTable 방식)
|
|
1495
|
-
const handleRangeChange = useCallback((range) => {
|
|
1496
|
-
// onLoadMore가 없으면 무한 스크롤 비활성화
|
|
1497
|
-
if (!onLoadMore) {
|
|
1498
|
-
return;
|
|
1499
|
-
}
|
|
1500
|
-
// 이미 로딩 중이면 중단 (초기 로딩만 체크, 더 가져오기 로딩은 허용)
|
|
1501
|
-
if (loading && data.length === 0) {
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
// 추가 안전장치: 너무 빠른 연속 호출 방지 (100ms 내 중복 호출 무시)
|
|
1505
|
-
const now = Date.now();
|
|
1506
|
-
const lastTime = window.lastRangeChangeTime || 0;
|
|
1507
|
-
if (now - lastTime < 100) {
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
window.lastRangeChangeTime = now;
|
|
1511
|
-
// 더 보수적인 조건: 90% 지점에서 로드 (기존 VirtualDataTable 방식)
|
|
1512
|
-
const bufferSize = Math.max(10, Math.floor(data.length * 0.1)); // 데이터의 10% 또는 최소 10개
|
|
1513
|
-
const shouldLoadMore = range.endIndex >= data.length - bufferSize;
|
|
1514
|
-
// 추가 조건: 최소 30개 이상의 데이터에서만 더 가져오기 실행
|
|
1515
|
-
const hasMinimumData = data.length >= 30;
|
|
1516
|
-
if (shouldLoadMore &&
|
|
1517
|
-
hasMinimumData &&
|
|
1518
|
-
onLoadMore &&
|
|
1519
|
-
!isLoadingMoreRef.current) {
|
|
1520
|
-
isLoadingMoreRef.current = true;
|
|
1521
|
-
const offset = data.length;
|
|
1522
|
-
const limit = 50;
|
|
1523
|
-
// console.log(">>> loadMore 호출 직전", {
|
|
1524
|
-
// offset,
|
|
1525
|
-
// limit,
|
|
1526
|
-
// range,
|
|
1527
|
-
// dataLength: data.length,
|
|
1528
|
-
// bufferSize,
|
|
1529
|
-
// endIndex: range.endIndex,
|
|
1530
|
-
// threshold: data.length - bufferSize,
|
|
1531
|
-
// timestamp: new Date().toISOString(),
|
|
1532
|
-
// });
|
|
1533
|
-
onLoadMore(offset, limit);
|
|
1534
|
-
// console.log(">>> loadMore 호출 완료");
|
|
1535
|
-
}
|
|
1536
|
-
}, [data.length, loading, onLoadMore]);
|
|
1537
|
-
// 로딩 상태가 변경되면 isLoadingMoreRef 업데이트 (기존 VirtualDataTable 방식)
|
|
1538
|
-
useEffect(() => {
|
|
1539
|
-
if (!loading) {
|
|
1540
|
-
isLoadingMoreRef.current = false;
|
|
1541
|
-
}
|
|
1542
|
-
}, [loading]);
|
|
1543
|
-
// 데이터가 비워지면 테이블을 재마운트하여 스크롤을 맨 위로 이동
|
|
1544
|
-
useEffect(() => {
|
|
1545
|
-
if (data.length === 0) {
|
|
1546
|
-
setTableKey((prev) => prev + 1);
|
|
1547
|
-
}
|
|
1548
|
-
}, [data.length]);
|
|
1549
|
-
/**
|
|
1550
|
-
* 전역 마우스 이벤트 리스너 설정
|
|
1551
|
-
* 드래그가 테이블 영역을 벗어나도 동작하도록 document에 이벤트 리스너 등록
|
|
1552
|
-
*/
|
|
1553
|
-
useEffect(() => {
|
|
1554
|
-
document.addEventListener("mousemove", handleMouseMove);
|
|
1555
|
-
document.addEventListener("mouseup", handleMouseUp);
|
|
1556
|
-
return () => {
|
|
1557
|
-
document.removeEventListener("mousemove", handleMouseMove);
|
|
1558
|
-
document.removeEventListener("mouseup", handleMouseUp);
|
|
1559
|
-
};
|
|
1560
|
-
}, [handleMouseMove, handleMouseUp]);
|
|
1561
|
-
/**
|
|
1562
|
-
* 테이블 고정 헤더 컨텐츠 정의 (기존 VirtualDataTable 스타일)
|
|
1563
|
-
* 정렬 기능이 포함된 컬럼 헤더를 렌더링
|
|
1564
|
-
*/
|
|
1565
|
-
const fixedHeaderContent = useCallback(() => {
|
|
1566
|
-
// 1. 그룹 정보 추출
|
|
1567
|
-
const groupMap = {};
|
|
1568
|
-
const noGroupColumns = [];
|
|
1569
|
-
columns.forEach((col) => {
|
|
1570
|
-
if (col.group) {
|
|
1571
|
-
if (!groupMap[col.group]) {
|
|
1572
|
-
groupMap[col.group] = [];
|
|
1573
|
-
}
|
|
1574
|
-
groupMap[col.group].push(col);
|
|
1575
|
-
}
|
|
1576
|
-
else {
|
|
1577
|
-
noGroupColumns.push(col);
|
|
1578
|
-
}
|
|
1579
|
-
});
|
|
1580
|
-
// 그룹이 있는 경우 2줄 헤더, 없는 경우 1줄 헤더
|
|
1581
|
-
const hasGroups = Object.keys(groupMap).length > 0;
|
|
1582
|
-
if (!hasGroups) {
|
|
1583
|
-
// 단일 행 헤더
|
|
1584
|
-
return (jsx(TableRow, { children: columns.map((col) => (jsx(TableCell, { align: col.align || "left", style: {
|
|
1585
|
-
width: col.width,
|
|
1586
|
-
minWidth: col.width,
|
|
1587
|
-
...col.style,
|
|
1588
|
-
fontWeight: "bold",
|
|
1589
|
-
position: "sticky",
|
|
1590
|
-
top: 0,
|
|
1591
|
-
zIndex: 2,
|
|
1592
|
-
padding: "16px",
|
|
1593
|
-
}, children: col.sortable ? (jsx("div", { style: {
|
|
1594
|
-
display: "flex",
|
|
1595
|
-
alignItems: "center",
|
|
1596
|
-
justifyContent: col.align === "center"
|
|
1597
|
-
? "center"
|
|
1598
|
-
: col.align === "right"
|
|
1599
|
-
? "flex-end"
|
|
1600
|
-
: "flex-start",
|
|
1601
|
-
position: "relative",
|
|
1602
|
-
width: "100%",
|
|
1603
|
-
}, children: jsxs(Box, { sx: {
|
|
1604
|
-
position: "relative",
|
|
1605
|
-
cursor: "default",
|
|
1606
|
-
"&:hover .MuiTableSortLabel-root": {
|
|
1607
|
-
opacity: "1 !important",
|
|
1608
|
-
"& .MuiSvgIcon-root": {
|
|
1609
|
-
color: "#000",
|
|
1610
|
-
opacity: "1 !important",
|
|
1611
|
-
},
|
|
1612
|
-
},
|
|
1613
|
-
}, onClick: () => handleSort(String(col.id)), children: [col.text, jsx(TableSortLabel, { active: sortBy === col.id, direction: sortBy === col.id
|
|
1614
|
-
? sortDirection
|
|
1615
|
-
: "desc", sx: {
|
|
1616
|
-
position: "absolute",
|
|
1617
|
-
left: "100%",
|
|
1618
|
-
top: "50%",
|
|
1619
|
-
transform: "translateY(-50%)",
|
|
1620
|
-
marginLeft: "4px",
|
|
1621
|
-
minWidth: "auto",
|
|
1622
|
-
width: "16px",
|
|
1623
|
-
height: "16px",
|
|
1624
|
-
cursor: "default",
|
|
1625
|
-
opacity: sortBy === col.id ? 1 : 0,
|
|
1626
|
-
transition: "opacity 0.2s ease",
|
|
1627
|
-
"& .MuiTableSortLabel-icon": {
|
|
1628
|
-
position: "relative",
|
|
1629
|
-
marginLeft: 0,
|
|
1630
|
-
marginRight: 0,
|
|
1631
|
-
opacity: 1,
|
|
1632
|
-
transition: "color 0.2s ease",
|
|
1633
|
-
},
|
|
1634
|
-
"& .MuiTableSortLabel-iconDirectionAsc": {
|
|
1635
|
-
transform: "rotate(180deg)",
|
|
1636
|
-
},
|
|
1637
|
-
"& .MuiTableSortLabel-iconDirectionDesc": {
|
|
1638
|
-
transform: "rotate(0deg)",
|
|
1639
|
-
},
|
|
1640
|
-
} })] }) })) : (col.text) }, String(col.id)))) }));
|
|
1641
|
-
}
|
|
1642
|
-
// 2줄 헤더 (그룹이 있는 경우)
|
|
1643
|
-
const firstRowCells = [
|
|
1644
|
-
...noGroupColumns.map((col) => (jsx(TableCell, { rowSpan: 2, align: col.align || "left", style: {
|
|
1645
|
-
width: col.width,
|
|
1646
|
-
minWidth: col.width,
|
|
1647
|
-
...col.style,
|
|
1648
|
-
fontWeight: "bold",
|
|
1649
|
-
position: "sticky",
|
|
1650
|
-
top: 0,
|
|
1651
|
-
zIndex: 2,
|
|
1652
|
-
padding: "16px",
|
|
1653
|
-
}, children: col.sortable && onSort ? (jsx("div", { style: {
|
|
1654
|
-
display: "flex",
|
|
1655
|
-
alignItems: "center",
|
|
1656
|
-
justifyContent: col.align === "center"
|
|
1657
|
-
? "center"
|
|
1658
|
-
: col.align === "right"
|
|
1659
|
-
? "flex-end"
|
|
1660
|
-
: "flex-start",
|
|
1661
|
-
position: "relative",
|
|
1662
|
-
width: "100%",
|
|
1663
|
-
}, children: jsxs(Box, { sx: {
|
|
1664
|
-
position: "relative",
|
|
1665
|
-
cursor: "default",
|
|
1666
|
-
"&:hover .MuiTableSortLabel-root": {
|
|
1667
|
-
opacity: "1 !important",
|
|
1668
|
-
"& .MuiSvgIcon-root": {
|
|
1669
|
-
color: "#000",
|
|
1670
|
-
opacity: "1 !important",
|
|
1671
|
-
},
|
|
1672
|
-
},
|
|
1673
|
-
}, onClick: () => handleSort(String(col.id)), children: [col.text, jsx(TableSortLabel, { active: sortBy === col.id, direction: sortBy === col.id
|
|
1674
|
-
? sortDirection
|
|
1675
|
-
: "desc", sx: {
|
|
1676
|
-
position: "absolute",
|
|
1677
|
-
left: "100%",
|
|
1678
|
-
top: "50%",
|
|
1679
|
-
transform: "translateY(-50%)",
|
|
1680
|
-
marginLeft: "4px",
|
|
1681
|
-
minWidth: "auto",
|
|
1682
|
-
width: "16px",
|
|
1683
|
-
height: "16px",
|
|
1684
|
-
cursor: "default",
|
|
1685
|
-
opacity: sortBy === col.id ? 1 : 0,
|
|
1686
|
-
transition: "opacity 0.2s ease",
|
|
1687
|
-
"&:hover": {
|
|
1688
|
-
opacity: "1 !important",
|
|
1689
|
-
"& .MuiSvgIcon-root": {
|
|
1690
|
-
color: "#000",
|
|
1691
|
-
opacity: "1 !important",
|
|
1692
|
-
},
|
|
1693
|
-
},
|
|
1694
|
-
"& .MuiTableSortLabel-icon": {
|
|
1695
|
-
position: "relative",
|
|
1696
|
-
marginLeft: 0,
|
|
1697
|
-
marginRight: 0,
|
|
1698
|
-
opacity: 1,
|
|
1699
|
-
transition: "color 0.2s ease",
|
|
1700
|
-
},
|
|
1701
|
-
"& .MuiTableSortLabel-iconDirectionAsc": {
|
|
1702
|
-
transform: "rotate(180deg)",
|
|
1703
|
-
},
|
|
1704
|
-
"& .MuiTableSortLabel-iconDirectionDesc": {
|
|
1705
|
-
transform: "rotate(0deg)",
|
|
1706
|
-
},
|
|
1707
|
-
} })] }) })) : (col.text) }, String(col.id)))),
|
|
1708
|
-
...Object.entries(groupMap).map(([group, cols]) => (jsx(TableCell, { align: "center", colSpan: cols.length, style: {
|
|
1709
|
-
fontWeight: "bold",
|
|
1710
|
-
position: "sticky",
|
|
1711
|
-
top: 0,
|
|
1712
|
-
zIndex: 2,
|
|
1713
|
-
padding: "16px",
|
|
1714
|
-
}, children: group }, group))),
|
|
1715
|
-
];
|
|
1716
|
-
const secondRowCells = [
|
|
1717
|
-
...Object.values(groupMap)
|
|
1718
|
-
.flat()
|
|
1719
|
-
.map((col) => (jsx(TableCell, { align: col.align || "left", style: {
|
|
1720
|
-
width: col.width,
|
|
1721
|
-
minWidth: col.width,
|
|
1722
|
-
...col.style,
|
|
1723
|
-
fontWeight: "bold",
|
|
1724
|
-
position: "sticky",
|
|
1725
|
-
top: 0,
|
|
1726
|
-
zIndex: 2,
|
|
1727
|
-
padding: "16px",
|
|
1728
|
-
}, children: col.sortable && onSort ? (jsx("div", { style: {
|
|
1729
|
-
display: "flex",
|
|
1730
|
-
alignItems: "center",
|
|
1731
|
-
justifyContent: col.align === "center"
|
|
1732
|
-
? "center"
|
|
1733
|
-
: col.align === "right"
|
|
1734
|
-
? "flex-end"
|
|
1735
|
-
: "flex-start",
|
|
1736
|
-
position: "relative",
|
|
1737
|
-
width: "100%",
|
|
1738
|
-
}, children: jsxs(Box, { sx: {
|
|
1739
|
-
position: "relative",
|
|
1740
|
-
cursor: "default",
|
|
1741
|
-
"&:hover .MuiTableSortLabel-root": {
|
|
1742
|
-
opacity: "1 !important",
|
|
1743
|
-
"& .MuiSvgIcon-root": {
|
|
1744
|
-
color: "#000",
|
|
1745
|
-
opacity: "1 !important",
|
|
1746
|
-
},
|
|
1747
|
-
},
|
|
1748
|
-
}, onClick: () => handleSort(String(col.id)), children: [col.text, jsx(TableSortLabel, { active: sortBy === col.id, direction: sortBy === col.id
|
|
1749
|
-
? sortDirection
|
|
1750
|
-
: "desc", sx: {
|
|
1751
|
-
position: "absolute",
|
|
1752
|
-
left: "100%",
|
|
1753
|
-
top: "50%",
|
|
1754
|
-
transform: "translateY(-50%)",
|
|
1755
|
-
marginLeft: "4px",
|
|
1756
|
-
minWidth: "auto",
|
|
1757
|
-
width: "16px",
|
|
1758
|
-
height: "16px",
|
|
1759
|
-
cursor: "default",
|
|
1760
|
-
opacity: sortBy === col.id ? 1 : 0,
|
|
1761
|
-
transition: "opacity 0.2s ease",
|
|
1762
|
-
"&:hover": {
|
|
1763
|
-
opacity: "1 !important",
|
|
1764
|
-
"& .MuiSvgIcon-root": {
|
|
1765
|
-
color: "#000",
|
|
1766
|
-
opacity: "1 !important",
|
|
1767
|
-
},
|
|
1768
|
-
},
|
|
1769
|
-
"& .MuiTableSortLabel-icon": {
|
|
1770
|
-
position: "relative",
|
|
1771
|
-
marginLeft: 0,
|
|
1772
|
-
marginRight: 0,
|
|
1773
|
-
opacity: 1,
|
|
1774
|
-
transition: "color 0.2s ease",
|
|
1775
|
-
},
|
|
1776
|
-
"& .MuiTableSortLabel-iconDirectionAsc": {
|
|
1777
|
-
transform: "rotate(0deg)",
|
|
1778
|
-
},
|
|
1779
|
-
"& .MuiTableSortLabel-iconDirectionDesc": {
|
|
1780
|
-
transform: "rotate(180deg)",
|
|
1781
|
-
},
|
|
1782
|
-
} })] }) })) : (col.text) }, String(col.id)))),
|
|
1783
|
-
];
|
|
1784
|
-
return (jsxs(Fragment, { children: [jsx(TableRow, { children: firstRowCells }), jsx(TableRow, { children: secondRowCells })] }));
|
|
1785
|
-
}, [columns, sortBy, sortDirection, handleSort, onSort, columnHeight]);
|
|
1786
|
-
/**
|
|
1787
|
-
* 테이블 행 컨텐츠 렌더링 함수 (기존 VirtualDataTable 스타일)
|
|
1788
|
-
* 각 데이터 항목에 대한 셀 내용을 정의
|
|
1789
|
-
*/
|
|
1790
|
-
const rowContent = useCallback((index, item) => {
|
|
1791
|
-
// console.log("rowContent 렌더링:", { index, item: item ? "있음" : "없음" });
|
|
1792
|
-
if (!item) {
|
|
1793
|
-
console.log("rowContent - 아이템 없음, 인덱스:", index);
|
|
1794
|
-
return null;
|
|
1795
|
-
}
|
|
1796
|
-
return (jsx(Fragment, { children: columns.map((column) => (jsx(TableCell, { align: column.align || "left", style: {
|
|
1797
|
-
width: column.width,
|
|
1798
|
-
minWidth: column.width,
|
|
1799
|
-
...column.style,
|
|
1800
|
-
padding: "8px 16px",
|
|
1801
|
-
}, children: column.render
|
|
1802
|
-
? column.render(item, index)
|
|
1803
|
-
: String(item[column.id] || "") }, String(column.id)))) }));
|
|
1804
|
-
}, [columns]);
|
|
1805
|
-
// 테이블 컴포넌트 정의 (기존 VirtualDataTable 스타일)
|
|
1806
|
-
const VirtuosoTableComponents = useMemo(() => ({
|
|
1807
|
-
// 스크롤 컨테이너 (외부에서 한 번만 생성된 안정적인 컴포넌트 사용)
|
|
1808
|
-
Scroller: VirtuosoScroller,
|
|
1809
|
-
// 테이블 컴포넌트
|
|
1810
|
-
Table: (props) => (jsx(Table, { ...props, sx: {
|
|
1811
|
-
borderCollapse: "separate",
|
|
1812
|
-
tableLayout: "fixed",
|
|
1813
|
-
marginRight: "16px",
|
|
1814
|
-
} })),
|
|
1815
|
-
// 테이블 헤더 (고정 위치)
|
|
1816
|
-
TableHead: forwardRef((props, ref) => (jsx(TableHead, { ...props, ref: ref, sx: {
|
|
1817
|
-
userSelect: "none",
|
|
1818
|
-
"& tr": {
|
|
1819
|
-
height: columnHeight,
|
|
1820
|
-
"& th": {
|
|
1821
|
-
padding: "16px",
|
|
1822
|
-
position: "sticky",
|
|
1823
|
-
top: 0,
|
|
1824
|
-
zIndex: 2,
|
|
1825
|
-
fontWeight: "bold",
|
|
1826
|
-
},
|
|
1827
|
-
},
|
|
1828
|
-
} }))),
|
|
1829
|
-
// 테이블 행 (클릭 이벤트 및 호버 효과 포함)
|
|
1830
|
-
// 테이블 행 (클릭 이벤트 및 호버 효과 포함)
|
|
1831
|
-
TableRow: (props) => {
|
|
1832
|
-
const { item, ...rest } = props;
|
|
1833
|
-
// react-virtuoso는 'data-index' 속성으로 index를 전달합니다
|
|
1834
|
-
const rowIndex = rest["data-index"] ?? 0;
|
|
1835
|
-
const isOddRow = rowIndex % 2 === 1;
|
|
1836
|
-
return (jsx(TableRow, { ...rest, onMouseDown: (e) => {
|
|
1837
|
-
// 마우스 다운 시 드래그 플래그 초기화 및 시작 위치 저장
|
|
1838
|
-
isScrollDraggingRef.current = false;
|
|
1839
|
-
mouseDownPositionRef.current = {
|
|
1840
|
-
x: e.clientX,
|
|
1841
|
-
y: e.clientY,
|
|
1842
|
-
};
|
|
1843
|
-
}, onMouseMove: (e) => {
|
|
1844
|
-
// 마우스가 5px 이상 움직였을 때만 드래그로 간주
|
|
1845
|
-
const deltaX = Math.abs(e.clientX - mouseDownPositionRef.current.x);
|
|
1846
|
-
const deltaY = Math.abs(e.clientY - mouseDownPositionRef.current.y);
|
|
1847
|
-
const dragThreshold = 5; // 5px 임계값
|
|
1848
|
-
if (deltaX > dragThreshold ||
|
|
1849
|
-
deltaY > dragThreshold) {
|
|
1850
|
-
isScrollDraggingRef.current = true;
|
|
1851
|
-
}
|
|
1852
|
-
}, onClick: () => {
|
|
1853
|
-
// 드래그 스크롤이 아니고, 아이템이 있고, onRowClick이 있을 때만 실행
|
|
1854
|
-
if (!isScrollDraggingRef.current &&
|
|
1855
|
-
!isDraggingRef.current &&
|
|
1856
|
-
item &&
|
|
1857
|
-
onRowClick) {
|
|
1858
|
-
onRowClick(item, rowIndex);
|
|
1859
|
-
}
|
|
1860
|
-
// 클릭 후 플래그 리셋
|
|
1861
|
-
isScrollDraggingRef.current = false;
|
|
1862
|
-
}, sx: {
|
|
1863
|
-
userSelect: "none",
|
|
1864
|
-
height: rowHeight,
|
|
1865
|
-
backgroundColor: isOddRow && stripedRowColor
|
|
1866
|
-
? stripedRowColor
|
|
1867
|
-
: "transparent",
|
|
1868
|
-
"& td": {
|
|
1869
|
-
padding: "8px 16px",
|
|
1870
|
-
borderBottom: rowDivider
|
|
1871
|
-
? "1px solid rgba(224, 224, 224, 1)"
|
|
1872
|
-
: "none",
|
|
1873
|
-
},
|
|
1874
|
-
"& th": {
|
|
1875
|
-
padding: "8px 16px",
|
|
1876
|
-
borderBottom: "none",
|
|
1877
|
-
},
|
|
1878
|
-
"&:hover": onRowClick
|
|
1879
|
-
? {
|
|
1880
|
-
backgroundColor: (theme) => {
|
|
1881
|
-
const isDark = theme.palette.mode === "dark";
|
|
1882
|
-
const defaultColor = "#000000";
|
|
1883
|
-
const color = rowHoverColor ?? defaultColor;
|
|
1884
|
-
const opacity = rowHoverOpacity ?? 0.06;
|
|
1885
|
-
// hex를 rgb로 변환
|
|
1886
|
-
const hex = color.replace("#", "");
|
|
1887
|
-
let r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
1888
|
-
let g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
1889
|
-
let b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
1890
|
-
// 다크 모드일 때 밝기만 반전 (HSL 변환)
|
|
1891
|
-
if (isDark) {
|
|
1892
|
-
// RGB to HSL
|
|
1893
|
-
const max = Math.max(r, g, b);
|
|
1894
|
-
const min = Math.min(r, g, b);
|
|
1895
|
-
let h = 0, s = 0, l = (max + min) / 2;
|
|
1896
|
-
if (max !== min) {
|
|
1897
|
-
const d = max - min;
|
|
1898
|
-
s =
|
|
1899
|
-
l > 0.5
|
|
1900
|
-
? d / (2 - max - min)
|
|
1901
|
-
: d / (max + min);
|
|
1902
|
-
switch (max) {
|
|
1903
|
-
case r:
|
|
1904
|
-
h =
|
|
1905
|
-
((g - b) / d +
|
|
1906
|
-
(g < b
|
|
1907
|
-
? 6
|
|
1908
|
-
: 0)) /
|
|
1909
|
-
6;
|
|
1910
|
-
break;
|
|
1911
|
-
case g:
|
|
1912
|
-
h =
|
|
1913
|
-
((b - r) / d +
|
|
1914
|
-
2) /
|
|
1915
|
-
6;
|
|
1916
|
-
break;
|
|
1917
|
-
case b:
|
|
1918
|
-
h =
|
|
1919
|
-
((r - g) / d +
|
|
1920
|
-
4) /
|
|
1921
|
-
6;
|
|
1922
|
-
break;
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
// 밝기만 반전 (0.0 <-> 1.0)
|
|
1926
|
-
l = 1 - l;
|
|
1927
|
-
// HSL to RGB
|
|
1928
|
-
const hue2rgb = (p, q, t) => {
|
|
1929
|
-
if (t < 0)
|
|
1930
|
-
t += 1;
|
|
1931
|
-
if (t > 1)
|
|
1932
|
-
t -= 1;
|
|
1933
|
-
if (t < 1 / 6)
|
|
1934
|
-
return (p + (q - p) * 6 * t);
|
|
1935
|
-
if (t < 1 / 2)
|
|
1936
|
-
return q;
|
|
1937
|
-
if (t < 2 / 3)
|
|
1938
|
-
return (p +
|
|
1939
|
-
(q - p) *
|
|
1940
|
-
(2 / 3 - t) *
|
|
1941
|
-
6);
|
|
1942
|
-
return p;
|
|
1943
|
-
};
|
|
1944
|
-
if (s === 0) {
|
|
1945
|
-
r = g = b = l;
|
|
1946
|
-
}
|
|
1947
|
-
else {
|
|
1948
|
-
const q = l < 0.5
|
|
1949
|
-
? l * (1 + s)
|
|
1950
|
-
: l + s - l * s;
|
|
1951
|
-
const p = 2 * l - q;
|
|
1952
|
-
r = hue2rgb(p, q, h + 1 / 3);
|
|
1953
|
-
g = hue2rgb(p, q, h);
|
|
1954
|
-
b = hue2rgb(p, q, h - 1 / 3);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${opacity})`;
|
|
1958
|
-
},
|
|
1959
|
-
transition: "background-color 0.2s ease",
|
|
1960
|
-
}
|
|
1961
|
-
: {},
|
|
1962
|
-
} }));
|
|
1963
|
-
},
|
|
1964
|
-
// 테이블 바디
|
|
1965
|
-
TableBody: forwardRef((props, ref) => (jsx(TableBody, { ...props, ref: ref }))),
|
|
1966
|
-
}), [
|
|
1967
|
-
onRowClick,
|
|
1968
|
-
rowHeight,
|
|
1969
|
-
handleMouseDown,
|
|
1970
|
-
stripedRowColor,
|
|
1971
|
-
rowDivider,
|
|
1972
|
-
columnHeight,
|
|
1973
|
-
rowHoverColor,
|
|
1974
|
-
rowHoverOpacity,
|
|
1975
|
-
VirtuosoScroller,
|
|
1976
|
-
]);
|
|
1977
|
-
// 공통 테이블 내용
|
|
1978
|
-
const tableContent = (jsxs(Box, { sx: {
|
|
1979
|
-
position: "relative",
|
|
1980
|
-
height: "100%",
|
|
1981
|
-
width: "100%",
|
|
1982
|
-
"& .MuiTableHead-root": {
|
|
1983
|
-
backgroundColor: (theme) => theme.palette.mode === "dark"
|
|
1984
|
-
? "#1e1e1e !important"
|
|
1985
|
-
: "#ffffff !important",
|
|
1986
|
-
},
|
|
1987
|
-
}, children: [jsx(TableVirtuoso, { ref: virtuosoRef, data: data, totalCount: onLoadMore ? data.length + 1 : data.length, fixedHeaderContent: fixedHeaderContent, itemContent: rowContent, rangeChanged: handleRangeChange, components: VirtuosoTableComponents, style: { height: "100%" }, increaseViewportBy: { top: 100, bottom: 300 }, overscan: 5, followOutput: false }, tableKey), data.length === 0 && !loading && (jsx(Box, { sx: {
|
|
1988
|
-
position: "absolute",
|
|
1989
|
-
top: 0,
|
|
1990
|
-
left: 0,
|
|
1991
|
-
right: 0,
|
|
1992
|
-
bottom: 0,
|
|
1993
|
-
display: "flex",
|
|
1994
|
-
flexDirection: "column",
|
|
1995
|
-
alignItems: "center",
|
|
1996
|
-
justifyContent: "center",
|
|
1997
|
-
gap: 2,
|
|
1998
|
-
}, children: typeof emptyMessage === "string" ? (jsxs(Fragment, { children: [jsx(Box, { sx: {
|
|
1999
|
-
width: 48,
|
|
2000
|
-
height: 48,
|
|
2001
|
-
display: "flex",
|
|
2002
|
-
alignItems: "center",
|
|
2003
|
-
justifyContent: "center",
|
|
2004
|
-
borderRadius: "50%",
|
|
2005
|
-
backgroundColor: "#f5f5f5",
|
|
2006
|
-
color: "#999",
|
|
2007
|
-
}, children: "\uD83D\uDCC4" }), jsx(Typography, { variant: "body1", sx: { color: "text.secondary" }, children: emptyMessage })] })) : (emptyMessage) })), shouldShowLoading && (jsx(Fragment, { children: LoadingComponent ? (jsx(LoadingComponent, { visible: loading, onComplete: handleLoadingComplete })) : (jsx(LoadingProgress, { visible: loading, onComplete: handleLoadingComplete, size: 40, sx: {
|
|
2008
|
-
top: `${columns.some((col) => col.group)
|
|
2009
|
-
? columnHeight * 2
|
|
2010
|
-
: columnHeight}px`,
|
|
2011
|
-
}, background: {
|
|
2012
|
-
show: data.length === 0, // 최초 로딩에만 배경 표시
|
|
2013
|
-
opacity: 0.8,
|
|
2014
|
-
} })) }))] }));
|
|
2015
|
-
return showPaper ? (jsx(Paper, { className: "grow", elevation: 1, sx: {
|
|
2016
|
-
padding: 0,
|
|
2017
|
-
paddingLeft: paddingX,
|
|
2018
|
-
height: "100%",
|
|
2019
|
-
minHeight: 0,
|
|
2020
|
-
flex: 1,
|
|
2021
|
-
display: "flex",
|
|
2022
|
-
flexDirection: "column",
|
|
2023
|
-
}, children: tableContent })) : (jsx(Box, { className: "grow", style: {
|
|
2024
|
-
padding: 0,
|
|
2025
|
-
paddingLeft: paddingX,
|
|
2026
|
-
height: "100%",
|
|
2027
|
-
minHeight: 0,
|
|
2028
|
-
flex: 1,
|
|
2029
|
-
display: "flex",
|
|
2030
|
-
flexDirection: "column",
|
|
2031
|
-
}, children: tableContent }));
|
|
2032
|
-
}
|
|
2033
|
-
const VirtualDataTable = memo(VirtualDataTableComponent);
|
|
2034
|
-
|
|
2035
|
-
export { VirtualDataTable };
|
|
2036
29
|
//# sourceMappingURL=index.esm.js.map
|