@ihabdevteam/core 0.349.0 → 0.351.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 CHANGED
@@ -7,6 +7,271 @@ All notable changes to this project will be documented in this file.
7
7
  > 적은 글이라, 지금 이름으로 고쳐 놓으면 그 시절에 그 이름으로 받은 사람이
8
8
  > 제 이야기를 못 찾는다.
9
9
 
10
+ ## 0.351.0 — 체크박스를 누르면 폰에서 창이 맨 위로 튀던 것
11
+
12
+ ### 체크박스를 누르면 폰에서 창이 맨 위로 튀던 것
13
+
14
+ 사장님 신고(2026-10-20) · 말귀가 **실제 폰에서 재현하고 자리까지 짚어** 왔다. 가입 창의
15
+ 동의 칸을 누르면 창이 맨 위로 튀었다.
16
+
17
+ `.checkbox__input` 은 진짜 `<input>` 을 **1px 로 숨기고**(`position: absolute` · `clip`) 곁의
18
+ 그림을 대신 그린다 — 자판과 읽어 주기가 살아 있으려면 칸을 지우면 안 되기 때문이다.
19
+ 그런데 감싸개 `.checkbox` 에 `position` 이 없어 그 칸이 **가장 가까운 자리 잡힌 조상**,
20
+ 즉 **창의 스크롤 통**에 매였다. 이름표를 누르면 숨은 칸이 초점을 받고 브라우저가 그것을
21
+ 보이려 거기로 굴린다.
22
+
23
+ **브라우저로 견줬다** — 놀이터의 체크박스 셋:
24
+
25
+ | | 고치기 전 | 고친 뒤 |
26
+ |---|---|---|
27
+ | 감싸개 `position` | `static` | `relative` |
28
+ | 숨은 칸의 `offsetParent` | **`main.playground-content`**(스크롤 통) | **`label.checkbox`** |
29
+ | `offsetTop` | 4916 · 5072 · 9142 | 84 · 52 · 21 |
30
+
31
+ `offsetParent` 가 바로 "가장 가까운 자리 잡힌 조상" 이라 이것을 물으면 눈으로 재는 것보다
32
+ 또렷하다(쪽이 3만px 라 `scrollIntoView` 로는 굴림이 안 멎어 흔들렸다).
33
+
34
+ **`.radio`·`.toggle` 은 진작 `position: relative` 를 갖고 있었고 `.checkbox` 만 빠져 있었다.**
35
+ 같은 꼴로 숨기는 자리가 여섯인데, **초점을 받는 것은 입력칸 셋뿐**이다(나머지는 읽어 주기용
36
+ 글이라 초점을 안 받아 안 튄다). `.word-table-cell__score-slot` 은 단추가 들지만 조상
37
+ (`.word-table-cell`)이 자리를 잡고 있어 멀쩡하다.
38
+
39
+ **못을 세웠다** — `test/hiddenInputNeedsAnchor1020.test.ts`. CSS 를 글자가 아니라 postcss 로
40
+ 파서, 숨기는 규칙(`position: absolute` + 눈에서 치우는 표시)을 찾고 그 **감싸개가 자리를
41
+ 잡았는지** 본다. 이 꼴은 **터지지도 울지도 콘솔에 남지도 않고 화면만 튄다** — 눈으로만 잡힌다.
42
+ `.checkbox` 와 `.radio` 양쪽에서 `position` 을 빼어 둘 다 자리를 또박또박 짚는 것을 봤다.
43
+
44
+ 앱이 `src/app/App.css` 에 임시로 덮어 둔 것은 이 판을 받으면 걷으면 된다.
45
+
46
+ **새 시험에 한글 이름을 또 썼다**(`못짚음`·`뜬것`) — `identifiersAreEnglish1013` 이 울렸다.
47
+ 집안 규약은 시험에도 걸리는데 **같은 자리에서 세 번째다.** 다음부터는 시험을 쓴 뒤 담기 전에
48
+ 그 문을 먼저 돌린다 — 관문 한 바퀴(10분)를 거기에 쓸 까닭이 없다.
49
+
50
+ ## 0.350.0 — `DatePicker` 를 손으로 치고, `PanelRowText` 가 마디를 받는다
51
+
52
+ ### `DatePicker` 연·월 — 가운데로 모으고, 1900년까지 연다
53
+
54
+ 사장님 지시(2026-10-20), 앞 판에 이어 둘.
55
+
56
+ **① 연·월이 양쪽 끝으로 벌어져 있었다.** 둘을 나비(`.date-picker__nav`)의 바로 밑에 두고
57
+ 저마다 `flex: 1` 을 주었더니 남는 자리를 반씩 먹어, **한 날짜를 가리키는 둘이 서로 남처럼**
58
+ 보였다. 묶음(`.date-picker__nav-ym`)으로 싸서 가운데에 세운다.
59
+
60
+ 묶은 것만으로는 모자랐다 — `Dropdown` 의 트리거가 `width: 100%` 에 `min-width: 120px` 라
61
+ `2026년`(54px)·`10월`(24px)인데도 **둘 다 120px** 로 섰다. 날짜 나비 안에서만 글자만큼 쥐게
62
+ 내린다(고르개 제 밑값은 폼 칸에 설 때 필요한 값이라 안 건드린다).
63
+
64
+ 브라우저 실측: 연 85px · 월 63px · 둘 사이 4px, **묶음 가운데 513 = 나비 가운데 513**,
65
+ 양 단추와의 사이도 43px 로 같다.
66
+
67
+ **② 맨 위로 올려도 2013년이었다.** 해 목록이 `viewY ± 10` 이라 열 해뿐이어서 **생년월일을
68
+ 못 골랐다.** 바닥을 `1900`(사장님: 1900년까지만 나오면 된다), 꼭대기를 **오늘에서 열 해**로
69
+ 둔다 — 1900~2036, 137해.
70
+
71
+ **기준이 오늘인 것이 요점이다.** `viewY` 기준이면 1950년으로 간 뒤 목록이 1830년까지
72
+ **따라 흘러간다** — 목록이 제자리에 안 선다. 바닥을 숫자로 박는 것은 여기서 맞다: **최근
73
+ 해**를 박으면 세월이 지나며 그 해가 범위 끝이 되지만, 바닥은 멀어지기만 한다.
74
+
75
+ 보고 있는 해는 `Math.min`/`Math.max` 로 **반드시 넣는다** — 범위 밖 `value` 가 오면 고르개가
76
+ 제 값을 목록에서 못 찾아 빈 이름으로 떨어진다.
77
+
78
+ 심어서 둘을 확인했다 — 옛 범위(`viewY ± 10`)로 되돌리면 *"맨 앞이 1900년이 아니다"*,
79
+ 기준을 보고 있는 해로 두면 *"먼 해로 갔더니 목록 맨 앞이 더 옛날로 밀렸다"* 로 운다.
80
+
81
+
82
+ ### `DatePicker` — 연·월을 따로 고르고, 손으로 쳐서 넣는다
83
+
84
+ 사장님 지시(2026-10-20), 둘이다.
85
+
86
+ #### ① 연월 한 덩어리를 **연·월 둘로**
87
+
88
+ `2026년 10월` 을 한 고르개에 **252줄**로 늘어놓고 있었다. 한 해 앞으로 가려면 열두 줄을
89
+ 지나야 했고, 목록이 길어 어디쯤인지도 안 보였다. 이제 `2026년 ∨` `10월 ∨` 둘이다.
90
+
91
+ **자르는 규칙(`min`·`max`)은 그대로다.** 달 목록은 **고른 해에 따라** 달라진다 —
92
+ `min = 2026-10` 이면 2026년에는 10·11·12월뿐이다.
93
+
94
+ **해를 바꿀 때 달이 범위 밖이면 당긴다.** 1월을 보다가 그 2026년을 고르면 1월은 그 해에
95
+ 없다 — 안 당기면 **빈 달력이 선다.** 터지지도 울지도 않고 날짜 칸만 사라져 눈으로만 잡힌다.
96
+ 범위 안이면 그대로 둔다(쓸데없이 안 움직인다).
97
+
98
+ 사전에 `datePicker.selectYear`·`selectMonth` 를 두고 `selectYearMonth` 는 걷었다(쓰는 데가
99
+ 없어져 두면 죽은 열쇠가 된다). 연월을 한 덩어리로 짓던 `yearMonthLabel` 은 **달력 머리줄
100
+ (`Calendar`)** 이 여전히 쓴다.
101
+
102
+ #### ② 트리거를 **입력칸으로** — 눌러서 바로 친다
103
+
104
+ `<button>` 이라 달력으로만 고를 수 있었다. 먼 날짜(생년월일 같은 것)는 달력으로 가는 길이
105
+ 멀다. 이제 누르면 달력이 열리면서 **글 쓸 자리도 같이 선다** — 둘 중 편한 쪽으로 간다.
106
+
107
+ 숫자만 남겨서 읽는다 — `2026. 10. 20.` · `2026-10-20` · `2026/10/20` · `20261020` 이 다 같은
108
+ 날이다. 사람이 어떤 구분자를 칠지 미리 못 정한다.
109
+
110
+ **지키는 것 둘.** ① *반쪽 글을 부르는 쪽에 안 돌린다* — `2026` 까지 쳤을 때는 아직 날짜가
111
+ 아닌데 돌리면 부르는 쪽이 `2026` 을 날짜로 받는다. 다 치면 그때 돌리고, 자리를 뜨면 반쪽
112
+ 글은 버린다. ② *없는 날을 안 받는다* — `Date` 는 2026-02-30 을 **3월 2일로 넘겨 버린다.**
113
+ 지은 날이 친 숫자와 같은지 되본다(윤년 2월 29일은 받는다). `min`·`max` 밖도 안 받는다 —
114
+ 달력으로 못 고르는 날을 글로는 넣을 수 있으면 안 된다.
115
+
116
+ 클래스(`date-picker__trigger`)는 그대로 뒀다 — 시험과 소비자가 그 글자로 집는다. 입력칸은
117
+ 자식을 못 가지므로 달력 아이콘은 밖으로 내고 둘을 한 상자가 겹친다.
118
+
119
+ #### 못이 셋을 잡았다
120
+
121
+ **① 누를 자리가 없었다.** 새로 만든 아이콘 단추가 그림 18px 그대로였다 —
122
+ `rawButtonHitArea` 가 바닥 24px 을 세어 울렸다. 32×32 로 넓히되 `position: absolute` 라 칸
123
+ 키는 그대로이고, `right` 를 12 → 8 로 당겨 그림은 제자리에 남는다.
124
+
125
+ **② 옛 시험 **다섯**이 옛 꼴을 읽고 있었다.** 안내글이 `<span class="date-picker__placeholder">`
126
+ 에서 **입력칸의 `placeholder`** 로 옮겼고(`fixInputsDatePickerI18n`), 연월이 한 고르개에서
127
+ 둘로 갈렸다 — **첫 고르개 하나만 읽는 자리가 넷**이었다(`datePickerGridKeys` ·
128
+ `kstTodayInputs0915` · `calendarTodayProp0915d` · `dateBoundaryFixes0915`). 해만 보고 달을
129
+ 못 보므로 `'2026년'` 에 `'10월'` 이 들었는지 물으면 운다. 다섯 다 **재는 것은 그대로 두고
130
+ 읽는 자리만** 고쳤다.
131
+
132
+ 관문이 이것을 **세 바퀴에 걸쳐 하나씩** 내주었다 — 한 바퀴에 한 시험만 울어서다. 두 바퀴째에
133
+ "같은 꼴이 더 있나" 를 **글자로 훑어** 남은 둘을 한꺼번에 찾았다(`querySelector('.date-picker__nav`
134
+ 를 `querySelectorAll` 없이 쓰는 자리). 울린 것만 고치면 바퀴 수만큼 걸린다.
135
+
136
+ 심어서 여섯을 확인했다 — 당김 걷기 · 달 목록을 해와 무관하게 · 없는 날 되보기 걷기 ·
137
+ 반쪽 글 돌리기 · `min`/`max` 안 보기 · 읽기 전용으로 두기.
138
+
139
+ **④ 가려 집으라고 붙인 클래스 둘을 걷었다.** 시험이 연·월을 가리려고
140
+ `--year`·`--month` 를 붙였는데 **꾸밈이 하나도 없었다** — `cssClassIntegrity` 가 울렸다.
141
+ 그 못의 까닭은 *"CSS 없는 클래스는 브라우저가 조용히 무시하고 민둥으로 그린다"* 이고,
142
+ 맞는 말이다. 필요한 것은 꾸밈이 아니라 **표식**이었다.
143
+
144
+ `Dropdown` 이 `data-*` 를 안 넘기므로 **차례로 집되**(연이 앞, 월이 뒤) 그 차례를 시험이
145
+ **이름으로 못 박는다**. 안 박으면 둘이 바뀌어도 그 파일이 통째로 반대쪽을 재면서 조용히
146
+ 초록일 수 있다 — 바꿔 심어 보니 첫 시험이 *"첫째가 연이 아니다"* 로 먼저 짚었다.
147
+
148
+ **⑤ 반대쪽 못이 죽은 CSS 둘을 짚었다.** 트리거가 입력칸이 되면서 안쪽 `<span>` 둘이
149
+ 사라졌고, 그 둘이 쥐던 `.date-picker__placeholder`·`.date-picker__value` 가 죽었다
150
+ (`coreCssDeadRules`). 걷기만 하면 **안내글이 값과 같은 진한 색**으로 떠서 빈 칸이 채워진
151
+ 것처럼 보인다 — 색을 입력칸 제 자리(`::placeholder`)로 옮겼다.
152
+
153
+ 브라우저에서 쟀다: 안내글 `rgb(150,164,184)` · 값 `rgb(67,77,86)`. **jsdom 으로는 못 보는
154
+ 자리다**(계산된 색을 안 준다).
155
+
156
+ **③ 내가 세운 못에 내가 걸렸다.** 연·월 이름을 지으려고
157
+ `i18n.language?.startsWith('ko') ? 'ko-KR' : …` 를 적었더니 `langBranchInDict1013` 이 울렸다 —
158
+ 말을 보고 가르는 자리를 세는 못이다. 울린 것이 **옳다**: 똑같은 줄이 이미 `date.ts` 에 있었고,
159
+ 그 파일만 *"글귀가 아니라 로케일을 고른다"* 는 까닭으로 봐주고 있었다.
160
+
161
+ 봐주는 표에 내 자리를 더하는 쪽이 아니라 **그 결정을 한 자리에 모으는** 쪽으로 갔다 —
162
+ `intlLocale(lang)` 을 `date.ts` 에 두고 `formatYearMonth` 도 그것을 쓴다. 같은 줄이 둘이면
163
+ 하나가 안 따라온다. 못이 "적어서 넘기라" 가 아니라 **"왜 둘이냐"** 를 물은 셈이다.
164
+
165
+ 거기에 `@public` 을 붙였다가 걷었다 — `src/utils.ts` 는 이름을 골라 내보내는데 이 이름은
166
+ 안 나간다. **오늘 `deviceKind.ts` 에서 한 것과 같은 실수다**(안 나가는 것을 공개라 적었다).
167
+
168
+ ### 놀이터에 **eMail** 칸 — 말귀가 보내는 메일 다섯 통
169
+
170
+ 사장님 지시(2026-10-20). `speechear-v2/docs/mail-preview/mail-forms.html` 를 디자인 시스템
171
+ 에서 보게 한다. 그 파일 머리말이 이미 *"앱에서는 디자인 시스템 → eMail 칸에서 같은 것을
172
+ 본다"* 고 적고 있었다 — 그 칸을 이제 만든다.
173
+
174
+ | | |
175
+ |---|---|
176
+ | 메뉴 | **eMail → 메일 폼** (여덟째 묶음) |
177
+ | 페이지 | `playground/src/pages/Email/MailFormsPage.tsx` |
178
+ | 글 | `playground/src/pages/Email/mailForms.ts` — 다섯 통 |
179
+
180
+ 이메일 인증번호 · 환불 완료(본인) · 환불 알림(관리자) · 환불 알림(토스 대시보드) ·
181
+ 이용권 만료. 칸마다 **언제 나가나 · 메일 제목 · 고치는 곳**(앱 쪽 함수)을 함께 적는다.
182
+
183
+ **컴포넌트 묶음에 안 섞는다.** 메일 HTML 은 코어 컴포넌트를 하나도 안 쓴다 — 메일
184
+ 클라이언트가 외부 CSS·`<style>`·flex·grid 를 안 읽어서 **표와 인라인**으로만 짠다. 컴포넌트
185
+ 사이에 두면 "이것도 코어가 그린다" 로 읽힌다.
186
+
187
+ **글은 한 글자도 안 고치고 옮겼다.** 고치면 미리보기와 실제 나가는 메일이 갈라지고, 갈라진
188
+ 것은 눈으로만 잡힌다. 그래서 여기는 **보는 자리이고 고치는 자리가 아니다** — 고치려면 칸에
189
+ 적힌 앱 쪽 함수를 고치고 다시 옮겨 온다.
190
+
191
+ `<iframe srcDoc>` 으로 **갈라 둔다**: 놀이터 CSS 가 닿으면 실제와 달라 보이고, 메일 쪽
192
+ `:hover` 규칙이 놀이터로 새지 않는다.
193
+
194
+ #### 브라우저에서 재다 셋을 고쳤다 — jsdom 으로는 못 보는 것들이다
195
+
196
+ **① 환불 셋이 잘려 있었다.** 원본대로 칸 키를 760px 로 박았더니 90~176px 씩 잘렸다(실측).
197
+ 고정 키는 잘리거나 빈다 — 글에 맞춰 재서 정한다. `onLoad` 뿐 아니라 `ResizeObserver` 로도
198
+ 보는 까닭은 로고 SVG 가 뒤늦게 들어오면 그만큼 키가 자라기 때문이다.
199
+
200
+ **② 그랬더니 칸마다 2px 씩 잘렸다.** 놀이터가 `border-box` 라 내가 준 글 키에서 테두리를
201
+ 도로 먹었다. `content-box` 로 바꾸면 넓힐 때 가로로 그만큼 넘친다 — 그래서 테두리를
202
+ **재어서**(`offsetHeight - clientHeight`) 더한다. 2 를 박으면 굵기가 바뀔 때 또 어긋난다.
203
+
204
+ **③ 넓히기 조작이 다섯인데 상태는 하나였다.** 하나를 눌렀는데 다섯이 다 움직여 고장처럼
205
+ 보였다. 조작도 하나로 모았다.
206
+
207
+ **`--color-line` 으로 테두리를 적었다가 고쳤다** — 그런 토큰이 없다(`--color-border` 다).
208
+ 안 고쳤으면 **오류 없이 테두리만 조용히 사라졌을** 자리다.
209
+
210
+ ### 인라인 `var()` 가 시험 눈 밖으로 나가던 것 — 못을 세웠다
211
+
212
+ `PanelRowText` 를 재다 jsdom 이 `var()` 를 버리는 것을 알았고, 말귀가 그것을 받아 **"헛빨강이
213
+ 아니라 헛초록이 날 자리라 더 위험하다"** 고 짚었다. 맞는 말이라 재 보고 못을 세웠다.
214
+
215
+ 갈리는 자리는 **속성의 종류**다(실측 2026-10-20):
216
+
217
+ ```
218
+ el.style.setProperty('--my-token', 'var(--color-main)') → "var(--color-main)" 산다
219
+ el.style.color = 'var(--color-main)' → "" 버린다
220
+ el.style.setProperty('color', 'var(--color-main)') → "" 버린다
221
+ el.style.color = 'red' → "red" 산다
222
+ ```
223
+
224
+ 버린 값은 `style` 어트리뷰트에도 안 남아, jsdom 안에서는 *"안 적었다"* 와 *"`var()` 를
225
+ 적었다"* 가 **구별되지 않는다.** 그래서 **화면이 아니라 소스를 센다** —
226
+ `test/inlineVarIsInvisible1020.test.ts`.
227
+
228
+ **왜 헛초록인가.** 빈 글자를 기대하는 잼이 둘 있다 — `fixDisplayIconPartyPopper:44` ·
229
+ `smallContracts0915d:59`, 둘 다 *"크기를 직접 박지 않는다"* 는 뜻이다. `Icon.tsx` 의
230
+ `maskStyle` 에 `width: 'var(--icon-size)'` 를 **심어 보니 그 열한 시험이 그대로 초록**이었고
231
+ 새 못만 울렸다. 브라우저에서는 폭이 실제로 박힌다 — 그 두 시험이 막으려던 바로 그것이다.
232
+
233
+ **금지가 아니다.** 인라인 `var()` 는 브라우저에서 멀쩡히 돈다 — 틀린 꼴이 아니라 **jsdom 으로
234
+ 못 재는** 꼴이다. 쓰려면 목록에 **까닭과 무엇으로 대신 쟀는지**를 적는다. 적는 자리가 곧
235
+ "안 재어진 곳" 의 목록이 된다. 지금 하나뿐이다(`WordSearchResultBlock` 의 결과 수 강조).
236
+
237
+ **짓다가 두 번 좁았다.** ① `style={{ … }}` 만 보게 했더니 **정작 그 자리를 못 잡았다** —
238
+ 실제 코드는 `style={maskStyle}` 로 **변수에 담아** 넘긴다. 파일 전체를 훑게 넓혔다.
239
+ ② 그러자 `sub` · `copyrightColor` · `title` 같은 **앱 색판 열쇠**가 28건 걸렸다. CSS 속성
240
+ 이름을 손으로 적지 않고 **jsdom 의 `CSSStyleDeclaration` 에서 759개를 뽑아** 좁혔다
241
+ (손 목록은 낡는다).
242
+
243
+ 심어서 셋을 확인했다 — 변수 객체에 더하기 · `setProperty` 로 주기 · 봐주는 목록이 낡는 것.
244
+
245
+ **그리고 못을 세우다 다른 못에 걸렸다** — 새 시험에 한글 이름 다섯(`판정한파일`·`몰래`·
246
+ `실재`·`낡음`)을 썼고 `identifiersAreEnglish1013` 이 울렸다. 집안 규약은 시험에도 걸린다
247
+ (같은 자리에서 두 번째다). 영어로 고쳤다 — 주석 속 한국어는 규약대로 그대로 둔다.
248
+
249
+ ### `PanelRowText.value` 가 마디(`ReactNode`)도 받는다
250
+
251
+ 말귀 청(2026-10-20). 고객 상세의 연락처 줄을 `<a href="tel:">` 로 만들어 폰에서 번호를 눌러
252
+ 바로 걸게 하려는데 `value: string` 이라 못 넣었다.
253
+
254
+ **그 줄만 앱이 손으로 그리면 한 패널 안에서 간격·글자 크기가 갈린다** — 오른쪽 자리는
255
+ `panel-row__text body-2` 가 쥐고 있고, 앱이 밖에서 그리면 그 꼴을 잃는다. 그래서 받는 쪽을
256
+ 넓힌다. 같은 파일의 `helperText`·`buttonLabel` 이 이미 `ReactNode` 라 꼴도 낯설지 않다.
257
+
258
+ ```ts
259
+ - value: string
260
+ + value: ReactNode
261
+ ```
262
+
263
+ **넓히기만 하면 되는 자리였다.** `text` 갈래의 `value` 는 **그리는 한 곳**(`PanelRow.tsx:320`)
264
+ 에서 그대로 내보낼 뿐이고, 글자로 다루는 자리(길이·자르기·견주기)가 없다. 코어 안에서 그
265
+ 갈래를 만드는 자리도 없다. 옛 쓰임(글자)은 그대로 산다.
266
+
267
+ `valueColor` 는 그 자리 `<span>` 에 걸리므로 마디가 제 색을 가지면 그것이 이긴다.
268
+
269
+ **jsdom 이 `var(...)` 를 `color` 로 안 받는다** — 속성 자체가 안 붙어 `style.color` 가 빈
270
+ 글자다(실측: `var(--color-main)` → `""`, `red` → `"red"`). 처음엔 그걸 모르고 토큰으로 재다
271
+ 빨강을 받았고, **코드가 아니라 재는 자리의 한계**였다. 받는 값으로 바꿔 재고 까닭을 적어 뒀다.
272
+
273
+ 좁혀서 심으면 타입이 네 자리에서 운다.
274
+
10
275
  ## 0.349.0 — 사전에서 죽은 열쇠 277개를 걷었다
11
276
 
12
277
  ### 사전에서 죽은 열쇠 277개를 걷었다 — 첫 화면 조각에서 약 16KB