업종을 먼저 고르게 하면 경계에서 멈춘다("우리는 카페인가 음식점인가"). 그런데 상호명은
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 통과
151 lines
6.3 KiB
TypeScript
151 lines
6.3 KiB
TypeScript
import {useState} from 'react';
|
|
import {Check} from 'lucide-react';
|
|
import type {IndustryType} from '@o2o/shared';
|
|
import {INDUSTRY_CONFIGS} from '@/data/industryData';
|
|
import {cn} from '@/lib/utils';
|
|
import {useBuilderStore} from '@/stores/builder';
|
|
import {INDUSTRY_ICONS} from './industryIcons';
|
|
import {useWizardStep} from './wizardUrl';
|
|
import {WizardFooter} from './WizardFooter';
|
|
import {WizardSteps} from './WizardSteps';
|
|
|
|
const INDUSTRY_ORDER: IndustryType[] = ['stay', 'cafe', 'restaurant', 'clinic'];
|
|
|
|
/**
|
|
* 업종 직접 고르기 — `?step=industry`.
|
|
*
|
|
* ★ 이 화면은 더 이상 위저드의 첫 화면이 아니다. 업종은 상호 검색 결과의 분류가 정하고
|
|
* (services/place_category.py), 여기는 **못 정했을 때와 바꿀 때만** 들른다.
|
|
* 진행 표시에서 1번을 유지하는 이유도 같다 — 이건 '내 가게 확인' 안의 갈래다.
|
|
*
|
|
* ★ 못 정해서 들어온 경우에는 아무 카드도 선택된 것으로 보이지 않게 한다. 스토어에는 언제나
|
|
* 기본 업종이 들어 있어서, 그걸 그대로 칠하면 사장님은 자기가 고르지도 않은 업종이
|
|
* 이미 정해진 것으로 읽는다.
|
|
*/
|
|
export function Step1Industry() {
|
|
const industry = useBuilderStore((s) => s.industry);
|
|
const pendingPick = useBuilderStore((s) => s.pendingPick);
|
|
const selectIndustry = useBuilderStore((s) => s.selectIndustry);
|
|
const setPendingPick = useBuilderStore((s) => s.setPendingPick);
|
|
const [, goToStep] = useWizardStep();
|
|
|
|
/** 검색이 업종을 못 정해 넘어온 경우 — 고르기 전에는 진행시키지 않는다. */
|
|
const mustChoose = Boolean(pendingPick && !pendingPick.industry);
|
|
const [touched, setTouched] = useState(false);
|
|
const showSelection = touched || !mustChoose;
|
|
|
|
const pick = (id: IndustryType) => {
|
|
setTouched(true);
|
|
selectIndustry(id);
|
|
};
|
|
|
|
/**
|
|
* 고른 업종을 들고 검색 화면으로 돌아간다.
|
|
*
|
|
* ★ 후보를 들고 왔으면 그 후보에 업종을 찍어 돌려보낸다 — 확정(사업장 생성)은 검색 화면
|
|
* 한 곳에서만 한다. 두 화면이 각자 확정하면 같은 가게가 두 번 만들어지는 길이 생긴다.
|
|
*/
|
|
const goBack = () => {
|
|
if (pendingPick) setPendingPick({...pendingPick, industry});
|
|
goToStep('search');
|
|
};
|
|
|
|
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-16 lg:px-8">
|
|
<div className="mx-auto flex w-full max-w-5xl flex-col">
|
|
<WizardSteps current={1} />
|
|
|
|
<div className="mb-10 text-center sm:mb-12">
|
|
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl lg:text-4xl">
|
|
{mustChoose ? '이 가게는 어떤 업종인가요?' : '업종을 바꿀까요?'}
|
|
</h1>
|
|
<p className="mx-auto mt-2.5 max-w-lg text-sm text-muted-foreground sm:text-base">
|
|
{mustChoose
|
|
? '지도 분류만으로는 업종을 정하지 못했습니다. 업종에 맞는 항목만 수집하고, 그 항목만 검증합니다.'
|
|
: '업종을 고르면 그 업종에 맞는 항목만 수집하고, 그 항목만 검증합니다.'}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-5 lg:grid-cols-4">
|
|
{INDUSTRY_ORDER.map((id) => {
|
|
const config = INDUSTRY_CONFIGS[id];
|
|
const Icon = INDUSTRY_ICONS[id];
|
|
const isSelected = showSelection && industry === id;
|
|
|
|
return (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
onClick={() => pick(id)}
|
|
aria-pressed={isSelected}
|
|
className={cn(
|
|
'flex cursor-pointer flex-col justify-between rounded-2xl border bg-card p-6 text-left transition-all',
|
|
isSelected
|
|
? 'border-primary ring-1 ring-primary'
|
|
: 'border-border hover:border-muted-foreground/30',
|
|
)}
|
|
>
|
|
<div className="mb-5 flex items-center justify-between">
|
|
<span
|
|
className={cn(
|
|
'flex size-10 items-center justify-center rounded-xl transition-colors',
|
|
isSelected
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted text-foreground',
|
|
)}
|
|
>
|
|
<Icon className="size-5" />
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
'flex size-5 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="mb-2">
|
|
<h2 className="text-base font-bold sm:text-lg">{config.name}</h2>
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
{config.subName}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-xs leading-relaxed text-muted-foreground sm:text-sm">
|
|
{config.description}
|
|
</p>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<WizardFooter
|
|
hint={
|
|
showSelection ? (
|
|
<>
|
|
선택된 업종:{' '}
|
|
<strong className="font-semibold text-foreground">
|
|
{INDUSTRY_CONFIGS[industry].name}
|
|
</strong>{' '}
|
|
({INDUSTRY_CONFIGS[industry].subName})
|
|
</>
|
|
) : (
|
|
'업종을 하나 골라 주세요.'
|
|
)
|
|
}
|
|
onPrev={() => goToStep('search')}
|
|
onNext={goBack}
|
|
nextDisabled={!showSelection}
|
|
nextLabel={pendingPick ? '이 업종으로 계속하기' : '내 가게 찾기로 돌아가기'}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|