@ihabdevteam/core 0.315.0 → 0.321.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 +425 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +2 -2
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +114 -15
- package/dist/packages/core/src/components/BarChart.js +16 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
- package/dist/packages/core/src/components/PanelRow.js +2 -2
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +4 -2
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +2 -2
- package/dist/packages/core/src/components/Popup/ListPopup.d.ts +12 -2
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.css +19 -0
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.d.ts +16 -1
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +8 -6
- package/dist/packages/core/src/components/Training/TrainingCardGroup.d.ts +20 -1
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +15 -0
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.d.ts +16 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +11 -6
- package/dist/packages/core/src/components/Training/TrainingStepCardOrder.js +14 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirm.d.ts +3 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +42 -30
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +13 -8
- package/dist/packages/core/src/components/Training/audioWaits.js +22 -4
- package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +82 -0
- package/dist/packages/core/src/components/Training/stepAnswerable.js +4 -0
- package/dist/packages/core/src/components/Training/useNoiseFade.js +5 -1
- package/dist/packages/core/src/components/Word/WordTableBlock.css +25 -0
- package/dist/packages/core/src/components/Word/WordTableBlock.js +2 -2
- package/dist/packages/core/src/components/wordTableLayout.d.ts +13 -1
- package/dist/packages/core/src/components/wordTableLayout.js +13 -1
- package/dist/packages/core/src/hooks/useWordAttributeFilter.d.ts +10 -1
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +11 -2
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +6 -4
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +46 -6
- package/dist/packages/core/src/i18n/locales/en.json +9 -1
- package/dist/packages/core/src/i18n/locales/ko.json +9 -1
- package/dist/packages/core/src/utils/phonemePairSearch.js +10 -3
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +37 -0
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +60 -3
- package/dist/packages/core/src/utils/wordTrainingPool.js +3 -2
- package/dist/packages/core/src/utils/wornDate.d.ts +14 -2
- package/dist/packages/core/src/utils/wornDate.js +38 -3
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +7 -3
- package/package.json +2 -1
|
@@ -672,7 +672,16 @@ export declare function buildDropContrastProblem(word: string, comp: Composition
|
|
|
672
672
|
*/
|
|
673
673
|
export declare function buildDropFinalConsonantProblem(word: string, _comp: CompositionFilter | null, pronMap: Map<string, string>, _pv: 'char' | 'pron', _ws: Set<string>, _dropPos?: number | null): TrainingProblem | null;
|
|
674
674
|
|
|
675
|
-
/**
|
|
675
|
+
/**
|
|
676
|
+
* @public 등급 FilterPanelSection — 세 화면 공용. labelAction/allLabel은 호출부가 자신의 i18n으로 구성해 넘긴다.
|
|
677
|
+
*
|
|
678
|
+
* **맨 끝에 `없음` 이 선다**((205), 2026-10-12) — 등급이 비어 있는 단어도 고를 수 있어야 한다.
|
|
679
|
+
* 맨 끝인 까닭은 **1~4 가 센 순서**이고 `없음` 은 그 줄 밖이기 때문이다(앞에 두면 `1등급` 앞에
|
|
680
|
+
* 숫자 아닌 것이 끼어 읽는 차례가 흔들린다).
|
|
681
|
+
*
|
|
682
|
+
* 이 줄의 글자는 `wordAttributeFilterCodes` 의 사전과 **같아야** 한다 — 거기서 코드로 못 읽으면
|
|
683
|
+
* 필터는 켜져 보이는데 한 단어도 안 걸린다. `test/gradeNoneOption.test.ts` 가 둘을 맞춰 센다.
|
|
684
|
+
*/
|
|
676
685
|
export declare function buildGradeSection(t: TFunction, keys: WordAttributeFilterKeys, opts?: {
|
|
677
686
|
allLabel?: string;
|
|
678
687
|
labelAction?: ReactNode;
|
|
@@ -1669,7 +1678,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1669
1678
|
*
|
|
1670
1679
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1671
1680
|
*/
|
|
1672
|
-
export declare const CORE_VERSION = "0.
|
|
1681
|
+
export declare const CORE_VERSION = "0.321.0";
|
|
1673
1682
|
|
|
1674
1683
|
/** @public */
|
|
1675
1684
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -4040,13 +4049,28 @@ export declare interface NoiseSettingsStore {
|
|
|
4040
4049
|
* 설정값은 NoisePlayerContext(useNoisePlayer)로 읽고 쓴다.
|
|
4041
4050
|
* @public
|
|
4042
4051
|
*/
|
|
4043
|
-
export declare function NoiseSettingsTooltip({ anchorRect, onClose }: NoiseSettingsTooltipProps): JSX.Element;
|
|
4052
|
+
export declare function NoiseSettingsTooltip({ anchorRect, onClose, hideNoiseRows }: NoiseSettingsTooltipProps): JSX.Element;
|
|
4044
4053
|
|
|
4045
4054
|
/** @public */
|
|
4046
4055
|
export declare interface NoiseSettingsTooltipProps {
|
|
4047
4056
|
/** 앵커(소음 설정 버튼) 위치. */
|
|
4048
4057
|
anchorRect: DOMRect | null;
|
|
4049
4058
|
onClose: () => void;
|
|
4059
|
+
/**
|
|
4060
|
+
* **소음 두 줄을 안 그린다** — `소음 종류`·`소음 비율`(사장님 지시 (207), 2026-10-12).
|
|
4061
|
+
*
|
|
4062
|
+
* 소음이 아예 없는 훈련(짧은 이야기 듣기 · 긴 이야기 듣기 · 문장 순서화)에서 쓴다.
|
|
4063
|
+
* `전체 소리`·`재생 속도` 는 그대로 선다.
|
|
4064
|
+
*
|
|
4065
|
+
* **흐리게가 아니라 안 그린다.** 흐린 줄은 *"지금은 못 바꾼다"* 를 말하는데, 여기서 할 말은
|
|
4066
|
+
* *"이 훈련에는 소음이 없다"* 다. 흐린 채로 두면 치료사가 **바꿀 수 있는 줄을 찾아 헤맨다**
|
|
4067
|
+
* (사장님이 그 줄을 짚으며 *"왜 이거 수정요청 했는데 계속 나오고 있지?"* 라 하셨다).
|
|
4068
|
+
*
|
|
4069
|
+
* **`noiseOverride` 와 따로 둔다 — 덮개는 *값*, 이 칸은 *보임*.** 묶으면 *"덮개는 걸되 줄은
|
|
4070
|
+
* 보이게"* 를 못 한다 — 소음 하 문장 듣기의 제안 갈래가 그 꼴이다(덮개를 걸고도 치료사가
|
|
4071
|
+
* 창에서 고친다). **한 칸이 두 가지를 뜻하게 하지 않는다.**
|
|
4072
|
+
*/
|
|
4073
|
+
hideNoiseRows?: boolean;
|
|
4050
4074
|
}
|
|
4051
4075
|
|
|
4052
4076
|
/**
|
|
@@ -5036,8 +5060,18 @@ export declare interface PopupListProps<T extends {
|
|
|
5036
5060
|
* 누르는 순간 닫힌다. 고른 뒤 닫으려면 `onSelect` 안에서 직접 닫는다.
|
|
5037
5061
|
*/
|
|
5038
5062
|
keepOpenOnSelect?: boolean;
|
|
5039
|
-
/**
|
|
5040
|
-
|
|
5063
|
+
/**
|
|
5064
|
+
* 행 선택 콜백 — **안 주면 줄을 누를 수 없는 표가 된다**(말귀 요청 (120), 2026-10-12).
|
|
5065
|
+
*
|
|
5066
|
+
* 이 값이 그대로 `Table` 의 `onRowClick` 이 되고, `Table` 은 **그것이 있다는 것만 보고** 줄을
|
|
5067
|
+
* 꾸민다(손가락 커서 · `tabindex="0"` · *"줄을 누르면 자세히 봅니다"*). 그래서 **누를 것이 없는
|
|
5068
|
+
* 표도 빈 함수를 넘겨야** 했고, 그 꾸밈이 그대로 섰다 — 눈으로 보는 사람에게는 손가락 커서가,
|
|
5069
|
+
* 자판으로 쓰는 사람에게는 **줄마다 멈춤**이(스무 줄이면 스무 번), 낭독기에는 아무 일도 안 하는
|
|
5070
|
+
* 설명이 읽혔다(말귀 실측: 눌러도 아무 일도 안 난다).
|
|
5071
|
+
*
|
|
5072
|
+
* 안 주면 `Table` 의 `onRowClick` 도 비어 **그 꾸밈이 저절로 사라진다.**
|
|
5073
|
+
*/
|
|
5074
|
+
onSelect?: (item: T) => void;
|
|
5041
5075
|
/** 닫기 콜백 */
|
|
5042
5076
|
onClose: () => void;
|
|
5043
5077
|
/** 페이지당 항목 수 옵션. 미지정 시 기본 [10, 25, 50, 100] */
|
|
@@ -8491,7 +8525,7 @@ export declare interface TrainingBlockProps {
|
|
|
8491
8525
|
export declare function TrainingCard({ title, titleLevel, description, topActions, children, footerLeft, footerRight, ariaLabel, onClick, draggable, onDragStart, onDragEnd, className, }: TrainingCardProps): JSX.Element;
|
|
8492
8526
|
|
|
8493
8527
|
/** @public */
|
|
8494
|
-
export declare function TrainingCardGroup({ title, titleLevel, count, collapsed, onToggle, sort, actions, empty, emptyContent, dragOver, onDragOver, onDragLeave, onDrop, children, className, }: TrainingCardGroupProps): JSX.Element;
|
|
8528
|
+
export declare function TrainingCardGroup({ title, titleLevel, count, collapsed, onToggle, beforeSort, sort, actions, empty, emptyContent, dragOver, onDragOver, onDragLeave, onDrop, children, className, }: TrainingCardGroupProps): JSX.Element;
|
|
8495
8529
|
|
|
8496
8530
|
/** @public */
|
|
8497
8531
|
declare type TrainingCardGroupProps = {
|
|
@@ -8525,6 +8559,25 @@ declare type TrainingCardGroupProps = {
|
|
|
8525
8559
|
* "접기, 단추" 로 읽혔다. 소비자 아홉 자리 중 여덟이 그랬다.)
|
|
8526
8560
|
*/
|
|
8527
8561
|
onToggle?: () => void;
|
|
8562
|
+
/**
|
|
8563
|
+
* **정렬 드롭다운 바로 왼쪽 자리**((209), 사장님 지시 2026-10-12 — 노션 QA *"단어표, 대립쌍표의
|
|
8564
|
+
* 그룹 순서 변경 기능 필요"*). 그룹 **차례를 바꾸는 손잡이**가 설 자리다.
|
|
8565
|
+
*
|
|
8566
|
+
* 사장님이 길 둘을 말씀하셨다 — *"그룹 자체를 드래그 하거나 정렬 왼쪽에 올림 내림 버튼을
|
|
8567
|
+
* 넣어서"*. **둘 다 이 한 자리에 든다**(끄는 손잡이든 ↑↓ 단추든). 그래서 코어는 자리만 열고
|
|
8568
|
+
* **무엇을 세울지는 부르는 쪽이 고른다** — 차례를 알고 간수하는 것이 그쪽이다.
|
|
8569
|
+
*
|
|
8570
|
+
* `actions` 와 **같은 대접**이다: 같은 `__actions` 감싸개 안이라 누름이 **접기 토글로 안
|
|
8571
|
+
* 번진다.** 밖에 두면 ↑ 를 누를 때마다 그 그룹이 접힌다.
|
|
8572
|
+
*
|
|
8573
|
+
* **낭독기 이름은 부르는 쪽이 단다** — `접기`·`정렬` 은 이 조각이 묶음 이름을 달지만(위
|
|
8574
|
+
* `titleLevel` 글), 넘겨받은 마디의 속을 이 조각이 고쳐 쓸 수는 없다. `actions` 와 같은
|
|
8575
|
+
* 규칙이다: **`위로` 가 그룹 수만큼 서면 단추 목록에서 못 가린다** — `위로` 가 아니라
|
|
8576
|
+
* `'sdfa' 그룹 위로` 꼴로 지어 넘긴다.
|
|
8577
|
+
*
|
|
8578
|
+
* 안 주면 자리를 안 만든다(지금과 같다).
|
|
8579
|
+
*/
|
|
8580
|
+
beforeSort?: ReactNode;
|
|
8528
8581
|
/** 헤더 우측, actions 앞에 놓이는 그룹 내 정렬 드롭다운 */
|
|
8529
8582
|
sort?: TrainingCardGroupSort;
|
|
8530
8583
|
/** 헤더 우측 액션 영역 (이름변경·추가·삭제 등) — 클릭이 토글로 전파되지 않음 */
|
|
@@ -8813,7 +8866,7 @@ export declare interface TrainingItemResult {
|
|
|
8813
8866
|
export declare const TrainingPagePopup: TrainingPagePopupComponent;
|
|
8814
8867
|
|
|
8815
8868
|
/** @public */
|
|
8816
|
-
export declare function TrainingPagePopupBottom({ useNoiseSettingBtn, onSettings, onNext, onResult, stepIndex, stepCount, pageIndex, pageCount, resultOpen, }: TrainingPagePopupBottomProps): JSX.Element;
|
|
8869
|
+
export declare function TrainingPagePopupBottom({ useNoiseSettingBtn, onSettings, onNext, onResult, stepIndex, stepCount, pageIndex, pageCount, resultOpen, hideNoiseRows, }: TrainingPagePopupBottomProps): JSX.Element;
|
|
8817
8870
|
|
|
8818
8871
|
/** @public */
|
|
8819
8872
|
export declare type TrainingPagePopupBottomProps = {
|
|
@@ -8828,6 +8881,21 @@ export declare type TrainingPagePopupBottomProps = {
|
|
|
8828
8881
|
pageIndex?: number;
|
|
8829
8882
|
/** 쪽 수. 0(쪽이 없음)이어도 '결과보기'(완료) 단추는 보이고, '다음 문제'(쪽 이동)만 숨는다. 기본 1. */
|
|
8830
8883
|
pageCount?: number;
|
|
8884
|
+
/**
|
|
8885
|
+
* **소음 두 줄을 안 그린다** — `소음 종류`·`소음 비율`(사장님 지시 (207), 2026-10-12).
|
|
8886
|
+
*
|
|
8887
|
+
* 소음이 아예 없는 훈련(짧은 이야기 듣기 · 긴 이야기 듣기 · 문장 순서화)에서 쓴다.
|
|
8888
|
+
* `전체 소리`·`재생 속도` 는 그대로 선다.
|
|
8889
|
+
*
|
|
8890
|
+
* **흐리게가 아니라 안 그린다.** 흐린 줄은 *"지금은 못 바꾼다"* 를 말하는데, 여기서 할 말은
|
|
8891
|
+
* *"이 훈련에는 소음이 없다"* 다. 흐린 채로 두면 치료사가 **바꿀 수 있는 줄을 찾아 헤맨다**
|
|
8892
|
+
* (사장님이 그 줄을 짚으며 *"왜 이거 수정요청 했는데 계속 나오고 있지?"* 라 하셨다).
|
|
8893
|
+
*
|
|
8894
|
+
* **`noiseOverride` 와 따로 둔다 — 덮개는 *값*, 이 칸은 *보임*.** 묶으면 *"덮개는 걸되 줄은
|
|
8895
|
+
* 보이게"* 를 못 한다 — 소음 하 문장 듣기의 제안 갈래가 그 꼴이다(덮개를 걸고도 치료사가
|
|
8896
|
+
* 창에서 고친다). **한 칸이 두 가지를 뜻하게 하지 않는다.**
|
|
8897
|
+
*/
|
|
8898
|
+
hideNoiseRows?: boolean;
|
|
8831
8899
|
};
|
|
8832
8900
|
|
|
8833
8901
|
/** @public */
|
|
@@ -8912,6 +8980,21 @@ export declare type TrainingPagePopupProps = {
|
|
|
8912
8980
|
* 판에 실제로 걸린 값은 `NoiseSettingsStore.applied` 로 받는다.
|
|
8913
8981
|
*/
|
|
8914
8982
|
noiseOverride?: Partial<CoreNoiseSettings> | ((saved: CoreNoiseSettings) => Partial<CoreNoiseSettings>);
|
|
8983
|
+
/**
|
|
8984
|
+
* **소음 두 줄을 안 그린다** — `소음 종류`·`소음 비율`(사장님 지시 (207), 2026-10-12).
|
|
8985
|
+
*
|
|
8986
|
+
* 소음이 아예 없는 훈련(짧은 이야기 듣기 · 긴 이야기 듣기 · 문장 순서화)에서 쓴다.
|
|
8987
|
+
* `전체 소리`·`재생 속도` 는 그대로 선다.
|
|
8988
|
+
*
|
|
8989
|
+
* **흐리게가 아니라 안 그린다.** 흐린 줄은 *"지금은 못 바꾼다"* 를 말하는데, 여기서 할 말은
|
|
8990
|
+
* *"이 훈련에는 소음이 없다"* 다. 흐린 채로 두면 치료사가 **바꿀 수 있는 줄을 찾아 헤맨다**
|
|
8991
|
+
* (사장님이 그 줄을 짚으며 *"왜 이거 수정요청 했는데 계속 나오고 있지?"* 라 하셨다).
|
|
8992
|
+
*
|
|
8993
|
+
* **`noiseOverride` 와 따로 둔다 — 덮개는 *값*, 이 칸은 *보임*.** 묶으면 *"덮개는 걸되 줄은
|
|
8994
|
+
* 보이게"* 를 못 한다 — 소음 하 문장 듣기의 제안 갈래가 그 꼴이다(덮개를 걸고도 치료사가
|
|
8995
|
+
* 창에서 고친다). **한 칸이 두 가지를 뜻하게 하지 않는다.**
|
|
8996
|
+
*/
|
|
8997
|
+
hideNoiseRows?: boolean;
|
|
8915
8998
|
/** 훈련 결과 팝업 렌더러 — 없으면 기본 결과 팝업(TrainingResultPopup)을 띄운다 */
|
|
8916
8999
|
renderResultPopup?: (props: {
|
|
8917
9000
|
score?: number | null;
|
|
@@ -9195,7 +9278,9 @@ export declare type TrainingStepConfirmProps = {
|
|
|
9195
9278
|
* 그 판이 통째로 사라졌다(말귀 실측: 「3대 영양소」 보기 여섯·정답 넷, 셋을 골라도 아무
|
|
9196
9279
|
* 일도 안 남). **기억을 묻는 문제에서 덜 기억한 답도 그 사람의 답이다.**
|
|
9197
9280
|
*
|
|
9198
|
-
*
|
|
9281
|
+
* 내는 자리는 **`결과보기`** 다 — (102) 때 세운 `확인` 단추는 (206) 으로 걷혔다. 고르는
|
|
9282
|
+
* 그 자리에서 매번 보고하되 **개수로는 넘어가지 않으므로** 위 두 까닭이 그대로 선다.
|
|
9283
|
+
* 이 값은 채점 쪽이 쓰라고 그대로 받아 둔다 — **이 조각은 이 수로 아무 가름도 하지 않는다.**
|
|
9199
9284
|
*/
|
|
9200
9285
|
correctCount?: number;
|
|
9201
9286
|
useDisabledCovered?: boolean;
|
|
@@ -10311,12 +10396,6 @@ export declare function useWordTrainingDataGeneration(types: WordTrainingType[],
|
|
|
10311
10396
|
generating: boolean;
|
|
10312
10397
|
};
|
|
10313
10398
|
|
|
10314
|
-
/**
|
|
10315
|
-
* 단어 훈련 그룹별 필터 상태.
|
|
10316
|
-
* @param groupKey 그룹 식별자 (localStorage 키 및 상태 격리에 사용)
|
|
10317
|
-
* @param caps 그룹이 노출할 설정 (난이도/단어구성). 제외 필터는 항상 노출.
|
|
10318
|
-
* @public
|
|
10319
|
-
*/
|
|
10320
10399
|
export declare function useWordTrainingFilter(groupKey: string, caps: WordTrainingFilterCaps, opts?: {
|
|
10321
10400
|
resetOnMount?: boolean;
|
|
10322
10401
|
resetKey?: string;
|
|
@@ -10494,7 +10573,19 @@ export declare function withStepTimes(payload: unknown, shownAt?: string, answer
|
|
|
10494
10573
|
/** text 뒤에 와/과를 붙여 반환한다. */
|
|
10495
10574
|
export declare function withWaGwa(text: string): string;
|
|
10496
10575
|
|
|
10497
|
-
/**
|
|
10576
|
+
/**
|
|
10577
|
+
* 약호 → i18n 열쇠 조각 표(품사·어종).
|
|
10578
|
+
*
|
|
10579
|
+
* **차례가 뜻을 갖는다**(2026-10-10, 말귀_2 가 쓰기 시작한 뒤 적음). 처음에는 *사전 덮임을 세는*
|
|
10580
|
+
* 데 쓰라고 내보냈는데(약호마다 말이 있나), 말귀가 **제안 폼 고르개의 차례**를 여기서 가져갔다 —
|
|
10581
|
+
* *"말과 무관한 것은 말에서 안 꺼낸다"* 는 가름이고 맞다. 그래서 이 표는 이제 **두 가지를 뜻한다**:
|
|
10582
|
+
* *어떤 약호가 있나* 와 *어떤 차례로 보이나.*
|
|
10583
|
+
*
|
|
10584
|
+
* **차례는 한국어 사전 차례다**(명 · 동 · 형 · 부 · 수 · 관 · 감 · 조 · 접 · 의 · 대 · 보동 · 보형).
|
|
10585
|
+
* 약호를 **더하는 것은 뒤에 붙이면** 쓰는 쪽이 안 흔들리지만, **차례를 바꾸는 것은 쓰는 쪽 화면을
|
|
10586
|
+
* 바꾼다** — 판 올릴 때 알려야 한다. *한 칸이 두 가지를 뜻하는 것은 어긋나는 날까지 안 보인다.*
|
|
10587
|
+
* @public
|
|
10588
|
+
*/
|
|
10498
10589
|
export declare const WORD_LABEL_I18N_KEYS: {
|
|
10499
10590
|
readonly pos: Record<string, string>;
|
|
10500
10591
|
readonly wordType: Record<string, string>;
|
|
@@ -11519,6 +11610,14 @@ export declare function wordTypeLabels(t: 번역): Record<string, string>;
|
|
|
11519
11610
|
*/
|
|
11520
11611
|
export declare function wrongItems(items: readonly TrainingItemResult[]): TrainingItemResult[];
|
|
11521
11612
|
|
|
11613
|
+
/**
|
|
11614
|
+
* 등급 고르개의 설명 줄들 — 지금 언어로. 사전에 배열이 없으면 한국어 원문.
|
|
11615
|
+
*
|
|
11616
|
+
* **공개 API 가 아니다**(대응표에 `internal`) — 코어 안에서만 쓰고, 내보내는 것은 못이 읽기 때문이다.
|
|
11617
|
+
* 앱은 제 열쇠(`wordSearch.filter.gradeInfo`)와 제 받는 함수를 따로 갖는다.
|
|
11618
|
+
*/
|
|
11619
|
+
export declare function 등급설명(t: TFunction): string[];
|
|
11620
|
+
|
|
11522
11621
|
/** `t` 하나만 받는 작은 꼴 — `TFunction` 을 들여오지 않아 이 모듈이 i18n 에 안 매인다 */
|
|
11523
11622
|
declare type 번역 = (k: string, o: {
|
|
11524
11623
|
defaultValue: string;
|
|
@@ -430,9 +430,24 @@ export default function BarChart({ focusableBars, size = 'medium', barWidth, tit
|
|
|
430
430
|
const isLarge = size === 'large';
|
|
431
431
|
const sectionClassName = [isSmall ? 'manager-section--compact' : isLarge ? 'manager-section--full' : undefined, className]
|
|
432
432
|
.filter(Boolean).join(' ') || undefined;
|
|
433
|
+
/**
|
|
434
|
+
* **상세보기 단추 이름에 그 카드 이름을 담는다**(말귀 요청 (119), 2026-10-12).
|
|
435
|
+
*
|
|
436
|
+
* 한 쪽에 이 차트가 여섯 장 서고 그중 셋이 이 단추를 단다. 이름이 늘 `상세보기` 한 가지라
|
|
437
|
+
* 낭독기로는 **"Details · Details · Details"** 뿐이어서 어느 카드의 것인지 가릴 길이 없었다
|
|
438
|
+
* (말귀 실측: 앱이 제 손으로 붙이는 고르개는 `Homework Progress view` 처럼 카드 이름을 담는데
|
|
439
|
+
* **코어가 세우는 셋만** 똑같았다).
|
|
440
|
+
*
|
|
441
|
+
* 이름은 바깥에서 더 받지 않는다 — **이미 `titleKey` 를 받고 있다.** 제목이 없을 때만 예전
|
|
442
|
+
* 글자로 떨어뜨려 쓰던 자리가 안 깨지게 한다.
|
|
443
|
+
*/
|
|
444
|
+
const 카드이름 = titleKey ? t(titleKey) : '';
|
|
445
|
+
const 상세이름 = 카드이름
|
|
446
|
+
? t('trainingStatus.detailFor', { name: 카드이름, defaultValue: '{{name}} 상세보기' })
|
|
447
|
+
: t('trainingStatus.detail', '상세보기');
|
|
433
448
|
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, icon: icon, disc: disc, onClick: onClick, ariaLabel: cardAriaLabel, tooltip: cardTooltip, headSegment: headSegment,
|
|
434
449
|
/* 좁아 고르개가 '⋯' 로 접히면 상세보기도 그 메뉴로 들어간다 — 머리에 단추 둘이 남지 않게 */
|
|
435
|
-
headMenuItems: onDetail ? [{ label:
|
|
450
|
+
headMenuItems: onDetail ? [{ label: 상세이름, icon: 'list', onClick: onDetail, closeOnSelect: true }] : undefined, title: t(titleKey), titleLevel: titleLevel, titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? (headExtra ?? undefined) : ({ collapsed }) => _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), headExtra, onDetail && !collapsed && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { htmlType: "button", size: detailSize, type: "dot", icon: "list", ariaLabel: 상세이름, showTooltipInfo: 상세이름, onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
|
|
436
451
|
// 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
|
|
437
452
|
_jsx(ChartTotal, { label: isSmall ? undefined : `${t(control.period === 'week' ? 'manager.chart.weeklyPrefix' : 'manager.chart.monthlyPrefix')} ${t(totalLabelKey ?? labelAKey)}`, children: totalFormat ? (
|
|
438
453
|
// 숫자는 h0, 단위(시간/분/초 등)는 h5로 분리
|
|
@@ -5,10 +5,10 @@ import Button from './Button';
|
|
|
5
5
|
import { formatWornDate } from '../utils/wornDate';
|
|
6
6
|
import './HearingDeviceInfo.css';
|
|
7
7
|
export default function HearingDeviceInfo({ devices, onAddDevice, onOpenList, onRemoveDevice }) {
|
|
8
|
-
const { t } = useTranslation();
|
|
8
|
+
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
9
9
|
return (_jsxs("div", { className: "hearing-device-info", children: [_jsx("div", { className: "hearing-device-info__title body-3", children: t('customers.device.title', '청각재활기기') }), _jsx("div", { className: "hearing-device-info__row", children: ['좌', '우'].map(side => {
|
|
10
10
|
const d = devices[side];
|
|
11
11
|
const sideLabel = side === '좌' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
12
|
-
return (_jsxs("div", { className: "hearing-device-info__side", children: [_jsx("span", { className: "hearing-device-info__side-label body-3", children: sideLabel }), d ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hearing-device-info__device", children: [_jsx("span", { className: "hearing-device-info__device-name body-3", children: d.brand }), _jsxs("span", { className: "hearing-device-info__device-detail", children: [d.device, d.fitType ? ` · ${d.fitType}` : ''] }), d.productName && _jsx("span", { className: "hearing-device-info__device-product", children: d.productName }), formatWornDate(d.wornYear, d.wornMonth, d.wornDay) && _jsx("span", { className: "hearing-device-info__device-date", children: formatWornDate(d.wornYear, d.wornMonth, d.wornDay) })] }), _jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "danger", onClick: () => onRemoveDevice(side), showTooltipInfo: t('device.removeDevice', '착용 해제'), ariaLabel: t('device.removeDevice', '착용 해제') })] })] })) : (_jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') })] }))] }, side));
|
|
12
|
+
return (_jsxs("div", { className: "hearing-device-info__side", children: [_jsx("span", { className: "hearing-device-info__side-label body-3", children: sideLabel }), d ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hearing-device-info__device", children: [_jsx("span", { className: "hearing-device-info__device-name body-3", children: d.brand }), _jsxs("span", { className: "hearing-device-info__device-detail", children: [d.device, d.fitType ? ` · ${d.fitType}` : ''] }), d.productName && _jsx("span", { className: "hearing-device-info__device-product", children: d.productName }), formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) && _jsx("span", { className: "hearing-device-info__device-date", children: formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) })] }), _jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "danger", onClick: () => onRemoveDevice(side), showTooltipInfo: t('device.removeDevice', '착용 해제'), ariaLabel: t('device.removeDevice', '착용 해제') })] })] })) : (_jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') })] }))] }, side));
|
|
13
13
|
}) })] }));
|
|
14
14
|
}
|
|
@@ -151,7 +151,7 @@ function 메타(d) {
|
|
|
151
151
|
return [...new Set(조각)].join(' · ');
|
|
152
152
|
}
|
|
153
153
|
export function PanelRowDevice({ devices, onRelease, emptyLabel }) {
|
|
154
|
-
const { t } = useTranslation();
|
|
154
|
+
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
155
155
|
const SIDES = ['좌', '우'];
|
|
156
156
|
return (_jsx("div", { className: "panel-row panel-row--device", children: SIDES.map(side => {
|
|
157
157
|
const d = devices[side];
|
|
@@ -162,6 +162,6 @@ export function PanelRowDevice({ devices, onRelease, emptyLabel }) {
|
|
|
162
162
|
(잠시 안 씀·수리 보냄 같은 경우가 실제로 있다). */
|
|
163
163
|
_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x",
|
|
164
164
|
// 좌·우 두 단추가 같은 이름이라 어느 쪽 기기를 내리는지 안 들렸다 — 이름에만 쪽을 붙인다(툴팁은 그대로)
|
|
165
|
-
ariaLabel: `${sideLabel} ${t('customers.device.unwear', '착용 해제')}`, showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: 기기종류이름(d.device, t) }), 메타(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: 메타(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), formatWornDate(d.wornYear, d.wornMonth, d.wornDay) && (_jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: formatWornDate(d.wornYear, d.wornMonth, d.wornDay) }) }))] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: emptyLabel ?? t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
165
|
+
ariaLabel: `${sideLabel} ${t('customers.device.unwear', '착용 해제')}`, showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: 기기종류이름(d.device, t) }), 메타(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: 메타(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) && (_jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) }) }))] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: emptyLabel ?? t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
166
166
|
}) }));
|
|
167
167
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, useId } from 'react';
|
|
4
4
|
import { getErrorMessage } from '../../utils/errorMessage';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import Popup from './Popup';
|
|
@@ -12,6 +12,8 @@ export default function CouponInputPopup({ onApply, onCancel }) {
|
|
|
12
12
|
const [loading, setLoading] = useState(false);
|
|
13
13
|
const [error, setError] = useState(null);
|
|
14
14
|
const inputRef = useRef(null);
|
|
15
|
+
/* 글귀와 칸을 잇는 id — 한 쪽에 이 창이 둘 설 일은 없지만 id 는 문서에서 하나여야 한다 */
|
|
16
|
+
const 글귀id = useId();
|
|
15
17
|
// Popup 자체 포커스 로직(패널 내 첫 focusable = 헤더 닫기 버튼)보다 먼저 실행되도록
|
|
16
18
|
// 자식 effect가 부모보다 먼저 도는 순서를 이용해 입력창을 선점 포커스한다.
|
|
17
19
|
useEffect(() => { inputRef.current?.focus(); }, []);
|
|
@@ -44,5 +46,5 @@ export default function CouponInputPopup({ onApply, onCancel }) {
|
|
|
44
46
|
return (_jsx(Popup, { className: "coupon-input-popup", title: t('usage.couponButton', '쿠폰 입력'), size: "small", onClose: onCancel, closeDisabled: loading, buttons: [
|
|
45
47
|
{ label: t('common.cancel', '취소'), variant: 'default', disabled: loading, onClick: ({ closeWithAnimation }) => closeWithAnimation() },
|
|
46
48
|
{ label: t('coupons.applyButton', '적용'), variant: 'main', disabled: !code.trim() || loading, onClick: ({ closeWithAnimation }) => void handleApply(closeWithAnimation) },
|
|
47
|
-
], children: ({ closeWithAnimation }) => (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, className: "coupon-input-popup__input body-2", "aria-label": t('coupons.input.label', '쿠폰 코드'), placeholder: t('coupons.input.placeholder', '쿠폰 코드 입력'), value: code, onChange: handleChange, onKeyDown: e => handleKey(e, closeWithAnimation), disabled: loading, autoComplete: "off", spellCheck: false }), error && _jsx("p", { className: "coupon-input-popup__error caption-1", role: "alert", children: error })] })) }));
|
|
49
|
+
], children: ({ closeWithAnimation }) => (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, className: "coupon-input-popup__input body-2", "aria-label": t('coupons.input.label', '쿠폰 코드'), "aria-invalid": error ? true : undefined, "aria-errormessage": error ? 글귀id : undefined, "aria-describedby": error ? 글귀id : undefined, placeholder: t('coupons.input.placeholder', '쿠폰 코드 입력'), value: code, onChange: handleChange, onKeyDown: e => handleKey(e, closeWithAnimation), disabled: loading, autoComplete: "off", spellCheck: false }), error && _jsx("p", { id: 글귀id, className: "coupon-input-popup__error caption-1", role: "alert", children: error })] })) }));
|
|
48
50
|
}
|
|
@@ -30,7 +30,7 @@ function 정렬값(r, key) {
|
|
|
30
30
|
return wornDateSortKey(r.wornYear, r.wornMonth, r.wornDay);
|
|
31
31
|
}
|
|
32
32
|
export default function DeviceSideTable({ rows, loading, onSelect, onDelete, onWear, onRelease, onAdd }) {
|
|
33
|
-
const { t } = useTranslation();
|
|
33
|
+
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
34
34
|
const 방향이름 = (side) => side === '좌' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
35
35
|
/* 머리행을 누르면 그 칸으로 정렬한다 — 같은 칸을 다시 누르면 방향이 뒤집힌다.
|
|
36
36
|
쪽넘김보다 **앞에서** 정렬한다(훅에 이미 정렬된 줄을 건넨다). */
|
|
@@ -88,7 +88,7 @@ export default function DeviceSideTable({ rows, loading, onSelect, onDelete, onW
|
|
|
88
88
|
{ content: _jsx("span", { children: row.fitType || '-' }) },
|
|
89
89
|
{ content: _jsx("span", { children: row.productName || '-' }) },
|
|
90
90
|
{
|
|
91
|
-
content: _jsx("span", { className: "dlp-date", children: formatWornDate(row.wornYear, row.wornMonth, row.wornDay) || '-' }),
|
|
91
|
+
content: _jsx("span", { className: "dlp-date", children: formatWornDate(row.wornYear, row.wornMonth, row.wornDay, i18n.language) || '-' }),
|
|
92
92
|
},
|
|
93
93
|
/* 삭제를 넘겨준 화면에서만 이 칸이 선다 — 안 넘기면 칸 자체가 없어,
|
|
94
94
|
못 하는 일을 내밀지 않는다. 눌림은 줄로 새지 않게 막는다(줄을 누르면
|
|
@@ -52,8 +52,18 @@ export interface PopupListProps<T extends {
|
|
|
52
52
|
* 누르는 순간 닫힌다. 고른 뒤 닫으려면 `onSelect` 안에서 직접 닫는다.
|
|
53
53
|
*/
|
|
54
54
|
keepOpenOnSelect?: boolean;
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 행 선택 콜백 — **안 주면 줄을 누를 수 없는 표가 된다**(말귀 요청 (120), 2026-10-12).
|
|
57
|
+
*
|
|
58
|
+
* 이 값이 그대로 `Table` 의 `onRowClick` 이 되고, `Table` 은 **그것이 있다는 것만 보고** 줄을
|
|
59
|
+
* 꾸민다(손가락 커서 · `tabindex="0"` · *"줄을 누르면 자세히 봅니다"*). 그래서 **누를 것이 없는
|
|
60
|
+
* 표도 빈 함수를 넘겨야** 했고, 그 꾸밈이 그대로 섰다 — 눈으로 보는 사람에게는 손가락 커서가,
|
|
61
|
+
* 자판으로 쓰는 사람에게는 **줄마다 멈춤**이(스무 줄이면 스무 번), 낭독기에는 아무 일도 안 하는
|
|
62
|
+
* 설명이 읽혔다(말귀 실측: 눌러도 아무 일도 안 난다).
|
|
63
|
+
*
|
|
64
|
+
* 안 주면 `Table` 의 `onRowClick` 도 비어 **그 꾸밈이 저절로 사라진다.**
|
|
65
|
+
*/
|
|
66
|
+
onSelect?: (item: T) => void;
|
|
57
67
|
/** 닫기 콜백 */
|
|
58
68
|
onClose: () => void;
|
|
59
69
|
/** 페이지당 항목 수 옵션. 미지정 시 기본 [10, 25, 50, 100] */
|
|
@@ -62,3 +62,22 @@
|
|
|
62
62
|
.noise-settings__value--disabled {
|
|
63
63
|
opacity: 0.4;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/* (208) 폭만 빌리는 그림자 줄 — NoiseSettingsTooltip.tsx 의 `noise-settings__width-ghost` 주석을 볼 것.
|
|
67
|
+
가로 폭은 참 줄과 똑같이 내고(그래서 좌우 패딩은 그대로), 높이만 0 으로 접는다.
|
|
68
|
+
`visibility: hidden` 이라 선도 그림자도 안 그린다. 줄 사이 구분선은 `.panel-row + .panel-row` 인데
|
|
69
|
+
이 감싸개는 `.panel-row` 가 아니라 그 규칙에 안 걸린다 — 그래도 안쪽 줄의 선·여백·최소높이를 0 으로 둔다. */
|
|
70
|
+
.noise-settings__width-ghost {
|
|
71
|
+
visibility: hidden;
|
|
72
|
+
height: 0;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
}
|
|
76
|
+
.noise-settings__width-ghost .panel-row {
|
|
77
|
+
padding-top: 0;
|
|
78
|
+
padding-bottom: 0;
|
|
79
|
+
margin: 0;
|
|
80
|
+
border: 0;
|
|
81
|
+
min-height: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
@@ -4,10 +4,25 @@ export interface NoiseSettingsTooltipProps {
|
|
|
4
4
|
/** 앵커(소음 설정 버튼) 위치. */
|
|
5
5
|
anchorRect: DOMRect | null;
|
|
6
6
|
onClose: () => void;
|
|
7
|
+
/**
|
|
8
|
+
* **소음 두 줄을 안 그린다** — `소음 종류`·`소음 비율`(사장님 지시 (207), 2026-10-12).
|
|
9
|
+
*
|
|
10
|
+
* 소음이 아예 없는 훈련(짧은 이야기 듣기 · 긴 이야기 듣기 · 문장 순서화)에서 쓴다.
|
|
11
|
+
* `전체 소리`·`재생 속도` 는 그대로 선다.
|
|
12
|
+
*
|
|
13
|
+
* **흐리게가 아니라 안 그린다.** 흐린 줄은 *"지금은 못 바꾼다"* 를 말하는데, 여기서 할 말은
|
|
14
|
+
* *"이 훈련에는 소음이 없다"* 다. 흐린 채로 두면 치료사가 **바꿀 수 있는 줄을 찾아 헤맨다**
|
|
15
|
+
* (사장님이 그 줄을 짚으며 *"왜 이거 수정요청 했는데 계속 나오고 있지?"* 라 하셨다).
|
|
16
|
+
*
|
|
17
|
+
* **`noiseOverride` 와 따로 둔다 — 덮개는 *값*, 이 칸은 *보임*.** 묶으면 *"덮개는 걸되 줄은
|
|
18
|
+
* 보이게"* 를 못 한다 — 소음 하 문장 듣기의 제안 갈래가 그 꼴이다(덮개를 걸고도 치료사가
|
|
19
|
+
* 창에서 고친다). **한 칸이 두 가지를 뜻하게 하지 않는다.**
|
|
20
|
+
*/
|
|
21
|
+
hideNoiseRows?: boolean;
|
|
7
22
|
}
|
|
8
23
|
/**
|
|
9
24
|
* 소음 설정 툴팁 — 각 항목을 행으로 세로 배치, 슬라이더는 가로 방향.
|
|
10
25
|
* 설정값은 NoisePlayerContext(useNoisePlayer)로 읽고 쓴다.
|
|
11
26
|
* @public
|
|
12
27
|
*/
|
|
13
|
-
export default function NoiseSettingsTooltip({ anchorRect, onClose }: NoiseSettingsTooltipProps): import("react").JSX.Element;
|
|
28
|
+
export default function NoiseSettingsTooltip({ anchorRect, onClose, hideNoiseRows }: NoiseSettingsTooltipProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import Tooltip from '../Tooltip/Tooltip';
|
|
@@ -29,7 +29,7 @@ function SliderPanelRow({ title, valueLabel, value, min, max, step, tickStep, si
|
|
|
29
29
|
* 설정값은 NoisePlayerContext(useNoisePlayer)로 읽고 쓴다.
|
|
30
30
|
* @public
|
|
31
31
|
*/
|
|
32
|
-
export default function NoiseSettingsTooltip({ anchorRect, onClose }) {
|
|
32
|
+
export default function NoiseSettingsTooltip({ anchorRect, onClose, hideNoiseRows = false }) {
|
|
33
33
|
const { t } = useTranslation();
|
|
34
34
|
// 모바일에서는 터치 영역 확보를 위해 슬라이더를 더 크게(40) 표시
|
|
35
35
|
const isMobile = useMediaQuery('(max-width: 767px)');
|
|
@@ -75,8 +75,10 @@ export default function NoiseSettingsTooltip({ anchorRect, onClose }) {
|
|
|
75
75
|
}
|
|
76
76
|
catch (e) { }
|
|
77
77
|
}, [noisePlayer]);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
/* 소음 종류 보기 — **한 곳**이다. 아래 참 줄과 폭 그림자 줄이 같은 배열을 쓴다 */
|
|
79
|
+
const 종류옵션 = [
|
|
80
|
+
{ key: NONE_KEY, label: t('training.noise.type.none', '없음') },
|
|
81
|
+
...getNoiseTypeOptions(t).map(opt => ({ key: opt.value, label: opt.label })),
|
|
82
|
+
];
|
|
83
|
+
return (_jsx(Tooltip, { anchorRect: anchorRect, offset: 12, onClose: onClose, ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), isMobilePopup: false, closeOnOutsideClick: false, header: _jsxs("div", { className: "noise-settings__header", children: [_jsx("span", { children: t('training.trainingSettingsTitle', '훈련 설정') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, ariaLabel: t('common.close', '닫기'), onClick: onClose })] }), children: _jsxs(Panel, { className: "noise-settings", useBorder: false, labelWidth: 72, children: [_jsx(SliderPanelRow, { title: t('training.playVolumeTitle', '전체 소리'), valueLabel: `${volPct}%`, value: volPct, min: 0, max: 100, step: 10, size: sliderSize, onChange: handlePlayVolumeChange }), _jsx(SliderPanelRow, { title: t('training.playSpeedTitle', '재생 속도'), valueLabel: `${playbackRate.toFixed(2)}x`, value: playbackRate, min: PLAY_SPEED_MIN, max: PLAY_SPEED_MAX, step: PLAY_SPEED_STEP, size: sliderSize, onChange: handlePlaySpeedChange }), !hideNoiseRows && (_jsxs(_Fragment, { children: [_jsx(PanelRow, { variant: "custom", label: t('training.noiseTypeTitle', '소음 종류'), useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: t('training.noiseTypeTitle', '소음 종류'), size: 40, shape: "rounded", options: 종류옵션, value: noiseEnabled ? noiseType : NONE_KEY, onChange: handleTypeChange }) }), _jsx(SliderPanelRow, { title: t('training.noiseRatioTitle', '소음 비율'), valueLabel: t('training.noiseRatioValue', '{{ratio}}배', { ratio: ratio.toFixed(1) }), value: ratio, min: 0, max: NOISE_RATIO_MAX, step: NOISE_RATIO_STEP, size: sliderSize, disabled: !noiseEnabled, onChange: handleRatioChange })] })), hideNoiseRows && (_jsx("div", { className: "noise-settings__width-ghost", "aria-hidden": "true", inert: true, children: _jsx(PanelRow, { variant: "custom", label: '\u00a0', useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: "width-ghost", size: 40, shape: "rounded", options: 종류옵션, value: NONE_KEY, onChange: () => { } }) }) }))] }) }));
|
|
82
84
|
}
|
|
@@ -43,6 +43,25 @@ export type TrainingCardGroupProps = {
|
|
|
43
43
|
* "접기, 단추" 로 읽혔다. 소비자 아홉 자리 중 여덟이 그랬다.)
|
|
44
44
|
*/
|
|
45
45
|
onToggle?: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* **정렬 드롭다운 바로 왼쪽 자리**((209), 사장님 지시 2026-10-12 — 노션 QA *"단어표, 대립쌍표의
|
|
48
|
+
* 그룹 순서 변경 기능 필요"*). 그룹 **차례를 바꾸는 손잡이**가 설 자리다.
|
|
49
|
+
*
|
|
50
|
+
* 사장님이 길 둘을 말씀하셨다 — *"그룹 자체를 드래그 하거나 정렬 왼쪽에 올림 내림 버튼을
|
|
51
|
+
* 넣어서"*. **둘 다 이 한 자리에 든다**(끄는 손잡이든 ↑↓ 단추든). 그래서 코어는 자리만 열고
|
|
52
|
+
* **무엇을 세울지는 부르는 쪽이 고른다** — 차례를 알고 간수하는 것이 그쪽이다.
|
|
53
|
+
*
|
|
54
|
+
* `actions` 와 **같은 대접**이다: 같은 `__actions` 감싸개 안이라 누름이 **접기 토글로 안
|
|
55
|
+
* 번진다.** 밖에 두면 ↑ 를 누를 때마다 그 그룹이 접힌다.
|
|
56
|
+
*
|
|
57
|
+
* **낭독기 이름은 부르는 쪽이 단다** — `접기`·`정렬` 은 이 조각이 묶음 이름을 달지만(위
|
|
58
|
+
* `titleLevel` 글), 넘겨받은 마디의 속을 이 조각이 고쳐 쓸 수는 없다. `actions` 와 같은
|
|
59
|
+
* 규칙이다: **`위로` 가 그룹 수만큼 서면 단추 목록에서 못 가린다** — `위로` 가 아니라
|
|
60
|
+
* `'sdfa' 그룹 위로` 꼴로 지어 넘긴다.
|
|
61
|
+
*
|
|
62
|
+
* 안 주면 자리를 안 만든다(지금과 같다).
|
|
63
|
+
*/
|
|
64
|
+
beforeSort?: ReactNode;
|
|
46
65
|
/** 헤더 우측, actions 앞에 놓이는 그룹 내 정렬 드롭다운 */
|
|
47
66
|
sort?: TrainingCardGroupSort;
|
|
48
67
|
/** 헤더 우측 액션 영역 (이름변경·추가·삭제 등) — 클릭이 토글로 전파되지 않음 */
|
|
@@ -60,4 +79,4 @@ export type TrainingCardGroupProps = {
|
|
|
60
79
|
className?: string;
|
|
61
80
|
};
|
|
62
81
|
/** @public */
|
|
63
|
-
export default function TrainingCardGroup({ title, titleLevel, count, collapsed, onToggle, sort, actions, empty, emptyContent, dragOver, onDragOver, onDragLeave, onDrop, children, className, }: TrainingCardGroupProps): import("react").JSX.Element;
|
|
82
|
+
export default function TrainingCardGroup({ title, titleLevel, count, collapsed, onToggle, beforeSort, sort, actions, empty, emptyContent, dragOver, onDragOver, onDragLeave, onDrop, children, className, }: TrainingCardGroupProps): import("react").JSX.Element;
|
|
@@ -7,7 +7,7 @@ import Button from '../Button';
|
|
|
7
7
|
import Dropdown from '../Dropdown';
|
|
8
8
|
import { useId } from 'react';
|
|
9
9
|
/** @public */
|
|
10
|
-
export default function TrainingCardGroup({ title, titleLevel = 3, count, collapsed = false, onToggle, sort, actions, empty = false, emptyContent, dragOver = false, onDragOver, onDragLeave, onDrop, children, className, }) {
|
|
10
|
+
export default function TrainingCardGroup({ title, titleLevel = 3, count, collapsed = false, onToggle, beforeSort, sort, actions, empty = false, emptyContent, dragOver = false, onDragOver, onDragLeave, onDrop, children, className, }) {
|
|
11
11
|
const { t } = useTranslation();
|
|
12
12
|
const 제목id = useId();
|
|
13
13
|
const 제목태그 = titleLevel ? `h${titleLevel}` : 'span';
|
|
@@ -27,7 +27,7 @@ export default function TrainingCardGroup({ title, titleLevel = 3, count, collap
|
|
|
27
27
|
? (collapsed ? t('common.expandFor', { name: title }) : t('common.collapseFor', { name: title }))
|
|
28
28
|
: (collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기')), className: "training-card-group__chevron", "aria-expanded": !collapsed, onClick: onToggle })), typeof title === 'string'
|
|
29
29
|
? _jsx(제목태그, { id: 제목id, className: "training-card-group__title h5b", children: title })
|
|
30
|
-
: title, count !== undefined && _jsx("span", { className: "training-card-group__count body-3b", children: count }), (sort || actions != null) && (_jsxs("span", { className: "training-card-group__actions", onClick: e => e.stopPropagation(), children: [sort && (_jsx(Dropdown, { size: 40, options: sort.options, value: sort.value, placeholder: sort.placeholder, onChange: sort.onChange,
|
|
30
|
+
: title, count !== undefined && _jsx("span", { className: "training-card-group__count body-3b", children: count }), (beforeSort != null || sort || actions != null) && (_jsxs("span", { className: "training-card-group__actions", onClick: e => e.stopPropagation(), children: [beforeSort, sort && (_jsx(Dropdown, { size: 40, options: sort.options, value: sort.value, placeholder: sort.placeholder, onChange: sort.onChange,
|
|
31
31
|
/* 접기와 같은 까닭으로 묶음 이름을 단다 — 그룹마다 `정렬` 이 하나씩 선다 */
|
|
32
32
|
ariaLabel: typeof title === 'string' && title ? t('common.sortFor', { name: title }) : undefined })), actions] }))] }), !collapsed && (empty
|
|
33
33
|
? _jsx("div", { className: "training-card-group__empty body-3", children: emptyContent })
|
|
@@ -116,6 +116,21 @@ export type TrainingPagePopupProps = {
|
|
|
116
116
|
* 판에 실제로 걸린 값은 `NoiseSettingsStore.applied` 로 받는다.
|
|
117
117
|
*/
|
|
118
118
|
noiseOverride?: Partial<CoreNoiseSettings> | ((saved: CoreNoiseSettings) => Partial<CoreNoiseSettings>);
|
|
119
|
+
/**
|
|
120
|
+
* **소음 두 줄을 안 그린다** — `소음 종류`·`소음 비율`(사장님 지시 (207), 2026-10-12).
|
|
121
|
+
*
|
|
122
|
+
* 소음이 아예 없는 훈련(짧은 이야기 듣기 · 긴 이야기 듣기 · 문장 순서화)에서 쓴다.
|
|
123
|
+
* `전체 소리`·`재생 속도` 는 그대로 선다.
|
|
124
|
+
*
|
|
125
|
+
* **흐리게가 아니라 안 그린다.** 흐린 줄은 *"지금은 못 바꾼다"* 를 말하는데, 여기서 할 말은
|
|
126
|
+
* *"이 훈련에는 소음이 없다"* 다. 흐린 채로 두면 치료사가 **바꿀 수 있는 줄을 찾아 헤맨다**
|
|
127
|
+
* (사장님이 그 줄을 짚으며 *"왜 이거 수정요청 했는데 계속 나오고 있지?"* 라 하셨다).
|
|
128
|
+
*
|
|
129
|
+
* **`noiseOverride` 와 따로 둔다 — 덮개는 *값*, 이 칸은 *보임*.** 묶으면 *"덮개는 걸되 줄은
|
|
130
|
+
* 보이게"* 를 못 한다 — 소음 하 문장 듣기의 제안 갈래가 그 꼴이다(덮개를 걸고도 치료사가
|
|
131
|
+
* 창에서 고친다). **한 칸이 두 가지를 뜻하게 하지 않는다.**
|
|
132
|
+
*/
|
|
133
|
+
hideNoiseRows?: boolean;
|
|
119
134
|
/** 훈련 결과 팝업 렌더러 — 없으면 기본 결과 팝업(TrainingResultPopup)을 띄운다 */
|
|
120
135
|
renderResultPopup?: (props: {
|
|
121
136
|
score?: number | null;
|
|
@@ -71,7 +71,7 @@ function 판결과셈(resultOverlay) {
|
|
|
71
71
|
const defaultRenderResult = (p) => _jsx(TrainingResultPopup, { score: p.score, contextsLength: p.contextsLength, results: p.results, onReplay: p.onReplay, onClose: p.onClose, pending: p.pending });
|
|
72
72
|
import { getScenarioPageId, getScenarioStepKey, createScenarioFlowState, resolveScenarioStepProps, } from './trainingScenario';
|
|
73
73
|
function TrainingPagePopupInner(props) {
|
|
74
|
-
const { scenario, title, subTitle, depth, icon, wrapperStyle, iconTooltip, pageIndex, pageCount, stepIndex, stepCount, onBack, onComplete, onRoundFinished, onNext, onSettings, useNoiseSetting = true, children, ariaLabel, label, buildResults, onResult: externalOnResultProp, typeKey, entryPoint, dailyTotal, itemRef, renderResultPopup, noiseOverride, } = props;
|
|
74
|
+
const { scenario, title, subTitle, depth, icon, wrapperStyle, iconTooltip, pageIndex, pageCount, stepIndex, stepCount, onBack, onComplete, onRoundFinished, onNext, onSettings, useNoiseSetting = true, children, ariaLabel, label, buildResults, onResult: externalOnResultProp, typeKey, entryPoint, dailyTotal, itemRef, renderResultPopup, noiseOverride, hideNoiseRows = false, } = props;
|
|
75
75
|
// 시간 기록 — 문장 4종은 각 래퍼(useTrainingSessionLog)에서 기록하므로 제외, 그 외(단어 등) 공통 기록
|
|
76
76
|
useTrainingSessionLog(typeKey && !SENTENCE_TIME_LOGGED_TYPES.has(typeKey) ? typeKey : '');
|
|
77
77
|
// 팝업 진입 시각 — 완료 정본의 소요 시간 계산 (마운트 시 1회만 effect에서 초기화)
|
|
@@ -1020,7 +1020,7 @@ function TrainingPagePopupInner(props) {
|
|
|
1020
1020
|
둘을 한 요소에 모은다. */
|
|
1021
1021
|
_jsxs("div", { className: `page-popup${closing ? ' page-popup--closing' : ''}`, role: "dialog", "aria-modal": "true", "aria-label": resolvedTitle, children: [_jsxs("div", { ref: panelRef, className: `page-popup__panel ${!measured ? 'page-popup__panel--measuring' : ''} ${mode === 'center' ? 'page-popup__panel--center' : mode === 'top' ? 'page-popup__panel--top' : ''}`, children: [_jsx(TrainingPagePopupTitle, { title: titleForHeader, subtitle: hasScenario ? (scenarioMeta.subtitle || scenarioMeta.label) : (Array.isArray(depth) ? depth.join(' / ') : undefined), desc: !hasScenario ? subTitle : undefined, depth: (hasScenario ? scenarioMeta.depth : depth), icon: hasScenario ? scenarioMeta.icon : icon, wrapperStyle: hasScenario ? scenarioMeta.wrapperStyle : wrapperStyle, iconTooltip: hasScenario ? scenarioMeta.iconTooltip : iconTooltip, progressMax: effectivePageCount, progressIndex: effectivePageIndex, pageIndex: effectivePageIndex, pageCount: effectivePageCount, onBack: handleCloseBack, onClose: handleCloseBack }), _jsx("div", { className: "page-popup__scroll", children: _jsx("div", { className: `page-popup__content ${mode === "center" ? "page-popup__content--center" : "page-popup__content--top"}${props.contentFull ? ' page-popup__content--full' : ''}`, children: isChildrenMode ? childrenContent : (hasScenario ? scenarioContent : (typeof children === 'function' ? children({ registerOnNext, registerOnResult: registerOnResult, panelRef, setResultOverlay }) : children)) }) }), _jsx(TrainingPagePopupBottom, { onSettings: onSettings, onNext: handleNext, onResult: handleViewResults, useNoiseSettingBtn: Boolean(hasScenario ? (scenarioMeta.useNoiseSetting ?? true) : useNoiseSetting), stepIndex: effectiveStepIndex, stepCount: effectiveStepCount, pageIndex: effectivePageIndex, pageCount: effectivePageCount,
|
|
1022
1022
|
/* 결과 창이 뜨면 바닥의 오디오 미터를 끈다 — 결과를 보는 동안 떠 있을 까닭이 없다 */
|
|
1023
|
-
resultOpen: Boolean(resultOverlay && !resultOverlay.isEmpty) })] }), (resultOverlay && !resultOverlay.isEmpty && (renderResultPopup ?? defaultRenderResult)({
|
|
1023
|
+
resultOpen: Boolean(resultOverlay && !resultOverlay.isEmpty), hideNoiseRows: hideNoiseRows })] }), (resultOverlay && !resultOverlay.isEmpty && (renderResultPopup ?? defaultRenderResult)({
|
|
1024
1024
|
score: resultOverlay.score,
|
|
1025
1025
|
results: (resultOverlay.results || resultOverlay.answers || []),
|
|
1026
1026
|
contextsLength: resultOverlay.results ? resultOverlay.results.length : (resultOverlay.answers ? resultOverlay.answers.length : 0),
|
|
@@ -12,6 +12,21 @@ export type TrainingPagePopupBottomProps = {
|
|
|
12
12
|
pageIndex?: number;
|
|
13
13
|
/** 쪽 수. 0(쪽이 없음)이어도 '결과보기'(완료) 단추는 보이고, '다음 문제'(쪽 이동)만 숨는다. 기본 1. */
|
|
14
14
|
pageCount?: number;
|
|
15
|
+
/**
|
|
16
|
+
* **소음 두 줄을 안 그린다** — `소음 종류`·`소음 비율`(사장님 지시 (207), 2026-10-12).
|
|
17
|
+
*
|
|
18
|
+
* 소음이 아예 없는 훈련(짧은 이야기 듣기 · 긴 이야기 듣기 · 문장 순서화)에서 쓴다.
|
|
19
|
+
* `전체 소리`·`재생 속도` 는 그대로 선다.
|
|
20
|
+
*
|
|
21
|
+
* **흐리게가 아니라 안 그린다.** 흐린 줄은 *"지금은 못 바꾼다"* 를 말하는데, 여기서 할 말은
|
|
22
|
+
* *"이 훈련에는 소음이 없다"* 다. 흐린 채로 두면 치료사가 **바꿀 수 있는 줄을 찾아 헤맨다**
|
|
23
|
+
* (사장님이 그 줄을 짚으며 *"왜 이거 수정요청 했는데 계속 나오고 있지?"* 라 하셨다).
|
|
24
|
+
*
|
|
25
|
+
* **`noiseOverride` 와 따로 둔다 — 덮개는 *값*, 이 칸은 *보임*.** 묶으면 *"덮개는 걸되 줄은
|
|
26
|
+
* 보이게"* 를 못 한다 — 소음 하 문장 듣기의 제안 갈래가 그 꼴이다(덮개를 걸고도 치료사가
|
|
27
|
+
* 창에서 고친다). **한 칸이 두 가지를 뜻하게 하지 않는다.**
|
|
28
|
+
*/
|
|
29
|
+
hideNoiseRows?: boolean;
|
|
15
30
|
};
|
|
16
31
|
/** @public */
|
|
17
|
-
export default function TrainingPagePopupBottom({ useNoiseSettingBtn, onSettings, onNext, onResult, stepIndex, stepCount, pageIndex, pageCount, resultOpen, }: TrainingPagePopupBottomProps): import("react").JSX.Element;
|
|
32
|
+
export default function TrainingPagePopupBottom({ useNoiseSettingBtn, onSettings, onNext, onResult, stepIndex, stepCount, pageIndex, pageCount, resultOpen, hideNoiseRows, }: TrainingPagePopupBottomProps): import("react").JSX.Element;
|