wing-ops/frontend/src/tabs/scat/components/ScatRightPanel.tsx
htlee 34cf046787 fix(css): CSS 회귀 버그 3건 수정 + SCAT 우측 패널 구현
- className 중복 속성 31건 수정 (12파일)
- KOSPS codeBox spread TypeError 해결
- HNS 페놀(C₆H₅OH) 물질 데이터 추가
- ScatRightPanel 280px 우측 패널 신규 구현 (3탭+액션버튼)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:11:21 +09:00

228 lines
8.3 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;
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>
);
}