feat: Implement validation error message display for required fields

- Added a new CSS class for displaying validation error messages below required input fields without affecting layout.
- Enhanced the `useDialogAutoValidation` hook to insert error messages dynamically when required fields are empty.
- Implemented logic to remove error messages when the input is cleared, improving user feedback during form validation.

Made-with: Cursor
This commit is contained in:
syc0123 2026-03-04 09:23:09 +09:00
parent 35dfe5bd79
commit cfd49020a0
3 changed files with 41 additions and 2 deletions

View File

@ -441,4 +441,21 @@ select {
border-color: hsl(var(--destructive)) !important;
}
/* 필수 입력 경고 문구 (입력 필드 아래, 레이아웃 영향 없음) */
.validation-error-msg-wrapper {
height: 0;
overflow: visible;
position: relative;
}
.validation-error-msg-wrapper > p {
position: absolute;
top: 1px;
left: 0;
font-size: 11px;
color: hsl(var(--destructive));
white-space: nowrap;
pointer-events: none;
}
/* ===== End of Global Styles ===== */

View File

@ -20,7 +20,6 @@ const DRAG_THRESHOLD = 5;
const SETTLE_MS = 70;
const DROP_SETTLE_MS = 180;
const BAR_PAD_X = 8;
const ACTIVE_TAB_BORDER_OPACITY = 0.3;
interface DragState {
tabId: string;
@ -502,7 +501,7 @@ export function TabBar() {
touchAction: "none",
...animStyle,
...(hiddenByGhost ? { opacity: 0 } : {}),
...(isActive ? { borderColor: `rgba(0,0,0,${ACTIVE_TAB_BORDER_OPACITY})` } : {}),
...(isActive ? { boxShadow: "0 -1px 28px rgba(0,0,0,0.9)" } : {}),
}}
title={tab.title}
>

View File

@ -6,6 +6,7 @@ import { toast } from "sonner";
const HIGHLIGHT_ATTR = "data-validation-highlight";
const ERROR_ATTR = "data-validation-error";
const MSG_WRAPPER_CLASS = "validation-error-msg-wrapper";
type TargetEl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;
@ -121,11 +122,32 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
function markError(input: TargetEl) {
input.setAttribute(ERROR_ATTR, "true");
errorFields.add(input);
showErrorMsg(input);
}
function clearError(input: TargetEl) {
input.removeAttribute(ERROR_ATTR);
errorFields.delete(input);
removeErrorMsg(input);
}
// 빈 필수 필드 아래에 경고 문구 삽입 (레이아웃 영향 없는 zero-height wrapper)
function showErrorMsg(input: TargetEl) {
if (input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
const wrapper = document.createElement("div");
wrapper.className = MSG_WRAPPER_CLASS;
const msg = document.createElement("p");
msg.textContent = "필수 입력 항목입니다";
wrapper.appendChild(msg);
input.insertAdjacentElement("afterend", wrapper);
}
function removeErrorMsg(input: TargetEl) {
const wrapper = input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
if (wrapper) wrapper.remove();
}
function highlightField(input: TargetEl) {
@ -200,6 +222,7 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
el.querySelectorAll(`[${HIGHLIGHT_ATTR}]`).forEach((node) => node.removeAttribute(HIGHLIGHT_ATTR));
el.querySelectorAll(`[${ERROR_ATTR}]`).forEach((node) => node.removeAttribute(ERROR_ATTR));
el.querySelectorAll(`.${MSG_WRAPPER_CLASS}`).forEach((node) => node.remove());
};
}, [mode, contentEl]);
}