wing-ops/frontend/src/tabs/scat/components/ScatRightPanel.tsx

286 lines
9.4 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,
}: 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>
);
}