feat(prediction): 오염분석 UI 개선 — HTML 디자인 참고 반영
- 다각형/원 분석 탭 버튼 사이즈 축소 + 활성 탭 스타일 통일 - 다각형 분석: 설명 텍스트 + 그라데이션 "다각형 분석수행" 버튼 - 원 분석: 반경(NM) 프리셋 버튼(1,3,5,10,15,20,30,50) + 직접 입력 사고지점 기준 원형 영역 면적 계산 (NM² + km²) - 분석 결과: NM²/km² 면적, 원 둘레, 반경 표시 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
6944a9e342
커밋
97e9d58cc1
@ -18,11 +18,10 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
const [shipExpanded, setShipExpanded] = useState(false)
|
const [shipExpanded, setShipExpanded] = useState(false)
|
||||||
const [insuranceExpanded, setInsuranceExpanded] = useState(false)
|
const [insuranceExpanded, setInsuranceExpanded] = useState(false)
|
||||||
const [polygonResult, setPolygonResult] = useState<{ areaKm2: number; perimeterKm: number; particleCount: number; hullPoints: number } | null>(null)
|
const [polygonResult, setPolygonResult] = useState<{ areaKm2: number; perimeterKm: number; particleCount: number; hullPoints: number } | null>(null)
|
||||||
const [showCircleInput, setShowCircleInput] = useState(false)
|
const [analysisTab, setAnalysisTab] = useState<'polygon' | 'circle'>('polygon')
|
||||||
const [circleCenterLat, setCircleCenterLat] = useState('')
|
const [circleRadiusNm, setCircleRadiusNm] = useState('5')
|
||||||
const [circleCenterLon, setCircleCenterLon] = useState('')
|
const [circleResult, setCircleResult] = useState<{ areaKm2: number; areaNm2: number; circumferenceKm: number; radiusNm: number } | null>(null)
|
||||||
const [circleRadiusKm, setCircleRadiusKm] = useState('')
|
const NM_PRESETS = [1, 3, 5, 10, 15, 20, 30, 50]
|
||||||
const [circleResult, setCircleResult] = useState<{ areaKm2: number; circumferenceKm: number; centerLat: number; centerLon: number; radiusKm: number } | null>(null)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[300px] min-w-[300px] bg-bg-1 border-l border-border flex flex-col">
|
<div className="w-[300px] min-w-[300px] bg-bg-1 border-l border-border flex flex-col">
|
||||||
@ -50,131 +49,136 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
|
|
||||||
{/* 오염분석 */}
|
{/* 오염분석 */}
|
||||||
<Section title="오염분석">
|
<Section title="오염분석">
|
||||||
<div className="flex gap-2 mb-2">
|
{/* 탭 버튼 */}
|
||||||
|
<div className="flex gap-1.5 mb-2.5">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => setAnalysisTab('polygon')}
|
||||||
if (oilTrajectory.length < 3) {
|
className="flex-1 py-1.5 px-2 rounded text-[10px] font-semibold font-korean cursor-pointer transition-colors"
|
||||||
alert('확산 예측을 먼저 실행하세요.')
|
style={analysisTab === 'polygon'
|
||||||
return
|
? { background: 'rgba(6,182,212,0.12)', border: '1px solid var(--cyan)', color: 'var(--cyan)' }
|
||||||
}
|
: { background: 'var(--bg0)', border: '1px solid var(--bd)', color: 'var(--t3)' }
|
||||||
const result = analyzeSpillPolygon(oilTrajectory)
|
}
|
||||||
setPolygonResult({ areaKm2: result.areaKm2, perimeterKm: result.perimeterKm, particleCount: result.particleCount, hullPoints: result.hull.length })
|
>다각형 분석</button>
|
||||||
}}
|
|
||||||
className="flex-1 py-2.5 px-2 rounded text-[10px] font-bold font-korean border cursor-pointer transition-colors hover:brightness-110"
|
|
||||||
style={{ background: 'rgba(168,85,247,0.1)', border: '1px solid rgba(168,85,247,0.3)', color: '#a855f7' }}>
|
|
||||||
📐 다각형 분석
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => setAnalysisTab('circle')}
|
||||||
setShowCircleInput(!showCircleInput)
|
className="flex-1 py-1.5 px-2 rounded text-[10px] font-semibold font-korean cursor-pointer transition-colors"
|
||||||
// 사고 지점 좌표를 기본값으로 설정
|
style={analysisTab === 'circle'
|
||||||
if (!showCircleInput && detail?.spill) {
|
? { background: 'rgba(6,182,212,0.12)', border: '1px solid var(--cyan)', color: 'var(--cyan)' }
|
||||||
if (!circleCenterLat) setCircleCenterLat(String(detail.spill.lat ?? ''))
|
: { background: 'var(--bg0)', border: '1px solid var(--bd)', color: 'var(--t3)' }
|
||||||
if (!circleCenterLon) setCircleCenterLon(String(detail.spill.lon ?? ''))
|
}
|
||||||
}
|
>원 분석</button>
|
||||||
}}
|
|
||||||
className="flex-1 py-2.5 px-2 rounded text-[10px] font-bold font-korean border cursor-pointer transition-colors hover:brightness-110"
|
|
||||||
style={showCircleInput
|
|
||||||
? { background: 'rgba(6,182,212,0.2)', border: '1px solid var(--cyan)', color: 'var(--cyan)' }
|
|
||||||
: { background: 'rgba(6,182,212,0.1)', border: '1px solid rgba(6,182,212,0.3)', color: 'var(--cyan)' }
|
|
||||||
}>
|
|
||||||
⭕ 원 분석
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 원 분석 입력 폼 */}
|
{/* ── 다각형 분석 탭 ── */}
|
||||||
{showCircleInput && (
|
{analysisTab === 'polygon' && (
|
||||||
<div className="flex flex-col gap-2 p-2.5 bg-bg-0 border border-[rgba(6,182,212,0.2)] rounded-md mb-2">
|
<>
|
||||||
<div className="text-[9px] font-bold text-primary-cyan font-korean">⭕ 원 분석 — 중심점 · 반경 입력</div>
|
<p className="text-[9px] text-text-3 font-korean leading-relaxed mb-2">
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
지도에서 다각형 영역을 지정하여 해당 범위 내 오염도를 분석합니다.
|
||||||
<div>
|
</p>
|
||||||
<label className="text-[8px] text-text-3 font-korean block mb-0.5">중심 위도 (°N)</label>
|
<button
|
||||||
<input
|
onClick={() => {
|
||||||
type="number"
|
if (oilTrajectory.length < 3) {
|
||||||
step="0.0001"
|
alert('확산 예측을 먼저 실행하세요.')
|
||||||
value={circleCenterLat}
|
return
|
||||||
onChange={e => setCircleCenterLat(e.target.value)}
|
}
|
||||||
placeholder="예: 34.7312"
|
const result = analyzeSpillPolygon(oilTrajectory)
|
||||||
className="w-full px-2 py-1.5 bg-bg-3 border border-border rounded text-[10px] font-mono text-text-1 outline-none focus:border-[var(--cyan)] transition-colors"
|
setPolygonResult({ areaKm2: result.areaKm2, perimeterKm: result.perimeterKm, particleCount: result.particleCount, hullPoints: result.hull.length })
|
||||||
/>
|
}}
|
||||||
</div>
|
className="w-full py-2 px-3 rounded text-[10px] font-bold font-korean cursor-pointer"
|
||||||
<div>
|
style={{ background: 'linear-gradient(to right, #a855f7, var(--cyan))', color: '#fff' }}
|
||||||
<label className="text-[8px] text-text-3 font-korean block mb-0.5">중심 경도 (°E)</label>
|
>
|
||||||
<input
|
📐 다각형 분석수행
|
||||||
type="number"
|
</button>
|
||||||
step="0.0001"
|
</>
|
||||||
value={circleCenterLon}
|
)}
|
||||||
onChange={e => setCircleCenterLon(e.target.value)}
|
|
||||||
placeholder="예: 127.6845"
|
{/* ── 원 분석 탭 ── */}
|
||||||
className="w-full px-2 py-1.5 bg-bg-3 border border-border rounded text-[10px] font-mono text-text-1 outline-none focus:border-[var(--cyan)] transition-colors"
|
{analysisTab === 'circle' && (
|
||||||
/>
|
<>
|
||||||
</div>
|
<p className="text-[9px] text-text-3 font-korean leading-relaxed mb-2">
|
||||||
|
반경(NM)을 지정하면 사고지점 기준 원형 영역의 오염도를 분석합니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 반경 선택 (NM) */}
|
||||||
|
<div className="text-[9px] font-bold text-text-2 font-korean mb-1.5">반경 선택 (NM)</div>
|
||||||
|
<div className="grid grid-cols-6 gap-1 mb-2">
|
||||||
|
{NM_PRESETS.map(nm => (
|
||||||
|
<button
|
||||||
|
key={nm}
|
||||||
|
onClick={() => setCircleRadiusNm(String(nm))}
|
||||||
|
className="py-1.5 rounded text-[10px] font-bold font-mono cursor-pointer transition-colors"
|
||||||
|
style={parseFloat(circleRadiusNm) === nm
|
||||||
|
? { background: 'rgba(6,182,212,0.2)', border: '1px solid var(--cyan)', color: 'var(--cyan)' }
|
||||||
|
: { background: 'var(--bg0)', border: '1px solid var(--bd)', color: 'var(--t3)' }
|
||||||
|
}
|
||||||
|
>{nm}</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="text-[8px] text-text-3 font-korean block mb-0.5">반경 (km)</label>
|
{/* 직접 입력 + 분석 실행 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[9px] text-text-3 font-korean shrink-0">직접 입력</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
min="0.1"
|
min="0.1"
|
||||||
value={circleRadiusKm}
|
value={circleRadiusNm}
|
||||||
onChange={e => setCircleRadiusKm(e.target.value)}
|
onChange={e => setCircleRadiusNm(e.target.value)}
|
||||||
placeholder="예: 3.0"
|
className="w-[60px] px-2 py-1.5 bg-bg-3 border border-border rounded text-[10px] font-mono text-text-1 text-center outline-none focus:border-[var(--cyan)] transition-colors"
|
||||||
className="w-full px-2 py-1.5 bg-bg-3 border border-border rounded text-[10px] font-mono text-text-1 outline-none focus:border-[var(--cyan)] transition-colors"
|
|
||||||
/>
|
/>
|
||||||
|
<span className="text-[9px] text-text-3 font-korean shrink-0">NM</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const nm = parseFloat(circleRadiusNm)
|
||||||
|
if (isNaN(nm) || nm <= 0) {
|
||||||
|
alert('반경을 올바르게 입력하세요.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const km = nm * 1.852
|
||||||
|
const areaKm2 = Math.PI * km * km
|
||||||
|
const areaNm2 = Math.PI * nm * nm
|
||||||
|
const circumferenceKm = 2 * Math.PI * km
|
||||||
|
setCircleResult({ areaKm2, areaNm2, circumferenceKm, radiusNm: nm })
|
||||||
|
}}
|
||||||
|
disabled={!circleRadiusNm || parseFloat(circleRadiusNm) <= 0}
|
||||||
|
className="ml-auto px-3 py-1.5 rounded text-[10px] font-bold font-korean cursor-pointer shrink-0 transition-colors"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(6,182,212,0.15)',
|
||||||
|
border: '1px solid var(--cyan)',
|
||||||
|
color: 'var(--cyan)',
|
||||||
|
}}
|
||||||
|
>분석 실행</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</>
|
||||||
onClick={() => {
|
|
||||||
const lat = parseFloat(circleCenterLat)
|
|
||||||
const lon = parseFloat(circleCenterLon)
|
|
||||||
const r = parseFloat(circleRadiusKm)
|
|
||||||
if (isNaN(lat) || isNaN(lon) || isNaN(r) || r <= 0) {
|
|
||||||
alert('중심 좌표와 반경을 올바르게 입력하세요.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const areaKm2 = Math.PI * r * r
|
|
||||||
const circumferenceKm = 2 * Math.PI * r
|
|
||||||
setCircleResult({ areaKm2, circumferenceKm, centerLat: lat, centerLon: lon, radiusKm: r })
|
|
||||||
}}
|
|
||||||
disabled={!circleCenterLat || !circleCenterLon || !circleRadiusKm}
|
|
||||||
className="w-full py-2 rounded text-[10px] font-bold font-korean cursor-pointer transition-colors"
|
|
||||||
style={{
|
|
||||||
background: (circleCenterLat && circleCenterLon && circleRadiusKm) ? 'rgba(6,182,212,0.15)' : 'var(--bg3)',
|
|
||||||
border: (circleCenterLat && circleCenterLon && circleRadiusKm) ? '1px solid var(--cyan)' : '1px solid var(--bd)',
|
|
||||||
color: (circleCenterLat && circleCenterLon && circleRadiusKm) ? 'var(--cyan)' : 'var(--t3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
⭕ 원 면적 계산
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 원 분석 결과 */}
|
{/* 원 분석 결과 */}
|
||||||
{circleResult && (
|
{analysisTab === 'circle' && circleResult && (
|
||||||
<div className="flex flex-col gap-1.5 mb-2">
|
<div className="flex flex-col gap-1.5 mt-2.5">
|
||||||
<div className="text-[9px] font-bold text-primary-cyan font-korean mb-0.5">⭕ 원 분석 결과</div>
|
<div className="text-[9px] font-bold text-primary-cyan font-korean">⭕ 원 분석 결과 (반경 {circleResult.radiusNm} NM)</div>
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
||||||
<div className="text-sm font-extrabold font-mono text-primary-cyan">{circleResult.areaKm2.toFixed(2)}</div>
|
<div className="text-sm font-extrabold font-mono text-primary-cyan">{circleResult.areaNm2.toFixed(1)}</div>
|
||||||
<div className="text-[7px] text-text-3 font-korean">오염 면적 (km²)</div>
|
<div className="text-[7px] text-text-3 font-korean">면적 (NM²)</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
||||||
<div className="text-sm font-extrabold font-mono text-status-orange">{circleResult.circumferenceKm.toFixed(1)}</div>
|
<div className="text-sm font-extrabold font-mono text-status-orange">{circleResult.areaKm2.toFixed(1)}</div>
|
||||||
|
<div className="text-[7px] text-text-3 font-korean">면적 (km²)</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
||||||
|
<div className="text-sm font-extrabold font-mono text-text-1">{circleResult.circumferenceKm.toFixed(1)}</div>
|
||||||
<div className="text-[7px] text-text-3 font-korean">원 둘레 (km)</div>
|
<div className="text-[7px] text-text-3 font-korean">원 둘레 (km)</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
||||||
<div className="text-sm font-extrabold font-mono text-text-1">{circleResult.radiusKm.toFixed(1)}</div>
|
<div className="text-sm font-extrabold font-mono text-text-1">{(circleResult.radiusNm * 1.852).toFixed(1)}</div>
|
||||||
<div className="text-[7px] text-text-3 font-korean">반경 (km)</div>
|
<div className="text-[7px] text-text-3 font-korean">반경 (km)</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center py-2 px-1 bg-bg-0 border border-border rounded">
|
|
||||||
<div className="text-[9px] font-bold font-mono text-text-2">{circleResult.centerLat.toFixed(4)}°N</div>
|
|
||||||
<div className="text-[9px] font-bold font-mono text-text-2">{circleResult.centerLon.toFixed(4)}°E</div>
|
|
||||||
<div className="text-[7px] text-text-3 font-korean">중심 좌표</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{polygonResult && (
|
{/* 다각형 분석 결과 */}
|
||||||
|
{analysisTab === 'polygon' && polygonResult && (
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="text-[9px] font-bold text-purple-400 font-korean mb-0.5">📐 Convex Hull 다각형 분석 결과</div>
|
<div className="text-[9px] font-bold text-purple-400 font-korean mb-0.5">📐 Convex Hull 다각형 분석 결과</div>
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user