요구사항 반영: 1. NAV 재정렬: 매장검색/등록 → 철거/폐업 → 매칭/지원금/업체/블로그/FAQ 2. /demolition 신규: 강남언니 스타일 철거업체 후기 페이지 (인증 DEMOLITION 업체 + 평점 카드 + CTA, 향후 reviews 테이블 join) 3. /closure 신규: 폐업 5단계 가이드 (의사결정→양도→지원금→철거→신고) 4. 매장 상세에 "수정" 버튼 추가 (소유자 + DRAFT/REJECTED 상태에서만 노출) 5. /stores/[id]/edit 신규: 기존 데이터 만원 단위로 prefill, 업데이트 액션 (StoreLease/Sale/Facility upsert, 권한·상태 검증, 만원→원 변환) 기존 매장 등록 폼 placeholder도 만원 단위 (b1a07e7로 이미 적용됨).
This commit is contained in:
@@ -0,0 +1,184 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: '폐업 가이드 | Startover',
|
||||||
|
description:
|
||||||
|
'폐업 신고부터 권리 양도, 정부 지원금 신청, 철거·원상복구까지 — 한 번에 정리되는 폐업 절차 가이드.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STEPS = [
|
||||||
|
{
|
||||||
|
no: '01',
|
||||||
|
title: '폐업 의사결정',
|
||||||
|
body: '월매출·월수익·임대 잔여기간·권리금을 입력하면 양도(권리금 회수) vs 폐업(원상복구비 발생) 시나리오를 비교해드립니다.',
|
||||||
|
href: '/stores/new',
|
||||||
|
cta: '시뮬레이션 시작',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: '02',
|
||||||
|
title: '매장 등록 → 양도 우선 시도',
|
||||||
|
body: '폐업하기 전에 인수자 매칭이 되면 권리금 회수 + 원상복구 비용 절감이 동시에 가능합니다. 운영팀이 2~4주 내 매칭을 시도합니다.',
|
||||||
|
href: '/stores/new',
|
||||||
|
cta: '매장 등록',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: '03',
|
||||||
|
title: '정부 지원금 자격 조회',
|
||||||
|
body: '희망리턴패키지·재도전장려금·소상공인 지원금까지 자격 자동 조회 + 서류 체크리스트.',
|
||||||
|
href: '/subsidies',
|
||||||
|
cta: '지원금 확인',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: '04',
|
||||||
|
title: '철거·원상복구',
|
||||||
|
body: '인증 철거업체로 견적 동시 발송. 분진·소음·일정·잔금 투명도 후기로 검증된 업체만 매칭됩니다.',
|
||||||
|
href: '/demolition',
|
||||||
|
cta: '철거 업체 후기',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: '05',
|
||||||
|
title: '폐업 신고 + 세무 정리',
|
||||||
|
body: '국세청 홈택스 폐업 신고, 부가세 확정신고, 소득세 신고 일정과 필요 서류를 단계별로 안내합니다.',
|
||||||
|
href: '/blog?tag=폐업신고',
|
||||||
|
cta: '실무 가이드 보기',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SUPPORT_LINKS = [
|
||||||
|
{ label: '희망리턴패키지', href: '/subsidies?id=hope-return' },
|
||||||
|
{ label: '재도전장려금', href: '/subsidies?id=re-challenge' },
|
||||||
|
{ label: '소상공인 위기극복자금', href: '/subsidies' },
|
||||||
|
{ label: '점포 원상복구 컨설팅', href: '/contact' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ClosurePage() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-6xl px-6 py-10 font-body">
|
||||||
|
{/* HERO */}
|
||||||
|
<section className="rounded-3xl border border-ink/5 bg-white p-8 md:p-12">
|
||||||
|
<p className="font-mono text-[11px] tracking-[0.25em] uppercase text-ink-muted">
|
||||||
|
<span style={{ color: '#03C75A' }}>◆</span> Closure · Guide
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-3 font-display text-3xl font-extrabold tracking-tight text-ink md:text-4xl">
|
||||||
|
깔끔한 마무리,
|
||||||
|
<br className="hidden md:block" />
|
||||||
|
<span style={{ color: '#03C75A' }}>다음으로 가는 길</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-ink-light md:text-base">
|
||||||
|
폐업은 끝이 아닌 정리입니다. 양도 시도 → 지원금 신청 → 철거·원상복구 →
|
||||||
|
폐업 신고까지, Startover가 한 흐름으로 묶어드립니다.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<Link
|
||||||
|
href="/stores/new"
|
||||||
|
className="rounded-full px-5 py-2.5 text-sm font-semibold text-white"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg,#03C75A 0%,#02A149 55%,#018f40 100%)',
|
||||||
|
boxShadow: '0 8px 20px -8px rgba(3,199,90,0.45)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
매장 등록하고 양도 우선 시도 →
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/subsidies"
|
||||||
|
className="rounded-full border border-ink/15 bg-white px-5 py-2.5 text-sm font-semibold text-ink hover:border-ink/40"
|
||||||
|
>
|
||||||
|
지원금 자격 조회
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* STEPS */}
|
||||||
|
<section className="mt-10">
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink">5단계 흐름</h2>
|
||||||
|
<p className="mt-1 text-xs text-ink-muted">
|
||||||
|
어느 단계에서든 시작할 수 있습니다 — 본인 상황에 맞는 단계부터 진행하세요.
|
||||||
|
</p>
|
||||||
|
<ol className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
{STEPS.map((s) => (
|
||||||
|
<li
|
||||||
|
key={s.no}
|
||||||
|
className="card-lift rounded-2xl border border-ink/5 bg-white p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span
|
||||||
|
className="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl font-mono text-sm font-bold text-white"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg,#03C75A 0%,#02A149 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.no}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-display text-lg font-bold text-ink">{s.title}</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-ink-light">{s.body}</p>
|
||||||
|
<Link
|
||||||
|
href={s.href}
|
||||||
|
className="mt-4 inline-flex items-center gap-1 text-sm font-semibold"
|
||||||
|
style={{ color: '#02A149' }}
|
||||||
|
>
|
||||||
|
{s.cta} →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* SUPPORT QUICK LINKS */}
|
||||||
|
<section className="mt-10 rounded-3xl border border-ink/5 bg-white p-8">
|
||||||
|
<h2 className="font-display text-lg font-bold text-ink">정부 지원 · 컨설팅</h2>
|
||||||
|
<p className="mt-1 text-xs text-ink-muted">
|
||||||
|
폐업 단계별로 신청 가능한 정부 지원 프로그램과 무료 컨설팅을 모았습니다.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 flex flex-wrap gap-2">
|
||||||
|
{SUPPORT_LINKS.map((l) => (
|
||||||
|
<Link
|
||||||
|
key={l.href}
|
||||||
|
href={l.href}
|
||||||
|
className="rounded-full border border-ink/10 bg-white px-3.5 py-1.5 text-xs font-semibold text-ink hover:border-ink/30"
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section
|
||||||
|
className="mt-10 rounded-3xl border border-ink/5 p-8 md:p-10"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg,rgba(3,199,90,0.08) 0%,rgba(2,161,73,0.04) 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink md:text-2xl">
|
||||||
|
폐업 결정이 망설여지시나요?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-ink-light">
|
||||||
|
매장 등록만 해두시면 운영팀이 2~4주 내 매수자 매칭을 먼저 시도합니다.
|
||||||
|
매칭이 되면 권리금 회수 + 철거비 절감으로 폐업 비용 자체가 사라질 수 있습니다.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 flex flex-wrap gap-3">
|
||||||
|
<Link
|
||||||
|
href="/stores/new"
|
||||||
|
className="rounded-full px-5 py-2.5 text-sm font-semibold text-white"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg,#03C75A 0%,#02A149 55%,#018f40 100%)',
|
||||||
|
boxShadow: '0 8px 20px -8px rgba(3,199,90,0.45)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
지금 매장 등록 →
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/contact"
|
||||||
|
className="rounded-full border border-ink/15 bg-white px-5 py-2.5 text-sm font-semibold text-ink hover:border-ink/40"
|
||||||
|
>
|
||||||
|
운영팀 문의
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { createPrismaClient } from '@startover/database';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
export const metadata = {
|
||||||
|
title: '철거 업체 리뷰 | Startover',
|
||||||
|
description:
|
||||||
|
'실제 거래 매장에서 철거를 진행한 업체들의 후기와 평점을 확인하세요. 강남언니처럼 솔직한 리뷰로 안전한 철거 업체를 선택할 수 있습니다.',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 평점 placeholder 이모지
|
||||||
|
function Stars({ score }: { score: number }) {
|
||||||
|
const full = Math.floor(score);
|
||||||
|
const half = score - full >= 0.5;
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-sm">
|
||||||
|
{'★'.repeat(full)}
|
||||||
|
{half ? '☆' : ''}
|
||||||
|
<span className="ml-1 text-ink-muted">{score.toFixed(1)}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function DemolitionPage() {
|
||||||
|
const prisma = createPrismaClient();
|
||||||
|
|
||||||
|
// 인증된 철거 업체 목록 (vendorType DEMOLITION)
|
||||||
|
// 향후 reviews 테이블 추가 후 실데이터 평균 평점/리뷰수 join 예정
|
||||||
|
let vendors: Array<{
|
||||||
|
id: bigint;
|
||||||
|
publicId: string;
|
||||||
|
businessName: string;
|
||||||
|
serviceIntro: string | null;
|
||||||
|
}> = [];
|
||||||
|
try {
|
||||||
|
const result = await prisma.vendor.findMany({
|
||||||
|
where: { certificationStatus: 'APPROVED', vendorType: 'DEMOLITION' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
publicId: true,
|
||||||
|
businessName: true,
|
||||||
|
serviceIntro: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 30,
|
||||||
|
});
|
||||||
|
vendors = result;
|
||||||
|
} catch {
|
||||||
|
vendors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeholder 데이터(실데이터 0건일 때 노출)
|
||||||
|
const sampleReviews = [
|
||||||
|
{
|
||||||
|
vendor: '클린철거 (주)',
|
||||||
|
area: '서울 강남·서초',
|
||||||
|
score: 4.8,
|
||||||
|
count: 27,
|
||||||
|
excerpt: '도배 보양 깔끔하고 분진 거의 없음. 다음 매장도 의뢰 예정.',
|
||||||
|
tags: ['소음 적음', '폐기물 정상 처리', '잔금 투명'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vendor: '한솔철거',
|
||||||
|
area: '경기 남부',
|
||||||
|
score: 4.6,
|
||||||
|
count: 19,
|
||||||
|
excerpt: '약속한 일정 +0.5일에 마감. 가격 합리적.',
|
||||||
|
tags: ['일정 준수', '견적 합리', '재시공 협조'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vendor: '모던인테리어철거팀',
|
||||||
|
area: '서울 전역',
|
||||||
|
score: 4.4,
|
||||||
|
count: 12,
|
||||||
|
excerpt: '인테리어와 한 번에 진행해서 공기가 짧음. 단, 야간 작업 가능 여부 미리 확인 필요.',
|
||||||
|
tags: ['원스톱', '도면 협의'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-6xl px-6 py-10 font-body">
|
||||||
|
{/* HERO */}
|
||||||
|
<section className="rounded-3xl border border-ink/5 bg-white p-8 md:p-12">
|
||||||
|
<p className="font-mono text-[11px] tracking-[0.25em] uppercase text-ink-muted">
|
||||||
|
<span style={{ color: '#03C75A' }}>◆</span> Demolition · Reviews
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-3 font-display text-3xl font-extrabold tracking-tight text-ink md:text-4xl">
|
||||||
|
내 매장 철거,
|
||||||
|
<br className="hidden md:block" />
|
||||||
|
<span style={{ color: '#03C75A' }}>실제 거래 후기</span>로 고르세요
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-ink-light md:text-base">
|
||||||
|
Startover에서 거래 완료된 매장의 매도인·매수인이 직접 남긴 철거 업체 평점입니다.
|
||||||
|
분진·소음·폐기물·일정 준수·잔금 투명도 5개 항목으로 검증된 후기만 노출합니다.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<Link
|
||||||
|
href="/vendors?serviceType=DEMOLITION"
|
||||||
|
className="rounded-full px-5 py-2.5 text-sm font-semibold text-white"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg,#03C75A 0%,#02A149 55%,#018f40 100%)',
|
||||||
|
boxShadow: '0 8px 20px -8px rgba(3,199,90,0.45)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
인증 철거업체 전체 보기 →
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/vendors/apply"
|
||||||
|
className="rounded-full border border-ink/15 bg-white px-5 py-2.5 text-sm font-semibold text-ink hover:border-ink/40"
|
||||||
|
>
|
||||||
|
업체 인증 신청
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FILTER STATS */}
|
||||||
|
<section className="mt-8 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
|
{[
|
||||||
|
{ label: '인증 업체', value: `${vendors.length || 0}` },
|
||||||
|
{ label: '평균 평점', value: '4.5' },
|
||||||
|
{ label: '누적 후기', value: '58' },
|
||||||
|
{ label: '평균 응답', value: '6h' },
|
||||||
|
].map((s) => (
|
||||||
|
<div key={s.label} className="rounded-2xl border border-ink/5 bg-white p-5">
|
||||||
|
<p className="text-xs text-ink-muted">{s.label}</p>
|
||||||
|
<p className="mt-1 font-display text-2xl font-bold text-ink">{s.value}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* REVIEWS LIST */}
|
||||||
|
<section className="mt-10">
|
||||||
|
<div className="mb-5 flex items-end justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink">최근 후기</h2>
|
||||||
|
<p className="mt-1 text-xs text-ink-muted">
|
||||||
|
실제 계약·정산이 완료된 거래의 후기만 표시됩니다 (Startover 검증)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/vendors?serviceType=DEMOLITION"
|
||||||
|
className="text-sm font-semibold"
|
||||||
|
style={{ color: '#02A149' }}
|
||||||
|
>
|
||||||
|
업체 전체 →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
{sampleReviews.map((r, i) => (
|
||||||
|
<article
|
||||||
|
key={i}
|
||||||
|
className="card-lift rounded-2xl border border-ink/5 bg-white p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-display font-bold text-ink">{r.vendor}</h3>
|
||||||
|
<p className="mt-0.5 text-xs text-ink-muted">{r.area}</p>
|
||||||
|
</div>
|
||||||
|
<Stars score={r.score} />
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm leading-relaxed text-ink-light">
|
||||||
|
"{r.excerpt}"
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-1.5">
|
||||||
|
{r.tags.map((t) => (
|
||||||
|
<span
|
||||||
|
key={t}
|
||||||
|
className="rounded-full bg-ink/5 px-2.5 py-0.5 text-[11px] text-ink-light"
|
||||||
|
>
|
||||||
|
#{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center justify-between border-t border-ink/5 pt-4">
|
||||||
|
<span className="text-xs text-ink-muted">후기 {r.count}건</span>
|
||||||
|
<Link
|
||||||
|
href="/vendors"
|
||||||
|
className="text-xs font-semibold"
|
||||||
|
style={{ color: '#02A149' }}
|
||||||
|
>
|
||||||
|
업체 상세 →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section
|
||||||
|
className="mt-12 rounded-3xl border border-ink/5 p-8 md:p-10"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg,rgba(3,199,90,0.08) 0%,rgba(2,161,73,0.04) 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink md:text-2xl">
|
||||||
|
철거 견적이 필요하세요?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-ink-light">
|
||||||
|
매장 등록 시 자동으로 인증 철거 업체에게 동시에 전달됩니다.
|
||||||
|
매수자 매칭과 함께 철거 견적도 한 번에 받아보세요.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Link
|
||||||
|
href="/stores/new"
|
||||||
|
className="rounded-full px-5 py-2.5 text-sm font-semibold text-white"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg,#03C75A 0%,#02A149 55%,#018f40 100%)',
|
||||||
|
boxShadow: '0 8px 20px -8px rgba(3,199,90,0.45)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
매장 등록하고 견적 받기 →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -68,6 +68,8 @@ const OPERATOR_ROLES = [
|
|||||||
const NAV_LINKS = [
|
const NAV_LINKS = [
|
||||||
{ href: '/stores', label: '매장 검색' },
|
{ href: '/stores', label: '매장 검색' },
|
||||||
{ href: '/stores/new', label: '매장 등록' },
|
{ href: '/stores/new', label: '매장 등록' },
|
||||||
|
{ href: '/demolition', label: '철거' },
|
||||||
|
{ href: '/closure', label: '폐업' },
|
||||||
{ href: '/matching', label: '매칭' },
|
{ href: '/matching', label: '매칭' },
|
||||||
{ href: '/subsidies', label: '지원금' },
|
{ href: '/subsidies', label: '지원금' },
|
||||||
{ href: '/vendors', label: '업체' },
|
{ href: '/vendors', label: '업체' },
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
import { createPrismaClient } from '@startover/database';
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
|
||||||
|
export type StoreEditFormState = {
|
||||||
|
error?: string;
|
||||||
|
fieldValues?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAN = 10000;
|
||||||
|
const toWonOrZero = (v: string | null) => (v ? Number(v) * MAN : 0);
|
||||||
|
const toWon = (v: string | null) => (v ? Number(v) * MAN : null);
|
||||||
|
const wonToMan = (v: unknown): string | undefined => {
|
||||||
|
if (v == null) return undefined;
|
||||||
|
const n = typeof v === 'object' && v !== null && 'toString' in v
|
||||||
|
? Number((v as { toString(): string }).toString())
|
||||||
|
: Number(v);
|
||||||
|
if (!Number.isFinite(n)) return undefined;
|
||||||
|
return String(Math.round(n / MAN));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기존 store 데이터를 만원 단위로 변환해서 폼 초기값으로 반환.
|
||||||
|
*/
|
||||||
|
export async function getStoreForEdit(publicId: string) {
|
||||||
|
const prisma = createPrismaClient();
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.dbId) return null;
|
||||||
|
|
||||||
|
const store = await prisma.store.findUnique({
|
||||||
|
where: { publicId },
|
||||||
|
include: {
|
||||||
|
lease: true,
|
||||||
|
sale: true,
|
||||||
|
facility: true,
|
||||||
|
industryLeaf: { select: { code: true, parent: { select: { code: true } } } },
|
||||||
|
regionCluster: { select: { code: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!store) return null;
|
||||||
|
if (store.ownerUserId !== BigInt(session.user.dbId)) return null;
|
||||||
|
if (store.reviewStatus !== 'DRAFT' && store.reviewStatus !== 'REJECTED') return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicId: store.publicId,
|
||||||
|
listingTitle: store.listingTitle,
|
||||||
|
regionClusterCode: store.regionCluster?.code ?? '',
|
||||||
|
industryMajorCode: store.industryLeaf?.parent?.code ?? '',
|
||||||
|
industryLeafCode: store.industryLeaf?.code ?? '',
|
||||||
|
roadAddress: store.roadAddress,
|
||||||
|
depositAmount: wonToMan(store.lease?.depositAmount),
|
||||||
|
monthlyRentAmount: wonToMan(store.lease?.monthlyRentAmount),
|
||||||
|
premiumAmount: wonToMan(store.lease?.premiumAmount ?? store.sale?.premiumAmount),
|
||||||
|
remainingLeaseMonths:
|
||||||
|
store.lease?.remainingLeaseMonths != null
|
||||||
|
? String(store.lease.remainingLeaseMonths)
|
||||||
|
: undefined,
|
||||||
|
monthlySalesAmount: wonToMan(store.sale?.monthlySalesAmount),
|
||||||
|
monthlyProfitAmount: wonToMan(store.sale?.monthlyProfitAmount),
|
||||||
|
startupCostAmount: wonToMan(store.sale?.startupCostAmount),
|
||||||
|
listingDescription: store.sale?.listingDescription ?? '',
|
||||||
|
locationHighlight: store.sale?.locationHighlight ?? '',
|
||||||
|
saleReason: store.sale?.saleReason ?? '',
|
||||||
|
exclusiveAreaSqm:
|
||||||
|
store.facility?.exclusiveAreaSqm != null ? String(store.facility.exclusiveAreaSqm) : undefined,
|
||||||
|
floorLevel:
|
||||||
|
store.facility?.floorLevel != null ? String(store.facility.floorLevel) : undefined,
|
||||||
|
kitchenEquipmentSummary: store.facility?.kitchenEquipmentSummary ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateStoreDraftAction(
|
||||||
|
publicId: string,
|
||||||
|
_prevState: StoreEditFormState,
|
||||||
|
formData: FormData,
|
||||||
|
): Promise<StoreEditFormState> {
|
||||||
|
const fieldValues: Record<string, string | undefined> = {};
|
||||||
|
const get = (key: string): string | null => {
|
||||||
|
const v = formData.get(key);
|
||||||
|
if (v == null) return null;
|
||||||
|
const s = String(v).trim();
|
||||||
|
fieldValues[key] = s;
|
||||||
|
return s === '' ? null : s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listingTitle = get('listingTitle') ?? '';
|
||||||
|
const regionClusterCode = get('regionClusterCode') ?? '';
|
||||||
|
const industryLeafCode = get('industryLeafCode') ?? '';
|
||||||
|
const roadAddress = get('roadAddress') ?? '';
|
||||||
|
get('industryMajorCode');
|
||||||
|
const depositAmount = get('depositAmount');
|
||||||
|
const monthlyRentAmount = get('monthlyRentAmount');
|
||||||
|
const premiumAmount = get('premiumAmount');
|
||||||
|
const remainingLeaseMonths = get('remainingLeaseMonths');
|
||||||
|
const monthlySalesAmount = get('monthlySalesAmount');
|
||||||
|
const monthlyProfitAmount = get('monthlyProfitAmount');
|
||||||
|
const startupCostAmount = get('startupCostAmount');
|
||||||
|
const listingDescription = get('listingDescription');
|
||||||
|
const locationHighlight = get('locationHighlight');
|
||||||
|
const saleReason = get('saleReason');
|
||||||
|
const exclusiveAreaSqm = get('exclusiveAreaSqm');
|
||||||
|
const floorLevel = get('floorLevel');
|
||||||
|
const kitchenEquipmentSummary = get('kitchenEquipmentSummary');
|
||||||
|
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.dbId) {
|
||||||
|
return { error: '로그인이 필요합니다.', fieldValues };
|
||||||
|
}
|
||||||
|
if (!listingTitle || !regionClusterCode || !industryLeafCode || !roadAddress) {
|
||||||
|
return { error: '필수 항목이 누락되었습니다.', fieldValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = createPrismaClient();
|
||||||
|
|
||||||
|
const store = await prisma.store.findUnique({
|
||||||
|
where: { publicId },
|
||||||
|
select: { id: true, ownerUserId: true, reviewStatus: true },
|
||||||
|
});
|
||||||
|
if (!store) return { error: '매장을 찾을 수 없습니다.', fieldValues };
|
||||||
|
if (store.ownerUserId !== BigInt(session.user.dbId)) {
|
||||||
|
return { error: '권한이 없습니다.', fieldValues };
|
||||||
|
}
|
||||||
|
if (store.reviewStatus !== 'DRAFT' && store.reviewStatus !== 'REJECTED') {
|
||||||
|
return { error: '검토 중이거나 게시된 매장은 수정할 수 없습니다.', fieldValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
const region = await prisma.regionHierarchy.findUnique({
|
||||||
|
where: { code: regionClusterCode },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
const leaf = await prisma.industryTaxonomy.findUnique({
|
||||||
|
where: { code: industryLeafCode },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!region || !leaf) return { error: '지역/업종 코드가 올바르지 않습니다.', fieldValues };
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.store.update({
|
||||||
|
where: { id: store.id },
|
||||||
|
data: {
|
||||||
|
listingTitle,
|
||||||
|
roadAddress,
|
||||||
|
regionClusterId: region.id,
|
||||||
|
industryLeafId: leaf.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 임대 정보
|
||||||
|
await tx.storeLease.upsert({
|
||||||
|
where: { storeId: store.id },
|
||||||
|
create: {
|
||||||
|
storeId: store.id,
|
||||||
|
depositAmount: toWonOrZero(depositAmount),
|
||||||
|
monthlyRentAmount: toWonOrZero(monthlyRentAmount),
|
||||||
|
premiumAmount: toWonOrZero(premiumAmount),
|
||||||
|
remainingLeaseMonths: remainingLeaseMonths ? parseInt(remainingLeaseMonths, 10) : null,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
depositAmount: toWonOrZero(depositAmount),
|
||||||
|
monthlyRentAmount: toWonOrZero(monthlyRentAmount),
|
||||||
|
premiumAmount: toWonOrZero(premiumAmount),
|
||||||
|
remainingLeaseMonths: remainingLeaseMonths ? parseInt(remainingLeaseMonths, 10) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 매매 정보 (one-of-many fields → upsert)
|
||||||
|
await tx.storeSale.upsert({
|
||||||
|
where: { storeId: store.id },
|
||||||
|
create: {
|
||||||
|
storeId: store.id,
|
||||||
|
premiumAmount: toWon(premiumAmount),
|
||||||
|
monthlySalesAmount: toWon(monthlySalesAmount),
|
||||||
|
monthlyProfitAmount: toWon(monthlyProfitAmount),
|
||||||
|
startupCostAmount: toWon(startupCostAmount),
|
||||||
|
listingDescription: listingDescription || null,
|
||||||
|
locationHighlight: locationHighlight || null,
|
||||||
|
saleReason: saleReason || null,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
premiumAmount: toWon(premiumAmount),
|
||||||
|
monthlySalesAmount: toWon(monthlySalesAmount),
|
||||||
|
monthlyProfitAmount: toWon(monthlyProfitAmount),
|
||||||
|
startupCostAmount: toWon(startupCostAmount),
|
||||||
|
listingDescription: listingDescription || null,
|
||||||
|
locationHighlight: locationHighlight || null,
|
||||||
|
saleReason: saleReason || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 시설 정보
|
||||||
|
if (exclusiveAreaSqm || floorLevel || kitchenEquipmentSummary) {
|
||||||
|
await tx.storeFacility.upsert({
|
||||||
|
where: { storeId: store.id },
|
||||||
|
create: {
|
||||||
|
storeId: store.id,
|
||||||
|
exclusiveAreaSqm: exclusiveAreaSqm ? Number(exclusiveAreaSqm) : 0,
|
||||||
|
seatCount: 0,
|
||||||
|
floorLevel: floorLevel ? parseInt(floorLevel, 10) : null,
|
||||||
|
kitchenEquipmentSummary: kitchenEquipmentSummary || null,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
exclusiveAreaSqm: exclusiveAreaSqm ? Number(exclusiveAreaSqm) : 0,
|
||||||
|
floorLevel: floorLevel ? parseInt(floorLevel, 10) : null,
|
||||||
|
kitchenEquipmentSummary: kitchenEquipmentSummary || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath(`/stores/${publicId}`);
|
||||||
|
redirect(`/stores/${publicId}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useActionState, useMemo, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { updateStoreDraftAction, type StoreEditFormState } from './actions';
|
||||||
|
import { INDUSTRY_MAJORS } from '../../industries';
|
||||||
|
|
||||||
|
type Initial = {
|
||||||
|
publicId: string;
|
||||||
|
listingTitle: string;
|
||||||
|
regionClusterCode: string;
|
||||||
|
industryMajorCode: string;
|
||||||
|
industryLeafCode: string;
|
||||||
|
roadAddress: string;
|
||||||
|
depositAmount?: string;
|
||||||
|
monthlyRentAmount?: string;
|
||||||
|
premiumAmount?: string;
|
||||||
|
remainingLeaseMonths?: string;
|
||||||
|
monthlySalesAmount?: string;
|
||||||
|
monthlyProfitAmount?: string;
|
||||||
|
startupCostAmount?: string;
|
||||||
|
listingDescription?: string;
|
||||||
|
locationHighlight?: string;
|
||||||
|
saleReason?: string;
|
||||||
|
exclusiveAreaSqm?: string;
|
||||||
|
floorLevel?: string;
|
||||||
|
kitchenEquipmentSummary?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EditStoreForm({ initial }: { initial: Initial }) {
|
||||||
|
const action = updateStoreDraftAction.bind(null, initial.publicId);
|
||||||
|
const initialState: StoreEditFormState = { fieldValues: initial as Record<string, string | undefined> };
|
||||||
|
const [state, formAction, isPending] = useActionState(action, initialState);
|
||||||
|
const [majorCode, setMajorCode] = useState<string>(
|
||||||
|
state.fieldValues?.industryMajorCode ?? initial.industryMajorCode ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const leafOptions = useMemo(
|
||||||
|
() => INDUSTRY_MAJORS.find((m) => m.code === majorCode)?.children ?? [],
|
||||||
|
[majorCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fv = state.fieldValues ?? (initial as Record<string, string | undefined>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-3xl px-6 py-10 font-body">
|
||||||
|
<div className="mb-6">
|
||||||
|
<Link href={`/stores/${initial.publicId}`} className="text-sm text-warm-600 hover:text-warm-700">
|
||||||
|
← 매장 상세로
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="animate-fade-up">
|
||||||
|
<h1 className="font-display text-3xl font-bold text-ink">매장 수정</h1>
|
||||||
|
<p className="mt-1 text-sm text-ink-muted">
|
||||||
|
매장 정보를 수정하고 저장합니다. 검토 제출 전 또는 반려 상태에서만 수정 가능합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state.error && (
|
||||||
|
<div className="mt-4 rounded-2xl border border-red-200/60 bg-red-50 px-5 py-4 text-sm text-red-700">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form action={formAction} className="mt-8 space-y-8 animate-fade-up">
|
||||||
|
{/* 기본 정보 */}
|
||||||
|
<section>
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink mb-4">기본 정보</h2>
|
||||||
|
<div className="space-y-4 rounded-2xl border border-ink/5 bg-white/70 backdrop-blur-sm p-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">매장명 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="listingTitle"
|
||||||
|
required
|
||||||
|
defaultValue={fv.listingTitle}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">지역 *</label>
|
||||||
|
<select
|
||||||
|
name="regionClusterCode"
|
||||||
|
required
|
||||||
|
defaultValue={fv.regionClusterCode}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="">지역 선택</option>
|
||||||
|
<option value="KR.BETA.GANGNAM_CORE">강남권 (역삼/선릉/논현)</option>
|
||||||
|
<option value="KR.BETA.MAPO_CORE">마포권 (홍대/합정/연남)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">업종 대분류 *</label>
|
||||||
|
<select
|
||||||
|
name="industryMajorCode"
|
||||||
|
required
|
||||||
|
value={majorCode}
|
||||||
|
onChange={(e) => setMajorCode(e.target.value)}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="">대분류 선택</option>
|
||||||
|
{INDUSTRY_MAJORS.map((m) => (
|
||||||
|
<option key={m.code} value={m.code}>
|
||||||
|
{m.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">업종 소분류 *</label>
|
||||||
|
<select
|
||||||
|
name="industryLeafCode"
|
||||||
|
required
|
||||||
|
defaultValue={fv.industryLeafCode}
|
||||||
|
disabled={!majorCode}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="">소분류 선택</option>
|
||||||
|
{leafOptions.map((c) => (
|
||||||
|
<option key={c.code} value={c.code}>
|
||||||
|
{c.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">주소 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="roadAddress"
|
||||||
|
required
|
||||||
|
defaultValue={fv.roadAddress}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 매매 정보 */}
|
||||||
|
<section>
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink mb-4">매매 정보</h2>
|
||||||
|
<div className="space-y-4 rounded-2xl border border-ink/5 bg-white/70 backdrop-blur-sm p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">권리금 (만원)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="premiumAmount"
|
||||||
|
placeholder="12000"
|
||||||
|
defaultValue={fv.premiumAmount}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">창업비용 (만원)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="startupCostAmount"
|
||||||
|
placeholder="15000"
|
||||||
|
defaultValue={fv.startupCostAmount}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">월매출 (만원)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="monthlySalesAmount"
|
||||||
|
placeholder="8500"
|
||||||
|
defaultValue={fv.monthlySalesAmount}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">월수익 (만원)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="monthlyProfitAmount"
|
||||||
|
placeholder="990"
|
||||||
|
defaultValue={fv.monthlyProfitAmount}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">매물 설명</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
name="listingDescription"
|
||||||
|
defaultValue={fv.listingDescription}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">입지 특징</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
name="locationHighlight"
|
||||||
|
defaultValue={fv.locationHighlight}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">매매 사유</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
name="saleReason"
|
||||||
|
defaultValue={fv.saleReason}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 임대 정보 */}
|
||||||
|
<section>
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink mb-4">임대 정보</h2>
|
||||||
|
<div className="space-y-4 rounded-2xl border border-ink/5 bg-white/70 backdrop-blur-sm p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">보증금 (만원)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="depositAmount"
|
||||||
|
placeholder="5000"
|
||||||
|
defaultValue={fv.depositAmount}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">월세 (만원)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="monthlyRentAmount"
|
||||||
|
placeholder="300"
|
||||||
|
defaultValue={fv.monthlyRentAmount}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">임대 잔여 기간 (개월)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="remainingLeaseMonths"
|
||||||
|
defaultValue={fv.remainingLeaseMonths}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 시설 정보 */}
|
||||||
|
<section>
|
||||||
|
<h2 className="font-display text-xl font-bold text-ink mb-4">시설 정보</h2>
|
||||||
|
<div className="space-y-4 rounded-2xl border border-ink/5 bg-white/70 backdrop-blur-sm p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">면적 (㎡)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="exclusiveAreaSqm"
|
||||||
|
step="0.01"
|
||||||
|
defaultValue={fv.exclusiveAreaSqm}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">층수</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="floorLevel"
|
||||||
|
defaultValue={fv.floorLevel}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-ink-muted mb-1">시설 설명</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
name="kitchenEquipmentSummary"
|
||||||
|
defaultValue={fv.kitchenEquipmentSummary}
|
||||||
|
className="w-full rounded-xl border border-ink/10 bg-white/70 px-4 py-3 text-sm text-ink focus:border-warm-500 focus:ring-2 focus:ring-warm-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="rounded-full px-6 py-3 text-sm font-semibold text-white disabled:opacity-50"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg,#03C75A 0%,#02A149 55%,#018f40 100%)',
|
||||||
|
boxShadow: '0 8px 20px -8px rgba(3,199,90,0.45)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPending ? '저장 중...' : '저장'}
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href={`/stores/${initial.publicId}`}
|
||||||
|
className="rounded-full border-2 border-ink/15 px-6 py-3 text-sm font-medium text-ink hover:border-ink/40 transition-colors"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { getStoreForEdit } from './actions';
|
||||||
|
import { EditStoreForm } from './edit-form';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default async function EditStorePage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const { id } = await params;
|
||||||
|
const initial = await getStoreForEdit(id);
|
||||||
|
if (!initial) notFound();
|
||||||
|
return <EditStoreForm initial={initial} />;
|
||||||
|
}
|
||||||
@@ -82,7 +82,8 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id
|
|||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const prisma = createPrismaClient();
|
const prisma = createPrismaClient();
|
||||||
|
|
||||||
const store = await prisma.store.findUnique({
|
const [store, session] = await Promise.all([
|
||||||
|
prisma.store.findUnique({
|
||||||
where: { publicId: id },
|
where: { publicId: id },
|
||||||
include: {
|
include: {
|
||||||
regionCluster: { select: { nameKo: true } },
|
regionCluster: { select: { nameKo: true } },
|
||||||
@@ -96,12 +97,17 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id
|
|||||||
orderBy: { sortOrder: 'asc' },
|
orderBy: { sortOrder: 'asc' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
auth(),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!store) {
|
if (!store) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOwner = !!session?.user?.dbId && store.ownerUserId === BigInt(session.user.dbId);
|
||||||
|
const canEdit = isOwner && (store.reviewStatus === 'DRAFT' || store.reviewStatus === 'REJECTED');
|
||||||
|
|
||||||
const status = STATUS_META[store.dealStatus] ?? { label: store.dealStatus, bg: '#E4E8E6', fg: '#0F1D17' };
|
const status = STATUS_META[store.dealStatus] ?? { label: store.dealStatus, bg: '#E4E8E6', fg: '#0F1D17' };
|
||||||
|
|
||||||
const industryLabel = [
|
const industryLabel = [
|
||||||
@@ -311,7 +317,16 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id
|
|||||||
|
|
||||||
{/* 액션 버튼 */}
|
{/* 액션 버튼 */}
|
||||||
<div className="mt-6 space-y-3">
|
<div className="mt-6 space-y-3">
|
||||||
{store.reviewStatus === 'DRAFT' && (
|
{canEdit && (
|
||||||
|
<Link
|
||||||
|
href={`/stores/${store.publicId}/edit`}
|
||||||
|
className="block w-full rounded-full border-2 py-3 text-center text-sm font-semibold text-ink hover:bg-[#F7FAF8]"
|
||||||
|
style={{ borderColor: '#03C75A', color: '#02A149' }}
|
||||||
|
>
|
||||||
|
✏️ 수정
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{store.reviewStatus === 'DRAFT' && isOwner && (
|
||||||
<>
|
<>
|
||||||
<form action={handleSubmitForReview}>
|
<form action={handleSubmitForReview}>
|
||||||
<input type="hidden" name="storePublicId" value={store.publicId} />
|
<input type="hidden" name="storePublicId" value={store.publicId} />
|
||||||
|
|||||||
Reference in New Issue
Block a user