"use client"; import { useId, useState, type ReactNode } from "react"; import { FoxFormLabel } from "../fox-form-label"; import { cx } from "../../utils"; import { FoxButton } from "../fox-button"; import { FoxInput } from "../fox-input"; /** * 주소 검색 결과. 호출부가 어떤 우편번호 서비스를 쓰든 이 모양으로 바꿔서 돌려준다 — * 그래야 `@fox`가 특정 API에 묶이지 않는다. */ export interface FoxAddressSearchResult { /** 우편번호. 첫 칸에 그대로 들어간다. */ zipCode: string; /** 도로명 또는 지번 주소. 둘째 칸에 그대로 들어간다. 어느 쪽을 쓸지는 호출부가 정한다. */ address: string; /** * 상세주소 칸에 미리 채울 값(건물명 같은 참고항목). * 넘기지 않으면 사용자가 적어 둔 상세주소를 건드리지 않는다. */ detail?: string; } /** 주소 한 벌. 화면의 세 칸을 그대로 담는다. */ export interface FoxAddressValue { zipCode: string; address: string; detail: string; } export const EMPTY_ADDRESS: FoxAddressValue = { zipCode: "", address: "", detail: "" }; export interface FoxAddressProps { value?: FoxAddressValue; defaultValue?: FoxAddressValue; onChange?: (value: FoxAddressValue) => void; /** * 주소 검색. 호출부가 우편번호 서비스를 띄우고 결과를 `FoxAddressSearchResult`로 * 돌려준다. 사용자가 고르지 않고 닫으면 `null`을 돌려주면 되고, 그때는 값이 그대로 * 남는다. throw·reject하면 `onSearchFail`을 부른다. */ onSearch: () => Promise; onSearchSuccess?: (result: FoxAddressSearchResult) => void; onSearchFail?: (error: unknown) => void; searchLabel?: string; label?: string; /** 헬퍼 메시지. 오류일 때 색이 바뀐다. */ message?: string; messageIcon?: ReactNode; zipCodePlaceholder?: string; addressPlaceholder?: string; detailPlaceholder?: string; invalid?: boolean; disabled?: boolean; /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ hidden?: boolean; /** 배치 조정용. */ className?: string; } /** * @fox 주소 입력. 우편번호와 주소는 검색 결과로만 채워지고, 상세주소만 손으로 적는다. * * 어떤 우편번호 서비스를 쓸지는 `@fox`가 알 필요가 없다 — 호출부가 `onSearch`에서 그 * 서비스를 띄우고 결과를 `FoxAddressSearchResult` 모양으로 바꿔서 돌려주면 된다. * * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` * (또는 개별 파티셜)로 한 번 불러와야 한다. */ export function FoxAddress({ value, defaultValue, onChange, onSearch, onSearchSuccess, onSearchFail, searchLabel = "주소 검색", label, message, messageIcon, zipCodePlaceholder = "우편번호", addressPlaceholder = "주소", detailPlaceholder = "상세주소", invalid = false, disabled = false, hidden = false, className, }: FoxAddressProps) { const autoId = useId(); const [inner, setInner] = useState(defaultValue ?? EMPTY_ADDRESS); const [searching, setSearching] = useState(false); if (hidden) { return null; } const current = value ?? inner; const messageId = `${autoId}-message`; const commit = (next: FoxAddressValue) => { if (value === undefined) { setInner(next); } onChange?.(next); }; const handleSearch = async () => { if (searching) { return; } setSearching(true); try { const result = await onSearch(); // 취소하면 `null`이 온다 — 그때는 값이 그대로 남는다. if (result) { commit({ zipCode: result.zipCode, address: result.address, detail: result.detail ?? current.detail, }); onSearchSuccess?.(result); } } catch (error) { onSearchFail?.(error); } finally { setSearching(false); } }; return (
{label && ( {label} )}
{/* 검색 결과로만 채워진다 — 손으로 고칠 수 없다. */}
commit({ ...current, detail: next })} placeholder={detailPlaceholder} invalid={invalid} disabled={disabled} /> {message && (

{messageIcon && ( )} {message}

)}
); }