"use client"; import { useEffect, useId, useRef, useState, type ChangeEvent } from "react"; import { FoxFormLabel } from "../fox-form-label"; import { cx } from "../../utils"; import { FoxButton } from "../fox-button"; import { FoxPhoneNumber } from "../fox-phone-number"; export interface FoxCertifiedProps { /** 전화번호. 숫자만 담긴 문자열이다 — `FoxPhoneNumber`와 같다. */ phone?: string; defaultPhone?: string; onPhoneChange?: (phone: string) => void; /** 전화번호 앞자리 목록. 밖에서 받은 값을 칸으로 나눌 때도 쓴다. */ prefixOptions?: string[]; /** * 인증번호 받기. resolve하면 성공으로 보고 인증번호 칸과 확인 버튼을 연다. * throw하거나 reject하면 실패로 보고 `onSendFail`을 부른다. */ onSend: (phone: string) => Promise; /** 인증번호 확인. resolve하면 성공, throw·reject하면 실패다. */ onVerify: (phone: string, code: string) => Promise; onSendSuccess?: () => void; onSendFail?: (error: unknown) => void; onVerifySuccess?: () => void; onVerifyFail?: (error: unknown) => void; /** 남은 시간이 0이 됐을 때 한 번 부른다. */ onExpire?: () => void; /** * 제한 시간(초). 주면 인증번호를 보낸 시점부터 컴포넌트가 직접 센다. * `remainingSeconds`를 함께 주면 그쪽이 이긴다. */ expiresIn?: number; /** 남은 시간(초)을 직접 셀 때. 주면 컴포넌트는 세지 않고 이 값만 그린다. */ remainingSeconds?: number; label?: string; sendLabel?: string; verifyLabel?: string; codePlaceholder?: string; codeMaxLength?: number; /** 전체 잠금. 인증에 성공하면 이 값과 무관하게 잠긴다. */ disabled?: boolean; /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ hidden?: boolean; /** 배치 조정용. */ className?: string; } /** 시안의 `24:16` 표기. 분이 두 자리를 넘어가면 그대로 늘어난다. */ function formatTimer(seconds: number): string { const safe = Math.max(0, Math.floor(seconds)); const minutes = Math.floor(safe / 60); const rest = safe % 60; return `${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`; } /** 시안의 ico/ClockCountdown 자리. @fox에 아이콘 세트가 없어 같은 모양을 직접 그린다. */ function TimerIcon() { return