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>
This commit is contained in:
부모
b00bb56af3
커밋
34cf046787
@ -312,7 +312,7 @@ export function LoginPage() {
|
|||||||
</div>{/* end form card */}
|
</div>{/* end form card */}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* 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>WING V2.0 | 해양경찰청 기동방제과 위기대응 통합시스템</div>
|
||||||
<div className="mt-0.5" style={{ color: 'rgba(134,144,166,0.6)' }}>
|
<div className="mt-0.5" style={{ color: 'rgba(134,144,166,0.6)' }}>
|
||||||
© 2026 Korea Coast Guard. All rights reserved.
|
© 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">
|
<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-[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-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>
|
<span className="text-[8px] font-mono text-[#64748b]">{p.cloud}</span>
|
||||||
</div>
|
</div>
|
||||||
{p.note && (
|
{p.note && (
|
||||||
|
|||||||
@ -128,7 +128,7 @@ function ShipInsurance() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">조회 기본값 — 조회 키 타입</label>
|
<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="mmsi">MMSI</option>
|
||||||
<option value="imo">IMO 번호</option>
|
<option value="imo">IMO 번호</option>
|
||||||
<option value="shipname">선박명</option>
|
<option value="shipname">선박명</option>
|
||||||
@ -137,7 +137,7 @@ function ShipInsurance() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">응답 형식</label>
|
<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="json">JSON</option>
|
||||||
<option value="xml">XML</option>
|
<option value="xml">XML</option>
|
||||||
</select>
|
</select>
|
||||||
@ -163,7 +163,7 @@ function ShipInsurance() {
|
|||||||
<div className="flex gap-2 items-end flex-wrap">
|
<div className="flex gap-2 items-end flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-semibold text-text-3 mb-1">조회 키 타입</label>
|
<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="mmsi">MMSI</option>
|
||||||
<option value="imo">IMO 번호</option>
|
<option value="imo">IMO 번호</option>
|
||||||
<option value="shipname">선박명</option>
|
<option value="shipname">선박명</option>
|
||||||
@ -177,7 +177,7 @@ function ShipInsurance() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-semibold text-text-3 mb-1">보험 종류</label>
|
<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>전체</option>
|
||||||
<option>P&I 보험</option>
|
<option>P&I 보험</option>
|
||||||
<option>선주책임보험</option>
|
<option>선주책임보험</option>
|
||||||
|
|||||||
@ -388,7 +388,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
</h4>
|
</h4>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{scenario.actions.map((action, i) => (
|
{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>
|
<span className="text-status-orange font-bold shrink-0">•</span>
|
||||||
{action}
|
{action}
|
||||||
</div>
|
</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: '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: '에틸렌', 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: '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 = [
|
const categories = [
|
||||||
@ -518,7 +519,7 @@ ${styles}
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
<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-green">30</td>
|
||||||
<td className="text-center font-mono text-status-orange">160</td>
|
<td className="text-center font-mono text-status-orange">160</td>
|
||||||
<td className="text-center font-mono text-status-red">1,100</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>
|
<td className="text-center font-semibold text-primary-purple">G/GD</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||||
<td className="font-semibold text-primary-cyan" className="py-1.5 px-2">CH₃OH 메탄올</td>
|
<td className="font-semibold text-primary-cyan py-1.5 px-2">CH₃OH 메탄올</td>
|
||||||
<td className="text-center font-mono text-status-green">530</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-orange">2,100</td>
|
||||||
<td className="text-center font-mono text-status-red">14,000</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>
|
<td className="text-center font-semibold text-primary-cyan">ED</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
<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>
|
<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>
|
<td className="text-center font-semibold text-primary-purple">G</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
<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>
|
<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>
|
<td className="text-center font-semibold text-primary-purple">G</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||||
<td className="font-semibold text-status-green" className="py-1.5 px-2">C₆H₅OH 페놀</td>
|
<td className="font-semibold text-status-green py-1.5 px-2">C₆H₅OH 페놀</td>
|
||||||
<td className="text-center font-mono text-status-green">19</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-orange">29</td>
|
||||||
<td className="text-center font-mono text-status-red">57</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>
|
<td className="text-center font-semibold text-status-green">S/SD</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="font-semibold text-status-yellow" className="py-1.5 px-2">C₇H₈ 톨루엔</td>
|
<td className="font-semibold text-status-yellow py-1.5 px-2">C₇H₈ 톨루엔</td>
|
||||||
<td className="text-center font-mono text-status-green">67</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-orange">560</td>
|
||||||
<td className="text-center font-mono text-status-red">3,700</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="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="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-[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>
|
||||||
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(168,85,247,.04)', border: '1px solid rgba(168,85,247,.12)' }}>
|
<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-[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>
|
||||||
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(6,182,212,.04)', border: '1px solid rgba(6,182,212,.12)' }}>
|
<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-[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>
|
</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="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="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="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>
|
||||||
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(168,85,247,.04)', border: '1px solid rgba(168,85,247,.1)' }}>
|
<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="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>
|
||||||
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(6,182,212,.04)', border: '1px solid rgba(6,182,212,.1)' }}>
|
<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="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>
|
||||||
<div className="text-center">
|
<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>
|
<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)'
|
const srcBg = c.source === '적부도' ? 'rgba(249,115,22,.1)' : c.source === '용선자' ? 'rgba(6,182,212,.1)' : 'rgba(34,197,94,.1)'
|
||||||
return (
|
return (
|
||||||
<tr key={i} style={{ borderBottom: i < s.cargoCodes.length - 1 ? '1px solid rgba(255,255,255,.04)' : undefined }}>
|
<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="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>
|
<td className="text-center"><span style={{ padding: '1px 6px', borderRadius: 3, background: srcBg, color: srcColor, fontSize: 7 }}>{c.source}</span></td>
|
||||||
</tr>
|
</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)'
|
const freqBg = p.frequency === '높음' ? 'rgba(239,68,68,.1)' : p.frequency === '중간' ? 'rgba(249,115,22,.1)' : 'rgba(34,197,94,.1)'
|
||||||
return (
|
return (
|
||||||
<tr key={i} style={{ borderBottom: i < s.portFrequency.length - 1 ? '1px solid rgba(255,255,255,.04)' : undefined }}>
|
<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-semibold 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-mono text-primary-cyan py-[5px] px-2">{p.portCode}</td>
|
||||||
<td className="text-center font-mono">{p.lastImport}</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>
|
<td className="text-center"><span style={{ padding: '1px 6px', borderRadius: 3, background: freqBg, color: freqColor, fontWeight: 600, fontSize: 8 }}>{p.frequency}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -507,7 +507,7 @@ WeatherPopup.displayName = 'WeatherPopup'
|
|||||||
|
|
||||||
function WxCell({ icon, label, value }: { icon: string; label: string; value: string }) {
|
function WxCell({ icon, label, value }: { icon: string; label: string; value: string }) {
|
||||||
return (
|
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>
|
<span className="text-[12px]">{icon}</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-text-3 text-[7px]">{label}</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 }}>
|
<div className="font-semibold text-[#1a1a2e]" style={{ marginBottom: 6 }}>
|
||||||
{incidentPopup.incident.name}
|
{incidentPopup.incident.name}
|
||||||
</div>
|
</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>상태: {getStatusLabel(incidentPopup.incident.status)}</div>
|
||||||
<div>
|
<div>
|
||||||
일시: {incidentPopup.incident.date} {incidentPopup.incident.time}
|
일시: {incidentPopup.incident.date} {incidentPopup.incident.time}
|
||||||
|
|||||||
@ -149,7 +149,7 @@ const OilBoomSection = ({
|
|||||||
확산 예측 기반 최적 배치안
|
확산 예측 기반 최적 배치안
|
||||||
</h4>
|
</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
|
{oilTrajectory.length > 0
|
||||||
? '확산 궤적을 분석하여 해류 직교 방향 1차 방어선, U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 생성합니다.'
|
? '확산 궤적을 분석하여 해류 직교 방향 1차 방어선, U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 생성합니다.'
|
||||||
: '상단에서 확산 예측을 실행한 뒤 AI 배치를 적용할 수 있습니다.'
|
: '상단에서 확산 예측을 실행한 뒤 AI 배치를 적용할 수 있습니다.'
|
||||||
|
|||||||
@ -584,7 +584,7 @@ function KospsPanel() {
|
|||||||
<div className="text-[10px] text-text-2 leading-[1.7]">
|
<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개 계수를 결정합니다.
|
전자해도(ENC) 무작위 수심자료를 계산격자로 보간할 때 <b className="text-status-green">Akima(1978) 2차원 5차다항식</b>(Bivariate Quintic Polynomial)을 사용합니다. 삼각망(TIN)을 구성하여 각 꼭지점의 편미분 연속성 조건으로 21개 계수를 결정합니다.
|
||||||
</div>
|
</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ⁱ yʲ <span className="text-[10px] text-text-3">(i≤5, i+j≤5)</span>
|
z(x,y) = Σ Σ qᵢⱼ xⁱ yʲ <span className="text-[10px] text-text-3">(i≤5, i+j≤5)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1068,7 +1068,7 @@ function OpenDriftPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] whitespace-nowrap text-text-3">2024</span>
|
<span className="text-[10px] whitespace-nowrap text-text-3">2024</span>
|
||||||
</div>
|
</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="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="grid grid-cols-2 gap-2 mb-2">
|
||||||
<div className="text-[11px] text-text-2 leading-[1.7]">
|
<div className="text-[11px] text-text-2 leading-[1.7]">
|
||||||
@ -1103,7 +1103,7 @@ function OpenDriftPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] whitespace-nowrap text-text-3">1998</span>
|
<span className="text-[10px] whitespace-nowrap text-text-3">1998</span>
|
||||||
</div>
|
</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.52–59, 1998</div>
|
<div className="text-[11px] mb-2 text-text-3">류청로, 김종규, 설동관, 강동욱 | 부경대학교 해양공학과 | 한국해양환경공학회지 Vol.1 No.2, pp.52–59, 1998</div>
|
||||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||||
<div className="text-[11px] text-text-2 leading-[1.7]">
|
<div className="text-[11px] text-text-2 leading-[1.7]">
|
||||||
@ -1135,7 +1135,7 @@ function OpenDriftPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] whitespace-nowrap text-text-3">2008</span>
|
<span className="text-[10px] whitespace-nowrap text-text-3">2008</span>
|
||||||
</div>
|
</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.60–63, 2008</div>
|
<div className="text-[11px] mb-2 text-text-3">정태성, 조형진 | 한남대학교 토목환경공학과 | 한국해안·해양공학회 학술발표논문집 제17권 pp.60–63, 2008</div>
|
||||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||||
<div className="text-[11px] text-text-2 leading-[1.7]">
|
<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(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(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(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>
|
</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' }}>
|
<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' && (
|
{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>
|
<colgroup><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /></colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
@ -447,7 +447,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sec.id === 'rescue-resource' && (
|
{sec.id === 'rescue-resource' && (
|
||||||
<table className="w-full text-[11px]" className="border-collapse">
|
<table className="w-full text-[11px] border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border">
|
<tr className="border-b border-border">
|
||||||
<th className="px-3 py-2 text-left text-text-3 font-korean">유형</th>
|
<th className="px-3 py-2 text-left text-text-3 font-korean">유형</th>
|
||||||
|
|||||||
@ -117,7 +117,7 @@ export function ReportsView() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<table className="w-full table-fixed" className="border-collapse">
|
<table className="w-full table-fixed border-collapse">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '3%' }} />
|
<col style={{ width: '3%' }} />
|
||||||
<col style={{ width: '4%' }} />
|
<col style={{ width: '4%' }} />
|
||||||
|
|||||||
@ -128,7 +128,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
|
|||||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
{template.sections.map((section, sIdx) => (
|
{template.sections.map((section, sIdx) => (
|
||||||
<div key={sIdx} className="mb-6 w-full">
|
<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">
|
<table className="w-full table-fixed border-collapse">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '180px' }} />
|
<col style={{ width: '180px' }} />
|
||||||
@ -235,7 +235,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
|
|||||||
{/* Report Title */}
|
{/* Report Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h2 className="text-[18px] font-bold text-text-1 font-korean mb-1">해양오염방제지원시스템</h2>
|
<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}
|
{formData['incident.name'] || template.label}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] text-text-3 font-korean mt-2">
|
<p className="text-[11px] text-text-3 font-korean mt-2">
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import ScatLeftPanel from './ScatLeftPanel';
|
|||||||
import ScatMap from './ScatMap';
|
import ScatMap from './ScatMap';
|
||||||
import ScatTimeline from './ScatTimeline';
|
import ScatTimeline from './ScatTimeline';
|
||||||
import ScatPopup from './ScatPopup';
|
import ScatPopup from './ScatPopup';
|
||||||
|
import ScatRightPanel from './ScatRightPanel';
|
||||||
|
|
||||||
// ═══ Main PreScatView ═══
|
// ═══ Main PreScatView ═══
|
||||||
|
|
||||||
@ -21,6 +22,8 @@ export function PreScatView() {
|
|||||||
const [statusFilter, setStatusFilter] = useState('전체');
|
const [statusFilter, setStatusFilter] = useState('전체');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [popupData, setPopupData] = useState<ScatDetail | null>(null);
|
const [popupData, setPopupData] = useState<ScatDetail | null>(null);
|
||||||
|
const [panelDetail, setPanelDetail] = useState<ScatDetail | null>(null);
|
||||||
|
const [panelLoading, setPanelLoading] = useState(false);
|
||||||
const [timelineIdx, setTimelineIdx] = useState(6);
|
const [timelineIdx, setTimelineIdx] = useState(6);
|
||||||
|
|
||||||
// API에서 구역 및 구간 데이터 로딩
|
// 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) => {
|
const filteredSegments = segments.filter((s) => {
|
||||||
if (jurisdictionFilter === '서귀포해양경비안전서') return s.jurisdiction === '서귀포';
|
if (jurisdictionFilter === '서귀포해양경비안전서') return s.jurisdiction === '서귀포';
|
||||||
@ -144,6 +162,11 @@ export function PreScatView() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ScatRightPanel
|
||||||
|
detail={panelDetail}
|
||||||
|
loading={panelLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
{popupData && (
|
{popupData && (
|
||||||
<ScatPopup data={popupData} segCode={selectedSeg.code} onClose={handleClosePopup} />
|
<ScatPopup data={popupData} segCode={selectedSeg.code} onClose={handleClosePopup} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
227
frontend/src/tabs/scat/components/ScatRightPanel.tsx
Normal file
227
frontend/src/tabs/scat/components/ScatRightPanel.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
불러오는 중...
Reference in New Issue
Block a user