diff --git a/src/features/dev/components/DevOnly.tsx b/src/features/dev/components/DevOnly.tsx new file mode 100644 index 0000000..f660205 --- /dev/null +++ b/src/features/dev/components/DevOnly.tsx @@ -0,0 +1,28 @@ +/** + * DevOnly — `/dev/*` 라우트 가드. + * + * `window.location.hostname` 이 로컬호스트 계열이 아닐 경우 루트로 리다이렉트. + * 클라이언트 사이드 가드라 보안 의미보다는 "운영 도메인에서 실수로 노출되는 것 방지" 용도. + * 진짜 차단이 필요하면 서버/CDN 레벨에서 경로를 막아야 한다. + */ +import { Navigate, Outlet } from 'react-router'; + +const LOCAL_HOSTNAMES = new Set([ + 'localhost', + '127.0.0.1', + '0.0.0.0', + '::1', + '[::1]', +]); + +function isLocalHost(): boolean { + if (typeof window === 'undefined') return false; + return LOCAL_HOSTNAMES.has(window.location.hostname); +} + +export default function DevOnly() { + if (!isLocalHost()) { + return ; + } + return ; +} diff --git a/src/features/dev/pages/ClinicsPage.tsx b/src/features/dev/pages/ClinicsPage.tsx new file mode 100644 index 0000000..2cd9d26 --- /dev/null +++ b/src/features/dev/pages/ClinicsPage.tsx @@ -0,0 +1,148 @@ +/** + * Dev: 클리닉 리스트 페이지. + * + * 백엔드 `GET /api/clinics` (listClinics) 응답을 표로 확인하는 개발용 화면. + * 운영 도메인 노출 방지는 라우트 단의 DevOnly 가드에 위임. + */ +import { useState } from 'react'; +import { useListClinics } from '@/shared/api/generated/clinics/clinics'; +import { PageContainer } from '@/shared/ui/page-container'; +import { Spinner } from '@/shared/ui/spinner'; +import { EmptyState } from '@/shared/ui/empty-state'; +import { Button } from '@/shared/ui/button'; + +const PAGE_SIZE = 50; + +function formatDate(raw: string): string { + try { + return new Date(raw).toLocaleString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return raw; + } +} + +export default function ClinicsPage() { + const [offset, setOffset] = useState(0); + + const { data, isLoading, error, refetch, isFetching } = useListClinics( + { limit: PAGE_SIZE, offset }, + { query: { staleTime: 0 } }, + ); + + const items = data?.status === 200 ? data.data : []; + + return ( +
+ +
+
+

+ Dev · Clinics +

+

+ 클리닉 리스트 +

+

+ GET /api/clinics · limit={PAGE_SIZE} · offset={offset} +

+
+
+ +
+
+ + {isLoading ? ( +
+ +
+ ) : error ? ( +
+ 클리닉 리스트 조회 실패: {String((error as Error)?.message ?? error)} +
+ ) : items.length === 0 ? ( + + ) : ( +
+ + + + + + + + + + + + + + {items.map((c) => ( + + + + + + + + + + ))} + +
병원명EN상태URL주소생성일hospital_id
{c.hospital_name}{c.hospital_name_en ?? '—'} + + {c.status} + + + {c.url ? ( + + {c.url} + + ) : ( + '—' + )} + {c.road_address ?? '—'}{formatDate(c.created_at)}{c.hospital_id}
+
+ )} + + {/* 페이지네이션 — 정확한 total 이 없어서 단순 prev/next 만 노출 */} +
+ + +
+
+
+ ); +} diff --git a/src/features/dev/routes.tsx b/src/features/dev/routes.tsx index 2b9968c..9ec7c18 100644 --- a/src/features/dev/routes.tsx +++ b/src/features/dev/routes.tsx @@ -1,7 +1,16 @@ import { lazy } from 'react' +import DevOnly from './components/DevOnly' const ComponentsPage = lazy(() => import('./pages/ComponentsPage')) +const ClinicsPage = lazy(() => import('./pages/ClinicsPage')) +// `/dev/*` 는 DevOnly 가드를 거쳐 로컬호스트에서만 접근 가능. export const devRoutes = [ - { path: 'dev/components', element: }, + { + element: , + children: [ + { path: 'dev/components', element: }, + { path: 'dev/clinics', element: }, + ], + }, ]