@ihabdevteam/core 0.356.0 → 0.358.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.
@@ -1754,7 +1754,7 @@ export declare function copyText(text: string): Promise<boolean>;
1754
1754
  *
1755
1755
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1756
1756
  */
1757
- export declare const CORE_VERSION = "0.356.0";
1757
+ export declare const CORE_VERSION = "0.358.0";
1758
1758
 
1759
1759
  /** @public */
1760
1760
  declare const coreDefault: Record<string, unknown>;
@@ -3890,16 +3890,25 @@ export declare const meaningKey: (word: string, homonum?: number) => string;
3890
3890
  /**
3891
3891
  * 글 하나를 재서 **쓰는 쪽이 그릴 만큼** 돌려준다.
3892
3892
  *
3893
- * `over` 는 *"한계를 넘었나"* 다 — 단문이면 90, 장문이면 2000 을 기준으로 본다.
3894
- * 장문까지 넘으면 보내는 쪽에서 **잘린다.**
3893
+ * 사실 **둘**을 따로 돌려준다 — 섞으면 그리는 쪽이 둘 중 하나를 못 보인다:
3894
+ *
3895
+ * · `over` — **한계**(단문 90 · 장문 2000)를 넘었나. 넘으면 *값이 바뀌거나 글이 잘린다.*
3896
+ * · `overTarget` — **목표**(`targetBytes`)를 넘었나. 넘어도 보내지는 데는 지장이 없고
3897
+ * **여유가 없다**는 뜻이다. 86~90 이 그 자리다.
3898
+ *
3899
+ * `targetBytes` 에 `null` 을 주면 목표를 안 본다(`target` 이 `null`, `overTarget` 이 거짓).
3895
3900
  *
3896
3901
  * @public
3897
3902
  */
3898
- export declare function measureSmsText(text: string): {
3903
+ export declare function measureSmsText(text: string, targetBytes?: number | null): {
3899
3904
  bytes: number;
3900
3905
  type: SmsType;
3901
3906
  limit: number;
3902
3907
  over: boolean;
3908
+ /** 겨눈 수. `null` 이면 목표를 안 본 것이다 */
3909
+ target: number | null;
3910
+ /** 목표를 넘었나 — `over` 와 **다른 사실**이다 */
3911
+ overTarget: boolean;
3903
3912
  };
3904
3913
 
3905
3914
  /**
@@ -4279,7 +4288,7 @@ export declare interface NotifyTextItem {
4279
4288
  }
4280
4289
 
4281
4290
  /** @public */
4282
- export declare function NotifyTextPreview({ items, className, ariaLabel, titleLevel }: NotifyTextPreviewProps): JSX.Element;
4291
+ export declare function NotifyTextPreview({ items, className, ariaLabel, titleLevel, targetBytes }: NotifyTextPreviewProps): JSX.Element;
4283
4292
 
4284
4293
  /** @public */
4285
4294
  export declare interface NotifyTextPreviewProps {
@@ -4296,6 +4305,14 @@ export declare interface NotifyTextPreviewProps {
4296
4305
  * `div` 로 그린다.
4297
4306
  */
4298
4307
  titleLevel?: 0 | 2 | 3 | 4 | 5 | 6;
4308
+ /**
4309
+ * 겨누는 바이트. 기본은 코어의 `SMS_TARGET_BYTES`(85)다.
4310
+ *
4311
+ * **한계와 목표는 다른 사실이다** — 90을 넘으면 *값이 바뀌고*, 85를 넘으면 *여유가 없다.*
4312
+ * 그래서 86~90 은 '한계 넘음'(빨강)이 아니라 **'목표 넘음'(주황)** 으로 선다. 목표를
4313
+ * 안 보려면 `null` 을 준다.
4314
+ */
4315
+ targetBytes?: number | null;
4299
4316
  }
4300
4317
 
4301
4318
  /**
@@ -7123,6 +7140,41 @@ export declare interface SliderProps {
7123
7140
  /** 단문(SMS) 한 통에 드는 한계 — 넘으면 장문(LMS)이다 */
7124
7141
  export declare const SMS_BYTE_LIMIT = 90;
7125
7142
 
7143
+ /**
7144
+ * **우리가 겨누는 수는 85다** — 통신사 한계(`SMS_BYTE_LIMIT`, 90)가 아니다.
7145
+ *
7146
+ * 사장님 지시(2026-10-21) — *"sms 전수조사 해 85바이트 이하로 전부 조정해"*. 그날 오전에
7147
+ * 초대 문자가 **장문 49.5원**으로 나가고 있던 것을 솔라피에서 잡으셨고, 그러고 보니 90에
7148
+ * **딱 붙은 글이 하나**(인증 해제, 90바이트) 있었다.
7149
+ *
7150
+ * **5바이트를 비워 두는 까닭** — 글을 한 글자 고치다 넘기기가 너무 쉽고, 넘어간 것은
7151
+ * 화면 어디에도 안 보인다(**청구서에만** 보인다). 90을 겨누면 *맞았다* 와 *넘었다* 사이에
7152
+ * 아무것도 없다.
7153
+ *
7154
+ * **한계와 목표는 다른 사실이다** — 90을 넘으면 *값이 바뀌고*, 85를 넘으면 *여유가 없다.*
7155
+ * 그래서 `measureSmsText` 가 둘을 따로 돌려주고, 그리는 쪽도 따로 그린다.
7156
+ *
7157
+ * ## 이것은 **사실이 아니라 정해진 수**다 — `eucKrBytes` 와 결이 다르다
7158
+ *
7159
+ * 처음에 여기 *"코어가 쥐는 까닭은 `eucKrBytes` 와 같다"* 고 적었다가 **고쳤다**(0.358.0).
7160
+ * 말귀가 되짚어 준 것이 맞다:
7161
+ *
7162
+ * · `eucKrBytes` 는 **사실**이다 — EUC-KR 이 한글을 2바이트로 적는다.
7163
+ * · `SMS_BYTE_LIMIT`(90)도 **사실**이다 — 통신사가 거기서 장문으로 받는다.
7164
+ * · `SMS_TARGET_BYTES`(85)는 **사람이 정한 수**다 — 사장님이 말귀를 두고 *"85바이트
7165
+ * 이하로 전부 조정해"* 라 하셨다. 얼마를 비워 둘지는 서비스마다 다를 수 있다.
7166
+ *
7167
+ * 요금(18원·45원)을 여기 안 박은 것과 **같은 까닭**이다. 그 잣대를 제대로 대면 85도
7168
+ * 저쪽이다 — 이 수를 들인 쪽은 코어가 80이나 88로 바꾸는 날 **아무도 정하지 않은 채**
7169
+ * 제 글의 자가 바뀐다.
7170
+ *
7171
+ * 그래서 **쥐지 않고 내준다** — 기본값으로 두되, 다른 여유가 맞는 쪽은 `measureSmsText`
7172
+ * 와 `NotifyTextPreview` 에 제 수를 넘긴다(`null` 이면 목표를 아예 안 본다). 말귀는
7173
+ * 제 수를 그대로 들고 **코어 것과 같은지 맞대는 못**을 세웠다 — 들이지 않아도 어긋남이
7174
+ * 조용히 지나가지 않는다. 그 꼴이 옳다.
7175
+ */
7176
+ export declare const SMS_TARGET_BYTES = 85;
7177
+
7126
7178
  /** 보내는 갈래 */
7127
7179
  export declare type SmsType = 'SMS' | 'LMS';
7128
7180
 
@@ -44,12 +44,18 @@
44
44
  color: var(--color-lightGray);
45
45
  }
46
46
 
47
+ /* 이름표 칸 — **가장 긴 이름표를 재서 정한다.**
48
+ `사업자등록번호`(말귀 청 (214))가 들어오면서 80px 로는 **여유가 1px** 이 됐다
49
+ (한글 79px · `Business no.` 77px, 13px semibold 로 실측). 안 넘치기는 하지만 글꼴이
50
+ 조금만 달라지거나 이름표가 한 글자 늘면 줄이 접히고, 그러면 네 줄 값이 서로 어긋나
51
+ 보인다 — 바이트와 같은 결의 자리다(여유가 0 인 수는 넘어간 것이 화면에 안 보인다).
52
+ 96px 로 두어 두 말 다 17px 를 남긴다. 네 줄이 똑같이 밀리므로 줄맞춤은 그대로다. */
47
53
  .footer-title {
48
54
  font-size: 13px;
49
55
  line-height: 20px;
50
56
  font-weight: 600;
51
57
  color: var(--color-lightGray);
52
- width: 80px;
58
+ width: 96px;
53
59
  display: inline-block;
54
60
  }
55
61
 
@@ -15,5 +15,5 @@ export default function Footer({ onTermsClick, onPrivacyClick, contact } = {}) {
15
15
  } }, children: t('footer.policy.terms', '이용약관') }) }), _jsx("li", { children: _jsx("a", { className: "footer-link", href: "#", onClick: e => { if (onPrivacyClick) {
16
16
  e.preventDefault();
17
17
  onPrivacyClick();
18
- } }, children: t('footer.policy.privacy', '개인정보 처리방침') }) })] })] })] })] }), _jsx("hr", { className: "site-footer__rule" }), _jsxs("div", { className: "site-footer__bottom", children: [_jsx("div", { className: "site-footer__brand", children: _jsx("img", { src: logoSrc, alt: "IHAB", className: "site-footer__logo" }) }), _jsxs("div", { className: "site-footer__meta", children: [_jsxs("div", { children: [_jsx("span", { className: "footer-title", children: t('footer.ceo', '대표') }), ' ', _jsx("span", { className: "footer-meta caption-1", children: t('footer.ceoName', '한지혜') })] }), _jsxs("div", { children: [_jsx("span", { className: "footer-title", children: t('footer.address', '주소') }), ' ', _jsx("span", { className: "footer-meta caption-1", children: t('footer.addressValue', '강원도 춘천시 시청길 32 해찬빌딩 3층 302호') })] }), _jsxs("div", { children: [_jsx("span", { className: "footer-title", children: t('footer.support', '고객센터') }), ' ', _jsxs("span", { className: "footer-meta caption-1", children: [email, " \u00A0 ", tel] })] })] })] }), _jsx("div", { className: "site-footer__copyright footer-copyright caption-1", children: "\u00A9 IHAB, Inc. All rights reserved." })] }) }));
18
+ } }, children: t('footer.policy.privacy', '개인정보 처리방침') }) })] })] })] })] }), _jsx("hr", { className: "site-footer__rule" }), _jsxs("div", { className: "site-footer__bottom", children: [_jsx("div", { className: "site-footer__brand", children: _jsx("img", { src: logoSrc, alt: "IHAB", className: "site-footer__logo" }) }), _jsxs("div", { className: "site-footer__meta", children: [_jsxs("div", { children: [_jsx("span", { className: "footer-title", children: t('footer.ceo', '대표') }), ' ', _jsx("span", { className: "footer-meta caption-1", children: t('footer.ceoName', '한지혜') })] }), _jsxs("div", { children: [_jsx("span", { className: "footer-title", children: t('footer.address', '주소') }), ' ', _jsx("span", { className: "footer-meta caption-1", children: t('footer.addressValue', '강원도 춘천시 시청길 32 해찬빌딩 3층 302호') })] }), _jsxs("div", { children: [_jsx("span", { className: "footer-title", children: t('footer.bizNo', '사업자등록번호') }), ' ', _jsx("span", { className: "footer-meta caption-1", children: t('footer.bizNoValue', '526-81-02926') })] }), _jsxs("div", { children: [_jsx("span", { className: "footer-title", children: t('footer.support', '고객센터') }), ' ', _jsxs("span", { className: "footer-meta caption-1", children: [email, " \u00A0 ", tel] })] })] })] }), _jsx("div", { className: "site-footer__copyright footer-copyright caption-1", children: "\u00A9 IHAB, Inc. All rights reserved." })] }) }));
19
19
  }
@@ -75,6 +75,19 @@
75
75
  font-weight: 600;
76
76
  }
77
77
 
78
+ /* 목표(85)를 넘은 줄 — 한계 넘음과 **다른 사실**이라 색도 다르다(주황).
79
+ **주황을 글자색으로 안 쓴다** — `--color-warn`(#e6b94e)은 흰 지면에서 1.9:1 이라
80
+ caption 크기 글이 안 읽힌다. 바탕과 테두리로 주황을 두고 글자는 짙은 쪽을 쓴다.
81
+ 여기도 색만으로 말하지 않는다 — 글(`목표 넘음`)이 함께 간다. */
82
+ .notify-text-preview__bytes.is-over-target {
83
+ padding: 0 6px;
84
+ border: 1px solid var(--color-warn);
85
+ border-radius: 999px;
86
+ background: var(--color-warn-a10);
87
+ color: var(--color-deepGray);
88
+ font-weight: 600;
89
+ }
90
+
78
91
  /* 글칸 — **테두리를 둔다.** 바탕색만으로는 칸으로 안 읽힌다(사장님이 화면에서 짚으셨다).
79
92
  문자와 알림톡이 나란히 설 때 어디까지가 한 통인지 눈이 먼저 알아야 한다. */
80
93
  .notify-text-preview__text {
@@ -60,6 +60,14 @@ export interface NotifyTextPreviewProps {
60
60
  * `div` 로 그린다.
61
61
  */
62
62
  titleLevel?: 0 | 2 | 3 | 4 | 5 | 6;
63
+ /**
64
+ * 겨누는 바이트. 기본은 코어의 `SMS_TARGET_BYTES`(85)다.
65
+ *
66
+ * **한계와 목표는 다른 사실이다** — 90을 넘으면 *값이 바뀌고*, 85를 넘으면 *여유가 없다.*
67
+ * 그래서 86~90 은 '한계 넘음'(빨강)이 아니라 **'목표 넘음'(주황)** 으로 선다. 목표를
68
+ * 안 보려면 `null` 을 준다.
69
+ */
70
+ targetBytes?: number | null;
63
71
  }
64
72
  /** @public */
65
- export default function NotifyTextPreview({ items, className, ariaLabel, titleLevel }: NotifyTextPreviewProps): import("react").JSX.Element;
73
+ export default function NotifyTextPreview({ items, className, ariaLabel, titleLevel, targetBytes }: NotifyTextPreviewProps): import("react").JSX.Element;
@@ -1,15 +1,19 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { measureSmsText } from '../utils/smsBytes';
3
+ import { measureSmsText, SMS_BYTE_LIMIT } from '../utils/smsBytes';
4
4
  import './NotifyTextPreview.css';
5
5
  /** @public */
6
- export default function NotifyTextPreview({ items, className, ariaLabel = '알림 글', titleLevel = 4 }) {
6
+ export default function NotifyTextPreview({ items, className, ariaLabel = '알림 글', titleLevel = 4, targetBytes }) {
7
7
  /* 글자 꼴(`h5b`)과 머리글 급(`titleLevel`)은 **다른 것**이다 — 급은 바깥 묶음이 정한다 */
8
8
  const TitleTag = titleLevel ? `h${titleLevel}` : 'div';
9
9
  return (_jsx("div", { className: `notify-text-preview${className ? ` ${className}` : ''}`, role: "group", "aria-label": ariaLabel, children: items.map((item) => {
10
- const m = measureSmsText(item.sms);
11
- return (_jsxs("section", { className: "notify-text-preview__item", children: [_jsxs("header", { className: "notify-text-preview__head", children: [_jsx(TitleTag, { className: "notify-text-preview__name h5b", children: item.name }), item.meta && Object.keys(item.meta).length > 0 ? (_jsx("dl", { className: "notify-text-preview__meta caption-1", children: Object.entries(item.meta).map(([k, v]) => (_jsxs("div", { className: "notify-text-preview__meta-row", children: [_jsx("dt", { children: k }), _jsx("dd", { children: v })] }, k))) })) : null] }), _jsxs("div", { className: "notify-text-preview__body", children: [_jsxs("div", { className: "notify-text-preview__col", children: [_jsxs("div", { className: "notify-text-preview__col-head caption-1", children: [_jsx("span", { children: "\uBB38\uC790" }), _jsxs("span", { className: `notify-text-preview__bytes${m.over ? ' is-over' : ''}`,
10
+ const m = targetBytes === undefined ? measureSmsText(item.sms) : measureSmsText(item.sms, targetBytes);
11
+ /* 장문이 됐다는 것이 **가장 돈 드는 사실**이다 — `over`(잘린다)보다 먼저 눈에 들어야 한다 */
12
+ const isLong = m.type === 'LMS';
13
+ return (_jsxs("section", { className: "notify-text-preview__item", children: [_jsxs("header", { className: "notify-text-preview__head", children: [_jsx(TitleTag, { className: "notify-text-preview__name h5b", children: item.name }), item.meta && Object.keys(item.meta).length > 0 ? (_jsx("dl", { className: "notify-text-preview__meta caption-1", children: Object.entries(item.meta).map(([k, v]) => (_jsxs("div", { className: "notify-text-preview__meta-row", children: [_jsx("dt", { children: k }), _jsx("dd", { children: v })] }, k))) })) : null] }), _jsxs("div", { className: "notify-text-preview__body", children: [_jsxs("div", { className: "notify-text-preview__col", children: [_jsxs("div", { className: "notify-text-preview__col-head caption-1", children: [_jsx("span", { children: "\uBB38\uC790" }), _jsxs("span", { className: `notify-text-preview__bytes${isLong ? ' is-over' : m.overTarget ? ' is-over-target' : ''}`,
12
14
  /* 넘은 것은 색만으로 말하지 않는다 — 색을 못 보는 사람에게도 글로 간다 */
13
- title: m.over ? `${m.limit}바이트를 넘었다 — 보낼 때 잘린다` : undefined, children: [m.bytes, "\uBC14\uC774\uD2B8 \u00B7 ", m.type, m.over ? ' · 한계 넘음' : ''] })] }), _jsx("pre", { className: "notify-text-preview__text", children: item.sms })] }), item.ata ? (_jsxs("div", { className: "notify-text-preview__col", children: [_jsx("div", { className: "notify-text-preview__col-head caption-1", children: _jsx("span", { children: "\uC54C\uB9BC\uD1A1" }) }), _jsx("pre", { className: "notify-text-preview__text", children: item.ata })] })) : null] })] }, item.name));
15
+ title: m.over ? `${m.limit}바이트를 넘었다 — 보낼 때 잘린다`
16
+ : isLong ? `${SMS_BYTE_LIMIT}바이트를 넘어 장문이다 — 한 통 값이 단문의 두 배 반이다`
17
+ : m.overTarget ? `겨누는 ${m.target}바이트를 넘었다 — 한 글자만 늘어도 ${SMS_BYTE_LIMIT}을 넘어 장문이 된다` : undefined, children: [m.bytes, m.target !== null ? ` / 목표 ${m.target}` : '', "\uBC14\uC774\uD2B8 \u00B7 ", isLong ? '장문' : '단문', m.over ? ' · 한계 넘음' : isLong ? ' · 값이 두 배 반' : m.overTarget ? ' · 목표 넘음' : ''] })] }), _jsx("pre", { className: "notify-text-preview__text", children: item.sms })] }), item.ata ? (_jsxs("div", { className: "notify-text-preview__col", children: [_jsx("div", { className: "notify-text-preview__col-head caption-1", children: _jsx("span", { children: "\uC54C\uB9BC\uD1A1" }) }), _jsx("pre", { className: "notify-text-preview__text", children: item.ata })] })) : null] })] }, item.name));
14
18
  }) }));
15
19
  }
@@ -911,9 +911,11 @@
911
911
  },
912
912
  "ceo": "CEO",
913
913
  "address": "Address",
914
+ "bizNo": "Business no.",
914
915
  "support": "Support",
915
916
  "ceoName": "Ji-hye Han",
916
- "addressValue": "302, 3F, Haechan Bldg, 32 Sicheong-gil, Chuncheon, Gangwon, Korea"
917
+ "addressValue": "302, 3F, Haechan Bldg, 32 Sicheong-gil, Chuncheon, Gangwon, Korea",
918
+ "bizNoValue": "526-81-02926"
917
919
  },
918
920
  "date": {
919
921
  "timeHM": "{{hh}}:{{mm}}",
@@ -898,9 +898,11 @@
898
898
  },
899
899
  "ceo": "대표",
900
900
  "address": "주소",
901
+ "bizNo": "사업자등록번호",
901
902
  "support": "고객센터",
902
903
  "ceoName": "한지혜",
903
- "addressValue": "강원도 춘천시 시청길 32 해찬빌딩 3층 302호"
904
+ "addressValue": "강원도 춘천시 시청길 32 해찬빌딩 3층 302호",
905
+ "bizNoValue": "526-81-02926"
904
906
  },
905
907
  "date": {
906
908
  "timeHM": "{{h}}시 {{m}}분",
@@ -27,6 +27,40 @@
27
27
  export declare const SMS_BYTE_LIMIT = 90;
28
28
  /** 장문(LMS) 본문 한계 */
29
29
  export declare const LMS_BYTE_LIMIT = 2000;
30
+ /**
31
+ * **우리가 겨누는 수는 85다** — 통신사 한계(`SMS_BYTE_LIMIT`, 90)가 아니다.
32
+ *
33
+ * 사장님 지시(2026-10-21) — *"sms 전수조사 해 85바이트 이하로 전부 조정해"*. 그날 오전에
34
+ * 초대 문자가 **장문 49.5원**으로 나가고 있던 것을 솔라피에서 잡으셨고, 그러고 보니 90에
35
+ * **딱 붙은 글이 하나**(인증 해제, 90바이트) 있었다.
36
+ *
37
+ * **5바이트를 비워 두는 까닭** — 글을 한 글자 고치다 넘기기가 너무 쉽고, 넘어간 것은
38
+ * 화면 어디에도 안 보인다(**청구서에만** 보인다). 90을 겨누면 *맞았다* 와 *넘었다* 사이에
39
+ * 아무것도 없다.
40
+ *
41
+ * **한계와 목표는 다른 사실이다** — 90을 넘으면 *값이 바뀌고*, 85를 넘으면 *여유가 없다.*
42
+ * 그래서 `measureSmsText` 가 둘을 따로 돌려주고, 그리는 쪽도 따로 그린다.
43
+ *
44
+ * ## 이것은 **사실이 아니라 정해진 수**다 — `eucKrBytes` 와 결이 다르다
45
+ *
46
+ * 처음에 여기 *"코어가 쥐는 까닭은 `eucKrBytes` 와 같다"* 고 적었다가 **고쳤다**(0.358.0).
47
+ * 말귀가 되짚어 준 것이 맞다:
48
+ *
49
+ * · `eucKrBytes` 는 **사실**이다 — EUC-KR 이 한글을 2바이트로 적는다.
50
+ * · `SMS_BYTE_LIMIT`(90)도 **사실**이다 — 통신사가 거기서 장문으로 받는다.
51
+ * · `SMS_TARGET_BYTES`(85)는 **사람이 정한 수**다 — 사장님이 말귀를 두고 *"85바이트
52
+ * 이하로 전부 조정해"* 라 하셨다. 얼마를 비워 둘지는 서비스마다 다를 수 있다.
53
+ *
54
+ * 요금(18원·45원)을 여기 안 박은 것과 **같은 까닭**이다. 그 잣대를 제대로 대면 85도
55
+ * 저쪽이다 — 이 수를 들인 쪽은 코어가 80이나 88로 바꾸는 날 **아무도 정하지 않은 채**
56
+ * 제 글의 자가 바뀐다.
57
+ *
58
+ * 그래서 **쥐지 않고 내준다** — 기본값으로 두되, 다른 여유가 맞는 쪽은 `measureSmsText`
59
+ * 와 `NotifyTextPreview` 에 제 수를 넘긴다(`null` 이면 목표를 아예 안 본다). 말귀는
60
+ * 제 수를 그대로 들고 **코어 것과 같은지 맞대는 못**을 세웠다 — 들이지 않아도 어긋남이
61
+ * 조용히 지나가지 않는다. 그 꼴이 옳다.
62
+ */
63
+ export declare const SMS_TARGET_BYTES = 85;
30
64
  /** 보내는 갈래 */
31
65
  export type SmsType = 'SMS' | 'LMS';
32
66
  /**
@@ -46,14 +80,23 @@ export declare function smsTypeFor(text: string): SmsType;
46
80
  /**
47
81
  * 글 하나를 재서 **쓰는 쪽이 그릴 만큼** 돌려준다.
48
82
  *
49
- * `over` 는 *"한계를 넘었나"* 다 — 단문이면 90, 장문이면 2000 을 기준으로 본다.
50
- * 장문까지 넘으면 보내는 쪽에서 **잘린다.**
83
+ * 사실 **둘**을 따로 돌려준다 — 섞으면 그리는 쪽이 둘 중 하나를 못 보인다:
84
+ *
85
+ * · `over` — **한계**(단문 90 · 장문 2000)를 넘었나. 넘으면 *값이 바뀌거나 글이 잘린다.*
86
+ * · `overTarget` — **목표**(`targetBytes`)를 넘었나. 넘어도 보내지는 데는 지장이 없고
87
+ * **여유가 없다**는 뜻이다. 86~90 이 그 자리다.
88
+ *
89
+ * `targetBytes` 에 `null` 을 주면 목표를 안 본다(`target` 이 `null`, `overTarget` 이 거짓).
51
90
  *
52
91
  * @public
53
92
  */
54
- export declare function measureSmsText(text: string): {
93
+ export declare function measureSmsText(text: string, targetBytes?: number | null): {
55
94
  bytes: number;
56
95
  type: SmsType;
57
96
  limit: number;
58
97
  over: boolean;
98
+ /** 겨눈 수. `null` 이면 목표를 안 본 것이다 */
99
+ target: number | null;
100
+ /** 목표를 넘었나 — `over` 와 **다른 사실**이다 */
101
+ overTarget: boolean;
59
102
  };
@@ -27,6 +27,40 @@
27
27
  export const SMS_BYTE_LIMIT = 90;
28
28
  /** 장문(LMS) 본문 한계 */
29
29
  export const LMS_BYTE_LIMIT = 2000;
30
+ /**
31
+ * **우리가 겨누는 수는 85다** — 통신사 한계(`SMS_BYTE_LIMIT`, 90)가 아니다.
32
+ *
33
+ * 사장님 지시(2026-10-21) — *"sms 전수조사 해 85바이트 이하로 전부 조정해"*. 그날 오전에
34
+ * 초대 문자가 **장문 49.5원**으로 나가고 있던 것을 솔라피에서 잡으셨고, 그러고 보니 90에
35
+ * **딱 붙은 글이 하나**(인증 해제, 90바이트) 있었다.
36
+ *
37
+ * **5바이트를 비워 두는 까닭** — 글을 한 글자 고치다 넘기기가 너무 쉽고, 넘어간 것은
38
+ * 화면 어디에도 안 보인다(**청구서에만** 보인다). 90을 겨누면 *맞았다* 와 *넘었다* 사이에
39
+ * 아무것도 없다.
40
+ *
41
+ * **한계와 목표는 다른 사실이다** — 90을 넘으면 *값이 바뀌고*, 85를 넘으면 *여유가 없다.*
42
+ * 그래서 `measureSmsText` 가 둘을 따로 돌려주고, 그리는 쪽도 따로 그린다.
43
+ *
44
+ * ## 이것은 **사실이 아니라 정해진 수**다 — `eucKrBytes` 와 결이 다르다
45
+ *
46
+ * 처음에 여기 *"코어가 쥐는 까닭은 `eucKrBytes` 와 같다"* 고 적었다가 **고쳤다**(0.358.0).
47
+ * 말귀가 되짚어 준 것이 맞다:
48
+ *
49
+ * · `eucKrBytes` 는 **사실**이다 — EUC-KR 이 한글을 2바이트로 적는다.
50
+ * · `SMS_BYTE_LIMIT`(90)도 **사실**이다 — 통신사가 거기서 장문으로 받는다.
51
+ * · `SMS_TARGET_BYTES`(85)는 **사람이 정한 수**다 — 사장님이 말귀를 두고 *"85바이트
52
+ * 이하로 전부 조정해"* 라 하셨다. 얼마를 비워 둘지는 서비스마다 다를 수 있다.
53
+ *
54
+ * 요금(18원·45원)을 여기 안 박은 것과 **같은 까닭**이다. 그 잣대를 제대로 대면 85도
55
+ * 저쪽이다 — 이 수를 들인 쪽은 코어가 80이나 88로 바꾸는 날 **아무도 정하지 않은 채**
56
+ * 제 글의 자가 바뀐다.
57
+ *
58
+ * 그래서 **쥐지 않고 내준다** — 기본값으로 두되, 다른 여유가 맞는 쪽은 `measureSmsText`
59
+ * 와 `NotifyTextPreview` 에 제 수를 넘긴다(`null` 이면 목표를 아예 안 본다). 말귀는
60
+ * 제 수를 그대로 들고 **코어 것과 같은지 맞대는 못**을 세웠다 — 들이지 않아도 어긋남이
61
+ * 조용히 지나가지 않는다. 그 꼴이 옳다.
62
+ */
63
+ export const SMS_TARGET_BYTES = 85;
30
64
  /**
31
65
  * EUC-KR 바이트 — 한글 2 · ASCII 1 · 줄바꿈 2.
32
66
  *
@@ -56,14 +90,20 @@ export function smsTypeFor(text) {
56
90
  /**
57
91
  * 글 하나를 재서 **쓰는 쪽이 그릴 만큼** 돌려준다.
58
92
  *
59
- * `over` 는 *"한계를 넘었나"* 다 — 단문이면 90, 장문이면 2000 을 기준으로 본다.
60
- * 장문까지 넘으면 보내는 쪽에서 **잘린다.**
93
+ * 사실 **둘**을 따로 돌려준다 — 섞으면 그리는 쪽이 둘 중 하나를 못 보인다:
94
+ *
95
+ * · `over` — **한계**(단문 90 · 장문 2000)를 넘었나. 넘으면 *값이 바뀌거나 글이 잘린다.*
96
+ * · `overTarget` — **목표**(`targetBytes`)를 넘었나. 넘어도 보내지는 데는 지장이 없고
97
+ * **여유가 없다**는 뜻이다. 86~90 이 그 자리다.
98
+ *
99
+ * `targetBytes` 에 `null` 을 주면 목표를 안 본다(`target` 이 `null`, `overTarget` 이 거짓).
61
100
  *
62
101
  * @public
63
102
  */
64
- export function measureSmsText(text) {
103
+ export function measureSmsText(text, targetBytes = SMS_TARGET_BYTES) {
65
104
  const bytes = eucKrBytes(text);
66
105
  const type = smsTypeFor(text);
67
106
  const limit = type === 'SMS' ? SMS_BYTE_LIMIT : LMS_BYTE_LIMIT;
68
- return { bytes, type, limit, over: bytes > limit };
107
+ const target = typeof targetBytes === 'number' ? targetBytes : null;
108
+ return { bytes, type, limit, over: bytes > limit, target, overTarget: target !== null && bytes > target };
69
109
  }
@@ -6,7 +6,7 @@ export { configureTTS, getTTSUrlResolver, getTTSFallbackUrlResolver, clearAudioE
6
6
  export { configurePronunciationResolver, getPronunciationResolver } from './utils/pronunciationConfig';
7
7
  export { getElementAnalyser, resumeAudioMeter, readLevel, routeElementAudio, setElementVolume, getElementVolume } from './utils/audioMeter';
8
8
  export { configureImageResolver, getImageResolver, checkImageExists, getImageExistsCached, prewarmImageCache, bulkSetImageExistence, clearImageExistenceCache, getImageExistenceStats } from './utils/imageConfig';
9
- export { eucKrBytes, smsTypeFor, measureSmsText, SMS_BYTE_LIMIT, LMS_BYTE_LIMIT } from './utils/smsBytes';
9
+ export { eucKrBytes, smsTypeFor, measureSmsText, SMS_BYTE_LIMIT, LMS_BYTE_LIMIT, SMS_TARGET_BYTES } from './utils/smsBytes';
10
10
  export type { SmsType } from './utils/smsBytes';
11
11
  export { buildPairIndex, getPairIndex, clearPairIndexCache } from './utils/wordPairIndex';
12
12
  export type { PairIndex, PairIndexEntry, PairInput } from './utils/wordPairIndex';
@@ -7,7 +7,7 @@ export { getElementAnalyser, resumeAudioMeter, readLevel, routeElementAudio, set
7
7
  export { configureImageResolver, getImageResolver, checkImageExists, getImageExistsCached, prewarmImageCache, bulkSetImageExistence, clearImageExistenceCache, getImageExistenceStats } from './utils/imageConfig';
8
8
  /* 문자 글의 바이트 — 90바이트가 단문(18원)과 장문(45원)을 가른다(말귀 청 (314)).
9
9
  말귀가 제 셈을 걷고 이것을 쓴다 — 한 결정이 두 곳에 적히지 않게. */
10
- export { eucKrBytes, smsTypeFor, measureSmsText, SMS_BYTE_LIMIT, LMS_BYTE_LIMIT } from './utils/smsBytes';
10
+ export { eucKrBytes, smsTypeFor, measureSmsText, SMS_BYTE_LIMIT, LMS_BYTE_LIMIT, SMS_TARGET_BYTES } from './utils/smsBytes';
11
11
  export { buildPairIndex, getPairIndex, clearPairIndexCache } from './utils/wordPairIndex';
12
12
  export { minimalPairDistractors, filterContrastCandidates, hasStructureContrast, buildMultiPartPosContrast, hasSel } from './utils/wordContrast';
13
13
  export { parseSyllable, composeSyllable, createDecompCache, CHOSEONG, JUNGSEONG, JONGSEONG } from './utils/koreanSyllable';
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.356.0";
49
+ export declare const CORE_VERSION = "0.358.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.356.0';
49
+ export const CORE_VERSION = '0.358.0';