`backend` 옆에 `front` 가 있을 이유가 없었다. negosium 의 negodata/front 를 그대로 베꼈고 그게 왜 front 인지는 따져보지 않았다 — 근거 없이 들여온 이름이라 바로잡는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
131 lines
5.7 KiB
TypeScript
131 lines
5.7 KiB
TypeScript
import {Check} from 'lucide-react';
|
|
import {INDUSTRY_CONFIGS} from '@/data/industryData';
|
|
import {queueSiteTemplateSave} from '@/features/publish/siteTemplate';
|
|
import {cn} from '@/lib/utils';
|
|
import {useBuilderStore} from '@/stores/builder';
|
|
import {WizardFooter} from './WizardFooter';
|
|
import {WizardSteps} from './WizardSteps';
|
|
|
|
export function Step4Template() {
|
|
const industry = useBuilderStore((s) => s.industry);
|
|
const templateId = useBuilderStore((s) => s.templateId);
|
|
const placeId = useBuilderStore((s) => s.placeId);
|
|
const selectTemplate = useBuilderStore((s) => s.selectTemplate);
|
|
const goToStep = useBuilderStore((s) => s.goToStep);
|
|
const startGenerating = useBuilderStore((s) => s.startGenerating);
|
|
|
|
const config = INDUSTRY_CONFIGS[industry];
|
|
|
|
/**
|
|
* 고른 순간 서버에도 남긴다.
|
|
*
|
|
* ★ 화면 상태로만 두면 발행 잡이 읽을 곳이 없어 업종 기본 템플릿으로 굽는다 —
|
|
* 사장님이 고른 디자인과 실제 발행본이 갈린다. 저장은 화면을 기다리게 하지 않는다.
|
|
*/
|
|
const chooseTemplate = (id: string) => {
|
|
selectTemplate(id);
|
|
queueSiteTemplateSave(placeId, id);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-1 flex-col justify-between bg-muted/30">
|
|
<div className="flex flex-1 flex-col items-center justify-center px-4 py-10 sm:px-6 sm:py-14 lg:px-8">
|
|
<div className="mx-auto flex w-full max-w-5xl flex-col">
|
|
<WizardSteps current={4} />
|
|
|
|
<div className="mb-8 text-center">
|
|
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
|
|
원하시는 디자인 스타일을 선택해주세요
|
|
</h1>
|
|
<p className="mx-auto mt-2 max-w-lg text-xs text-muted-foreground sm:text-sm">
|
|
{config.name}에 맞춘 템플릿입니다. 색상과 서체는 에디터에서 언제든 바꿀 수 있고,
|
|
어떤 템플릿을 골라도 구조화 데이터와 필수 섹션은 동일하게 들어갑니다.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-5 lg:grid-cols-3">
|
|
{config.templates.map((template) => {
|
|
const isSelected = templateId === template.id;
|
|
|
|
return (
|
|
<button
|
|
key={template.id}
|
|
type="button"
|
|
onClick={() => chooseTemplate(template.id)}
|
|
aria-pressed={isSelected}
|
|
className={cn(
|
|
'flex cursor-pointer flex-col justify-between rounded-2xl border bg-card p-5 text-left transition-all',
|
|
isSelected
|
|
? 'border-primary ring-1 ring-primary'
|
|
: 'border-border hover:border-muted-foreground/30',
|
|
)}
|
|
>
|
|
<div className="mb-3 flex items-start justify-between">
|
|
<div>
|
|
<h2 className="text-base font-bold">{template.name}</h2>
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
{template.toneLabel} · {template.fontStyle}
|
|
</span>
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
'flex size-5 shrink-0 items-center justify-center rounded-full transition-all',
|
|
isSelected
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'border border-border bg-card',
|
|
)}
|
|
>
|
|
{isSelected && <Check className="size-3" strokeWidth={3} />}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 색·레이아웃 미리보기 와이어프레임 */}
|
|
<div className="my-3 space-y-2.5 rounded-xl border border-border bg-muted/50 p-3.5">
|
|
<div className="flex items-center gap-1.5">
|
|
{(['primary', 'secondary', 'card'] as const).map((key) => (
|
|
<span
|
|
key={key}
|
|
className="size-3.5 rounded-full border border-black/10"
|
|
style={{backgroundColor: template.colors[key]}}
|
|
title={key}
|
|
/>
|
|
))}
|
|
<span className="ml-1 font-mono text-[10px] text-muted-foreground">
|
|
{template.colors.primary}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 pt-1">
|
|
<div
|
|
className="h-2.5 w-2/3 rounded"
|
|
style={{backgroundColor: template.colors.primary, opacity: 0.85}}
|
|
/>
|
|
<div className="h-1.5 w-full rounded bg-border" />
|
|
<div className="h-1.5 w-4/5 rounded bg-border" />
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{template.description}
|
|
</p>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<WizardFooter
|
|
onPrev={() => goToStep(3)}
|
|
onNext={() => {
|
|
// ★ 아무것도 안 누르고 넘어가는 경우(첫 템플릿이 이미 선택돼 있다)도 서버에 남긴다 —
|
|
// 화면이 보여준 그 템플릿이 발행본이 되어야 한다. 같은 값이면 서버가 재빌드 표시도 찍지 않는다.
|
|
queueSiteTemplateSave(placeId, templateId);
|
|
startGenerating();
|
|
}}
|
|
nextLabel="이 템플릿으로 사이트 생성하기"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|