@ihabdevteam/core 0.198.0 → 0.200.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 +129 -3
- package/dist/components.cjs.js +25 -25
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +184 -184
- package/dist/ihabdevteam-core.d.ts +137 -1
- package/dist/packages/core/src/components/Header.css +49 -0
- package/dist/packages/core/src/components/Header.js +8 -2
- package/dist/packages/core/src/components/ItemActions.css +12 -0
- package/dist/packages/core/src/components/ItemActions.d.ts +95 -0
- package/dist/packages/core/src/components/ItemActions.js +82 -0
- package/dist/packages/core/src/components/SideNav.css +24 -5
- package/dist/packages/core/src/components.d.ts +2 -0
- package/dist/packages/core/src/components.js +1 -0
- package/dist/packages/core/src/utils/wordRow.d.ts +37 -0
- package/dist/packages/core/src/utils/wordRow.js +19 -0
- package/dist/packages/core/src/utils.d.ts +2 -0
- package/dist/packages/core/src/utils.js +1 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -1626,7 +1626,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1626
1626
|
*
|
|
1627
1627
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1628
1628
|
*/
|
|
1629
|
-
export declare const CORE_VERSION = "0.
|
|
1629
|
+
export declare const CORE_VERSION = "0.200.0";
|
|
1630
1630
|
|
|
1631
1631
|
/** @public */
|
|
1632
1632
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -3047,6 +3047,90 @@ export declare function isValidPhone(phone: string): boolean;
|
|
|
3047
3047
|
/** @public 단어가 해당 타입의 문제(정답) 단어로 구조적으로 유효한가. 제약 없는 타입은 항상 true. */
|
|
3048
3048
|
export declare function isValidQuestionWord(typeKey: WordTrainingTypeKey, word: string): boolean;
|
|
3049
3049
|
|
|
3050
|
+
/** @public */
|
|
3051
|
+
export declare interface ItemAction {
|
|
3052
|
+
/**
|
|
3053
|
+
* 읽어 줄 이름. 아이콘뿐인 단추라 **이름과 말풍선에 같은 말**이 붙는다 —
|
|
3054
|
+
* 한쪽만 달면 눈으로 본 말과 낭독기가 읽는 말이 갈린다.
|
|
3055
|
+
*/
|
|
3056
|
+
label: string;
|
|
3057
|
+
onClick: () => void;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
/**
|
|
3061
|
+
* **보고 있는 한 항목에 하는 일들** — 창 바닥 왼쪽에 서는 아이콘 단추 줄.
|
|
3062
|
+
*
|
|
3063
|
+
* 무언가를 **보여 주는 창**이 동시에 **그것을 손대는 자리**이기도 한 곳에 선다. 말귀에 그런
|
|
3064
|
+
* 창이 셋 있었고(훈련 묶음 실행 · 단어훈련 카드 실행 · 단어표 미리보기), 세 곳이 같은 다섯
|
|
3065
|
+
* 단추를 같은 차례로 세우면서 규칙을 한 벌씩 베껴 적고 있었다(2026-09-26).
|
|
3066
|
+
*
|
|
3067
|
+
* 벌리는 일은 이미 코어 몫이다(`Popup` 의 `footerStart`/`footerEnd`, 0.190.0). 남아 있던 것은
|
|
3068
|
+
* **무엇을 어떤 차례로 세우고 무엇을 닫고 하는가**인데, 그것이 베껴져 있으면 한 곳만 고쳐도
|
|
3069
|
+
* 아무 데서도 안 터진다 — 같은 모양의 창들이 조용히 서로 다르게 움직인다. 실제로 **링크 단추의
|
|
3070
|
+
* 조건이 세 곳에서 달랐다**(둘은 길이 있을 때만, 하나는 늘 세움).
|
|
3071
|
+
*
|
|
3072
|
+
* 이 조각이 쥐는 것 —
|
|
3073
|
+
* · **차례와 꼴** — 고치기 · 복제 · 발행 · 링크 · 지우기, 모두 `size={40} type="dot"`.
|
|
3074
|
+
* · **닫고 할 것과 안 닫고 할 것** — 고치기 · 지우기는 `onClose` 를 먼저 부른다. 둘 다 **다른 창을
|
|
3075
|
+
* 여는** 일이라 안 닫으면 창 위에 창이 겹치고, 지운 것을 계속 보고 있게 된다. 복제 · 발행 ·
|
|
3076
|
+
* 링크는 안 닫는다 — 보던 것을 그대로 두고 곁에서 일어나는 일이다.
|
|
3077
|
+
* · **안 준 것은 안 세운다** — 부르는 쪽이 손으로 `&&` 하던 자리다. 갈라진 그 자리가 여기다.
|
|
3078
|
+
* · **뒤집히는 단추는 하나** — `publish.published` 가 어느 쪽인지 고른다.
|
|
3079
|
+
*
|
|
3080
|
+
* 새 갈래가 필요하면 **여기에 칸을 만든다.** 아무거나 끼울 구멍(`children` 같은 것)을 두지 않는
|
|
3081
|
+
* 까닭은, 그 구멍으로 들어온 단추는 닫고 할지 말지를 또 부르는 쪽이 정하게 되어 이 조각이
|
|
3082
|
+
* 막으려던 갈라짐이 그대로 돌아오기 때문이다.
|
|
3083
|
+
*
|
|
3084
|
+
* `Popup` 의 `footerStart` 안에서도, 손수 지은 바닥 줄 안에서도 같게 선다 — 제 간격을 스스로
|
|
3085
|
+
* 든다(`--spacing-xs`).
|
|
3086
|
+
*
|
|
3087
|
+
* 이름표는 부르는 쪽의 말이다 — 아래 예의 글자 자리에 그쪽 사전에서 꺼낸 말을 넣는다.
|
|
3088
|
+
* (여기에 `t('…')` 을 적지 않는 까닭은, 소스를 훑는 시험이 **주석 속 예시도 실제로 부르는
|
|
3089
|
+
* 열쇠로 읽기** 때문이다. 글이 시험을 깨뜨릴 수 있으면 안 된다.)
|
|
3090
|
+
*
|
|
3091
|
+
* @example
|
|
3092
|
+
* <Popup
|
|
3093
|
+
* title={표.name}
|
|
3094
|
+
* footerStart={({ closeWithAnimation }) => (
|
|
3095
|
+
* <ItemActions
|
|
3096
|
+
* onClose={closeWithAnimation}
|
|
3097
|
+
* edit={{ label: '수정', onClick: () => openEdit(표) }}
|
|
3098
|
+
* duplicate={{ label: '복제', onClick: () => duplicate(표) }}
|
|
3099
|
+
* publish={{
|
|
3100
|
+
* published: 표.isPublished,
|
|
3101
|
+
* publish: { label: '말귀자료실에 올리기', onClick: () => publish(표) },
|
|
3102
|
+
* unpublish: { label: '말귀자료실에서 내리기', onClick: () => unpublish(표) },
|
|
3103
|
+
* }}
|
|
3104
|
+
* copyLink={링크길있나 ? { label: '링크 복사', onClick: () => copyLink(표) } : undefined}
|
|
3105
|
+
* remove={{ label: '삭제', onClick: () => askDelete(표) }} />
|
|
3106
|
+
* )} />
|
|
3107
|
+
* @public
|
|
3108
|
+
*/
|
|
3109
|
+
export declare function ItemActions(props: ItemActionsProps): JSX.Element;
|
|
3110
|
+
|
|
3111
|
+
/** @public */
|
|
3112
|
+
export declare interface ItemActionsProps {
|
|
3113
|
+
/**
|
|
3114
|
+
* 이 창을 닫는 길. **닫고 하는 것**(고치기 · 지우기)이 제 일을 하기 전에 이것을 먼저 부른다.
|
|
3115
|
+
*
|
|
3116
|
+
* 줄 것이 없어 보여도 비우지 않는다 — 안 닫으면 고칠 창이 보던 창 **위에 겹쳐** 서고,
|
|
3117
|
+
* 지운 것을 계속 보고 있는 창이 남는다. 그 사고를 막는 것이 이 줄의 규칙이라
|
|
3118
|
+
* 타입으로 못박아 둔다.
|
|
3119
|
+
*/
|
|
3120
|
+
onClose: () => void;
|
|
3121
|
+
/** 고치기 — **닫고 한다**(다른 창을 연다) */
|
|
3122
|
+
edit?: ItemAction;
|
|
3123
|
+
/** 복제 — 안 닫는다(보던 것을 계속 본다) */
|
|
3124
|
+
duplicate?: ItemAction;
|
|
3125
|
+
/** 발행 ↔ 발행취소 — 안 닫는다. 올리고 내릴 길이 없으면 아예 주지 않는다 */
|
|
3126
|
+
publish?: ItemPublishAction;
|
|
3127
|
+
/** 링크 복사 — 안 닫는다. 손잡이를 안 준 화면에서는 주지 않는다 */
|
|
3128
|
+
copyLink?: ItemAction;
|
|
3129
|
+
/** 지우기 — **닫고 한다** */
|
|
3130
|
+
remove?: ItemAction;
|
|
3131
|
+
className?: string;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3050
3134
|
/**
|
|
3051
3135
|
* 맞았는지 — 훈련 유형마다 칸 이름이 다르다.
|
|
3052
3136
|
*
|
|
@@ -3056,6 +3140,20 @@ export declare function isValidQuestionWord(typeKey: WordTrainingTypeKey, word:
|
|
|
3056
3140
|
*/
|
|
3057
3141
|
export declare function itemCorrectness(r: RawResult): boolean | null;
|
|
3058
3142
|
|
|
3143
|
+
/**
|
|
3144
|
+
* **한 단추가 뒤집히는 자리**(올리기 ↔ 내리기).
|
|
3145
|
+
*
|
|
3146
|
+
* 둘을 나란히 세우지 않는 까닭은 자리가 아니라 읽힘이다 — 나란히 두면 **지금 어느 쪽인지**를
|
|
3147
|
+
* 화면이 말해 주지 않는다. 아이콘은 그대로 두고 하는 일과 이름만 갈린다.
|
|
3148
|
+
* @public
|
|
3149
|
+
*/
|
|
3150
|
+
export declare interface ItemPublishAction {
|
|
3151
|
+
/** 지금 올라가 있나 — 참이면 `unpublish` 쪽이 선다 */
|
|
3152
|
+
published: boolean;
|
|
3153
|
+
publish: ItemAction;
|
|
3154
|
+
unpublish: ItemAction;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3059
3157
|
/**
|
|
3060
3158
|
* @public TTS 전용 변환 — 문장 속 따옴표로 감싼 낱개 자모("ㄱ")를 낱자 이름("기역")으로 치환하고 뒤 조사를 맞춘다.
|
|
3061
3159
|
* 표시 텍스트는 자모 그대로 두고(교육용 표기), 합성·캐시 키에만 이 변환을 적용한다. 자음은 명칭(ㄱ→기역),
|
|
@@ -7816,6 +7914,18 @@ export declare function toUserErrorMessage(msg: string): string;
|
|
|
7816
7914
|
*/
|
|
7817
7915
|
export declare function toWordAttributeFilterCodes(selected: Record<string, unknown> | null | undefined): WordAttributeFilterCodes;
|
|
7818
7916
|
|
|
7917
|
+
/**
|
|
7918
|
+
* 표의 한 줄을 `WordEntry` 로 옮긴다.
|
|
7919
|
+
*
|
|
7920
|
+
* 두 가지를 지킨다(두 앱이 지키던 그대로):
|
|
7921
|
+
* - **`CP` 가 없으면 `undefined`** 로 둔다. `null` 을 그대로 넘기면 "구어체 발음이 있는데 비었다"
|
|
7922
|
+
* 와 "아예 없다" 가 구별되지 않는다.
|
|
7923
|
+
* - **`Grade` 는 글로, `Homonum` 은 수로.** 표가 둘 다 수·글 어느 쪽으로도 줄 수 있어
|
|
7924
|
+
* 받는 쪽에서 견줄 때 꼴이 갈린다.
|
|
7925
|
+
* @public
|
|
7926
|
+
*/
|
|
7927
|
+
export declare const toWordEntry: (row: WordRow) => WordEntry;
|
|
7928
|
+
|
|
7819
7929
|
/** 타입 칩 호버 → TrainingTooltip 표시까지의 지연(ms). 말귀자료실 카드와
|
|
7820
7930
|
* 훈련설정 페이지가 동일한 호버 프리뷰 동작을 각자 구현하고 있어 공유한다.
|
|
7821
7931
|
* @public */
|
|
@@ -9893,6 +10003,32 @@ export declare interface WordPoolEntry {
|
|
|
9893
10003
|
grade?: string;
|
|
9894
10004
|
}
|
|
9895
10005
|
|
|
10006
|
+
/**
|
|
10007
|
+
* **낱말 표(`Word`)의 한 줄** — 서버가 주는 그대로의 꼴.
|
|
10008
|
+
*
|
|
10009
|
+
* 열 이름이 짧고 뜻이 안 드러난다(`SP`·`CP`·`Class`). 그래서 받는 쪽마다 "이게 뭐였지" 를
|
|
10010
|
+
* 다시 풀어야 했고, 두 앱이 같은 풀이를 한 벌씩 들고 있었다(회원앱 `useWordData`,
|
|
10011
|
+
* 어드민 `useAllWords` — 글자까지 같은 줄이 열여덟, 2026-09-25).
|
|
10012
|
+
*
|
|
10013
|
+
* **결과 꼴(`WordEntry`)은 이미 코어 것**이었다. 코어가 *무엇이 되는지*만 정하고
|
|
10014
|
+
* *어떻게 그 꼴이 되는지*는 두 앱이 따로 정하고 있던 셈이다 — 열 이름이 바뀌거나 새 열이
|
|
10015
|
+
* 붙었을 때 한쪽만 고치면 **같은 낱말이 회원앱과 어드민에서 다른 발음·다른 급수로 보인다.**
|
|
10016
|
+
* 그런데 아무 시험도 안 넘어진다. 그래서 여기로 옮긴다.
|
|
10017
|
+
*
|
|
10018
|
+
* 받아 오는 길(쪽 나눔·캐시·진행률)은 앱마다 달라 **그대로 앱에 둔다** — 여기 있는 것은
|
|
10019
|
+
* 한 줄을 옮기는 일뿐이다.
|
|
10020
|
+
* @public
|
|
10021
|
+
*/
|
|
10022
|
+
export declare interface WordRow {
|
|
10023
|
+
Grade: string | number;
|
|
10024
|
+
Word: string;
|
|
10025
|
+
POS: string;
|
|
10026
|
+
Class: string;
|
|
10027
|
+
SP: string;
|
|
10028
|
+
CP?: string | null;
|
|
10029
|
+
Homonum?: number | null;
|
|
10030
|
+
}
|
|
10031
|
+
|
|
9896
10032
|
/**
|
|
9897
10033
|
* 단어를 어떤 조건으로 좁힐지 고르는 FilterPanel 셸(shell) — 등급/품사/어종/음절 수/선택 섹션 자체는
|
|
9898
10034
|
* 호출부가 구성해 넘긴다. WordSearchPage의 [단어 검색 설정] 패널과 WordTrainingFilterPanels의
|
|
@@ -166,6 +166,18 @@
|
|
|
166
166
|
align-items: center;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/* **짧은 구분선도 가운데로.** 그 선은 `margin: 4px var(--spacing-m)` 로 서는데, 명시 폭
|
|
170
|
+
(`--spacing-xl`, 24px)을 가진 플렉스 항목이라 `stretch` 가 안 먹고 **시작 모서리**로
|
|
171
|
+
떨어진다. 그래서 52px 목록 안에서 좌 16 자리에 서고 중심이 38 이 된다 — 레일 중심 36,
|
|
172
|
+
항목·아이콘 중심 36 인데 그 선만 **2px 오른쪽**이었다(실측 2026-09-26, 사장님이
|
|
173
|
+
"이 선이 태블릿일 때 가운데가 아니네" 로 짚으신 것이 이것이다). 가로도 넘친다 —
|
|
174
|
+
16 + 24 + 16 = 56 인데 목록은 52 라 오른쪽 여백 4px 이 잘린다.
|
|
175
|
+
`auto` 여백은 플렉스 항목을 남는 자리 가운데로 민다. 폭·높이·색은 그대로다. */
|
|
176
|
+
.header .side-nav__separator--short {
|
|
177
|
+
margin-left: auto;
|
|
178
|
+
margin-right: auto;
|
|
179
|
+
}
|
|
180
|
+
|
|
169
181
|
.header:hover .side-nav__menu,
|
|
170
182
|
.header:focus-within .side-nav__menu {
|
|
171
183
|
padding: 0 var(--spacing-xs);
|
|
@@ -187,6 +199,15 @@
|
|
|
187
199
|
align-items: stretch;
|
|
188
200
|
}
|
|
189
201
|
|
|
202
|
+
/* **펼쳐지면 가운데 맞춤을 푼다** — 16px 은 눈대중이 아니라 **아이콘 왼쪽 모서리와 줄을
|
|
203
|
+
맞추는 값**이다(220px 레일 실측: 목록 x=482 · 아이콘 x=498 · 선 x=498). 접힌 꼴의
|
|
204
|
+
가운데 맞춤이 펼친 꼴까지 따라오면 그 선만 목록 한복판(+72px)으로 튄다. */
|
|
205
|
+
.header:hover .side-nav__separator--short,
|
|
206
|
+
.header:focus-within .side-nav__separator--short {
|
|
207
|
+
margin-left: var(--spacing-m);
|
|
208
|
+
margin-right: var(--spacing-m);
|
|
209
|
+
}
|
|
210
|
+
|
|
190
211
|
/* Hide label completely from layout when collapsed; fade in on hover */
|
|
191
212
|
.header .side-nav__label {
|
|
192
213
|
display: inline-block;
|
|
@@ -233,6 +254,12 @@
|
|
|
233
254
|
.header.header--force-collapsed:hover {
|
|
234
255
|
width: 72px;
|
|
235
256
|
}
|
|
257
|
+
/* 레일이 안 펼쳐지니 구분선도 가운데에 그대로 있어야 한다 */
|
|
258
|
+
.header.header--force-collapsed:hover .side-nav__separator--short,
|
|
259
|
+
.header.header--force-collapsed:focus-within .side-nav__separator--short {
|
|
260
|
+
margin-left: auto;
|
|
261
|
+
margin-right: auto;
|
|
262
|
+
}
|
|
236
263
|
}
|
|
237
264
|
|
|
238
265
|
/* Mobile: top header + bottom bar */
|
|
@@ -279,12 +306,34 @@
|
|
|
279
306
|
object-fit: contain;
|
|
280
307
|
}
|
|
281
308
|
|
|
309
|
+
/* **심볼과 로고 중 어느 쪽을 보일지는 여기서 고른다** — 까닭은 `Header.tsx` 의 `BrandIdentity` 에.
|
|
310
|
+
기본은 로고다(넓은 화면·폰). 심볼은 접힌 레일에서만 선다. */
|
|
311
|
+
.brand-identity__image--symbol { display: none; }
|
|
312
|
+
|
|
282
313
|
@media (min-width: 1024px) {
|
|
283
314
|
.brand-identity__root { height: 100px; }
|
|
284
315
|
}
|
|
285
316
|
|
|
286
317
|
@media (min-width: 768px) and (max-width: 1023px) {
|
|
287
318
|
.brand-identity__root { height: 100px; }
|
|
319
|
+
|
|
320
|
+
/* 접힌 72px 레일에는 로고가 설 자리가 없다 — 심볼로 선다. */
|
|
321
|
+
.brand-identity__image--symbol { display: block; }
|
|
322
|
+
.brand-identity__image--logo { display: none; }
|
|
323
|
+
|
|
324
|
+
/* **펼치면 넓은 화면과 같은 얼굴이 된다.** 220px 으로 펼친 레일은 넓은 화면과 같은 폭이라,
|
|
325
|
+
거기서 심볼로 남으면 같은 레일이 같은 폭에서 다른 얼굴을 한다. */
|
|
326
|
+
.header:hover .brand-identity__image--symbol,
|
|
327
|
+
.header:focus-within .brand-identity__image--symbol { display: none; }
|
|
328
|
+
.header:hover .brand-identity__image--logo,
|
|
329
|
+
.header:focus-within .brand-identity__image--logo { display: block; }
|
|
330
|
+
|
|
331
|
+
/* **못 박아 접어 둔 레일은 호버해도 안 펼쳐진다**(`:hover { width: 72px }`) — 그때는 심볼 그대로다.
|
|
332
|
+
이 갈래가 없으면 72px 자리에 로고가 들어가 잘린다. */
|
|
333
|
+
.header.header--force-collapsed:hover .brand-identity__image--symbol,
|
|
334
|
+
.header.header--force-collapsed:focus-within .brand-identity__image--symbol { display: block; }
|
|
335
|
+
.header.header--force-collapsed:hover .brand-identity__image--logo,
|
|
336
|
+
.header.header--force-collapsed:focus-within .brand-identity__image--logo { display: none; }
|
|
288
337
|
}
|
|
289
338
|
|
|
290
339
|
@media (max-width: 767px) {
|
|
@@ -26,8 +26,14 @@ export function BrandIdentity({ appMode }) {
|
|
|
26
26
|
? (typeof LogoManagerSvg === 'string' && LogoManagerSvg) || resolveAssetUrl('logo/Logo_speechearManager_kr.svg')
|
|
27
27
|
: (typeof LogoSvg === 'string' && LogoSvg) || resolveAssetUrl('logo/Logo_speechear_kr.svg');
|
|
28
28
|
const symbol = (typeof SymbolSvg === 'string' && SymbolSvg) || resolveAssetUrl('logo/Symbol_speechear.svg');
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/* **둘을 다 그려 두고 고르는 것은 CSS 가 한다.** 예전에는 여기서 화면 폭만 보고
|
|
30
|
+
(`(min-width: 768px) and (max-width: 1023px)`) 심볼이냐 로고냐를 갈랐는데, **그 폭에서
|
|
31
|
+
레일은 두 모양이다** — 접힌 72px 과, 호버·포커스로 펼친 220px. 펼친 220 은 넓은 화면과
|
|
32
|
+
같은 폭인데 BI 만 심볼로 남아, 같은 레일이 같은 폭에서 다른 얼굴을 했다(사장님 지시
|
|
33
|
+
2026-09-26 — "테블릿에서 호버했을 때 데스크탑과 동일하게"). 펼침은 `:hover`·`:focus-within`
|
|
34
|
+
이라 **CSS 만 아는 상태**이고, JS 분기로는 `img` 의 `src` 를 바꿀 길이 없다.
|
|
35
|
+
안 보이는 쪽은 `display: none` 이라 낭독기도 안 읽는다 — `alt` 를 둘 다 둬도 한 번만 읽힌다. */
|
|
36
|
+
return (_jsxs("div", { className: "brand-identity__root", "aria-label": t('header.biArea', 'BI 영역'), children: [_jsx("img", { src: symbol, alt: t('header.biSymbol', 'BI 심볼'), className: "brand-identity__image brand-identity__image--symbol" }), _jsx("img", { src: logo, alt: isManager ? t('header.logoManager', '말귀매니저 로고') : t('header.logoMalgwi', '말귀 로고'), className: "brand-identity__image brand-identity__image--logo" })] }));
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* 앱 전체 레이아웃의 내비게이션 헤더(사이드바)입니다.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* 보고 있는 한 항목에 하는 일들 — 까닭과 쓰는 법은 `ItemActions.tsx` 의 글에.
|
|
2
|
+
|
|
3
|
+
제 간격을 스스로 든다. `Popup` 의 `footerStart` 는 이미 같은 간격을 주지만(`--spacing-xs`),
|
|
4
|
+
바닥 줄을 손수 지어 쓰는 창(`WordTablePopup` 의 `renderFooter`)도 있어서 거기서도 같게
|
|
5
|
+
서야 한다 — 말귀가 그 자리에 이 네 줄을 그대로 베껴 두고 있었다(2026-09-26). */
|
|
6
|
+
.item-actions {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: var(--spacing-xs);
|
|
10
|
+
/* 좁아져도 잘리는 대신 줄을 바꾼다 — 바닥이 잘려 못 누르던 자리를 겪었다(0.185.0) */
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import './ItemActions.css';
|
|
2
|
+
/** @public */
|
|
3
|
+
export interface ItemAction {
|
|
4
|
+
/**
|
|
5
|
+
* 읽어 줄 이름. 아이콘뿐인 단추라 **이름과 말풍선에 같은 말**이 붙는다 —
|
|
6
|
+
* 한쪽만 달면 눈으로 본 말과 낭독기가 읽는 말이 갈린다.
|
|
7
|
+
*/
|
|
8
|
+
label: string;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* **한 단추가 뒤집히는 자리**(올리기 ↔ 내리기).
|
|
13
|
+
*
|
|
14
|
+
* 둘을 나란히 세우지 않는 까닭은 자리가 아니라 읽힘이다 — 나란히 두면 **지금 어느 쪽인지**를
|
|
15
|
+
* 화면이 말해 주지 않는다. 아이콘은 그대로 두고 하는 일과 이름만 갈린다.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export interface ItemPublishAction {
|
|
19
|
+
/** 지금 올라가 있나 — 참이면 `unpublish` 쪽이 선다 */
|
|
20
|
+
published: boolean;
|
|
21
|
+
publish: ItemAction;
|
|
22
|
+
unpublish: ItemAction;
|
|
23
|
+
}
|
|
24
|
+
/** @public */
|
|
25
|
+
export interface ItemActionsProps {
|
|
26
|
+
/**
|
|
27
|
+
* 이 창을 닫는 길. **닫고 하는 것**(고치기 · 지우기)이 제 일을 하기 전에 이것을 먼저 부른다.
|
|
28
|
+
*
|
|
29
|
+
* 줄 것이 없어 보여도 비우지 않는다 — 안 닫으면 고칠 창이 보던 창 **위에 겹쳐** 서고,
|
|
30
|
+
* 지운 것을 계속 보고 있는 창이 남는다. 그 사고를 막는 것이 이 줄의 규칙이라
|
|
31
|
+
* 타입으로 못박아 둔다.
|
|
32
|
+
*/
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
/** 고치기 — **닫고 한다**(다른 창을 연다) */
|
|
35
|
+
edit?: ItemAction;
|
|
36
|
+
/** 복제 — 안 닫는다(보던 것을 계속 본다) */
|
|
37
|
+
duplicate?: ItemAction;
|
|
38
|
+
/** 발행 ↔ 발행취소 — 안 닫는다. 올리고 내릴 길이 없으면 아예 주지 않는다 */
|
|
39
|
+
publish?: ItemPublishAction;
|
|
40
|
+
/** 링크 복사 — 안 닫는다. 손잡이를 안 준 화면에서는 주지 않는다 */
|
|
41
|
+
copyLink?: ItemAction;
|
|
42
|
+
/** 지우기 — **닫고 한다** */
|
|
43
|
+
remove?: ItemAction;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* **보고 있는 한 항목에 하는 일들** — 창 바닥 왼쪽에 서는 아이콘 단추 줄.
|
|
48
|
+
*
|
|
49
|
+
* 무언가를 **보여 주는 창**이 동시에 **그것을 손대는 자리**이기도 한 곳에 선다. 말귀에 그런
|
|
50
|
+
* 창이 셋 있었고(훈련 묶음 실행 · 단어훈련 카드 실행 · 단어표 미리보기), 세 곳이 같은 다섯
|
|
51
|
+
* 단추를 같은 차례로 세우면서 규칙을 한 벌씩 베껴 적고 있었다(2026-09-26).
|
|
52
|
+
*
|
|
53
|
+
* 벌리는 일은 이미 코어 몫이다(`Popup` 의 `footerStart`/`footerEnd`, 0.190.0). 남아 있던 것은
|
|
54
|
+
* **무엇을 어떤 차례로 세우고 무엇을 닫고 하는가**인데, 그것이 베껴져 있으면 한 곳만 고쳐도
|
|
55
|
+
* 아무 데서도 안 터진다 — 같은 모양의 창들이 조용히 서로 다르게 움직인다. 실제로 **링크 단추의
|
|
56
|
+
* 조건이 세 곳에서 달랐다**(둘은 길이 있을 때만, 하나는 늘 세움).
|
|
57
|
+
*
|
|
58
|
+
* 이 조각이 쥐는 것 —
|
|
59
|
+
* · **차례와 꼴** — 고치기 · 복제 · 발행 · 링크 · 지우기, 모두 `size={40} type="dot"`.
|
|
60
|
+
* · **닫고 할 것과 안 닫고 할 것** — 고치기 · 지우기는 `onClose` 를 먼저 부른다. 둘 다 **다른 창을
|
|
61
|
+
* 여는** 일이라 안 닫으면 창 위에 창이 겹치고, 지운 것을 계속 보고 있게 된다. 복제 · 발행 ·
|
|
62
|
+
* 링크는 안 닫는다 — 보던 것을 그대로 두고 곁에서 일어나는 일이다.
|
|
63
|
+
* · **안 준 것은 안 세운다** — 부르는 쪽이 손으로 `&&` 하던 자리다. 갈라진 그 자리가 여기다.
|
|
64
|
+
* · **뒤집히는 단추는 하나** — `publish.published` 가 어느 쪽인지 고른다.
|
|
65
|
+
*
|
|
66
|
+
* 새 갈래가 필요하면 **여기에 칸을 만든다.** 아무거나 끼울 구멍(`children` 같은 것)을 두지 않는
|
|
67
|
+
* 까닭은, 그 구멍으로 들어온 단추는 닫고 할지 말지를 또 부르는 쪽이 정하게 되어 이 조각이
|
|
68
|
+
* 막으려던 갈라짐이 그대로 돌아오기 때문이다.
|
|
69
|
+
*
|
|
70
|
+
* `Popup` 의 `footerStart` 안에서도, 손수 지은 바닥 줄 안에서도 같게 선다 — 제 간격을 스스로
|
|
71
|
+
* 든다(`--spacing-xs`).
|
|
72
|
+
*
|
|
73
|
+
* 이름표는 부르는 쪽의 말이다 — 아래 예의 글자 자리에 그쪽 사전에서 꺼낸 말을 넣는다.
|
|
74
|
+
* (여기에 `t('…')` 을 적지 않는 까닭은, 소스를 훑는 시험이 **주석 속 예시도 실제로 부르는
|
|
75
|
+
* 열쇠로 읽기** 때문이다. 글이 시험을 깨뜨릴 수 있으면 안 된다.)
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* <Popup
|
|
79
|
+
* title={표.name}
|
|
80
|
+
* footerStart={({ closeWithAnimation }) => (
|
|
81
|
+
* <ItemActions
|
|
82
|
+
* onClose={closeWithAnimation}
|
|
83
|
+
* edit={{ label: '수정', onClick: () => openEdit(표) }}
|
|
84
|
+
* duplicate={{ label: '복제', onClick: () => duplicate(표) }}
|
|
85
|
+
* publish={{
|
|
86
|
+
* published: 표.isPublished,
|
|
87
|
+
* publish: { label: '말귀자료실에 올리기', onClick: () => publish(표) },
|
|
88
|
+
* unpublish: { label: '말귀자료실에서 내리기', onClick: () => unpublish(표) },
|
|
89
|
+
* }}
|
|
90
|
+
* copyLink={링크길있나 ? { label: '링크 복사', onClick: () => copyLink(표) } : undefined}
|
|
91
|
+
* remove={{ label: '삭제', onClick: () => askDelete(표) }} />
|
|
92
|
+
* )} />
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
export default function ItemActions(props: ItemActionsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import Button from './Button';
|
|
4
|
+
import { cx } from '../utils/cx';
|
|
5
|
+
import './ItemActions.css';
|
|
6
|
+
/**
|
|
7
|
+
* **차례 · 아이콘 · 무엇을 닫고 하는가**를 한 표로 둔다.
|
|
8
|
+
*
|
|
9
|
+
* 세 곳에 베껴져 있을 때 실제로 갈라진 것이 이 표의 값들이었다 — 링크 단추의 조건이
|
|
10
|
+
* 둘은 `&&`, 하나는 늘 세움이었다. 한자리에 있으면 갈라질 자리가 없다.
|
|
11
|
+
*/
|
|
12
|
+
const 차례 = [
|
|
13
|
+
{ 열쇠: 'edit', icon: 'edit-2', 닫고할까: true },
|
|
14
|
+
{ 열쇠: 'duplicate', icon: 'copy', 닫고할까: false },
|
|
15
|
+
{ 열쇠: 'publish', icon: 'book-marked', 닫고할까: false },
|
|
16
|
+
{ 열쇠: 'copyLink', icon: 'link', 닫고할까: false },
|
|
17
|
+
{ 열쇠: 'remove', icon: 'trash-2', 닫고할까: true },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* **보고 있는 한 항목에 하는 일들** — 창 바닥 왼쪽에 서는 아이콘 단추 줄.
|
|
21
|
+
*
|
|
22
|
+
* 무언가를 **보여 주는 창**이 동시에 **그것을 손대는 자리**이기도 한 곳에 선다. 말귀에 그런
|
|
23
|
+
* 창이 셋 있었고(훈련 묶음 실행 · 단어훈련 카드 실행 · 단어표 미리보기), 세 곳이 같은 다섯
|
|
24
|
+
* 단추를 같은 차례로 세우면서 규칙을 한 벌씩 베껴 적고 있었다(2026-09-26).
|
|
25
|
+
*
|
|
26
|
+
* 벌리는 일은 이미 코어 몫이다(`Popup` 의 `footerStart`/`footerEnd`, 0.190.0). 남아 있던 것은
|
|
27
|
+
* **무엇을 어떤 차례로 세우고 무엇을 닫고 하는가**인데, 그것이 베껴져 있으면 한 곳만 고쳐도
|
|
28
|
+
* 아무 데서도 안 터진다 — 같은 모양의 창들이 조용히 서로 다르게 움직인다. 실제로 **링크 단추의
|
|
29
|
+
* 조건이 세 곳에서 달랐다**(둘은 길이 있을 때만, 하나는 늘 세움).
|
|
30
|
+
*
|
|
31
|
+
* 이 조각이 쥐는 것 —
|
|
32
|
+
* · **차례와 꼴** — 고치기 · 복제 · 발행 · 링크 · 지우기, 모두 `size={40} type="dot"`.
|
|
33
|
+
* · **닫고 할 것과 안 닫고 할 것** — 고치기 · 지우기는 `onClose` 를 먼저 부른다. 둘 다 **다른 창을
|
|
34
|
+
* 여는** 일이라 안 닫으면 창 위에 창이 겹치고, 지운 것을 계속 보고 있게 된다. 복제 · 발행 ·
|
|
35
|
+
* 링크는 안 닫는다 — 보던 것을 그대로 두고 곁에서 일어나는 일이다.
|
|
36
|
+
* · **안 준 것은 안 세운다** — 부르는 쪽이 손으로 `&&` 하던 자리다. 갈라진 그 자리가 여기다.
|
|
37
|
+
* · **뒤집히는 단추는 하나** — `publish.published` 가 어느 쪽인지 고른다.
|
|
38
|
+
*
|
|
39
|
+
* 새 갈래가 필요하면 **여기에 칸을 만든다.** 아무거나 끼울 구멍(`children` 같은 것)을 두지 않는
|
|
40
|
+
* 까닭은, 그 구멍으로 들어온 단추는 닫고 할지 말지를 또 부르는 쪽이 정하게 되어 이 조각이
|
|
41
|
+
* 막으려던 갈라짐이 그대로 돌아오기 때문이다.
|
|
42
|
+
*
|
|
43
|
+
* `Popup` 의 `footerStart` 안에서도, 손수 지은 바닥 줄 안에서도 같게 선다 — 제 간격을 스스로
|
|
44
|
+
* 든다(`--spacing-xs`).
|
|
45
|
+
*
|
|
46
|
+
* 이름표는 부르는 쪽의 말이다 — 아래 예의 글자 자리에 그쪽 사전에서 꺼낸 말을 넣는다.
|
|
47
|
+
* (여기에 `t('…')` 을 적지 않는 까닭은, 소스를 훑는 시험이 **주석 속 예시도 실제로 부르는
|
|
48
|
+
* 열쇠로 읽기** 때문이다. 글이 시험을 깨뜨릴 수 있으면 안 된다.)
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* <Popup
|
|
52
|
+
* title={표.name}
|
|
53
|
+
* footerStart={({ closeWithAnimation }) => (
|
|
54
|
+
* <ItemActions
|
|
55
|
+
* onClose={closeWithAnimation}
|
|
56
|
+
* edit={{ label: '수정', onClick: () => openEdit(표) }}
|
|
57
|
+
* duplicate={{ label: '복제', onClick: () => duplicate(표) }}
|
|
58
|
+
* publish={{
|
|
59
|
+
* published: 표.isPublished,
|
|
60
|
+
* publish: { label: '말귀자료실에 올리기', onClick: () => publish(표) },
|
|
61
|
+
* unpublish: { label: '말귀자료실에서 내리기', onClick: () => unpublish(표) },
|
|
62
|
+
* }}
|
|
63
|
+
* copyLink={링크길있나 ? { label: '링크 복사', onClick: () => copyLink(표) } : undefined}
|
|
64
|
+
* remove={{ label: '삭제', onClick: () => askDelete(표) }} />
|
|
65
|
+
* )} />
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export default function ItemActions(props) {
|
|
69
|
+
const { onClose, className } = props;
|
|
70
|
+
return (_jsx("div", { className: cx('item-actions', className), children: 차례.map(({ 열쇠, icon, 닫고할까 }) => {
|
|
71
|
+
const 받은것 = props[열쇠];
|
|
72
|
+
if (!받은것)
|
|
73
|
+
return null;
|
|
74
|
+
const 할일 = 열쇠 === 'publish'
|
|
75
|
+
? (받은것.published
|
|
76
|
+
? 받은것.unpublish
|
|
77
|
+
: 받은것.publish)
|
|
78
|
+
: 받은것;
|
|
79
|
+
return (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: icon, ariaLabel: 할일.label, showTooltipInfo: 할일.label, onClick: () => { if (닫고할까)
|
|
80
|
+
onClose(); 할일.onClick(); } }, 열쇠));
|
|
81
|
+
}) }));
|
|
82
|
+
}
|
|
@@ -16,10 +16,17 @@
|
|
|
16
16
|
`패딩 1 + 막대 11` 로 채운다(사장님 지시 2026-09-25). 그래서 좌우 패딩이 다르다 — 숫자가
|
|
17
17
|
달라 보여도 **눈에 보이는 틈은 같다.**
|
|
18
18
|
|
|
19
|
-
막대가 11px 인 것은 고른 값이 아니라 **브라우저가 정하는 값**이다.
|
|
20
|
-
`scrollbar-width
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
막대가 11px 인 것은 고른 값이 아니라 **브라우저가 정하는 값**이다. 실측(Chrome 152,
|
|
20
|
+
2026-09-25): `scrollbar-width` 가 thin 이면 11 · auto 15 · none 0 이고,
|
|
21
|
+
`::-webkit-scrollbar { width }` 는 **있든 없든 값이 같다** — 이 저장소가 스크롤바 폭을 적어 둔
|
|
22
|
+
자리들(`styles.css` 6px, 훈련 팝업 4px 등)은 **이 브라우저에서는** 아무 일도 안 한다.
|
|
23
|
+
|
|
24
|
+
**두 가지를 확인하지 못했다.** ① 왜 안 먹는지 — 처음에는 "표준 속성이 걸려 있어 Chrome 이
|
|
25
|
+
webkit 쪽을 안 본다" 고 적었는데, 그 규칙을 못 찾은 페이지에서 재도 11 이었다는 실측이 있다.
|
|
26
|
+
② **사파리는 `::-webkit-scrollbar` 를 존중한다** — 여기서 잴 수단이 없어 못 봤다. 그러니
|
|
27
|
+
그 규칙들을 "죽은 줄" 로 단정해 지우면 안 된다(말귀가 같은 까닭으로 제 쪽 4px 을 안 걷었다).
|
|
28
|
+
아래에서 막대를 직접 그리는 쪽으로 간 것은 그래서 더 낫다 — 브라우저마다 다른 이 자리를
|
|
29
|
+
아예 안 밟는다.
|
|
23
30
|
그러니 이 11 은 기계·설정에 따라 달라질 수 있는 수다. macOS 오버레이 스크롤바에서는 0 이라
|
|
24
31
|
오른쪽이 1px 만 남는다 — 그 환경에서 좌우가 어긋나 보이면 그때 다시 정하면 된다.
|
|
25
32
|
|
|
@@ -133,10 +140,22 @@
|
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
/* ── footer: 항상 list 아래에 위치, 줄어들지 않음 ─────────────────────────── */
|
|
143
|
+
/* **배지를 얹으려고 알림 단추에만 한 겹 두른 것**(`position: relative` 가 배지의 기준이다).
|
|
144
|
+
그 한 겹 때문에 이 하나만 발치 줄에서 다르게 놓였다.
|
|
145
|
+
|
|
146
|
+
`align-self: flex-start` 였다 — 접힌 레일에서 발치 줄은 `align-items: center` 인데 이 선언이
|
|
147
|
+
그것을 덮어 **혼자 왼쪽에 붙었다**. 단추가 동그라미라 다섯이 한 줄로 서면 하나 틀어진 것이
|
|
148
|
+
바로 보인다(말귀 실측 2026-09-26: 넷은 가운데 36, 이것만 30 — 6px. 놀이터는 단추가 한 급
|
|
149
|
+
작아져 4px 이지만 같은 일이다. 어긋남은 `(줄 폭 − 단추 폭) / 2` 다).
|
|
150
|
+
|
|
151
|
+
**`align-self` 만 걷으면 안 된다.** 펼친 줄은 `align-items: stretch` 라 이 span 이 늘어난다 —
|
|
152
|
+
1440px 에서 **194px** 이 됐다(실측). 단추는 48 그대로라 눈에는 같지만, 줄 전체가 알림 단추의
|
|
153
|
+
몸이 되어 빈 자리에서도 호버·클릭이 걸린다. `width: fit-content` 는 definite width 라
|
|
154
|
+
stretch 를 막는다. 둘을 함께 둬야 두 폭 모두 제자리다(768·1440 재확인). */
|
|
136
155
|
.side-nav__notification-wrap {
|
|
137
156
|
position: relative;
|
|
138
157
|
display: inline-flex;
|
|
139
|
-
|
|
158
|
+
width: fit-content;
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
.side-nav__footer {
|
|
@@ -134,6 +134,8 @@ export { default as Checkbox } from './components/Checkbox';
|
|
|
134
134
|
export type { CheckboxProps } from './components/Checkbox';
|
|
135
135
|
export { default as BulkBar } from './components/BulkBar';
|
|
136
136
|
export type { BulkBarProps, BulkAction } from './components/BulkBar';
|
|
137
|
+
export { default as ItemActions } from './components/ItemActions';
|
|
138
|
+
export type { ItemActionsProps, ItemAction, ItemPublishAction } from './components/ItemActions';
|
|
137
139
|
export { default as DatePicker } from './components/DatePicker';
|
|
138
140
|
export type { DatePickerProps } from './components/DatePicker';
|
|
139
141
|
export { useCalendarMonth } from './components/useCalendarMonth';
|
|
@@ -87,6 +87,7 @@ export { useAnchoredMenu } from './hooks/useAnchoredMenu';
|
|
|
87
87
|
export { default as Toggle } from './components/Toggle';
|
|
88
88
|
export { default as Checkbox } from './components/Checkbox';
|
|
89
89
|
export { default as BulkBar } from './components/BulkBar';
|
|
90
|
+
export { default as ItemActions } from './components/ItemActions';
|
|
90
91
|
export { default as DatePicker } from './components/DatePicker';
|
|
91
92
|
export { useCalendarMonth } from './components/useCalendarMonth';
|
|
92
93
|
export { default as BarChart, BarChartControls } from './components/BarChart';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { WordEntry } from './phonemePairSearchTypes';
|
|
2
|
+
/**
|
|
3
|
+
* **낱말 표(`Word`)의 한 줄** — 서버가 주는 그대로의 꼴.
|
|
4
|
+
*
|
|
5
|
+
* 열 이름이 짧고 뜻이 안 드러난다(`SP`·`CP`·`Class`). 그래서 받는 쪽마다 "이게 뭐였지" 를
|
|
6
|
+
* 다시 풀어야 했고, 두 앱이 같은 풀이를 한 벌씩 들고 있었다(회원앱 `useWordData`,
|
|
7
|
+
* 어드민 `useAllWords` — 글자까지 같은 줄이 열여덟, 2026-09-25).
|
|
8
|
+
*
|
|
9
|
+
* **결과 꼴(`WordEntry`)은 이미 코어 것**이었다. 코어가 *무엇이 되는지*만 정하고
|
|
10
|
+
* *어떻게 그 꼴이 되는지*는 두 앱이 따로 정하고 있던 셈이다 — 열 이름이 바뀌거나 새 열이
|
|
11
|
+
* 붙었을 때 한쪽만 고치면 **같은 낱말이 회원앱과 어드민에서 다른 발음·다른 급수로 보인다.**
|
|
12
|
+
* 그런데 아무 시험도 안 넘어진다. 그래서 여기로 옮긴다.
|
|
13
|
+
*
|
|
14
|
+
* 받아 오는 길(쪽 나눔·캐시·진행률)은 앱마다 달라 **그대로 앱에 둔다** — 여기 있는 것은
|
|
15
|
+
* 한 줄을 옮기는 일뿐이다.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export interface WordRow {
|
|
19
|
+
Grade: string | number;
|
|
20
|
+
Word: string;
|
|
21
|
+
POS: string;
|
|
22
|
+
Class: string;
|
|
23
|
+
SP: string;
|
|
24
|
+
CP?: string | null;
|
|
25
|
+
Homonum?: number | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 표의 한 줄을 `WordEntry` 로 옮긴다.
|
|
29
|
+
*
|
|
30
|
+
* 두 가지를 지킨다(두 앱이 지키던 그대로):
|
|
31
|
+
* - **`CP` 가 없으면 `undefined`** 로 둔다. `null` 을 그대로 넘기면 "구어체 발음이 있는데 비었다"
|
|
32
|
+
* 와 "아예 없다" 가 구별되지 않는다.
|
|
33
|
+
* - **`Grade` 는 글로, `Homonum` 은 수로.** 표가 둘 다 수·글 어느 쪽으로도 줄 수 있어
|
|
34
|
+
* 받는 쪽에서 견줄 때 꼴이 갈린다.
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export declare const toWordEntry: (row: WordRow) => WordEntry;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 표의 한 줄을 `WordEntry` 로 옮긴다.
|
|
3
|
+
*
|
|
4
|
+
* 두 가지를 지킨다(두 앱이 지키던 그대로):
|
|
5
|
+
* - **`CP` 가 없으면 `undefined`** 로 둔다. `null` 을 그대로 넘기면 "구어체 발음이 있는데 비었다"
|
|
6
|
+
* 와 "아예 없다" 가 구별되지 않는다.
|
|
7
|
+
* - **`Grade` 는 글로, `Homonum` 은 수로.** 표가 둘 다 수·글 어느 쪽으로도 줄 수 있어
|
|
8
|
+
* 받는 쪽에서 견줄 때 꼴이 갈린다.
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export const toWordEntry = (row) => ({
|
|
12
|
+
word: row.Word,
|
|
13
|
+
pronunciation: row.SP,
|
|
14
|
+
colloquialPronunciation: row.CP ?? undefined,
|
|
15
|
+
grade: String(row.Grade),
|
|
16
|
+
pos: row.POS,
|
|
17
|
+
wordType: row.Class,
|
|
18
|
+
homonum: row.Homonum != null ? Number(row.Homonum) : undefined,
|
|
19
|
+
});
|
|
@@ -58,4 +58,6 @@ export { WORD_SUGGESTION_WEEKLY_QUOTA, WORD_SUGGESTION_ACCOUNT_LIMIT, WORD_SUGGE
|
|
|
58
58
|
export { recoverFromSkew, skewReloadPending } from './utils/chunkSkew';
|
|
59
59
|
export { lazyPage } from './utils/lazyPage';
|
|
60
60
|
export { prefetchChunk } from './utils/prefetchChunk';
|
|
61
|
+
export { toWordEntry } from './utils/wordRow';
|
|
62
|
+
export type { WordRow } from './utils/wordRow';
|
|
61
63
|
export { CORE_VERSION } from './version';
|
|
@@ -47,4 +47,5 @@ export { WORD_SUGGESTION_WEEKLY_QUOTA, WORD_SUGGESTION_ACCOUNT_LIMIT, WORD_SUGGE
|
|
|
47
47
|
export { recoverFromSkew, skewReloadPending } from './utils/chunkSkew';
|
|
48
48
|
export { lazyPage } from './utils/lazyPage';
|
|
49
49
|
export { prefetchChunk } from './utils/prefetchChunk';
|
|
50
|
+
export { toWordEntry } from './utils/wordRow';
|
|
50
51
|
export { CORE_VERSION } from './version';
|