feat: 블로그·정보 페이지 추가 및 매매정보 도메인 확장
Deploy Startover / deploy (push) Failing after 0s

AdSense 승인을 위한 콘텐츠 인프라와 점포라인형 매매 정보 모델을 도입.

- 업종 분류 확장: 7개 대분류(휴게음식점/일반음식점/주류점/오락스포츠/판매업/서비스업/기타업종) 하위 소분류
- StoreSale 모델 추가: 월매출·월수익·창업비용·매물설명·입지특징·매매사유
- 매장 검색 카드 재설계(대표 사진 + 권리금 + 월수익), 등록/상세 페이지 매매정보 섹션
- 블로그 시스템: 17개 포스트(폐업/창업/지원금/인테리어), /blog, /blog/[slug]
- 정보 페이지: /about, /terms, /privacy, /faq, /contact
- SEO: sitemap.ts, robots.ts, 페이지별 메타데이터, Article·FAQ JSON-LD, OG 태그
- 주소 라벨 도로명 주소 → 주소

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-04-17 10:52:21 +09:00
parent 2b6de14064
commit df7857a8ef
43 changed files with 7096 additions and 2996 deletions
+4
View File
@@ -2,4 +2,8 @@
module.exports = {
root: true,
extends: ['next/core-web-vitals'],
rules: {
'react/no-unescaped-entities': 'off',
'@next/next/no-img-element': 'off',
},
};
+88
View File
@@ -0,0 +1,88 @@
import type { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: '회사 소개 | Startover',
description:
'Startover는 폐업·양도·창업을 잇는 중개 플랫폼입니다. 소상공인의 깔끔한 마무리와 합리적인 재시작을 돕습니다.',
alternates: { canonical: 'https://startover.co.kr/about' },
};
export default function AboutPage() {
return (
<main className="mx-auto max-w-3xl px-6 py-16 font-body">
<p className="text-sm font-medium tracking-[0.2em] uppercase text-warm-600">About</p>
<h1 className="mt-4 font-display text-4xl font-bold text-ink">
Startover가
</h1>
<p className="mt-6 text-lg leading-relaxed text-ink-light">
Startover는 ,
· .
, , ,
.
</p>
<section className="mt-12 space-y-6">
<h2 className="font-display text-2xl font-bold text-ink"> </h2>
<p className="leading-relaxed text-ink-light">
. ,
, ,
. .
, .
</p>
<p className="leading-relaxed text-ink-light">
Startover는 .
· ,
.
</p>
</section>
<section className="mt-12 space-y-6">
<h2 className="font-display text-2xl font-bold text-ink"> </h2>
<ul className="space-y-4 text-ink-light">
<li>
<strong className="text-ink"> :</strong> , , ,
, . .
</li>
<li>
<strong className="text-ink"> :</strong>
. .
</li>
<li>
<strong className="text-ink"> :</strong>
, .
</li>
<li>
<strong className="text-ink"> :</strong>
.
</li>
</ul>
</section>
<section className="mt-12 space-y-6">
<h2 className="font-display text-2xl font-bold text-ink"> </h2>
<p className="leading-relaxed text-ink-light">
(··) (··)
, ······
.
.
</p>
</section>
<div className="mt-16 flex flex-wrap gap-3 border-t border-ink/5 pt-8">
<Link
href="/stores"
className="rounded-full bg-ink px-6 py-3 text-sm font-medium text-warm-50 hover:bg-warm-800 transition-colors"
>
</Link>
<Link
href="/contact"
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>
</main>
);
}
+128
View File
@@ -0,0 +1,128 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getPostBySlug, getPostsSortedByDate, POSTS } from '../posts';
const CATEGORY_COLORS: Record<string, string> = {
: 'bg-warm-500/10 text-warm-700',
: 'bg-sage-500/10 text-sage-700',
: 'bg-warm-400/15 text-warm-800',
: 'bg-ink/5 text-ink',
};
export function generateStaticParams() {
return POSTS.map((p) => ({ slug: p.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return { title: 'Not Found' };
const url = `https://startover.co.kr/blog/${post.slug}`;
return {
title: `${post.title} | Startover 블로그`,
description: post.description,
alternates: { canonical: url },
openGraph: {
title: post.title,
description: post.description,
url,
type: 'article',
publishedTime: post.publishedAt,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.description,
},
};
}
export default async function BlogDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
notFound();
}
const related = getPostsSortedByDate()
.filter((p) => p.slug !== post.slug && p.category === post.category)
.slice(0, 3);
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.description,
datePublished: post.publishedAt,
author: { '@type': 'Organization', name: 'Startover' },
publisher: { '@type': 'Organization', name: 'Startover' },
mainEntityOfPage: `https://startover.co.kr/blog/${post.slug}`,
};
return (
<main className="mx-auto max-w-3xl px-6 py-16 font-body">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div className="mb-8">
<Link href="/blog" className="text-sm text-warm-600 hover:text-warm-700">
</Link>
</div>
<header>
<div className="flex items-center gap-2">
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${CATEGORY_COLORS[post.category] ?? 'bg-ink/5 text-ink'}`}
>
{post.category}
</span>
<span className="text-xs text-ink-muted">
{post.publishedAt} · {post.readMinutes}
</span>
</div>
<h1 className="mt-4 font-display text-4xl font-bold leading-tight text-ink">
{post.title}
</h1>
<p className="mt-4 text-lg leading-relaxed text-ink-light">{post.description}</p>
</header>
<article className="mt-10">{post.content()}</article>
{related.length > 0 && (
<aside className="mt-20 border-t border-ink/5 pt-10">
<h2 className="font-display text-xl font-bold text-ink"> </h2>
<ul className="mt-4 space-y-3">
{related.map((r) => (
<li key={r.slug}>
<Link
href={`/blog/${r.slug}`}
className="group flex items-center justify-between rounded-xl border border-ink/5 bg-white/70 px-4 py-3 backdrop-blur-sm"
>
<span className="font-medium text-ink group-hover:text-warm-700 transition-colors">
{r.title}
</span>
<span className="text-ink-muted transition-transform group-hover:translate-x-1">
</span>
</Link>
</li>
))}
</ul>
</aside>
)}
</main>
);
}
+45
View File
@@ -0,0 +1,45 @@
import type { ReactNode } from 'react';
export function H2({ children }: { children: ReactNode }) {
return (
<h2 className="mt-12 mb-4 font-display text-2xl font-bold text-ink">{children}</h2>
);
}
export function H3({ children }: { children: ReactNode }) {
return (
<h3 className="mt-8 mb-3 font-display text-xl font-bold text-ink">{children}</h3>
);
}
export function P({ children }: { children: ReactNode }) {
return <p className="mt-4 text-base leading-relaxed text-ink-light">{children}</p>;
}
export function UL({ children }: { children: ReactNode }) {
return <ul className="mt-4 list-disc space-y-2 pl-6 text-ink-light">{children}</ul>;
}
export function OL({ children }: { children: ReactNode }) {
return <ol className="mt-4 list-decimal space-y-2 pl-6 text-ink-light">{children}</ol>;
}
export function Strong({ children }: { children: ReactNode }) {
return <strong className="font-semibold text-ink">{children}</strong>;
}
export function Quote({ children }: { children: ReactNode }) {
return (
<blockquote className="mt-6 border-l-4 border-warm-500 bg-warm-50/60 py-3 pl-5 pr-4 text-sm italic text-ink-light">
{children}
</blockquote>
);
}
export function Note({ children }: { children: ReactNode }) {
return (
<aside className="mt-6 rounded-xl bg-warm-100/70 p-5 text-sm leading-relaxed text-ink">
{children}
</aside>
);
}
+65
View File
@@ -0,0 +1,65 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { getPostsSortedByDate } from './posts';
export const metadata: Metadata = {
title: '블로그 | Startover',
description:
'폐업·양도·창업 절차, 정부 지원금, 인테리어·철거 실무 가이드를 정리합니다. 현장에서 검증된 체크리스트와 사례 중심의 글을 만나보세요.',
alternates: { canonical: 'https://startover.co.kr/blog' },
openGraph: {
title: '블로그 | Startover',
description: '폐업·창업·지원금·인테리어 실무 가이드',
url: 'https://startover.co.kr/blog',
type: 'website',
},
};
const CATEGORY_COLORS: Record<string, string> = {
: 'bg-warm-500/10 text-warm-700',
: 'bg-sage-500/10 text-sage-700',
: 'bg-warm-400/15 text-warm-800',
: 'bg-ink/5 text-ink',
};
export default function BlogListPage() {
const posts = getPostsSortedByDate();
return (
<main className="mx-auto max-w-5xl px-6 py-16 font-body">
<p className="text-sm font-medium tracking-[0.2em] uppercase text-warm-600">Journal</p>
<h1 className="mt-4 font-display text-4xl font-bold text-ink">Startover </h1>
<p className="mt-4 max-w-2xl text-ink-light">
·· .
, , , .
</p>
<div className="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="card-lift group block rounded-2xl border border-ink/5 bg-white/70 p-6 backdrop-blur-sm"
>
<div className="flex items-center gap-2">
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${CATEGORY_COLORS[post.category] ?? 'bg-ink/5 text-ink'}`}
>
{post.category}
</span>
<span className="text-xs text-ink-muted">
{post.publishedAt} · {post.readMinutes}
</span>
</div>
<h2 className="mt-3 font-display text-xl font-bold text-ink group-hover:text-warm-700 transition-colors">
{post.title}
</h2>
<p className="mt-2 text-sm leading-relaxed text-ink-light line-clamp-3">
{post.description}
</p>
</Link>
))}
</div>
</main>
);
}
@@ -0,0 +1,127 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'closure-guide-2026',
title: '2026년 소상공인 폐업 절차 완전 가이드',
description:
'폐업을 결심한 순간부터 매장 정리·세무 신고·지원금 신청까지, 소상공인이 실제로 밟아야 하는 순서를 단계별로 정리했습니다.',
category: '폐업',
publishedAt: '2026-04-01',
readMinutes: 9,
content: () => (
<>
<P>
&quot; &quot; . ,
, ,
. 2026
.
</P>
<H2>1단계: 폐업 </H2>
<P>
.
.
</P>
<UL>
<li>
<Strong> </Strong>: ,
.
</li>
<li>
<Strong> </Strong>:
. .
</li>
<li>
<Strong> </Strong>: .
&quot; &quot; .
</li>
<li>
<Strong> </Strong>: ··
.
</li>
</UL>
<H2>2단계: 양도· </H2>
<P>
. ,
.
</P>
<OL>
<li> (, , ·, ) .</li>
<li> .</li>
<li> · .</li>
<li>· , .</li>
</OL>
<Note>
Startover는 ·
. .
</Note>
<H2>3단계: 양도가 </H2>
<P>
.
.
</P>
<H3> </H3>
<UL>
<li> 거래: 주방 , , POS .</li>
<li> 업체: 시간이 .</li>
<li> 인수: 이전 .</li>
</UL>
<H3> </H3>
<P>
3 ,
. ,
(··· ), .
</P>
<H2>4단계: 세무 </H2>
<OL>
<li>
<Strong> </Strong>: 25
.
</li>
<li>
<Strong> </Strong>: 5 .
</li>
<li>
<Strong>4 </Strong>: ···
.
</li>
<li>
<Strong> </Strong>:
. .
</li>
</OL>
<H2>5단계: 지원금 </H2>
<P>
.
.
</P>
<UL>
<li>
<Strong></Strong>: , ,
· .
</li>
<li>
<Strong> </Strong>: · .
</li>
<li>
<Strong></Strong>: · .
</li>
</UL>
<H2> </H2>
<UL>
<li> ·· </li>
<li> · </li>
<li> · · </li>
<li>·· </li>
<li> </li>
</UL>
</>
),
};
@@ -0,0 +1,128 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'closure-subsidy-2026',
title: '2026년 소상공인 폐업 지원금 총정리',
description:
'2026년 기준 소상공인이 폐업 시 받을 수 있는 주요 지원금 프로그램의 자격·지원금액·신청 방법을 한눈에 정리합니다.',
category: '지원금',
publishedAt: '2026-04-11',
readMinutes: 10,
content: () => (
<>
<P>
·
. 2026
. ·
.
</P>
<H2> </H2>
<P>
.
, .
</P>
<UL>
<li><Strong> </Strong>: (· )</li>
<li><Strong> </Strong>: · </li>
<li><Strong> </Strong>: ·· </li>
</UL>
<H2>1. </H2>
<P>
· .
.
</P>
<H3> </H3>
<UL>
<li>·· 1:1 </li>
<li> , , </li>
<li> ()</li>
</UL>
<H3> </H3>
<UL>
<li> · </li>
<li> 13~15 , (, )</li>
<li> </li>
</UL>
<H3>· </H3>
<UL>
<li> 희망자: 교육비· , </li>
<li> 희망자: 교육 + </li>
</UL>
<H2>2. </H2>
<P>
,
. ··
.
</P>
<UL>
<li> 자격: 관할 </li>
<li> 한도: 200~400 ( , )</li>
<li> 제한: 정부 </li>
</UL>
<H2>3. </H2>
<P>
.
<Strong> </Strong> , 1()·2()
.
</P>
<UL>
<li>1유형: 50~60() 6 </li>
<li>2유형: 직업훈련비· </li>
<li> ( ) </li>
</UL>
<H2>4. </H2>
<P>
· .
· .
</P>
<UL>
<li>서울시: 서울시 , </li>
<li>경기도: 경기신용보증재단의 , </li>
<li>· 광역시: 철거비·· </li>
</UL>
<H2>5. </H2>
<P>
,
.
</P>
<OL>
<li> ( · ) </li>
<li> ( 60~1 ) </li>
<li>· </li>
<li> </li>
</OL>
<H2>6. </H2>
<P>
<Strong> </Strong> .
.
</P>
<UL>
<li> ( )</li>
<li> </li>
<li> </li>
<li> ( )</li>
<li> ( )</li>
</UL>
<H2>7. </H2>
<UL>
<li> · .</li>
<li> .</li>
<li> .</li>
<li> .</li>
</UL>
<Note>
Startover는 ,
.
</Note>
</>
),
};
@@ -0,0 +1,169 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'closure-support-programs',
title: '폐업 후 재취업·재창업 지원 제도',
description:
'희망리턴패키지, 국민취업지원제도, 소상공인 재기 프로그램 등 폐업 후 받을 수 있는 주요 지원 제도의 자격과 신청 방법을 정리합니다.',
category: '폐업',
publishedAt: '2026-04-09',
readMinutes: 9,
content: () => (
<>
<P>
.
,
.
.
</P>
<H2> 3</H2>
<P>
.
.
</P>
<UL>
<li>
<Strong></Strong>: ,
</li>
<li>
<Strong></Strong>: ,
</li>
<li>
<Strong> </Strong>: · ,
</li>
</UL>
<H2></H2>
<P>
·
. .
</P>
<H3> </H3>
<UL>
<li>
<Strong> </Strong>: · 1:1 ,
</li>
<li>
<Strong> </Strong>: ( · ,
)
</li>
<li>
<Strong> </Strong>:
</li>
<li>
<Strong> ·</Strong>: ,
</li>
</UL>
<H3> </H3>
<UL>
<li> ( )</li>
<li> (·)</li>
<li> ·· </li>
</UL>
<Note>
· .
,
.
</Note>
<H2></H2>
<P>
,
. .
</P>
<H3>()</H3>
<UL>
<li> 50 6 ( )</li>
<li> · , 2 100 · </li>
<li> · </li>
</UL>
<H3>()</H3>
<UL>
<li> · </li>
<li> </li>
</UL>
<H2> </H2>
<P>
· .
· .
</P>
<UL>
<li>
<Strong> </Strong>:
(· )
</li>
<li>
<Strong> </Strong>: ··
</li>
<li>
<Strong> </Strong>:
</li>
</UL>
<H2> </H2>
<P>
. .
</P>
<UL>
<li>
<Strong> </Strong>: 1 ( 1~2
)
</li>
<li>
<Strong> </Strong>: ,
(· ).
</li>
<li>
<Strong> </Strong>:
</li>
</UL>
<Note>
.
.
</Note>
<H2> </H2>
<OL>
<li>
<Strong> </Strong>:
( )
</li>
<li>
<Strong> </Strong>: · ,
</li>
<li>
<Strong> 14 </Strong>: 4 ,
</li>
<li>
<Strong> 1 </Strong>: ,
</li>
<li>
<Strong> 5</Strong>: ,
</li>
</OL>
<H3> </H3>
<UL>
<li> </li>
<li> 2 · </li>
<li> </li>
<li> · ( , )</li>
<li>4 · ()</li>
<li>, , · ()</li>
</UL>
<P>
<Strong>
</Strong>.
,
.
</P>
</>
),
};
@@ -0,0 +1,108 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'closure-tax-guide',
title: '폐업 시 세금 처리 방법 총정리',
description:
'부가가치세 확정 신고, 종합소득세, 사업자 폐업 신고까지 — 폐업 시 놓치기 쉬운 세금 처리 순서와 기한, 절세 포인트를 정리합니다.',
category: '폐업',
publishedAt: '2026-04-03',
readMinutes: 8,
content: () => (
<>
<P>
.
·· ,
. ,
.
.
</P>
<H2> </H2>
<P>
.
.
</P>
<UL>
<li><Strong> </Strong> </li>
<li><Strong> </Strong> </li>
<li><Strong> </Strong> 5</li>
<li><Strong>· </Strong> </li>
<li><Strong>4 </Strong> ···</li>
</UL>
<H2> </H2>
<P>
<Strong> 25 </Strong>
. 4 10 5 25
· . (20%)
( 0.022%) .
</P>
<H3> </H3>
<P>
,
. ·
.
</P>
<H3>() </H3>
<P>
<Strong></Strong>.
, ,
.
500 45 ( ) .
</P>
<Note>
.
.
</Note>
<H2> </H2>
<P>
1 1 5
. (·· ) .
</P>
<UL>
<li> (20%) .</li>
<li> 10 (, ).</li>
<li> · .</li>
</UL>
<H2> </H2>
<P>
<Strong>··/</Strong>
. 1 ( )
.
(: 식품위생법상 ).
</P>
<H2>4 </H2>
<P>
<Strong>14 </Strong> 4
. ·· 4
.
.
</P>
<H2> </H2>
<OL>
<li> · </li>
<li> ( , )</li>
<li>· </li>
<li> ( )</li>
<li> · ( )</li>
<li> </li>
<li>( ) </li>
</OL>
<P>
· .
··
.
</P>
</>
),
};
@@ -0,0 +1,117 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'closure-vs-hiatus',
title: '폐업 vs 휴업, 뭐가 유리할까?',
description:
'매출 부진으로 잠시 쉬고 싶은 소상공인이 폐업과 휴업 중 무엇을 선택해야 하는지, 세금·임대차·지원금 측면에서 비교합니다.',
category: '폐업',
publishedAt: '2026-04-05',
readMinutes: 7,
content: () => (
<>
<P>
,
. "잠시 쉬는 것" "완전히 그만두는 것"
, ·· .
.
</P>
<H2> </H2>
<P>
<Strong></Strong> .
,
.
</P>
<P>
<Strong></Strong> .
, .
</P>
<H2> </H2>
<H3></H3>
<UL>
<li>
<Strong></Strong>: 25 .
.
</li>
<li>
<Strong></Strong>:
· "0" .
.
</li>
</UL>
<H3></H3>
<UL>
<li> 5 .</li>
<li> 5 .</li>
</UL>
<H2> </H2>
<P>
,
.
.
</P>
<Note>
.
"언제 재개할 것인가" .
.
</Note>
<H2> </H2>
<P>
· .
</P>
<UL>
<li>
<Strong>(··)</Strong>:
. .
</li>
<li>
<Strong></Strong>:
. .
</li>
<li>
<Strong> </Strong>: ( )
. .
</li>
</UL>
<H2> </H2>
<UL>
<li>3~6 · </li>
<li> </li>
<li> </li>
<li>(: 면허) · </li>
</UL>
<H2> </H2>
<UL>
<li> </li>
<li>(·) </li>
<li>· </li>
<li>· </li>
<li>· </li>
</UL>
<H2> </H2>
<OL>
<li> (3 /6 /)</li>
<li> · </li>
<li> </li>
<li> </li>
<li> ( )</li>
<li> </li>
</OL>
<P>
, <Strong> </Strong>, <Strong>
</Strong> . "일단 쉬자"
.
</P>
</>
),
};
@@ -0,0 +1,120 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'commercial-area-analysis',
title: '상권 분석하는 방법 (무료 도구 활용)',
description:
'소상공인시장진흥공단 상권정보시스템, 통계청 SGIS, 카카오맵 등 무료 도구로 상권을 분석하는 실전 방법을 단계별로 정리합니다.',
category: '창업',
publishedAt: '2026-04-06',
readMinutes: 9,
content: () => (
<>
<P>
.
, ·
. ,
.
</P>
<H2> 3 </H2>
<OL>
<li><Strong> ?</Strong> , · </li>
<li><Strong> ?</Strong> ·· </li>
<li><Strong> ?</Strong> · </li>
</OL>
<P>
7 .
.
</P>
<H2>1. </H2>
<P>
<Strong>sg.sbiz.or.kr</Strong> . ·
, , , .
</P>
<H3> </H3>
<UL>
<li><Strong> </Strong>: </li>
<li><Strong> </Strong>: /, </li>
<li><Strong> </Strong>: 500m . </li>
<li><Strong></Strong>: </li>
</UL>
<H2>2. SGIS </H2>
<P>
<Strong>sgis.kostat.go.kr</Strong>
. "내 고객이 누구인지" .
</P>
<H3> </H3>
<UL>
<li><Strong> </Strong>: · , 20~40 </li>
<li><Strong> </Strong>: 1 , </li>
<li><Strong> </Strong>: ·· </li>
<li><Strong> </Strong>: (· ) </li>
</UL>
<H2>3. · </H2>
<P>
"실제 영업 중인" .
</P>
<OL>
<li> 300m, 500m, 1km .</li>
<li> (: 카페, ) .</li>
<li> <Strong> ·· </Strong> .</li>
<li> · / .</li>
</OL>
<H2>4. </H2>
<P>
<Strong>data.go.kr</Strong> , · ,
.
"최근 3년간 이 동네에서 카페가 몇 개 생기고 몇 개가 닫혔는지"
.
</P>
<H2>5. </H2>
<P>
. ·
2 .
</P>
<UL>
<li>·· 15 </li>
<li> (·· ) </li>
<li>· , </li>
<li> ·· </li>
<li> ( ) "임대" </li>
</UL>
<H2> </H2>
<UL>
<li><Strong>·</Strong>: + (, )</li>
<li><Strong>·</Strong>: </li>
<li><Strong>·</Strong>: </li>
<li><Strong>· </Strong>: · </li>
<li><Strong></Strong>: 24 , · </li>
</UL>
<H2> </H2>
<Note>
"권리금이 저렴한 매물" .
6 .
, POS , .
</Note>
<UL>
<li><Strong> </Strong>: 5~10 </li>
<li><Strong> </Strong>: 1~2 </li>
<li><Strong> </Strong>: </li>
<li><Strong> </Strong>: · · </li>
</UL>
<H2></H2>
<P>
3, 3, 4.
"평균", ··
.
</P>
</>
),
};
@@ -0,0 +1,137 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'demolition-cost-by-industry',
title: '매장 철거 비용 얼마나 들까? 업종별 가이드',
description:
'카페, 음식점, 주점, 미용실 등 업종별 매장 철거 비용의 평균 범위와 비용을 결정하는 요소, 견적을 비교할 때 확인해야 할 항목을 정리합니다.',
category: '인테리어',
publishedAt: '2026-04-14',
readMinutes: 9,
content: () => (
<>
<P>
"평당 얼마" . 10
.
, , ,
. 2026
.
</P>
<H2> 4 </H2>
<H3>1. </H3>
<P>
. ,
.
. 10
25 , 30 15~20 .
</P>
<H3>2. </H3>
<P>
, · , , , ,
.
.
</P>
<H3>3. </H3>
<P>
, ,
. 8~15 , 15~25
. 100
.
</P>
<H3>4. </H3>
<P>
, ,
. 1
.
</P>
<H2> </H2>
<P>
10 () .
.
</P>
<UL>
<li>
<Strong></Strong>: 250~400 . , ,
, .
</li>
<li>
<Strong></Strong>: 400~700 . ,
, .
</li>
<li>
<Strong>·</Strong>: 500~900 . , ,
.
</li>
<li>
<Strong></Strong>: 200~350 . , ,
.
</li>
<li>
<Strong>PC방</Strong>: 400~650 . , ,
· .
</li>
<li>
<Strong></Strong>: 300~500 . · ,
, · .
</li>
</UL>
<H2> </H2>
<OL>
<li>
<Strong> </Strong>:
.
.
</li>
<li>
<Strong> </Strong>: "포함"
. .
</li>
<li>
<Strong> </Strong>: ·· ,
· · .
</li>
<li>
<Strong> </Strong>: "예상치 못한 구조물 발견 시 협의"
. .
</li>
<li>
<Strong> </Strong>:
.
</li>
</OL>
<H2> </H2>
<P>
30% .
.
</P>
<UL>
<li> · </li>
<li>· </li>
<li> "구조물 추가 발견" </li>
<li> </li>
</UL>
<Note>
.
.
</Note>
<H2> </H2>
<UL>
<li> 3 </li>
<li> ·· </li>
<li> </li>
<li> </li>
<li> · 3 </li>
</UL>
</>
),
};
@@ -0,0 +1,131 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'franchise-vs-independent',
title: '프랜차이즈 vs 개인 창업, 장단점 비교',
description:
'가맹비·로열티가 있는 프랜차이즈와 모든 것을 직접 설계해야 하는 개인 창업 — 초기 비용, 운영 자율성, 리스크 면에서 어느 쪽이 적합한지 비교합니다.',
category: '창업',
publishedAt: '2026-04-08',
readMinutes: 8,
content: () => (
<>
<P>
"처음 창업하는데 프랜차이즈가 안전할까요, 개인 창업이 나을까요?"
. ··· .
6 .
</P>
<H2> </H2>
<P>
"창업비용 5천만 원" .
.
</P>
<UL>
<li><Strong></Strong>: (500~3,000 )</li>
<li><Strong></Strong>: · ( )</li>
<li><Strong>()</Strong>: · </li>
<li><Strong>·</Strong>: </li>
<li><Strong></Strong>: (0~5%) </li>
<li><Strong> </Strong>: · </li>
</UL>
<P>
<Strong></Strong> .
, , .
</P>
<H2> </H2>
<P>
···· .
, .
</P>
<UL>
<li><Strong>·</Strong>: , </li>
<li><Strong> ·</Strong>: </li>
<li><Strong>·</Strong>: ·· </li>
<li><Strong></Strong>: SNS·· </li>
</UL>
<H2> </H2>
<P>
10 .
</P>
<UL>
<li><Strong> </Strong>: ··· 6,000~9,000 </li>
<li><Strong> </Strong>: ·· 4,000~7,000 </li>
</UL>
<P>
, "표준화된
품질" "시행착오 비용"
.
</P>
<H2> </H2>
<P>
- .
</P>
<UL>
<li><Strong> </Strong>: 10~20% </li>
<li><Strong> </Strong>: </li>
<li><Strong> </Strong>: </li>
</UL>
<P>
<Strong>
100% </Strong> .
</P>
<H2> vs </H2>
<H3> </H3>
<UL>
<li> </li>
<li> · </li>
<li> ·POS· </li>
</UL>
<H3> </H3>
<UL>
<li>··· </li>
<li> </li>
<li> ( , ) </li>
</UL>
<H2> </H2>
<P>
.
</P>
<UL>
<li><Strong></Strong>: · · , , · </li>
<li><Strong> </Strong>: 0 , ·· , ·· </li>
</UL>
<Note>
. 3 ,
.
</Note>
<H2> </H2>
<H3> </H3>
<UL>
<li> · </li>
<li> </li>
<li> </li>
</UL>
<H3> </H3>
<UL>
<li> · · </li>
<li> (· ) </li>
<li> · </li>
</UL>
<H2> </H2>
<OL>
<li> <Strong></Strong> · </li>
<li> <Strong>· · </Strong> </li>
<li> 3 · </li>
<li> · · </li>
<li> ·· </li>
</OL>
</>
),
};
@@ -0,0 +1,132 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'hope-return-package',
title: '희망리턴패키지 신청 방법과 혜택',
description:
'소상공인시장진흥공단의 희망리턴패키지 프로그램의 세 가지 트랙과 각 트랙의 지원 내용·신청 서류·선발 기준을 상세히 안내합니다.',
category: '지원금',
publishedAt: '2026-04-12',
readMinutes: 9,
content: () => (
<>
<P>
() ·
. ·
.
, .
</P>
<H2> </H2>
<P>
<Strong> </Strong>, <Strong> </Strong>,
<Strong> · </Strong> .
. ,
.
</P>
<H2>1. </H2>
<P>
·· .
1:1 .
</P>
<UL>
<li> 컨설팅: 부가세· , , </li>
<li> 컨설팅: 임대차 , , </li>
<li> 컨설팅: 양도· , , </li>
</UL>
<P>
( 1~2)
(). .
</P>
<H2>2. </H2>
<P>
, ·
.
.
</P>
<H3> </H3>
<UL>
<li> (·· )</li>
<li> </li>
<li>·· </li>
</UL>
<H3> (2026 , )</H3>
<UL>
<li> 13~15 </li>
<li> ( )</li>
<li> </li>
</UL>
<H3> </H3>
<OL>
<li> </li>
<li> </li>
<li> · </li>
<li> </li>
</OL>
<H2>3. · </H2>
<P>
·· .
, ·
.
</P>
<UL>
<li> 교육: 직무 + + </li>
<li> 교육: 업종 , , </li>
<li>멘토링: 선배 1:1 </li>
</UL>
<H2>4. </H2>
<P>
.
.
</P>
<OL>
<li> ( · )</li>
<li> ( 60 ) </li>
<li>· </li>
<li>· </li>
<li> </li>
</OL>
<H2>5. </H2>
<P>
<Strong> (edu.sbiz.or.kr)</Strong>
. .
</P>
<OL>
<li> </li>
<li> ( / / ·) </li>
<li> </li>
<li> </li>
<li> </li>
</OL>
<H2>6. </H2>
<UL>
<li> </li>
<li> </li>
<li> </li>
<li>· </li>
<li> ( )</li>
<li> ( )</li>
</UL>
<H2>7. </H2>
<UL>
<li> .</li>
<li> "실비" .</li>
<li> .</li>
<li>· ( 80% ) .</li>
</UL>
<Note>
Startover에 .
.
</Note>
</>
),
};
+40
View File
@@ -0,0 +1,40 @@
import type { BlogPost } from './types';
import { post as post01 } from './closure-guide-2026';
import { post as post02 } from './closure-tax-guide';
import { post as post03 } from './closure-vs-hiatus';
import { post as post04 } from './restoration-cost-tips';
import { post as post05 } from './closure-support-programs';
import { post as post06 } from './startup-small-capital-2026';
import { post as post07 } from './takeover-vs-newopen';
import { post as post08 } from './commercial-area-analysis';
import { post as post09 } from './franchise-vs-independent';
import { post as post10 } from './startup-legal-checklist';
import { post as post11 } from './closure-subsidy-2026';
import { post as post12 } from './hope-return-package';
import { post as post13 } from './re-startup-programs';
import { post as post14 } from './demolition-cost-by-industry';
import { post as post15 } from './interior-vendor-tips';
import { post as post16 } from './restoration-obligation';
import { post as post17 } from './startup-subsidy-programs';
export const POSTS: BlogPost[] = [
post01, post02, post03, post04, post05,
post06, post07, post08, post09, post10,
post11, post12, post13, post14, post15,
post16, post17,
];
export function getPostBySlug(slug: string): BlogPost | undefined {
return POSTS.find((p) => p.slug === slug);
}
export function getPostsSortedByDate(): BlogPost[] {
return [...POSTS].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));
}
export function getPostsByCategory(category: BlogPost['category']): BlogPost[] {
return POSTS.filter((p) => p.category === category);
}
export type { BlogPost } from './types';
@@ -0,0 +1,112 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'interior-vendor-tips',
title: '인테리어 업체 선정 시 주의할 점 5가지',
description:
'견적 비교부터 표준 계약서, 중도금 지급 조건, 하자 보수 기간까지 — 인테리어 업체를 선정할 때 반드시 확인해야 할 5가지 체크 포인트를 정리합니다.',
category: '인테리어',
publishedAt: '2026-04-15',
readMinutes: 8,
content: () => (
<>
<P>
,
.
5 , ,
. 5
.
</P>
<H2> 1. </H2>
<P>
.
· , 3
.
</P>
<UL>
<li>····· </li>
<li> ··</li>
<li> (, , )</li>
<li> </li>
</UL>
<P>
"일식 견적" .
20 .
</P>
<H2> 2. </H2>
<P>
. , ,
, , .
"작업 중 발견"
.
</P>
<Note>
.
.
</Note>
<H2> 3. </H2>
<P>
· ,
, , ,
.
.
</P>
<H3> </H3>
<UL>
<li> (··· )</li>
<li> ·· "동급 자재로 대체 가능" </li>
<li> </li>
<li> ( 3, 1~2)</li>
<li> </li>
</UL>
<H2> 4. </H2>
<P>
.
10~20%, 40~50%, 30~40% .
, .
</P>
<UL>
<li> 10~20% </li>
<li> 30%, 60% </li>
<li> 2 </li>
<li>1 </li>
</UL>
<H2> 5. </H2>
<P>
1 5
. "보증 6개월"
, . .
</P>
<UL>
<li> ( 3~5 )</li>
<li> </li>
<li> </li>
</UL>
<H2>사례: 계약서 </H2>
<P>
1 A씨는 "자재는 동급 이상으로
대체 가능" .
, "동급"
. B씨는 · ,
.
.
</P>
<H2> </H2>
<UL>
<li> 3, · </li>
<li> </li>
<li> · </li>
<li> ·· , </li>
<li> , </li>
</UL>
</>
),
};
@@ -0,0 +1,127 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 're-startup-programs',
title: '소상공인 재기 지원 프로그램 안내',
description:
'폐업 이후 재창업을 준비하는 소상공인을 위한 재기 지원 프로그램의 종류와 자격, 혜택 내용을 비교하고 신청 순서를 안내합니다.',
category: '지원금',
publishedAt: '2026-04-13',
readMinutes: 9,
content: () => (
<>
<P>
.
··
. .
</P>
<H2> </H2>
<P>
<Strong>"재창업까지의 전 과정을 구조화하는
프로그램"</Strong>. .
</P>
<UL>
<li> : 재창업 , </li>
<li> : 재기 , </li>
<li> : 업종 , , </li>
<li> : 선배 · , </li>
</UL>
<H2>1. </H2>
<P>
<Strong> </Strong>
<Strong> </Strong> .
.
</P>
<UL>
<li> 보증: 지역신용보증재단을 </li>
<li>한도: 업종· ~ (, )</li>
<li>금리: 일반 </li>
<li>심사: 재창업 </li>
</UL>
<H2>2. </H2>
<P>
. , .
</P>
<H3> </H3>
<UL>
<li> (···)</li>
<li> vs. </li>
<li> </li>
<li> </li>
</UL>
<H2>3. </H2>
<P>
. ··
.
</P>
<UL>
<li> 금액: 인당 100~200 ()</li>
<li> 교육: 업종별 , , ·</li>
<li> 기한: 발급 </li>
</UL>
<H2>4. </H2>
<P>
· .
, ·· .
</P>
<UL>
<li>대상: 폐업 </li>
<li>기간: 통상 6~1</li>
<li>혜택: 사업화 ( , ), , </li>
<li>선발: 서류 </li>
</UL>
<H2>5. </H2>
<OL>
<li> ( )</li>
<li> </li>
<li>· </li>
<li> · </li>
<li> </li>
</OL>
<H2>6. </H2>
<UL>
<li> ( )</li>
<li> · </li>
<li>· </li>
<li>(· )</li>
<li> (··· )</li>
<li> ·</li>
</UL>
<H2>7. </H2>
<P>
.
</P>
<UL>
<li><Strong> </Strong>: + </li>
<li><Strong> </Strong>: </li>
<li><Strong> · </Strong>: / </li>
</UL>
<H2> </H2>
<UL>
<li> </li>
<li> </li>
<li> ( )</li>
<li> · 2 </li>
<li> </li>
<li> · K- </li>
</UL>
<Note>
Startover는 .
,
.
</Note>
</>
),
};
@@ -0,0 +1,136 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'restoration-cost-tips',
title: '매장 원상복구 비용 줄이는 방법',
description:
'원상복구 조항의 해석, 임대인 협상 포인트, 현장 실사 준비까지 — 불필요한 철거 비용을 줄이는 실전 팁을 정리합니다.',
category: '폐업',
publishedAt: '2026-04-07',
readMinutes: 7,
content: () => (
<>
<P>
. 500 ,
1,500 .
,
.
.
</P>
<H2> </H2>
<P>
654( ) .
"계약 개시 당시의 상태로 되돌려 놓아야 한다" ,
<Strong> </Strong> . ,
, "공실 골조 상태"
.
</P>
<Note>
( 2019252042 ).
.
</Note>
<H2> </H2>
<UL>
<li>
<Strong> </Strong>: "원상으로 회복하여 반환한다" ,
"공실 상태로 철거 반환" .
</li>
<li>
<Strong> </Strong>: "현 시설 인수" "시설 승계"
.
</li>
<li>
<Strong> </Strong>: "계약 종료 시 임차인의 시설은 임대인에게
귀속된다" .
</li>
<li>
<Strong> </Strong>:
.
</li>
</UL>
<H2> </H2>
<P>
"해야 한다 vs 안 한다" , .
.
</P>
<OL>
<li>
<Strong> </Strong>:
.
.
</li>
<li>
<Strong> </Strong>: (: 음식점)
·· .
</li>
<li>
<Strong> </Strong>: "바닥·벽체는 원상으로, 설비는
잔존" .
</li>
<li>
<Strong> </Strong>:
( &lt; ).
</li>
</OL>
<H2> </H2>
<P>
90% "계약 당시 상태" .
.
</P>
<UL>
<li> ·( )</li>
<li> </li>
<li> </li>
<li> ( )</li>
</UL>
<H2> </H2>
<P>
.
</P>
<OL>
<li> ( // )</li>
<li> · </li>
<li> "남기면 좋은 것/반드시 철거할 것" </li>
<li> ( )</li>
</OL>
<H2> </H2>
<P>
<Strong> </Strong>
. .
··
.
</P>
<Note>
Startover는 ·
, .
.
</Note>
<H2> </H2>
<UL>
<li> · </li>
<li> · </li>
<li> </li>
<li> </li>
<li> 3 ( )</li>
<li> · </li>
<li> </li>
</UL>
<P>
, . "해야 한다"
,
.
</P>
</>
),
};
@@ -0,0 +1,127 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'restoration-obligation',
title: '원상복구 의무, 어디까지 해야 할까?',
description:
'임차인의 원상복구 범위를 둘러싼 판례, 표준 계약서 조항, 양도 시 의무 이전 가능성을 사례와 함께 정리합니다.',
category: '인테리어',
publishedAt: '2026-04-16',
readMinutes: 8,
content: () => (
<>
<P>
. "들어올
때 상태로 되돌려 놓으라" , "들어올 때 상태"
.
.
</P>
<H2> </H2>
<P>
615 "차주는 차용물을 반환할 때에 이를 원상에 회복하여야 한다"
. ,
. ,
.
</P>
<H2> : "현저한 변경" </H2>
<P>
"임차인의 원상복구 의무는 임차인이 임차물에 가한 변경에
한정된다" . ·
,
.
</P>
<H3> </H3>
<UL>
<li>
<Strong> </Strong>: ·
,
.
</li>
<li>
<Strong> </Strong>:
,
.
</li>
<li>
<Strong> </Strong>: , ,
.
</li>
</UL>
<H2> </H2>
<P>
.
.
</P>
<H3>"임차인은 반환 시 원상으로 복구한다"</H3>
<P>
. ,
. .
</P>
<H3>"임차인은 모든 인테리어를 철거하여 공실 상태로 반환한다"</H3>
<P>
"공실 상태" "스켈레톤 상태"
. "현 임차인이
설치한 부분에 한함" .
</P>
<H3>"원상복구 불이행 시 임대인이 복구하고 그 비용을 청구한다"</H3>
<P>
.
2~3 .
"임차인이 지정 업체를 통해 직접 복구한다" .
</P>
<H2> </H2>
<P>
?
.
, · · 3
.
</P>
<OL>
<li> .</li>
<li> "신 임차인이 현재 시설을 인수하고, 퇴거 시 원상복구 의무를 승계한다" .</li>
<li> .</li>
</OL>
<Note>
,
. 3
.
</Note>
<H2> </H2>
<UL>
<li>
<Strong>"현 상태 인수" </Strong>:
, .
</li>
<li>
<Strong> </Strong>: "바닥·벽체·천장 마감만 복구,
설비·집기는 제외" .
</li>
<li>
<Strong> </Strong>: ,
.
</li>
<li>
<Strong> </Strong>:
, .
</li>
</UL>
<H2> </H2>
<UL>
<li> · </li>
<li> </li>
<li> </li>
<li> 3 </li>
<li> </li>
</UL>
</>
),
};
@@ -0,0 +1,132 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'startup-legal-checklist',
title: '창업 전 꼭 알아야 할 법적 절차',
description:
'사업자등록부터 영업신고, 위생·소방 점검, 4대 보험 가입까지 — 창업자가 개업 전에 반드시 완료해야 하는 법적 절차를 체크리스트로 정리합니다.',
category: '창업',
publishedAt: '2026-04-10',
readMinutes: 9,
content: () => (
<>
<P>
"법적 절차" .
,
.
.
</P>
<H2>1. 20 </H2>
<P>
.
<Strong> 20 </Strong>
.
.
</P>
<UL>
<li> 서류: 신분증, , ·· ( )</li>
<li>· 선택: 연매출 1 400(2026 , ) </li>
<li> : 동업계약서 </li>
</UL>
<H2>2. ·</H2>
<P>
· .
.
</P>
<H3> </H3>
<UL>
<li> </li>
<li> ( 6) </li>
<li>() </li>
<li> 기준: 조리장·· </li>
</UL>
<H3> </H3>
<UL>
<li>·· </li>
<li> </li>
<li> </li>
</UL>
<H3> </H3>
<UL>
<li> </li>
<li>· </li>
<li> · </li>
</UL>
<H2>3. </H2>
<P>
<Strong></Strong> ,
6, 3 . ·
.
</P>
<P>
<Strong>()</Strong>
, . 1
.
</P>
<H2>4. </H2>
<P>
.
(··· )
<Strong> </Strong>
.
</P>
<UL>
<li>·· </li>
<li> · </li>
<li> · </li>
</UL>
<H2>5. · </H2>
<P>
<Strong>(KIPRIS)</Strong>
· .
· .
</P>
<OL>
<li>KIPRIS에서 · </li>
<li> </li>
<li> ( 20 , ) </li>
</OL>
<H2>6. 4 </H2>
<P>
···
. 14 4
.
</P>
<UL>
<li> · </li>
<li> · </li>
<li> </li>
</UL>
<H2>7. ( )</H2>
<P>
(·· )
<Strong> </Strong> .
, (PG) .
</P>
<H2> </H2>
<UL>
<li> ( 20 )</li>
<li> · </li>
<li> · </li>
<li> ( )</li>
<li>· KIPRIS </li>
<li> 4 </li>
<li> </li>
</UL>
<Note>
,
. Startover에서는 ·
.
</Note>
</>
),
};
@@ -0,0 +1,159 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'startup-small-capital-2026',
title: '2026년 소자본 창업 아이템 TOP 10',
description:
'초기 투자 5천만 원 이하로 시작할 수 있는 2026년형 소자본 창업 아이템 10가지를 업종별 특징·손익 구조와 함께 정리합니다.',
category: '창업',
publishedAt: '2026-04-02',
readMinutes: 10,
content: () => (
<>
<P>
·
.
. 5
2026 10 .
, , .
</P>
<Note>
·
, ·· .
</Note>
<H2> </H2>
<P>
. ,
. ,
1 . ,
.
.
</P>
<H2>TOP 10 </H2>
<H3>1. (··)</H3>
<UL>
<li><Strong> </Strong>: 3~5 ( )</li>
<li><Strong></Strong>: 500~1,500 </li>
<li><Strong></Strong>: · , </li>
</UL>
<P>
0
. CCTV·· .
</P>
<H3>2. </H3>
<UL>
<li><Strong> </Strong>: 2~4 </li>
<li><Strong></Strong>: 700~2,000 </li>
<li><Strong></Strong>: · </li>
</UL>
<P>
, (10~15) .
25~35%
.
</P>
<H3>3. </H3>
<UL>
<li><Strong> </Strong>: 1~3 </li>
<li><Strong></Strong>: 500~1,500 </li>
<li><Strong></Strong>: </li>
</UL>
<P>
.
.
</P>
<H3>4. </H3>
<UL>
<li><Strong> </Strong>: 2~4 </li>
<li><Strong></Strong>: 400~1,200 </li>
<li><Strong></Strong>: · </li>
</UL>
<P>
1 . SNS
.
</P>
<H3>5. ( · )</H3>
<UL>
<li><Strong> </Strong>: 4~5 </li>
<li><Strong></Strong>: 800~1,800 </li>
<li><Strong></Strong>: ·, </li>
</UL>
<P>
,
. .
</P>
<H3>6. </H3>
<UL>
<li><Strong> </Strong>: 2~4 </li>
<li><Strong></Strong>: 500~1,500 </li>
<li><Strong></Strong>: · </li>
</UL>
<P>
1 .
, .
</P>
<H3>7. </H3>
<UL>
<li><Strong> </Strong>: 3~5 </li>
<li><Strong></Strong>: 600~1,700 </li>
<li><Strong></Strong>: </li>
</UL>
<P>
· ··
. .
</P>
<H3>8. ( )</H3>
<UL>
<li><Strong> </Strong>: 4~5 ( )</li>
<li><Strong></Strong>: 400~900 </li>
<li><Strong></Strong>: , </li>
</UL>
<P>
24 . ·
.
</P>
<H3>9. 1 ( )</H3>
<UL>
<li><Strong> </Strong>: 3~5 </li>
<li><Strong></Strong>: 700~2,000 </li>
<li><Strong></Strong>: </li>
</UL>
<P>
10 , ·
. (25~30%) .
</P>
<H3>10. (··)</H3>
<UL>
<li><Strong> </Strong>: 2~4 </li>
<li><Strong></Strong>: 400~1,200 </li>
<li><Strong></Strong>: , </li>
</UL>
<P>
.
.
</P>
<H2> </H2>
<OL>
<li> <Strong>··</Strong> </li>
<li> </li>
<li> </li>
<li> 30% </li>
<li>· </li>
</OL>
</>
),
};
@@ -0,0 +1,161 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'startup-subsidy-programs',
title: '정부 창업 지원금 종류와 신청 방법',
description:
'예비창업패키지, 초기창업패키지, 청년창업사관학교 등 대표적인 정부 창업 지원 프로그램을 비교하고 신청 전략을 안내합니다.',
category: '지원금',
publishedAt: '2026-04-17',
readMinutes: 10,
content: () => (
<>
<P>
,
. ,
, · .
.
</P>
<H2> </H2>
<P>
( ) (3 )
(3~7) .
, .
</P>
<UL>
<li>예비창업기: 예비창업패키지, ()</li>
<li>초기창업기: 초기창업패키지, ()</li>
<li>도약기: 창업도약패키지, TIPS </li>
<li>재도전기: 재도전 </li>
</UL>
<H2></H2>
<P>
.
1 ( 5 ) ,
.
</P>
<H3> </H3>
<UL>
<li> </li>
<li>·· </li>
<li> </li>
</UL>
<H3> </H3>
<P>
2~3 K-Startup .
3 , 5:1에서 10:1 .
</P>
<H2></H2>
<P>
3 .
1 , , .
</P>
<H3> </H3>
<UL>
<li> 3 </li>
<li>· </li>
<li> </li>
</UL>
<H3> </H3>
<P>
· .
, , .
</P>
<H2></H2>
<P>
39 1 .
, .
1 , , .
</P>
<H3></H3>
<UL>
<li> ( · )</li>
<li> </li>
<li> 18 (··· ) </li>
</UL>
<H2> </H2>
<P>
. 1
6 ,
.
</P>
<H3> </H3>
<UL>
<li> 3 </li>
<li> ( · )</li>
<li> · </li>
</UL>
<Note>
"이전 폐업의 원인 분석과 재창업의
차별점" .
, .
</Note>
<H2> vs </H2>
<P>
() .
,
.
</P>
<UL>
<li>
<Strong> </Strong>: ( 1 ),
. .
</li>
<li>
<Strong> </Strong>: (500~5 )
. , ,
.
</li>
</UL>
<H2> </H2>
<OL>
<li>( PSST )</li>
<li> ·</li>
<li>( )</li>
<li> ( )</li>
<li>· ()</li>
<li> ( 39 )</li>
</OL>
<H2> : 사업계획서 </H2>
<P>
PSST (Problem - Solution - Scale-up - Team)
. .
</P>
<UL>
<li>
<Strong>Problem</Strong>:
. · .
</li>
<li>
<Strong>Solution</Strong>: 3 .
· .
</li>
<li>
<Strong>Scale-up</Strong>: 3~5 · .
.
</li>
<li>
<Strong>Team</Strong>: · .
</li>
</UL>
<H2> </H2>
<UL>
<li> (···) </li>
<li>K-Startup </li>
<li> PSST </li>
<li>· </li>
<li>· </li>
</UL>
</>
),
};
@@ -0,0 +1,114 @@
import { H2, H3, P, UL, OL, Strong, Note } from '../components';
import type { BlogPost } from './types';
export const post: BlogPost = {
slug: 'takeover-vs-newopen',
title: '창업 시 양도양수 vs 신규 개업 비교 분석',
description:
'권리금을 내고 기존 매장을 양도받는 것과 신규로 개업하는 것 — 초기 비용, 시장 검증, 리스크 측면에서 어떤 선택이 유리한지 비교합니다.',
category: '창업',
publishedAt: '2026-04-04',
readMinutes: 8,
content: () => (
<>
<P>
"기존 매장을 양도받을까, 아예
새로 차릴까". , ,
. 6 ,
.
</P>
<H2> </H2>
<H3></H3>
<UL>
<li><Strong> </Strong>: · .</li>
<li><Strong> </Strong>: ·· .</li>
<li><Strong> </Strong>: ·· .</li>
<li><Strong> </Strong>: ( ) .</li>
</UL>
<H3></H3>
<UL>
<li><Strong> </Strong>: + + ~ .</li>
<li><Strong> </Strong>: , · .</li>
<li><Strong> </Strong>: .</li>
</UL>
<H2> </H2>
<H3></H3>
<UL>
<li><Strong>· </Strong>: ·· .</li>
<li><Strong> ( )</Strong>: · .</li>
<li><Strong> ·</Strong>: .</li>
</UL>
<H3></H3>
<UL>
<li><Strong> </Strong>: .</li>
<li><Strong>· </Strong>: · .</li>
<li><Strong> </Strong>: 3~6 .</li>
</UL>
<H2> </H2>
<P>
10 .
</P>
<UL>
<li><Strong></Strong>: 3,000 + 2,000 + 500 5,500 </li>
<li><Strong> </Strong>: 2,000 + 3,000 + · 1,500 6,500 </li>
</UL>
<P>
, "즉시 매출이 발생하는 매장",
"0원에서 시작하는 매장" .
</P>
<H2> </H2>
<P>
3~6 .
6 50~70% .
6 .
</P>
<H2> </H2>
<P>
.
</P>
<OL>
<li><Strong></Strong>: · </li>
<li><Strong></Strong>: · </li>
<li><Strong></Strong>: </li>
</OL>
<P>
,
.
<Strong> "권리금은 투자금이 아닌 비용" </Strong> .
</P>
<H2> , </H2>
<H3> </H3>
<UL>
<li> </li>
<li> </li>
<li> · </li>
</UL>
<H3> </H3>
<UL>
<li> · </li>
<li> </li>
<li>6 </li>
</UL>
<Note>
Startover는 ,
.
</Note>
<H2> </H2>
<OL>
<li> 30% </li>
<li> 12 </li>
<li> </li>
<li> </li>
<li> 3 · </li>
</OL>
</>
),
};
+13
View File
@@ -0,0 +1,13 @@
import type { ReactNode } from 'react';
export type BlogCategory = '폐업' | '창업' | '지원금' | '인테리어';
export interface BlogPost {
slug: string;
title: string;
description: string;
category: BlogCategory;
publishedAt: string;
readMinutes: number;
content: () => ReactNode;
}
+50
View File
@@ -0,0 +1,50 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '문의하기 | Startover',
description: 'Startover 서비스 이용, 매장 등록, 업체 인증, 제휴에 관한 문의를 접수합니다.',
alternates: { canonical: 'https://startover.co.kr/contact' },
};
export default function ContactPage() {
return (
<main className="mx-auto max-w-3xl px-6 py-16 font-body">
<p className="text-sm font-medium tracking-[0.2em] uppercase text-warm-600">Contact</p>
<h1 className="mt-4 font-display text-4xl font-bold text-ink"></h1>
<p className="mt-4 text-ink-light">
, , , · .
1~2 .
</p>
<div className="mt-12 space-y-6">
<div className="rounded-2xl border border-ink/5 bg-white/70 p-6 backdrop-blur-sm">
<p className="text-xs font-medium tracking-widest uppercase text-ink-muted"> </p>
<p className="mt-2 font-display text-xl font-bold text-ink">hello@startover.co.kr</p>
<p className="mt-1 text-sm text-ink-light"> , , </p>
</div>
<div className="rounded-2xl border border-ink/5 bg-white/70 p-6 backdrop-blur-sm">
<p className="text-xs font-medium tracking-widest uppercase text-ink-muted"> </p>
<p className="mt-2 font-display text-xl font-bold text-ink">partners@startover.co.kr</p>
<p className="mt-1 text-sm text-ink-light">· , </p>
</div>
<div className="rounded-2xl border border-ink/5 bg-white/70 p-6 backdrop-blur-sm">
<p className="text-xs font-medium tracking-widest uppercase text-ink-muted"> </p>
<p className="mt-2 font-display text-xl font-bold text-ink">privacy@startover.co.kr</p>
<p className="mt-1 text-sm text-ink-light"> ·· , </p>
</div>
<div className="rounded-2xl border border-ink/5 bg-white/70 p-6 backdrop-blur-sm">
<p className="text-xs font-medium tracking-widest uppercase text-ink-muted">·</p>
<p className="mt-2 font-display text-xl font-bold text-ink">press@startover.co.kr</p>
<p className="mt-1 text-sm text-ink-light"> , </p>
</div>
</div>
<p className="mt-12 text-sm text-ink-muted">
[ ] .
</p>
</main>
);
}
+101
View File
@@ -0,0 +1,101 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '자주 묻는 질문 | Startover',
description:
'Startover 이용에 자주 묻는 질문과 답변을 모았습니다. 매장 등록, 거래 절차, 지원금, 인증 업체 관련 궁금증을 확인해 보세요.',
alternates: { canonical: 'https://startover.co.kr/faq' },
};
const FAQS = [
{
q: 'Startover에서 매장을 등록하려면 어떻게 해야 하나요?',
a: '회원가입 후 [매장 등록] 메뉴에서 업종, 지역, 권리금, 월매출 등 기본 정보를 입력하면 초안이 생성됩니다. 운영팀 검수를 거쳐 공개가 승인되면 검색 결과에 노출됩니다.',
},
{
q: '매장 등록은 무료인가요?',
a: '현재 베타 기간 중에는 매장 등록과 기본 매칭이 무료입니다. 계약이 성사되어 에스크로 결제가 이루어지는 시점에 표준 중개 수수료가 발생합니다.',
},
{
q: '권리금 회수기간은 어떻게 계산하나요?',
a: '권리금 ÷ 월수익으로 계산한 개월 수를 표시합니다. 예를 들어 권리금 1억 2천만 원, 월수익 990만 원인 매장은 약 12개월의 권리금 회수기간을 가집니다.',
},
{
q: '월매출과 월수익 정보는 누가 검증하나요?',
a: '월매출·월수익은 매도인이 제공한 자료이며 법적 근거로 사용할 수 없습니다. 본 정보는 참고용이며, 실제 거래 전 반드시 실사와 세무 자료로 검증하시기 바랍니다.',
},
{
q: '매장 검색은 어떤 조건으로 필터할 수 있나요?',
a: '지역(강남권/마포권 등), 업종 대분류(휴게음식점/일반음식점/주류점 등), 소분류, 거래 상태(거래 가능/매칭 중) 기준으로 필터할 수 있습니다.',
},
{
q: '정부 지원금은 어떻게 신청하나요?',
a: '매장 상세 페이지에서 [지원금 확인] 버튼을 누르면 해당 매장이 받을 수 있는 지원금 프로그램 후보가 표시됩니다. 체크리스트와 서류 업로드 흐름을 따라가면 운영팀이 제출 가능 여부를 판단해 안내합니다.',
},
{
q: '철거 업체나 인테리어 업체는 어떻게 고르나요?',
a: '인증을 통과한 업체만 매칭 풀에 포함됩니다. 매장 등록 후 [매칭 요청 보내기]로 철거/인테리어 요청을 등록하면 지역·업종에 맞는 인증 업체에게 전달되고, 업체가 수락하면 계약 단계로 넘어갑니다.',
},
{
q: '에스크로 결제는 어떤 방식으로 작동하나요?',
a: '계약 체결 후 매수인이 입금한 권리금·공사비는 회사가 지정한 에스크로 계좌에 보관되며, 사전검수·중간검수·최종검수 단계별로 단계적으로 해제·정산됩니다.',
},
{
q: '계약 후 분쟁이 발생하면 어떻게 하나요?',
a: '계약 상세 페이지에서 분쟁 접수가 가능합니다. 접수된 분쟁은 운영팀이 배정되어 사실관계를 확인하고, 조정안을 제시하여 당사자 간 합의를 돕습니다.',
},
{
q: '베타 지역 외 매장도 등록할 수 있나요?',
a: '현재 베타는 강남권·마포권을 중심으로 운영되며, 이외 지역은 순차적으로 확장됩니다. 지역 확장 요청은 문의 페이지로 접수해 주시면 우선순위에 반영합니다.',
},
{
q: '매장을 공개하지 않고 제한된 사람에게만 보여줄 수 있나요?',
a: '네. 매장 공개 상태를 PRIVATE(비공개) 또는 RESTRICTED(제한 공개)로 설정할 수 있으며, 매칭이 성사된 상대방에게만 상세 정보가 공개되는 옵션도 제공합니다.',
},
{
q: '서비스 관련 문의는 어디로 하면 되나요?',
a: '[문의하기] 페이지를 통해 접수해 주시면 영업일 기준 1~2일 이내에 회신드립니다.',
},
];
export default function FaqPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: FAQS.map(({ q, a }) => ({
'@type': 'Question',
name: q,
acceptedAnswer: { '@type': 'Answer', text: a },
})),
};
return (
<main className="mx-auto max-w-3xl px-6 py-16 font-body">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<p className="text-sm font-medium tracking-[0.2em] uppercase text-warm-600">FAQ</p>
<h1 className="mt-4 font-display text-4xl font-bold text-ink"> </h1>
<p className="mt-4 text-ink-light">
Startover .
</p>
<div className="mt-12 space-y-4">
{FAQS.map(({ q, a }, i) => (
<details
key={i}
className="group rounded-2xl border border-ink/5 bg-white/70 p-6 backdrop-blur-sm"
>
<summary className="flex cursor-pointer items-start justify-between gap-4 list-none">
<span className="font-display font-semibold text-ink">{q}</span>
<span className="mt-1 shrink-0 text-ink-muted transition-transform group-open:rotate-45">+</span>
</summary>
<p className="mt-4 text-sm leading-relaxed text-ink-light">{a}</p>
</details>
))}
</div>
</main>
);
}
+97 -5
View File
@@ -7,8 +7,51 @@ import { AuthButtons } from './auth-buttons';
import './globals.css';
export const metadata: Metadata = {
title: 'Startover',
description: 'Startover - 폐업 · 양도 · 창업을 잇는 중개 플랫폼',
metadataBase: new URL('https://startover.co.kr'),
title: {
default: 'Startover | 폐업 · 양도 · 창업을 잇다',
template: '%s | Startover',
},
description:
'소상공인의 폐업 · 양도 · 창업을 한 흐름으로 연결합니다. 매장 한 번 등록으로 매수자, 철거·인테리어 업체와 매칭되고 정부 지원금 신청까지 이어집니다.',
keywords: [
'폐업',
'양도',
'창업',
'권리금',
'상가 양도',
'소상공인',
'점포 매매',
'철거',
'인테리어',
'희망리턴패키지',
],
alternates: {
canonical: 'https://startover.co.kr',
},
openGraph: {
title: 'Startover | 폐업 · 양도 · 창업을 잇다',
description: '소상공인의 폐업 · 양도 · 창업을 한 흐름으로 연결하는 중개 플랫폼',
url: 'https://startover.co.kr',
siteName: 'Startover',
locale: 'ko_KR',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Startover | 폐업 · 양도 · 창업을 잇다',
description: '폐업·양도·창업을 잇는 중개 플랫폼',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
other: {
'google-adsense-account': 'ca-pub-9505789508299290',
},
@@ -32,7 +75,7 @@ async function Navigation() {
<Link href="/" className="font-display text-xl font-bold text-ink">
Startover
</Link>
<div className="flex items-center gap-6">
<div className="flex items-center gap-5">
<Link href="/stores" className="text-sm text-ink-light hover:text-warm-600 transition-colors">
</Link>
@@ -48,8 +91,11 @@ async function Navigation() {
<Link href="/vendors" className="text-sm text-ink-light hover:text-warm-600 transition-colors">
</Link>
<Link href="/contracts" className="text-sm text-ink-light hover:text-warm-600 transition-colors">
<Link href="/blog" className="text-sm text-ink-light hover:text-warm-600 transition-colors">
</Link>
<Link href="/faq" className="text-sm text-ink-light hover:text-warm-600 transition-colors">
FAQ
</Link>
{isOperator && (
<Link
@@ -66,6 +112,51 @@ async function Navigation() {
);
}
function Footer() {
return (
<footer className="mt-24 border-t border-ink/5 bg-warm-50/60 font-body">
<div className="mx-auto grid max-w-7xl grid-cols-2 gap-8 px-6 py-12 md:grid-cols-4">
<div>
<p className="font-display text-lg font-bold text-ink">Startover</p>
<p className="mt-2 text-xs leading-relaxed text-ink-muted">
· ·
<br />
</p>
</div>
<div>
<p className="text-xs font-medium tracking-widest uppercase text-ink-muted"></p>
<ul className="mt-3 space-y-2 text-sm">
<li><Link href="/stores" className="text-ink-light hover:text-warm-700"> </Link></li>
<li><Link href="/stores/new" className="text-ink-light hover:text-warm-700"> </Link></li>
<li><Link href="/subsidies" className="text-ink-light hover:text-warm-700"></Link></li>
<li><Link href="/vendors" className="text-ink-light hover:text-warm-700"> </Link></li>
</ul>
</div>
<div>
<p className="text-xs font-medium tracking-widest uppercase text-ink-muted"></p>
<ul className="mt-3 space-y-2 text-sm">
<li><Link href="/about" className="text-ink-light hover:text-warm-700"> </Link></li>
<li><Link href="/blog" className="text-ink-light hover:text-warm-700"></Link></li>
<li><Link href="/faq" className="text-ink-light hover:text-warm-700"> </Link></li>
<li><Link href="/contact" className="text-ink-light hover:text-warm-700"></Link></li>
</ul>
</div>
<div>
<p className="text-xs font-medium tracking-widest uppercase text-ink-muted"></p>
<ul className="mt-3 space-y-2 text-sm">
<li><Link href="/terms" className="text-ink-light hover:text-warm-700"></Link></li>
<li><Link href="/privacy" className="text-ink-light hover:text-warm-700"></Link></li>
</ul>
</div>
</div>
<div className="border-t border-ink/5 px-6 py-4 text-center text-xs text-ink-muted">
© 2026 Startover. All rights reserved.
</div>
</footer>
);
}
export default function RootLayout({
children,
}: Readonly<{
@@ -84,6 +175,7 @@ export default function RootLayout({
<body className="min-h-screen bg-warm-50 font-body">
<Navigation />
{children}
<Footer />
</body>
</html>
);
+88
View File
@@ -0,0 +1,88 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '개인정보처리방침 | Startover',
description: 'Startover 개인정보처리방침입니다.',
alternates: { canonical: 'https://startover.co.kr/privacy' },
};
export default function PrivacyPage() {
return (
<main className="mx-auto max-w-3xl px-6 py-16 font-body">
<h1 className="font-display text-3xl font-bold text-ink"></h1>
<p className="mt-2 text-sm text-ink-muted"> 개정일: 2026년 1 1</p>
<section className="mt-10 space-y-6 text-ink-light leading-relaxed">
<p>
Startover( &quot;&quot;) ,
.
, , 3 .
</p>
<h2 className="font-display text-xl font-bold text-ink">1. </h2>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>·</strong>: , , (), ,
</li>
<li>
<strong> ·</strong>: , , ·
</li>
<li>
<strong> </strong>: IP, ,
</li>
</ul>
<h2 className="font-display text-xl font-bold text-ink">2. </h2>
<ul className="list-disc space-y-2 pl-6">
<li> </li>
<li>, , </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
<h2 className="font-display text-xl font-bold text-ink">3. </h2>
<p>
.
, .
</p>
<ul className="list-disc space-y-2 pl-6">
<li> 기록: 5년</li>
<li> 기록: 5년</li>
<li> 기록: 3년</li>
</ul>
<h2 className="font-display text-xl font-bold text-ink">4. 3 </h2>
<p>
. ,
.
</p>
<ul className="list-disc space-y-2 pl-6">
<li> </li>
<li> </li>
</ul>
<h2 className="font-display text-xl font-bold text-ink">5. </h2>
<p>
· ,
.
</p>
<h2 className="font-display text-xl font-bold text-ink">6. </h2>
<p>
(, ), ,
, · .
</p>
<h2 className="font-display text-xl font-bold text-ink">7. </h2>
<p>
.
</p>
<ul className="list-disc space-y-2 pl-6">
<li>이메일: privacy@startover.co.kr</li>
<li> : <a href="/contact" className="text-warm-700 underline">startover.co.kr/contact</a></li>
</ul>
</section>
</main>
);
}
+14
View File
@@ -0,0 +1,14 @@
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/auth/'],
},
],
sitemap: 'https://startover.co.kr/sitemap.xml',
};
}
+28
View File
@@ -0,0 +1,28 @@
import type { MetadataRoute } from 'next';
import { POSTS } from './blog/posts';
const BASE_URL = 'https://startover.co.kr';
export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date();
const staticRoutes: MetadataRoute.Sitemap = [
{ url: `${BASE_URL}/`, lastModified: now, changeFrequency: 'weekly', priority: 1.0 },
{ url: `${BASE_URL}/about`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 },
{ url: `${BASE_URL}/faq`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 },
{ url: `${BASE_URL}/contact`, lastModified: now, changeFrequency: 'monthly', priority: 0.5 },
{ url: `${BASE_URL}/terms`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
{ url: `${BASE_URL}/privacy`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
{ url: `${BASE_URL}/stores`, lastModified: now, changeFrequency: 'daily', priority: 0.9 },
{ url: `${BASE_URL}/blog`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 },
];
const blogRoutes: MetadataRoute.Sitemap = POSTS.map((post) => ({
url: `${BASE_URL}/blog/${post.slug}`,
lastModified: new Date(post.publishedAt),
changeFrequency: 'monthly',
priority: 0.6,
}));
return [...staticRoutes, ...blogRoutes];
}
+135 -21
View File
@@ -36,6 +36,7 @@ async function handleDeleteDraft(formData: FormData) {
await prisma.$transaction(async (tx) => {
await tx.storeLease.deleteMany({ where: { storeId: store.id } });
await tx.storeSale.deleteMany({ where: { storeId: store.id } });
await tx.storeFacility.deleteMany({ where: { storeId: store.id } });
await tx.store.delete({ where: { id: store.id } });
});
@@ -43,6 +44,24 @@ async function handleDeleteDraft(formData: FormData) {
redirect('/stores');
}
function formatKRW(value: number | null | undefined): string {
if (value == null) return '-';
return `${Number(value).toLocaleString('ko-KR')}`;
}
function formatMargin(sales?: number | null, profit?: number | null): string {
if (!sales || !profit) return '-';
const pct = (Number(profit) / Number(sales)) * 100;
return `${pct.toFixed(1)}%`;
}
function formatPaybackMonths(premium?: number | null, profit?: number | null): string {
if (!premium || !profit) return '-';
const months = Number(premium) / Number(profit);
if (months >= 12) return `${(months / 12).toFixed(1)}`;
return `${months.toFixed(1)}개월`;
}
export default async function StoreDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const prisma = createPrismaClient();
@@ -51,9 +70,15 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id
where: { publicId: id },
include: {
regionCluster: { select: { nameKo: true } },
industryLeaf: { select: { nameKo: true } },
industryLeaf: {
select: { nameKo: true, parent: { select: { nameKo: true } } },
},
lease: true,
sale: true,
facility: true,
photos: {
orderBy: { sortOrder: 'asc' },
},
},
});
@@ -77,6 +102,18 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id
? 'bg-warm-400/15 text-warm-700'
: 'bg-ink/5 text-ink-muted';
const industryLabel = [
store.industryLeaf?.parent?.nameKo,
store.industryLeaf?.nameKo,
]
.filter(Boolean)
.join(' · ');
const premium = store.sale?.premiumAmount ?? store.lease?.premiumAmount ?? null;
const sales = store.sale?.monthlySalesAmount ?? null;
const profit = store.sale?.monthlyProfitAmount ?? null;
const startup = store.sale?.startupCostAmount ?? null;
return (
<main className="mx-auto max-w-4xl px-6 py-10 font-body">
<div className="mb-6">
@@ -101,39 +138,96 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id
<h2 className="font-display text-xl font-bold text-ink mb-4"> </h2>
<div className="grid grid-cols-2 gap-4">
<InfoItem label="지역" value={store.regionCluster?.nameKo ?? '-'} />
<InfoItem label="업종" value={store.industryLeaf?.nameKo ?? '-'} />
<InfoItem label="도로명 주소" value={store.roadAddress} />
<InfoItem label="업종" value={industryLabel || '-'} />
<InfoItem label="주소" value={store.roadAddress} />
<InfoItem label="등록일" value={store.createdAt.toLocaleDateString('ko-KR')} />
</div>
</section>
{/* 매매 정보 */}
<section className="mt-8 rounded-2xl border border-warm-300/40 bg-warm-50/70 p-6">
<h2 className="font-display text-xl font-bold text-ink mb-4"> </h2>
{store.sale?.listingDescription && (
<p className="mb-4 text-sm leading-relaxed text-ink whitespace-pre-line">
{store.sale.listingDescription}
</p>
)}
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
<BigInfoItem label="권리금" value={formatKRW(premium != null ? Number(premium) : null)} accent />
<BigInfoItem label="창업비용" value={formatKRW(startup != null ? Number(startup) : null)} />
<BigInfoItem label="월매출" value={formatKRW(sales != null ? Number(sales) : null)} />
<BigInfoItem label="월수익" value={formatKRW(profit != null ? Number(profit) : null)} accent />
<BigInfoItem
label="월수익률"
value={formatMargin(
sales != null ? Number(sales) : undefined,
profit != null ? Number(profit) : undefined,
)}
/>
<BigInfoItem
label="권리금 회수기간"
value={formatPaybackMonths(
premium != null ? Number(premium) : undefined,
profit != null ? Number(profit) : undefined,
)}
/>
</div>
{(store.sale?.locationHighlight || store.sale?.saleReason) && (
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
{store.sale?.locationHighlight && (
<div className="rounded-xl bg-white/70 p-4">
<p className="text-xs font-medium tracking-widest uppercase text-warm-700"> </p>
<p className="mt-2 text-sm text-ink whitespace-pre-line">
{store.sale.locationHighlight}
</p>
</div>
)}
{store.sale?.saleReason && (
<div className="rounded-xl bg-white/70 p-4">
<p className="text-xs font-medium tracking-widest uppercase text-warm-700"> </p>
<p className="mt-2 text-sm text-ink whitespace-pre-line">{store.sale.saleReason}</p>
</div>
)}
</div>
)}
</section>
{/* 매장 사진 */}
{store.photos.length > 0 && (
<section className="mt-8">
<h2 className="font-display text-xl font-bold text-ink mb-4"> </h2>
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
{store.photos.map((photo) => (
<div
key={photo.id.toString()}
className="aspect-square overflow-hidden rounded-xl bg-warm-100"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={photo.storageKey}
alt="매장 사진"
className="h-full w-full object-cover"
/>
</div>
))}
</div>
</section>
)}
{/* 임대 정보 */}
<section className="mt-8">
<h2 className="font-display text-xl font-bold text-ink mb-4"> </h2>
<div className="grid grid-cols-2 gap-4">
<InfoItem
label="보증금"
value={
store.lease?.depositAmount != null
? `${Number(store.lease.depositAmount).toLocaleString('ko-KR')}`
: '-'
}
value={formatKRW(store.lease?.depositAmount != null ? Number(store.lease.depositAmount) : null)}
/>
<InfoItem
label="월세"
value={
store.lease?.monthlyRentAmount != null
? `${Number(store.lease.monthlyRentAmount).toLocaleString('ko-KR')}`
: '-'
}
/>
<InfoItem
label="권리금"
value={
store.lease?.premiumAmount != null
? `${Number(store.lease.premiumAmount).toLocaleString('ko-KR')}`
: '-'
}
value={formatKRW(store.lease?.monthlyRentAmount != null ? Number(store.lease.monthlyRentAmount) : null)}
/>
<InfoItem
label="임대 잔여 기간"
@@ -219,6 +313,13 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id
)}
</div>
</div>
<p className="mt-6 rounded-2xl bg-warm-100/60 px-5 py-4 text-xs leading-relaxed text-ink-muted">
주의: 창업에이전트는 ,
. · , .
, .
.
</p>
</main>
);
}
@@ -231,3 +332,16 @@ function InfoItem({ label, value }: { label: string; value: string }) {
</div>
);
}
function BigInfoItem({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
return (
<div>
<p className="text-sm text-ink-muted">{label}</p>
<p
className={`mt-1 font-display text-lg font-bold ${accent ? 'text-warm-700' : 'text-ink'}`}
>
{value}
</p>
</div>
);
}
+113
View File
@@ -0,0 +1,113 @@
export type IndustryMajor = {
code: string;
label: string;
children: { code: string; label: string }[];
};
export const INDUSTRY_MAJORS: IndustryMajor[] = [
{
code: 'REST_LIGHT',
label: '휴게음식점',
children: [
{ code: 'REST_LIGHT.CAFE', label: '카페' },
{ code: 'REST_LIGHT.CHICKEN_PIZZA', label: '치킨/피자' },
{ code: 'REST_LIGHT.FASTFOOD', label: '패스트푸드' },
{ code: 'REST_LIGHT.BAKERY', label: '제과점' },
{ code: 'REST_LIGHT.SNACK', label: '분식' },
{ code: 'REST_LIGHT.ICECREAM', label: '아이스크림' },
{ code: 'REST_LIGHT.ETC', label: '기타' },
],
},
{
code: 'REST_FULL',
label: '일반음식점',
children: [
{ code: 'REST_FULL.KOREAN', label: '한식' },
{ code: 'REST_FULL.JAPANESE', label: '일식' },
{ code: 'REST_FULL.CHINESE', label: '중식' },
{ code: 'REST_FULL.SNACK', label: '분식' },
{ code: 'REST_FULL.SEAFOOD', label: '해물' },
{ code: 'REST_FULL.RESTAURANT', label: '레스토랑' },
],
},
{
code: 'BAR',
label: '주류점',
children: [
{ code: 'BAR.BEER', label: '맥주호프' },
{ code: 'BAR.NORAE', label: '노래주점' },
{ code: 'BAR.WINE', label: '와인/위스키바' },
{ code: 'BAR.IZAKAYA', label: '이자카야' },
{ code: 'BAR.POCHA', label: '실내포차' },
{ code: 'BAR.ETC', label: '기타주점' },
],
},
{
code: 'LEISURE',
label: '오락스포츠',
children: [
{ code: 'LEISURE.GYM', label: '헬스장' },
{ code: 'LEISURE.YOGA', label: '요가/필라테스' },
{ code: 'LEISURE.SCREENGOLF', label: '스크린골프장' },
{ code: 'LEISURE.BILLIARD', label: '당구장' },
{ code: 'LEISURE.GOLF_RANGE', label: '골프연습장' },
{ code: 'LEISURE.PCCAFE', label: '피씨방' },
{ code: 'LEISURE.ETC', label: '기타오락' },
],
},
{
code: 'RETAIL',
label: '판매업',
children: [
{ code: 'RETAIL.CVS', label: '편의점' },
{ code: 'RETAIL.SUPERMARKET', label: '슈퍼마켓' },
{ code: 'RETAIL.STATIONERY', label: '문구점' },
{ code: 'RETAIL.COSMETICS', label: '화장품점' },
{ code: 'RETAIL.MOBILE', label: '이동통신점' },
{ code: 'RETAIL.ETC', label: '기타판매' },
],
},
{
code: 'SERVICE',
label: '서비스업',
children: [
{ code: 'SERVICE.HAIR', label: '미용실' },
{ code: 'SERVICE.READING_ROOM', label: '독서실' },
{ code: 'SERVICE.SKIN', label: '피부미용' },
{ code: 'SERVICE.LAUNDRY', label: '빨래방' },
{ code: 'SERVICE.NAIL', label: '네일아트' },
{ code: 'SERVICE.CARWASH', label: '세차장/카센터' },
{ code: 'SERVICE.KIDS_CAFE', label: '키즈카페' },
{ code: 'SERVICE.MASSAGE', label: '마사지' },
{ code: 'SERVICE.ETC', label: '기타서비스업' },
],
},
{
code: 'OTHER',
label: '기타업종',
children: [
{ code: 'OTHER.MEDICAL', label: '병원/약국' },
{ code: 'OTHER.GOSIWON', label: '고시원' },
{ code: 'OTHER.MOTEL', label: '모텔' },
{ code: 'OTHER.ACADEMY', label: '학원' },
{ code: 'OTHER.PENSION', label: '펜션' },
{ code: 'OTHER.REALESTATE', label: '상가매매/임대' },
{ code: 'OTHER.REALESTATE_ETC', label: '기타부동산' },
],
},
];
export function findMajorByLeaf(leafCode: string): string | undefined {
for (const major of INDUSTRY_MAJORS) {
if (major.children.some((c) => c.code === leafCode)) return major.code;
}
return undefined;
}
export function labelForLeaf(leafCode: string): string | undefined {
for (const major of INDUSTRY_MAJORS) {
const c = major.children.find((c) => c.code === leafCode);
if (c) return c.label;
}
return undefined;
}
+45
View File
@@ -11,12 +11,19 @@ export type StoreFormState = {
fieldValues?: {
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;
@@ -29,12 +36,21 @@ export async function createStoreDraftAction(
): Promise<StoreFormState> {
const listingTitle = (formData.get('listingTitle') as string | null)?.trim() ?? '';
const regionClusterCode = (formData.get('regionClusterCode') as string | null) ?? '';
const industryMajorCode = (formData.get('industryMajorCode') 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 monthlySalesAmount = formData.get('monthlySalesAmount') as string | null;
const monthlyProfitAmount = formData.get('monthlyProfitAmount') as string | null;
const startupCostAmount = formData.get('startupCostAmount') as string | null;
const listingDescription =
(formData.get('listingDescription') as string | null)?.trim() ?? '';
const locationHighlight =
(formData.get('locationHighlight') as string | null)?.trim() ?? '';
const saleReason = (formData.get('saleReason') as string | null)?.trim() ?? '';
const exclusiveAreaSqm = formData.get('exclusiveAreaSqm') as string | null;
const floorLevel = formData.get('floorLevel') as string | null;
const kitchenEquipmentSummary =
@@ -43,12 +59,19 @@ export async function createStoreDraftAction(
const fieldValues = {
listingTitle,
regionClusterCode,
industryMajorCode,
industryLeafCode,
roadAddress,
depositAmount: depositAmount ?? undefined,
monthlyRentAmount: monthlyRentAmount ?? undefined,
premiumAmount: premiumAmount ?? undefined,
remainingLeaseMonths: remainingLeaseMonths ?? undefined,
monthlySalesAmount: monthlySalesAmount ?? undefined,
monthlyProfitAmount: monthlyProfitAmount ?? undefined,
startupCostAmount: startupCostAmount ?? undefined,
listingDescription,
locationHighlight,
saleReason,
exclusiveAreaSqm: exclusiveAreaSqm ?? undefined,
floorLevel: floorLevel ?? undefined,
kitchenEquipmentSummary,
@@ -59,6 +82,15 @@ export async function createStoreDraftAction(
return { error: '로그인이 필요합니다.', fieldValues };
}
const hasSale =
premiumAmount ||
monthlySalesAmount ||
monthlyProfitAmount ||
startupCostAmount ||
listingDescription ||
locationHighlight ||
saleReason;
const input: CreateStoreDraftInput = {
ownerUserId: session.user.dbId,
listingTitle,
@@ -77,6 +109,19 @@ export async function createStoreDraftAction(
},
}
: {}),
...(hasSale
? {
sale: {
premiumAmount: premiumAmount ? Number(premiumAmount) : undefined,
monthlySalesAmount: monthlySalesAmount ? Number(monthlySalesAmount) : undefined,
monthlyProfitAmount: monthlyProfitAmount ? Number(monthlyProfitAmount) : undefined,
startupCostAmount: startupCostAmount ? Number(startupCostAmount) : undefined,
listingDescription: listingDescription || undefined,
locationHighlight: locationHighlight || undefined,
saleReason: saleReason || undefined,
},
}
: {}),
...(exclusiveAreaSqm || floorLevel || kitchenEquipmentSummary
? {
facility: {
+119 -22
View File
@@ -1,13 +1,22 @@
'use client';
import { useActionState } from 'react';
import { useActionState, useMemo, useState } from 'react';
import Link from 'next/link';
import { createStoreDraftAction, type StoreFormState } from './actions';
import { INDUSTRY_MAJORS } from '../industries';
const initialState: StoreFormState = {};
export default function NewStorePage() {
const [state, formAction, isPending] = useActionState(createStoreDraftAction, initialState);
const [majorCode, setMajorCode] = useState<string>(
state.fieldValues?.industryMajorCode ?? '',
);
const leafOptions = useMemo(
() => INDUSTRY_MAJORS.find((m) => m.code === majorCode)?.children ?? [],
[majorCode],
);
return (
<main className="mx-auto max-w-3xl px-6 py-10 font-body">
@@ -46,7 +55,7 @@ export default function NewStorePage() {
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-2 gap-4">
<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
@@ -61,23 +70,42 @@ export default function NewStorePage() {
</select>
</div>
<div>
<label className="block text-sm text-ink-muted mb-1"> *</label>
<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={state.fieldValues?.industryLeafCode}
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={!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>
<option value="FNB.CAFE"></option>
<option value="FNB.KOREAN"></option>
<option value="FNB.WESTERN"></option>
<option value="FNB.JAPANESE"></option>
<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>
<label className="block text-sm text-ink-muted mb-1"> *</label>
<input
type="text"
name="roadAddress"
@@ -90,6 +118,87 @@ export default function NewStorePage() {
</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="120000000"
defaultValue={state.fieldValues?.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="150000000"
defaultValue={state.fieldValues?.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="85000000"
defaultValue={state.fieldValues?.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="9900000"
defaultValue={state.fieldValues?.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"
placeholder="예: 역세권 1층 풀오토매장, 최고매출·최고순이익"
defaultValue={state.fieldValues?.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"
placeholder="예: 지하철역 도보 1분, 대로변, 유동인구 많음"
defaultValue={state.fieldValues?.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"
placeholder="예: 건강상의 이유로 매장 정리"
defaultValue={state.fieldValues?.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>
@@ -116,17 +225,6 @@ export default function NewStorePage() {
/>
</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="premiumAmount"
placeholder="30000000"
defaultValue={state.fieldValues?.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
@@ -138,7 +236,6 @@ export default function NewStorePage() {
/>
</div>
</div>
</div>
</section>
{/* 시설 정보 */}
+93 -33
View File
@@ -1,28 +1,60 @@
import Link from 'next/link';
import { createPrismaClient } from '@startover/database';
import { StoreFilters } from './store-filters';
import { INDUSTRY_MAJORS } from './industries';
export const dynamic = 'force-dynamic';
function formatKRWShort(value: number | null | undefined): string {
if (value == null) return '-';
const v = Number(value);
if (v >= 100_000_000) {
const eok = v / 100_000_000;
return `${Number.isInteger(eok) ? eok : eok.toFixed(1)}`;
}
if (v >= 10_000) {
const man = Math.round(v / 10_000);
return `${man.toLocaleString('ko-KR')}`;
}
return `${v.toLocaleString('ko-KR')}`;
}
export default async function StoresPage({
searchParams,
}: {
searchParams: Promise<{ region?: string; industry?: string; status?: string }>;
searchParams: Promise<{ region?: string; industry?: string; industryMajor?: string; status?: string }>;
}) {
const params = await searchParams;
const prisma = createPrismaClient();
const where: Record<string, unknown> = { publicationStatus: 'PUBLISHED' as const };
if (params.region) where['regionCluster'] = { code: params.region };
if (params.industry) where['industryLeaf'] = { code: params.industry };
if (params.industry) {
where['industryLeaf'] = { code: params.industry };
} else if (params.industryMajor) {
const major = INDUSTRY_MAJORS.find((m) => m.code === params.industryMajor);
if (major) {
where['industryLeaf'] = { code: { in: major.children.map((c) => c.code) } };
}
}
if (params.status) where['dealStatus'] = params.status;
const query = {
where,
include: {
regionCluster: { select: { nameKo: true } },
industryLeaf: { select: { nameKo: true } },
lease: { select: { depositAmount: true, monthlyRentAmount: true } },
industryLeaf: {
select: { nameKo: true, parent: { select: { nameKo: true } } },
},
sale: {
select: { premiumAmount: true, monthlyProfitAmount: true, monthlySalesAmount: true },
},
lease: { select: { premiumAmount: true } },
photos: {
where: { isRepresentative: true },
take: 1,
select: { storageKey: true },
},
},
orderBy: { createdAt: 'desc' as const },
};
@@ -55,27 +87,50 @@ export default async function StoresPage({
<StoreFilters />
{/* 매장 목록 */}
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 animate-fade-up">
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 animate-fade-up">
{stores.length === 0 ? (
<div className="col-span-full py-16 text-center text-sm text-ink-muted">
</div>
) : (
stores.map((store: (typeof stores)[number]) => (
stores.map((store) => {
const premium = store.sale?.premiumAmount ?? store.lease?.premiumAmount ?? null;
const profit = store.sale?.monthlyProfitAmount ?? null;
const industryLabel = [
store.industryLeaf?.parent?.nameKo,
store.industryLeaf?.nameKo,
]
.filter(Boolean)
.join('/');
const photoSrc = store.photos[0]?.storageKey ?? null;
return (
<Link
key={store.publicId}
href={`/stores/${store.publicId}`}
className="block rounded-2xl border border-ink/5 bg-white/70 backdrop-blur-sm p-5 card-lift"
className="block overflow-hidden rounded-2xl border border-ink/5 bg-white/70 backdrop-blur-sm card-lift"
>
<div className="flex items-start justify-between">
<h3 className="font-display font-semibold text-ink">{store.listingTitle}</h3>
{/* 사진 영역 */}
<div className="relative aspect-[4/3] w-full bg-warm-100">
{photoSrc ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={photoSrc}
alt={store.listingTitle}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-xs text-ink-muted">
</div>
)}
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
className={`absolute right-3 top-3 rounded-full px-2 py-0.5 text-xs font-medium ${
store.dealStatus === 'OPEN'
? 'bg-sage-500/10 text-sage-600'
? 'bg-sage-500/90 text-white'
: store.dealStatus === 'MATCHING'
? 'bg-warm-400/15 text-warm-700'
: 'bg-ink/5 text-ink-muted'
? 'bg-warm-600/90 text-white'
: 'bg-ink/70 text-white'
}`}
>
{store.dealStatus === 'OPEN'
@@ -85,34 +140,39 @@ export default async function StoresPage({
: store.dealStatus}
</span>
</div>
<p className="mt-1 text-sm text-ink-muted">
{store.regionCluster?.nameKo ?? '-'} · {store.industryLeaf?.nameKo ?? '-'}
{/* 정보 영역 */}
<div className="p-4">
<p className="text-xs text-ink-muted">
{store.regionCluster?.nameKo ?? '-'}
{industryLabel ? ` · ${industryLabel}` : ''}
</p>
<div className="mt-3 flex gap-4 text-sm text-ink-muted">
<span>
{' '}
<span className="font-medium text-ink">
{store.lease?.depositAmount != null
? `${Number(store.lease.depositAmount).toLocaleString('ko-KR')}`
: '-'}
</span>
</span>
<span>
{' '}
<span className="font-medium text-ink">
{store.lease?.monthlyRentAmount != null
? `${Number(store.lease.monthlyRentAmount).toLocaleString('ko-KR')}`
: '-'}
</span>
<h3 className="mt-1 line-clamp-1 font-display font-bold text-ink">
{store.listingTitle}
</h3>
<div className="mt-3 space-y-1">
<div className="flex items-baseline justify-between">
<span className="text-xs text-ink-muted"></span>
<span className="font-display font-bold text-warm-700">
{formatKRWShort(premium != null ? Number(premium) : null)}
</span>
</div>
<div className="flex items-baseline justify-between">
<span className="text-xs text-ink-muted"></span>
<span className="font-display font-bold text-ink">
{formatKRWShort(profit != null ? Number(profit) : null)}
</span>
</div>
</div>
</div>
</Link>
))
);
})
)}
</div>
<div className="mt-8 text-center text-sm text-ink-muted">
서비스: 강남권· F&B
·
</div>
</main>
);
+30 -14
View File
@@ -1,7 +1,8 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { INDUSTRY_MAJORS } from './industries';
const REGION_OPTIONS = [
{ value: '', label: '전체 지역' },
@@ -9,14 +10,6 @@ const REGION_OPTIONS = [
{ value: 'KR.BETA.MAPO_CORE', label: '마포권 (홍대/합정/연남)' },
];
const INDUSTRY_OPTIONS = [
{ value: '', label: '전체 업종' },
{ value: 'FNB.CAFE', label: '카페' },
{ value: 'FNB.KOREAN', label: '한식' },
{ value: 'FNB.WESTERN', label: '양식' },
{ value: 'FNB.JAPANESE', label: '일식' },
];
const STATUS_OPTIONS = [
{ value: '', label: '전체 상태' },
{ value: 'OPEN', label: '거래 가능' },
@@ -28,15 +21,23 @@ export function StoreFilters() {
const router = useRouter();
const searchParams = useSearchParams();
const [majorCode, setMajorCode] = useState<string>(searchParams.get('industryMajor') ?? '');
const subOptions = useMemo(
() => INDUSTRY_MAJORS.find((m) => m.code === majorCode)?.children ?? [],
[majorCode],
);
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const params = new URLSearchParams();
const region = data.get('region') as string;
const industryMajor = data.get('industryMajor') as string;
const industry = data.get('industry') as string;
const status = data.get('status') as string;
if (region) params.set('region', region);
if (industryMajor) params.set('industryMajor', industryMajor);
if (industry) params.set('industry', industry);
if (status) params.set('status', status);
const qs = params.toString();
@@ -62,13 +63,28 @@ export function StoreFilters() {
))}
</select>
<select
name="industry"
defaultValue={searchParams.get('industry') ?? ''}
name="industryMajor"
value={majorCode}
onChange={(e) => setMajorCode(e.target.value)}
className="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"
>
{INDUSTRY_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
<option value=""> </option>
{INDUSTRY_MAJORS.map((m) => (
<option key={m.code} value={m.code}>
{m.label}
</option>
))}
</select>
<select
name="industry"
defaultValue={searchParams.get('industry') ?? ''}
disabled={!majorCode}
className="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>
{subOptions.map((c) => (
<option key={c.code} value={c.code}>
{c.label}
</option>
))}
</select>
+93
View File
@@ -0,0 +1,93 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '이용약관 | Startover',
description: 'Startover 이용약관입니다.',
alternates: { canonical: 'https://startover.co.kr/terms' },
};
export default function TermsPage() {
return (
<main className="mx-auto max-w-3xl px-6 py-16 font-body">
<h1 className="font-display text-3xl font-bold text-ink"></h1>
<p className="mt-2 text-sm text-ink-muted">시행일: 2026년 1 1</p>
<section className="prose mt-10 space-y-6 text-ink-light leading-relaxed">
<h2 className="font-display text-xl font-bold text-ink">1 ()</h2>
<p>
Startover( &quot;&quot;) ··
( &quot;&quot;) ·
.
</p>
<h2 className="font-display text-xl font-bold text-ink">2 ()</h2>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong></strong>: .
</li>
<li>
<strong></strong>: · .
</li>
<li>
<strong></strong>: · .
</li>
<li>
<strong> </strong>: , .
</li>
</ul>
<h2 className="font-display text-xl font-bold text-ink">3 ( )</h2>
<p>
. ,
7 .
</p>
<h2 className="font-display text-xl font-bold text-ink">4 ( )</h2>
<p>
.
</p>
<ul className="list-disc space-y-2 pl-6">
<li> </li>
<li>·· </li>
<li> </li>
<li> , , </li>
</ul>
<h2 className="font-display text-xl font-bold text-ink">5 ( )</h2>
<p>
.
</p>
<ul className="list-disc space-y-2 pl-6">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
<h2 className="font-display text-xl font-bold text-ink">6 ( )</h2>
<p>
,
. ,
, .
</p>
<p>
· .
.
</p>
<h2 className="font-display text-xl font-bold text-ink">7 ( )</h2>
<p>
,
.
.
</p>
<h2 className="font-display text-xl font-bold text-ink">8 ()</h2>
<p>
.
</p>
</section>
</main>
);
}
+15
View File
@@ -119,6 +119,21 @@ export async function createStoreDraftService(
});
}
if (draft.sale) {
await tx.storeSale.create({
data: {
storeId: created.id,
premiumAmount: draft.sale.premiumAmount ?? null,
monthlySalesAmount: draft.sale.monthlySalesAmount ?? null,
monthlyProfitAmount: draft.sale.monthlyProfitAmount ?? null,
startupCostAmount: draft.sale.startupCostAmount ?? null,
listingDescription: draft.sale.listingDescription ?? null,
locationHighlight: draft.sale.locationHighlight ?? null,
saleReason: draft.sale.saleReason ?? null,
},
});
}
await createAuditLog(tx, {
resourceType: 'STORE',
resourceId: created.publicId,
+21
View File
@@ -541,6 +541,7 @@ model Store {
policyVersion PolicyVersion? @relation(fields: [policyVersionId], references: [id])
lease StoreLease?
sale StoreSale?
facility StoreFacility?
lifecycle StoreLifecycle?
photos StorePhoto[]
@@ -575,6 +576,26 @@ model StoreLease {
@@map("store_leases")
}
/// 매매 정보 (권리금/월매출/월수익/창업비용/매물설명 등) (Store 1:1 확장)
model StoreSale {
id BigInt @id @default(autoincrement()) @map("id")
storeId BigInt @unique @map("store_id")
premiumAmount Decimal? @map("premium_amount") @db.Decimal(14, 2)
monthlySalesAmount Decimal? @map("monthly_sales_amount") @db.Decimal(14, 2)
monthlyProfitAmount Decimal? @map("monthly_profit_amount") @db.Decimal(14, 2)
startupCostAmount Decimal? @map("startup_cost_amount") @db.Decimal(14, 2)
listingDescription String? @map("listing_description")
locationHighlight String? @map("location_highlight")
saleReason String? @map("sale_reason")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
// Relations
store Store @relation(fields: [storeId], references: [id])
@@map("store_sales")
}
/// F&B 필터링과 인수 판단에 필요한 설비 정보 (Store 1:1 확장)
/// CHECK constraint: exclusiveAreaSqm > 0 (custom SQL migration)
model StoreFacility {
@@ -1,9 +1,56 @@
code,name_ko,parent_code,depth,sort_order,is_leaf,is_active,is_beta_enabled
FNB,음식점/카페,,0,1,false,true,true
FNB.CAFE,카페,FNB,1,1,true,true,true
FNB.BAKERY,베이커리,FNB,1,2,true,true,true
FNB.KOREAN,한식,FNB,1,3,true,true,true
FNB.CHICKEN,치킨,FNB,1,4,true,true,true
FNB.BAR,주점,FNB,1,5,true,true,true
FNB.DESSERT,디저트,FNB,1,6,true,true,true
FNB.FASTCASUAL,간편식/패스트캐주얼,FNB,1,7,true,true,true
REST_LIGHT,휴게음식점,,0,1,false,true,true
REST_LIGHT.CAFE,카페,REST_LIGHT,1,1,true,true,true
REST_LIGHT.CHICKEN_PIZZA,치킨/피자,REST_LIGHT,1,2,true,true,true
REST_LIGHT.FASTFOOD,패스트푸드,REST_LIGHT,1,3,true,true,true
REST_LIGHT.BAKERY,제과점,REST_LIGHT,1,4,true,true,true
REST_LIGHT.SNACK,분식,REST_LIGHT,1,5,true,true,true
REST_LIGHT.ICECREAM,아이스크림,REST_LIGHT,1,6,true,true,true
REST_LIGHT.ETC,기타,REST_LIGHT,1,7,true,true,true
REST_FULL,일반음식점,,0,2,false,true,true
REST_FULL.KOREAN,한식,REST_FULL,1,1,true,true,true
REST_FULL.JAPANESE,일식,REST_FULL,1,2,true,true,true
REST_FULL.CHINESE,중식,REST_FULL,1,3,true,true,true
REST_FULL.SNACK,분식,REST_FULL,1,4,true,true,true
REST_FULL.SEAFOOD,해물,REST_FULL,1,5,true,true,true
REST_FULL.RESTAURANT,레스토랑,REST_FULL,1,6,true,true,true
BAR,주류점,,0,3,false,true,true
BAR.BEER,맥주호프,BAR,1,1,true,true,true
BAR.NORAE,노래주점,BAR,1,2,true,true,true
BAR.WINE,와인/위스키바,BAR,1,3,true,true,true
BAR.IZAKAYA,이자카야,BAR,1,4,true,true,true
BAR.POCHA,실내포차,BAR,1,5,true,true,true
BAR.ETC,기타주점,BAR,1,6,true,true,true
LEISURE,오락스포츠,,0,4,false,true,true
LEISURE.GYM,헬스장,LEISURE,1,1,true,true,true
LEISURE.YOGA,요가/필라테스,LEISURE,1,2,true,true,true
LEISURE.SCREENGOLF,스크린골프장,LEISURE,1,3,true,true,true
LEISURE.BILLIARD,당구장,LEISURE,1,4,true,true,true
LEISURE.GOLF_RANGE,골프연습장,LEISURE,1,5,true,true,true
LEISURE.PCCAFE,피씨방,LEISURE,1,6,true,true,true
LEISURE.ETC,기타오락,LEISURE,1,7,true,true,true
RETAIL,판매업,,0,5,false,true,true
RETAIL.CVS,편의점,RETAIL,1,1,true,true,true
RETAIL.SUPERMARKET,슈퍼마켓,RETAIL,1,2,true,true,true
RETAIL.STATIONERY,문구점,RETAIL,1,3,true,true,true
RETAIL.COSMETICS,화장품점,RETAIL,1,4,true,true,true
RETAIL.MOBILE,이동통신점,RETAIL,1,5,true,true,true
RETAIL.ETC,기타판매,RETAIL,1,6,true,true,true
SERVICE,서비스업,,0,6,false,true,true
SERVICE.HAIR,미용실,SERVICE,1,1,true,true,true
SERVICE.READING_ROOM,독서실,SERVICE,1,2,true,true,true
SERVICE.SKIN,피부미용,SERVICE,1,3,true,true,true
SERVICE.LAUNDRY,빨래방,SERVICE,1,4,true,true,true
SERVICE.NAIL,네일아트,SERVICE,1,5,true,true,true
SERVICE.CARWASH,세차장/카센터,SERVICE,1,6,true,true,true
SERVICE.KIDS_CAFE,키즈카페,SERVICE,1,7,true,true,true
SERVICE.MASSAGE,마사지,SERVICE,1,8,true,true,true
SERVICE.ETC,기타서비스업,SERVICE,1,9,true,true,true
OTHER,기타업종,,0,7,false,true,true
OTHER.MEDICAL,병원/약국,OTHER,1,1,true,true,true
OTHER.GOSIWON,고시원,OTHER,1,2,true,true,true
OTHER.MOTEL,모텔,OTHER,1,3,true,true,true
OTHER.ACADEMY,학원,OTHER,1,4,true,true,true
OTHER.PENSION,펜션,OTHER,1,5,true,true,true
OTHER.REALESTATE,상가매매/임대,OTHER,1,6,true,true,true
OTHER.REALESTATE_ETC,기타부동산,OTHER,1,7,true,true,true
1 code name_ko parent_code depth sort_order is_leaf is_active is_beta_enabled
2 FNB REST_LIGHT 음식점/카페 휴게음식점 0 1 false true true
3 FNB.CAFE REST_LIGHT.CAFE 카페 FNB REST_LIGHT 1 1 true true true
4 FNB.BAKERY REST_LIGHT.CHICKEN_PIZZA 베이커리 치킨/피자 FNB REST_LIGHT 1 2 true true true
5 FNB.KOREAN REST_LIGHT.FASTFOOD 한식 패스트푸드 FNB REST_LIGHT 1 3 true true true
6 FNB.CHICKEN REST_LIGHT.BAKERY 치킨 제과점 FNB REST_LIGHT 1 4 true true true
7 FNB.BAR REST_LIGHT.SNACK 주점 분식 FNB REST_LIGHT 1 5 true true true
8 FNB.DESSERT REST_LIGHT.ICECREAM 디저트 아이스크림 FNB REST_LIGHT 1 6 true true true
9 FNB.FASTCASUAL REST_LIGHT.ETC 간편식/패스트캐주얼 기타 FNB REST_LIGHT 1 7 true true true
10 REST_FULL 일반음식점 0 2 false true true
11 REST_FULL.KOREAN 한식 REST_FULL 1 1 true true true
12 REST_FULL.JAPANESE 일식 REST_FULL 1 2 true true true
13 REST_FULL.CHINESE 중식 REST_FULL 1 3 true true true
14 REST_FULL.SNACK 분식 REST_FULL 1 4 true true true
15 REST_FULL.SEAFOOD 해물 REST_FULL 1 5 true true true
16 REST_FULL.RESTAURANT 레스토랑 REST_FULL 1 6 true true true
17 BAR 주류점 0 3 false true true
18 BAR.BEER 맥주호프 BAR 1 1 true true true
19 BAR.NORAE 노래주점 BAR 1 2 true true true
20 BAR.WINE 와인/위스키바 BAR 1 3 true true true
21 BAR.IZAKAYA 이자카야 BAR 1 4 true true true
22 BAR.POCHA 실내포차 BAR 1 5 true true true
23 BAR.ETC 기타주점 BAR 1 6 true true true
24 LEISURE 오락스포츠 0 4 false true true
25 LEISURE.GYM 헬스장 LEISURE 1 1 true true true
26 LEISURE.YOGA 요가/필라테스 LEISURE 1 2 true true true
27 LEISURE.SCREENGOLF 스크린골프장 LEISURE 1 3 true true true
28 LEISURE.BILLIARD 당구장 LEISURE 1 4 true true true
29 LEISURE.GOLF_RANGE 골프연습장 LEISURE 1 5 true true true
30 LEISURE.PCCAFE 피씨방 LEISURE 1 6 true true true
31 LEISURE.ETC 기타오락 LEISURE 1 7 true true true
32 RETAIL 판매업 0 5 false true true
33 RETAIL.CVS 편의점 RETAIL 1 1 true true true
34 RETAIL.SUPERMARKET 슈퍼마켓 RETAIL 1 2 true true true
35 RETAIL.STATIONERY 문구점 RETAIL 1 3 true true true
36 RETAIL.COSMETICS 화장품점 RETAIL 1 4 true true true
37 RETAIL.MOBILE 이동통신점 RETAIL 1 5 true true true
38 RETAIL.ETC 기타판매 RETAIL 1 6 true true true
39 SERVICE 서비스업 0 6 false true true
40 SERVICE.HAIR 미용실 SERVICE 1 1 true true true
41 SERVICE.READING_ROOM 독서실 SERVICE 1 2 true true true
42 SERVICE.SKIN 피부미용 SERVICE 1 3 true true true
43 SERVICE.LAUNDRY 빨래방 SERVICE 1 4 true true true
44 SERVICE.NAIL 네일아트 SERVICE 1 5 true true true
45 SERVICE.CARWASH 세차장/카센터 SERVICE 1 6 true true true
46 SERVICE.KIDS_CAFE 키즈카페 SERVICE 1 7 true true true
47 SERVICE.MASSAGE 마사지 SERVICE 1 8 true true true
48 SERVICE.ETC 기타서비스업 SERVICE 1 9 true true true
49 OTHER 기타업종 0 7 false true true
50 OTHER.MEDICAL 병원/약국 OTHER 1 1 true true true
51 OTHER.GOSIWON 고시원 OTHER 1 2 true true true
52 OTHER.MOTEL 모텔 OTHER 1 3 true true true
53 OTHER.ACADEMY 학원 OTHER 1 4 true true true
54 OTHER.PENSION 펜션 OTHER 1 5 true true true
55 OTHER.REALESTATE 상가매매/임대 OTHER 1 6 true true true
56 OTHER.REALESTATE_ETC 기타부동산 OTHER 1 7 true true true
@@ -26,6 +26,16 @@ export interface IndustryChecker {
// Input / Output types
// ---------------------------------------------------------------------------
export interface StoreSaleInput {
readonly premiumAmount?: number;
readonly monthlySalesAmount?: number;
readonly monthlyProfitAmount?: number;
readonly startupCostAmount?: number;
readonly listingDescription?: string;
readonly locationHighlight?: string;
readonly saleReason?: string;
}
export interface CreateStoreDraftInput {
readonly ownerUserId: string;
readonly listingTitle: string;
@@ -34,6 +44,7 @@ export interface CreateStoreDraftInput {
readonly roadAddress: string;
readonly detailAddress?: string;
readonly lease?: StoreLeaseInput;
readonly sale?: StoreSaleInput;
readonly facility?: StoreFacilityInput;
}
@@ -48,6 +59,7 @@ export interface StoreDraft {
readonly publicationStatus: 'PRIVATE';
readonly dealStatus: 'OPEN';
readonly lease?: StoreLeaseProps;
readonly sale?: StoreSaleInput;
readonly facility?: StoreFacilityProps;
}
@@ -76,7 +88,7 @@ function validateRequiredFields(input: CreateStoreDraftInput): readonly FieldErr
errors.push({ field: 'regionClusterCode', message: '지역 클러스터 코드는 필수입니다.' });
}
if (!input.roadAddress.trim()) {
errors.push({ field: 'roadAddress', message: '도로명 주소는 필수입니다.' });
errors.push({ field: 'roadAddress', message: '주소는 필수입니다.' });
}
return errors;
@@ -158,6 +170,7 @@ export function createStoreDraft(
publicationStatus: 'PRIVATE' as const,
dealStatus: 'OPEN' as const,
lease: leaseProps,
sale: input.sale,
facility: facilityProps,
});
}
+1
View File
@@ -10,6 +10,7 @@ export {
type StoreDraft,
type RegionChecker,
type IndustryChecker,
type StoreSaleInput,
} from './create-store-draft.js';
export {
reviewStore,
+3337 -2865
View File
File diff suppressed because it is too large Load Diff