- className 중복 속성 31건 수정 (12파일) - KOSPS codeBox spread TypeError 해결 - HNS 페놀(C₆H₅OH) 물질 데이터 추가 - ScatRightPanel 280px 우측 패널 신규 구현 (3탭+액션버튼) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
228 lines
8.3 KiB
TypeScript
228 lines
8.3 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, 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 />}
|
||
{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() {
|
||
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 className="px-3 py-1.5 rounded text-[10px] text-text-3"
|
||
style={{ background: 'rgba(6,182,212,.06)', border: '1px solid rgba(6,182,212,.15)' }}>
|
||
사진 업로드 API 연동 예정
|
||
</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>
|
||
);
|
||
}
|