`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
125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
/**
|
|
* 지역 가이드 · 탭 — 맛집/명소/축제를 탭으로 갈아 끼운다.
|
|
* 목록이 길어 스크롤이 부담스러울 때 화면 길이를 1/3 로 줄인다.
|
|
*/
|
|
import {useState} from 'react';
|
|
import {Calendar, MapPin, Utensils} from 'lucide-react';
|
|
import {cn} from '@/lib/utils';
|
|
import {
|
|
EmptyStateNotice,
|
|
ListCard,
|
|
Pill,
|
|
PlaceRow,
|
|
SectionBody,
|
|
SectionFrame,
|
|
SectionHeading,
|
|
} from '../../primitives';
|
|
import type {SectionRenderProps} from '../../types';
|
|
import type {FestivalItem, NearbyPlace} from './types';
|
|
|
|
const naverSearch = (q: string) =>
|
|
`https://search.naver.com/search.naver?query=${encodeURIComponent(q)}`;
|
|
|
|
type TabKey = 'food' | 'spots' | 'festivals';
|
|
|
|
const TABS: {key: TabKey; label: string; icon: typeof Utensils}[] = [
|
|
{key: 'food', label: '맛집 · 카페', icon: Utensils},
|
|
{key: 'spots', label: '명소', icon: MapPin},
|
|
{key: 'festivals', label: '축제', icon: Calendar},
|
|
];
|
|
|
|
export function LocalTabs(props: SectionRenderProps) {
|
|
const {section, isSelected, onSelect, industryId, location, template} = props;
|
|
const [tab, setTab] = useState<TabKey>('food');
|
|
const isStay = industryId === 'stay';
|
|
const tabs = isStay ? TABS : TABS.filter((t) => t.key !== 'festivals');
|
|
const locName = location.split(' ')[0] || '주변';
|
|
// ★ 세 탭 모두 제주 애월 시연용 목록이다 — 실사업장은 빈 목록으로 떨어진다.
|
|
// 지역 정보는 서버(local.local_contents)가 소유하고 캔버스 계약에 없다 — 시연용 목록을 깔지 않는다.
|
|
const foods: NearbyPlace[] = [];
|
|
const spots: NearbyPlace[] = [];
|
|
const festivals: FestivalItem[] = [];
|
|
// 탭을 눌렀는데 아무것도 없으면 섹션이 사라진 것처럼 보인다 — 자리는 지키고 상태만 알린다.
|
|
const isEmpty = (tab === 'food' ? foods : tab === 'spots' ? spots : festivals).length === 0;
|
|
|
|
return (
|
|
<SectionFrame section={section} isSelected={isSelected} onSelect={onSelect} tone="paper">
|
|
<SectionBody>
|
|
<SectionHeading
|
|
variant="eyebrow"
|
|
eyebrow="AI Local Guide"
|
|
title={`AI ${locName} 실시간 가이드`}
|
|
subtitle="탭을 눌러 원하는 정보만 보세요"
|
|
colors={template.colors}
|
|
/>
|
|
|
|
<div className="flex items-center gap-1 rounded-xl border border-stone-200/80 bg-white p-1">
|
|
{tabs.map(({key, label, icon: Icon}) => (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setTab(key);
|
|
}}
|
|
aria-pressed={tab === key}
|
|
style={tab === key ? {backgroundColor: template.colors.primary} : undefined}
|
|
className={cn(
|
|
'flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-lg py-2 text-xs font-semibold transition-colors',
|
|
tab === key ? 'text-white' : 'text-stone-500 hover:bg-stone-100',
|
|
)}
|
|
>
|
|
<Icon className="size-3.5" />
|
|
<span>{label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{isEmpty ? (
|
|
<EmptyStateNotice>주변 정보는 아직 준비 중입니다.</EmptyStateNotice>
|
|
) : (
|
|
<ListCard>
|
|
{tab === 'food' &&
|
|
foods.map((place) => (
|
|
<PlaceRow
|
|
key={place.name}
|
|
name={place.name}
|
|
meta={place.distance}
|
|
description={place.description}
|
|
href={naverSearch(place.searchQuery)}
|
|
colors={template.colors}
|
|
/>
|
|
))}
|
|
|
|
{tab === 'spots' &&
|
|
spots.map((spot) => (
|
|
<PlaceRow
|
|
key={spot.name}
|
|
name={spot.name}
|
|
meta={spot.duration}
|
|
description={spot.description}
|
|
href={naverSearch(spot.searchQuery)}
|
|
actionLabel="상세정보"
|
|
colors={template.colors}
|
|
/>
|
|
))}
|
|
|
|
{tab === 'festivals' &&
|
|
festivals.map((fest) => (
|
|
<PlaceRow
|
|
key={fest.name}
|
|
name={fest.name}
|
|
description={fest.description || fest.period}
|
|
href={naverSearch(fest.searchQuery)}
|
|
actionLabel="검색"
|
|
leading={<Pill tone="accent">{fest.month}</Pill>}
|
|
colors={template.colors}
|
|
/>
|
|
))}
|
|
</ListCard>
|
|
)}
|
|
</SectionBody>
|
|
</SectionFrame>
|
|
);
|
|
}
|