o2o-site-AEO/solution/frontend/src/data/industryData.ts
Mina Choi 362c76d6c9 [fix] solution/frontend: 뒤로가기 후 재검색이 옛 사업장을 되살림 — placeId URL 우선순위 정정
주소창의 placeId 가 store 보다 우선이라(BuilderPage.tsx), 뒤로가기로 검색 단계에
돌아와도 그 placeId 가 그대로 남아 있으면 usePlaceSync 가 옛 사업장을 다시 확정
정보로 채워 넣었다 — 다른 상호로 재검색해도 이전 확정 화면이 그대로 떴다.

- BuilderPage.tsx: step === 'search' 일 땐 주소창 placeId 를 안 쓴다
- Step2PlaceSearch.tsx: 뒤로가기 가드가 store 뿐 아니라 주소창 placeId·flow 도 지운다

[feat] solution/frontend: "고택" 템플릿 — 숙박·요식업·카페 공용, stay2 목업 색·서체 이식

industryData.ts 에 LOOK.paper(크림 종이·가는 명조·그림자 없음, stay2 실물에서 추출)와
paperTemplate() 을 추가해 stay·restaurant·cafe 세 업종의 네 번째 템플릿으로 등록.
DOM 구조 이식은 solution/site 커밋에서 별도로 한다.
2026-09-23 13:17:39 +09:00

335 lines
19 KiB
TypeScript

import type {IndustryData, IndustryType, TemplateItem} from '@o2o/shared';
/**
* 템플릿 세 벌.
*
* ★ 예전엔 업종마다 다섯 벌이었는데 넷이 "흰 바탕 + 고딕 + 둥근 모서리"에 색조만 달랐다 —
* 골라도 뭐가 달라지는지 알 수 없었다. 지금은 서체·모서리·테두리·그림자·여백까지 갈린다.
* 같은 얼굴을 색만 바꿔 늘리지 않는다.
* ★ 업종이 바꾸는 건 accent 하나다. 생김새는 업종이 아니라 취향의 문제다.
*/
const LOOK = {
simple: {
fontHeading: "'Pretendard Variable', 'Noto Sans KR', system-ui, sans-serif",
fontBody: "'Pretendard Variable', 'Noto Sans KR', system-ui, sans-serif",
radius: '0.75rem',
borderWidth: '1px',
shadow: '0 1px 2px rgb(0 0 0 / 0.06)',
headingTracking: '-0.02em',
headingWeight: '700',
sectionSpace: '3.5rem',
},
magazine: {
fontHeading: "'Noto Serif KR', 'Batang', 'Times New Roman', serif",
fontBody: "'Pretendard Variable', 'Noto Sans KR', system-ui, sans-serif",
radius: '0px',
borderWidth: '1px',
shadow: 'none',
headingTracking: '-0.03em',
headingWeight: '700',
sectionSpace: '5rem',
},
retro: {
fontHeading: "'Gugi', 'Noto Sans KR', sans-serif",
fontBody: "'Gowun Batang', 'Noto Serif KR', serif",
radius: '0px',
borderWidth: '2px',
shadow: '4px 4px 0 rgb(27 26 21 / 0.16)',
// 간판체는 자간을 벌리면 글자가 흩어진다. 굵기도 한 벌뿐이라 700 을 주면 가짜 볼드가 씌워진다.
headingTracking: '0em',
headingWeight: '400',
sectionSpace: '4rem',
/**
* 갱지 결 — 가로 3px · 세로 4px 간격의 아주 옅은 줄 두 겹.
*
* ★ 이 칸이 비어 있어서 '옛 항구'를 골라도 면이 매끈했다. 타입(`TemplateLook.texture`)에도
* 있고 발행본이 심는 코드(`seo/head.ts`)도 있는데 **주는 쪽만 없었다** — 색과 서체는
* 레트로인데 종이가 아니라, 인쇄물이 아니라 '갈색 웹페이지'로 보였다.
*/
texture:
'repeating-linear-gradient(0deg,rgba(27,26,21,.028) 0 1px,transparent 1px 3px),repeating-linear-gradient(90deg,rgba(27,26,21,.02) 0 1px,transparent 1px 4px)',
},
paper: {
fontHeading: "'Noto Serif KR', 'AppleMyungjo', 'Nanum Myeongjo', serif",
fontBody: "'Pretendard Variable', 'Noto Sans KR', system-ui, sans-serif",
radius: '0px',
borderWidth: '1px',
shadow: 'none',
headingTracking: '0.03em',
headingWeight: '400',
sectionSpace: '4.5rem',
},
} as const;
/** 업종 하나의 템플릿 세 벌. accent 만 업종이 정한다. */
function templatesFor(
industryId: IndustryType,
accent: string,
/**
* 레트로 한 벌은 업종마다 이름·설명이 다르고, **끄는 섹션과 배리에이션도 다르다.**
* ★ 공통으로 박으면 숙박에서 내린 결정(예약 안내를 안 쓴다)이 음식점의 '노포'까지 따라간다 —
* 거긴 예약·포장 안내가 그 집의 핵심이다.
*/
retro: {
name: string;
description: string;
disabledSectionTypes?: string[];
defaultVariants?: Record<string, string>;
/**
* 이 업종의 **기본 템플릿**으로 세운다 — 목록 맨 앞에 온다.
*
* ★ 순서가 곧 기본값이다. 에디터는 저장된 templateId 가 없으면 업종의 **첫 템플릿**으로
* 그린다(`stores/builder.ts`). 그래서 "기본을 옛 항구로" 는 배열 순서를 바꾸는 일이다.
* ★ 백엔드 `site_payload._DEFAULT_THEME` 의 templateId 와 **같아야 한다.**
* 어긋나면 에디터는 옛 항구로 그리는데 발행본은 다른 템플릿으로 나간다.
*/
isDefault?: boolean;
},
): TemplateItem[] {
const items: TemplateItem[] = [
{
id: `${industryId}-simple`,
industryId,
name: '심플',
tone: 'info',
toneLabel: '깔끔한 기본',
description: '흰 바탕에 고딕. 읽기 쉽고 어디에도 어울립니다. 무엇을 고를지 모르겠으면 이것.',
colors: {
primary: '#18181b',
secondary: '#52525b',
bg: '#ffffff',
card: '#fafafa',
text: '#09090b',
accent,
},
fontStyle: '고딕',
look: LOOK.simple,
},
{
id: `${industryId}-magazine`,
industryId,
name: '매거진',
tone: 'photo',
toneLabel: '잡지 편집',
description: '명조 제목에 각진 모서리, 그림자 없이 선으로만. 사진과 글이 많을 때 품이 납니다.',
colors: {
primary: '#111111',
secondary: '#57534e',
bg: '#ffffff',
card: '#f7f6f4',
text: '#111111',
accent,
},
fontStyle: '명조 제목',
look: LOOK.magazine,
},
{
id: `${industryId}-retro`,
industryId,
name: retro.name,
tone: 'photo',
toneLabel: '레트로 감성',
description: retro.description,
colors: {
// 시안(/s/stay)의 :root 값 그대로 — paper / ink-soft / paper-2.
primary: '#1b1a15',
secondary: '#4c4739',
bg: '#e4dac0',
card: '#efe7d3',
text: '#1b1a15',
// 레트로의 정체성이 이 주(朱) 잉크다 — 업종 accent 로 갈아끼우지 않는다.
accent: '#bf2f1b',
},
fontStyle: '옛 간판체',
look: LOOK.retro,
// 이 템플릿이 팔려는 게 바로 이 아이템들이다.
/**
* 이 템플릿이 데려오는 아이템.
*
* ★ 예전 값('course' · 'schedule')은 **렌더러에 없는 타입**이었다. 시안이 그 둘을
* itinerary · event 로 갈아치웠는데 여기만 남아서, '옛 항구'를 골라도 아이템이
* 하나도 안 붙었다(실측 2026-09-09).
* ★ 가요·인물·연표·엽서·퀴즈는 여기 넣지 않는다 — '지역 이야기'(story) 탭이 데이터가
* 있는 것만 골라 그린다. 따로 붙이면 탭 밖에 한 번 더 선다.
*/
defaultSectionTypes: ['event', 'video', 'festival', 'itinerary', 'story'],
...(retro.disabledSectionTypes ? {disabledSectionTypes: retro.disabledSectionTypes} : {}),
...(retro.defaultVariants ? {defaultVariants: retro.defaultVariants} : {}),
},
];
// 기본 템플릿을 맨 앞으로. 나머지 순서는 그대로 둔다.
return retro.isDefault ? [items[2], items[0], items[1]] : items;
}
function paperTemplate(industryId: IndustryType, description: string): TemplateItem {
return {
id: `${industryId}-paper`,
industryId,
name: '고택',
tone: 'book',
toneLabel: '고택 지면',
description,
colors: {
primary: '#1f1d19',
secondary: '#726c61',
bg: '#fdfcfa',
card: '#f2efe8',
text: '#1f1d19',
accent: '#1f1d19',
},
fontStyle: '정갈한 명조',
look: LOOK.paper,
};
}
export const INDUSTRY_CONFIGS: Record<IndustryType, IndustryData> = {
stay: {
id: 'stay',
name: '숙박',
subName: '펜션 · 스테이',
description: '감성 스테이, 풀빌라, 독채 펜션, 게스트하우스',
channels: [
{ id: 'stay_platforms', name: '야놀자 · 여기어때', checked: true },
{ id: 'naver_place', name: '네이버 플레이스', checked: true },
{ id: 'instagram', name: '인스타그램', checked: true },
],
sections: [
// ★ 백엔드 기본 표(`site_payload._DEFAULT_THEME`)와 id·순서·이름·잠금이 1:1 이어야 한다.
// 어긋나면 에디터에서 본 섹션이 발행본에서 통째로 사라진다.
{ id: 'hero', type: 'hero', name: '히어로', isLocked: true, isEnabled: true, description: '상단 메인 비주얼과 대표 문구' },
{ id: 'intro', type: 'intro', name: '소개', isLocked: false, isEnabled: true, description: '스테이의 철학과 공간 스토리' },
{ id: 'rooms', type: 'rooms', name: '객실 안내', isLocked: false, isEnabled: true, description: '객실 타입, 구조, 비치 물품' },
{ id: 'event', type: 'event', name: '소식', isLocked: false, isEnabled: true, description: '지금 하는 행사 · 공지' },
{ id: 'info', type: 'info', name: '기본 정보', isLocked: true, isEnabled: true, description: '체크인, 주차, 시설 핵심 정보' },
{ id: 'booking', type: 'booking', name: '예약 안내', isLocked: false, isEnabled: true, description: '요금 · 예약 창구 안내' },
{ id: 'video', type: 'video', name: '영상', isLocked: false, isEnabled: true, description: '유튜브 주소 하나면 됩니다' },
{ id: 'photos', type: 'photos', name: '사진 갤러리', isLocked: false, isEnabled: true, description: '감성 인테리어와 외부 풍경' },
{ id: 'map', type: 'map', name: '오시는 길', isLocked: true, isEnabled: true, description: '위치 안내 및 대중교통 경로' },
{ id: 'festival', type: 'festival', name: '계절별 축제', isLocked: false, isEnabled: true, description: '주변에서 열리는 축제 — 계절로 묶어 보여줍니다' },
{ id: 'local', type: 'local', name: '지역 정보', isLocked: false, isEnabled: true, description: '주변 관광지 및 맛집 추천' },
{ id: 'itinerary', type: 'itinerary', name: '추천 일정', isLocked: false, isEnabled: true, description: '숙소에서 출발하는 하루 코스' },
{ id: 'story', type: 'story', name: '지역 이야기', isLocked: false, isEnabled: true, description: '가요·인물·연표·엽서·퀴즈를 탭으로' },
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '고객들이 자주 묻는 질문과 답변' },
{ id: 'weather', type: 'weather', name: '날씨', isLocked: false, isEnabled: true, description: '현재 기온과 사업장 주변 날씨' },
{ id: 'social', type: 'social', name: 'SNS 게시글', isLocked: false, isEnabled: false, description: '승인해 함께 발행한 소식 · 홈페이지 맨 아래' },
],
templates: [
...templatesFor('stay', '#2563eb', {
name: '옛 항구',
description: '갱지 바탕에 간판체. 도넛판·일력·승차권이 함께 들어옵니다. 오래된 항구 도시의 인상으로 묵는 곳을 소개합니다.',
// 숙박의 기본 템플릿 (2026-09-10, 사장님 지시). 백엔드 `_DEFAULT_THEME` 도 stay-retro 다.
isDefault: true,
/** 시안의 사진 갤러리는 캐러셀이다. 색·서체만 맞고 모양이 기본이면 시안이 안 된다. */
defaultVariants: {photos: 'photos.carousel'},
}),
paperTemplate('stay', '크림빛 종이에 가는 명조. 그림자도 장식도 없이, 백 년 된 집의 정갈함을 그대로 보여줍니다.'),
],
},
cafe: {
id: 'cafe',
name: '카페',
subName: '대형카페 · 로스터리',
description: '스페셜티 로스터리, 베이커리 카페, 오션·마운틴 뷰 대형 카페',
channels: [
{ id: 'naver_place', name: '네이버 플레이스', checked: true },
{ id: 'instagram', name: '인스타그램', checked: true },
{ id: 'kakao_map', name: '카카오맵', checked: true },
],
sections: [
{ id: 'hero', type: 'hero', name: '히어로', isLocked: true, isEnabled: true, description: '시그니처 비주얼과 카페 슬로건' },
{ id: 'intro', type: 'intro', name: '소개', isLocked: false, isEnabled: true, description: '로스팅 철학과 공간 스토리' },
{ id: 'menu', type: 'menu', name: '시그니처 메뉴', isLocked: false, isEnabled: true, description: '대표 원두, 음료, 시그니처 디저트' },
{ id: 'info', type: 'info', name: '기본 정보', isLocked: true, isEnabled: true, description: '영업시간, 좌석수, 편의시설 정보' },
{ id: 'space', type: 'space', name: '공간 · 좌석 안내', isLocked: false, isEnabled: true, description: '1/2층 공간 구성 및 야외 테라스석' },
{ id: 'photos', type: 'photos', name: '사진 갤러리', isLocked: false, isEnabled: true, description: '인테리어, 커피, 베이커리 비주얼' },
{ id: 'inquiry', type: 'inquiry', name: '대관 및 단체 문의', isLocked: false, isEnabled: true, description: '촬영 대관 및 단체 예약 접수' },
{ id: 'map', type: 'map', name: '오시는 길', isLocked: true, isEnabled: true, description: '드라이브 코스 및 주차 진입로 안내' },
{ id: 'weather', type: 'weather', name: '날씨', isLocked: false, isEnabled: true, description: '현재 기온과 매장 주변 날씨' },
{ id: 'local', type: 'local', name: '주변 나들이', isLocked: false, isEnabled: true, description: '양평 드라이브 코스 및 명소' },
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '반려견 동반, 주차, 케어키즈존 안내' },
{ id: 'social', type: 'social', name: 'SNS 게시글', isLocked: false, isEnabled: false, description: '승인해 함께 발행한 소식 · 홈페이지 맨 아래' },
],
templates: [
...templatesFor('cafe', '#b45309', {
name: '옛 다방',
description: '갱지 바탕에 간판체. LP 와 손글씨로, 다방 시절의 인상으로 지금의 커피를 이야기합니다.',
}),
paperTemplate('cafe', '크림빛 종이에 가는 명조. 그림자도 장식도 없이, 조용한 카페의 정갈함을 그대로 보여줍니다.'),
],
},
restaurant: {
id: 'restaurant',
name: '음식점',
subName: '식당 · 주점',
description: '한식 다이닝, 일식 오마카세, 이탈리안 비스트로, 고기집',
channels: [
{ id: 'naver_place', name: '네이버 플레이스', checked: true },
{ id: 'catchtable', name: '캐치테이블', checked: true },
{ id: 'instagram', name: '인스타그램', checked: true },
],
sections: [
{ id: 'hero', type: 'hero', name: '히어로', isLocked: true, isEnabled: true, description: '대표 요리 비주얼 및 다이닝 소개' },
{ id: 'intro', type: 'intro', name: '소개', isLocked: false, isEnabled: true, description: '셰프의 조리 철학과 식재료 원산지 이야기' },
{ id: 'menu', type: 'menu', name: '코스 및 메뉴', isLocked: false, isEnabled: true, description: '점심/저녁 코스, 단품 요리, 주류 페어링' },
{ id: 'info', type: 'info', name: '기본 정보', isLocked: true, isEnabled: true, description: '영업시간, 휴무일, 주차, 예약 안내' },
{ id: 'booking', type: 'booking', name: '예약 · 포장 안내', isLocked: false, isEnabled: true, description: '캐치테이블 실시간 룸 예약 및 포장' },
{ id: 'photos', type: 'photos', name: '사진 갤러리', isLocked: false, isEnabled: true, description: '플레이팅, 룸 인테리어, 정갈한 상차림' },
{ id: 'inquiry', type: 'inquiry', name: '단체 행사 문의', isLocked: false, isEnabled: true, description: '상견례, 돌잔치, 기업 대관 문의' },
{ id: 'map', type: 'map', name: '오시는 길', isLocked: true, isEnabled: true, description: '지하철역 출구 및 발렛부스 위치' },
{ id: 'weather', type: 'weather', name: '날씨', isLocked: false, isEnabled: true, description: '현재 기온과 매장 주변 날씨' },
{ id: 'local', type: 'local', name: '주변 안내', isLocked: false, isEnabled: true, description: '청담 명품거리 및 갤러리 안내' },
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '콜키지 정책, 알러지 케어, 주차 안내' },
{ id: 'social', type: 'social', name: 'SNS 게시글', isLocked: false, isEnabled: false, description: '승인해 함께 발행한 소식 · 홈페이지 맨 아래' },
],
templates: [
...templatesFor('restaurant', '#16a34a', {
name: '노포',
description: '갱지 바탕에 간판체. 오래 해온 집이라는 사실 자체가 메뉴판이 됩니다.',
}),
paperTemplate('restaurant', '크림빛 종이에 가는 명조. 고택에서 차리는 한 상처럼, 담백하고 단정한 인상을 남깁니다.'),
],
},
clinic: {
id: 'clinic',
name: '피부과 · 성형외과',
subName: '의원 · 클리닉',
description: '피부과, 성형외과, 미용 클리닉',
channels: [
{ id: 'naver_place', name: '네이버 플레이스', checked: true },
{ id: 'kakao_channel', name: '카카오톡 채널', checked: true },
{ id: 'instagram', name: '인스타그램', checked: true },
],
sections: [
{ id: 'hero', type: 'hero', name: '히어로', isLocked: true, isEnabled: true, description: '병원 대표 이미지와 진료 분야' },
{ id: 'intro', type: 'intro', name: '병원 소개', isLocked: false, isEnabled: true, description: '진료 철학과 의료진 소개' },
{ id: 'programs', type: 'programs', name: '시술 안내', isLocked: false, isEnabled: true, description: '시술명, 소요 시간, 비용' },
{ id: 'info', type: 'info', name: '기본 정보', isLocked: true, isEnabled: true, description: '진료시간, 휴진일, 예약, 주차' },
{ id: 'exhibition', type: 'exhibition', name: '진료 안내', isLocked: false, isEnabled: true, description: '상담 절차와 보험 적용 안내' },
{ id: 'photos', type: 'photos', name: '사진 갤러리', isLocked: false, isEnabled: true, description: '진료실, 상담실, 대기 공간' },
{ id: 'inquiry', type: 'inquiry', name: '상담 문의', isLocked: false, isEnabled: true, description: '방문·전화 상담 접수' },
{ id: 'map', type: 'map', name: '오시는 길', isLocked: true, isEnabled: true, description: '역에서 오는 길과 주차장' },
{ id: 'weather', type: 'weather', name: '날씨', isLocked: false, isEnabled: false, description: '현재 기온과 주변 날씨' },
{ id: 'local', type: 'local', name: '주변 정보', isLocked: false, isEnabled: false, description: '주변 편의시설' },
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '예약 변경, 회복 기간, 주의사항' },
{ id: 'social', type: 'social', name: 'SNS 게시글', isLocked: false, isEnabled: false, description: '승인해 함께 발행한 소식 · 홈페이지 맨 아래' },
],
templates: templatesFor('clinic', '#4A9DC4', {
name: '클린',
description: '여백과 낮은 채도. 과장 없이 정보를 먼저 보여줍니다.',
}),
},
};
/**
* 업종을 아직 모를 때 떨어질 자리.
*
* ★ 스토어의 초기 업종이자, 서버 category 를 못 알아봤을 때의 기본값이다(placeAdapter).
* 두 곳이 다른 값을 쓰면 "빌더가 처음 보여준 화면"과 "사업장을 열었을 때 화면"의
* 섹션·문구가 달라진다 — 사장님 눈에는 값이 바뀐 것으로 보인다.
*/
export const FALLBACK_INDUSTRY: IndustryType = 'stay';