"use client"; import { FoxInput, FoxInputState, FoxSelect, FoxSelectItem, FoxSelectOption, FoxSelectOptionItem } from "@/@fox/core/components"; import { useState, type CSSProperties, type ReactNode } from "react"; import { FoxButton } from "../core/components/fox-button"; import { FoxButtonGroup } from "../core/components/fox-button-group"; import { FoxButtonPanel } from "../core/components/fox-button-panel"; import { FoxIconButton } from "../core/components/fox-icon-button"; import { FoxLinkButton } from "../core/components/fox-link-button"; import { FoxAddress, type FoxAddressSearchResult, type FoxAddressValue } from "../core/components/fox-address"; import { FoxCertified } from "../core/components/fox-certified"; import { FoxConditionalOption } from "../core/components/fox-conditional-option"; import { FoxEmail } from "../core/components/fox-email"; import { FoxFormLabel } from "../core/components/fox-form-label"; import { FoxFileUpload, type FoxFileItem } from "../core/components/fox-file-upload"; import { FoxPhoneNumber, type FoxPhoneNumberState } from "../core/components/fox-phone-number"; import { FoxTextArea, type FoxTextAreaState } from "../core/components/fox-text-area"; import { FoxTextButton } from "../core/components/fox-text-button"; import { FoxRadio } from "../core/components/fox-radio"; import { FoxRadioGroup } from "../core/components/fox-radio-group"; import { FoxRadioIcon } from "../core/components/fox-radio-icon"; import styles from "./dev-test.module.scss"; export interface ComponentExample { /** 해시 링크에 쓰이는 kebab-case 식별자. */ id: string; name: string; description?: string; /** 눈으로 비교해야 하는 조합을 빠짐없이 넣는다 — 예제가 곧 회귀 확인 수단이다. */ variants: { label: string; node: ReactNode }[]; } // 자리 확인용 도형. `currentColor`로 그려야 계열별 색이 입혀진다. style은 FoxSelect // 힌트 아이콘처럼 크기를 줄여 써야 하는 자리에 쓴다. function DemoIcon({ style }: { style?: CSSProperties } = {}) { return ( ); } function DemoStar() { return ( ); } // overlay 계열은 어두운 반투명 배경이라 밝은 바탕 위에서는 대비를 볼 수 없다. function OnPhoto({ children }: { children: ReactNode }) { return ( {children} ); } // 입력 예제는 폭이 있어야 시안(360px)처럼 보인다. function InputStage({ children, wide = false }: { children: ReactNode; wide?: boolean }) { return
{children}
; } // 지우기 버튼과 자동 상태 판정을 실제로 눌러 보려면 예제가 값을 들고 있어야 한다. function InputLiveDemo() { const [text, setText] = useState("지우기를 눌러보세요"); return ( <> ); } // 카운터가 실제로 올라가는 걸 보려면 예제가 값을 들고 있어야 한다. function TextAreaLiveDemo() { const [text, setText] = useState("입력하면 아래 숫자가 따라 올라갑니다."); return ( <> ); } const PHONE_PREFIXES = ["010", "011", "016", "017", "018", "019", "02", "031", "051"]; // 값이 숫자만 담긴 문자열 하나라는 걸 보려면 예제가 그 값을 그대로 보여줘야 한다. function PhoneNumberLiveDemo() { const [unit, setUnit] = useState("01012345678"); const [combine, setCombine] = useState("0212345678"); return ( <> ); } const EMAIL_DOMAINS = ["naver.com", "gmail.com", "daum.net", "kakao.com"]; // 값이 합쳐진 문자열 하나라는 걸 보려면 예제가 그 값을 그대로 보여줘야 한다. function EmailLiveDemo() { const [email, setEmail] = useState("hong@naver.com"); return ( ); } // 어떤 우편번호 서비스를 쓰든 이 모양으로 돌려주면 된다 — 여기서는 지연만 흉내낸다. const FAKE_SEARCH_RESULT: FoxAddressSearchResult = { zipCode: "06236", address: "서울 강남구 테헤란로 152", detail: "강남파이낸스센터", }; function AddressLiveDemo({ cancel = false }) { const [value, setValue] = useState({ zipCode: "", address: "", detail: "" }); return ( <> { await new Promise((resolve) => setTimeout(resolve, 400)); return cancel ? null : FAKE_SEARCH_RESULT; }} />
{JSON.stringify(value, null, 2)}
); } // 목록은 업로드 결과에 따라 바뀌므로, 오류 줄까지 보려면 예제가 값을 들고 있어야 한다. const SAMPLE_FILES: FoxFileItem[] = [ { id: "a", name: "사업자등록증", meta: ".PDF (20KB)" }, { id: "b", name: "아주 긴 파일 이름이라 가운데에서 말줄임되어야 하는 첨부물", meta: ".XLSX (1.2MB)" }, { id: "c", name: "용량초과파일", meta: ".ZIP (32MB)", error: true, message: "등록 가능한 파일 용량을 초과하였습니다. 20MB 미만의 파일만 등록할 수 있습니다.", }, ]; function FileUploadLiveDemo({ mode }: { mode: "default" | "area" }) { const [files, setFiles] = useState(SAMPLE_FILES); return ( setFiles((prev) => [ ...prev, ...picked.map((file) => ({ id: file.name, name: file.name, meta: `(${file.size}B)` })), ]) } onRemove={(id) => setFiles((prev) => prev.filter((item) => item.id !== id))} description={mode === "default" ? "JPG, PNG 파일만 가능 (최대 5MB)" : undefined} multiple /> ); } // 이벤트가 실제로 도는 걸 보려면 예제가 결과를 적어 둬야 한다. 서버 대신 지연만 흉내낸다. function CertifiedLiveDemo({ failSend = false, failVerify = false }) { const [log, setLog] = useState([]); const write = (line: string) => setLog((prev) => [line, ...prev].slice(0, 5)); const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); return ( <> { await wait(400); if (failSend) throw new Error("발송 한도를 넘었습니다"); write(`보내기 요청 → ${phone}`); }} onVerify={async (_phone, code) => { await wait(400); if (failVerify || code !== "123456") throw new Error("번호가 맞지 않습니다"); }} onSendSuccess={() => write("onSendSuccess")} onSendFail={(error) => write(`onSendFail — ${(error as Error).message}`)} onVerifySuccess={() => write("onVerifySuccess — 이제 전부 잠깁니다")} onVerifyFail={(error) => write(`onVerifyFail — ${(error as Error).message}`)} onExpire={() => write("onExpire")} />
{log.length ? log.join("\n") : "이벤트가 여기에 쌓입니다 (인증번호는 123456)"}
); } // active는 호출부가 소유하므로 켜고 끄는 걸 보려면 예제가 상태를 들고 있어야 한다. function IconToggleDemo() { const [on, setOn] = useState(false); return ( <> } active={on} onAction={() => setOn((prev) => !prev)} /> } active={on} onAction={() => setOn((prev) => !prev)} /> } active={on} onAction={() => setOn((prev) => !prev)} /> ); } const SELECT_ITEMS: FoxSelectItem[] = [ { value: "a", label: "옵션 A" }, { value: "b", label: "옵션 B" }, { value: "c", label: "옵션 C" }, ]; // 옵션 아이콘은 별도 목록으로 둔다 — 위 목록에 섞으면 크기·상태 예제가 전부 같이 바뀌어 // 무엇을 보는 예제인지 흐려진다. 아이콘은 시안 미정이라 자리 확인용 도형이다. const SELECT_ITEMS_RICH: FoxSelectItem[] = [ { value: "a", label: "아이콘 있는 옵션", icon: }, { value: "b", label: "아이콘 없는 옵션" }, { value: "c", label: "아주 긴 옵션 라벨 — 트리거에서는 말줄임으로 잘린다" }, ]; /** 컴포넌트를 만들 때마다 항목 하나를 추가한다 — 다른 파일은 고치지 않는다. */ export const COMPONENT_EXAMPLES: ComponentExample[] = [ { id: "fox-button", name: "FoxButton", description: "Figma 시안(btn, 컴포넌트 세트 106:1606)을 토큰만으로 옮겼습니다. 너비는 내용에 따라 가변이고 높이만 고정입니다. hover·pressed는 마우스를 올리거나 눌러 확인하세요. error 계열은 시안 미정이라 비어 있습니다.", variants: [ { label: "계열 (type) — hover·pressed 확인 가능", node: ( <> ), }, { label: "크기 (size)", node: ( <> ), }, { label: "아이콘", node: ( <> } /> } /> } trailingIcon={} /> ), }, { label: "상태 — disabled / loading (둘 다 클릭이 막힌다)", node: ( <> undefined} /> } /> ), }, { label: "fullWidth", node: , }, { label: "htmlType — 폼 안에서의 역할", node: ( <> ), }, { label: "hidden — 렌더되지 않는다(아래는 비어 있는 것이 정상)", node: