임동욱 임동욱 08-18
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
+++ app/(protected)/(basic)/students/_components/student-detail-modal.tsx
@@ -8,6 +8,7 @@
 } from '@fox/core/components/fox-description-list';
 import { FoxModal } from '@fox/core/components/fox-modal';
 import { FoxToggleSwitch } from '@fox/core/components/fox-toggle-switch';
+import { formatPhoneNumber } from '@/lib/domain/phone-number';
 import {
   formatGradeClassNumber,
   formatOptionalValue,
@@ -74,7 +75,7 @@
     {
       key: 'phoneNumber',
       term: '휴대전화 번호',
-      description: formatOptionalValue(member.phoneNumber),
+      description: formatOptionalValue(formatPhoneNumber(member.phoneNumber)),
     },
     {
       key: 'email',
@@ -89,7 +90,10 @@
     {
       key: 'guardian',
       term: '보호자 이름 / 연락처',
-      description: joinPair(member.guardianName, member.guardianPhoneNumber),
+      description: joinPair(
+        member.guardianName,
+        formatPhoneNumber(member.guardianPhoneNumber)
+      ),
     },
     {
       key: 'school',
app/(protected)/(basic)/students/_components/student-list.tsx
--- app/(protected)/(basic)/students/_components/student-list.tsx
+++ app/(protected)/(basic)/students/_components/student-list.tsx
@@ -13,6 +13,7 @@
   FoxArrowsDownUpIcon,
   FoxDownloadSimpleIcon,
 } from '@fox/core/icons';
+import { formatPhoneNumber } from '@/lib/domain/phone-number';
 import type { StudentMember } from '@/lib/domain/student-member';
 import {
   STUDENT_MEMBERS_EXCEL_PATH,
@@ -94,7 +95,7 @@
       key: 'phoneNumber',
       header: '휴대전화번호',
       width: 160,
-      render: (row) => text(row.phoneNumber),
+      render: (row) => text(formatPhoneNumber(row.phoneNumber)),
     },
     { key: 'email', header: '이메일', width: 200, render: (row) => text(row.email) },
     {
@@ -124,7 +125,7 @@
       key: 'guardianPhoneNumber',
       header: '보호자연락처',
       width: 160,
-      render: (row) => text(row.guardianPhoneNumber),
+      render: (row) => text(formatPhoneNumber(row.guardianPhoneNumber)),
     },
     {
       key: 'joinedAt',
 
lib/domain/phone-number.ts (added)
+++ lib/domain/phone-number.ts
@@ -0,0 +1,59 @@
+/**
+ * 전화번호 표기.
+ *
+ * 백엔드는 하이픈 없이 숫자만 저장한다(예: `01011233400`). 시안은 `010-1123-3400`으로
+ * 끊어 보여주므로 화면에 나가기 직전에 여기서 끊는다 — 저장된 값 자체는 건드리지 않는다.
+ *
+ * 도메인마다 두지 않고 한 파일에 모은 이유는 학생·관리자·보호자가 같은 규칙을 써야 하기
+ * 때문이다.
+ */
+
+/** 자릿수별 묶음 규칙. 앞자리가 `02`인 서울 번호만 지역번호가 두 자리다. */
+function splitGroups(digits: string): string[] | null {
+  if (digits.startsWith('02')) {
+    if (digits.length === 9) {
+      return [digits.slice(0, 2), digits.slice(2, 5), digits.slice(5)];
+    }
+    if (digits.length === 10) {
+      return [digits.slice(0, 2), digits.slice(2, 6), digits.slice(6)];
+    }
+    return null;
+  }
+
+  if (digits.length === 11) {
+    return [digits.slice(0, 3), digits.slice(3, 7), digits.slice(7)];
+  }
+  if (digits.length === 10) {
+    return [digits.slice(0, 3), digits.slice(3, 6), digits.slice(6)];
+  }
+  // 지역번호 없이 저장된 국번+번호.
+  if (digits.length === 8) {
+    return [digits.slice(0, 4), digits.slice(4)];
+  }
+
+  return null;
+}
+
+/**
+ * 하이픈을 넣어 돌려준다.
+ *
+ * - 이미 하이픈이 들어 있으면 그대로 둔다 — 백엔드가 형식을 갖춰 준 값을 다시 끊지 않는다.
+ * - 자릿수가 알려진 규칙에 맞지 않으면 **원본을 그대로** 돌려준다. 임의로 끊으면 잘못된
+ *   번호를 그럴듯하게 보여주게 되므로, 판단이 서지 않을 때는 손대지 않는다.
+ * - 값이 없으면 null 그대로다. `-` 표기는 호출부의 `formatOptionalValue`가 맡는다.
+ */
+export function formatPhoneNumber(value: string | null): string | null {
+  if (value === null) {
+    return null;
+  }
+
+  const trimmed = value.trim();
+  if (trimmed === '' || trimmed.includes('-')) {
+    return trimmed === '' ? null : trimmed;
+  }
+
+  const digits = trimmed.replace(/\D/g, '');
+  const groups = digits.length === trimmed.length ? splitGroups(digits) : null;
+
+  return groups === null ? trimmed : groups.join('-');
+}
Add a comment
List