"use client"; import { FoxInput, FoxInputState, FoxSelect, FoxSelectItem, FoxSelectOption, FoxSelectOptionItem } from "@/@fox/core/components"; import { useState, type CSSProperties, type ReactNode } from "react"; import { FoxAddress, type FoxAddressSearchResult, type FoxAddressValue } from "../core/components/fox-address"; import { FoxBreadcrumb } from "../core/components/fox-breadcrumb"; import { FoxButton } from "../core/components/fox-button"; import { FoxButtonGroup } from "../core/components/fox-button-group"; import { FoxButtonPanel } from "../core/components/fox-button-panel"; import { FoxCarouselArea } from "../core/components/fox-carousel-area"; import { FoxCarouselButton } from "../core/components/fox-carousel-button"; import { FoxCarouselIndicator } from "../core/components/fox-carousel-indicator"; import { FoxCertified } from "../core/components/fox-certified"; import { FoxCheckIcon } from "../core/components/fox-check-icon"; import { FoxCheckbox } from "../core/components/fox-checkbox"; import { FoxCheckboxGroup } from "../core/components/fox-checkbox-group"; import { FoxConditionalOption } from "../core/components/fox-conditional-option"; import { FoxEmail } from "../core/components/fox-email"; import { FoxFileUpload, type FoxFileItem } from "../core/components/fox-file-upload"; import { FoxFormLabel } from "../core/components/fox-form-label"; import { FoxIconButton } from "../core/components/fox-icon-button"; import { FoxLinkButton } from "../core/components/fox-link-button"; import { FoxPagination } from "../core/components/fox-pagination"; import { FoxPhoneNumber, type FoxPhoneNumberState } from "../core/components/fox-phone-number"; import { FoxRadio } from "../core/components/fox-radio"; import { FoxRadioGroup } from "../core/components/fox-radio-group"; import { FoxRadioIcon } from "../core/components/fox-radio-icon"; import { FoxTag } from "../core/components/fox-tag"; import { FoxTagList } from "../core/components/fox-tag-list"; import { FoxTextArea, type FoxTextAreaState } from "../core/components/fox-text-area"; import { FoxTextButton } from "../core/components/fox-text-button"; import { FoxToggleSwitch } from "../core/components/fox-toggle-switch"; import { FoxCaretLeftIcon, FoxCaretUpDownIcon, FoxDotsThreeVerticalIcon, FoxCaretRightIcon, FoxPauseIcon, FoxPlayIcon, FoxPlusIcon, } from "../core/icons"; import { FoxBulletList } from "../core/components/fox-bullet-list"; import { FoxBulletListItem } from "../core/components/fox-bullet-list-item"; import { FoxBadge } from "../core/components/fox-badge"; import { FoxBadgeDot } from "../core/components/fox-badge-dot"; import { FoxBadgeGroup } from "../core/components/fox-badge-group"; import { FoxTable } from "../core/components/fox-table"; import { FoxTableCell } from "../core/components/fox-table-cell"; import { FoxTableRow } from "../core/components/fox-table-row"; import { FoxTab } from "../core/components/fox-tab"; import { FoxTabItem } from "../core/components/fox-tab-item"; 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: "아주 긴 옵션 라벨 — 트리거에서는 말줄임으로 잘린다" }, ]; const BREADCRUMB_ITEMS = [ { label: "홈", href: "#" }, { label: "1Depth", href: "#" }, { label: "2Depth", href: "#" }, { label: "3Depth" }, ]; function PaginationLiveDemo() { const [page, setPage] = useState(1); return (

현재 {page} / 12 페이지

); } function CarouselDotPickDemo() { const [page, setPage] = useState(1); return ; } function CarouselLiveDemo() { const total = 5; const [page, setPage] = useState(1); const [playing, setPlaying] = useState(true); const move = (step: number) => setPage((prev) => ((prev - 1 + step + total) % total) + 1); return ( } onAction={() => move(-1)} /> } onAction={() => move(1)} /> : } onAction={() => setPlaying((prev) => !prev)} /> ); } // 태그 목록은 상태를 갖지 않으므로, 닫기 아이콘이 실제로 뭔가를 떼는지 보려면 예제가 목록을 // 들고 있어야 한다. 전부 떼면 다시 채울 방법이 없어 초기화 버튼을 같이 둔다. const TAG_FILTERS = ["서울", "경기", "인천", "부산"]; function TagListLiveDemo() { const [filters, setFilters] = useState(TAG_FILTERS); return ( <> {filters.map((name) => ( setFilters((prev) => prev.filter((it) => it !== name))} /> ))} setFilters(TAG_FILTERS)} disabled={filters.length === TAG_FILTERS.length} /> ); } const BADGE_COLORS = [ "neutral", "primary", "secondary", "accent", "danger", "warning", "success", "information", "disabled", ] as const; /** 시안의 9색 × 3계열 표를 그대로 펼친다 — 눈으로 비교해야 하는 조합이라 빠짐없이 낸다. */ function BadgeMatrix({ size }: { size: "lg" | "md" | "sm" }) { return (
{(["outline", "solid", "pastel"] as const).map((type) => (
{BADGE_COLORS.map((color) => ( 레이블 ))}
))}
); } /** 시안 tbl-list(699:6304) 그대로 — 체크 열·정렬 머리·제목 강조·버튼·더보기·고른 줄. */ function TableListSample() { const [selected, setSelected] = useState([2]); const rows = [1, 2, 3]; const allOn = selected.length === rows.length; const toggle = (n: number) => setSelected((prev) => (prev.includes(n) ? prev.filter((v) => v !== n) : [...prev, n])); return ( 0 && !allOn} onChange={(on) => setSelected(on ? rows : [])} /> 제목 제목 제목 제목 {rows.map((n) => ( toggle(n)} /> 내용 내용 내용 } /> ))} ); } /** 시안 tab(391:4497) — 계열·크기 조합을 눌러 확인한다. */ function TabLiveDemo({ type, size }: { type: "line" | "fill"; size: "lg" | "md" }) { const [value, setValue] = useState("a"); return ( label label 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: