/** * Mandu
Component * Progressive Enhancement: JS 없으면 일반 HTML form, JS 있으면 fetch 기반 제출 */ import { useState, useCallback, useRef, type FormEvent, type ReactNode } from "react"; import { submitAction, type ActionResult } from "./router"; export interface FormState { submitting: boolean; error: string | null; } export interface FormProps extends Omit, "action" | "children"> { /** 제출 대상 URL */ action: string; /** Action 이름 (서버의 filling.action(name)과 매칭) */ actionName?: string; /** HTTP 메서드 */ method?: "post" | "put" | "patch" | "delete"; /** JS에서 fetch 방식으로 전환 (기본: true, false면 일반 HTML form 제출) */ enhance?: boolean; /** Action 성공 후 콜백 */ onActionSuccess?: (result: ActionResult) => void; /** Action 실패 후 콜백 */ onActionError?: (error: Error) => void; /** render props 또는 일반 children */ children: ReactNode | ((state: FormState) => ReactNode); } /** * Progressive Enhancement Form 컴포넌트 * * @example * ```tsx * * * * * * // render props로 제출 상태 접근 *
* {({ submitting, error }) => ( * <> * * * {error &&

{error}

} * * )} *
* ``` */ export function Form({ action, actionName = "default", method = "post", enhance = true, onActionSuccess, onActionError, children, ...rest }: FormProps) { const [state, setState] = useState({ submitting: false, error: null }); const submittingRef = useRef(false); const formMethod = method === "post" ? "post" : "post"; const handleSubmit = useCallback(async (e: FormEvent) => { if (!enhance) return; if (submittingRef.current) return; // 이중 제출 방지 e.preventDefault(); submittingRef.current = true; setState({ submitting: true, error: null }); try { const formData = new FormData(e.currentTarget); const result = await submitAction(action, formData, actionName, method); if (result.ok) { setState({ submitting: false, error: null }); onActionSuccess?.(result); } else { const message = "요청이 실패했습니다."; setState({ submitting: false, error: message }); onActionError?.(new Error(message)); } } catch (error) { const message = error instanceof Error ? error.message : "요청 실패"; setState({ submitting: false, error: message }); onActionError?.(error instanceof Error ? error : new Error(message)); } finally { submittingRef.current = false; } }, [action, actionName, enhance, method, onActionSuccess, onActionError]); return (
{method !== "post" && } {typeof children === "function" ? children(state) : children}
); }