업종을 먼저 고르게 하면 경계에서 멈춘다("우리는 카페인가 음식점인가"). 그런데 상호명은
100% 안다. 그리고 업종은 AI 를 부를 필요가 없다 — 카카오·네이버 검색 응답에 분류가
이미 들어 있고(category_group_code), 지금까지 받아 놓고 안 썼다.
- 단계가 스토어에서 주소창으로: ?step=search|industry|collect|template|generating|editor.
번호가 아니라 이름인 이유 — 단계가 4→3 으로 줄어 옛 북마크가 다른 화면을 연다
- 시작점이 상호명 검색(Step2PlaceSearch)이다. 업종 선택은 못 정했을 때의 갈래로 남는다
- 업종은 후보의 category 로 잡히고, 못 정하면 고르게 하고, 정해져도 [바꾸기] 로 바꾼다.
★ 확정 뒤 변경은 신원 확인부터 다시 받는다 — Req_UpdatePlace 에 category 가 없어
PATCH 로 못 고치고, placeAdapter 가 리페치마다 덮어써서 조용히 되돌아간다
- 랜딩 진입: ?new=1 · ?q=<상호명> · ?industry=<업종>. 한 번 읽고 replace 로 지운다
- ★ ?new=1 이 setSearchParams({}) 로 **모든 쿼리를 날리던 것**을 고쳤다 — q 가 읽히기 전에 사라졌다
- selectIndustry 가 사장님이 친 상호·위치를 지우던 것도 고쳤다(업종이 첫 화면일 땐 늘 빈 값이라 안 보였다)
- 로고는 어디서나 / 로 간다. 에디터에서는 span 이라 아예 안 눌렸다
tsc·eslint·vite build 통과
183 lines
6.8 KiB
TypeScript
183 lines
6.8 KiB
TypeScript
import {Check} from 'lucide-react';
|
|
import type {TemplateItem} from '@o2o/shared';
|
|
import {INDUSTRY_CONFIGS} from '@/data/industryData';
|
|
import {queueSiteTemplateSave} from '@/features/publish/siteTemplate';
|
|
import {cn} from '@/lib/utils';
|
|
import {useBuilderStore} from '@/stores/builder';
|
|
import {useWizardStep} from './wizardUrl';
|
|
import {WizardFooter} from './WizardFooter';
|
|
import {WizardSteps} from './WizardSteps';
|
|
|
|
|
|
/**
|
|
* 템플릿 미리보기 — 그 템플릿의 서체·모서리·테두리·그림자로 **실제로** 그린다.
|
|
*
|
|
* ★ 예전에는 회색 막대 세 줄과 색 동그라미였다. 다섯 템플릿이 전부 같은 그림이라
|
|
* 무엇을 고르는지 알 수 없었고, 그래서 아무거나 골랐다.
|
|
*/
|
|
function TemplatePreview({template}: {template: TemplateItem}) {
|
|
const {look, colors} = template;
|
|
|
|
return (
|
|
<div
|
|
className="my-3 overflow-hidden p-3.5"
|
|
style={{
|
|
backgroundColor: colors.bg,
|
|
color: colors.text,
|
|
borderRadius: look.radius,
|
|
border: `${look.borderWidth} solid ${colors.secondary}22`,
|
|
}}
|
|
>
|
|
<p
|
|
className="text-[9px] uppercase"
|
|
style={{color: colors.accent, letterSpacing: '0.22em', fontFamily: look.fontBody}}
|
|
>
|
|
Section
|
|
</p>
|
|
<p
|
|
className="mt-1.5 text-base leading-tight"
|
|
style={{
|
|
fontFamily: look.fontHeading,
|
|
fontWeight: look.headingWeight,
|
|
letterSpacing: look.headingTracking,
|
|
}}
|
|
>
|
|
오래 머무는 자리
|
|
</p>
|
|
<p
|
|
className="mt-1.5 text-[10px] leading-relaxed"
|
|
style={{fontFamily: look.fontBody, color: colors.secondary}}
|
|
>
|
|
제목은 {template.fontStyle}, 본문은 이 서체로 나갑니다.
|
|
</p>
|
|
|
|
<div
|
|
className="mt-3 p-2"
|
|
style={{
|
|
backgroundColor: colors.card,
|
|
borderRadius: `calc(${look.radius} * 0.67)`,
|
|
border: `${look.borderWidth} solid ${colors.secondary}22`,
|
|
boxShadow: look.shadow,
|
|
}}
|
|
>
|
|
<span className="text-[9px]" style={{fontFamily: look.fontBody, color: colors.secondary}}>
|
|
카드 · 모서리 {look.radius === '0px' ? '각짐' : '둥금'} · 그림자{' '}
|
|
{look.shadow === 'none' ? '없음' : '있음'}
|
|
</span>
|
|
</div>
|
|
|
|
<span
|
|
className="mt-2.5 inline-block px-2.5 py-1 text-[9px] font-semibold"
|
|
style={{
|
|
backgroundColor: colors.accent,
|
|
color: colors.bg,
|
|
borderRadius: `calc(${look.radius} * 0.5)`,
|
|
fontFamily: look.fontBody,
|
|
}}
|
|
>
|
|
예약 문의
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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] = useWizardStep();
|
|
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={3} />
|
|
|
|
<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>
|
|
|
|
<TemplatePreview template={template} />
|
|
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{template.description}
|
|
</p>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<WizardFooter
|
|
onPrev={() => goToStep('collect')}
|
|
onNext={() => {
|
|
// ★ 아무것도 안 누르고 넘어가는 경우(첫 템플릿이 이미 선택돼 있다)도 서버에 남긴다 —
|
|
// 화면이 보여준 그 템플릿이 발행본이 되어야 한다. 같은 값이면 서버가 재빌드 표시도 찍지 않는다.
|
|
queueSiteTemplateSave(placeId, templateId);
|
|
startGenerating();
|
|
goToStep('generating');
|
|
}}
|
|
nextLabel="이 템플릿으로 사이트 생성하기"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|