fix(dev): /dev/clinics 페이지 제거 — list 엔드포인트 사라져 빌드 깨짐
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>main
parent
b4691b029a
commit
ea7b21dee3
|
|
@ -1,159 +0,0 @@
|
||||||
/**
|
|
||||||
* Dev: 클리닉 리스트 페이지.
|
|
||||||
*
|
|
||||||
* 백엔드 `GET /api/clinics` (getClinics) 응답을 표로 확인하는 개발용 화면.
|
|
||||||
* 운영 도메인 노출 방지는 라우트 단의 DevOnly 가드에 위임.
|
|
||||||
*/
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { useGetClinics } 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 } = useGetClinics(
|
|
||||||
{ limit: PAGE_SIZE, offset },
|
|
||||||
{ query: { staleTime: 0 } },
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = data?.status === 200 ? data.data.items : [];
|
|
||||||
const total = data?.status === 200 ? data.data.total : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pt-20 pb-16">
|
|
||||||
<PageContainer>
|
|
||||||
<header className="flex items-end justify-between mb-6 gap-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-semibold text-[#6C5CE7] tracking-widest uppercase mb-2">
|
|
||||||
Dev · Clinics
|
|
||||||
</p>
|
|
||||||
<h1 className="font-serif text-3xl font-bold text-[#0A1128]">
|
|
||||||
클리닉 리스트
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-slate-500 mt-1">
|
|
||||||
GET /api/clinics · limit={PAGE_SIZE} · offset={offset} · total={total}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
disabled={isFetching}
|
|
||||||
>
|
|
||||||
{isFetching ? <Spinner size="xs" /> : null}
|
|
||||||
새로고침
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center py-20">
|
|
||||||
<Spinner size="lg" className="text-[#6C5CE7]" />
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
||||||
클리닉 리스트 조회 실패: {String((error as Error)?.message ?? error)}
|
|
||||||
</div>
|
|
||||||
) : items.length === 0 ? (
|
|
||||||
<EmptyState size="lg" hint="등록된 클리닉이 없습니다." />
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto rounded-2xl border border-slate-200 bg-white shadow-[3px_4px_12px_rgba(0,0,0,0.04)]">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-slate-50 text-slate-600">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">병원명</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">EN</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">상태</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">URL</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">주소</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">생성일</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium font-mono text-xs">hospital_id</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-100">
|
|
||||||
{items.map((c) => (
|
|
||||||
<tr key={c.hospital_id} className="hover:bg-slate-50/50">
|
|
||||||
<td className="px-4 py-3 font-medium text-[#0A1128]">
|
|
||||||
<Link
|
|
||||||
to={`/clinics/${c.hospital_id}`}
|
|
||||||
className="text-[#0A1128] hover:text-[#6C5CE7] hover:underline"
|
|
||||||
>
|
|
||||||
{c.hospital_name}
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-slate-600">{c.hospital_name_en ?? '—'}</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-700">
|
|
||||||
{c.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-slate-600 max-w-[240px] truncate">
|
|
||||||
{c.url ? (
|
|
||||||
<a
|
|
||||||
href={c.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-[#6C5CE7] hover:underline"
|
|
||||||
>
|
|
||||||
{c.url}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
'—'
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-slate-600 max-w-[260px] truncate">{c.road_address ?? '—'}</td>
|
|
||||||
<td className="px-4 py-3 text-slate-500 whitespace-nowrap">{formatDate(c.created_at)}</td>
|
|
||||||
<td className="px-4 py-3 font-mono text-xs text-slate-400">{c.hospital_id}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 mt-4">
|
|
||||||
<span className="text-xs text-slate-500 mr-2">
|
|
||||||
{total > 0 ? `${offset + 1}-${Math.min(offset + items.length, total)} / ${total}` : ''}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}
|
|
||||||
disabled={offset === 0 || isFetching}
|
|
||||||
>
|
|
||||||
이전
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setOffset((o) => o + PAGE_SIZE)}
|
|
||||||
disabled={offset + items.length >= total || isFetching}
|
|
||||||
>
|
|
||||||
다음
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PageContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { lazy } from 'react'
|
||||||
import DevOnly from './components/DevOnly'
|
import DevOnly from './components/DevOnly'
|
||||||
|
|
||||||
const ComponentsPage = lazy(() => import('./pages/ComponentsPage'))
|
const ComponentsPage = lazy(() => import('./pages/ComponentsPage'))
|
||||||
const ClinicsPage = lazy(() => import('./pages/ClinicsPage'))
|
|
||||||
const TestPrefillPage = lazy(() => import('./pages/TestPrefillPage'))
|
const TestPrefillPage = lazy(() => import('./pages/TestPrefillPage'))
|
||||||
|
|
||||||
// `/dev/*` 와 `/test` 는 DevOnly 가드를 거쳐 로컬호스트에서만 접근 가능.
|
// `/dev/*` 와 `/test` 는 DevOnly 가드를 거쳐 로컬호스트에서만 접근 가능.
|
||||||
|
|
@ -11,7 +10,6 @@ export const devRoutes = [
|
||||||
element: <DevOnly />,
|
element: <DevOnly />,
|
||||||
children: [
|
children: [
|
||||||
{ path: 'dev/components', element: <ComponentsPage /> },
|
{ path: 'dev/components', element: <ComponentsPage /> },
|
||||||
{ path: 'dev/clinics', element: <ClinicsPage /> },
|
|
||||||
{ path: 'test', element: <TestPrefillPage /> },
|
{ path: 'test', element: <TestPrefillPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue