@ihabdevteam/core 0.142.0 → 0.144.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 +79 -0
- package/dist/components.cjs.js +185 -180
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +191 -186
- package/dist/ihabdevteam-core.d.ts +66 -22
- package/dist/packages/core/src/components/Badge.js +3 -1
- 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 +1 -1
- package/dist/packages/core/src/components/Button.js +12 -5
- package/dist/packages/core/src/components/Calendar.js +9 -6
- package/dist/packages/core/src/components/DatePicker.js +9 -4
- package/dist/packages/core/src/components/Dialog.js +3 -1
- package/dist/packages/core/src/components/Dropdown.d.ts +3 -1
- package/dist/packages/core/src/components/Dropdown.js +10 -5
- 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/Loader.js +6 -1
- package/dist/packages/core/src/components/Pagination.js +1 -1
- package/dist/packages/core/src/components/PanelRow.js +13 -4
- 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 +1 -1
- package/dist/packages/core/src/components/Popup/Popup.js +26 -6
- package/dist/packages/core/src/components/ProgressBar.js +1 -1
- package/dist/packages/core/src/components/SideNav.js +1 -1
- package/dist/packages/core/src/components/Slider.js +10 -1
- package/dist/packages/core/src/components/Table.js +2 -0
- package/dist/packages/core/src/components/TextField.js +3 -1
- package/dist/packages/core/src/components/Toolbar.js +3 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +7 -2
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +3 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +49 -2
- 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/Word/wordTablePrint.js +27 -8
- 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/useCalendarMonth.js +3 -2
- package/dist/packages/core/src/components/useToastLifecycle.js +5 -0
- 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/useVisitHistory.d.ts +2 -0
- package/dist/packages/core/src/hooks/useVisitHistory.js +5 -6
- 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 +57 -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/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.144.0";
|
|
1380
1390
|
|
|
1381
1391
|
/** @public */
|
|
1382
1392
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -1820,7 +1830,7 @@ export declare interface DraftItem<T> {
|
|
|
1820
1830
|
export declare function draftValues<T>(draft: readonly DraftItem<T>[]): T[];
|
|
1821
1831
|
|
|
1822
1832
|
/** @public */
|
|
1823
|
-
export declare function Dropdown({ options, value, onChange, placeholder, disabled, block, size, error, className, ariaLabel }: DropdownProps): JSX.Element;
|
|
1833
|
+
export declare function Dropdown({ options, value, onChange, placeholder, disabled, block, size, error, className, ariaLabel, ariaDescribedBy }: DropdownProps): JSX.Element;
|
|
1824
1834
|
|
|
1825
1835
|
export declare type DropdownItem = {
|
|
1826
1836
|
options: {
|
|
@@ -1857,6 +1867,8 @@ export declare interface DropdownProps {
|
|
|
1857
1867
|
className?: string;
|
|
1858
1868
|
/** 화면에 보이는 라벨 없이 스크린리더용 이름을 지정할 때 사용 */
|
|
1859
1869
|
ariaLabel?: string;
|
|
1870
|
+
/** 트리거를 설명할 요소 id(들) — 오류 문구 등. 고른 값은 이것과 함께 설명으로 따로 읽힌다. */
|
|
1871
|
+
ariaDescribedBy?: string;
|
|
1860
1872
|
}
|
|
1861
1873
|
|
|
1862
1874
|
/**
|
|
@@ -2267,11 +2279,13 @@ export declare interface FormatYmdOptions {
|
|
|
2267
2279
|
/** @public */
|
|
2268
2280
|
export declare function getAssetBaseUrl(): string;
|
|
2269
2281
|
|
|
2270
|
-
/**
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2282
|
+
/**
|
|
2283
|
+
* 현재 컨트롤 선택(주/월)의 기간 — 상세보기(기간별 리스트)용. **KST 의 날로 자른다**(해외 기기도 같은 기간).
|
|
2284
|
+
*
|
|
2285
|
+
* @param offset 몇 기간 앞·뒤인가 — `-1` 이면 앞 달 한 달(달력 달 그대로, 30일 앞이 아니다) · 앞 주 한 주.
|
|
2286
|
+
* @public
|
|
2287
|
+
*/
|
|
2288
|
+
export declare function getControlDateRange(control: BarChartControl, offset?: number): ControlDateRange;
|
|
2275
2289
|
|
|
2276
2290
|
/** @public */
|
|
2277
2291
|
export declare function getDailySelection(): {
|
|
@@ -2383,6 +2397,14 @@ export declare function getSteppedPayload(pageAnswers: unknown, itemIndex: numbe
|
|
|
2383
2397
|
/** KST(Asia/Seoul) 기준 오늘 날짜를 'YYYY-MM-DD'로 반환. DB 날짜 비교 기준선으로 사용. */
|
|
2384
2398
|
export declare function getTodayKst(): string;
|
|
2385
2399
|
|
|
2400
|
+
/**
|
|
2401
|
+
* KST 오늘을 **기기 달력의 자정 Date** 로 — 연·월·일 칸을 `getFullYear()/getMonth()/getDate()` 로 세는 곳(BarChart)의
|
|
2402
|
+
* '지금'. 칸에 넣는 자료도 같은 모양(`parseLocalYmd(toKstYmd(시각))`)이어야 해외 기기에서도 KST 의 날에 선다.
|
|
2403
|
+
*
|
|
2404
|
+
* @public
|
|
2405
|
+
*/
|
|
2406
|
+
export declare function getTodayKstDate(): Date;
|
|
2407
|
+
|
|
2386
2408
|
/**
|
|
2387
2409
|
* 등록된 주제 카테고리 목록을 반환합니다.
|
|
2388
2410
|
* configureTrainingCategories() 미호출 시 기본값을 반환합니다.
|
|
@@ -2647,6 +2669,14 @@ export declare const JONGSEONG: readonly ["", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ",
|
|
|
2647
2669
|
|
|
2648
2670
|
export declare const JUNGSEONG: readonly ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"];
|
|
2649
2671
|
|
|
2672
|
+
/**
|
|
2673
|
+
* KST 달력 날짜(연, 0부터 세는 월, 일)의 **00:00 KST 순간.** 일·월은 넘쳐도 된다(`Date.UTC` 처럼 — 32일은 다음 달 1일).
|
|
2674
|
+
* 서버에 기간을 물을 때(`toISOString()`) 기기 시간대가 아니라 KST 의 하루로 자르려고 쓴다.
|
|
2675
|
+
*
|
|
2676
|
+
* @public
|
|
2677
|
+
*/
|
|
2678
|
+
export declare function kstStartOfDay(year: number, month0: number, day: number): Date;
|
|
2679
|
+
|
|
2650
2680
|
/**
|
|
2651
2681
|
* 칸 번호 → 그때 보인 낱말. 채점과 **함께 저장한다.**
|
|
2652
2682
|
*
|
|
@@ -3543,6 +3573,18 @@ export declare type PanelRowToggle = Base & {
|
|
|
3543
3573
|
|
|
3544
3574
|
declare type PanelSize = 'small' | 'medium' | 'large';
|
|
3545
3575
|
|
|
3576
|
+
/**
|
|
3577
|
+
* 'YYYY-MM-DD' 날짜 전용 문자열(예: 서버가 KST 기준으로 집계해 내려주는 log_date)을
|
|
3578
|
+
* 로컬 타임존 자정 Date로 파싱한다. new Date('YYYY-MM-DD')는 UTC 자정으로 해석되어,
|
|
3579
|
+
* UTC보다 서쪽 시간대(예: 미국)에서 getDate()/getMonth() 같은 로컬 getter로 읽으면
|
|
3580
|
+
* 하루 전으로 밀린다 — 차트를 날짜별로 버킷팅할 때 특히 문제가 된다.
|
|
3581
|
+
*
|
|
3582
|
+
* 시각(ISO)에서 KST 의 날을 달력 Date 로 얻으려면 `parseLocalYmd(toKstYmd(시각))`.
|
|
3583
|
+
*
|
|
3584
|
+
* @public
|
|
3585
|
+
*/
|
|
3586
|
+
export declare function parseLocalYmd(ymd: string): Date;
|
|
3587
|
+
|
|
3546
3588
|
/**
|
|
3547
3589
|
* 드롭다운 값을 정렬 상태로 되돌린다.
|
|
3548
3590
|
*
|
|
@@ -4113,7 +4155,7 @@ export declare type PopupOptions = {
|
|
|
4113
4155
|
/** @public */
|
|
4114
4156
|
export declare interface PopupProps {
|
|
4115
4157
|
title?: ReactNode;
|
|
4116
|
-
/** 제목 아래 보조 설명(부제). title
|
|
4158
|
+
/** 제목 아래 보조 설명(부제). 제목 줄이 그려질 때(useTitle 이고 title·headerActions·onClose 중 하나가 있을 때) 보인다. */
|
|
4117
4159
|
subtitle?: ReactNode;
|
|
4118
4160
|
headerActions?: ReactNode;
|
|
4119
4161
|
children?: ReactNode | ((opts: PopupOptions) => ReactNode);
|
|
@@ -6484,6 +6526,8 @@ export declare const toBase64UrlSafe: typeof toStorageName;
|
|
|
6484
6526
|
export declare function toChoseong(text: string): string;
|
|
6485
6527
|
|
|
6486
6528
|
/**
|
|
6529
|
+
* 오늘의 **KST** 연·월·일(두 자리 월·일). 방문 폼의 기본 날짜 — 소비자가 이 칸을 KST 벽시계로 읽어 저장하므로, 기기 달력이면
|
|
6530
|
+
* 해외 기기에서 손대지 않고 등록한 방문이 하루 전으로 저장됐다(해외 기기 공식 지원).
|
|
6487
6531
|
* @public
|
|
6488
6532
|
*/
|
|
6489
6533
|
export declare const today: () => {
|
|
@@ -7296,7 +7340,7 @@ export declare type TrainingPagePopupProps = {
|
|
|
7296
7340
|
* 판에 실제로 걸린 값은 `NoiseSettingsStore.applied` 로 받는다.
|
|
7297
7341
|
*/
|
|
7298
7342
|
noiseOverride?: Partial<CoreNoiseSettings> | ((saved: CoreNoiseSettings) => Partial<CoreNoiseSettings>);
|
|
7299
|
-
/** 훈련 결과 팝업 렌더러 — 없으면 결과
|
|
7343
|
+
/** 훈련 결과 팝업 렌더러 — 없으면 기본 결과 팝업(TrainingResultPopup)을 띄운다 */
|
|
7300
7344
|
renderResultPopup?: (props: {
|
|
7301
7345
|
score?: number | null;
|
|
7302
7346
|
results?: unknown[];
|
|
@@ -8715,9 +8759,9 @@ export declare interface WordFilterSnapshot {
|
|
|
8715
8759
|
sameStructureOnly: boolean;
|
|
8716
8760
|
phonemeView: 'char' | 'pron' | 'direct';
|
|
8717
8761
|
gradeFilter: string[];
|
|
8718
|
-
/** 품사
|
|
8762
|
+
/** 품사 필터 — 고정 코드(noun·verb·modifier). 라벨(옛 스냅샷)도 받는다. 없으면 되돌릴 때 지금 값을 그대로 둔다. */
|
|
8719
8763
|
posFilter?: string[];
|
|
8720
|
-
/** 어종
|
|
8764
|
+
/** 어종 필터 — 고정 코드(native·sino·foreign·mixed). 라벨(옛 스냅샷)도 받는다. 없으면 되돌릴 때 지금 값을 그대로 둔다. */
|
|
8721
8765
|
wordTypeFilter?: string[];
|
|
8722
8766
|
}
|
|
8723
8767
|
|
|
@@ -8900,7 +8944,7 @@ export declare interface WordTableBlockProps {
|
|
|
8900
8944
|
tableName?: string;
|
|
8901
8945
|
/** 전달하면 외부 데이터 표시 모드, 없으면 편집 모드 */
|
|
8902
8946
|
items?: WordSearchResultItem[];
|
|
8903
|
-
/** 전달하면 저장
|
|
8947
|
+
/** 전달하면 저장하기가 저장 팝업(`renderSavePopup` 이 그림 — 없으면 아무것도 뜨지 않는다)을 열고 확인 후 name·items·description을 전달.
|
|
8904
8948
|
* saveAsNew=true면 '다른 이름으로 저장'(수정 대신 새 표 생성)을 뜻한다. */
|
|
8905
8949
|
onSaveComplete?: (name: string, items: WordSearchResultItem[], description?: string, saveAsNew?: boolean) => void;
|
|
8906
8950
|
onLoad?: () => void;
|
|
@@ -8926,7 +8970,7 @@ export declare interface WordTableBlockProps {
|
|
|
8926
8970
|
editingName?: string;
|
|
8927
8971
|
/** 지정하면 인쇄하기 왼쪽에 '취소' 버튼을 표시(PagePopup 닫기 등). */
|
|
8928
8972
|
onCancel?: () => void;
|
|
8929
|
-
/** 전달하면 카드 드래그로 순서 변경
|
|
8973
|
+
/** 전달하면 카드 드래그로 순서 변경 가능. 쪽이 나뉘면 같은 쪽 안에서만 옮기며, 자리는 전체 목록 기준이다. */
|
|
8930
8974
|
onReorder?: (fromIndex: number, toIndex: number) => void;
|
|
8931
8975
|
/** display 모드에서도 칸 수 조절 허용 */
|
|
8932
8976
|
showCellCount?: boolean;
|
|
@@ -8960,19 +9004,19 @@ export declare interface WordTableBlockProps {
|
|
|
8960
9004
|
|
|
8961
9005
|
/**
|
|
8962
9006
|
* @public 단어표를 '문서형' 미리보기 카드로 표현하는 컴포넌트.
|
|
8963
|
-
* 내용 썸네일(WordTableThumbnail) + 이름 +
|
|
9007
|
+
* 내용 썸네일(WordTableThumbnail) + 이름 + 날짜 + (발행 배지 / 더보기 / 스토어 푸터).
|
|
8964
9008
|
*/
|
|
8965
9009
|
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;
|
|
8966
9010
|
|
|
8967
9011
|
/** @public */
|
|
8968
9012
|
export declare interface WordTableButtonProps {
|
|
8969
|
-
/** 단어표 내용(썸네일 미리보기
|
|
9013
|
+
/** 단어표 내용(썸네일 미리보기) */
|
|
8970
9014
|
items: WordSearchResultItem[];
|
|
8971
9015
|
/** 단어표 이름 */
|
|
8972
9016
|
name: string;
|
|
8973
9017
|
/** 설명 — 제목 아래 body-3로 표시(있을 때만) */
|
|
8974
9018
|
description?: string;
|
|
8975
|
-
/** 생성일(ISO) — 메타(날짜)에 표시.
|
|
9019
|
+
/** 생성일(ISO) — 메타(날짜)에 표시. */
|
|
8976
9020
|
createdAt?: string;
|
|
8977
9021
|
/** 스토어 발행 배지 표시 */
|
|
8978
9022
|
published?: boolean;
|
|
@@ -49,7 +49,9 @@ export default function Badge(props) {
|
|
|
49
49
|
내게** 한다 — 그래야 이벤트의 target·currentTarget 도 실제와 같다. */
|
|
50
50
|
e.preventDefault();
|
|
51
51
|
e.currentTarget.click();
|
|
52
|
-
} : undefined, "aria-label": ariaHidden ? undefined : ariaLabel ?? defaultLabel, "aria-hidden": ariaHidden || undefined,
|
|
52
|
+
} : undefined, "aria-label": ariaHidden ? undefined : ariaLabel ?? defaultLabel, "aria-hidden": ariaHidden || undefined,
|
|
53
|
+
// 누를 수 없는 점도 이름이 있으면 그림(img)으로 — 역할 없는 span 의 aria-label 은 낭독기가 버려 "선택됨" 이 안 들렸다(bar 와 같게)
|
|
54
|
+
role: onClick ? 'button' : (ariaHidden ? undefined : 'img'), tabIndex: onClick ? 0 : undefined, children: [number !== undefined && number, number === undefined && dotIcon === 'check' && _jsx(CheckIcon, {}), number === undefined && dotIcon === 'remove' && _jsx(RemoveIcon, {}), number === undefined && dotIcon === 'minus' && _jsx(MinusIcon, {})] }));
|
|
53
55
|
}
|
|
54
56
|
// ── bar (텍스트) ────────────────────────────────────────────────────────
|
|
55
57
|
const { label, children, variant = 'default', fill = 'filled', minWidth, iconLeft, iconRight, color } = props;
|
|
@@ -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();
|
|
@@ -77,5 +77,5 @@ export default function BulkBar({ count, actions, onClear, clearLabel: clearLabe
|
|
|
77
77
|
단추가 Tab 순서와 낭독기에 남아, 보이지 않는 '선택 해제'·'삭제' 에 포커스가 갔다.
|
|
78
78
|
inert 로 포커스·낭독기에서 함께 뺀다(보이는 모양은 CSS 그대로). aria-hidden 은
|
|
79
79
|
inert 를 모르는 오래된 브라우저를 위한 덧댐이다. */
|
|
80
|
-
inert: !show || undefined, "aria-hidden": show ? undefined : true, children: [_jsx("span", { ref: contentRef, className: "bulk-bar__count body-2", children: content ?? _jsx("b", { children: t('common.selectedCount', { count }) }) }), max !== undefined && (_jsx(Badge, { size: "small", fill: "soft", variant: "main", label: `${count} / ${max}` })), _jsxs("div", { className: `bulk-bar__actions${stacked ? ' bulk-bar__actions--stacked' : ''}`, children: [_jsx(Button, { htmlType: "button", size: 40, label: clearLabel, onClick: onClear }), actions.map((a, i) => (_jsx(Button, { htmlType: "button", size: 40, variant: a.variant ?? 'default', icon: a.icon, label: a.label, onClick: a.onClick }, i)))] })] }));
|
|
80
|
+
inert: !show || undefined, "aria-hidden": show ? undefined : true, children: [_jsx("span", { ref: contentRef, className: "bulk-bar__count body-2", role: "status", children: content ?? _jsx("b", { children: t('common.selectedCount', { count }) }) }), max !== undefined && (_jsx(Badge, { size: "small", fill: "soft", variant: "main", label: `${count} / ${max}` })), _jsxs("div", { className: `bulk-bar__actions${stacked ? ' bulk-bar__actions--stacked' : ''}`, children: [_jsx(Button, { htmlType: "button", size: 40, label: clearLabel, onClick: onClear }), actions.map((a, i) => (_jsx(Button, { htmlType: "button", size: 40, variant: a.variant ?? 'default', icon: a.icon, label: a.label, onClick: a.onClick }, i)))] })] }));
|
|
81
81
|
}
|
|
@@ -91,9 +91,10 @@ export default function Button(props) {
|
|
|
91
91
|
}, [disabledProp]);
|
|
92
92
|
const isSelected = typeof selected === 'boolean' ? Boolean(selected) : internalSelected;
|
|
93
93
|
// toggleable 또는 controlled selected가 있을 때만 aria-pressed 설정
|
|
94
|
+
// 그 밖에는 소비자가 넘긴 aria-pressed 를 그대로 둔다(아이콘이 바뀌는 재생/멈춤 단추 등) — 전에는 undefined 로 덮었다
|
|
94
95
|
const ariaPressed = (toggleable || typeof selected === 'boolean')
|
|
95
96
|
? (isSelected ? 'true' : 'false')
|
|
96
|
-
:
|
|
97
|
+
: rest['aria-pressed'];
|
|
97
98
|
const classes = cx('button', sizeBase, sizeTypeClass, variantClass, alignClass, shapeClass, isSelected ? 'is-selected' : null, !useBG ? 'button--no-bg' : null, tooltipOnly ? 'button--tooltip-only' : null, className);
|
|
98
99
|
const iconClassName = `button__icon button__icon--${iconSize}`;
|
|
99
100
|
const subLabels = Array.isArray(subLabel) ? subLabel : (subLabel !== undefined ? [subLabel] : []);
|
|
@@ -107,14 +108,19 @@ export default function Button(props) {
|
|
|
107
108
|
const hasSub = subLabel !== undefined;
|
|
108
109
|
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
110
|
};
|
|
111
|
+
/* 떠날 때 비제어 선택 표시를 푼다 — **toggleable 은 제외**(눌러 켠 것이 포인터가 떠나면 꺼졌다).
|
|
112
|
+
blur 도 이 정리를 쓰지만 소비자의 onMouseLeave 는 부르지 않는다(포커스를 잃은 것은 마우스가 떠난 것이 아니다). */
|
|
113
|
+
const 떠남정리 = (e) => {
|
|
114
|
+
if (typeof selected !== 'boolean' && !toggleable)
|
|
115
|
+
setInternalSelected(false);
|
|
116
|
+
e.currentTarget.blur();
|
|
117
|
+
};
|
|
110
118
|
const handleMouseLeave = (e) => {
|
|
111
119
|
if (disabledProp)
|
|
112
120
|
return;
|
|
113
121
|
if (typeof rest.onMouseLeave === 'function')
|
|
114
122
|
rest.onMouseLeave(e);
|
|
115
|
-
|
|
116
|
-
setInternalSelected(false);
|
|
117
|
-
e.currentTarget.blur();
|
|
123
|
+
떠남정리(e);
|
|
118
124
|
};
|
|
119
125
|
const handleClickInternal = (e) => {
|
|
120
126
|
if (disabledProp) {
|
|
@@ -202,7 +208,8 @@ export default function Button(props) {
|
|
|
202
208
|
if (!disabledProp && typeof rest.onFocus === 'function')
|
|
203
209
|
rest.onFocus(e);
|
|
204
210
|
}, onBlur: (e) => {
|
|
205
|
-
|
|
211
|
+
if (!disabledProp)
|
|
212
|
+
떠남정리(e);
|
|
206
213
|
hideTooltipFn();
|
|
207
214
|
if (!disabledProp && typeof rest.onBlur === 'function')
|
|
208
215
|
rest.onBlur(e);
|
|
@@ -4,6 +4,7 @@ import { useCallback, useMemo, useState, useRef } from 'react';
|
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import Icon from './Icon';
|
|
6
6
|
import { formatTimeHM } from '../utils/format';
|
|
7
|
+
import { getTodayKst } from '../utils/date';
|
|
7
8
|
import './Calendar.css';
|
|
8
9
|
import { pressable } from '../utils/pressable';
|
|
9
10
|
function daysInMonth(year, month) { return new Date(year, month + 1, 0).getDate(); }
|
|
@@ -12,11 +13,13 @@ function firstDayOfMonth(year, month) { return new Date(year, month, 1).getDay()
|
|
|
12
13
|
export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDayClick, }) {
|
|
13
14
|
const { t } = useTranslation();
|
|
14
15
|
const DOW = useMemo(() => t('calendar.dow', { returnObjects: true }), [t]);
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/* '오늘' 은 KST 의 날 — 방문 날짜가 KST 달력(toKstYmd)이라, 기기 달력으로 열면 해외 기기에서 오늘 방문이 안 보이는 달에 있고
|
|
17
|
+
오늘 표시가 다른 날에 붙었다(해외 기기 공식 지원) */
|
|
18
|
+
const 오늘Ymd = getTodayKst();
|
|
19
|
+
const [year, setYear] = useState(() => Number(오늘Ymd.slice(0, 4)));
|
|
20
|
+
const [month, setMonth] = useState(() => Number(오늘Ymd.slice(5, 7)) - 1);
|
|
17
21
|
const [dragOverDate, setDragOverDate] = useState(null);
|
|
18
22
|
const dragVisitId = useRef(null);
|
|
19
|
-
const today = new Date();
|
|
20
23
|
const visitsByDate = useMemo(() => {
|
|
21
24
|
const map = {};
|
|
22
25
|
visits.forEach((v) => {
|
|
@@ -51,15 +54,15 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
|
|
|
51
54
|
else
|
|
52
55
|
setMonth(m => m + 1); }, [month]);
|
|
53
56
|
const fmtDate = (d) => `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
54
|
-
const isToday = (d) =>
|
|
55
|
-
return (_jsxs("div", { children: [_jsxs("div", { className: "calendar-nav", children: [_jsx("button", { type: "button", className: "calendar-nav__button", onClick: prev, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(Icon, { icon: "chevron-left", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsx("span", { className: "calendar-nav__title h5", children: t('calendar.yearMonth', { year, month: month + 1 }) }), _jsx("button", { type: "button", className: "calendar-nav__button", onClick: next, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(Icon, { icon: "chevron-right", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsxs("div", { className: "calendar-nav__legend", children: [_jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--main" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.visited', '방문') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--sub" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.scheduled', '예약') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--disabled" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.noVisit', '미방문') })] })] }), _jsxs("div", { className: "calendar-grid", children: [DOW.map(d => _jsx("div", { className: "calendar-day-header body-3", children: d }, d)), cells.map((d, i) => {
|
|
57
|
+
const isToday = (d) => fmtDate(d) === 오늘Ymd;
|
|
58
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "calendar-nav", children: [_jsx("button", { type: "button", className: "calendar-nav__button", onClick: prev, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(Icon, { icon: "chevron-left", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsx("span", { className: "calendar-nav__title h5", "aria-live": "polite", children: t('calendar.yearMonth', { year, month: month + 1 }) }), _jsx("button", { type: "button", className: "calendar-nav__button", onClick: next, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(Icon, { icon: "chevron-right", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsxs("div", { className: "calendar-nav__legend", children: [_jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--main" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.visited', '방문') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--sub" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.scheduled', '예약') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--disabled" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.noVisit', '미방문') })] })] }), _jsxs("div", { className: "calendar-grid", children: [DOW.map(d => _jsx("div", { className: "calendar-day-header body-3", children: d }, d)), cells.map((d, i) => {
|
|
56
59
|
if (d === null)
|
|
57
60
|
return _jsx("div", { className: "calendar-cell empty" }, `empty-${i}`);
|
|
58
61
|
const day = d;
|
|
59
62
|
const dateKey = fmtDate(day);
|
|
60
63
|
const dayVisits = visitsByDate[dateKey] ?? [];
|
|
61
64
|
const isDragOver = dragOverDate === dateKey;
|
|
62
|
-
return (_jsxs("div", { className: `calendar-cell${isToday(day) ? ' today' : ''}${isDragOver ? ' drag-over' : ''}${onDayClick ? ' calendar-cell--clickable' : ''}`, role: onDayClick ? 'button' : undefined, tabIndex: onDayClick ? 0 : undefined, "aria-label": onDayClick ? t('calendar.addOnDate', { date: dateKey }) : undefined, onClick: onDayClick ? () => onDayClick(dateKey) : undefined,
|
|
65
|
+
return (_jsxs("div", { className: `calendar-cell${isToday(day) ? ' today' : ''}${isDragOver ? ' drag-over' : ''}${onDayClick ? ' calendar-cell--clickable' : ''}`, role: onDayClick ? 'button' : undefined, tabIndex: onDayClick ? 0 : undefined, "aria-label": onDayClick ? t('calendar.addOnDate', { date: dateKey }) : undefined, "aria-current": isToday(day) ? 'date' : undefined, onClick: onDayClick ? () => onDayClick(dateKey) : undefined,
|
|
63
66
|
/* `<div role="button">` 은 브라우저가 Enter·Space 를 눌림으로 바꿔 주지 않는다 */
|
|
64
67
|
onKeyDown: onDayClick ? (e) => {
|
|
65
68
|
/* 칸 안의 방문 표시에서 누른 Enter 가 올라온 것이면 넘긴다 — 마우스 click 은 표시가
|
|
@@ -9,6 +9,7 @@ import { cx } from '../utils/cx';
|
|
|
9
9
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
import { createPortal } from 'react-dom';
|
|
12
|
+
import { getTodayKst } from '../utils/date';
|
|
12
13
|
import Calendar from 'lucide-react/dist/esm/icons/calendar.mjs';
|
|
13
14
|
import ChevronLeft from 'lucide-react/dist/esm/icons/chevron-left.mjs';
|
|
14
15
|
import ChevronRight from 'lucide-react/dist/esm/icons/chevron-right.mjs';
|
|
@@ -55,7 +56,8 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
55
56
|
영어 화면의 날짜 고르개만 한국어 요일이 떴다 — 같은 저장소의 `Calendar` 는
|
|
56
57
|
진작 `calendar.dow` 를 쓰고 있었고, 사전에는 영어(`Sun`·`Mon`…)도 들어 있었다. */
|
|
57
58
|
const DOW = useMemo(() => t('calendar.dow', { returnObjects: true }), [t]);
|
|
58
|
-
|
|
59
|
+
// 오늘 표시·열 때 포커스는 KST 의 날 — 기기 달력이면 해외 기기에서 하루 어긋나 min=오늘 달력이 앞 달로 열렸다
|
|
60
|
+
const 오늘ISO = getTodayKst();
|
|
59
61
|
const { viewY, viewM, prevMonth, nextMonth, setViewYM } = useCalendarMonth(value);
|
|
60
62
|
const computeStyle = useCallback((rect) => {
|
|
61
63
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
@@ -122,7 +124,6 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
122
124
|
useEffect(() => {
|
|
123
125
|
if (!open)
|
|
124
126
|
return;
|
|
125
|
-
const 오늘ISO = toISO(today.getFullYear(), today.getMonth(), today.getDate());
|
|
126
127
|
const 첫자리 = value && value.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +value.slice(8)
|
|
127
128
|
: 오늘ISO.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +오늘ISO.slice(8)
|
|
128
129
|
: 1;
|
|
@@ -195,9 +196,13 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
195
196
|
e.preventDefault();
|
|
196
197
|
}, [viewY, viewM, 활성일, 옮기기]);
|
|
197
198
|
const selectDay = useCallback((d) => {
|
|
199
|
+
if (disabled)
|
|
200
|
+
return; // 열린 채 disabled 가 되면 남은 달력에서 고른 날이 그대로 나갔다
|
|
198
201
|
onChange(toISO(viewY, viewM, d));
|
|
199
202
|
setOpen(false);
|
|
200
|
-
}, [viewY, viewM, onChange, setOpen]);
|
|
203
|
+
}, [viewY, viewM, onChange, setOpen, disabled]);
|
|
204
|
+
useEffect(() => { if (disabled)
|
|
205
|
+
setOpen(false); }, [disabled, setOpen]);
|
|
201
206
|
const isDisabledDay = (d) => {
|
|
202
207
|
const iso = toISO(viewY, viewM, d);
|
|
203
208
|
if (min && iso < min)
|
|
@@ -233,7 +238,7 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
233
238
|
return _jsx("div", {}, `e${i}`);
|
|
234
239
|
const iso = toISO(viewY, viewM, d);
|
|
235
240
|
const isSelected = iso === value;
|
|
236
|
-
const isToday = iso ===
|
|
241
|
+
const isToday = iso === 오늘ISO;
|
|
237
242
|
const dis = isDisabledDay(d);
|
|
238
243
|
const col = (i % 7);
|
|
239
244
|
return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d === 탭자리 ? 0 : -1, "aria-current": isToday ? 'date' : undefined, onClick: () => !dis && selectDay(d), className: cx('date-picker__day caption-1', isSelected && 'date-picker__day--selected', isToday && !isSelected && 'date-picker__day--today', dis && 'date-picker__day--disabled', col === 0 && 'date-picker__day--sun', col === 6 && 'date-picker__day--sat'), children: d }, d));
|
|
@@ -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 에서 직접 들었다.
|
|
@@ -18,6 +18,8 @@ export interface DropdownProps {
|
|
|
18
18
|
className?: string;
|
|
19
19
|
/** 화면에 보이는 라벨 없이 스크린리더용 이름을 지정할 때 사용 */
|
|
20
20
|
ariaLabel?: string;
|
|
21
|
+
/** 트리거를 설명할 요소 id(들) — 오류 문구 등. 고른 값은 이것과 함께 설명으로 따로 읽힌다. */
|
|
22
|
+
ariaDescribedBy?: string;
|
|
21
23
|
}
|
|
22
24
|
/** @public */
|
|
23
|
-
export default function Dropdown({ options, value, onChange, placeholder, disabled, block, size, error, className, ariaLabel }: DropdownProps): import("react").JSX.Element;
|
|
25
|
+
export default function Dropdown({ options, value, onChange, placeholder, disabled, block, size, error, className, ariaLabel, ariaDescribedBy }: DropdownProps): import("react").JSX.Element;
|
|
@@ -7,10 +7,11 @@ import { useTranslation } from 'react-i18next';
|
|
|
7
7
|
import { useAnchoredMenu } from '../hooks/useAnchoredMenu';
|
|
8
8
|
import './Dropdown.css';
|
|
9
9
|
/** @public */
|
|
10
|
-
export default function Dropdown({ options, value, onChange, placeholder, disabled = false, block = false, size = 48, error = false, className = '', ariaLabel }) {
|
|
10
|
+
export default function Dropdown({ options, value, onChange, placeholder, disabled = false, block = false, size = 48, error = false, className = '', ariaLabel, ariaDescribedBy }) {
|
|
11
11
|
const { t } = useTranslation();
|
|
12
12
|
const resolvedPlaceholder = placeholder ?? t('dropdown.placeholder', '선택');
|
|
13
13
|
const listId = useId();
|
|
14
|
+
const valueId = useId();
|
|
14
15
|
const containerRef = useRef(null);
|
|
15
16
|
const computeStyle = useCallback((rect) => {
|
|
16
17
|
// 아래쪽 공간이 메뉴 최대 높이(260px, Dropdown.css)보다 부족하고 위쪽에 공간이
|
|
@@ -62,11 +63,15 @@ export default function Dropdown({ options, value, onChange, placeholder, disabl
|
|
|
62
63
|
const toggle = useCallback(() => { if (!disabled)
|
|
63
64
|
rawToggle(); }, [disabled, rawToggle]);
|
|
64
65
|
const select = useCallback((opt) => {
|
|
65
|
-
|
|
66
|
+
// 열린 채 disabled 가 되면(저장 중 잠금) 남은 메뉴에서 고른 것이 그대로 onChange 로 나갔다
|
|
67
|
+
if (opt.disabled || disabled)
|
|
66
68
|
return;
|
|
67
69
|
onChange?.(opt.value);
|
|
68
70
|
setOpen(false);
|
|
69
|
-
}, [onChange, setOpen]);
|
|
71
|
+
}, [onChange, setOpen, disabled]);
|
|
72
|
+
// 막히면 열린 메뉴도 닫는다
|
|
73
|
+
useEffect(() => { if (disabled)
|
|
74
|
+
setOpen(false); }, [disabled, setOpen]);
|
|
70
75
|
const handleKeyDown = useCallback((e) => {
|
|
71
76
|
if (disabled)
|
|
72
77
|
return;
|
|
@@ -113,12 +118,12 @@ export default function Dropdown({ options, value, onChange, placeholder, disabl
|
|
|
113
118
|
// mousedown 에서는 기본 동작(포커스 이동 → 트리거 blur → 메뉴 닫힘)만 막아,
|
|
114
119
|
// click 이 도달하기 전에 메뉴가 사라지지 않게 한다.
|
|
115
120
|
onMouseDown: e => e.preventDefault(), onClick: () => select(opt), onMouseEnter: () => setActiveIdx(i), disabled: opt.disabled, children: [_jsx("span", { className: "dropdown__option-check", children: opt.value === value && (_jsx("svg", { viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "2 7 5.5 10.5 12 3.5" }) })) }), opt.label] }, opt.value))) })) : null;
|
|
116
|
-
return (_jsxs("div", { ref: containerRef, className: cx('dropdown', `dropdown--s${size}`, open ? 'dropdown--open' : '', block ? 'dropdown--block' : '', error ? 'dropdown--error' : '', className), onKeyDown: handleKeyDown, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "dropdown__trigger", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-activedescendant": open && activeIdx >= 0 ? `${listId}-option-${activeIdx}` : undefined, "aria-label": ariaLabel, "aria-invalid": error || undefined, disabled: disabled, onClick: toggle,
|
|
121
|
+
return (_jsxs("div", { ref: containerRef, className: cx('dropdown', `dropdown--s${size}`, open ? 'dropdown--open' : '', block ? 'dropdown--block' : '', error ? 'dropdown--error' : '', className), onKeyDown: handleKeyDown, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "dropdown__trigger", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-activedescendant": open && activeIdx >= 0 ? `${listId}-option-${activeIdx}` : undefined, "aria-label": ariaLabel, "aria-describedby": [ariaLabel && selected ? valueId : '', ariaDescribedBy ?? ''].filter(Boolean).join(' ') || undefined, "aria-invalid": error || undefined, disabled: disabled, onClick: toggle,
|
|
117
122
|
/* Tab 으로 포커스가 떠나면 닫는다 — 포커스는 트리거에 두고 aria-activedescendant 로 가리키는 목록이라, 떠난 뒤
|
|
118
123
|
열린 채 남으면 화살표·Enter 가 더는 메뉴로 가지 않는 죽은 메뉴가 된다. 항목·메뉴를 누르는 것은 위에서
|
|
119
124
|
mousedown 기본 동작을 막아 blur 가 나지 않는다. */
|
|
120
125
|
onBlur: e => { if (open && !menuRef.current?.contains(e.relatedTarget))
|
|
121
|
-
setOpen(false); }, children: [_jsx("span", { className: `dropdown__trigger-label body-2${!selected ? ' dropdown__trigger-label--placeholder' : ''}`, children: selected ? selected.label : resolvedPlaceholder }), _jsx("svg", { className: "dropdown__chevron", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "4 6 8 10 12 6" }) })] }), typeof document !== 'undefined' && createPortal(menu,
|
|
126
|
+
setOpen(false); }, children: [_jsx("span", { id: valueId, className: `dropdown__trigger-label body-2${!selected ? ' dropdown__trigger-label--placeholder' : ''}`, children: selected ? selected.label : resolvedPlaceholder }), _jsx("svg", { className: "dropdown__chevron", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "4 6 8 10 12 6" }) })] }), typeof document !== 'undefined' && createPortal(menu,
|
|
122
127
|
// 코어 CSS를 .ihabdevteam-core-scope 로 스코프한 소비자(플레이그라운드 등)에서도
|
|
123
128
|
// body로 portal된 메뉴가 스타일을 잃지 않도록, 가까운 scope 안으로 portal
|
|
124
129
|
// createPortal의 target은 렌더링 시점에 동기적으로 결정되어야 하므로 ref 읽기를 effect로 옮길 수 없다
|
|
@@ -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;
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import './Loader.css';
|
|
5
|
+
/** 눈에는 안 보이고 낭독기에는 읽히는 글자 */
|
|
6
|
+
const 보이지않게 = {
|
|
7
|
+
position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden',
|
|
8
|
+
clipPath: 'inset(50%)', whiteSpace: 'nowrap', border: 0,
|
|
9
|
+
};
|
|
5
10
|
/** @public */
|
|
6
11
|
export default function Loader(props) {
|
|
7
12
|
const { t } = useTranslation();
|
|
@@ -13,5 +18,5 @@ export default function Loader(props) {
|
|
|
13
18
|
return (_jsx("div", { className: "loader-bar", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, "aria-label": name, children: _jsx("div", { className: "loader-bar__inner", style: { width: `${pct}%` } }) }));
|
|
14
19
|
}
|
|
15
20
|
const dim = typeof size === 'number' ? `${size}px` : size;
|
|
16
|
-
return (
|
|
21
|
+
return (_jsxs("div", { className: "loader-spinner", style: { width: dim, height: dim }, role: "status", "aria-label": name, children: [_jsx("span", { style: 보이지않게, children: name }), _jsxs("svg", { className: "loader-spinner__svg", viewBox: "0 0 50 50", width: size, height: size, children: [_jsx("circle", { className: "loader-spinner__bg", cx: "25", cy: "25", r: "20", fill: "none", strokeWidth: "4" }), _jsx("circle", { className: "loader-spinner__fg", cx: "25", cy: "25", r: "20", fill: "none", strokeWidth: "4", strokeLinecap: "round" })] })] }));
|
|
17
22
|
}
|