@ihabdevteam/core 0.324.0 → 0.326.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 +64 -0
- package/dist/components.cjs.js +184 -184
- 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 +1 -1
- package/dist/packages/core/src/components/Calendar.js +2 -1
- package/dist/packages/core/src/components/DatePicker.js +3 -2
- package/dist/packages/core/src/components/Word/wordTablePrint.js +33 -1
- package/dist/packages/core/src/components/useBarChartControl.js +3 -2
- package/dist/packages/core/src/i18n/locales/en.json +0 -1
- package/dist/packages/core/src/i18n/locales/ko.json +0 -1
- package/dist/packages/core/src/utils/date.d.ts +27 -0
- package/dist/packages/core/src/utils/date.js +37 -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 +3 -3
- package/package.json +1 -1
|
@@ -1678,7 +1678,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1678
1678
|
*
|
|
1679
1679
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1680
1680
|
*/
|
|
1681
|
-
export declare const CORE_VERSION = "0.
|
|
1681
|
+
export declare const CORE_VERSION = "0.326.0";
|
|
1682
1682
|
|
|
1683
1683
|
/** @public */
|
|
1684
1684
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useMemo, useState, useRef } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { yearMonthLabel } from '../utils/date';
|
|
5
6
|
import Icon from './Icon';
|
|
6
7
|
import { formatTimeHM } from '../utils/format';
|
|
7
8
|
import { todayYmdOr, formatDateLocale } from '../utils/date';
|
|
@@ -55,7 +56,7 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
|
|
|
55
56
|
setMonth(m => m + 1); }, [month]);
|
|
56
57
|
const fmtDate = (d) => `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
57
58
|
const isToday = (d) => fmtDate(d) === todayYmd;
|
|
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
|
|
59
|
+
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: yearMonthLabel(t, i18n.language, year, 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) => {
|
|
59
60
|
if (d === null)
|
|
60
61
|
return _jsx("div", { className: "calendar-cell empty" }, `empty-${i}`);
|
|
61
62
|
const day = d;
|
|
@@ -8,6 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
import { cx } from '../utils/cx';
|
|
9
9
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import { yearMonthLabel } from '../utils/date';
|
|
11
12
|
import { createPortal } from 'react-dom';
|
|
12
13
|
import { todayYmdOr } from '../utils/date';
|
|
13
14
|
import Calendar from 'lucide-react/dist/esm/icons/calendar.mjs';
|
|
@@ -48,7 +49,7 @@ function buildYMOptions(viewY, ymLabel, min, max) {
|
|
|
48
49
|
return opts;
|
|
49
50
|
}
|
|
50
51
|
export default function DatePicker({ value, onChange, size = 48, min, max, disabled, placeholder: placeholderProp, className, today, }) {
|
|
51
|
-
const { t } = useTranslation();
|
|
52
|
+
const { t, i18n } = useTranslation();
|
|
52
53
|
/* 기본 안내 문구도 사전에서 — 예전에는 '날짜 선택' 이 박혀 있어 영어 화면에서도 한국어였다.
|
|
53
54
|
달력 판의 이름(`datePicker.chooseDate`)과 같은 말이라 같은 열쇠를 쓴다. */
|
|
54
55
|
const placeholder = placeholderProp ?? t('datePicker.chooseDate', '날짜 선택');
|
|
@@ -254,7 +255,7 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
254
255
|
for (let d = 1; d <= daysInMonth(viewY, viewM); d++)
|
|
255
256
|
cells.push(d);
|
|
256
257
|
const iconSize = size === 64 ? 20 : size === 40 ? 16 : 18;
|
|
257
|
-
const ymOptions = buildYMOptions(viewY, (year, month) => t
|
|
258
|
+
const ymOptions = buildYMOptions(viewY, (year, month) => yearMonthLabel(t, i18n.language, year, month), min, max);
|
|
258
259
|
const ymValue = `${viewY}-${String(viewM + 1).padStart(2, '0')}`;
|
|
259
260
|
const dropdown = open ? createPortal(_jsxs("div", { ref: menuRef, id: panelId, className: "date-picker__dropdown", style: dropdownStyle, "data-tooltip-ignore": true, role: "dialog", "aria-label": t('datePicker.chooseDate', '날짜 선택'), tabIndex: -1, children: [_jsxs("div", { className: "date-picker__nav", children: [_jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: prevMonth, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(ChevronLeft, { size: 16, strokeWidth: 2 }) }), _jsx(Dropdown, { className: "date-picker__nav-dropdown", ariaLabel: t('datePicker.selectYearMonth', '연월 선택'), options: ymOptions, value: ymValue, onChange: (v) => {
|
|
260
261
|
const [y, m] = v.split('-');
|
|
@@ -6,9 +6,21 @@
|
|
|
6
6
|
// autoPrint: 문서 로드 후 자동으로 인쇄 대화상자를 띄운다.
|
|
7
7
|
// (win.onload를 밖에서 나중에 붙이면 document.write+close가 이미 load를 발생시켜
|
|
8
8
|
// 못 잡는 경우가 있어, 문서 자신의 <script> 안에서 직접 호출해 타이밍을 보장한다)
|
|
9
|
+
/** **글 속**에 넣을 때 — `<title>` 안, 마디 사이 */
|
|
9
10
|
function escapeHtml(s) {
|
|
10
11
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* **속성 값**에 넣을 때 — 따옴표까지 막는다.
|
|
15
|
+
*
|
|
16
|
+
* 글 속에서는 `"` 가 아무것도 안 깨지만 **속성 안에서는 값을 끝내 버린다.** `lang` 을 박으면서
|
|
17
|
+
* `escapeHtml` 을 그대로 썼다가 제 못에 걸렸다(2026-10-14): `ko" onload="x` 를 넣으니
|
|
18
|
+
* `<html lang="ko" onload="x" …>` 로 **새 속성이 생겼다.** 둘은 다른 자리이고, 같은 함수로
|
|
19
|
+
* 덮으면 다음 사람도 같은 자리를 밟는다 — 그래서 이름을 갈라 둔다.
|
|
20
|
+
*/
|
|
21
|
+
function escapeAttr(s) {
|
|
22
|
+
return escapeHtml(s).replace(/"/g, '"').replace(/'/g, ''');
|
|
23
|
+
}
|
|
12
24
|
/**
|
|
13
25
|
* 파일명(문서 title·다운로드 파일명)에 쓰이는 사용자 입력값(단어표 이름 등)에서 Windows/macOS
|
|
14
26
|
* 파일시스템에서 문제되는 문자(\ / : * ? " < > |)를 밑줄로 치환한다. 없으면 "PDF로 저장"·
|
|
@@ -85,6 +97,26 @@ export function captureHtml(els, paperW, paperH, autoPrint = false, fileName = '
|
|
|
85
97
|
흰 종이에 흰 글자로 인쇄·새 창·내려받기가 나갔다. 속성이 없으면 "auto" 로 취급돼 OS 다크 모드에서 팝업만 어두워지던 것도
|
|
86
98
|
이것으로 막힌다. 화면 테마 값은 문서 글자에 끼우지 않는다(주입 걱정도 없다). */
|
|
87
99
|
const theme = 'light';
|
|
100
|
+
/**
|
|
101
|
+
* **인쇄 문서에도 말을 적는다** — 말귀_1 실측(2026-10-14): 이 문서의 `lang` 이 **빈 채**로
|
|
102
|
+
* 나갔다(앱 제 인쇄 길은 `ko` 를 박는다). 낭독기 발음·글꼴 고르기·줄나눔이 그 값을 본다.
|
|
103
|
+
*
|
|
104
|
+
* **산 문서에서 읽는다.** 코어 `i18n` 이 처음 뜰 때(`syncHtmlLang(getSavedLocale())`)와
|
|
105
|
+
* 말이 바뀔 때(`languageChanged`) 둘 다 `document.documentElement.lang` 을 세워 두므로,
|
|
106
|
+
* 거기서 가져오면 늘 지금 말이다. 이 조각은 **들임이 하나도 없는** 것이 값이라
|
|
107
|
+
* (어디서든 부를 수 있다) `i18n` 을 끌어들이지 않는다.
|
|
108
|
+
*
|
|
109
|
+
* 비어 있으면 **안 적는다** — 모르는 말을 지어 적느니 없는 편이 낫다. 다만 코어가 세우는
|
|
110
|
+
* 자리가 있으므로 비는 것은 그 자리가 깨졌다는 뜻이고, `wordTablePrintLang` 못이 운다.
|
|
111
|
+
*/
|
|
112
|
+
const docLang = (() => {
|
|
113
|
+
try {
|
|
114
|
+
return document.documentElement.lang || '';
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
88
120
|
/* 종이 맨 위에 찍는 한 줄(누구의·언제·무슨 표). **격자만 인쇄되므로** 이것이 없으면
|
|
89
121
|
뽑아 둔 채점지가 며칠 뒤 누구 것인지 알 수 없는 종이가 된다. 쪽마다 찍는다 —
|
|
90
122
|
여러 장이 섞이면 첫 장에만 있는 이름은 소용이 없다. */
|
|
@@ -123,7 +155,7 @@ export function captureHtml(els, paperW, paperH, autoPrint = false, fileName = '
|
|
|
123
155
|
얹는 것과 같은 결의 처리다. */
|
|
124
156
|
/* eslint-disable no-useless-escape -- 아래 문자열의 `<\/script>`는 실제 <script> 태그로 파싱되어 삽입되므로, 이스케이프가 없으면 바깥 <script> 태그가 조기 종료된다 */
|
|
125
157
|
return `<!DOCTYPE html>
|
|
126
|
-
<html data-theme="${theme}">
|
|
158
|
+
<html${docLang ? ` lang="${escapeAttr(docLang)}"` : ''} data-theme="${theme}">
|
|
127
159
|
<head>
|
|
128
160
|
<meta charset="utf-8">
|
|
129
161
|
${fileName ? `<title>${escapeHtml(fileName)}</title>` : ''}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { yearMonthLabel } from '../utils/date';
|
|
4
5
|
import { firstMondayOfMonth, todayWeekValue, monthOptionsNow, weekLabel } from './barChartData';
|
|
5
6
|
/**
|
|
6
7
|
* BarChart 기간 상태/옵션/네비게이션을 캡슐화한 훅.
|
|
@@ -8,7 +9,7 @@ import { firstMondayOfMonth, todayWeekValue, monthOptionsNow, weekLabel } from '
|
|
|
8
9
|
* @public
|
|
9
10
|
*/
|
|
10
11
|
export function useBarChartControl(data) {
|
|
11
|
-
const { t } = useTranslation();
|
|
12
|
+
const { t, i18n } = useTranslation();
|
|
12
13
|
const [period, setPeriod] = useState('week');
|
|
13
14
|
// 달 옵션·오늘은 자료가 바뀔 때 다시 센다 — 모듈 로드 때 한 번 세면 자정을 넘긴 탭에서 이번 달·오늘 주가 옛날에 굳었다
|
|
14
15
|
const DATA_MONTH_OPTIONS = useMemo(() => monthOptionsNow().filter(o => o.value in data), [data]);
|
|
@@ -25,7 +26,7 @@ export function useBarChartControl(data) {
|
|
|
25
26
|
return opts;
|
|
26
27
|
}, [DATA_MONTH_OPTIONS]);
|
|
27
28
|
const weekOptionLabel = useCallback((o) => {
|
|
28
|
-
const m = t
|
|
29
|
+
const m = yearMonthLabel(t, i18n.language, o.year, o.month);
|
|
29
30
|
return `${m} ${weekLabel(t, o.week)}`;
|
|
30
31
|
}, [t]);
|
|
31
32
|
const monthOptions = useMemo(() => DATA_MONTH_OPTIONS.map(o => ({
|
|
@@ -12,6 +12,33 @@ export declare function formatDateLongKo(iso: string): string;
|
|
|
12
12
|
* @param lang useTranslation()의 i18n.language
|
|
13
13
|
*/
|
|
14
14
|
export declare function formatDateLocale(iso: string, lang: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* **연·월 이름표** — `2026년 10월` · `October 2026`. @public
|
|
17
|
+
*
|
|
18
|
+
* 사장님 지시(2026-10-14). 전에는 사전 글귀(`calendar.yearMonth`)가 영어에서
|
|
19
|
+
* `{{month}}/{{year}}` 이라 **`10/2026`** 으로 떴다(말귀_1 이 훈련 현황 도구줄에서 봤다).
|
|
20
|
+
* 달 이름은 틀로는 못 만든다 — 열두 이름을 두 말로 적거나, 브라우저가 든 것을 쓰거나다.
|
|
21
|
+
* **브라우저 쪽을 쓴다**: 한 줄이고 두 말이 같이 맞고, 말이 늘어도 따라온다.
|
|
22
|
+
*
|
|
23
|
+
* `month` 는 **1 부터**다(화면이 쓰는 꼴). `Date` 의 0 부터와 헷갈리기 쉬워 여기서 받아
|
|
24
|
+
* 한 번만 내린다 — 부르는 쪽 셋이 저마다 `+1`·`-1` 을 하면 그중 하나가 어긋난다.
|
|
25
|
+
*
|
|
26
|
+
* **사전에 그 열쇠가 있으면 그쪽이 이긴다.** 그 글귀는 부르는 쪽이 덮으라고 열어 둔
|
|
27
|
+
* 자리다(`DatePicker` 머리글). 코어 사전에서는 뺐으므로, 안 덮으면 여기로 온다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatYearMonth(year: number, month: number, lang: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* **연·월 이름표 — 사전이 있으면 사전, 없으면 브라우저.** @public
|
|
32
|
+
*
|
|
33
|
+
* `calendar.yearMonth` 는 **부르는 쪽이 덮으라고 열어 둔 자리**다(`DatePicker` 머리글).
|
|
34
|
+
* 코어 사전에서는 뺐으므로 안 덮으면 `formatYearMonth` 로 온다.
|
|
35
|
+
*
|
|
36
|
+
* **이 가름을 한 자리에 둔다.** 쓰는 곳이 셋(`Calendar` 머리줄 · `DatePicker` 연월 고르개 ·
|
|
37
|
+
* 막대그래프 이름표)인데 저마다 적으면 그중 하나가 안 따라온다 — 이 저장소에서 여러 번 겪은
|
|
38
|
+
* 꼴이다. `defaultValue: ''` 로 묻는 까닭은 `exists()` 없이도 "사전에 있나" 를 가를 수
|
|
39
|
+
* 있어서다(없으면 빈 글자가 온다).
|
|
40
|
+
*/
|
|
41
|
+
export declare function yearMonthLabel(t: (key: string, opts?: Record<string, unknown>) => string, lang: string, year: number, month: number): string;
|
|
15
42
|
/** formatDateLongKo의 로케일 인식 버전 — en: "January 15, 2024, 03:30 PM" */
|
|
16
43
|
export declare function formatDateLongLocale(iso: string, lang: string): string;
|
|
17
44
|
/** "2024. 01. 15. 오후 03:30" (2-digit month + time) */
|
|
@@ -75,6 +75,43 @@ export function formatDateLocale(iso, lang) {
|
|
|
75
75
|
return '';
|
|
76
76
|
return d.toLocaleDateString(locale, { timeZone: KST, year: 'numeric', month: 'short', day: 'numeric' });
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* **연·월 이름표** — `2026년 10월` · `October 2026`. @public
|
|
80
|
+
*
|
|
81
|
+
* 사장님 지시(2026-10-14). 전에는 사전 글귀(`calendar.yearMonth`)가 영어에서
|
|
82
|
+
* `{{month}}/{{year}}` 이라 **`10/2026`** 으로 떴다(말귀_1 이 훈련 현황 도구줄에서 봤다).
|
|
83
|
+
* 달 이름은 틀로는 못 만든다 — 열두 이름을 두 말로 적거나, 브라우저가 든 것을 쓰거나다.
|
|
84
|
+
* **브라우저 쪽을 쓴다**: 한 줄이고 두 말이 같이 맞고, 말이 늘어도 따라온다.
|
|
85
|
+
*
|
|
86
|
+
* `month` 는 **1 부터**다(화면이 쓰는 꼴). `Date` 의 0 부터와 헷갈리기 쉬워 여기서 받아
|
|
87
|
+
* 한 번만 내린다 — 부르는 쪽 셋이 저마다 `+1`·`-1` 을 하면 그중 하나가 어긋난다.
|
|
88
|
+
*
|
|
89
|
+
* **사전에 그 열쇠가 있으면 그쪽이 이긴다.** 그 글귀는 부르는 쪽이 덮으라고 열어 둔
|
|
90
|
+
* 자리다(`DatePicker` 머리글). 코어 사전에서는 뺐으므로, 안 덮으면 여기로 온다.
|
|
91
|
+
*/
|
|
92
|
+
export function formatYearMonth(year, month, lang) {
|
|
93
|
+
/* 달이 범위 밖이면 `Date` 가 해를 넘겨 버린다(13월 → 다음 해 1월). 조용히 딴 해를
|
|
94
|
+
보여 주느니 받은 그대로 적는다. */
|
|
95
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12)
|
|
96
|
+
return `${year}-${month}`;
|
|
97
|
+
const locale = lang && lang.startsWith('ko') ? 'ko-KR' : lang || 'en-US';
|
|
98
|
+
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long' }).format(new Date(year, month - 1, 1));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* **연·월 이름표 — 사전이 있으면 사전, 없으면 브라우저.** @public
|
|
102
|
+
*
|
|
103
|
+
* `calendar.yearMonth` 는 **부르는 쪽이 덮으라고 열어 둔 자리**다(`DatePicker` 머리글).
|
|
104
|
+
* 코어 사전에서는 뺐으므로 안 덮으면 `formatYearMonth` 로 온다.
|
|
105
|
+
*
|
|
106
|
+
* **이 가름을 한 자리에 둔다.** 쓰는 곳이 셋(`Calendar` 머리줄 · `DatePicker` 연월 고르개 ·
|
|
107
|
+
* 막대그래프 이름표)인데 저마다 적으면 그중 하나가 안 따라온다 — 이 저장소에서 여러 번 겪은
|
|
108
|
+
* 꼴이다. `defaultValue: ''` 로 묻는 까닭은 `exists()` 없이도 "사전에 있나" 를 가를 수
|
|
109
|
+
* 있어서다(없으면 빈 글자가 온다).
|
|
110
|
+
*/
|
|
111
|
+
export function yearMonthLabel(t, lang, year, month) {
|
|
112
|
+
const fromDict = t('calendar.yearMonth', { year, month, defaultValue: '' });
|
|
113
|
+
return fromDict || formatYearMonth(year, month, lang);
|
|
114
|
+
}
|
|
78
115
|
/** formatDateLongKo의 로케일 인식 버전 — en: "January 15, 2024, 03:30 PM" */
|
|
79
116
|
export function formatDateLongLocale(iso, lang) {
|
|
80
117
|
const locale = lang.startsWith('ko') ? 'ko-KR' : 'en-US';
|