@ihabdevteam/core 0.141.0 → 0.143.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 +73 -0
- package/dist/components.cjs.js +176 -176
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +182 -182
- package/dist/ihabdevteam-core.d.ts +81 -29
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +4 -3
- package/dist/packages/core/src/components/BottomNav.js +4 -0
- package/dist/packages/core/src/components/BulkBar.js +15 -4
- package/dist/packages/core/src/components/Button.js +10 -4
- package/dist/packages/core/src/components/DatePicker.js +5 -1
- package/dist/packages/core/src/components/Dialog.js +3 -1
- package/dist/packages/core/src/components/Dropdown.js +6 -2
- package/dist/packages/core/src/components/FileUpload.js +3 -1
- package/dist/packages/core/src/components/FilterPanel.js +3 -2
- package/dist/packages/core/src/components/PanelRow.js +5 -0
- package/dist/packages/core/src/components/PaymentPlan.d.ts +2 -2
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +3 -1
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +5 -2
- package/dist/packages/core/src/components/Popup/Popup.d.ts +6 -6
- package/dist/packages/core/src/components/Popup/Popup.js +11 -9
- package/dist/packages/core/src/components/Popup/customerDetailUtils.d.ts +1 -1
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +1 -1
- package/dist/packages/core/src/components/TextField.js +3 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +7 -1
- package/dist/packages/core/src/components/Training/TrainingBlock.js +2 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +7 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +52 -2
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +3 -1
- package/dist/packages/core/src/components/Training/WordTablePopup.js +9 -1
- package/dist/packages/core/src/components/Training/useAudioPreview.js +28 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +19 -5
- package/dist/packages/core/src/components/Word/WordTableBlock.js +5 -2
- package/dist/packages/core/src/components/Word/WordTableButton.d.ts +3 -3
- package/dist/packages/core/src/components/Word/WordTableButton.js +1 -1
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +1 -1
- package/dist/packages/core/src/components/Word/wordTableBlockTypes.d.ts +2 -2
- package/dist/packages/core/src/components/barChartData.d.ts +3 -5
- package/dist/packages/core/src/components/barChartData.js +7 -3
- package/dist/packages/core/src/components/barChartTypes.d.ts +16 -3
- package/dist/packages/core/src/components/barChartTypes.js +27 -13
- package/dist/packages/core/src/components.d.ts +1 -0
- package/dist/packages/core/src/hooks/focusReturn.d.ts +4 -0
- package/dist/packages/core/src/hooks/focusReturn.js +27 -0
- package/dist/packages/core/src/hooks/inputModality.d.ts +2 -0
- package/dist/packages/core/src/hooks/inputModality.js +31 -0
- package/dist/packages/core/src/hooks/useDocumentTitle.d.ts +12 -0
- package/dist/packages/core/src/hooks/useDocumentTitle.js +7 -6
- package/dist/packages/core/src/hooks/useEditableList.js +3 -3
- package/dist/packages/core/src/hooks/useWordMeanings.js +6 -3
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +2 -2
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +54 -17
- package/dist/packages/core/src/utils/date.d.ts +18 -0
- package/dist/packages/core/src/utils/date.js +23 -0
- package/dist/packages/core/src/utils/format.js +2 -1
- package/dist/packages/core/src/utils/imageConfig.d.ts +2 -0
- package/dist/packages/core/src/utils/imageConfig.js +34 -5
- package/dist/packages/core/src/utils/josa.js +2 -0
- package/dist/packages/core/src/utils/koreanSyllable.d.ts +5 -0
- package/dist/packages/core/src/utils/koreanSyllable.js +69 -43
- package/dist/packages/core/src/utils/phonemePairSearch.js +17 -0
- package/dist/packages/core/src/utils/textSort.d.ts +10 -0
- package/dist/packages/core/src/utils/textSort.js +16 -11
- package/dist/packages/core/src/utils/timeoutSignal.d.ts +5 -0
- package/dist/packages/core/src/utils/timeoutSignal.js +12 -0
- package/dist/packages/core/src/utils/ttsConfig.js +2 -1
- package/dist/packages/core/src/utils/wordContrast.js +6 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +14 -5
- package/dist/packages/core/src/utils.d.ts +1 -1
- package/dist/packages/core/src/utils.js +1 -1
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -293,7 +293,7 @@ export declare type BadgeType = 'bar' | 'dot';
|
|
|
293
293
|
/**
|
|
294
294
|
* 막대차트 섹션 — 제목 + 구간 네비게이션(이전/다음 + 드롭다운) + 월/주 토글 + 2계열 막대차트.
|
|
295
295
|
* - 월(month): 월 드롭다운 + 주별 막대(1주~5주)
|
|
296
|
-
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(
|
|
296
|
+
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
297
297
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
298
298
|
*/
|
|
299
299
|
export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, tightMax, avgMode, totalFormat, series, loading, onDetail, inGrid, }: BarChartProps): JSX.Element;
|
|
@@ -395,10 +395,6 @@ export declare interface BarChartProps {
|
|
|
395
395
|
/** 시맨틱 사이즈 — 차트 캔버스 높이·막대 너비 프리셋 */
|
|
396
396
|
export declare type BarChartSize = 'small' | 'medium' | 'large';
|
|
397
397
|
|
|
398
|
-
/**
|
|
399
|
-
* barChartData.ts
|
|
400
|
-
* BarChart의 순수 데이터 계층 — 날짜→월/주/일 막대 데이터 집계 (React 무관).
|
|
401
|
-
*/
|
|
402
398
|
/** 막대 하나의 데이터 — a/b 두 계열. a === -1 이면 미래(데이터 없음)로 표시하지 않음.
|
|
403
399
|
* parts: 다계열 stacked용 계열별 값(있으면 a는 합계로 사용). */
|
|
404
400
|
export declare interface BarData {
|
|
@@ -906,7 +902,9 @@ export declare interface ChartLegendItem {
|
|
|
906
902
|
/** 글자→발음: 자모를 해당 발음 버튼 값으로. 대응 버튼이 없으면 null(선택 해제). */
|
|
907
903
|
export declare function charToPronValue(part: CompositionPart, jamo: string): string | null;
|
|
908
904
|
|
|
909
|
-
/** 날짜별 카운트 포인트 — a/b 2계열 또는 parts(다계열 stacked, a는 합계)
|
|
905
|
+
/** 날짜별 카운트 포인트 — a/b 2계열 또는 parts(다계열 stacked, a는 합계).
|
|
906
|
+
* `date` 는 **KST 달력 날짜를 기기 달력 자정 Date 로** 옮긴 것: 시각이면 `parseLocalYmd(toKstYmd(createdAt))`,
|
|
907
|
+
* 서버가 KST 로 집계한 'YYYY-MM-DD' 면 `parseLocalYmd(log_date)`. */
|
|
910
908
|
export declare interface ChartPoint {
|
|
911
909
|
date: Date;
|
|
912
910
|
a: number;
|
|
@@ -1314,6 +1312,18 @@ export declare interface ContrastFilterPanelProps {
|
|
|
1314
1312
|
className?: string;
|
|
1315
1313
|
}
|
|
1316
1314
|
|
|
1315
|
+
/** @public getControlDateRange 의 결과 — 기간의 첫날 00:00 KST ~ 끝날 23:59:59.999 KST 순간과, 그 두 날의 KST 달력 날짜 */
|
|
1316
|
+
export declare interface ControlDateRange {
|
|
1317
|
+
/** 첫날 00:00:00.000 KST 순간 — 서버에 물을 때 `from.toISOString()` */
|
|
1318
|
+
from: Date;
|
|
1319
|
+
/** 끝날 23:59:59.999 KST 순간 */
|
|
1320
|
+
to: Date;
|
|
1321
|
+
/** 첫날 KST 'YYYY-MM-DD' — 이름표·달력 셈은 `parseLocalYmd(fromYmd)` 로(순간 `from` 을 기기 getter 로 읽으면 해외에서 하루 어긋난다) */
|
|
1322
|
+
fromYmd: string;
|
|
1323
|
+
/** 끝날 KST 'YYYY-MM-DD' */
|
|
1324
|
+
toYmd: string;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1317
1327
|
/** 모드 전환 시 composition의 초성/모음/종성을 대상 모드 값으로 변환. 종성 '없음'(NO_CODA)은 유지. */
|
|
1318
1328
|
export declare function convertComposition(c: WordComposition, to: 'char' | 'pron', preferredCharJung?: string | null, preferredCharJong?: string | null): WordComposition;
|
|
1319
1329
|
|
|
@@ -1376,7 +1386,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1376
1386
|
*
|
|
1377
1387
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1378
1388
|
*/
|
|
1379
|
-
export declare const CORE_VERSION = "0.
|
|
1389
|
+
export declare const CORE_VERSION = "0.143.0";
|
|
1380
1390
|
|
|
1381
1391
|
/** @public */
|
|
1382
1392
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -2206,7 +2216,7 @@ export declare function formatDateLongLocale(iso: string, lang: string): string;
|
|
|
2206
2216
|
export declare function formatDatetimeKo(iso: string): string;
|
|
2207
2217
|
|
|
2208
2218
|
/**
|
|
2209
|
-
* 초 → "M분 S초"(또는 "M분"·"S초"). 없거나 0
|
|
2219
|
+
* 초 → "M분 S초"(또는 "M분"·"S초"). 초 단위로 반올림한 뒤 나눈다 — 없거나(숫자가 아님) 반올림해 0 이하(0.5초 미만 포함)면 '-'.
|
|
2210
2220
|
*
|
|
2211
2221
|
* 문구는 지금 언어로(`customers.duration.*`, 사전에 없으면 한국어). `durationStr` 를 주면
|
|
2212
2222
|
* 그 함수가 만든 글을 그대로 쓴다 — `calcAge` 의 `ageStr` 와 같은 결.
|
|
@@ -2267,11 +2277,13 @@ export declare interface FormatYmdOptions {
|
|
|
2267
2277
|
/** @public */
|
|
2268
2278
|
export declare function getAssetBaseUrl(): string;
|
|
2269
2279
|
|
|
2270
|
-
/**
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2280
|
+
/**
|
|
2281
|
+
* 현재 컨트롤 선택(주/월)의 기간 — 상세보기(기간별 리스트)용. **KST 의 날로 자른다**(해외 기기도 같은 기간).
|
|
2282
|
+
*
|
|
2283
|
+
* @param offset 몇 기간 앞·뒤인가 — `-1` 이면 앞 달 한 달(달력 달 그대로, 30일 앞이 아니다) · 앞 주 한 주.
|
|
2284
|
+
* @public
|
|
2285
|
+
*/
|
|
2286
|
+
export declare function getControlDateRange(control: BarChartControl, offset?: number): ControlDateRange;
|
|
2275
2287
|
|
|
2276
2288
|
/** @public */
|
|
2277
2289
|
export declare function getDailySelection(): {
|
|
@@ -2383,6 +2395,14 @@ export declare function getSteppedPayload(pageAnswers: unknown, itemIndex: numbe
|
|
|
2383
2395
|
/** KST(Asia/Seoul) 기준 오늘 날짜를 'YYYY-MM-DD'로 반환. DB 날짜 비교 기준선으로 사용. */
|
|
2384
2396
|
export declare function getTodayKst(): string;
|
|
2385
2397
|
|
|
2398
|
+
/**
|
|
2399
|
+
* KST 오늘을 **기기 달력의 자정 Date** 로 — 연·월·일 칸을 `getFullYear()/getMonth()/getDate()` 로 세는 곳(BarChart)의
|
|
2400
|
+
* '지금'. 칸에 넣는 자료도 같은 모양(`parseLocalYmd(toKstYmd(시각))`)이어야 해외 기기에서도 KST 의 날에 선다.
|
|
2401
|
+
*
|
|
2402
|
+
* @public
|
|
2403
|
+
*/
|
|
2404
|
+
export declare function getTodayKstDate(): Date;
|
|
2405
|
+
|
|
2386
2406
|
/**
|
|
2387
2407
|
* 등록된 주제 카테고리 목록을 반환합니다.
|
|
2388
2408
|
* configureTrainingCategories() 미호출 시 기본값을 반환합니다.
|
|
@@ -2647,6 +2667,14 @@ export declare const JONGSEONG: readonly ["", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ",
|
|
|
2647
2667
|
|
|
2648
2668
|
export declare const JUNGSEONG: readonly ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"];
|
|
2649
2669
|
|
|
2670
|
+
/**
|
|
2671
|
+
* KST 달력 날짜(연, 0부터 세는 월, 일)의 **00:00 KST 순간.** 일·월은 넘쳐도 된다(`Date.UTC` 처럼 — 32일은 다음 달 1일).
|
|
2672
|
+
* 서버에 기간을 물을 때(`toISOString()`) 기기 시간대가 아니라 KST 의 하루로 자르려고 쓴다.
|
|
2673
|
+
*
|
|
2674
|
+
* @public
|
|
2675
|
+
*/
|
|
2676
|
+
export declare function kstStartOfDay(year: number, month0: number, day: number): Date;
|
|
2677
|
+
|
|
2650
2678
|
/**
|
|
2651
2679
|
* 칸 번호 → 그때 보인 낱말. 채점과 **함께 저장한다.**
|
|
2652
2680
|
*
|
|
@@ -3543,6 +3571,18 @@ export declare type PanelRowToggle = Base & {
|
|
|
3543
3571
|
|
|
3544
3572
|
declare type PanelSize = 'small' | 'medium' | 'large';
|
|
3545
3573
|
|
|
3574
|
+
/**
|
|
3575
|
+
* 'YYYY-MM-DD' 날짜 전용 문자열(예: 서버가 KST 기준으로 집계해 내려주는 log_date)을
|
|
3576
|
+
* 로컬 타임존 자정 Date로 파싱한다. new Date('YYYY-MM-DD')는 UTC 자정으로 해석되어,
|
|
3577
|
+
* UTC보다 서쪽 시간대(예: 미국)에서 getDate()/getMonth() 같은 로컬 getter로 읽으면
|
|
3578
|
+
* 하루 전으로 밀린다 — 차트를 날짜별로 버킷팅할 때 특히 문제가 된다.
|
|
3579
|
+
*
|
|
3580
|
+
* 시각(ISO)에서 KST 의 날을 달력 Date 로 얻으려면 `parseLocalYmd(toKstYmd(시각))`.
|
|
3581
|
+
*
|
|
3582
|
+
* @public
|
|
3583
|
+
*/
|
|
3584
|
+
export declare function parseLocalYmd(ymd: string): Date;
|
|
3585
|
+
|
|
3546
3586
|
/**
|
|
3547
3587
|
* 드롭다운 값을 정렬 상태로 되돌린다.
|
|
3548
3588
|
*
|
|
@@ -3807,13 +3847,13 @@ export declare interface PaymentPlanTier {
|
|
|
3807
3847
|
highlight?: boolean;
|
|
3808
3848
|
/**
|
|
3809
3849
|
* 할인 배지에 '오픈 특가'를 함께 얹는다 — **표시 전용**.
|
|
3810
|
-
* 계정당 1회 제한은 라벨의 '오픈 특가'
|
|
3850
|
+
* 계정당 1회 제한은 `onceOnly`(없으면 라벨의 '오픈 특가' 문자열)로 판정하므로, 이 플래그만으로는
|
|
3811
3851
|
* 구매 횟수가 제한되지 않는다(같은 프로모션이되 1회 제한 대상은 아닌 tier용).
|
|
3812
3852
|
*/
|
|
3813
3853
|
openPromo?: boolean;
|
|
3814
3854
|
/**
|
|
3815
3855
|
* **계정당 1회만 살 수 있는 tier**(오픈 특가 30일 등). 이미 산 것(`ownedCodes`)이면 숨기고 기본 tier 를 카드로 올린다.
|
|
3816
|
-
* 안 주면
|
|
3856
|
+
* 안 주면 라벨에 '오픈 특가' 가 있는지로 판정한다 — 그런데 라벨은 번역되므로 영어 화면에서는 판정이 실패해
|
|
3817
3857
|
* 이미 산 특가 카드가 남고 정가 30일권을 살 길이 없어진다. 번역되는 라벨 대신 이 값을 넘긴다.
|
|
3818
3858
|
*/
|
|
3819
3859
|
onceOnly?: boolean;
|
|
@@ -4113,7 +4153,7 @@ export declare type PopupOptions = {
|
|
|
4113
4153
|
/** @public */
|
|
4114
4154
|
export declare interface PopupProps {
|
|
4115
4155
|
title?: ReactNode;
|
|
4116
|
-
/** 제목 아래 보조 설명(부제). title
|
|
4156
|
+
/** 제목 아래 보조 설명(부제). 제목 줄이 그려질 때(useTitle 이고 title·headerActions·onClose 중 하나가 있을 때) 보인다. */
|
|
4117
4157
|
subtitle?: ReactNode;
|
|
4118
4158
|
headerActions?: ReactNode;
|
|
4119
4159
|
children?: ReactNode | ((opts: PopupOptions) => ReactNode);
|
|
@@ -4135,12 +4175,12 @@ export declare interface PopupProps {
|
|
|
4135
4175
|
/**
|
|
4136
4176
|
* 닫기 직전에 불린다. false를 반환하면 닫기를 중단한다(닫힘 애니메이션도 시작하지 않음).
|
|
4137
4177
|
*
|
|
4138
|
-
* "입력 중인데 정말 닫을까요?" 같은 확인 절차용. onClose에서
|
|
4139
|
-
* onClose는 이미
|
|
4140
|
-
* 거기서 닫기를
|
|
4141
|
-
*
|
|
4178
|
+
* "입력 중인데 정말 닫을까요?" 같은 확인 절차용. onClose에서 막지 않는 이유 —
|
|
4179
|
+
* onClose는 이미 닫힘 애니메이션(260ms)이 끝난 뒤(animateThen의 setTimeout 안)에 불리므로,
|
|
4180
|
+
* 거기서 닫기를 취소하면 창이 한 번 사라졌다가 되살아난다(animateThen 은 콜백 뒤에도 창이
|
|
4181
|
+
* 남아 있으면 되살린다). 사라지지 않고 그대로 머물게 하려면 이 시점에 판단한다.
|
|
4142
4182
|
*
|
|
4143
|
-
* 미지정이면
|
|
4183
|
+
* 미지정이면 묻지 않고 닫는다.
|
|
4144
4184
|
*/
|
|
4145
4185
|
beforeClose?: () => boolean;
|
|
4146
4186
|
/**
|
|
@@ -7296,7 +7336,7 @@ export declare type TrainingPagePopupProps = {
|
|
|
7296
7336
|
* 판에 실제로 걸린 값은 `NoiseSettingsStore.applied` 로 받는다.
|
|
7297
7337
|
*/
|
|
7298
7338
|
noiseOverride?: Partial<CoreNoiseSettings> | ((saved: CoreNoiseSettings) => Partial<CoreNoiseSettings>);
|
|
7299
|
-
/** 훈련 결과 팝업 렌더러 — 없으면 결과
|
|
7339
|
+
/** 훈련 결과 팝업 렌더러 — 없으면 기본 결과 팝업(TrainingResultPopup)을 띄운다 */
|
|
7300
7340
|
renderResultPopup?: (props: {
|
|
7301
7341
|
score?: number | null;
|
|
7302
7342
|
results?: unknown[];
|
|
@@ -7920,6 +7960,18 @@ export declare function useDeviceManagement(): {
|
|
|
7920
7960
|
handleSelectFromList: (form: DeviceForm) => void;
|
|
7921
7961
|
};
|
|
7922
7962
|
|
|
7963
|
+
/**
|
|
7964
|
+
* 현재 화면의 제목을 `document.title`에 반영한다 — `'<제목> · <앱 이름>'`.
|
|
7965
|
+
*
|
|
7966
|
+
* SPA는 라우트를 옮겨도 문서 제목이 그대로라, 브라우저 탭·히스토리·북마크로 위치를
|
|
7967
|
+
* 구분할 수 없고 화면낭독기도 이동을 인지하지 못한다(WCAG 2.4.2).
|
|
7968
|
+
* 여러 화면이 함께 걸면 제목이 쌓여 가장 나중에 건 것이 보이고, 언마운트하면
|
|
7969
|
+
* 남은 것 중 가장 나중에 건 제목으로(남은 것이 없으면 처음 제목으로) 되돌린다.
|
|
7970
|
+
*
|
|
7971
|
+
* @param title 페이지 제목. 비어 있거나 문자열이 아니면 아무것도 하지 않는다.
|
|
7972
|
+
*
|
|
7973
|
+
* @public
|
|
7974
|
+
*/
|
|
7923
7975
|
export declare function useDocumentTitle(title?: string | null): void;
|
|
7924
7976
|
|
|
7925
7977
|
/**
|
|
@@ -8703,9 +8755,9 @@ export declare interface WordFilterSnapshot {
|
|
|
8703
8755
|
sameStructureOnly: boolean;
|
|
8704
8756
|
phonemeView: 'char' | 'pron' | 'direct';
|
|
8705
8757
|
gradeFilter: string[];
|
|
8706
|
-
/** 품사
|
|
8758
|
+
/** 품사 필터 — 고정 코드(noun·verb·modifier). 라벨(옛 스냅샷)도 받는다. 없으면 되돌릴 때 지금 값을 그대로 둔다. */
|
|
8707
8759
|
posFilter?: string[];
|
|
8708
|
-
/** 어종
|
|
8760
|
+
/** 어종 필터 — 고정 코드(native·sino·foreign·mixed). 라벨(옛 스냅샷)도 받는다. 없으면 되돌릴 때 지금 값을 그대로 둔다. */
|
|
8709
8761
|
wordTypeFilter?: string[];
|
|
8710
8762
|
}
|
|
8711
8763
|
|
|
@@ -8888,7 +8940,7 @@ export declare interface WordTableBlockProps {
|
|
|
8888
8940
|
tableName?: string;
|
|
8889
8941
|
/** 전달하면 외부 데이터 표시 모드, 없으면 편집 모드 */
|
|
8890
8942
|
items?: WordSearchResultItem[];
|
|
8891
|
-
/** 전달하면 저장
|
|
8943
|
+
/** 전달하면 저장하기가 저장 팝업(`renderSavePopup` 이 그림 — 없으면 아무것도 뜨지 않는다)을 열고 확인 후 name·items·description을 전달.
|
|
8892
8944
|
* saveAsNew=true면 '다른 이름으로 저장'(수정 대신 새 표 생성)을 뜻한다. */
|
|
8893
8945
|
onSaveComplete?: (name: string, items: WordSearchResultItem[], description?: string, saveAsNew?: boolean) => void;
|
|
8894
8946
|
onLoad?: () => void;
|
|
@@ -8914,7 +8966,7 @@ export declare interface WordTableBlockProps {
|
|
|
8914
8966
|
editingName?: string;
|
|
8915
8967
|
/** 지정하면 인쇄하기 왼쪽에 '취소' 버튼을 표시(PagePopup 닫기 등). */
|
|
8916
8968
|
onCancel?: () => void;
|
|
8917
|
-
/** 전달하면 카드 드래그로 순서 변경
|
|
8969
|
+
/** 전달하면 카드 드래그로 순서 변경 가능. 쪽이 나뉘면 같은 쪽 안에서만 옮기며, 자리는 전체 목록 기준이다. */
|
|
8918
8970
|
onReorder?: (fromIndex: number, toIndex: number) => void;
|
|
8919
8971
|
/** display 모드에서도 칸 수 조절 허용 */
|
|
8920
8972
|
showCellCount?: boolean;
|
|
@@ -8948,19 +9000,19 @@ export declare interface WordTableBlockProps {
|
|
|
8948
9000
|
|
|
8949
9001
|
/**
|
|
8950
9002
|
* @public 단어표를 '문서형' 미리보기 카드로 표현하는 컴포넌트.
|
|
8951
|
-
* 내용 썸네일(WordTableThumbnail) + 이름 +
|
|
9003
|
+
* 내용 썸네일(WordTableThumbnail) + 이름 + 날짜 + (발행 배지 / 더보기 / 스토어 푸터).
|
|
8952
9004
|
*/
|
|
8953
9005
|
export declare function WordTableButton({ items, name, description, createdAt, published, publishedLabel, shared, sharedLabel, isNew, publisherName, publishedAt, likeCount, downloadCount, onPublisherClick, selected, onClick, onMore, titleNode, className, }: WordTableButtonProps): JSX.Element;
|
|
8954
9006
|
|
|
8955
9007
|
/** @public */
|
|
8956
9008
|
export declare interface WordTableButtonProps {
|
|
8957
|
-
/** 단어표 내용(썸네일 미리보기
|
|
9009
|
+
/** 단어표 내용(썸네일 미리보기) */
|
|
8958
9010
|
items: WordSearchResultItem[];
|
|
8959
9011
|
/** 단어표 이름 */
|
|
8960
9012
|
name: string;
|
|
8961
9013
|
/** 설명 — 제목 아래 body-3로 표시(있을 때만) */
|
|
8962
9014
|
description?: string;
|
|
8963
|
-
/** 생성일(ISO) — 메타(날짜)에 표시.
|
|
9015
|
+
/** 생성일(ISO) — 메타(날짜)에 표시. */
|
|
8964
9016
|
createdAt?: string;
|
|
8965
9017
|
/** 스토어 발행 배지 표시 */
|
|
8966
9018
|
published?: boolean;
|
|
@@ -10,7 +10,7 @@ import type { BarChartProps } from './barChartTypes';
|
|
|
10
10
|
/**
|
|
11
11
|
* 막대차트 섹션 — 제목 + 구간 네비게이션(이전/다음 + 드롭다운) + 월/주 토글 + 2계열 막대차트.
|
|
12
12
|
* - 월(month): 월 드롭다운 + 주별 막대(1주~5주)
|
|
13
|
-
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(
|
|
13
|
+
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
14
14
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
15
15
|
*/
|
|
16
16
|
export default function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, tightMax, avgMode, totalFormat, series, loading, onDetail, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
@@ -7,6 +7,7 @@ import Dropdown from './Dropdown';
|
|
|
7
7
|
import SegmentedControl from './SegmentedControl';
|
|
8
8
|
import Button from './Button';
|
|
9
9
|
import Loader from './Loader';
|
|
10
|
+
import { getTodayKstDate } from '../utils/date';
|
|
10
11
|
import DescriptionTooltip from './Tooltip/DescriptionTooltip';
|
|
11
12
|
import ChartSection, { ChartTotal, ChartLegend } from './ChartSection';
|
|
12
13
|
import { firstMondayOfMonth, KO_ORD } from './barChartData';
|
|
@@ -142,7 +143,7 @@ function 주의날들(data, monday, now) {
|
|
|
142
143
|
function barsFromControl(data, control, t, lang) {
|
|
143
144
|
const { period, monthRange, weekRange, weekRanges, DOW } = control;
|
|
144
145
|
const isKo = lang?.startsWith('ko');
|
|
145
|
-
const now =
|
|
146
|
+
const now = getTodayKstDate(); // 칸은 KST 달력 — 오늘·이번 주 표시도 KST 로
|
|
146
147
|
const nowY = now.getFullYear(), nowM = now.getMonth() + 1, nowD = now.getDate();
|
|
147
148
|
if (period === 'month') {
|
|
148
149
|
const md = data[monthRange] ?? { weekly: [], daily: [] };
|
|
@@ -197,7 +198,7 @@ const BAR_SIZE_PRESET = {
|
|
|
197
198
|
/**
|
|
198
199
|
* 막대차트 섹션 — 제목 + 구간 네비게이션(이전/다음 + 드롭다운) + 월/주 토글 + 2계열 막대차트.
|
|
199
200
|
* - 월(month): 월 드롭다운 + 주별 막대(1주~5주)
|
|
200
|
-
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(
|
|
201
|
+
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
201
202
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
202
203
|
*/
|
|
203
204
|
export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, tightMax = false, avgMode = false, totalFormat, series, loading = false, onDetail, inGrid = false, }) {
|
|
@@ -216,7 +217,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
216
217
|
if (!rate)
|
|
217
218
|
return rawBars;
|
|
218
219
|
const 월 = control.period === 'month' ? control.monthRange.match(/^(\d+)년 (\d+)월$/) : null;
|
|
219
|
-
const now =
|
|
220
|
+
const now = getTodayKstDate();
|
|
220
221
|
return rawBars.map((d, i) => {
|
|
221
222
|
let a = d.a === -1 ? -1 : (d.b > 0 ? Math.round((d.a / d.b) * 100) : 0);
|
|
222
223
|
if (월 && d.a !== -1) {
|
|
@@ -76,7 +76,11 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
|
|
|
76
76
|
badgeCount: it.badgeCount,
|
|
77
77
|
selected: it.key === activeKey,
|
|
78
78
|
closeOnSelect: true,
|
|
79
|
+
// "…" 로 넘어간 꺼진 항목도 꺼진 채로 — 줄에서는 막히는데 메뉴에서는 골라졌다
|
|
80
|
+
disabled: it.state === 'disabled',
|
|
79
81
|
onClick: () => {
|
|
82
|
+
if (it.state === 'disabled')
|
|
83
|
+
return;
|
|
80
84
|
if (it.key && onChange)
|
|
81
85
|
onChange(it.key);
|
|
82
86
|
handleCloseMore();
|
|
@@ -56,10 +56,21 @@ export default function BulkBar({ count, actions, onClear, clearLabel: clearLabe
|
|
|
56
56
|
return;
|
|
57
57
|
const obs = new ResizeObserver(check);
|
|
58
58
|
obs.observe(el);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
/* 막대 자체의 폭이 바뀌면(창 크기뿐 아니라 옆 메뉴 접기로 본문 열이 넓어질 때도) 기억한 가로 칸 폭은 옛 것이다 — 버리고 다시
|
|
60
|
+
잰다. 안 그러면 넘친 순간의 좁은 폭을 들고 있어 넓어져도 쌓임이 영영 안 풀렸다. 단추 배치로는 막대 폭이 안 바뀐다. */
|
|
61
|
+
const 막대 = el.parentElement;
|
|
62
|
+
let 막대폭 = 막대?.clientWidth ?? 0;
|
|
63
|
+
const 막대obs = new ResizeObserver(() => {
|
|
64
|
+
const w = 막대?.clientWidth ?? 0;
|
|
65
|
+
if (w === 막대폭)
|
|
66
|
+
return;
|
|
67
|
+
막대폭 = w;
|
|
68
|
+
가로칸폭Ref.current = null;
|
|
69
|
+
check();
|
|
70
|
+
});
|
|
71
|
+
if (막대)
|
|
72
|
+
막대obs.observe(막대);
|
|
73
|
+
return () => { obs.disconnect(); 막대obs.disconnect(); };
|
|
63
74
|
}, [content, count]);
|
|
64
75
|
return (_jsxs("div", { className: `bulk-bar ${show ? 'bulk-bar--visible' : ''}`,
|
|
65
76
|
/* 숨은 막대는 CSS 로 투명(opacity 0)할 뿐 DOM 에 그대로 있다 — 예전에는 그 안의
|
|
@@ -107,14 +107,19 @@ export default function Button(props) {
|
|
|
107
107
|
const hasSub = subLabel !== undefined;
|
|
108
108
|
return (_jsxs("span", { className: `button__label-text${hasSub ? ' button__label-text--has-sub' : ''}`, children: [displayedLabel, count != null && _jsx("span", { className: "button__count", children: count }), hasSub && _jsx("span", { className: "button__sub-label-text caption-1", children: subLabel })] }));
|
|
109
109
|
};
|
|
110
|
+
/* 떠날 때 비제어 선택 표시를 푼다 — **toggleable 은 제외**(눌러 켠 것이 포인터가 떠나면 꺼졌다).
|
|
111
|
+
blur 도 이 정리를 쓰지만 소비자의 onMouseLeave 는 부르지 않는다(포커스를 잃은 것은 마우스가 떠난 것이 아니다). */
|
|
112
|
+
const 떠남정리 = (e) => {
|
|
113
|
+
if (typeof selected !== 'boolean' && !toggleable)
|
|
114
|
+
setInternalSelected(false);
|
|
115
|
+
e.currentTarget.blur();
|
|
116
|
+
};
|
|
110
117
|
const handleMouseLeave = (e) => {
|
|
111
118
|
if (disabledProp)
|
|
112
119
|
return;
|
|
113
120
|
if (typeof rest.onMouseLeave === 'function')
|
|
114
121
|
rest.onMouseLeave(e);
|
|
115
|
-
|
|
116
|
-
setInternalSelected(false);
|
|
117
|
-
e.currentTarget.blur();
|
|
122
|
+
떠남정리(e);
|
|
118
123
|
};
|
|
119
124
|
const handleClickInternal = (e) => {
|
|
120
125
|
if (disabledProp) {
|
|
@@ -202,7 +207,8 @@ export default function Button(props) {
|
|
|
202
207
|
if (!disabledProp && typeof rest.onFocus === 'function')
|
|
203
208
|
rest.onFocus(e);
|
|
204
209
|
}, onBlur: (e) => {
|
|
205
|
-
|
|
210
|
+
if (!disabledProp)
|
|
211
|
+
떠남정리(e);
|
|
206
212
|
hideTooltipFn();
|
|
207
213
|
if (!disabledProp && typeof rest.onBlur === 'function')
|
|
208
214
|
rest.onBlur(e);
|
|
@@ -195,9 +195,13 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
195
195
|
e.preventDefault();
|
|
196
196
|
}, [viewY, viewM, 활성일, 옮기기]);
|
|
197
197
|
const selectDay = useCallback((d) => {
|
|
198
|
+
if (disabled)
|
|
199
|
+
return; // 열린 채 disabled 가 되면 남은 달력에서 고른 날이 그대로 나갔다
|
|
198
200
|
onChange(toISO(viewY, viewM, d));
|
|
199
201
|
setOpen(false);
|
|
200
|
-
}, [viewY, viewM, onChange, setOpen]);
|
|
202
|
+
}, [viewY, viewM, onChange, setOpen, disabled]);
|
|
203
|
+
useEffect(() => { if (disabled)
|
|
204
|
+
setOpen(false); }, [disabled, setOpen]);
|
|
201
205
|
const isDisabledDay = (d) => {
|
|
202
206
|
const iso = toISO(viewY, viewM, d);
|
|
203
207
|
if (min && iso < min)
|
|
@@ -7,6 +7,7 @@ import { cx } from '../utils/cx';
|
|
|
7
7
|
import { lockBodyScroll } from '../hooks/bodyScrollLock';
|
|
8
8
|
import { pushEsc } from '../hooks/escStack';
|
|
9
9
|
import { useFocusTrap } from '../hooks/useFocusTrap';
|
|
10
|
+
import { rememberFocusReturn, resolveFocusReturn } from '../hooks/focusReturn';
|
|
10
11
|
import './Dialog.css';
|
|
11
12
|
function CloseIcon() {
|
|
12
13
|
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true, children: _jsx("path", { d: "M6 6l12 12M18 6 6 18" }) }));
|
|
@@ -45,13 +46,14 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
|
|
|
45
46
|
닫으면 overflow:hidden 으로 영영 굳었다. */
|
|
46
47
|
const 풀기 = lockBodyScroll();
|
|
47
48
|
const 판 = surfaceRef.current;
|
|
49
|
+
rememberFocusReturn(판, 돌아갈곳);
|
|
48
50
|
판?.focus();
|
|
49
51
|
return () => {
|
|
50
52
|
풀기();
|
|
51
53
|
// 포커스가 이 창 안이거나 갈 곳을 잃었을 때만 되돌린다 — 이미 다른 창으로 옮겨 갔으면 빼앗지 않는다(Popup 과 같다)
|
|
52
54
|
const 지금 = document.activeElement;
|
|
53
55
|
if (!지금 || 지금 === document.body || (판 && 판.contains(지금)))
|
|
54
|
-
|
|
56
|
+
resolveFocusReturn(돌아갈곳)?.focus?.();
|
|
55
57
|
};
|
|
56
58
|
}, [open]);
|
|
57
59
|
/* ESC 는 **공유 스택**에 맡긴다 — 예전에는 window 에서 직접 들었다.
|
|
@@ -62,11 +62,15 @@ export default function Dropdown({ options, value, onChange, placeholder, disabl
|
|
|
62
62
|
const toggle = useCallback(() => { if (!disabled)
|
|
63
63
|
rawToggle(); }, [disabled, rawToggle]);
|
|
64
64
|
const select = useCallback((opt) => {
|
|
65
|
-
|
|
65
|
+
// 열린 채 disabled 가 되면(저장 중 잠금) 남은 메뉴에서 고른 것이 그대로 onChange 로 나갔다
|
|
66
|
+
if (opt.disabled || disabled)
|
|
66
67
|
return;
|
|
67
68
|
onChange?.(opt.value);
|
|
68
69
|
setOpen(false);
|
|
69
|
-
}, [onChange, setOpen]);
|
|
70
|
+
}, [onChange, setOpen, disabled]);
|
|
71
|
+
// 막히면 열린 메뉴도 닫는다
|
|
72
|
+
useEffect(() => { if (disabled)
|
|
73
|
+
setOpen(false); }, [disabled, setOpen]);
|
|
70
74
|
const handleKeyDown = useCallback((e) => {
|
|
71
75
|
if (disabled)
|
|
72
76
|
return;
|
|
@@ -92,6 +92,8 @@ export default function FileUpload({ onFilesSelected, accept, multiple = false,
|
|
|
92
92
|
inputRef.current?.click();
|
|
93
93
|
};
|
|
94
94
|
const handleRemove = (index) => {
|
|
95
|
+
if (disabled)
|
|
96
|
+
return; // 꺼진 칸의 목록 제거 단추가 눌렸다
|
|
95
97
|
/* onRemove 를 주면 알림은 그쪽 한 번으로 끝낸다(onFilesSelected 로 이중으로 알리지 않는다). 다만 비제어 모드에서는
|
|
96
98
|
**보이는 목록도 줄인다** — 전에는 줄이기 전에 돌아가 버려 뺀 파일이 화면에 남았다. 제어 모드(files 를 부모가
|
|
97
99
|
쥠)는 부모가 onRemove 에서 줄인다. */
|
|
@@ -108,5 +110,5 @@ export default function FileUpload({ onFilesSelected, accept, multiple = false,
|
|
|
108
110
|
return (_jsxs("div", { className: `file-upload ${className}`.trim(), children: [_jsxs("div", { className: cx('file-upload__dropzone', isDragging && 'file-upload__dropzone--dragging', disabled && 'file-upload__dropzone--disabled'), role: "button", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled, onClick: handleBrowseClick, onKeyDown: e => { if (e.key === 'Enter' || e.key === ' ') {
|
|
109
111
|
e.preventDefault();
|
|
110
112
|
handleBrowseClick();
|
|
111
|
-
} }, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [_jsx(Icon, { icon: "upload", size: 24, className: "file-upload__icon" }), _jsx("span", { className: "file-upload__prompt body-2", children: t('fileUpload.dragPrompt', '파일을 이곳에 드래그하여 업로드') }), _jsx("button", { type: "button", className: "file-upload__browse-button body-3", disabled: disabled, onClick: e => { e.stopPropagation(); handleBrowseClick(); }, children: t('fileUpload.browseButton', '파일 선택') }), _jsx("input", { ref: inputRef, type: "file", className: "file-upload__input", accept: accept, multiple: multiple, disabled: disabled, onChange: handleInputChange, tabIndex: -1 })] }), showFileList && files.length > 0 && (_jsx("ul", { className: "file-upload__list", children: files.map((file, index) => (_jsxs("li", { className: "file-upload__list-item body-3", children: [_jsx("span", { className: "file-upload__file-name body-3", children: file.name }), _jsx("button", { type: "button", className: "file-upload__remove-button", "aria-label": t('fileUpload.removeAriaLabel', { name: file.name }), onClick: () => handleRemove(index), children: _jsx(Icon, { icon: "x", size: 14 }) })] }, `${file.name}-${index}`))) }))] }));
|
|
113
|
+
} }, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [_jsx(Icon, { icon: "upload", size: 24, className: "file-upload__icon" }), _jsx("span", { className: "file-upload__prompt body-2", children: t('fileUpload.dragPrompt', '파일을 이곳에 드래그하여 업로드') }), _jsx("button", { type: "button", className: "file-upload__browse-button body-3", disabled: disabled, onClick: e => { e.stopPropagation(); handleBrowseClick(); }, children: t('fileUpload.browseButton', '파일 선택') }), _jsx("input", { ref: inputRef, type: "file", className: "file-upload__input", accept: accept, multiple: multiple, disabled: disabled, onChange: handleInputChange, tabIndex: -1 })] }), showFileList && files.length > 0 && (_jsx("ul", { className: "file-upload__list", children: files.map((file, index) => (_jsxs("li", { className: "file-upload__list-item body-3", children: [_jsx("span", { className: "file-upload__file-name body-3", children: file.name }), _jsx("button", { type: "button", className: "file-upload__remove-button", "aria-label": t('fileUpload.removeAriaLabel', { name: file.name }), disabled: disabled, onClick: () => handleRemove(index), children: _jsx(Icon, { icon: "x", size: 14 }) })] }, `${file.name}-${index}`))) }))] }));
|
|
112
114
|
}
|
|
@@ -66,7 +66,8 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
|
|
|
66
66
|
기본값 '선택 탭'으로 읽혀, 필터가 여럿인 화면에서 어느 그룹인지
|
|
67
67
|
구분이 안 된다(단어표 검색 설정은 세그먼트가 열 개 가까이 된다). */
|
|
68
68
|
ariaLabel: section.displayLabel ?? section.label, options: [
|
|
69
|
-
|
|
69
|
+
// allOptionDisabled 가 버튼 표시에서만 먹고 segmented·dropdown 의 '전체' 는 눌렸다
|
|
70
|
+
...(section.allOption ? [{ key: FILTER_ALL_VALUE, label: section.allLabel ?? t('common.all', '전체'), disabled: section.allOptionDisabled }] : []),
|
|
70
71
|
...items.map(item => {
|
|
71
72
|
const value = typeof item === 'string' ? item : item.value;
|
|
72
73
|
const displayLabel = typeof item !== 'string' ? item.displayLabel : undefined;
|
|
@@ -77,7 +78,7 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
|
|
|
77
78
|
/* 아무것도 안 골랐으면 빈 값 — undefined 를 넘기면 SegmentedControl 의 기본값(첫 보기)이 걸려, 상태는
|
|
78
79
|
비었는데 첫 보기가 골라진 것처럼 보였다(필터는 걸리지 않았는데 걸린 듯). */
|
|
79
80
|
value: isAllSelected && section.allOption ? FILTER_ALL_VALUE : selected[section.label] ?? '', onChange: (value) => onSelect?.(value, section.label), size: 32 })) : (!section.multiple && section.display === 'dropdown') ? (_jsx(Dropdown, { className: "filter-panel__dropdown", size: 40, options: [
|
|
80
|
-
...(section.allOption ? [{ value: FILTER_ALL_VALUE, label: section.allLabel ?? t('common.all', '전체') }] : []),
|
|
81
|
+
...(section.allOption ? [{ value: FILTER_ALL_VALUE, label: section.allLabel ?? t('common.all', '전체'), disabled: section.allOptionDisabled }] : []),
|
|
81
82
|
...items.map(item => {
|
|
82
83
|
const value = typeof item === 'string' ? item : item.value;
|
|
83
84
|
const displayLabel = typeof item !== 'string' ? item.displayLabel : undefined;
|
|
@@ -114,6 +114,11 @@ export function PanelRow(props) {
|
|
|
114
114
|
const p = props;
|
|
115
115
|
right = p.children;
|
|
116
116
|
}
|
|
117
|
+
/* error 는 Base 에 있어 **모든 variant** 가 받는다 — 그런데 input(TextField)·dropdown 만 문구를 그리고 나머지는 받아
|
|
118
|
+
버렸다(소리방향성 결과 저장 폴더: custom 행에 error 를 줬는데 빨간 상자만 뜨고 까닭이 안 보였다). dropdown 과 같은 모양으로 붙인다. */
|
|
119
|
+
if (error && variant !== 'input' && variant !== 'dropdown') {
|
|
120
|
+
right = (_jsxs("div", { className: `panel-row__dropdown-error-wrap${useFullWidth ? ' panel-row__dropdown-error-wrap--full' : ''}`, children: [right, _jsx("span", { className: "caption-1 panel-row__error", role: "alert", children: error })] }));
|
|
121
|
+
}
|
|
117
122
|
const isLabelFixed = variant === 'input' || variant === 'dropdown' || ((variant === 'text' || variant === 'custom') && useFullWidth);
|
|
118
123
|
// 위 children 모드와 동일한 키보드 처리 — 이쪽 경로에만 빠져 있어서,
|
|
119
124
|
// variant 모드에 onClick을 주면 조용히 마우스 전용이 된다(현재 사용처는 없지만 예방).
|
|
@@ -16,13 +16,13 @@ export interface PaymentPlanTier {
|
|
|
16
16
|
highlight?: boolean;
|
|
17
17
|
/**
|
|
18
18
|
* 할인 배지에 '오픈 특가'를 함께 얹는다 — **표시 전용**.
|
|
19
|
-
* 계정당 1회 제한은 라벨의 '오픈 특가'
|
|
19
|
+
* 계정당 1회 제한은 `onceOnly`(없으면 라벨의 '오픈 특가' 문자열)로 판정하므로, 이 플래그만으로는
|
|
20
20
|
* 구매 횟수가 제한되지 않는다(같은 프로모션이되 1회 제한 대상은 아닌 tier용).
|
|
21
21
|
*/
|
|
22
22
|
openPromo?: boolean;
|
|
23
23
|
/**
|
|
24
24
|
* **계정당 1회만 살 수 있는 tier**(오픈 특가 30일 등). 이미 산 것(`ownedCodes`)이면 숨기고 기본 tier 를 카드로 올린다.
|
|
25
|
-
* 안 주면
|
|
25
|
+
* 안 주면 라벨에 '오픈 특가' 가 있는지로 판정한다 — 그런데 라벨은 번역되므로 영어 화면에서는 판정이 실패해
|
|
26
26
|
* 이미 산 특가 카드가 남고 정가 30일권을 살 길이 없어진다. 번역되는 라벨 대신 이 값을 넘긴다.
|
|
27
27
|
*/
|
|
28
28
|
onceOnly?: boolean;
|
|
@@ -67,7 +67,9 @@ export default function DeviceSideTable({ rows, onSelect, onDelete, onWear, onRe
|
|
|
67
67
|
말하고, 바꾸려면 줄 오른쪽 끝의 다른 단추를 눌러야 했다. 보는 자리와
|
|
68
68
|
바꾸는 자리가 갈려 있으면 무엇이 무엇을 바꾸는지 짚이지 않는다.
|
|
69
69
|
체크가 눌림을 삼키게 해 줄이 함께 열리지 않도록 막는다. */
|
|
70
|
-
content: (_jsx("span", { className: "dlp-worn-check", onClick: e => e.stopPropagation(), children: _jsx(Checkbox, { checked: row.worn,
|
|
70
|
+
content: (_jsx("span", { className: "dlp-worn-check", onClick: e => e.stopPropagation(), children: _jsx(Checkbox, { checked: row.worn,
|
|
71
|
+
// 켤 수 있는지는 그 줄이 할 일로 본다 — 둘 중 하나만 준 화면(말귀 고객 상세)에서 체크가 통째로 잠겼다
|
|
72
|
+
disabled: row.worn ? !onRelease : !onWear, ariaLabel: row.worn
|
|
71
73
|
? t('customers.device.unwear', '착용 해제')
|
|
72
74
|
: t('customers.device.wear', '착용'), onChange: next => { if (next)
|
|
73
75
|
onWear?.(row);
|
|
@@ -62,6 +62,9 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
62
62
|
return copy.sort((a, b) => b.items.length - a.items.length);
|
|
63
63
|
return copy;
|
|
64
64
|
}, [tables, sortKey]);
|
|
65
|
+
/* 목록을 받아(tables) 보여 주는데 onDelete 가 없으면 지울 길이 없다 — 전에는 브라우저 저장소에서 같은 id 의 표를 지우고
|
|
66
|
+
보이는 목록은 그대로였다(보이지 않는 표를 지움). 그때는 삭제 단추를 두지 않는다. */
|
|
67
|
+
const canDelete = !!onDeleteProp || tablesProp === undefined;
|
|
65
68
|
const handleDelete = useCallback((id, e) => {
|
|
66
69
|
e.stopPropagation();
|
|
67
70
|
if (onDeleteProp) {
|
|
@@ -84,7 +87,7 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
84
87
|
const headerActions = useMemo(() => tables.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Dropdown, { options: VIEW_OPTIONS, value: viewMode, onChange: handleSetViewMode }), _jsx(Dropdown, { options: SORT_OPTIONS, value: sortKey, onChange: handleSetSortKey })] })) : undefined, [tables.length, viewMode, sortKey, handleSetViewMode, handleSetSortKey, VIEW_OPTIONS, SORT_OPTIONS]);
|
|
85
88
|
return (_jsx(Popup, { className: "popup--load-table", title: t('wordTable.loadTable.title', '저장된 단어 표 불러오기'), headerActions: headerActions, onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "load-table-popup__footer-actions", children: [_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation, width: "fit-content" }), _jsx(Button, { htmlType: "button", size: 48, type: "bar", variant: "main", label: t('wordTable.loadTable.loadButton', '불러오기'), width: "fit-content",
|
|
86
89
|
// 고른 표가 지금 목록에 있을 때만 — 부모 목록에서 빠진 표를 가리키면 눌러도 아무 일이 없었다
|
|
87
|
-
disabled: !selected || !tables.some(t => t.id === selected), onClick: () => handleLoadSelected(animateThen) })] })), children: _jsx("div", { className: "load-table-popup__body", children: tables.length === 0 ? (_jsx("p", { className: "load-table-popup__empty body-2", children: t('wordTable.loadTable.empty', '저장된 단어 표가 없습니다.') })) : (_jsxs(_Fragment, { children: [viewMode === 'grid' && (_jsx("ul", { className: "load-table-popup__grid", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__card${selected === table.id ? ' load-table-popup__card--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') }), _jsx(TableThumbnail, { table: table }), _jsxs("div", { className: "load-table-popup__card-info", children: [_jsx("span", { className: "load-table-popup__card-name body-2", children: table.name }), _jsxs("span", { className: "load-table-popup__card-meta caption-1", children: [formatDate(table.savedAt), " \u00B7 ", t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' })] })] })] }, table.id))) })), viewMode === 'list' && (_jsx("ul", { className: "load-table-popup__list", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__row${selected === table.id ? ' load-table-popup__row--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx("span", { className: "load-table-popup__row-check", children: selected === table.id && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: _jsx("polyline", { points: "4 13 9 18 20 6", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), _jsxs("div", { className: "load-table-popup__row-main", children: [_jsx("span", { className: "load-table-popup__row-name body-2", children: table.name }), _jsx("div", { className: "load-table-popup__row-words", children: table.items.map((item, i) => (_jsx("span", { className: "load-table-popup__row-word caption-1", children: (item.words ?? []).length >= 2
|
|
90
|
+
disabled: !selected || !tables.some(t => t.id === selected), onClick: () => handleLoadSelected(animateThen) })] })), children: _jsx("div", { className: "load-table-popup__body", children: tables.length === 0 ? (_jsx("p", { className: "load-table-popup__empty body-2", children: t('wordTable.loadTable.empty', '저장된 단어 표가 없습니다.') })) : (_jsxs(_Fragment, { children: [viewMode === 'grid' && (_jsx("ul", { className: "load-table-popup__grid", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__card${selected === table.id ? ' load-table-popup__card--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [canDelete && _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') }), _jsx(TableThumbnail, { table: table }), _jsxs("div", { className: "load-table-popup__card-info", children: [_jsx("span", { className: "load-table-popup__card-name body-2", children: table.name }), _jsxs("span", { className: "load-table-popup__card-meta caption-1", children: [formatDate(table.savedAt), " \u00B7 ", t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' })] })] })] }, table.id))) })), viewMode === 'list' && (_jsx("ul", { className: "load-table-popup__list", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__row${selected === table.id ? ' load-table-popup__row--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx("span", { className: "load-table-popup__row-check", children: selected === table.id && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: _jsx("polyline", { points: "4 13 9 18 20 6", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), _jsxs("div", { className: "load-table-popup__row-main", children: [_jsx("span", { className: "load-table-popup__row-name body-2", children: table.name }), _jsx("div", { className: "load-table-popup__row-words", children: table.items.map((item, i) => (_jsx("span", { className: "load-table-popup__row-word caption-1", children: (item.words ?? []).length >= 2
|
|
88
91
|
? `${item.words[0].label} · ${item.words[1].label}`
|
|
89
|
-
: item.words?.[0]?.label ?? item.id }, i))) })] }), _jsx("span", { className: "load-table-popup__row-count caption-1", children: t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' }) }), _jsx("span", { className: "load-table-popup__row-date caption-1", children: formatDate(table.savedAt) }), _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') })] }, table.id))) }))] })) }) }));
|
|
92
|
+
: item.words?.[0]?.label ?? item.id }, i))) })] }), _jsx("span", { className: "load-table-popup__row-count caption-1", children: t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' }) }), _jsx("span", { className: "load-table-popup__row-date caption-1", children: formatDate(table.savedAt) }), canDelete && _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') })] }, table.id))) }))] })) }) }));
|
|
90
93
|
}
|
|
@@ -16,7 +16,7 @@ export type PopupButtonConfig = {
|
|
|
16
16
|
/** @public */
|
|
17
17
|
export interface PopupProps {
|
|
18
18
|
title?: ReactNode;
|
|
19
|
-
/** 제목 아래 보조 설명(부제). title
|
|
19
|
+
/** 제목 아래 보조 설명(부제). 제목 줄이 그려질 때(useTitle 이고 title·headerActions·onClose 중 하나가 있을 때) 보인다. */
|
|
20
20
|
subtitle?: ReactNode;
|
|
21
21
|
headerActions?: ReactNode;
|
|
22
22
|
children?: ReactNode | ((opts: PopupOptions) => ReactNode);
|
|
@@ -38,12 +38,12 @@ export interface PopupProps {
|
|
|
38
38
|
/**
|
|
39
39
|
* 닫기 직전에 불린다. false를 반환하면 닫기를 중단한다(닫힘 애니메이션도 시작하지 않음).
|
|
40
40
|
*
|
|
41
|
-
* "입력 중인데 정말 닫을까요?" 같은 확인 절차용. onClose에서
|
|
42
|
-
* onClose는 이미
|
|
43
|
-
* 거기서 닫기를
|
|
44
|
-
*
|
|
41
|
+
* "입력 중인데 정말 닫을까요?" 같은 확인 절차용. onClose에서 막지 않는 이유 —
|
|
42
|
+
* onClose는 이미 닫힘 애니메이션(260ms)이 끝난 뒤(animateThen의 setTimeout 안)에 불리므로,
|
|
43
|
+
* 거기서 닫기를 취소하면 창이 한 번 사라졌다가 되살아난다(animateThen 은 콜백 뒤에도 창이
|
|
44
|
+
* 남아 있으면 되살린다). 사라지지 않고 그대로 머물게 하려면 이 시점에 판단한다.
|
|
45
45
|
*
|
|
46
|
-
* 미지정이면
|
|
46
|
+
* 미지정이면 묻지 않고 닫는다.
|
|
47
47
|
*/
|
|
48
48
|
beforeClose?: () => boolean;
|
|
49
49
|
/**
|