feat: 전화번호·계좌번호·사업자등록번호 입력을 숫자 전용으로 만든다
FoxInput에 digitsOnly를 둔다. 타이핑·붙여넣기·끌어놓기·IME 어느 경로든 input 이벤트에서 숫자만 남기고, 커서 앞에서 지운 글자 수만큼 커서를 당겨 제자리를 지킨다. 붙여넣기는 따로 받는다 — 브라우저가 maxLength를 붙여넣은 원문에 먼저 적용해 `123-45-67890`의 뒷자리를 잘라 버리기 때문이다. 초기값의 하이픈도 걷는다. FoxPhoneNumber는 이미 숫자만 받았지만 번호 전체를 한 칸에 붙여넣으면 그 칸 자릿수만 남았다. 붙여넣은 숫자를 세 칸에 나눠 채우고 마지막 칸으로 포커스를 옮긴다. onlyDigits는 두 컴포넌트가 같이 쓰도록 utils로 뺀다. 가맹몰 관리 모달(사업자등록번호·전화번호·입금계좌번호)과 가맹몰 상세(업체 연락처)에 켜고, 도메인 검증도 숫자만 허용하도록 조인다 — 사업자등록번호는 10자리. Co-Authored-By: Claude Fable 5.1
@deb800d309984388a3544f35d35f442abf90bd54
--- @fox/core/components/fox-input/fox-input.tsx
+++ @fox/core/components/fox-input/fox-input.tsx
... | ... | @@ -6,12 +6,13 @@ |
| 6 | 6 |
useRef, |
| 7 | 7 |
useState, |
| 8 | 8 |
type ChangeEvent, |
| 9 |
+ type ClipboardEvent, |
|
| 9 | 10 |
type ComponentPropsWithRef, |
| 10 | 11 |
type FocusEvent, |
| 11 | 12 |
type FormEvent, |
| 12 | 13 |
type ReactNode, |
| 13 | 14 |
} from "react"; |
| 14 |
-import { cx } from "../../utils";
|
|
| 15 |
+import { cx, onlyDigits } from "../../utils";
|
|
| 15 | 16 |
import { FoxFormLabel, type FoxFormLabelRequirement } from "../fox-form-label";
|
| 16 | 17 |
import {
|
| 17 | 18 |
foxValidationMessage, |
... | ... | @@ -56,6 +57,12 @@ |
| 56 | 57 |
invalid?: boolean; |
| 57 | 58 |
/** 주면 모양이 그 상태로 고정된다. 없으면 포커스·값·비활성으로 브라우저가 판단한다. */ |
| 58 | 59 |
state?: FoxInputState; |
| 60 |
+ /** |
|
| 61 |
+ * 숫자만 받는다. 타이핑·붙여넣기·끌어놓기·IME 어느 경로로 들어와도 숫자가 아닌 글자는 |
|
| 62 |
+ * 그 자리에서 버리고 커서를 지킨다. `value`·`defaultValue`에 하이픈이 섞여 와도 숫자만 |
|
| 63 |
+ * 보인다. `inputMode`를 따로 주지 않으면 `numeric`이 된다. |
|
| 64 |
+ */ |
|
| 65 |
+ digitsOnly?: boolean; |
|
| 59 | 66 |
|
| 60 | 67 |
validators?: FoxValidator[]; |
| 61 | 68 |
validationMessages?: FoxValidationMessages; |
... | ... | @@ -100,6 +107,7 @@ |
| 100 | 107 |
clearLabel = "입력 지우기", |
| 101 | 108 |
invalid = false, |
| 102 | 109 |
state, |
| 110 |
+ digitsOnly = false, |
|
| 103 | 111 |
validators, |
| 104 | 112 |
validationMessages, |
| 105 | 113 |
validateOn = "blur", |
... | ... | @@ -114,11 +122,15 @@ |
| 114 | 122 |
}: FoxInputProps) {
|
| 115 | 123 |
const autoId = useId(); |
| 116 | 124 |
|
| 125 |
+ const fieldValue = digitsOnly && value !== undefined ? onlyDigits(String(value)) : value; |
|
| 126 |
+ const fieldDefaultValue = |
|
| 127 |
+ digitsOnly && defaultValue !== undefined ? onlyDigits(String(defaultValue)) : defaultValue; |
|
| 128 |
+ |
|
| 117 | 129 |
// 지우기 버튼은 값이 있을 때만 뜬다. 비제어 입력은 DOM만 알고 있으므로 직접 따라간다. |
| 118 |
- const [typedIn, setTypedIn] = useState(() => String(defaultValue ?? "").length > 0); |
|
| 130 |
+ const [typedIn, setTypedIn] = useState(() => String(fieldDefaultValue ?? "").length > 0); |
|
| 119 | 131 |
const fieldRef = useRef<HTMLInputElement>(null); |
| 120 | 132 |
|
| 121 |
- const [draft, setDraft] = useState(() => String(defaultValue ?? "")); |
|
| 133 |
+ const [draft, setDraft] = useState(() => String(fieldDefaultValue ?? "")); |
|
| 122 | 134 |
const [touched, setTouched] = useState(false); |
| 123 | 135 |
|
| 124 | 136 |
const validationErrors = |
... | ... | @@ -164,12 +176,33 @@ |
| 164 | 176 |
} |
| 165 | 177 |
}; |
| 166 | 178 |
|
| 167 |
- const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
| 168 |
- if (value === undefined) {
|
|
| 169 |
- setTypedIn(event.target.value.length > 0); |
|
| 179 |
+ // 숫자 전용이면 DOM 값을 그 자리에서 고쳐 쓴다. 커서 앞에서 지운 글자 수만큼 커서를 당겨 |
|
| 180 |
+ // 제자리를 지킨다 — 값을 통째로 바꾸면 브라우저가 커서를 끝으로 보내기 때문이다. |
|
| 181 |
+ const readFieldValue = (field: HTMLInputElement): string => {
|
|
| 182 |
+ const raw = field.value; |
|
| 183 |
+ if (!digitsOnly) {
|
|
| 184 |
+ return raw; |
|
| 170 | 185 |
} |
| 171 |
- setDraft(event.target.value); |
|
| 172 |
- onChange?.(event.target.value, event); |
|
| 186 |
+ const clean = onlyDigits(raw); |
|
| 187 |
+ if (clean === raw) {
|
|
| 188 |
+ return raw; |
|
| 189 |
+ } |
|
| 190 |
+ const caret = field.selectionStart; |
|
| 191 |
+ field.value = clean; |
|
| 192 |
+ if (caret !== null) {
|
|
| 193 |
+ const position = onlyDigits(raw.slice(0, caret)).length; |
|
| 194 |
+ field.setSelectionRange(position, position); |
|
| 195 |
+ } |
|
| 196 |
+ return clean; |
|
| 197 |
+ }; |
|
| 198 |
+ |
|
| 199 |
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
| 200 |
+ const next = readFieldValue(event.target); |
|
| 201 |
+ if (value === undefined) {
|
|
| 202 |
+ setTypedIn(next.length > 0); |
|
| 203 |
+ } |
|
| 204 |
+ setDraft(next); |
|
| 205 |
+ onChange?.(next, event); |
|
| 173 | 206 |
}; |
| 174 | 207 |
|
| 175 | 208 |
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
... | ... | @@ -177,8 +210,39 @@ |
| 177 | 210 |
rest.onBlur?.(event); |
| 178 | 211 |
}; |
| 179 | 212 |
|
| 213 |
+ // 숫자 전용 붙여넣기는 숫자만 추려 직접 넣는다. 브라우저는 maxLength를 붙여넣은 원문에 먼저 |
|
| 214 |
+ // 적용해 `123-45-67890`의 뒷자리를 잘라 버리므로 기본 동작에 맡길 수 없다. |
|
| 215 |
+ const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
|
| 216 |
+ rest.onPaste?.(event); |
|
| 217 |
+ if (!digitsOnly || event.defaultPrevented) {
|
|
| 218 |
+ return; |
|
| 219 |
+ } |
|
| 220 |
+ event.preventDefault(); |
|
| 221 |
+ const digits = onlyDigits(event.clipboardData.getData("text"));
|
|
| 222 |
+ if (digits === "") {
|
|
| 223 |
+ return; |
|
| 224 |
+ } |
|
| 225 |
+ const field = event.currentTarget; |
|
| 226 |
+ if (document.execCommand("insertText", false, digits)) {
|
|
| 227 |
+ return; |
|
| 228 |
+ } |
|
| 229 |
+ // execCommand가 없는 환경 — 값을 직접 넣고 input 이벤트를 흘린다. React가 노드에 심어 둔 |
|
| 230 |
+ // 값 추적기를 피하려면 프로토타입의 setter를 써야 onChange가 돈다. |
|
| 231 |
+ const start = field.selectionStart ?? field.value.length; |
|
| 232 |
+ const end = field.selectionEnd ?? start; |
|
| 233 |
+ let next = field.value.slice(0, start) + digits + field.value.slice(end); |
|
| 234 |
+ if (field.maxLength > 0) {
|
|
| 235 |
+ next = next.slice(0, field.maxLength); |
|
| 236 |
+ } |
|
| 237 |
+ const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set; |
|
| 238 |
+ setValue?.call(field, next); |
|
| 239 |
+ const caret = Math.min(start + digits.length, next.length); |
|
| 240 |
+ field.setSelectionRange(caret, caret); |
|
| 241 |
+ field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
| 242 |
+ }; |
|
| 243 |
+ |
|
| 180 | 244 |
const handleInput = (event: FormEvent<HTMLInputElement>) => {
|
| 181 |
- onInput?.((event.target as HTMLInputElement).value, event); |
|
| 245 |
+ onInput?.(readFieldValue(event.target as HTMLInputElement), event); |
|
| 182 | 246 |
}; |
| 183 | 247 |
|
| 184 | 248 |
const handleClear = () => {
|
... | ... | @@ -207,16 +271,18 @@ |
| 207 | 271 |
<div className="fox-input__box"> |
| 208 | 272 |
<input |
| 209 | 273 |
{...rest}
|
| 274 |
+ inputMode={rest.inputMode ?? (digitsOnly ? "numeric" : undefined)}
|
|
| 210 | 275 |
ref={attachField}
|
| 211 | 276 |
id={fieldId}
|
| 212 | 277 |
className="fox-input__field" |
| 213 |
- value={value}
|
|
| 214 |
- defaultValue={defaultValue}
|
|
| 278 |
+ value={fieldValue}
|
|
| 279 |
+ defaultValue={fieldDefaultValue}
|
|
| 215 | 280 |
aria-invalid={errored || undefined}
|
| 216 | 281 |
aria-describedby={shownMessage ? messageId : undefined}
|
| 217 | 282 |
onChange={handleChange}
|
| 218 | 283 |
onInput={handleInput}
|
| 219 | 284 |
onBlur={handleBlur}
|
| 285 |
+ onPaste={handlePaste}
|
|
| 220 | 286 |
/> |
| 221 | 287 |
|
| 222 | 288 |
{clearable && hasValue && (
|
--- @fox/core/components/fox-phone-number/fox-phone-number.tsx
+++ @fox/core/components/fox-phone-number/fox-phone-number.tsx
... | ... | @@ -6,11 +6,12 @@ |
| 6 | 6 |
useRef, |
| 7 | 7 |
useState, |
| 8 | 8 |
type ChangeEvent, |
| 9 |
+ type ClipboardEvent, |
|
| 9 | 10 |
type KeyboardEvent, |
| 10 | 11 |
type ReactNode, |
| 11 | 12 |
} from "react"; |
| 12 | 13 |
import { FoxFormLabel } from "../fox-form-label";
|
| 13 |
-import { cx } from "../../utils";
|
|
| 14 |
+import { cx, onlyDigits } from "../../utils";
|
|
| 14 | 15 |
import { FoxInput, type FoxInputState } from "../fox-input";
|
| 15 | 16 |
import { FoxSelect, type FoxSelectItem } from "../fox-select";
|
| 16 | 17 |
|
... | ... | @@ -63,11 +64,6 @@ |
| 63 | 64 |
const DEFAULT_PLACEHOLDERS: Parts = ["010", "1234", "5678"]; |
| 64 | 65 |
const DEFAULT_MAX_LENGTHS: Lengths = [3, 4, 4]; |
| 65 | 66 |
const SEGMENT_LABELS: Parts = ["전화번호 앞자리", "전화번호 가운데자리", "전화번호 뒷자리"]; |
| 66 |
- |
|
| 67 |
-/** 숫자가 아닌 글자는 버린다 — 붙여넣기로 들어온 하이픈·공백도 여기서 걸린다. */ |
|
| 68 |
-function onlyDigits(text: string): string {
|
|
| 69 |
- return text.replace(/\D/g, ""); |
|
| 70 |
-} |
|
| 71 | 67 |
|
| 72 | 68 |
/** |
| 73 | 69 |
* 숫자만 담긴 값을 화면의 세 칸으로 나눈다. |
... | ... | @@ -145,13 +141,17 @@ |
| 145 | 141 |
const messageId = `${rootId}-message`;
|
| 146 | 142 |
const errored = invalid || state === "error"; |
| 147 | 143 |
|
| 148 |
- const commit = (index: number, next: string) => {
|
|
| 149 |
- const nextParts = [...parts] as Parts; |
|
| 150 |
- nextParts[index] = next; |
|
| 144 |
+ const commitParts = (nextParts: Parts) => {
|
|
| 151 | 145 |
setParts(nextParts); |
| 152 | 146 |
const joined = nextParts.join("");
|
| 153 | 147 |
setSeenValue(joined); |
| 154 | 148 |
onChange?.(joined); |
| 149 |
+ }; |
|
| 150 |
+ |
|
| 151 |
+ const commit = (index: number, next: string) => {
|
|
| 152 |
+ const nextParts = [...parts] as Parts; |
|
| 153 |
+ nextParts[index] = next; |
|
| 154 |
+ commitParts(nextParts); |
|
| 155 | 155 |
}; |
| 156 | 156 |
|
| 157 | 157 |
const focusSegment = (index: number) => {
|
... | ... | @@ -165,6 +165,35 @@ |
| 165 | 165 |
commit(index, digits); |
| 166 | 166 |
if (autoAdvance && index < 2 && digits.length >= maxLengths[index]) {
|
| 167 | 167 |
focusSegment(index + 1); |
| 168 |
+ } |
|
| 169 |
+ }; |
|
| 170 |
+ |
|
| 171 |
+ // 번호 전체를 한 칸에 붙여넣으면 넘치는 숫자를 뒤 칸으로 흘려보낸다. 브라우저가 maxLength로 |
|
| 172 |
+ // 붙여넣은 글자를 먼저 잘라 버리므로 기본 동작을 막고 직접 나눈다. |
|
| 173 |
+ const handleSegmentPaste = (index: number) => (event: ClipboardEvent<HTMLInputElement>) => {
|
|
| 174 |
+ event.preventDefault(); |
|
| 175 |
+ const pasted = onlyDigits(event.clipboardData.getData("text"));
|
|
| 176 |
+ if (pasted === "") {
|
|
| 177 |
+ return; |
|
| 178 |
+ } |
|
| 179 |
+ const field = event.currentTarget; |
|
| 180 |
+ const start = field.selectionStart ?? field.value.length; |
|
| 181 |
+ const end = field.selectionEnd ?? start; |
|
| 182 |
+ let rest = onlyDigits(field.value.slice(0, start) + pasted + field.value.slice(end)); |
|
| 183 |
+ |
|
| 184 |
+ const nextParts = [...parts] as Parts; |
|
| 185 |
+ let last = index; |
|
| 186 |
+ for (let cursor = index; cursor < 3 && (cursor === index || rest !== ""); cursor += 1) {
|
|
| 187 |
+ nextParts[cursor] = rest.slice(0, maxLengths[cursor]); |
|
| 188 |
+ rest = rest.slice(maxLengths[cursor]); |
|
| 189 |
+ last = cursor; |
|
| 190 |
+ } |
|
| 191 |
+ commitParts(nextParts); |
|
| 192 |
+ |
|
| 193 |
+ if (autoAdvance && last < 2 && nextParts[last].length >= maxLengths[last]) {
|
|
| 194 |
+ focusSegment(last + 1); |
|
| 195 |
+ } else if (last !== index) {
|
|
| 196 |
+ focusSegment(last); |
|
| 168 | 197 |
} |
| 169 | 198 |
}; |
| 170 | 199 |
|
... | ... | @@ -219,6 +248,7 @@ |
| 219 | 248 |
value={parts[index]}
|
| 220 | 249 |
onChange={handleSegmentChange(index)}
|
| 221 | 250 |
onKeyDown={handleSegmentKeyDown(index)}
|
| 251 |
+ onPaste={handleSegmentPaste(index)}
|
|
| 222 | 252 |
placeholder={placeholders[index]}
|
| 223 | 253 |
maxLength={maxLengths[index]}
|
| 224 | 254 |
invalid={invalid}
|
... | ... | @@ -258,6 +288,7 @@ |
| 258 | 288 |
handleSegmentChange(index)(event.target.value) |
| 259 | 289 |
} |
| 260 | 290 |
onKeyDown={handleSegmentKeyDown(index)}
|
| 291 |
+ onPaste={handleSegmentPaste(index)}
|
|
| 261 | 292 |
ref={(node) => {
|
| 262 | 293 |
tailRefs.current[index] = node; |
| 263 | 294 |
}} |
--- @fox/core/utils/index.ts
+++ @fox/core/utils/index.ts
... | ... | @@ -1,1 +1,2 @@ |
| 1 | 1 |
export { cx, type ClassValue } from "./cx";
|
| 2 |
+export { onlyDigits } from "./only-digits";
|
+++ @fox/core/utils/only-digits.ts
... | ... | @@ -0,0 +1,4 @@ |
| 1 | +/** 숫자가 아닌 글자를 버린다 — 붙여넣기로 들어온 하이픈·공백도 여기서 걸린다. */ | |
| 2 | +export function onlyDigits(text: string): string { | |
| 3 | + return text.replace(/\D/g, ""); | |
| 4 | +} |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -1519,9 +1519,22 @@ |
| 1519 | 1519 |
</InputStage> |
| 1520 | 1520 |
), |
| 1521 | 1521 |
}, |
| 1522 |
- |
|
| 1522 |
+ {
|
|
| 1523 |
+ label: "숫자 전용 (digitsOnly) — 타이핑·붙여넣기 모두 숫자만 남고, 초기값의 하이픈도 걷힌다", |
|
| 1524 |
+ node: ( |
|
| 1525 |
+ <InputStage> |
|
| 1526 |
+ <FoxInput |
|
| 1527 |
+ size="md" |
|
| 1528 |
+ label="사업자등록번호" |
|
| 1529 |
+ digitsOnly |
|
| 1530 |
+ maxLength={10}
|
|
| 1531 |
+ defaultValue="123-45-67890" |
|
| 1532 |
+ placeholder="0000000000" |
|
| 1533 |
+ /> |
|
| 1534 |
+ </InputStage> |
|
| 1535 |
+ ), |
|
| 1536 |
+ }, |
|
| 1523 | 1537 |
], |
| 1524 |
- |
|
| 1525 | 1538 |
}, |
| 1526 | 1539 |
{
|
| 1527 | 1540 |
id: "fox-select", |
... | ... | @@ -2017,7 +2030,7 @@ |
| 2017 | 2030 |
id: "fox-phone-number", |
| 2018 | 2031 |
name: "FoxPhoneNumber", |
| 2019 | 2032 |
description: |
| 2020 |
- "Figma 시안(form-phonenumber 391:3156)을 옮겼습니다. unit은 상자 셋을 하이픈으로 잇고 앞자리가 선택이며, combine은 상자 하나에 세 칸을 둡니다. 화면은 세 칸이지만 값은 숫자만 담긴 문자열 하나입니다 — 하이픈은 값에 들어가지 않습니다. 칸을 다 채우면 다음 칸으로 넘어가고, 빈 칸에서 지우면 앞칸으로 돌아갑니다.", |
|
| 2033 |
+ "Figma 시안(form-phonenumber 391:3156)을 옮겼습니다. unit은 상자 셋을 하이픈으로 잇고 앞자리가 선택이며, combine은 상자 하나에 세 칸을 둡니다. 화면은 세 칸이지만 값은 숫자만 담긴 문자열 하나입니다 — 하이픈은 값에 들어가지 않습니다. 칸을 다 채우면 다음 칸으로 넘어가고, 빈 칸에서 지우면 앞칸으로 돌아갑니다. 번호 전체를 한 칸에 붙여넣으면 숫자만 추려 세 칸에 나눠 채웁니다.", |
|
| 2021 | 2034 |
variants: [ |
| 2022 | 2035 |
{
|
| 2023 | 2036 |
label: "직접 조작 — 값이 그대로 라벨에 보입니다", |
--- app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
+++ app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
... | ... | @@ -232,7 +232,8 @@ |
| 232 | 232 |
name="phoneNumber" |
| 233 | 233 |
label="업체 연락처" |
| 234 | 234 |
requirement="required" |
| 235 |
- placeholder="0507-1413-6625" |
|
| 235 |
+ digitsOnly |
|
| 236 |
+ placeholder="숫자만 입력하세요." |
|
| 236 | 237 |
defaultValue={original.phoneNumber}
|
| 237 | 238 |
invalid={Boolean(errors.phoneNumber)}
|
| 238 | 239 |
message={errors.phoneNumber}
|
--- app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
... | ... | @@ -29,6 +29,7 @@ |
| 29 | 29 |
MEMO_MAX_LENGTH, |
| 30 | 30 |
NAME_MAX_LENGTH, |
| 31 | 31 |
ACCOUNT_NUMBER_MAX_LENGTH, |
| 32 |
+ BUSINESS_NUMBER_LENGTH, |
|
| 32 | 33 |
OWNER_MAX_LENGTH, |
| 33 | 34 |
} from '@/lib/domain/merchant-form'; |
| 34 | 35 |
import { saveMerchantAction } from '../_actions';
|
... | ... | @@ -222,8 +223,10 @@ |
| 222 | 223 |
size="md" |
| 223 | 224 |
name="businessNumber" |
| 224 | 225 |
label="사업자등록번호" |
| 226 |
+ digitsOnly |
|
| 227 |
+ maxLength={BUSINESS_NUMBER_LENGTH}
|
|
| 225 | 228 |
defaultValue={merchant?.businessNumber ?? ''}
|
| 226 |
- placeholder="000-00-00000" |
|
| 229 |
+ placeholder="0000000000" |
|
| 227 | 230 |
invalid={Boolean(errors.businessNumber)}
|
| 228 | 231 |
message={errors.businessNumber}
|
| 229 | 232 |
/> |
... | ... | @@ -244,6 +247,7 @@ |
| 244 | 247 |
name="phoneNumber" |
| 245 | 248 |
label="전화번호" |
| 246 | 249 |
requirement="required" |
| 250 |
+ digitsOnly |
|
| 247 | 251 |
defaultValue={merchant?.phoneNumber ?? ''}
|
| 248 | 252 |
placeholder="가게 전화번호를 입력하세요." |
| 249 | 253 |
invalid={Boolean(errors.phoneNumber)}
|
... | ... | @@ -340,7 +344,8 @@ |
| 340 | 344 |
size="md" |
| 341 | 345 |
name="accountNumber" |
| 342 | 346 |
aria-label="입금계좌번호" |
| 343 |
- placeholder="입금계좌번호 (숫자와 -)" |
|
| 347 |
+ digitsOnly |
|
| 348 |
+ placeholder="입금계좌번호 (숫자만)" |
|
| 344 | 349 |
maxLength={ACCOUNT_NUMBER_MAX_LENGTH}
|
| 345 | 350 |
invalid={Boolean(errors.accountNumber)}
|
| 346 | 351 |
message={errors.accountNumber}
|
--- lib/domain/merchant-detail-form.ts
+++ lib/domain/merchant-detail-form.ts
... | ... | @@ -15,7 +15,7 @@ |
| 15 | 15 |
export const ADDRESS_MAX_LENGTH = 200; |
| 16 | 16 |
const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/; |
| 17 | 17 |
const POSTAL_CODE_PATTERN = /^\d{5}$/;
|
| 18 |
-const PHONE_PATTERN = /^[\d-]+$/; |
|
| 18 |
+const PHONE_PATTERN = /^\d+$/; |
|
| 19 | 19 |
|
| 20 | 20 |
/** 상세 화면이 함께 다루는 업체(가맹점) 항목 — 백엔드 `MngrVocBaseVo`의 일부. */ |
| 21 | 21 |
export type MerchantDetailMerchantValues = {
|
... | ... | @@ -91,7 +91,7 @@ |
| 91 | 91 |
if (!phoneNumber) {
|
| 92 | 92 |
errors.phoneNumber = '업체 연락처를 입력해 주세요.'; |
| 93 | 93 |
} else if (!PHONE_PATTERN.test(phoneNumber)) {
|
| 94 |
- errors.phoneNumber = '연락처는 숫자와 -만 입력할 수 있습니다.'; |
|
| 94 |
+ errors.phoneNumber = '연락처는 숫자만 입력할 수 있습니다.'; |
|
| 95 | 95 |
} |
| 96 | 96 |
|
| 97 | 97 |
const postalCode = values.postalCode.trim(); |
--- lib/domain/merchant-form.ts
+++ lib/domain/merchant-form.ts
... | ... | @@ -4,6 +4,7 @@ |
| 4 | 4 |
export const ADDRESS_DETAIL_MAX_LENGTH = 200; |
| 5 | 5 |
export const MEMO_MAX_LENGTH = 500; |
| 6 | 6 |
export const ACCOUNT_NUMBER_MAX_LENGTH = 30; |
| 7 |
+export const BUSINESS_NUMBER_LENGTH = 10; |
|
| 7 | 8 |
|
| 8 | 9 |
export interface MerchantValues {
|
| 9 | 10 |
id: string; |
... | ... | @@ -45,8 +46,7 @@ |
| 45 | 46 |
|
| 46 | 47 |
export const REQUIRED_MESSAGE = '필수입력 사항을 입력하세요.'; |
| 47 | 48 |
|
| 48 |
-/** 사업자등록번호는 선택이지만, 넣었다면 형태를 지킨다(기획 예시 `000-00-00000`). */ |
|
| 49 |
-const BUSINESS_NUMBER_PATTERN = /^\d{3}-?\d{2}-?\d{5}$/;
|
|
| 49 |
+const BUSINESS_NUMBER_PATTERN = new RegExp(`^\\d{${BUSINESS_NUMBER_LENGTH}}$`);
|
|
| 50 | 50 |
|
| 51 | 51 |
type ValidationResult = |
| 52 | 52 |
| { ok: true; values: MerchantValues }
|
... | ... | @@ -69,8 +69,8 @@ |
| 69 | 69 |
const phoneNumber = values.phoneNumber.trim(); |
| 70 | 70 |
if (!phoneNumber) {
|
| 71 | 71 |
errors.phoneNumber = REQUIRED_MESSAGE; |
| 72 |
- } else if (!/^[\d-]+$/.test(phoneNumber)) {
|
|
| 73 |
- errors.phoneNumber = '전화번호는 숫자와 -만 입력할 수 있습니다.'; |
|
| 72 |
+ } else if (!/^\d+$/.test(phoneNumber)) {
|
|
| 73 |
+ errors.phoneNumber = '전화번호는 숫자만 입력할 수 있습니다.'; |
|
| 74 | 74 |
} |
| 75 | 75 |
|
| 76 | 76 |
if (!values.address.trim()) {
|
... | ... | @@ -84,12 +84,12 @@ |
| 84 | 84 |
|
| 85 | 85 |
const businessNumber = values.businessNumber.trim(); |
| 86 | 86 |
if (businessNumber && !BUSINESS_NUMBER_PATTERN.test(businessNumber)) {
|
| 87 |
- errors.businessNumber = '사업자등록번호는 000-00-00000 형식으로 입력해 주세요.'; |
|
| 87 |
+ errors.businessNumber = `사업자등록번호는 숫자 ${BUSINESS_NUMBER_LENGTH}자리로 입력해 주세요.`;
|
|
| 88 | 88 |
} |
| 89 | 89 |
|
| 90 | 90 |
const accountNumber = values.accountNumber.trim(); |
| 91 |
- if (accountNumber && !/^[\d-]+$/.test(accountNumber)) {
|
|
| 92 |
- errors.accountNumber = '계좌번호는 숫자와 -만 입력할 수 있습니다.'; |
|
| 91 |
+ if (accountNumber && !/^\d+$/.test(accountNumber)) {
|
|
| 92 |
+ errors.accountNumber = '계좌번호는 숫자만 입력할 수 있습니다.'; |
|
| 93 | 93 |
} |
| 94 | 94 |
|
| 95 | 95 |
if (Object.keys(errors).length > 0) {
|
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?