feat(prediction): 원 분석 기능 — 중심점/반경 입력으로 원형 오염 면적 계산

- 원 분석 버튼 클릭 시 입력 폼 토글 (중심 위도, 경도, 반경 km)
- 사고 지점 좌표를 기본값으로 자동 설정
- πr² 면적, 2πr 둘레 계산 결과 표시
- 결과: 오염 면적(km²), 원 둘레(km), 반경(km), 중심 좌표

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-03-16 08:34:29 +09:00
부모 fb74df5c1f
커밋 6944a9e342

파일 보기

@ -18,6 +18,11 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
const [shipExpanded, setShipExpanded] = useState(false)
const [insuranceExpanded, setInsuranceExpanded] = useState(false)
const [polygonResult, setPolygonResult] = useState<{ areaKm2: number; perimeterKm: number; particleCount: number; hullPoints: number } | null>(null)
const [showCircleInput, setShowCircleInput] = useState(false)
const [circleCenterLat, setCircleCenterLat] = useState('')
const [circleCenterLon, setCircleCenterLon] = useState('')
const [circleRadiusKm, setCircleRadiusKm] = useState('')
const [circleResult, setCircleResult] = useState<{ areaKm2: number; circumferenceKm: number; centerLat: number; centerLon: number; radiusKm: number } | null>(null)
return (
<div className="w-[300px] min-w-[300px] bg-bg-1 border-l border-border flex flex-col">
@ -60,12 +65,115 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
📐
</button>
<button
onClick={() => alert('원 분석 기능은 향후 오픈 예정입니다.')}
onClick={() => {
setShowCircleInput(!showCircleInput)
// 사고 지점 좌표를 기본값으로 설정
if (!showCircleInput && detail?.spill) {
if (!circleCenterLat) setCircleCenterLat(String(detail.spill.lat ?? ''))
if (!circleCenterLon) setCircleCenterLon(String(detail.spill.lon ?? ''))
}
}}
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(6,182,212,0.1)', border: '1px solid rgba(6,182,212,0.3)', color: 'var(--cyan)' }}>
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>
{/* 원 분석 입력 폼 */}
{showCircleInput && (
<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>
<div className="grid grid-cols-2 gap-1.5">
<div>
<label className="text-[8px] text-text-3 font-korean block mb-0.5"> (°N)</label>
<input
type="number"
step="0.0001"
value={circleCenterLat}
onChange={e => setCircleCenterLat(e.target.value)}
placeholder="예: 34.7312"
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"
/>
</div>
<div>
<label className="text-[8px] text-text-3 font-korean block mb-0.5"> (°E)</label>
<input
type="number"
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"
/>
</div>
</div>
<div>
<label className="text-[8px] text-text-3 font-korean block mb-0.5"> (km)</label>
<input
type="number"
step="0.1"
min="0.1"
value={circleRadiusKm}
onChange={e => setCircleRadiusKm(e.target.value)}
placeholder="예: 3.0"
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"
/>
</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 && (
<div className="flex flex-col gap-1.5 mb-2">
<div className="text-[9px] font-bold text-primary-cyan font-korean mb-0.5"> </div>
<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-sm font-extrabold font-mono text-primary-cyan">{circleResult.areaKm2.toFixed(2)}</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-status-orange">{circleResult.circumferenceKm.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.radiusKm.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-[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>
)}
{polygonResult && (
<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>