o2o-site-AEO/solution/frontend/src/features/builder/EditorHeader.tsx
Mina Choi a61f9724ea [feat] solution/frontend: 온보딩을 상호명부터 — 단계를 주소창으로 옮기고 업종은 검색이 정한다
업종을 먼저 고르게 하면 경계에서 멈춘다("우리는 카페인가 음식점인가"). 그런데 상호명은
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 통과
2026-09-03 10:20:57 +09:00

98 lines
4.4 KiB
TypeScript

import {Eye, Pencil, Send} from 'lucide-react';
import {Link} from 'react-router';
import {SiteStatus} from '@o2o/shared';
import {getAccessToken, useGetSite} from '@/api';
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
import {INDUSTRY_CONFIGS} from '@/data/industryData';
import {useBuilderStore, useUnverifiedFields} from '@/stores/builder';
export function EditorHeader() {
const industry = useBuilderStore((s) => s.industry);
const storeName = useBuilderStore((s) => s.storeName);
const isPreviewMode = useBuilderStore((s) => s.isPreviewMode);
const togglePreview = useBuilderStore((s) => s.togglePreview);
const openPublishModal = useBuilderStore((s) => s.openPublishModal);
const placeId = useBuilderStore((s) => s.placeId);
const unverifiedCount = useUnverifiedFields().length;
/**
* 한 번이라도 발행했는가 — 버튼 문구가 [사이트 발행] 인지 [사이트 재발행] 인지를 가른다.
*
* ★ usePlaceSync 가 이미 읽는 같은 쿼리다(같은 키 → 같은 캐시). 여기서 다시 부른다고
* 요청이 한 번 더 나가지 않는다.
*/
const siteQuery = useGetSite(placeId ?? '', {
query: {enabled: Boolean(placeId) && Boolean(getAccessToken())},
});
const isRepublish = siteQuery.data?.site?.status === SiteStatus.PUBLISHED;
return (
<header className="z-30 flex h-14 shrink-0 select-none items-center justify-between border-b border-border bg-card px-4 sm:px-6">
<div className="flex min-w-0 items-center gap-3.5">
{/* ★ 실사업장을 편집 중이면 로고는 **아무 데도 가지 않는다.**
예전엔 사업장 목록(/places)으로 갔지만 그 화면은 내부 운영 앱(admin)으로 나갔다 —
여기 링크를 남기면 사장님 앱에서 404 이고, 내부 경로 이름이 사장님 번들에도 남는다.
reset() 을 부르지 않는 이유는 그대로다: 편집하던 가게를 떠나 데모 위저드로
떨어지면 사장님 눈에는 자기 가게가 사라진 것으로 보인다.
(사장님용 "내 사이트 관리"가 생기면 그때 그리로 잇는다 — ARCHITECTURE.md 4절)
★ 데모에서 돌아가는 길은 `?new=1` 이다 — 스토어를 직접 비우면 화면만 위저드로 바뀌고
주소는 그대로라, 뒤로가기가 편집기로 돌아오지 않는다(단계는 주소창이 소유한다). */}
{placeId ? (
// 편집 중이어도 로고는 홈으로 간다 — 눌리지 않는 로고는 고장으로 읽힌다.
// 입력값은 서버에 저장되므로 나갔다 들어와도 그대로다.
<Link to="/" title="처음으로 이동" className="group flex items-center gap-2">
<LogoMark />
</Link>
) : (
<Link to="/" title="처음으로 이동" className="group flex items-center gap-2">
<LogoMark />
</Link>
)}
<span className="hidden h-3.5 w-px bg-border sm:block" />
<div className="flex min-w-0 items-center gap-2">
<span className="max-w-[140px] truncate text-xs font-semibold sm:max-w-[220px] sm:text-sm">
{storeName}
</span>
<Badge className="hidden sm:inline-flex">{INDUSTRY_CONFIGS[industry].name}</Badge>
{unverifiedCount > 0 && (
<Badge variant="warning" className="hidden sm:inline-flex">
확인 필요 {unverifiedCount}
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant={isPreviewMode ? 'contrast' : 'outline'}
size="sm"
onClick={togglePreview}
>
{isPreviewMode ? <Pencil /> : <Eye />}
<span>{isPreviewMode ? '편집 모드' : '미리보기'}</span>
</Button>
{/* 발행 = 빌드다. 누르면 HTML 을 다시 굽는다(usePublishSite 주석 참고). */}
<Button variant="primary" size="sm" onClick={openPublishModal}>
<Send />
<span>{isRepublish ? '사이트 재발행' : '사이트 발행'}</span>
</Button>
</div>
</header>
);
}
function LogoMark() {
return (
<img
src="/brand/web4ai-wordmark.svg"
alt="Web4Ai"
className="h-6 w-auto opacity-100 transition-opacity group-hover:opacity-60"
/>
);
}