feat: Implement automatic validation for modal forms

- Introduced a new hook `useDialogAutoValidation` to handle automatic validation of required fields in modals.
- Added visual feedback for empty required fields, including red borders and error messages.
- Disabled action buttons when required fields are not filled, enhancing user experience.
- Updated `DialogContent` to integrate the new validation logic, ensuring that only user mode modals are validated.

Made-with: Cursor
This commit is contained in:
2026-03-03 12:07:12 +09:00
parent eb471d087f
commit aa020bfdd8
4 changed files with 402 additions and 3 deletions

View File

@@ -44,7 +44,14 @@ function Button({
}) {
const Comp = asChild ? Slot : "button";
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
return (
<Comp
data-slot="button"
data-variant={variant || "default"}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
import { useModalPortal } from "@/lib/modalPortalRef";
import { useTabId } from "@/contexts/TabIdContext";
import { useTabStore } from "@/stores/tabStore";
import { useDialogAutoValidation } from "@/lib/hooks/useDialogAutoValidation";
// Dialog: 탭 시스템 내에서 자동으로 modal={false} + 비활성 탭이면 open={false} 처리
const Dialog: React.FC<React.ComponentProps<typeof DialogPrimitive.Root>> = ({
@@ -82,6 +83,18 @@ const DialogContent = React.forwardRef<
const container = explicitContainer !== undefined ? explicitContainer : autoContainer;
const scoped = !!container;
// 모달 자동 검증용 내부 ref
const internalRef = React.useRef<HTMLDivElement>(null);
const mergedRef = React.useCallback(
(node: HTMLDivElement | null) => {
internalRef.current = node;
if (typeof ref === "function") ref(node);
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
},
[ref],
);
useDialogAutoValidation(internalRef);
const handleInteractOutside = React.useCallback(
(e: any) => {
if (scoped && container) {
@@ -125,7 +138,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Overlay className="fixed inset-0 z-999 bg-black/60" />
)}
<DialogPrimitive.Content
ref={ref}
ref={mergedRef}
onInteractOutside={handleInteractOutside}
onFocusOutside={handleFocusOutside}
className={cn(
@@ -156,7 +169,7 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 shrink-0", className)} {...props} />
<div data-slot="dialog-footer" className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 shrink-0", className)} {...props} />
);
DialogFooter.displayName = "DialogFooter";