286 lines
9.4 KiB
TypeScript
286 lines
9.4 KiB
TypeScript
import { useState } from 'react';
|
||
import type { ScatDetail } from './scatTypes';
|
||
import { sensColor, statusColor } from './scatConstants';
|
||
|
||
interface ScatRightPanelProps {
|
||
detail: ScatDetail | null;
|
||
loading: boolean;
|
||
onOpenReport?: () => void;
|
||
onNewSurvey?: () => void;
|
||
}
|
||
|
||
const tabs = [
|
||
{ id: 0, label: '구간 상세', icon: '📋' },
|
||
{ id: 1, label: '현장 사진', icon: '📷' },
|
||
{ id: 2, label: '방제 권고', icon: '🛡️' },
|
||
] as const;
|
||
|
||
export default function ScatRightPanel({
|
||
detail,
|
||
loading,
|
||
}: Omit<ScatRightPanelProps, 'onOpenReport' | 'onNewSurvey'>) {
|
||
const [activeTab, setActiveTab] = useState(0);
|
||
|
||
if (!detail && !loading) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-full h-full">
|
||
<div className="text-3xl mb-2">🏖️</div>
|
||
<div className="text-center text-fg-disabled text-label-2 leading-relaxed">
|
||
좌측 목록에서 구간을
|
||
<br />
|
||
선택하면 상세 정보가
|
||
<br />
|
||
여기에 표시됩니다.
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden h-full w-full">
|
||
{/* 헤더 */}
|
||
<div className="px-3.5 py-2.5 border-b border-stroke shrink-0">
|
||
{detail ? (
|
||
<div className="flex items-center gap-2">
|
||
<span
|
||
className="px-2 py-0.5 rounded text-caption font-bold text-white"
|
||
style={{ background: detail.esiColor || 'var(--color-accent)' }}
|
||
>
|
||
{detail.esi}
|
||
</span>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-caption font-bold truncate">{detail.name}</div>
|
||
<div className="text-caption text-fg-disabled">{detail.code}</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="text-caption text-fg-disabled">로딩 중...</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 탭 바 */}
|
||
<div className="flex border-b border-stroke shrink-0">
|
||
{tabs.map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id)}
|
||
className={`flex-1 py-2 text-center text-label-2 font-semibold cursor-pointer transition-colors ${
|
||
activeTab === tab.id
|
||
? 'text-color-accent border-b-2 border-color-accent'
|
||
: 'text-fg-disabled hover:text-fg-sub'
|
||
}`}
|
||
>
|
||
{/* {tab.icon} */}
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* 스크롤 영역 */}
|
||
<div className="flex-1 h-0 overflow-y-auto p-2.5 scrollbar-thin">
|
||
{loading ? (
|
||
<div className="flex items-center justify-center h-full text-fg-disabled text-label-2">
|
||
데이터 로딩 중...
|
||
</div>
|
||
) : detail ? (
|
||
<>
|
||
{activeTab === 0 && <DetailTab detail={detail} />}
|
||
{activeTab === 1 && <PhotoTab detail={detail} />}
|
||
{activeTab === 2 && <CleanupTab detail={detail} />}
|
||
</>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* 하단 버튼 */}
|
||
{/* <div className="flex flex-col gap-1.5 p-2.5 border-t border-stroke shrink-0">
|
||
<button onClick={onOpenReport}
|
||
className="w-full py-2 text-label-2 font-semibold rounded-md cursor-pointer text-color-accent"
|
||
style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.25)' }}>
|
||
📄 해안평가 보고서
|
||
</button>
|
||
<button onClick={onNewSurvey}
|
||
className="w-full py-2 text-label-2 font-semibold rounded-md cursor-pointer text-color-success"
|
||
style={{ background: 'rgba(34,197,94,.08)', border: '1px solid rgba(34,197,94,.25)' }}>
|
||
➕ 신규 조사
|
||
</button>
|
||
</div> */}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ═══ 탭 0: 구간 상세 ═══ */
|
||
function DetailTab({ detail }: { detail: ScatDetail }) {
|
||
return (
|
||
<div className="flex flex-col gap-2">
|
||
{/* 기본 정보 */}
|
||
<Section title="기본 정보">
|
||
<InfoRow label="해안 유형" value={detail.type} />
|
||
<InfoRow label="기질" value={detail.substrate} />
|
||
<InfoRow label="구간 길이" value={detail.length} />
|
||
<InfoRow
|
||
label="민감도"
|
||
value={detail.sensitivity}
|
||
valueColor={sensColor[detail.sensitivity]}
|
||
/>
|
||
<InfoRow label="조사 상태" value={detail.status} valueColor={statusColor[detail.status]} />
|
||
<InfoRow label="좌표" value={`${detail.lat.toFixed(4)}°N, ${detail.lng.toFixed(4)}°E`} />
|
||
</Section>
|
||
|
||
{/* 접근성 */}
|
||
<Section title="접근성">
|
||
<div className="flex flex-col gap-2">
|
||
<InfoBlock label="접근 방법" value={detail.access || '-'} />
|
||
<InfoBlock label="접근 포인트" value={detail.accessPt || '-'} />
|
||
</div>
|
||
</Section>
|
||
|
||
{/* 민감 자원 */}
|
||
{detail.sensitive && detail.sensitive.length > 0 && (
|
||
<Section title="민감 자원">
|
||
<div className="flex flex-col gap-2">
|
||
{detail.sensitive.map((s, i) => (
|
||
<InfoBlock key={i} label={s.t} value={s.v} />
|
||
))}
|
||
</div>
|
||
</Section>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ═══ 탭 1: 현장 사진 ═══ */
|
||
function PhotoTab({ detail }: { detail: ScatDetail }) {
|
||
const [imgError, setImgError] = useState(false);
|
||
const imgSrc = `/scat/img/${detail.code}-1.png`;
|
||
|
||
if (imgError) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center py-10 gap-3">
|
||
<div className="text-3xl">📷</div>
|
||
<div className="text-label-2 text-fg-disabled text-center leading-relaxed">
|
||
해당 구간의 사진이
|
||
<br />
|
||
등록되지 않았습니다.
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="rounded-md overflow-hidden border border-stroke">
|
||
<img
|
||
src={imgSrc}
|
||
alt={`${detail.name} 해안 사진`}
|
||
className="w-full h-auto object-cover"
|
||
onError={() => setImgError(true)}
|
||
/>
|
||
</div>
|
||
<div className="text-caption text-fg-disabled text-center">{detail.code} 해안 조사 사진</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ═══ 탭 2: 방제 권고 ═══ */
|
||
function CleanupTab({ detail }: { detail: ScatDetail }) {
|
||
return (
|
||
<div className="flex flex-col gap-2">
|
||
{/* 방제 방법 */}
|
||
<Section title="방제 방법">
|
||
{detail.cleanup && detail.cleanup.length > 0 ? (
|
||
<div className="flex flex-wrap gap-1">
|
||
{detail.cleanup.map((method, i) => (
|
||
<span
|
||
key={i}
|
||
className="px-2 py-0.5 rounded text-caption font-semibold text-color-accent bg-[color-mix(in_srgb,var(--color-accent)_8%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_20%,transparent)]"
|
||
>
|
||
{method}
|
||
</span>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-caption text-fg-disabled">등록된 방제 방법 없음</div>
|
||
)}
|
||
</Section>
|
||
|
||
{/* 종료 기준 */}
|
||
<Section title="종료 기준">
|
||
{detail.endCriteria && detail.endCriteria.length > 0 ? (
|
||
<div className="flex flex-col gap-1">
|
||
{detail.endCriteria.map((c, i) => (
|
||
<div key={i} className="flex items-start gap-1.5 text-caption text-fg-sub">
|
||
<span className="text-color-success font-bold shrink-0">✓</span>
|
||
<span>{c}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-caption text-fg-disabled">등록된 종료 기준 없음</div>
|
||
)}
|
||
</Section>
|
||
|
||
{/* 참고사항 */}
|
||
<Section title="참고사항">
|
||
{detail.notes && detail.notes.length > 0 ? (
|
||
<div className="flex flex-col gap-1">
|
||
{detail.notes.map((note, i) => (
|
||
<div
|
||
key={i}
|
||
className="text-caption text-fg-sub leading-[1.6] px-2 py-1.5 rounded bg-bg-base"
|
||
>
|
||
{note}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-caption text-fg-disabled">등록된 참고사항 없음</div>
|
||
)}
|
||
</Section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ═══ 공통 UI ═══ */
|
||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||
return (
|
||
<div className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
||
<div className="text-label-2 font-bold mb-2 text-fg-sub">{title}</div>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function InfoRow({
|
||
label,
|
||
value,
|
||
valueColor,
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
valueColor?: string;
|
||
}) {
|
||
return (
|
||
<div className="flex items-center justify-between py-0.5 gap-2">
|
||
<span className="text-caption text-fg-disabled shrink-0">{label}</span>
|
||
<span
|
||
className="text-caption text-right min-w-0 break-all"
|
||
style={valueColor ? { color: valueColor } : undefined}
|
||
>
|
||
{value}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function InfoBlock({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<div>
|
||
<div className="text-caption text-fg-disabled mb-0.5">{label}</div>
|
||
<div className="flex items-start gap-1.5 text-caption text-fg-sub pl-1">
|
||
<span className="text-color-accent shrink-0 leading-tight">•</span>
|
||
<span className="break-all">{value}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|