"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 { FoxCheckIcon } from "../core/components/fox-check-icon";
import { FoxCheckbox } from "../core/components/fox-checkbox";
import { FoxCheckboxGroup } from "../core/components/fox-checkbox-group";
import { FoxChip, type FoxChipItem } from "../core/components/fox-chip";
import { FoxChipArea } from "../core/components/fox-chip-area";
import { FoxSelectText } from "../core/components/fox-select-text";
import { FoxSegmentedControl } from "../core/components/fox-segmented-control";
import { FoxSegmentedControlItem } from "../core/components/fox-segmented-control-item";
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 { FoxTag } from "../core/components/fox-tag";
import { FoxTagList } from "../core/components/fox-tag-list";
import { FoxToggleQuantity } from "../core/components/fox-toggle-quantity";
import { FoxToggleSwitch } from "../core/components/fox-toggle-switch";
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 (
);
}
// FoxChip select 계열의 앞 아이콘 자리를 확인하기 위한 달력 도형(시안이 그 자리에 달력을
// 쓴다). `currentColor`로 그려야 상태별 글자 색을 따라간다.
function DemoCalendar() {
return (
);
}
const CHIP_PERIOD_OPTIONS: FoxChipItem[] = [
{ value: "7d", label: "최근 7일" },
{ value: "1m", label: "최근 1개월" },
{ value: "3m", label: "최근 3개월" },
{ value: "custom", label: "직접 입력" },
];
const SELECT_TEXT_MONTHS: FoxSelectItem[] = [
{ value: "2026-06", label: "2026년 6월" },
{ value: "2026-07", label: "2026년 7월" },
{ value: "2026-08", label: "2026년 8월" },
];
const SELECT_TEXT_SORTS: FoxSelectItem[] = [
{ value: "latest", label: "최신순" },
{ value: "oldest", label: "오래된순" },
{ value: "amount", label: "금액 높은순" },
];
// 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 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}
/>
>
);
}
/** 컴포넌트를 만들 때마다 항목 하나를 추가한다 — 다른 파일은 고치지 않는다. */
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: ,
},
],
},
{
id: "fox-text-button",
name: "FoxTextButton",
description:
"Figma 시안(btn-txt, 컴포넌트 세트 106:2468)을 토큰만으로 옮겼습니다. 높이를 고정하지 않고 패딩과 line-height로만 크기를 잡습니다. lg만 상하 패딩과 간격이 한 단계 넓습니다.",
variants: [
{
label: "계열 (type) — primary는 굵기가 한 단계 더 굵다",
node: (
<>
>
),
},
{
label: "크기 (size)",
node: (
<>
>
),
},
{
label: "아이콘",
node: (
<>
} />
} />
}
trailingIcon={}
/>
>
),
},
{
label: "상태 — disabled",
node: (
<>
undefined} />
>
),
},
],
},
{
id: "fox-link-button",
name: "FoxLinkButton",
description:
"Figma 시안(btn-lnk, 컴포넌트 세트 107:207)을 토큰만으로 옮겼습니다. href를 주면 , 없으면