"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)"}