From 7794a3dd8c783f738b2577a3e104ff832bbe13af Mon Sep 17 00:00:00 2001
From: Johngreen
Date: Sun, 8 Mar 2026 23:53:12 +0900
Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A7=A4=EC=9E=A5=20=EB=93=B1=EB=A1=9D?=
=?UTF-8?q?=20=ED=8F=BC=20=EC=97=90=EB=9F=AC=20=EC=8B=9C=20=EC=9E=85?=
=?UTF-8?q?=EB=A0=A5=EA=B0=92=20=EC=9C=A0=EC=A7=80=20(useActionState=20?=
=?UTF-8?q?=EC=A0=81=EC=9A=A9)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- server action을 actions.ts로 분리
- page.tsx를 client component로 전환 (useActionState)
- 에러 시 redirect 대신 state 반환 → 입력값 유지
- 제출 중 버튼 비활성화 (isPending)
---
apps/web/src/app/stores/new/actions.ts | 73 ++++++++++++++++++
apps/web/src/app/stores/new/page.tsx | 103 ++++---------------------
2 files changed, 90 insertions(+), 86 deletions(-)
create mode 100644 apps/web/src/app/stores/new/actions.ts
diff --git a/apps/web/src/app/stores/new/actions.ts b/apps/web/src/app/stores/new/actions.ts
new file mode 100644
index 0000000..86165c4
--- /dev/null
+++ b/apps/web/src/app/stores/new/actions.ts
@@ -0,0 +1,73 @@
+'use server';
+
+import { redirect } from 'next/navigation';
+import { createPrismaClient } from '@startover/database';
+import { auth } from '@/lib/auth';
+import { createStoreDraftService } from '@/services/store-service';
+import type { CreateStoreDraftInput } from '@startover/domain';
+
+export type StoreFormState = {
+ error?: string;
+};
+
+export async function createStoreDraftAction(
+ _prevState: StoreFormState,
+ formData: FormData,
+): Promise {
+ const session = await auth();
+ if (!session?.user?.dbId) {
+ return { error: '로그인이 필요합니다.' };
+ }
+
+ const listingTitle = (formData.get('listingTitle') as string | null)?.trim() ?? '';
+ const regionClusterCode = (formData.get('regionClusterCode') as string | null) ?? '';
+ const industryLeafCode = (formData.get('industryLeafCode') as string | null) ?? '';
+ const roadAddress = (formData.get('roadAddress') as string | null)?.trim() ?? '';
+ const depositAmount = formData.get('depositAmount') as string | null;
+ const monthlyRentAmount = formData.get('monthlyRentAmount') as string | null;
+ const premiumAmount = formData.get('premiumAmount') as string | null;
+ const remainingLeaseMonths = formData.get('remainingLeaseMonths') as string | null;
+ const exclusiveAreaSqm = formData.get('exclusiveAreaSqm') as string | null;
+ const floorLevel = formData.get('floorLevel') as string | null;
+ const kitchenEquipmentSummary =
+ (formData.get('kitchenEquipmentSummary') as string | null)?.trim() ?? '';
+
+ const input: CreateStoreDraftInput = {
+ ownerUserId: session.user.dbId,
+ listingTitle,
+ industryLeafCode,
+ regionClusterCode,
+ roadAddress,
+ ...(depositAmount || monthlyRentAmount || premiumAmount || remainingLeaseMonths
+ ? {
+ lease: {
+ depositAmount: depositAmount ? Number(depositAmount) : 0,
+ monthlyRentAmount: monthlyRentAmount ? Number(monthlyRentAmount) : 0,
+ premiumAmount: premiumAmount ? Number(premiumAmount) : 0,
+ remainingLeaseMonths: remainingLeaseMonths
+ ? parseInt(remainingLeaseMonths, 10)
+ : undefined,
+ },
+ }
+ : {}),
+ ...(exclusiveAreaSqm || floorLevel || kitchenEquipmentSummary
+ ? {
+ facility: {
+ exclusiveAreaSqm: exclusiveAreaSqm ? Number(exclusiveAreaSqm) : 0,
+ seatCount: 0,
+ floorLevel: floorLevel ? parseInt(floorLevel, 10) : undefined,
+ kitchenEquipmentSummary: kitchenEquipmentSummary || undefined,
+ },
+ }
+ : {}),
+ };
+
+ const prisma = createPrismaClient();
+ const result = await createStoreDraftService(prisma, input);
+
+ if (!result.ok) {
+ return { error: result.error.message };
+ }
+
+ redirect(`/stores/${result.value.publicId}`);
+}
diff --git a/apps/web/src/app/stores/new/page.tsx b/apps/web/src/app/stores/new/page.tsx
index a964e39..39adcaf 100644
--- a/apps/web/src/app/stores/new/page.tsx
+++ b/apps/web/src/app/stores/new/page.tsx
@@ -1,83 +1,19 @@
+'use client';
+
+import { useActionState } from 'react';
import Link from 'next/link';
-import { redirect } from 'next/navigation';
-import { createPrismaClient } from '@startover/database';
-import { auth } from '@/lib/auth';
-import { createStoreDraftService } from '@/services/store-service';
-import type { CreateStoreDraftInput } from '@startover/domain';
+import { createStoreDraftAction, type StoreFormState } from './actions';
-export const dynamic = 'force-dynamic';
+const initialState: StoreFormState = {};
-async function createStoreDraftAction(formData: FormData) {
- 'use server';
+export default function NewStorePage() {
+ const [state, formAction, isPending] = useActionState(createStoreDraftAction, initialState);
- const session = await auth();
- if (!session?.user?.dbId) {
- redirect('/auth/signin');
- }
-
- const listingTitle = (formData.get('listingTitle') as string | null)?.trim() ?? '';
- const regionClusterCode = (formData.get('regionClusterCode') as string | null) ?? '';
- const industryLeafCode = (formData.get('industryLeafCode') as string | null) ?? '';
- const roadAddress = (formData.get('roadAddress') as string | null)?.trim() ?? '';
- const depositAmount = formData.get('depositAmount') as string | null;
- const monthlyRentAmount = formData.get('monthlyRentAmount') as string | null;
- const premiumAmount = formData.get('premiumAmount') as string | null;
- const remainingLeaseMonths = formData.get('remainingLeaseMonths') as string | null;
- const exclusiveAreaSqm = formData.get('exclusiveAreaSqm') as string | null;
- const floorLevel = formData.get('floorLevel') as string | null;
- const kitchenEquipmentSummary =
- (formData.get('kitchenEquipmentSummary') as string | null)?.trim() ?? '';
-
- const input: CreateStoreDraftInput = {
- ownerUserId: session.user.dbId,
- listingTitle,
- industryLeafCode,
- regionClusterCode,
- roadAddress,
- ...(depositAmount || monthlyRentAmount || premiumAmount || remainingLeaseMonths
- ? {
- lease: {
- depositAmount: depositAmount ? Number(depositAmount) : 0,
- monthlyRentAmount: monthlyRentAmount ? Number(monthlyRentAmount) : 0,
- premiumAmount: premiumAmount ? Number(premiumAmount) : 0,
- remainingLeaseMonths: remainingLeaseMonths
- ? parseInt(remainingLeaseMonths, 10)
- : undefined,
- },
- }
- : {}),
- ...(exclusiveAreaSqm || floorLevel || kitchenEquipmentSummary
- ? {
- facility: {
- exclusiveAreaSqm: exclusiveAreaSqm ? Number(exclusiveAreaSqm) : 0,
- seatCount: 0,
- floorLevel: floorLevel ? parseInt(floorLevel, 10) : undefined,
- kitchenEquipmentSummary: kitchenEquipmentSummary || undefined,
- },
- }
- : {}),
- };
-
- const prisma = createPrismaClient();
- const result = await createStoreDraftService(prisma, input);
-
- if (!result.ok) {
- redirect(`/stores/new?error=${encodeURIComponent(result.error.message)}`);
- }
-
- redirect(`/stores/${result.value.publicId}`);
-}
-
-export default function NewStorePage({
- searchParams,
-}: {
- searchParams: Promise<{ error?: string }>;
-}) {
return (
- ← 매장 목록으로
+ ← 매장 목록으로
@@ -86,9 +22,13 @@ export default function NewStorePage({
매장 정보를 등록하면 창업자, 철거업체, 인테리어업체와 매칭됩니다
-
+ {state.error && (
+
+ {state.error}
+
+ )}
-