wing-ops/frontend/src/tabs/scat/components/ScatRightPanel.tsx
leedano e4fa46db81 refactor(scat): prediction/scat 파이프라인 제거 + UI 수정
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:15:02 +09:00

246 lines
8.8 KiB
TypeScript
Raw Blame 히스토리

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function ScatRightPanel({ detail, loading, onOpenReport, onNewSurvey }: ScatRightPanelProps) {
const [activeTab, setActiveTab] = useState(0);
if (!detail && !loading) {
return (
<div className="flex flex-col items-center justify-center bg-bg-1 border-l border-border w-[280px] min-w-[280px] h-full">
<div className="text-3xl mb-2">🏖</div>
<div className="text-center text-text-3 text-[11px] leading-relaxed">
<br /> <br /> .
</div>
</div>
);
}
return (
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden h-full w-[280px] min-w-[280px]">
{/* 헤더 */}
<div className="px-3.5 py-2.5 border-b border-border shrink-0">
{detail ? (
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded text-[10px] font-bold text-white"
style={{ background: detail.esiColor || 'var(--cyan)' }}>
{detail.esi}
</span>
<div className="flex-1 min-w-0">
<div className="text-xs font-bold truncate">{detail.name}</div>
<div className="text-[10px] text-text-3">{detail.code}</div>
</div>
</div>
) : (
<div className="text-xs text-text-3"> ...</div>
)}
</div>
{/* 탭 바 */}
<div className="flex border-b border-border shrink-0">
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)}
className={`flex-1 py-2 text-center text-[11px] font-semibold cursor-pointer transition-colors ${
activeTab === tab.id
? 'text-primary-cyan border-b-2 border-primary-cyan'
: 'text-text-3 hover:text-text-2'
}`}>
{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-text-3 text-[11px]">
...
</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-border shrink-0">
<button onClick={onOpenReport}
className="w-full py-2 text-[11px] font-semibold rounded-md cursor-pointer text-primary-cyan"
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-[11px] font-semibold rounded-md cursor-pointer text-status-green"
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="접근성">
<InfoRow label="접근 방법" value={detail.access || '-'} />
<InfoRow label="접근 포인트" value={detail.accessPt || '-'} />
</Section>
{/* 민감 자원 */}
{detail.sensitive && detail.sensitive.length > 0 && (
<Section title="민감 자원">
<div className="flex flex-col gap-1">
{detail.sensitive.map((s, i) => (
<div key={i} className="flex items-start gap-1.5 text-[10px]">
<span className="text-primary-cyan font-bold shrink-0">{s.t}</span>
<span className="text-text-2">{s.v}</span>
</div>
))}
</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-[11px] text-text-3 text-center leading-relaxed">
<br /> .
</div>
</div>
);
}
return (
<div className="flex flex-col gap-2">
<div className="rounded-md overflow-hidden border border-border">
<img
src={imgSrc}
alt={`${detail.name} 해안 사진`}
className="w-full h-auto object-cover"
onError={() => setImgError(true)}
/>
</div>
<div className="text-[10px] text-text-3 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-[10px] font-semibold text-primary-cyan"
style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.2)' }}>
{method}
</span>
))}
</div>
) : (
<div className="text-[10px] text-text-3"> </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-[10px] text-text-2">
<span className="text-status-green font-bold shrink-0"></span>
<span>{c}</span>
</div>
))}
</div>
) : (
<div className="text-[10px] text-text-3"> </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-[10px] text-text-2 leading-[1.6] px-2 py-1.5 rounded bg-bg-0">
{note}
</div>
))}
</div>
) : (
<div className="text-[10px] text-text-3"> </div>
)}
</Section>
</div>
);
}
/* ═══ 공통 UI ═══ */
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="bg-bg-2 border border-border rounded-md p-2.5">
<div className="text-[11px] font-bold mb-2 text-text-2">{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">
<span className="text-[10px] text-text-3">{label}</span>
<span className="text-[10px] font-semibold" style={valueColor ? { color: valueColor } : undefined}>
{value}
</span>
</div>
);
}