인앱 미니 블로그(AI 자동 포스트, 이메일 승인)·이용후기(즉시 게시)·예약 요청(메일 발송)을
새로 붙였고, 병행해서 /s/stay 목업과 발행 사이트 공통 렌더러(UnitsSection·FestivalSection·
LocalGuideSection·WeatherSection 등)의 UI 버그를 다수 고쳤다. 범위가 넓지만 한 주 분량
작업을 한 커밋으로 묶어 달라는 요청에 따라 하나로 묶는다.
- solution/backend: post/review/booking_request 라우터·서비스·CRUD 추가, 스케줄러에
블로그 초안 생성(새벽 4:10)·발송(아침 9:00) cron 등록, 마이그레이션 4건 추가
- solution/frontend, admin/frontend: 생성된 API 클라이언트 갱신, 리뷰 모더레이션·
블로그 글 관리 페이지 추가
- solution/site/src: 객실 상세+실시간예약(날짜선택·연락처 폼)을 모달로 통합, 축제·
주변안내 카드 클릭 시 모달 전환, 후기 목록 카드 UI, 공용 Modal 컴포넌트 신설,
날씨 문구 동기화 버그 수정(하늘줄·기온줄 한 타이머로), 시설·편의 가능/불가 아이콘
색상 하이라이트, 헤더 메뉴 순서를 실제 섹션 순서에 맞춤, 하단 탭바 아이콘 정렬 버그
(line-height) 수정, 추천일정 점선 연결+데스크톱 자동펼침/모바일 축소, 채널 라벨에
크롤링 원문("NOL")이 새던 것을 bookingLabel() 로 교체
- solution/site/scripts/mockup: /s/stay 패치 스크립트·주입 CSS·JS 다수 수정, stay4~6
빌드 스크립트 추가(다른 세션 작업)
테스트: solution/site `npx tsc --noEmit` 통과, `npx vitest run` 93 passed,
solution/backend `pytest tests/test_booking_request.py` 6 passed(로컬 DB 대상).
예약 요청 메일은 실제 발송까지 확인(place 66894a1b 소유자 이메일 누락을 DB에서 보정).
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
import {Building2, CalendarDays, MessageSquareQuote} from 'lucide-react';
|
|
import {createBrowserRouter, Navigate, Outlet} from 'react-router';
|
|
import {AppShell, type NavItem} from '@/components/layout/AppShell';
|
|
import {RequireAuth} from '@/components/layout/RequireAuth';
|
|
import {LocalContentPage} from '@admin/pages/LocalContentPage';
|
|
import {ReviewModerationPage} from '@admin/pages/ReviewModerationPage';
|
|
import {LoginPage} from '@/pages/LoginPage';
|
|
import {NotFoundPage} from '@/pages/NotFoundPage';
|
|
import {PlaceDetailPage} from '@admin/pages/PlaceDetailPage';
|
|
import {PlaceListPage} from '@admin/pages/PlaceListPage';
|
|
import {SeoAuditPage} from '@admin/pages/SeoAuditPage';
|
|
|
|
/**
|
|
* ★ 내부 메뉴는 여기 있다. AppShell(사장님 앱 소유)에 두면 이 경로 이름들이
|
|
* 사장님 번들에 문자열로 남는다 — 앱을 가른 이유가 사라진다.
|
|
*/
|
|
const ADMIN_NAV: NavItem[] = [
|
|
{to: '/places', match: '/places', label: '사업장', icon: Building2},
|
|
{to: '/local-content', match: '/local-content', label: '지역 콘텐츠', icon: CalendarDays},
|
|
{to: '/reviews', match: '/reviews', label: '이용 후기', icon: MessageSquareQuote},
|
|
];
|
|
|
|
/**
|
|
* 내부 운영 화면. **전부 RequireAuth 뒤에 둔다** — 예외를 하나 두는 순간
|
|
* 그 예외가 기본값이 된다. 사장님 앱과 앱을 가른 이유가 이 규칙을 지키기 위해서다.
|
|
*/
|
|
export const router = createBrowserRouter([
|
|
// selfServe=false: 내부 운영 계정은 우리가 만들어 준다 — 가입 링크도 구글 로그인도 두지 않는다.
|
|
{path: '/login', element: <LoginPage selfServe={false} />},
|
|
|
|
{path: '/', element: <Navigate to="/places" replace />},
|
|
|
|
{
|
|
element: (
|
|
<RequireAuth>
|
|
<AppShell nav={ADMIN_NAV}>
|
|
<Outlet />
|
|
</AppShell>
|
|
</RequireAuth>
|
|
),
|
|
children: [
|
|
{path: '/places', element: <PlaceListPage />},
|
|
{path: '/places/:placeId', element: <PlaceDetailPage />},
|
|
{path: '/places/:placeId/seo', element: <SeoAuditPage />},
|
|
{path: '/local-content', element: <LocalContentPage />},
|
|
{path: '/reviews', element: <ReviewModerationPage />},
|
|
],
|
|
},
|
|
|
|
{path: '*', element: <NotFoundPage />},
|
|
]);
|