--- app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
... | ... | @@ -39,6 +39,7 @@ |
| 39 | 39 |
} from '../_actions'; |
| 40 | 40 |
import { AdminMemberFormFields } from './admin-member-form-fields';
|
| 41 | 41 |
import styles from './admin-member-modal.module.scss'; |
| 42 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 42 | 43 |
|
| 43 | 44 |
interface AdminMemberCreateModalProps {
|
| 44 | 45 |
onClose: () => void; |
... | ... | @@ -159,7 +160,7 @@ |
| 159 | 160 |
</FoxButtonGroup> |
| 160 | 161 |
} |
| 161 | 162 |
> |
| 162 |
- <form ref={formRef} action={formAction} className={styles.formList}>
|
|
| 163 |
+ <form ref={formRef} onSubmit={(event) => submitFormAction(event, formAction)} className={styles.formList}>
|
|
| 163 | 164 |
<FoxInput |
| 164 | 165 |
size="md" |
| 165 | 166 |
name="name" |
--- app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
... | ... | @@ -23,6 +23,7 @@ |
| 23 | 23 |
import { updateAdminMemberAction } from '../_actions';
|
| 24 | 24 |
import { AdminMemberFormFields } from './admin-member-form-fields';
|
| 25 | 25 |
import styles from './admin-member-modal.module.scss'; |
| 26 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 26 | 27 |
|
| 27 | 28 |
interface AdminMemberEditModalProps {
|
| 28 | 29 |
member: AdminMember; |
... | ... | @@ -100,7 +101,7 @@ |
| 100 | 101 |
</FoxButtonGroup> |
| 101 | 102 |
} |
| 102 | 103 |
> |
| 103 |
- <form ref={formRef} action={formAction} className={styles.formList}>
|
|
| 104 |
+ <form ref={formRef} onSubmit={(event) => submitFormAction(event, formAction)} className={styles.formList}>
|
|
| 104 | 105 |
{/* 수정 대상을 가리키는 유일한 입력. 이름·ID는 읽기 전용이라 아예 제출하지 않는다. */}
|
| 105 | 106 |
<input type="hidden" name="id" value={member.id} />
|
| 106 | 107 |
|
--- app/(protected)/(basic)/boards/_components/board-post-form-modal.tsx
+++ app/(protected)/(basic)/boards/_components/board-post-form-modal.tsx
... | ... | @@ -24,6 +24,7 @@ |
| 24 | 24 |
import { createBoardPostAction, updateBoardPostAction } from '../_actions';
|
| 25 | 25 |
import { BoardAttachmentField } from './board-attachment-field';
|
| 26 | 26 |
import { BoardContentField } from './board-content-field';
|
| 27 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 27 | 28 |
|
| 28 | 29 |
interface BoardPostFormModalProps {
|
| 29 | 30 |
boardType: BoardType; |
... | ... | @@ -115,7 +116,7 @@ |
| 115 | 116 |
</> |
| 116 | 117 |
} |
| 117 | 118 |
> |
| 118 |
- <form id={FORM_ID} action={formAction} className="flex flex-col gap-4">
|
|
| 119 |
+ <form id={FORM_ID} onSubmit={(event) => submitFormAction(event, formAction)} className="flex flex-col gap-4">
|
|
| 119 | 120 |
<input type="hidden" name="boardType" value={boardType} />
|
| 120 | 121 |
{!isCreate && post && <input type="hidden" name="id" value={post.id} />}
|
| 121 | 122 |
|
--- app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
+++ app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
... | ... | @@ -21,6 +21,7 @@ |
| 21 | 21 |
import { BoardAttachmentField } from '../../_components/board-attachment-field';
|
| 22 | 22 |
import { BoardContentField } from '../../_components/board-content-field';
|
| 23 | 23 |
import { FieldError } from '../../_components/board-post-form-modal';
|
| 24 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 24 | 25 |
|
| 25 | 26 |
interface InquiryAnswerModalProps {
|
| 26 | 27 |
post: BoardPost; |
... | ... | @@ -81,7 +82,7 @@ |
| 81 | 82 |
</> |
| 82 | 83 |
} |
| 83 | 84 |
> |
| 84 |
- <form id={FORM_ID} action={formAction} className="flex flex-col gap-4">
|
|
| 85 |
+ <form id={FORM_ID} onSubmit={(event) => submitFormAction(event, formAction)} className="flex flex-col gap-4">
|
|
| 85 | 86 |
<input type="hidden" name="id" value={post.id} />
|
| 86 | 87 |
|
| 87 | 88 |
<Field label="질문유형"> |
--- app/(protected)/(basic)/decoration-items/_components/decoration-item-create-modal.tsx
+++ app/(protected)/(basic)/decoration-items/_components/decoration-item-create-modal.tsx
... | ... | @@ -11,6 +11,7 @@ |
| 11 | 11 |
import { INITIAL_DECORATION_ITEM_FORM_STATE } from '@/lib/domain/decoration-item-form';
|
| 12 | 12 |
import { createDecorationItemAction } from '../_actions';
|
| 13 | 13 |
import { DecorationItemFormFields } from './decoration-item-form-fields';
|
| 14 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 14 | 15 |
|
| 15 | 16 |
interface DecorationItemCreateModalProps {
|
| 16 | 17 |
/** 등록 버튼을 누른 시점의 활성 탭(유형) — 폼의 유형 기본값으로 쓴다. */ |
... | ... | @@ -73,7 +74,7 @@ |
| 73 | 74 |
</> |
| 74 | 75 |
} |
| 75 | 76 |
> |
| 76 |
- <form ref={formRef} action={formAction}>
|
|
| 77 |
+ <form ref={formRef} onSubmit={(event) => submitFormAction(event, formAction)}>
|
|
| 77 | 78 |
<DecorationItemFormFields |
| 78 | 79 |
defaultItemType={defaultItemType}
|
| 79 | 80 |
typeTotalCount={typeTotalCount}
|
--- app/(protected)/(basic)/decoration-items/_components/decoration-item-edit-modal.tsx
+++ app/(protected)/(basic)/decoration-items/_components/decoration-item-edit-modal.tsx
... | ... | @@ -11,6 +11,7 @@ |
| 11 | 11 |
import { INITIAL_DECORATION_ITEM_FORM_STATE } from '@/lib/domain/decoration-item-form';
|
| 12 | 12 |
import { updateDecorationItemAction } from '../_actions';
|
| 13 | 13 |
import { DecorationItemFormFields } from './decoration-item-form-fields';
|
| 14 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 14 | 15 |
|
| 15 | 16 |
interface DecorationItemEditModalProps {
|
| 16 | 17 |
item: DecorationItem; |
... | ... | @@ -69,7 +70,7 @@ |
| 69 | 70 |
</> |
| 70 | 71 |
} |
| 71 | 72 |
> |
| 72 |
- <form ref={formRef} action={formAction}>
|
|
| 73 |
+ <form ref={formRef} onSubmit={(event) => submitFormAction(event, formAction)}>
|
|
| 73 | 74 |
<input type="hidden" name="itemSn" value={item.itemSn} />
|
| 74 | 75 |
|
| 75 | 76 |
<DecorationItemFormFields |
--- app/(protected)/(basic)/system/codes/_components/code-detail-modal.tsx
+++ app/(protected)/(basic)/system/codes/_components/code-detail-modal.tsx
... | ... | @@ -10,6 +10,7 @@ |
| 10 | 10 |
import { INITIAL_COMMON_CODE_FORM_STATE } from '@/lib/domain/common-code-form';
|
| 11 | 11 |
import { createCodeDetailAction, updateCodeDetailAction } from '../_actions';
|
| 12 | 12 |
import styles from './code-form.module.scss'; |
| 13 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 13 | 14 |
|
| 14 | 15 |
interface CodeDetailModalProps {
|
| 15 | 16 |
/** 소속 공통코드 — 좌측에서 고른 값이라 팝업에서는 바꿀 수 없다. */ |
... | ... | @@ -76,7 +77,7 @@ |
| 76 | 77 |
</> |
| 77 | 78 |
} |
| 78 | 79 |
> |
| 79 |
- <form ref={formRef} action={formAction} className={styles.fields}>
|
|
| 80 |
+ <form ref={formRef} onSubmit={(event) => submitFormAction(event, formAction)} className={styles.fields}>
|
|
| 80 | 81 |
<input type="hidden" name={isEdit ? 'targetComCd' : 'comCd'} value={comCd} />
|
| 81 | 82 |
{isEdit && (
|
| 82 | 83 |
<input |
--- app/(protected)/(basic)/system/codes/_components/code-group-modal.tsx
+++ app/(protected)/(basic)/system/codes/_components/code-group-modal.tsx
... | ... | @@ -17,6 +17,7 @@ |
| 17 | 17 |
import { INITIAL_COMMON_CODE_FORM_STATE } from '@/lib/domain/common-code-form';
|
| 18 | 18 |
import { createCodeGroupAction, updateCodeGroupAction } from '../_actions';
|
| 19 | 19 |
import styles from './code-form.module.scss'; |
| 20 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 20 | 21 |
|
| 21 | 22 |
interface CodeGroupModalProps {
|
| 22 | 23 |
/** 수정 대상. 없으면 등록 팝업이다. */ |
... | ... | @@ -80,7 +81,7 @@ |
| 80 | 81 |
</> |
| 81 | 82 |
} |
| 82 | 83 |
> |
| 83 |
- <form ref={formRef} action={formAction} className={styles.fields}>
|
|
| 84 |
+ <form ref={formRef} onSubmit={(event) => submitFormAction(event, formAction)} className={styles.fields}>
|
|
| 84 | 85 |
{/* 수정 대상은 읽기 전용 칸이 아니라 이 값으로 정해진다 — 칸이 위조돼도 대상은 안 바뀐다. */}
|
| 85 | 86 |
{isEdit && (
|
| 86 | 87 |
<input type="hidden" name="targetComCd" value={group.comCd} />
|
--- app/(public)/login/_components/login-form.tsx
+++ app/(public)/login/_components/login-form.tsx
... | ... | @@ -6,6 +6,7 @@ |
| 6 | 6 |
import { FoxEyeIcon, FoxEyeSlashIcon } from '@fox/core/icons';
|
| 7 | 7 |
import { login, type LoginFormState } from '../_actions';
|
| 8 | 8 |
import styles from '../login.module.scss'; |
| 9 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 9 | 10 |
|
| 10 | 11 |
const initialState: LoginFormState = {};
|
| 11 | 12 |
|
... | ... | @@ -35,7 +36,7 @@ |
| 35 | 36 |
const passwordId = useId(); |
| 36 | 37 |
|
| 37 | 38 |
return ( |
| 38 |
- <form action={formAction} className={styles.fields}>
|
|
| 39 |
+ <form onSubmit={(event) => submitFormAction(event, formAction)} className={styles.fields}>
|
|
| 39 | 40 |
<FoxInput |
| 40 | 41 |
size="lg" |
| 41 | 42 |
label="ID" |
+++ app/_hooks/submit-form-action.ts
... | ... | @@ -0,0 +1,24 @@ |
| 1 | +'use client'; | |
| 2 | + | |
| 3 | +import { startTransition, type FormEvent } from 'react'; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * `<form action={formAction}>` 대신 쓰는 제출 핸들러. | |
| 7 | + * | |
| 8 | + * **React는 폼 액션이 끝나면 성공·실패를 가리지 않고 폼을 리셋한다.** 그래서 검증 오류가 하나만 | |
| 9 | + * 나도 사용자가 채운 값이 전부 날아가고, 특히 `type="file"` 입력은 프로그램으로 되채울 수 없어 | |
| 10 | + * 다음 제출에서는 "이미지를 등록해 주세요"만 반복된다(꾸미기 아이템 등록에서 실제로 그랬다). | |
| 11 | + * | |
| 12 | + * 액션을 `action` 프롭에 맡기지 않고 transition 안에서 직접 부르면 그 리셋이 일어나지 않는다. | |
| 13 | + * 확인: 같은 액션을 두 방식으로 제출했을 때 `action={...}` 쪽만 입력값이 빈 문자열이 됐다. | |
| 14 | + * | |
| 15 | + * 성공 후 폼을 비워야 하는 화면은 각자 팝업을 닫으므로(다음에 열 때 새 폼이다) 리셋이 필요 없다. | |
| 16 | + */ | |
| 17 | +export function submitFormAction( | |
| 18 | + event: FormEvent<HTMLFormElement>, | |
| 19 | + formAction: (formData: FormData) => void | |
| 20 | +): void { | |
| 21 | + event.preventDefault(); | |
| 22 | + const formData = new FormData(event.currentTarget); | |
| 23 | + startTransition(() => formAction(formData)); | |
| 24 | +} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?