feat: 전화번호를 하이픈 표기로 — 목록·조회 팝업
백엔드는 `USER_TELNO`를 하이픈 없이 숫자열로 준다(`01012345678`). 시안은 `010-1234-5678`로 끊어 보여주므로 화면에 나가기 직전에 끊는다 — 저장된 값 자체는 건드리지 않는다. lib/domain/phone-number.ts를 새로 뒀다. 학생·관리자·보호자가 같은 규칙을 써야 해서 도메인마다 두지 않고 한 곳에 모았다(지금은 학생 화면만 쓴다). 규칙 — 휴대전화 11자리는 3-4-4, 10자리는 3-3-4, 서울(02)은 9자리 2-3-4·10자리 2-4-4, 지역번호 없는 8자리는 4-4다. **판단이 서지 않으면 손대지 않는다**: 이미 하이픈이 있거나 자릿수가 규칙에 맞지 않으면 원본을 그대로 돌려준다 — 임의로 끊으면 잘못된 번호를 그럴듯하게 보여주게 된다. 적용 — 목록의 휴대전화번호·보호자연락처, 조회 팝업의 휴대전화 번호·보호자 연락처. 검증 — 함수를 직접 돌려 확인했다. 01011233400→010-1123-3400, 0111234567→011-123-4567, 0212345678→02-1234-5678, 021234567→02-123-4567, 12345678→1234-5678, 이미 하이픈 있는 값과 123·14자리는 원본 유지, 빈 문자열·공백은 null. 화면에서도 01012345678이 010-1234-5678로, 보호자가 010-2222-1234로 나온다. Co-Authored-By: Claude Opus 5
@f131241a77d8a9a2d245d483637a5fc0e7e2c718
--- app/(protected)/(basic)/students/_components/student-detail-modal.tsx
+++ app/(protected)/(basic)/students/_components/student-detail-modal.tsx
... | ... | @@ -8,6 +8,7 @@ |
| 8 | 8 |
} from '@fox/core/components/fox-description-list'; |
| 9 | 9 |
import { FoxModal } from '@fox/core/components/fox-modal';
|
| 10 | 10 |
import { FoxToggleSwitch } from '@fox/core/components/fox-toggle-switch';
|
| 11 |
+import { formatPhoneNumber } from '@/lib/domain/phone-number';
|
|
| 11 | 12 |
import {
|
| 12 | 13 |
formatGradeClassNumber, |
| 13 | 14 |
formatOptionalValue, |
... | ... | @@ -74,7 +75,7 @@ |
| 74 | 75 |
{
|
| 75 | 76 |
key: 'phoneNumber', |
| 76 | 77 |
term: '휴대전화 번호', |
| 77 |
- description: formatOptionalValue(member.phoneNumber), |
|
| 78 |
+ description: formatOptionalValue(formatPhoneNumber(member.phoneNumber)), |
|
| 78 | 79 |
}, |
| 79 | 80 |
{
|
| 80 | 81 |
key: 'email', |
... | ... | @@ -89,7 +90,10 @@ |
| 89 | 90 |
{
|
| 90 | 91 |
key: 'guardian', |
| 91 | 92 |
term: '보호자 이름 / 연락처', |
| 92 |
- description: joinPair(member.guardianName, member.guardianPhoneNumber), |
|
| 93 |
+ description: joinPair( |
|
| 94 |
+ member.guardianName, |
|
| 95 |
+ formatPhoneNumber(member.guardianPhoneNumber) |
|
| 96 |
+ ), |
|
| 93 | 97 |
}, |
| 94 | 98 |
{
|
| 95 | 99 |
key: 'school', |
--- app/(protected)/(basic)/students/_components/student-list.tsx
+++ app/(protected)/(basic)/students/_components/student-list.tsx
... | ... | @@ -13,6 +13,7 @@ |
| 13 | 13 |
FoxArrowsDownUpIcon, |
| 14 | 14 |
FoxDownloadSimpleIcon, |
| 15 | 15 |
} from '@fox/core/icons'; |
| 16 |
+import { formatPhoneNumber } from '@/lib/domain/phone-number';
|
|
| 16 | 17 |
import type { StudentMember } from '@/lib/domain/student-member';
|
| 17 | 18 |
import {
|
| 18 | 19 |
STUDENT_MEMBERS_EXCEL_PATH, |
... | ... | @@ -94,7 +95,7 @@ |
| 94 | 95 |
key: 'phoneNumber', |
| 95 | 96 |
header: '휴대전화번호', |
| 96 | 97 |
width: 160, |
| 97 |
- render: (row) => text(row.phoneNumber), |
|
| 98 |
+ render: (row) => text(formatPhoneNumber(row.phoneNumber)), |
|
| 98 | 99 |
}, |
| 99 | 100 |
{ key: 'email', header: '이메일', width: 200, render: (row) => text(row.email) },
|
| 100 | 101 |
{
|
... | ... | @@ -124,7 +125,7 @@ |
| 124 | 125 |
key: 'guardianPhoneNumber', |
| 125 | 126 |
header: '보호자연락처', |
| 126 | 127 |
width: 160, |
| 127 |
- render: (row) => text(row.guardianPhoneNumber), |
|
| 128 |
+ render: (row) => text(formatPhoneNumber(row.guardianPhoneNumber)), |
|
| 128 | 129 |
}, |
| 129 | 130 |
{
|
| 130 | 131 |
key: 'joinedAt', |
+++ lib/domain/phone-number.ts
... | ... | @@ -0,0 +1,59 @@ |
| 1 | +/** | |
| 2 | + * 전화번호 표기. | |
| 3 | + * | |
| 4 | + * 백엔드는 하이픈 없이 숫자만 저장한다(예: `01011233400`). 시안은 `010-1123-3400`으로 | |
| 5 | + * 끊어 보여주므로 화면에 나가기 직전에 여기서 끊는다 — 저장된 값 자체는 건드리지 않는다. | |
| 6 | + * | |
| 7 | + * 도메인마다 두지 않고 한 파일에 모은 이유는 학생·관리자·보호자가 같은 규칙을 써야 하기 | |
| 8 | + * 때문이다. | |
| 9 | + */ | |
| 10 | + | |
| 11 | +/** 자릿수별 묶음 규칙. 앞자리가 `02`인 서울 번호만 지역번호가 두 자리다. */ | |
| 12 | +function splitGroups(digits: string): string[] | null { | |
| 13 | + if (digits.startsWith('02')) { | |
| 14 | + if (digits.length === 9) { | |
| 15 | + return [digits.slice(0, 2), digits.slice(2, 5), digits.slice(5)]; | |
| 16 | + } | |
| 17 | + if (digits.length === 10) { | |
| 18 | + return [digits.slice(0, 2), digits.slice(2, 6), digits.slice(6)]; | |
| 19 | + } | |
| 20 | + return null; | |
| 21 | + } | |
| 22 | + | |
| 23 | + if (digits.length === 11) { | |
| 24 | + return [digits.slice(0, 3), digits.slice(3, 7), digits.slice(7)]; | |
| 25 | + } | |
| 26 | + if (digits.length === 10) { | |
| 27 | + return [digits.slice(0, 3), digits.slice(3, 6), digits.slice(6)]; | |
| 28 | + } | |
| 29 | + // 지역번호 없이 저장된 국번+번호. | |
| 30 | + if (digits.length === 8) { | |
| 31 | + return [digits.slice(0, 4), digits.slice(4)]; | |
| 32 | + } | |
| 33 | + | |
| 34 | + return null; | |
| 35 | +} | |
| 36 | + | |
| 37 | +/** | |
| 38 | + * 하이픈을 넣어 돌려준다. | |
| 39 | + * | |
| 40 | + * - 이미 하이픈이 들어 있으면 그대로 둔다 — 백엔드가 형식을 갖춰 준 값을 다시 끊지 않는다. | |
| 41 | + * - 자릿수가 알려진 규칙에 맞지 않으면 **원본을 그대로** 돌려준다. 임의로 끊으면 잘못된 | |
| 42 | + * 번호를 그럴듯하게 보여주게 되므로, 판단이 서지 않을 때는 손대지 않는다. | |
| 43 | + * - 값이 없으면 null 그대로다. `-` 표기는 호출부의 `formatOptionalValue`가 맡는다. | |
| 44 | + */ | |
| 45 | +export function formatPhoneNumber(value: string | null): string | null { | |
| 46 | + if (value === null) { | |
| 47 | + return null; | |
| 48 | + } | |
| 49 | + | |
| 50 | + const trimmed = value.trim(); | |
| 51 | + if (trimmed === '' || trimmed.includes('-')) { | |
| 52 | + return trimmed === '' ? null : trimmed; | |
| 53 | + } | |
| 54 | + | |
| 55 | + const digits = trimmed.replace(/\D/g, ''); | |
| 56 | + const groups = digits.length === trimmed.length ? splitGroups(digits) : null; | |
| 57 | + | |
| 58 | + return groups === null ? trimmed : groups.join('-'); | |
| 59 | +} |
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?