develop #60

병합
htlee develop 에서 main 로 2 commits 를 머지했습니다 2026-03-01 13:32:51 +09:00
14개의 변경된 파일287개의 추가작업 그리고 36개의 파일을 삭제

파일 보기

@ -312,7 +312,7 @@ export function LoginPage() {
</div>{/* end form card */}
{/* Footer */}
<div className="text-center text-[9px] text-text-3 mt-6" className="leading-[1.6]">
<div className="text-center text-[9px] text-text-3 mt-6 leading-[1.6]">
<div>WING V2.0 | </div>
<div className="mt-0.5" style={{ color: 'rgba(134,144,166,0.6)' }}>
&copy; 2026 Korea Coast Guard. All rights reserved.

파일 보기

@ -737,7 +737,7 @@ export function SatelliteRequest() {
<div className="flex-1 flex items-center gap-3 min-w-0">
<span className="text-[10px] font-bold font-korean min-w-[100px] text-[#e2e8f0]">{p.sat}</span>
<span className="text-[9px] font-bold font-mono min-w-[110px] text-[#60a5fa]">{p.time}</span>
<span className="text-[9px] font-mono" className="text-cyan-500">{p.res}</span>
<span className="text-[9px] font-mono text-cyan-500">{p.res}</span>
<span className="text-[8px] font-mono text-[#64748b]">{p.cloud}</span>
</div>
{p.note && (

파일 보기

@ -128,7 +128,7 @@ function ShipInsurance() {
</div>
<div>
<label className="block text-[11px] font-semibold text-text-2 mb-1.5"> </label>
<select value={configKeyType} onChange={e => setConfigKeyType(e.target.value)} className="prd-i w-full" className="border-border">
<select value={configKeyType} onChange={e => setConfigKeyType(e.target.value)} className="prd-i w-full border-border">
<option value="mmsi">MMSI</option>
<option value="imo">IMO </option>
<option value="shipname"></option>
@ -137,7 +137,7 @@ function ShipInsurance() {
</div>
<div>
<label className="block text-[11px] font-semibold text-text-2 mb-1.5"> </label>
<select value={configRespType} onChange={e => setConfigRespType(e.target.value)} className="prd-i w-full" className="border-border">
<select value={configRespType} onChange={e => setConfigRespType(e.target.value)} className="prd-i w-full border-border">
<option value="json">JSON</option>
<option value="xml">XML</option>
</select>
@ -163,7 +163,7 @@ function ShipInsurance() {
<div className="flex gap-2 items-end flex-wrap">
<div>
<label className="block text-[10px] font-semibold text-text-3 mb-1"> </label>
<select value={searchType} onChange={e => setSearchType(e.target.value)} className="prd-i min-w-[120px]" className="border-border">
<select value={searchType} onChange={e => setSearchType(e.target.value)} className="prd-i min-w-[120px] border-border">
<option value="mmsi">MMSI</option>
<option value="imo">IMO </option>
<option value="shipname"></option>
@ -177,7 +177,7 @@ function ShipInsurance() {
</div>
<div>
<label className="block text-[10px] font-semibold text-text-3 mb-1"> </label>
<select value={insTypeFilter} onChange={e => setInsTypeFilter(e.target.value)} className="prd-i min-w-[140px]" className="border-border">
<select value={insTypeFilter} onChange={e => setInsTypeFilter(e.target.value)} className="prd-i min-w-[140px] border-border">
<option></option>
<option>P&I </option>
<option></option>

파일 보기

@ -388,7 +388,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
</h4>
<div className="flex flex-col gap-1.5">
{scenario.actions.map((action, i) => (
<div key={i} className="flex items-start gap-1.5 text-[10px] text-text-2 bg-bg-0 rounded-sm leading-[1.4]" className="py-[5px] px-2">
<div key={i} className="flex items-start gap-1.5 text-[10px] text-text-2 bg-bg-0 rounded-sm leading-[1.4] py-[5px] px-2">
<span className="text-status-orange font-bold shrink-0"></span>
{action}
</div>

파일 보기

@ -39,6 +39,7 @@ const substances: HNSSubstance[] = [
{ name: 'LPG', nameEn: 'LPG (Propane/Butane)', formula: 'C₃H₈/C₄H₁₀', unNumber: 'UN1075', casNumber: '68476-85-7', imdgClass: 'Class 2.1', sebc: 'G (가스)', flashPoint: '-104°C', boilingPoint: '-42°C', specificGravity: '0.50', vaporPressure: '8,460 mmHg', solubility: '0.01%', idlh: '2,100 ppm', twa: '1,000 ppm', aegl1: '-', aegl2: '17,000 ppm', aegl3: '33,000 ppm', category: 'flammable_gas', color: '#f97316' },
{ name: '에틸렌', nameEn: 'Ethylene', formula: 'C₂H₄', unNumber: 'UN1962', casNumber: '74-85-1', imdgClass: 'Class 2.1', sebc: 'G (가스)', flashPoint: '-', boilingPoint: '-104°C', specificGravity: '0.57', vaporPressure: '-', solubility: '0.01%', idlh: '-', twa: '-', aegl1: '-', aegl2: '-', aegl3: '-', category: 'flammable_gas', color: '#a855f7' },
{ name: '1,2-디클로로에탄', nameEn: '1,2-Dichloroethane (EDC)', formula: 'C₂H₄Cl₂', unNumber: 'UN1184', casNumber: '107-06-2', imdgClass: 'Class 3', sebc: 'S (침강)', flashPoint: '13°C', boilingPoint: '83°C', specificGravity: '1.253', vaporPressure: '87 mmHg', solubility: '0.87%', idlh: '50 ppm', twa: '1 ppm', aegl1: '-', aegl2: '20 ppm', aegl3: '200 ppm', category: 'toxic_liquid', color: '#ef4444' },
{ name: '페놀', nameEn: 'Phenol', formula: 'C₆H₅OH', unNumber: 'UN2312', casNumber: '108-95-2', imdgClass: 'Class 6.1', sebc: 'S/SD (침강/용해)', flashPoint: '79°C', boilingPoint: '182°C', specificGravity: '1.07', vaporPressure: '0.35 mmHg', solubility: '8.4%', idlh: '250 ppm', twa: '5 ppm', aegl1: '19 ppm', aegl2: '29 ppm', aegl3: '57 ppm', category: 'toxic_liquid', color: '#22c55e' },
]
const categories = [
@ -518,7 +519,7 @@ ${styles}
</thead>
<tbody>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
<td className="font-semibold text-primary-purple" className="py-1.5 px-2">NH </td>
<td className="font-semibold text-primary-purple py-1.5 px-2">NH </td>
<td className="text-center font-mono text-status-green">30</td>
<td className="text-center font-mono text-status-orange">160</td>
<td className="text-center font-mono text-status-red">1,100</td>
@ -528,7 +529,7 @@ ${styles}
<td className="text-center font-semibold text-primary-purple">G/GD</td>
</tr>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
<td className="font-semibold text-primary-cyan" className="py-1.5 px-2">CHOH </td>
<td className="font-semibold text-primary-cyan py-1.5 px-2">CHOH </td>
<td className="text-center font-mono text-status-green">530</td>
<td className="text-center font-mono text-status-orange">2,100</td>
<td className="text-center font-mono text-status-red">14,000</td>
@ -538,7 +539,7 @@ ${styles}
<td className="text-center font-semibold text-primary-cyan">ED</td>
</tr>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
<td className="font-semibold text-status-red" className="py-1.5 px-2">H </td>
<td className="font-semibold text-status-red py-1.5 px-2">H </td>
<td className="text-center text-text-3">-</td>
<td className="text-center text-text-3">-</td>
<td className="text-center text-text-3">-</td>
@ -548,7 +549,7 @@ ${styles}
<td className="text-center font-semibold text-primary-purple">G</td>
</tr>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
<td className="font-semibold text-status-orange" className="py-1.5 px-2">CH LNG</td>
<td className="font-semibold text-status-orange py-1.5 px-2">CH LNG</td>
<td className="text-center text-text-3">-</td>
<td className="text-center text-text-3">-</td>
<td className="text-center text-text-3">-</td>
@ -558,7 +559,7 @@ ${styles}
<td className="text-center font-semibold text-primary-purple">G</td>
</tr>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
<td className="font-semibold text-status-green" className="py-1.5 px-2">CHOH </td>
<td className="font-semibold text-status-green py-1.5 px-2">CHOH </td>
<td className="text-center font-mono text-status-green">19</td>
<td className="text-center font-mono text-status-orange">29</td>
<td className="text-center font-mono text-status-red">57</td>
@ -568,7 +569,7 @@ ${styles}
<td className="text-center font-semibold text-status-green">S/SD</td>
</tr>
<tr>
<td className="font-semibold text-status-yellow" className="py-1.5 px-2">CH </td>
<td className="font-semibold text-status-yellow py-1.5 px-2">CH </td>
<td className="text-center font-mono text-status-green">67</td>
<td className="text-center font-mono text-status-orange">560</td>
<td className="text-center font-mono text-status-red">3,700</td>
@ -795,15 +796,15 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
<div className="p-3 flex flex-col gap-2">
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.12)' }}>
<div className="text-[10px] font-bold text-status-orange mb-1">🔥 </div>
<div className="text-[9px] text-text-2" className="leading-[1.6]">: <b className="text-status-red">{s.responseDistanceFire}</b> </div>
<div className="text-[9px] text-text-2 leading-[1.6]">: <b className="text-status-red">{s.responseDistanceFire}</b> </div>
</div>
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(168,85,247,.04)', border: '1px solid rgba(168,85,247,.12)' }}>
<div className="text-[10px] font-bold text-primary-purple mb-1">💨 ()</div>
<div className="text-[9px] text-text-2" className="leading-[1.6]"> : <b className="text-primary-purple">{s.responseDistanceSpillDay}</b><br /> : <b className="text-status-red">{s.responseDistanceSpillNight}</b></div>
<div className="text-[9px] text-text-2 leading-[1.6]"> : <b className="text-primary-purple">{s.responseDistanceSpillDay}</b><br /> : <b className="text-status-red">{s.responseDistanceSpillNight}</b></div>
</div>
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(6,182,212,.04)', border: '1px solid rgba(6,182,212,.12)' }}>
<div className="text-[10px] font-bold text-primary-cyan mb-1">🌊 </div>
<div className="text-[9px] text-text-2" className="leading-[1.6]">{s.marineResponse}</div>
<div className="text-[9px] text-text-2 leading-[1.6]">{s.marineResponse}</div>
</div>
</div>
</div>
@ -898,15 +899,15 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
<div className="p-3 flex flex-col gap-2 text-[9px]">
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.1)' }}>
<div className="font-bold text-status-red mb-[3px]">🔥 </div>
<div className="text-text-2" className="leading-[1.6]">{s.emsFire}</div>
<div className="text-text-2 leading-[1.6]">{s.emsFire}</div>
</div>
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(168,85,247,.04)', border: '1px solid rgba(168,85,247,.1)' }}>
<div className="font-bold text-primary-purple mb-[3px]">💧 </div>
<div className="text-text-2" className="leading-[1.6]">{s.emsSpill}</div>
<div className="text-text-2 leading-[1.6]">{s.emsSpill}</div>
</div>
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(6,182,212,.04)', border: '1px solid rgba(6,182,212,.1)' }}>
<div className="font-bold text-primary-cyan mb-[3px]">🏥 </div>
<div className="text-text-2" className="leading-[1.6]">{s.emsFirstAid}</div>
<div className="text-text-2 leading-[1.6]">{s.emsFirstAid}</div>
</div>
<div className="text-center">
<button className="text-[10px] font-semibold cursor-pointer rounded-sm text-status-red" style={{ padding: '6px 16px', background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)' }}>📋 EmS </button>
@ -941,9 +942,9 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
const srcBg = c.source === '적부도' ? 'rgba(249,115,22,.1)' : c.source === '용선자' ? 'rgba(6,182,212,.1)' : 'rgba(34,197,94,.1)'
return (
<tr key={i} style={{ borderBottom: i < s.cargoCodes.length - 1 ? '1px solid rgba(255,255,255,.04)' : undefined }}>
<td className="font-mono text-primary-purple font-semibold cursor-pointer" className="py-[5px] px-2">{c.code}</td>
<td className="font-mono text-primary-purple font-semibold cursor-pointer py-[5px] px-2">{c.code}</td>
<td className="py-[5px] px-2">{c.name}</td>
<td className="text-text-3" className="py-[5px] px-2">{c.company}</td>
<td className="text-text-3 py-[5px] px-2">{c.company}</td>
<td className="text-center"><span style={{ padding: '1px 6px', borderRadius: 3, background: srcBg, color: srcColor, fontSize: 7 }}>{c.source}</span></td>
</tr>
)
@ -973,8 +974,8 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
const freqBg = p.frequency === '높음' ? 'rgba(239,68,68,.1)' : p.frequency === '중간' ? 'rgba(249,115,22,.1)' : 'rgba(34,197,94,.1)'
return (
<tr key={i} style={{ borderBottom: i < s.portFrequency.length - 1 ? '1px solid rgba(255,255,255,.04)' : undefined }}>
<td className="font-semibold" className="py-[5px] px-2">{p.port}</td>
<td className="font-mono text-primary-cyan" className="py-[5px] px-2">{p.portCode}</td>
<td className="font-semibold py-[5px] px-2">{p.port}</td>
<td className="font-mono text-primary-cyan py-[5px] px-2">{p.portCode}</td>
<td className="text-center font-mono">{p.lastImport}</td>
<td className="text-center"><span style={{ padding: '1px 6px', borderRadius: 3, background: freqBg, color: freqColor, fontWeight: 600, fontSize: 8 }}>{p.frequency}</span></td>
</tr>

파일 보기

@ -507,7 +507,7 @@ WeatherPopup.displayName = 'WeatherPopup'
function WxCell({ icon, label, value }: { icon: string; label: string; value: string }) {
return (
<div className="flex items-center bg-bg-0 rounded gap-[6px]" className="py-1.5 px-2">
<div className="flex items-center bg-bg-0 rounded gap-[6px] py-1.5 px-2">
<span className="text-[12px]">{icon}</span>
<div>
<div className="text-text-3 text-[7px]">{label}</div>

파일 보기

@ -339,7 +339,7 @@ export function IncidentsView() {
<div className="font-semibold text-[#1a1a2e]" style={{ marginBottom: 6 }}>
{incidentPopup.incident.name}
</div>
<div className="text-[11px] text-[#555]" className="leading-[1.6]">
<div className="text-[11px] text-[#555] leading-[1.6]">
<div>: {getStatusLabel(incidentPopup.incident.status)}</div>
<div>
: {incidentPopup.incident.date} {incidentPopup.incident.time}

파일 보기

@ -149,7 +149,7 @@ const OilBoomSection = ({
</h4>
<p className="leading-normal" className="text-[9px] text-text-3 mb-2.5">
<p className="leading-normal text-[9px] text-text-3 mb-2.5">
{oilTrajectory.length > 0
? '확산 궤적을 분석하여 해류 직교 방향 1차 방어선, U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 생성합니다.'
: '상단에서 확산 예측을 실행한 뒤 AI 배치를 적용할 수 있습니다.'

파일 보기

@ -584,7 +584,7 @@ function KospsPanel() {
<div className="text-[10px] text-text-2 leading-[1.7]">
(ENC) <b className="text-status-green">Akima(1978) 2 5</b>(Bivariate Quintic Polynomial) . (TIN) 21 .
</div>
<div className="mt-1.5 p-1.5 rounded" style={{ ...codeBox, padding: '6px' }}>
<div className="mt-1.5 p-1.5 rounded bg-bg-0 font-mono text-xs leading-loose">
z(x,y) = Σ Σ qᵢⱼ xⁱ <span className="text-[10px] text-text-3">(i5, i+j5)</span>
</div>
</div>
@ -1068,7 +1068,7 @@ function OpenDriftPanel() {
</div>
<span className="text-[10px] whitespace-nowrap text-text-3">2024</span>
</div>
<div className="text-[11px] font-bold mb-1" className="leading-normal">Numerical Model Test of Spilled Oil Transport Near the Korean Coasts Using Various Input Parametric Models</div>
<div className="text-[11px] font-bold mb-1 leading-normal">Numerical Model Test of Spilled Oil Transport Near the Korean Coasts Using Various Input Parametric Models</div>
<div className="text-[11px] mb-2 text-text-3">Hai Van Dang, Suchan Joo, Junhyeok Lim, Jinhwan Hur, Sungwon Shin | Hanyang University ERICA | Journal of Ocean Engineering and Technology, 2024</div>
<div className="grid grid-cols-2 gap-2 mb-2">
<div className="text-[11px] text-text-2 leading-[1.7]">
@ -1103,7 +1103,7 @@ function OpenDriftPanel() {
</div>
<span className="text-[10px] whitespace-nowrap text-text-3">1998</span>
</div>
<div className="text-[11px] font-bold mb-1" className="leading-normal"> (Oil Spill Behavior Forecasting Model in South-eastern Coastal Area of Korea)</div>
<div className="text-[11px] font-bold mb-1 leading-normal"> (Oil Spill Behavior Forecasting Model in South-eastern Coastal Area of Korea)</div>
<div className="text-[11px] mb-2 text-text-3">, , , | | Vol.1 No.2, pp.5259, 1998</div>
<div className="grid grid-cols-2 gap-2 mb-2">
<div className="text-[11px] text-text-2 leading-[1.7]">
@ -1135,7 +1135,7 @@ function OpenDriftPanel() {
</div>
<span className="text-[10px] whitespace-nowrap text-text-3">2008</span>
</div>
<div className="text-[11px] font-bold mb-1" className="leading-normal"> (Analysis of Oil Spill Dispersion in Taean Coastal Zone)</div>
<div className="text-[11px] font-bold mb-1 leading-normal"> (Analysis of Oil Spill Dispersion in Taean Coastal Zone)</div>
<div className="text-[11px] mb-2 text-text-3">, | | · 17 pp.6063, 2008</div>
<div className="grid grid-cols-2 gap-2 mb-2">
<div className="text-[11px] text-text-2 leading-[1.7]">
@ -1159,7 +1159,7 @@ function OpenDriftPanel() {
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.12)' }}><div className="font-bold text-status-red">α = 3%</div><div className="text-text-3"> </div></div>
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(234,179,8,.06)', border: '1px solid rgba(234,179,8,.12)' }}><div className="font-bold text-status-yellow">α = 2.5%</div><div className="text-text-3"> </div></div>
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(34,197,94,.08)', border: '1px solid rgba(34,197,94,.2)' }}><div className="font-bold text-status-green">α = 2% </div><div className="text-text-3"> </div></div>
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(6,182,212,.06)', border: '1px solid rgba(6,182,212,.12)' }}><div className="font-bold" className="text-cyan-500">θ = 20° </div><div className="text-text-3"> </div></div>
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(6,182,212,.06)', border: '1px solid rgba(6,182,212,.12)' }}><div className="font-bold text-cyan-500">θ = 20° </div><div className="text-text-3"> </div></div>
</div>
</div>
<div className="p-2 rounded-md text-[10px]" style={{ background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.1)', color: 'var(--t2)', lineHeight: '1.7' }}>

파일 보기

@ -249,7 +249,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
</>
)}
{sec.id === 'oil-pollution' && (
<table className="w-full table-fixed" className="border-collapse">
<table className="w-full table-fixed border-collapse">
<colgroup><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /></colgroup>
<tbody>
{[
@ -447,7 +447,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
</div>
)}
{sec.id === 'rescue-resource' && (
<table className="w-full text-[11px]" className="border-collapse">
<table className="w-full text-[11px] border-collapse">
<thead>
<tr className="border-b border-border">
<th className="px-3 py-2 text-left text-text-3 font-korean"></th>

파일 보기

@ -117,7 +117,7 @@ export function ReportsView() {
</div>
) : (
<div className="flex-1 overflow-auto">
<table className="w-full table-fixed" className="border-collapse">
<table className="w-full table-fixed border-collapse">
<colgroup>
<col style={{ width: '3%' }} />
<col style={{ width: '4%' }} />

파일 보기

@ -128,7 +128,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex-1 overflow-y-auto px-6 py-5">
{template.sections.map((section, sIdx) => (
<div key={sIdx} className="mb-6 w-full">
<h4 className="text-[13px] font-bold font-korean mb-3" className="text-cyan-500">{section.title}</h4>
<h4 className="text-[13px] font-bold font-korean mb-3 text-cyan-500">{section.title}</h4>
<table className="w-full table-fixed border-collapse">
<colgroup>
<col style={{ width: '180px' }} />
@ -235,7 +235,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
{/* Report Title */}
<div className="text-center mb-8">
<h2 className="text-[18px] font-bold text-text-1 font-korean mb-1"></h2>
<h3 className="text-[15px] font-semibold font-korean" className="text-cyan-500">
<h3 className="text-[15px] font-semibold font-korean text-cyan-500">
{formData['incident.name'] || template.label}
</h3>
<p className="text-[11px] text-text-3 font-korean mt-2">

파일 보기

@ -6,6 +6,7 @@ import ScatLeftPanel from './ScatLeftPanel';
import ScatMap from './ScatMap';
import ScatTimeline from './ScatTimeline';
import ScatPopup from './ScatPopup';
import ScatRightPanel from './ScatRightPanel';
// ═══ Main PreScatView ═══
@ -21,6 +22,8 @@ export function PreScatView() {
const [statusFilter, setStatusFilter] = useState('전체');
const [searchTerm, setSearchTerm] = useState('');
const [popupData, setPopupData] = useState<ScatDetail | null>(null);
const [panelDetail, setPanelDetail] = useState<ScatDetail | null>(null);
const [panelLoading, setPanelLoading] = useState(false);
const [timelineIdx, setTimelineIdx] = useState(6);
// API에서 구역 및 구간 데이터 로딩
@ -51,6 +54,21 @@ export function PreScatView() {
};
}, []);
// 선택 구간 변경 시 우측 패널 상세 로딩
useEffect(() => {
if (!selectedSeg) {
setPanelDetail(null);
return;
}
let cancelled = false;
setPanelLoading(true);
fetchSectionDetail(selectedSeg.id)
.then(detail => { if (!cancelled) setPanelDetail(detail); })
.catch(err => console.error('[SCAT] 패널 상세 로딩 오류:', err))
.finally(() => { if (!cancelled) setPanelLoading(false); });
return () => { cancelled = true; };
}, [selectedSeg]);
// 관할 기반 세그먼트 필터링
const filteredSegments = segments.filter((s) => {
if (jurisdictionFilter === '서귀포해양경비안전서') return s.jurisdiction === '서귀포';
@ -144,6 +162,11 @@ export function PreScatView() {
/>
</div>
<ScatRightPanel
detail={panelDetail}
loading={panelLoading}
/>
{popupData && (
<ScatPopup data={popupData} segCode={selectedSeg.code} onClose={handleClosePopup} />
)}

파일 보기

@ -0,0 +1,227 @@
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>
);
}