@ihabdevteam/core 0.322.0 → 0.324.0
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/CHANGELOG.md +557 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +55 -17
- package/dist/packages/core/src/components/Accordion.js +3 -3
- package/dist/packages/core/src/components/Alert.js +2 -2
- package/dist/packages/core/src/components/BarChart.js +120 -119
- package/dist/packages/core/src/components/BottomNav.js +18 -18
- package/dist/packages/core/src/components/BulkBar.js +25 -25
- package/dist/packages/core/src/components/Button.js +16 -16
- package/dist/packages/core/src/components/Calendar.js +17 -17
- package/dist/packages/core/src/components/ChartSection.js +30 -30
- package/dist/packages/core/src/components/CountTile.js +38 -38
- package/dist/packages/core/src/components/DatePicker.js +59 -59
- package/dist/packages/core/src/components/Dialog.js +14 -14
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
- package/dist/packages/core/src/components/GameIntro.js +1 -1
- package/dist/packages/core/src/components/Icon.js +2 -2
- package/dist/packages/core/src/components/ItemActions.js +16 -16
- package/dist/packages/core/src/components/LandingHeader.js +6 -6
- package/dist/packages/core/src/components/LineChart.js +24 -24
- package/dist/packages/core/src/components/Loader.js +5 -5
- package/dist/packages/core/src/components/NoticeBox.js +3 -3
- package/dist/packages/core/src/components/PageTitle.js +2 -2
- package/dist/packages/core/src/components/Pagination.js +4 -4
- package/dist/packages/core/src/components/Panel.js +2 -2
- package/dist/packages/core/src/components/PanelRow.js +6 -13
- package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
- package/dist/packages/core/src/components/PaymentPlan.js +5 -5
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +18 -25
- package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
- package/dist/packages/core/src/components/Popup/Popup.js +27 -27
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
- package/dist/packages/core/src/components/Rating.js +20 -20
- package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
- package/dist/packages/core/src/components/RingChart.js +5 -5
- package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
- package/dist/packages/core/src/components/SegmentedControl.js +34 -34
- package/dist/packages/core/src/components/SideNav.js +37 -37
- package/dist/packages/core/src/components/Slider.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +5 -5
- package/dist/packages/core/src/components/Table.js +257 -98
- package/dist/packages/core/src/components/Tabs.js +12 -12
- package/dist/packages/core/src/components/TextField.js +34 -34
- package/dist/packages/core/src/components/Toast.js +6 -6
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
- package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
- package/dist/packages/core/src/components/TopBanner.js +28 -28
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
- package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
- package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
- package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
- package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +81 -81
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
- package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
- package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
- package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
- package/dist/packages/core/src/components/Training/oneShotSound.js +13 -13
- package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
- package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
- package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
- package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
- package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
- package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
- package/dist/packages/core/src/components/UpdateAlert.js +6 -6
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
- package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +10 -10
- package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
- package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
- package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
- package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
- package/dist/packages/core/src/components/Word/wordScoreStats.js +15 -15
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
- package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
- package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
- package/dist/packages/core/src/components/WordTable.js +130 -130
- package/dist/packages/core/src/components/barChartData.d.ts +16 -1
- package/dist/packages/core/src/components/barChartData.js +49 -29
- package/dist/packages/core/src/components/barChartTypes.js +8 -8
- package/dist/packages/core/src/components/tableTrackList.js +11 -11
- package/dist/packages/core/src/components/updateAlertStack.js +28 -28
- package/dist/packages/core/src/components/useBarChartControl.js +4 -6
- package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
- package/dist/packages/core/src/components/wordTableLayout.d.ts +17 -4
- package/dist/packages/core/src/components/wordTableLayout.js +5 -5
- package/dist/packages/core/src/context/I18nProvider.js +6 -6
- package/dist/packages/core/src/hooks/focusReturn.js +27 -27
- package/dist/packages/core/src/hooks/inputModality.js +13 -13
- package/dist/packages/core/src/hooks/touchToggle.js +39 -39
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
- package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
- package/dist/packages/core/src/hooks/useEditableList.js +4 -4
- package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
- package/dist/packages/core/src/hooks/useFontScale.js +6 -6
- package/dist/packages/core/src/hooks/useIcon.js +7 -7
- package/dist/packages/core/src/hooks/useListControls.js +2 -2
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +3 -3
- package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
- package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
- package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +0 -11
- package/dist/packages/core/src/hooks/useTablePaging.js +29 -34
- package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
- package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
- package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -7
- package/dist/packages/core/src/i18n/locales/en.json +14 -2
- package/dist/packages/core/src/i18n/locales/ko.json +13 -1
- package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
- package/dist/packages/core/src/utils/audioMeter.js +5 -5
- package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
- package/dist/packages/core/src/utils/audioPeaks.js +18 -18
- package/dist/packages/core/src/utils/blockGrid.js +4 -4
- package/dist/packages/core/src/utils/chunkSkew.js +9 -9
- package/dist/packages/core/src/utils/clipboard.js +3 -3
- package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
- package/dist/packages/core/src/utils/date.js +12 -12
- package/dist/packages/core/src/utils/devWarn.js +4 -4
- package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
- package/dist/packages/core/src/utils/deviceKind.js +53 -10
- package/dist/packages/core/src/utils/errorMessage.js +7 -7
- package/dist/packages/core/src/utils/filterPanelSummary.js +14 -14
- package/dist/packages/core/src/utils/format.js +4 -4
- package/dist/packages/core/src/utils/getTrainingData.js +9 -9
- package/dist/packages/core/src/utils/imageConfig.js +17 -17
- package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
- package/dist/packages/core/src/utils/navScroll.js +2 -2
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
- package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +129 -105
- package/dist/packages/core/src/utils/phonemePairSearchTypes.d.ts +2 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
- package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
- package/dist/packages/core/src/utils/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +0 -10
- package/dist/packages/core/src/utils/textSort.js +38 -48
- package/dist/packages/core/src/utils/theme.js +3 -3
- package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
- package/dist/packages/core/src/utils/ttsConfig.js +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +1 -1
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +65 -45
- package/dist/packages/core/src/utils/wordContrast.js +3 -3
- package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
- package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
- package/dist/packages/core/src/utils/wornDate.js +14 -14
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +4 -4
- package/package.json +1 -1
|
@@ -96,21 +96,21 @@ function SortArrow({ dir }) {
|
|
|
96
96
|
* 그대로 산다. 칸에는 `role`·`tabIndex` 를 주지 않으므로 탭 정지점은 단추 하나뿐이다.
|
|
97
97
|
*/
|
|
98
98
|
function TableField({ children, onClick, className, sortDir, ariaSort, onResizeStart }) {
|
|
99
|
-
const
|
|
99
|
+
const inner = _jsxs(_Fragment, { children: [children, sortDir && _jsx(SortArrow, { dir: sortDir })] });
|
|
100
100
|
return (_jsxs("div", { onClick: onClick, role: "columnheader", "aria-sort": ariaSort, className: cx('table-field body-3', className, sortDir ? 'table-field--sorted' : '', onResizeStart ? 'table-field--resizable' : ''), children: [onClick
|
|
101
101
|
/* 단추 안에 **또 다른 조작**을 넣지 않는다 — `label` 에 `role="button"` 인 조각이나
|
|
102
102
|
Checkbox 를 넣어 두면 그것이 이 단추 안으로 들어와 겹친 조작이 된다(HTML 위반).
|
|
103
103
|
그런 머리는 `onClick` 없이 두고, 정렬은 그 조각이 알아서 맡는다. */
|
|
104
|
-
? _jsx("button", { type: "button", className: "table-field__button", children:
|
|
105
|
-
:
|
|
104
|
+
? _jsx("button", { type: "button", className: "table-field__button", children: inner })
|
|
105
|
+
: inner, onResizeStart && (_jsx("span", { className: "table-field__resizer", onPointerDown: onResizeStart, onClick: e => e.stopPropagation(), "aria-hidden": "true" }))] }));
|
|
106
106
|
}
|
|
107
107
|
/** @public */
|
|
108
108
|
export default function Table({ fields = [], headerGroups, items = [], renderRow, onRowClick, isRowClickable, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, mobileGridTemplateColumns, resizable = false, resizeMode = 'grow', resizeStorageKey, size = 'small', title, icon, count, collapsed = false, onToggleCollapse, headerActions, empty, loading, dragOver = false, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }) {
|
|
109
109
|
const { t } = useTranslation();
|
|
110
110
|
const uid = useId();
|
|
111
|
-
const
|
|
111
|
+
const titleId = `table-title-${uid}`;
|
|
112
112
|
/** 줄을 눌러 들어가는 표에서만 쓰는 설명 id — `role="button"` 을 잃은 자리를 메운다 */
|
|
113
|
-
const
|
|
113
|
+
const rowHintId = `table-row-hint-${uid}`;
|
|
114
114
|
/**
|
|
115
115
|
* **좁은 화면에서는 폭 조절을 통째로 끈다** — 손잡이·초기화 단추·저장값.
|
|
116
116
|
*
|
|
@@ -132,9 +132,9 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
132
132
|
* 옆으로 미는 쪽을 골랐다. 그러니 담을 수 없을 만큼 좁을 때 칸을 눌러 담으려 드는 코드를
|
|
133
133
|
* 다시 넣지 말 것 — 그게 위에 적은 48px 사건이었다.
|
|
134
134
|
*/
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
135
|
+
const isNarrow = useMediaQuery('(max-width: 767px)');
|
|
136
|
+
const narrowTemplate = mobileGridTemplateColumns && isNarrow ? mobileGridTemplateColumns : null;
|
|
137
|
+
const canResize = resizable && !isNarrow;
|
|
138
138
|
// ── 컬럼 폭 드래그 리사이즈 (resizable) ──
|
|
139
139
|
/** 만든 사람이 밑값을 안 적은 칸에 쓰는 바닥 — 이보다 좁으면 손잡이를 다시 잡을 수도 없다 */
|
|
140
140
|
const MIN_COL_W = 48;
|
|
@@ -148,7 +148,7 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
148
148
|
*
|
|
149
149
|
* 자동맞춤은 이 값보다 위에서 정의되므로 ref 로 건넨다(isFixedPxRef 와 같은 이유).
|
|
150
150
|
*/
|
|
151
|
-
const
|
|
151
|
+
const minWidthRef = useRef(() => MIN_COL_W);
|
|
152
152
|
const storeKey = resizeStorageKey ? `table-cols:${resizeStorageKey}` : null;
|
|
153
153
|
const fieldsRef = useRef(null);
|
|
154
154
|
/* **가로로 넘치나** — 창 크기·칸 폭·내용에 따라 바뀌므로 지켜본다(말귀 요청 ㊿).
|
|
@@ -162,14 +162,23 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
162
162
|
958/333 인데 셋 다 안 붙은 채 굳었다) 다시 내지는 못했다 — **재현은 못 했지만 구멍은
|
|
163
163
|
실재한다.** 그래서 `ref` 를 **함수로** 받는다: 마디가 바뀔 때마다 React 가 불러 주므로
|
|
164
164
|
보개가 늘 지금 마디를 본다. */
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
165
|
+
const resizeObserverRef = useRef(null);
|
|
166
|
+
const scrollOuterRef = useRef(null);
|
|
167
|
+
const scrollInnerRef = useRef(null);
|
|
168
|
+
const [overflowing, setOverflowing] = useState(false);
|
|
169
|
+
/* 자동맞춤이 통 폭을 따라가려면 신호가 필요하다 — 아래에서 적는다 */
|
|
170
|
+
const [boxWidth, setBoxWidth] = useState(0);
|
|
171
|
+
const measureOverflow = useCallback(() => {
|
|
172
|
+
const el = scrollOuterRef.current;
|
|
173
|
+
if (!el)
|
|
174
|
+
return;
|
|
175
|
+
setOverflowing(el.scrollWidth > el.clientWidth + 1); // 1px 은 반올림 몫
|
|
176
|
+
/* **통 폭도 여기서 적어 둔다.** 자동맞춤은 통이 넓어지면 따라가야 하는데, 통을 보는
|
|
177
|
+
보개(`ResizeObserver`)가 이미 이 함수를 부른다 — 보개를 하나 더 세우거나 `observe`
|
|
178
|
+
의 꼴을 건드리면 바로 위 머리말의 그 경합(0.237.0)을 다시 들인다. **같은 자리에
|
|
179
|
+
얹는다.** 이 함수는 그려질 때마다 불리지만 같은 수면 React 가 다시 안 그리므로
|
|
180
|
+
도는 일은 없다. */
|
|
181
|
+
setBoxWidth(el.clientWidth);
|
|
173
182
|
}, []);
|
|
174
183
|
/* **보개의 목숨을 `ref` 콜백 하나가 쥔다 — effect 와 나눠 갖지 않는다.**
|
|
175
184
|
|
|
@@ -182,34 +191,34 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
182
191
|
|
|
183
192
|
그래서 세우는 것도 거두는 것도 `ref` 콜백이 한다. 마디가 다 떨어졌을 때 거둔다 —
|
|
184
193
|
React 는 떼어낼 때 `ref` 를 `null` 로 불러 주므로 그것이 곧 뒷정리다. */
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
const observe = useCallback((slot, el) => {
|
|
195
|
+
const prev = slot.current;
|
|
196
|
+
if (prev && resizeObserverRef.current)
|
|
197
|
+
resizeObserverRef.current.unobserve(prev);
|
|
198
|
+
slot.current = el;
|
|
190
199
|
if (el) {
|
|
191
200
|
/* 없는 환경(구형 브라우저·SSR·시험 러너)에서는 생성자 호출이 그대로 던져 렌더가
|
|
192
201
|
깨진다 — 보개 없이 아래 `넘침재기()` 로 한 번 잰 값만 둔다.
|
|
193
202
|
**살아 있나를 여기서 본다** — 거둬진 뒤에도 다시 세운다. */
|
|
194
|
-
if (
|
|
195
|
-
|
|
203
|
+
if (!resizeObserverRef.current && typeof ResizeObserver !== 'undefined') {
|
|
204
|
+
resizeObserverRef.current = new ResizeObserver(() => measureOverflow());
|
|
196
205
|
}
|
|
197
|
-
|
|
206
|
+
resizeObserverRef.current?.observe(el);
|
|
198
207
|
}
|
|
199
|
-
else if (
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
else if (!scrollOuterRef.current && !scrollInnerRef.current) {
|
|
209
|
+
resizeObserverRef.current?.disconnect();
|
|
210
|
+
resizeObserverRef.current = null;
|
|
202
211
|
}
|
|
203
|
-
|
|
204
|
-
}, [
|
|
205
|
-
const
|
|
212
|
+
measureOverflow();
|
|
213
|
+
}, [measureOverflow]);
|
|
214
|
+
const attachOuter = useCallback((el) => observe(scrollOuterRef, el), [observe]);
|
|
206
215
|
/* **통만 보면 안엣것이 넓어질 때를 놓친다** — 칸 폭을 끌어 넓히거나 줄이 늘 때다.
|
|
207
216
|
그때 통의 바깥 상자는 그대로이고 `scrollWidth` 만 바뀐다. */
|
|
208
|
-
const
|
|
217
|
+
const attachInner = useCallback((el) => observe(scrollInnerRef, el), [observe]);
|
|
209
218
|
/* **안전줄 — 그려질 때마다 한 번 더 잰다.**
|
|
210
219
|
보개가 어떤 까닭으로든 못 울린 자리를 덮는다. 같은 값이면 React 가 다시 안 그리므로
|
|
211
220
|
도는 일은 없고, 읽는 것은 마디 하나의 `scrollWidth`·`clientWidth` 둘뿐이다. */
|
|
212
|
-
useEffect(() => {
|
|
221
|
+
useEffect(() => { measureOverflow(); });
|
|
213
222
|
/* **창 크기는 따로 듣는다 — 상자 보개에만 기대지 않는다.**
|
|
214
223
|
|
|
215
224
|
넘치나는 `scrollWidth > clientWidth` 인데, `ResizeObserver` 는 **상자가 바뀔 때**만
|
|
@@ -224,8 +233,8 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
224
233
|
useEffect(() => {
|
|
225
234
|
if (typeof window === 'undefined')
|
|
226
235
|
return;
|
|
227
|
-
const
|
|
228
|
-
? requestAnimationFrame(() =>
|
|
236
|
+
const nextFrame = typeof requestAnimationFrame === 'function'
|
|
237
|
+
? requestAnimationFrame(() => measureOverflow())
|
|
229
238
|
: null;
|
|
230
239
|
/* **안-모음: 모을 값이 없다**(2026-10-10 조사에서 한 번 모아 봤다가 되돌림). `넘침재기` 는
|
|
231
240
|
`scrollWidth > clientWidth` 하나를 **불리언**으로 넣는데, 같은 값이면 React 가 그 자리에서
|
|
@@ -234,20 +243,20 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
234
243
|
(처음엔 *"못이 이 배선을 글자로 박아 두었다"* 도 까닭으로 적었는데, 그 못을 **꼴을 재는 쪽으로
|
|
235
244
|
고쳤으니** 이제 그 까닭은 없다 — 지운다. 남는 까닭은 위의 하나뿐이고, 그것만으로 충분하다.
|
|
236
245
|
**까닭이 틀린 주석은 다음 사람에게 잘못된 값을 준다.**) */
|
|
237
|
-
window.addEventListener('resize',
|
|
246
|
+
window.addEventListener('resize', measureOverflow);
|
|
238
247
|
return () => {
|
|
239
|
-
if (
|
|
240
|
-
cancelAnimationFrame(
|
|
241
|
-
window.removeEventListener('resize',
|
|
248
|
+
if (nextFrame != null)
|
|
249
|
+
cancelAnimationFrame(nextFrame);
|
|
250
|
+
window.removeEventListener('resize', measureOverflow);
|
|
242
251
|
};
|
|
243
|
-
}, [
|
|
252
|
+
}, [measureOverflow]);
|
|
244
253
|
const isFixedPxRef = useRef(() => false);
|
|
245
254
|
const defaultColsRef = useRef([]);
|
|
246
255
|
// fit 모드에서는 이웃(next)에서 같은 만큼 덜어 오므로 그 컬럼의 시작 폭도 들고 있어야 한다
|
|
247
256
|
const dragRef = useRef(null);
|
|
248
257
|
/* 저장한 폭 읽기 — 숫자이기만 하면 받던 것을 **유한하고 최소 폭 이상인 수**만 받는다(0·음수·NaN 이 들어오면 격자 틀이
|
|
249
258
|
무효가 됐다). */
|
|
250
|
-
const
|
|
259
|
+
const readStoredWidths = (key) => {
|
|
251
260
|
if (!key || typeof window === 'undefined')
|
|
252
261
|
return null;
|
|
253
262
|
try {
|
|
@@ -258,13 +267,29 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
258
267
|
return null;
|
|
259
268
|
}
|
|
260
269
|
};
|
|
261
|
-
const [colWidths, setColWidths] = useState(() =>
|
|
270
|
+
const [colWidths, setColWidths] = useState(() => readStoredWidths(storeKey));
|
|
262
271
|
/* 마운트된 채 resizeStorageKey 가 바뀌면 **새 열쇠의 폭**을 읽는다 — 마운트 때 한 번만 읽어, 열 수가 같으면 앞 열쇠의 폭이
|
|
263
272
|
다른 열에 그대로 씌워졌다. 렌더 중에 맞춘다(바뀐 그 렌더에서 옛 폭이 한 번이라도 그려지지 않게). */
|
|
264
|
-
const [
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
const [loadedKey, setLoadedKey] = useState(storeKey);
|
|
274
|
+
/**
|
|
275
|
+
* **자동 ↔ 손, 두 모드다. 저장소에 값이 있으면 손이다.**
|
|
276
|
+
*
|
|
277
|
+
* 손으로 조절한 적이 없는 표는 늘 자동맞춤으로 서고, 조절하면 그 값을 쓰고, '칸 너비
|
|
278
|
+
* 초기화' 를 누르면 다시 자동으로 돌아간다(사장님 지시 2026-10-13).
|
|
279
|
+
*
|
|
280
|
+
* 전에는 초기화가 **재서 그 숫자를 저장**했다. 그러면 손으로 끈 것과 구별이 없어지고,
|
|
281
|
+
* 무엇보다 **그때 화면에 있던 줄로 잰 '한 순간의 사진'** 으로 굳었다 — 그 뒤 창을
|
|
282
|
+
* 넓히거나 긴 글자가 든 줄이 들어와도 안 따라왔다. 그것이 "상시로 활용" 이 아니었다.
|
|
283
|
+
*
|
|
284
|
+
* 새 칸을 두지 않고 **저장소의 없음 = 자동** 으로 갈래를 둘로 줄인다. 그래서 이미 끌어 둔
|
|
285
|
+
* 사람의 폭은 판이 올라도 그대로 손 모드로 읽힌다 — **옛 값을 밀지 않는다.**
|
|
286
|
+
*/
|
|
287
|
+
const [manual, setManual] = useState(() => readStoredWidths(storeKey) !== null);
|
|
288
|
+
if (loadedKey !== storeKey) {
|
|
289
|
+
setLoadedKey(storeKey);
|
|
290
|
+
const stored = readStoredWidths(storeKey);
|
|
291
|
+
setColWidths(stored);
|
|
292
|
+
setManual(stored !== null);
|
|
268
293
|
}
|
|
269
294
|
// 최신 colWidths를 참조로 유지 (드래그 종료 시 저장용)
|
|
270
295
|
const colWidthsRef = useRef(colWidths);
|
|
@@ -284,12 +309,12 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
284
309
|
// fit — 이동량을 두 컬럼이 나눠 갖는다. 둘 다 최소폭 밑으로 내려가지 않도록
|
|
285
310
|
// 이동량 자체를 먼저 자른다(한쪽만 자르면 합이 달라져 표 폭이 흔들린다).
|
|
286
311
|
const raw = e.clientX - d.startX;
|
|
287
|
-
const delta = Math.max(
|
|
312
|
+
const delta = Math.max(minWidthRef.current(d.index) - d.startW, Math.min(d.startWNext - minWidthRef.current(d.next), raw));
|
|
288
313
|
copy[d.index] = d.startW + delta;
|
|
289
314
|
copy[d.next] = d.startWNext - delta;
|
|
290
315
|
}
|
|
291
316
|
else {
|
|
292
|
-
copy[d.index] = Math.max(
|
|
317
|
+
copy[d.index] = Math.max(minWidthRef.current(d.index), d.startW + (e.clientX - d.startX));
|
|
293
318
|
}
|
|
294
319
|
colWidthsRef.current = copy;
|
|
295
320
|
setColWidths(copy);
|
|
@@ -331,6 +356,9 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
331
356
|
: { index, startX: e.clientX, startW: measured[index] };
|
|
332
357
|
colWidthsRef.current = measured;
|
|
333
358
|
setColWidths(measured);
|
|
359
|
+
/* **끌기 시작한 그 순간부터 손이다.** 끝날 때 저장하는 것으로 미루면, 끄는 동안
|
|
360
|
+
자동 효과가 살아 있어 끌던 폭을 덮는다(지문의 통 폭이 바뀌면 바로 그렇다). */
|
|
361
|
+
setManual(true);
|
|
334
362
|
/* **안-모음: 곧바로 써야 한다** — `onResizeMove` 가 `colWidthsRef` 를 그 자리에서 맞춘다.
|
|
335
363
|
한 틀 미루면 마지막 이동 직후 손을 떼었을 때 **화면은 새 폭인데 저장소에는 한 박자 전 폭**이
|
|
336
364
|
남는다(그 함수 머리말에 적힌 까닭). 자리 재기와 달리 이것은 *값을 쓰는* 일이다. */
|
|
@@ -342,7 +370,10 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
342
370
|
document.body.style.userSelect = 'none';
|
|
343
371
|
}, [onResizeMove, onResizeEnd, resizeMode, fields]);
|
|
344
372
|
/**
|
|
345
|
-
* **칸 너비를 내용에 맞춰
|
|
373
|
+
* **칸 너비를 내용에 맞춰 잰다 — 재기만 한다.**
|
|
374
|
+
*
|
|
375
|
+
* 쓰는 것도 저장하는 것도 부르는 쪽이 한다(꼬리의 까닭을 볼 것). 전에는 이 함수가
|
|
376
|
+
* 재서 쓰고 **저장까지** 했고, 그것이 '칸 너비 초기화' 를 한 순간의 사진으로 굳혔다.
|
|
346
377
|
*
|
|
347
378
|
* 손으로 끌어 놓은 폭을 그냥 지우면(=기본 격자로 되돌리면) 만든 사람이 적어 둔
|
|
348
379
|
* 틀로 갈 뿐, 지금 화면에 든 글자에는 안 맞는다. 그래서 **재서** 맞춘다 —
|
|
@@ -352,35 +383,35 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
352
383
|
* 고정 px 로 못 박은 열(아이콘·숫자 칸)은 그대로 둔다 — 내용에 맞추면 정해 둔
|
|
353
384
|
* 크기를 잃는다.
|
|
354
385
|
*/
|
|
355
|
-
const
|
|
386
|
+
const measureAutoWidths = useCallback(() => {
|
|
356
387
|
const el = fieldsRef.current;
|
|
357
388
|
const wrap = el?.closest('.table-wrap');
|
|
358
389
|
if (!el || !wrap)
|
|
359
|
-
return;
|
|
360
|
-
const
|
|
390
|
+
return null;
|
|
391
|
+
const before = wrap.style.getPropertyValue('--table-grid-cols');
|
|
361
392
|
wrap.style.setProperty('--table-grid-cols', fields.map((_, i) => (isFixedPxRef.current(i) ? defaultColsRef.current[i] : 'max-content')).join(' '));
|
|
362
393
|
/* 재는 동안만 max-content 다. `.table-fields` 와 각 줄은 서로 다른 격자라
|
|
363
394
|
이때는 열이 어긋나 보이지만, 같은 함수 안에서 되돌리므로 화면에는 안 나온다. */
|
|
364
|
-
const
|
|
395
|
+
const rows = Array.from(wrap.querySelectorAll('.table-row'))
|
|
365
396
|
.filter(r => !r.classList.contains('table-row--loading') && !r.classList.contains('table-row--empty'));
|
|
366
|
-
const
|
|
397
|
+
const measured = fields.map((_, i) => {
|
|
367
398
|
let w = el.children[i]?.getBoundingClientRect().width ?? 0;
|
|
368
|
-
for (const
|
|
369
|
-
const c =
|
|
399
|
+
for (const row of rows) {
|
|
400
|
+
const c = row.children[i];
|
|
370
401
|
if (c)
|
|
371
402
|
w = Math.max(w, c.getBoundingClientRect().width);
|
|
372
403
|
}
|
|
373
404
|
/* 고정 px 열은 정해 둔 값 그대로 — 최소 폭(MIN_COL_W)으로 올린 값을 고정 합에 넣으면 격자에는 원래 값(40px)으로 나가
|
|
374
405
|
그만큼(8px) 표 오른쪽이 비었다(말귀 어드민 버그 탭·쿠폰) */
|
|
375
|
-
const
|
|
376
|
-
return Number.isFinite(
|
|
406
|
+
const fixedWidth = isFixedPxRef.current(i) ? parseFloat(defaultColsRef.current[i]) : NaN;
|
|
407
|
+
return Number.isFinite(fixedWidth) ? fixedWidth : Math.max(minWidthRef.current(i), Math.ceil(w));
|
|
377
408
|
});
|
|
378
409
|
/* 격자가 실제로 채우는 폭 — `clientWidth` 는 안쪽 여백을 포함하므로 뺀다
|
|
379
410
|
(되돌리기 단추 자리로 40px 을 비워 두었다). */
|
|
380
411
|
const cs = window.getComputedStyle(el);
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
wrap.style.setProperty('--table-grid-cols',
|
|
412
|
+
const containerWidth = el.clientWidth - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0);
|
|
413
|
+
if (before)
|
|
414
|
+
wrap.style.setProperty('--table-grid-cols', before);
|
|
384
415
|
else
|
|
385
416
|
wrap.style.removeProperty('--table-grid-cols');
|
|
386
417
|
/* **남는 자리를 비율대로 나눈다.** 잰 값을 그대로 쓰면 칸이 제 글자만큼만
|
|
@@ -391,33 +422,139 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
391
422
|
|
|
392
423
|
`fit` 은 이 값이 fr 가중치가 되어 어차피 비율로 채우지만, 저장되는 숫자도
|
|
393
424
|
같은 뜻이 되게 함께 맞춘다. */
|
|
394
|
-
let
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
if (
|
|
399
|
-
const
|
|
400
|
-
|
|
425
|
+
let finalWidths = measured;
|
|
426
|
+
const fixedTotal = measured.reduce((a, w, i) => a + (isFixedPxRef.current(i) ? w : 0), 0);
|
|
427
|
+
const fluidTotal = measured.reduce((a, w, i) => a + (isFixedPxRef.current(i) ? 0 : w), 0);
|
|
428
|
+
const available = containerWidth - fixedTotal;
|
|
429
|
+
if (containerWidth > 0 && fluidTotal > 0 && available > 0) {
|
|
430
|
+
const ratio = available / fluidTotal;
|
|
431
|
+
finalWidths = measured.map((w, i) => (isFixedPxRef.current(i) ? w : Math.max(minWidthRef.current(i), Math.round(w * ratio))));
|
|
401
432
|
/* 반올림으로 남은 몇 px 은 가장 넓은 유동 열에 얹는다 — 안 그러면 늘 몇 px 이
|
|
402
433
|
비거나 넘친다. 최소 폭에 걸린 열이 있으면 넘칠 수 있는데, 그건 담을 수
|
|
403
434
|
없을 만큼 좁다는 뜻이라 가로 스크롤로 두는 편이 맞다. */
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
406
|
-
let
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
435
|
+
const diff = Math.round(available - finalWidths.reduce((a, w, i) => a + (isFixedPxRef.current(i) ? 0 : w), 0));
|
|
436
|
+
if (diff !== 0) {
|
|
437
|
+
let widestIndex = -1;
|
|
438
|
+
finalWidths.forEach((w, i) => { if (!isFixedPxRef.current(i) && (widestIndex < 0 || w > finalWidths[widestIndex]))
|
|
439
|
+
widestIndex = i; });
|
|
440
|
+
if (widestIndex >= 0)
|
|
441
|
+
finalWidths[widestIndex] = Math.max(minWidthRef.current(widestIndex), finalWidths[widestIndex] + diff);
|
|
411
442
|
}
|
|
412
443
|
}
|
|
413
|
-
|
|
444
|
+
/* **쓰지도 저장하지도 않는다 — 잰 값을 돌려줄 뿐이다.** 자동 모드에서는 저장하면
|
|
445
|
+
안 된다(저장소에 값이 있으면 손 모드이므로, 저장하는 순간 자동으로 돌아갈 길이
|
|
446
|
+
사라진다). 쓰는 것은 부르는 쪽이 한다. */
|
|
447
|
+
return finalWidths;
|
|
448
|
+
}, [fields, resizeMode]);
|
|
449
|
+
/* 잰 값이 지금 것과 같은가 — 아래 효과가 **돌지 않게** 하는 못이다.
|
|
450
|
+
같으면 `setColWidths` 가 옛 배열을 그대로 돌려주어 React 가 다시 안 그린다. */
|
|
451
|
+
const sameWidths = (a, b) => a !== null && a.length === b.length && a.every((n, i) => n === b[i]);
|
|
452
|
+
/* 재는 함수를 참조로 쥔다 — `fields` 가 소비처에서 매 렌더 새 배열인 자리가 많아
|
|
453
|
+
그 identity 를 효과의 걸이에 넣으면 **그려질 때마다** 표 전체를 다시 잰다
|
|
454
|
+
(`max-content` 로 바꿔 모든 칸의 상자를 읽는 일이다. 싸지 않다). */
|
|
455
|
+
const measureRef = useRef(measureAutoWidths);
|
|
456
|
+
useEffect(() => { measureRef.current = measureAutoWidths; });
|
|
457
|
+
/**
|
|
458
|
+
* **자동 모드일 때 다시 잰다 — 줄이 들어온 뒤에, 그리고 바뀔 때마다.**
|
|
459
|
+
*
|
|
460
|
+
* 첫 렌더에는 받는 중이라 **잴 줄이 없다**(`measureAutoWidths` 는 `--loading`·`--empty` 줄을
|
|
461
|
+
* 일부러 뺀다). 그때 한 번 재고 끝내면 빈 표 기준으로 굳으므로, 줄이 들어온 뒤에 잰다.
|
|
462
|
+
*
|
|
463
|
+
* 줄이 없을 때는 `null` 로 두어 **만든 사람이 적은 격자**로 선다 — 머리만 재어 굳히는
|
|
464
|
+
* 것보다 낫다.
|
|
465
|
+
*
|
|
466
|
+
* 좁은 화면에서는 아무것도 안 한다. `canResize` 가 이미 `!isNarrow` 이고, 좁은 화면에서는
|
|
467
|
+
* 폭 조절을 통째로 끄는 것이 이 컴포넌트의 규칙이다(손잡이·초기화 단추·저장값). 자동으로
|
|
468
|
+
* 재면 모바일 격자와 싸운다.
|
|
469
|
+
*/
|
|
470
|
+
/* **다시 재라는 신호.** 초기화 단추가 이것을 올린다 — 없으면 *이미 자동인* 표에서
|
|
471
|
+
단추가 아무 일도 안 한다(저장값을 지워도 모드가 안 바뀌어 아래 지문이 그대로다).
|
|
472
|
+
실제로 그랬다: 눌렀더니 잰 폭이 **지워지고 만든 사람의 틀로 돌아갔다**(`40px 1fr 1fr`).
|
|
473
|
+
손→자동으로 갈 때는 `manual` 이 바뀌어 지문이 달라지므로 그 길만 멀쩡했고, 더 흔한
|
|
474
|
+
자동→자동 길이 조용히 죽어 있었다. 시험 둘이 이것을 잡았다. */
|
|
475
|
+
const [refitNonce, setRefitNonce] = useState(0);
|
|
476
|
+
/* **저장 열쇠가 있는 표만 자동으로 선다.** 열쇠가 없으면 '손으로 조절했다' 를 기억할
|
|
477
|
+
자리가 없어 영영 자동이 되는데, 이 저장소에서 열쇠 없는 `resizable` 표는 전부
|
|
478
|
+
**치면서 걸러 보는 창**이다(`ListPopup` 을 거치는 일곱 자리 + 둘). 거기서는 글자를 칠
|
|
479
|
+
때마다 결과 줄이 갈리고, "줄이 바뀌면 다시 잰다" 가 **보는 사람 눈앞에서 칸을 움직인다**
|
|
480
|
+
— 폭을 기억하지 않는 것과 폭이 매 글자마다 바뀌는 것은 다른 일이고 뒤쪽이 더 거슬린다
|
|
481
|
+
(말귀가 그 창들을 열어 재어 짚었다, 2026-10-13).
|
|
482
|
+
그 창들이 적어 둔 칸 틀에는 까닭이 있다 — `minmax` 바닥은 좁은 창에서 유동 칸이 0px 로
|
|
483
|
+
눌려 글자가 포개지는 것을 막고, 백분율은 걸러도 비율이 안 흔들리게 한다. 자동맞춤은
|
|
484
|
+
유동 칸을 내용 크기로 바꾸므로 그 결을 매번 덮는다.
|
|
485
|
+
나중에 그 창들도 자동으로 서는 게 낫다면 **그 표에 저장 열쇠를 주면** 된다 — 그러면
|
|
486
|
+
처음엔 자동, 끌면 기억, 초기화하면 다시 자동으로 지시가 통째로 들어맞는다. */
|
|
487
|
+
/**
|
|
488
|
+
* **다시 재는 계기에 '줄 바뀜' 은 없다.** 줄이 *처음* 들어온 것만 계기다.
|
|
489
|
+
*
|
|
490
|
+
* 처음에는 "줄이 바뀔 때마다" 재게 두었다(말귀 청의 ①이 그렇게 적혀 있었다). **말귀가
|
|
491
|
+
* 제 앱을 재어 그 청을 스스로 물렸다**(2026-10-13): 저장열쇠가 있는 표 스물여덟 중
|
|
492
|
+
* **열아홉**이 찾는 칸이나 쪽 나누기를 갖고 있다. 거기서는 걸러도, 쪽을 넘겨도 줄이
|
|
493
|
+
* 갈리고 그때마다 칸이 다시 선다 — 예외가 아니라 **대다수**다.
|
|
494
|
+
*
|
|
495
|
+
* 쪽 넘기기가 더 눈에 띈다. 치는 것은 내가 하는 일이라 움직임이 내 탓으로 읽히지만, 쪽
|
|
496
|
+
* 넘기기는 "같은 표의 다음 장" 이라 **같은 표가 다른 모습**으로 보인다.
|
|
497
|
+
*
|
|
498
|
+
* 첫 줄 묶음으로 잰 폭이 뒤 쪽의 긴 글자에 안 맞을 수 있다. 그런데 그것은 **사람이 손으로
|
|
499
|
+
* 끌어 둔 표와 똑같은 흠**이다 — 끌어 둔 폭도 다음 쪽 글자에 안 맞는다. 안 맞으면 초기화를
|
|
500
|
+
* 누르면 되고 그 단추가 바로 그 자리에 있다. **들쭉날쭉한 맞음보다 가만한 안 맞음이 낫다.**
|
|
501
|
+
*
|
|
502
|
+
* ('늦추기' 는 안 쓴다 — 움직임을 늦출 뿐 없애지 못하고, 치다 멈춘 뒤 한 박자 있다 칸이
|
|
503
|
+
* 움직이면 내가 한 일과 끊겨 보여 더 뜻 모를 움직임이 된다.)
|
|
504
|
+
*
|
|
505
|
+
* 지문에 드는 것: 다시 재라는 신호 · 칸 수 · 통 폭 · **줄이 있나 없나**(수가 아니다).
|
|
506
|
+
*/
|
|
507
|
+
const hasRows = items.length > 0;
|
|
508
|
+
/* **재야 하는 조건** — 이것이 같으면 한 번만 잰다. 줄이 있나 없나는 여기 **안 든다**:
|
|
509
|
+
들면 걸러서 0건이 되었다 돌아올 때 지문이 두 번 바뀌어 **다시 재고 칸이 튄다**(실측:
|
|
510
|
+
`200px 400px` → `120px 480px`). 저장 열쇠가 드는 까닭은 열쇠가 갈리면 다른 표의
|
|
511
|
+
조건이기 때문이다 — 안 넣으면 새 열쇠의 표가 옛 표의 잼을 물려받고 안 잰다. */
|
|
512
|
+
const refitBasis = `${storeKey ?? ''}|${refitNonce}|${fields.length}|${boxWidth}`;
|
|
513
|
+
/* 걸이에는 `hasRows` 도 든다 — 줄이 **처음** 들어온 그때 효과가 돌아야 하므로. 두 번째
|
|
514
|
+
부터는 아래 걸쇠가 막는다. */
|
|
515
|
+
const autoSignature = !manual && canResize && storeKey !== null && !loading
|
|
516
|
+
? `${refitBasis}|${hasRows ? 1 : 0}`
|
|
517
|
+
: null;
|
|
518
|
+
const measuredForRef = useRef(null);
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
if (autoSignature === null)
|
|
521
|
+
return;
|
|
522
|
+
/* 아직 잴 줄이 없다 — 먼저 선 격자를 **그대로 둔다.** `null` 로 되돌리지 않는다:
|
|
523
|
+
걸러서 0건이 되었을 때 만든 사람의 틀로 튀고, 줄이 돌아오면 또 튄다. */
|
|
524
|
+
if (!hasRows)
|
|
525
|
+
return;
|
|
526
|
+
/* **이 조건에서 이미 쟀다.** 줄만 갈린 것으로는 다시 재지 않는다(위 머리말). */
|
|
527
|
+
if (measuredForRef.current === refitBasis)
|
|
528
|
+
return;
|
|
529
|
+
const measured = measureRef.current();
|
|
530
|
+
if (!measured)
|
|
531
|
+
return;
|
|
532
|
+
measuredForRef.current = refitBasis;
|
|
533
|
+
setColWidths(prev => (sameWidths(prev, measured) ? prev : measured));
|
|
534
|
+
}, [autoSignature]);
|
|
535
|
+
/**
|
|
536
|
+
* **'칸 너비 초기화' — 자동 모드로 되돌아간다.**
|
|
537
|
+
*
|
|
538
|
+
* 저장값을 **지운다**. 재서 그 숫자를 저장하던 전 꼴은 손으로 끈 것과 구별이 없어졌고
|
|
539
|
+
* 한 순간의 사진으로 굳었다(위 `manual` 머리말).
|
|
540
|
+
*/
|
|
541
|
+
const resetColumns = useCallback(() => {
|
|
414
542
|
if (storeKey) {
|
|
415
543
|
try {
|
|
416
|
-
window.localStorage.
|
|
544
|
+
window.localStorage.removeItem(storeKey);
|
|
417
545
|
}
|
|
418
|
-
catch { /*
|
|
546
|
+
catch { /* 지우기 실패 무시 */ }
|
|
419
547
|
}
|
|
420
|
-
|
|
548
|
+
setManual(false);
|
|
549
|
+
/* **지금 자리에서 곧 재어 쓴다.** 신호(아래)만 올리면 저장 열쇠가 없는 표에서는 아무
|
|
550
|
+
일도 안 일어난다 — 그 표에는 자동 모드가 없으므로 위 지문이 `null` 이고, 눌러도
|
|
551
|
+
만든 사람의 틀로 돌아가 끝난다. 열쇠가 있는 표에서는 아래 신호가 그 뒤로도 계속
|
|
552
|
+
맞춰 주고, 같은 수가 나오면 React 가 다시 안 그린다. */
|
|
553
|
+
const measured = measureRef.current();
|
|
554
|
+
setColWidths(measured);
|
|
555
|
+
colWidthsRef.current = measured;
|
|
556
|
+
setRefitNonce(n => n + 1);
|
|
557
|
+
}, [storeKey]);
|
|
421
558
|
// 컬럼 리사이즈 드래그 도중(pointerup 발생 전) Table이 언마운트되면(예: 필터링/페이지 전환으로
|
|
422
559
|
// 목록이 통째로 바뀜) startResize에서 window에 등록한 pointermove/pointerup 리스너가 정리되지
|
|
423
560
|
// 않고 남아 언마운트된 컴포넌트의 setColWidths를 계속 호출하고, body 커서/userSelect도 고정된
|
|
@@ -463,9 +600,9 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
463
600
|
/* `minmax(auto, 1fr)` 처럼 **안에 든** 것도 잡는다 — 최솟값이 내용 기반이면
|
|
464
601
|
머리와 몸통에서 다르게 풀리는 것은 마찬가지다. 처음에 트랙의 **맨 앞**만 보다가
|
|
465
602
|
심어서 그 구멍을 찾았다(2026-09-09). */
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
devWarnOnce(`table-content-track:${gridTemplateColumns}`, `[ihabdevteam-core] Table 의 gridTemplateColumns 에 내용으로 크기가 정해지는 트랙(${
|
|
603
|
+
const contentTracks = defaultCols.filter((t) => /\b(auto|min-content|max-content)\b|fit-content\(/.test(t));
|
|
604
|
+
if (contentTracks.length > 0) {
|
|
605
|
+
devWarnOnce(`table-content-track:${gridTemplateColumns}`, `[ihabdevteam-core] Table 의 gridTemplateColumns 에 내용으로 크기가 정해지는 트랙(${contentTracks.join(', ')})이 있습니다. ` +
|
|
469
606
|
'머리와 몸통은 서로 다른 격자라 그 트랙이 각자 다르게 풀려 열이 어긋납니다. px 이나 fr 을 쓰세요. ' +
|
|
470
607
|
`(받은 값: ${gridTemplateColumns})`);
|
|
471
608
|
}
|
|
@@ -473,41 +610,63 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
473
610
|
둘에 붙으면 낭독기가 어느 쪽이 지금 정렬인지 못 고른다. 여럿이 오면 **첫 칸만** 방향을 갖고
|
|
474
611
|
나머지는 누를 수 있는 칸의 기본값(none)으로 떨어뜨린 뒤 개발 모드에서 알린다.
|
|
475
612
|
(실제 부르는 쪽 — `useTablePaging`·`ListPopup` — 은 이미 하나만 준다.) */
|
|
476
|
-
const
|
|
613
|
+
const sortedColumn = fields.findIndex(f => !!f.sortDir);
|
|
477
614
|
if (fields.filter(f => !!f.sortDir).length > 1) {
|
|
478
615
|
devWarnOnce('table-multi-sort', '[ihabdevteam-core] Table 의 fields 에 sortDir 이 둘 이상 있습니다. ' +
|
|
479
616
|
'aria-sort 는 표 하나에 한 칸만 가질 수 있어 첫 칸만 방향으로 알리고 나머지는 none 으로 둡니다.');
|
|
480
617
|
}
|
|
481
618
|
/** 머리 칸의 `aria-sort` — 정렬 중인 한 칸만 방향, 누를 수 있는 나머지는 none, 정렬과 무관한 칸에는 안 붙인다 */
|
|
482
619
|
const ariaSortOf = (i) => {
|
|
483
|
-
if (i ===
|
|
620
|
+
if (i === sortedColumn)
|
|
484
621
|
return fields[i].sortDir === 'asc' ? 'ascending' : 'descending';
|
|
485
622
|
return fields[i].onClick ? 'none' : undefined;
|
|
486
623
|
};
|
|
487
624
|
const isFixedPx = (i) => /^\d+(\.\d+)?px$/.test(defaultCols[i] ?? '');
|
|
488
625
|
/** 그 칸에 적힌 밑값 — `minmax(130px, …)` 의 130, 못 박은 `150px` 의 150, 없으면 MIN_COL_W */
|
|
489
|
-
const
|
|
626
|
+
const declaredMinWidth = (i) => {
|
|
490
627
|
const t = defaultCols[i] ?? '';
|
|
491
628
|
const m = /^minmax\(\s*(\d+(?:\.\d+)?)px\s*,/.exec(t) ?? /^(\d+(?:\.\d+)?)px$/.exec(t);
|
|
492
629
|
return m ? parseFloat(m[1]) : MIN_COL_W;
|
|
493
630
|
};
|
|
494
631
|
/* `자동맞춤` 은 이 셋보다 위에서 정의되므로(콜백이라 부를 때 읽힌다) ref 로 건넨다 */
|
|
495
632
|
isFixedPxRef.current = isFixedPx;
|
|
496
|
-
|
|
633
|
+
minWidthRef.current = declaredMinWidth;
|
|
497
634
|
defaultColsRef.current = defaultCols;
|
|
498
635
|
const widthUnit = (i) => (resizeMode === 'fit' && !isFixedPx(i)) ? 'fr' : 'px';
|
|
499
636
|
/* 좁은 화면에서는 저장값을 아예 안 쓴다 — 위 `폭조절` 의 글을 보라(넓은 화면에서 끈 폭이 폰에 씌워진다) */
|
|
637
|
+
/**
|
|
638
|
+
* **줄이 0개일 때도 쥔 폭을 쓴다.** 한때 `(resizeMode === 'fit' || items.length > 0)` 이
|
|
639
|
+
* 붙어 있었다 — `grow` 에서 줄이 없으면 쥔 폭을 버리고 만든 사람의 틀로 되돌렸다.
|
|
640
|
+
* 그 까닭이 지워진 주석에 남아 있다:
|
|
641
|
+
*
|
|
642
|
+
* > 항목이 0개일 때(로딩/빈 상태)는 저장된 절대 px 폭 합이 현재 컨테이너 폭보다 커서
|
|
643
|
+
* > 불필요한 가로 스크롤이 생길 수 있어, 이때는 기본(fr 단위) grid로 되돌린다.
|
|
644
|
+
*
|
|
645
|
+
* **그 막음은 뜻을 잃었다.** `grow` 는 끈 만큼 넓어져 **줄이 있을 때 이미** 그 가로
|
|
646
|
+
* 스크롤이 서는 모드다(그것이 `fit` 과 갈리는 점이다). 저장된 px 합이 통보다 크다는 것은
|
|
647
|
+
* 그 사람이 그만큼 넓혀 두었다는 뜻이다. 그러니 이 조건이 없앤 것은 가로 스크롤이 아니라
|
|
648
|
+
* **빈 상태만 다르게 보이는 것**이었고, 막는 것보다 어긋나게 만드는 쪽이 컸다.
|
|
649
|
+
*
|
|
650
|
+
* 어긋남은 두 자리에서 보인다 — ① 화면에 들어갈 때(`loading` 이면 `items` 가 비어 있다)
|
|
651
|
+
* 만든 사람의 틀로 섰다가 자료가 오면 쥔 폭으로 **튄다**, ② 걸러서 0건이 되면 또 튄다.
|
|
652
|
+
* 말귀가 제 앱을 재어 `grow` 표 스물하나(그중 0건이 날 수 있는 것 열일곱)를 세어 왔다.
|
|
653
|
+
*
|
|
654
|
+
* **그리고 자동맞춤이 이것을 모두의 일로 바꿨다.** 전에는 `colWidths` 가 끌어 본 사람에게만
|
|
655
|
+
* 있어 이 튐도 그 사람에게만 났다. 이제는 아무도 안 끌어도 찬다.
|
|
656
|
+
*
|
|
657
|
+
* 빈 줄(`.table-row--empty`)은 격자 줄이 아니라 `display: flex` 라 칸 폭을 안 쓴다 —
|
|
658
|
+
* 걷어도 움직이는 것은 **머리뿐**이다.
|
|
659
|
+
*/
|
|
500
660
|
const canApplyWidths = colWidths && colWidths.length === fields.length
|
|
501
|
-
&&
|
|
502
|
-
|
|
503
|
-
const effectiveGrid = 좁은틀 ?? (canApplyWidths
|
|
661
|
+
&& !isNarrow;
|
|
662
|
+
const effectiveGrid = narrowTemplate ?? (canApplyWidths
|
|
504
663
|
? fields.map((f, i) => (f.resizable === false && defaultCols[i]) ? defaultCols[i] : `${colWidths[i]}${widthUnit(i)}`).join(' ')
|
|
505
664
|
: gridTemplateColumns);
|
|
506
665
|
const gridStyle = effectiveGrid
|
|
507
666
|
? { '--table-grid-cols': effectiveGrid }
|
|
508
667
|
: undefined;
|
|
509
668
|
const hasHeader = title !== undefined;
|
|
510
|
-
return (_jsxs("div", { ref:
|
|
669
|
+
return (_jsxs("div", { ref: attachOuter, className: `table-wrap table-wrap--${size}${canResize ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop,
|
|
511
670
|
/* **넘칠 때만 초점을 받는다**(말귀 요청 ㊿, 2026-10-01).
|
|
512
671
|
이 통은 `overflow-x: auto` 인데 초점을 못 받았다. 안에 초점 받을 것이 없으면
|
|
513
672
|
자판만 쓰는 사람은 **오른쪽 칸을 영영 못 본다** — Tab 이 왼쪽 칸을 벗어나지
|
|
@@ -518,7 +677,7 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
518
677
|
한 칸씩 는다. 그래서 `scrollWidth > clientWidth` 를 재서 켠다.
|
|
519
678
|
**이름 있는 `region`** 으로 둔다: 이름 없는 `region` 은 낭독기가 건너뛰고,
|
|
520
679
|
초점만 받는 `div` 는 "여기서 뭘 하라는 건지" 를 말해 주지 못한다. */
|
|
521
|
-
tabIndex:
|
|
680
|
+
tabIndex: overflowing ? 0 : undefined, role: overflowing ? 'region' : undefined, "aria-label": overflowing ? (typeof title === 'string' ? title : t('table.scrollRegion', '가로로 굴릴 수 있는 표')) : undefined, children: [!!onRowClick && _jsx("span", { id: rowHintId, className: "table-row-hint", children: t('table.rowActionHint', '줄을 누르면 자세히 봅니다') }), _jsxs("div", { ref: attachInner, role: "table", "aria-labelledby": hasHeader && typeof title === 'string' ? titleId : undefined, className: "table body-2", children: [hasHeader && (_jsxs("div", { className: cx('table-header', onToggleCollapse && 'table-header--clickable'), onClick: onToggleCollapse,
|
|
522
681
|
/**
|
|
523
682
|
* **머리는 늘 `caption` 이다 — 단추가 아니다**(사장님 지시 2026-10-02).
|
|
524
683
|
*
|
|
@@ -540,28 +699,28 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
540
699
|
*/
|
|
541
700
|
role: "caption", children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'),
|
|
542
701
|
/* 여닫힘은 **여는 것**이 말한다 — 전에는 겉의 `div` 에 달려 있었다(위 머리글) */
|
|
543
|
-
ariaExpanded: !collapsed, className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { id:
|
|
702
|
+
ariaExpanded: !collapsed, className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { id: titleId, className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { role: "row", className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { role: "columnheader", "aria-colspan": g.span, className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { role: "presentation", className: cx('table-fields-strip', canResize && 'table-fields-strip--resizable'), children: [_jsx("div", { role: "row", className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir, ariaSort: ariaSortOf(i),
|
|
544
703
|
/* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
|
|
545
704
|
손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
|
|
546
|
-
onResizeStart:
|
|
705
|
+
onResizeStart: canResize && i < fields.length - 1 && f.resizable !== false
|
|
547
706
|
&& (resizeMode !== 'fit' || fields.some((g, j) => j > i && g.resizable !== false))
|
|
548
|
-
? (e) => startResize(i, e) : undefined, children: f.label }, i))) }),
|
|
707
|
+
? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), canResize && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: "chevrons-left-right-ellipsis", ariaLabel: t('table.resetColumns', '칸 너비 초기화'), showTooltipInfo: t('table.resetColumns', '칸 너비 초기화'), className: "table-fields__reset", onClick: resetColumns }))] }), _jsx("div", { role: "rowgroup", className: "table-body", children: loading ? (_jsx(TableRow, { variant: "loading", colSpan: fields.length || undefined })) : items.length === 0 && empty ? (_jsx(TableRow, { variant: "empty", colSpan: fields.length || undefined, children: empty })) : items.map((item, idx) => {
|
|
549
708
|
const isDragging = draggingId != null && item.id === draggingId;
|
|
550
|
-
const
|
|
709
|
+
const clickable = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
|
|
551
710
|
/* 글자로 견준다 — URL·폼에서 온 '5' 와 자료의 5 가 같은 줄인데 `===` 로는 안 펴졌다 */
|
|
552
711
|
const isExpanded = expandedId != null && item.id != null && (expandedId instanceof Set
|
|
553
712
|
? expandedId.has(item.id) || [...expandedId].some(x => String(x) === String(item.id))
|
|
554
713
|
: String(item.id) === String(expandedId));
|
|
555
714
|
return (_jsx(TableRow
|
|
556
715
|
// 타입까지 열쇠에 — id 없는 줄(idx 0)과 id:0, 1 과 '1' 이 같은 열쇠가 되어 목록을 바꾼 뒤 옛 줄이 남았다
|
|
557
|
-
, { className: `${
|
|
716
|
+
, { className: `${clickable ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
|
|
558
717
|
// 옆의 핸들러들처럼 있을 때만 넘긴다. 항상 함수를 만들어 넘기면
|
|
559
718
|
// TableRow 가 "클릭 가능"으로 보고 role="button" · tabIndex=0 을 붙여,
|
|
560
719
|
// 클릭 동작이 없는 표의 모든 행이 버튼으로 읽히고 탭 정지점이 된다.
|
|
561
|
-
onClick:
|
|
720
|
+
onClick: clickable ? () => onRowClick(item) : undefined, colSpan: fields.length || undefined,
|
|
562
721
|
/* **줄 안에 진짜 단추가 서면 줄은 정지점에서 내린다** — 한 가지 일에
|
|
563
722
|
정지점이 둘이면 자판으로 도는 사람이 같은 줄을 두 번 잡는다(0.254.0 에서
|
|
564
723
|
접기 머리에 걷은 그 꼴). 설명도 그때는 필요 없다 — 단추가 제 이름으로 말한다. */
|
|
565
|
-
rowHasButton: renderRow(item).some(c => !!c.rowLabel), describedBy:
|
|
724
|
+
rowHasButton: renderRow(item).some(c => !!c.rowLabel), describedBy: rowHintId, onMouseEnter: onRowMouseEnter ? (e) => onRowMouseEnter(item, e) : undefined, onMouseLeave: onRowMouseLeave ? (e) => onRowMouseLeave(item, e) : undefined, onDragStart: onRowDragStart ? (e) => onRowDragStart(item, e) : undefined, onDragEnd: onRowDragEnd ? (e) => onRowDragEnd(item, e) : undefined, expandedContent: isExpanded && renderExpandedRow ? renderExpandedRow(item) : undefined, children: renderRow(item).map((cell, i) => (_jsx(TableCell, { className: cell.className, style: cell.style, rowLabel: clickable ? cell.rowLabel : undefined, onRowClick: clickable ? () => onRowClick(item) : undefined, children: cell.content }, i))) }, item.id != null ? `${typeof item.id}:${item.id}` : `__idx:${idx}`));
|
|
566
725
|
}) })] }))] })] }));
|
|
567
726
|
}
|