CCTV 실시간 영상: - CCTVPlayer 컴포넌트 (hls.js 기반 HLS/MJPEG/MP4 재생) - 백엔드 HLS 프록시 엔드포인트 (CORS 우회, m3u8 URL 재작성) - KHOA 15개 + KBS 6개 실제 해안 CCTV 연동 - Vite dev proxy, 스트림 타입 자동 감지 유틸리티 HNS 분석: - HNS 시나리오 저장/불러오기/재계산 기능 - 물질 DB 검색 및 상세 정보 연동 - 좌표/파라미터 입력 UI 개선 - Python 확산 모델 스크립트 (hns_dispersion.py) 공통: - 3D 지도 토글, 보고서 생성 개선 - useSubMenu 훅, mapUtils 확장 - ESLint set-state-in-effect 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1024 lines
83 KiB
TypeScript
Executable File
1024 lines
83 KiB
TypeScript
Executable File
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
||
import { sanitizeHtml } from '@common/utils/sanitize'
|
||
import { api } from '@common/services/api'
|
||
import type { HNSSearchSubstance } from '@common/types/hns'
|
||
|
||
/* ═══ HNS 물질 데이터베이스 ═══ */
|
||
interface HNSSubstance {
|
||
name: string
|
||
nameEn: string
|
||
formula: string
|
||
unNumber: string
|
||
casNumber: string
|
||
imdgClass: string
|
||
sebc: string
|
||
flashPoint: string
|
||
boilingPoint: string
|
||
specificGravity: string
|
||
vaporPressure: string
|
||
solubility: string
|
||
idlh: string
|
||
twa: string
|
||
aegl1: string
|
||
aegl2: string
|
||
aegl3: string
|
||
category: string
|
||
color: string
|
||
}
|
||
|
||
const substances: HNSSubstance[] = [
|
||
{ name: '톨루엔', nameEn: 'Toluene', formula: 'C₇H₈', unNumber: 'UN1294', casNumber: '108-88-3', imdgClass: 'Class 3', sebc: 'E (증발)', flashPoint: '4°C', boilingPoint: '111°C', specificGravity: '0.867', vaporPressure: '28.4 mmHg', solubility: '0.05%', idlh: '500 ppm', twa: '20 ppm', aegl1: '37 ppm', aegl2: '150 ppm', aegl3: '500 ppm', category: 'flammable_liquid', color: '#06b6d4' },
|
||
{ name: '벤젠', nameEn: 'Benzene', formula: 'C₆H₆', unNumber: 'UN1114', casNumber: '71-43-2', imdgClass: 'Class 3', sebc: 'E (증발)', flashPoint: '-11°C', boilingPoint: '80°C', specificGravity: '0.879', vaporPressure: '94.8 mmHg', solubility: '0.18%', idlh: '500 ppm', twa: '0.5 ppm', aegl1: '9 ppm', aegl2: '800 ppm', aegl3: '4,000 ppm', category: 'flammable_liquid', color: '#ef4444' },
|
||
{ name: '암모니아', nameEn: 'Ammonia', formula: 'NH₃', unNumber: 'UN1005', casNumber: '7664-41-7', imdgClass: 'Class 2.3', sebc: 'G (가스)', flashPoint: '-', boilingPoint: '-33°C', specificGravity: '0.73', vaporPressure: '8,585 mmHg', solubility: '31%', idlh: '300 ppm', twa: '25 ppm', aegl1: '30 ppm', aegl2: '160 ppm', aegl3: '1,100 ppm', category: 'toxic_gas', color: '#a855f7' },
|
||
{ name: '메탄올', nameEn: 'Methanol', formula: 'CH₃OH', unNumber: 'UN1230', casNumber: '67-56-1', imdgClass: 'Class 3', sebc: 'D (용해)', flashPoint: '11°C', boilingPoint: '65°C', specificGravity: '0.791', vaporPressure: '127 mmHg', solubility: '완전 용해', idlh: '6,000 ppm', twa: '200 ppm', aegl1: '-', aegl2: '2,100 ppm', aegl3: '14,000 ppm', category: 'flammable_liquid', color: '#3b82f6' },
|
||
{ name: '자일렌', nameEn: 'Xylene', formula: 'C₈H₁₀', unNumber: 'UN1307', casNumber: '1330-20-7', imdgClass: 'Class 3', sebc: 'F (부유)', flashPoint: '27°C', boilingPoint: '144°C', specificGravity: '0.864', vaporPressure: '8.8 mmHg', solubility: '0.02%', idlh: '900 ppm', twa: '100 ppm', aegl1: '130 ppm', aegl2: '920 ppm', aegl3: '2,500 ppm', category: 'flammable_liquid', color: '#f97316' },
|
||
{ name: '스타이렌', nameEn: 'Styrene', formula: 'C₈H₈', unNumber: 'UN2055', casNumber: '100-42-5', imdgClass: 'Class 3', sebc: 'F (부유)', flashPoint: '31°C', boilingPoint: '145°C', specificGravity: '0.906', vaporPressure: '6.4 mmHg', solubility: '0.03%', idlh: '700 ppm', twa: '20 ppm', aegl1: '20 ppm', aegl2: '130 ppm', aegl3: '1,100 ppm', category: 'flammable_liquid', color: '#eab308' },
|
||
{ name: '아세톤', nameEn: 'Acetone', formula: 'C₃H₆O', unNumber: 'UN1090', casNumber: '67-64-1', imdgClass: 'Class 3', sebc: 'E (증발)', flashPoint: '-20°C', boilingPoint: '56°C', specificGravity: '0.784', vaporPressure: '231 mmHg', solubility: '완전 용해', idlh: '2,500 ppm', twa: '500 ppm', aegl1: '200 ppm', aegl2: '3,200 ppm', aegl3: '13,000 ppm', category: 'flammable_liquid', color: '#22c55e' },
|
||
{ name: '염소', nameEn: 'Chlorine', formula: 'Cl₂', unNumber: 'UN1017', casNumber: '7782-50-5', imdgClass: 'Class 2.3', sebc: 'G (가스)', flashPoint: '-', boilingPoint: '-34°C', specificGravity: '1.56', vaporPressure: '6,627 mmHg', solubility: '0.7%', idlh: '10 ppm', twa: '0.5 ppm', aegl1: '0.5 ppm', aegl2: '2 ppm', aegl3: '20 ppm', category: 'toxic_gas', color: '#ef4444' },
|
||
{ name: '수소', nameEn: 'Hydrogen', formula: 'H₂', unNumber: 'UN1049', casNumber: '1333-74-0', imdgClass: 'Class 2.1', sebc: 'G (가스)', flashPoint: '-', boilingPoint: '-253°C', specificGravity: '0.07', vaporPressure: '-', solubility: '0.0016%', idlh: '-', twa: '-', aegl1: '-', aegl2: '-', aegl3: '-', category: 'flammable_gas', color: '#06b6d4' },
|
||
{ 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 = [
|
||
{ id: 'all', label: '전체', icon: '📋' },
|
||
{ id: 'toxic_liquid', label: '유독성 액체', icon: '🧪' },
|
||
{ id: 'toxic_gas', label: '유독성 가스', icon: '☠️' },
|
||
{ id: 'flammable_liquid', label: '인화성 액체', icon: '🔥' },
|
||
{ id: 'flammable_gas', label: '인화성 가스', icon: '💨' },
|
||
]
|
||
|
||
export function HNSSubstanceView() {
|
||
const [activeTab, setActiveTab] = useState(0)
|
||
const [selectedCategory, setSelectedCategory] = useState('all')
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const [detailSearchName, setDetailSearchName] = useState('')
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const [detailSearchCas, setDetailSearchCas] = useState('')
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const [detailSearchSebc, setDetailSearchSebc] = useState('전체 거동분류')
|
||
/* Panel 3: 물질 상세검색 state */
|
||
const [hmsSearchType, setHmsSearchType] = useState<'all' | 'abbr' | 'korName' | 'engName' | 'cas' | 'un'>('all')
|
||
const [hmsSearchInput, setHmsSearchInput] = useState('')
|
||
const [hmsFilterSebc, setHmsFilterSebc] = useState('전체 거동분류')
|
||
const [hmsSelectedId, setHmsSelectedId] = useState<number | null>(null)
|
||
const [hmsDetailTab, setHmsDetailTab] = useState(0)
|
||
const [hmsPage, setHmsPage] = useState(1)
|
||
const [hmsResults, setHmsResults] = useState<HNSSearchSubstance[]>([])
|
||
const [hmsTotal, setHmsTotal] = useState(0)
|
||
const [hmsLoading, setHmsLoading] = useState(false)
|
||
const [hmsSelectedSubstance, setHmsSelectedSubstance] = useState<HNSSearchSubstance | null>(null)
|
||
const contentRef = useRef<HTMLDivElement>(null)
|
||
|
||
// 검색 타입 매핑 (프론트엔드 → API)
|
||
const searchTypeMap: Record<string, string> = {
|
||
abbr: 'abbreviation', korName: 'nameKr', engName: 'nameEn', cas: 'casNumber', un: 'unNumber',
|
||
}
|
||
|
||
// HNS 물질 검색 API 호출
|
||
const fetchHnsSubstances = useCallback(async () => {
|
||
setHmsLoading(true)
|
||
try {
|
||
const params: Record<string, string | number> = { page: hmsPage, limit: 10 }
|
||
if (hmsSearchInput.trim()) {
|
||
params.q = hmsSearchInput.trim()
|
||
if (hmsSearchType !== 'all') {
|
||
params.type = searchTypeMap[hmsSearchType] || 'abbreviation'
|
||
}
|
||
}
|
||
if (hmsFilterSebc !== '전체 거동분류') {
|
||
params.sebc = hmsFilterSebc.split(' ')[0]
|
||
}
|
||
const { data } = await api.get('/hns', { params })
|
||
setHmsResults(data.items)
|
||
setHmsTotal(data.total)
|
||
} catch (err) {
|
||
console.error('[HNS] 물질 검색 오류:', err)
|
||
setHmsResults([])
|
||
setHmsTotal(0)
|
||
} finally {
|
||
setHmsLoading(false)
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [hmsSearchInput, hmsSearchType, hmsFilterSebc, hmsPage])
|
||
|
||
// 검색 조건 변경 시 API 호출 (디바운스)
|
||
useEffect(() => {
|
||
const timer = setTimeout(fetchHnsSubstances, 300)
|
||
return () => clearTimeout(timer)
|
||
}, [fetchHnsSubstances])
|
||
|
||
// 물질 선택 시 상세 정보 조회
|
||
useEffect(() => {
|
||
if (hmsSelectedId === null) {
|
||
setHmsSelectedSubstance(null)
|
||
return
|
||
}
|
||
api.get(`/hns/${hmsSelectedId}`).then(({ data }) => {
|
||
setHmsSelectedSubstance(data)
|
||
}).catch(() => {
|
||
setHmsSelectedSubstance(null)
|
||
})
|
||
}, [hmsSelectedId])
|
||
|
||
const handleExportPDF = () => {
|
||
if (!contentRef.current) return
|
||
const clone = contentRef.current.cloneNode(true) as HTMLElement
|
||
clone.querySelectorAll('[data-html2pdf-ignore]').forEach(el => el.remove())
|
||
const content = sanitizeHtml(clone.innerHTML)
|
||
const styles = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]'))
|
||
.map(el => el.outerHTML).join('\n')
|
||
const fullHtml = `<!DOCTYPE html>
|
||
<html><head><meta charset="utf-8"/>
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'none';"/>
|
||
<title>HNS 물질정보</title>
|
||
${styles}
|
||
<style>
|
||
:root { --t1: #ffffff !important; --t2: #d0d6e6 !important; --t3: #a8b0c8 !important; }
|
||
@media print { @page { size: A4; margin: 10mm; } body { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } }
|
||
body { background: var(--bg0); color: var(--t1); font-family: 'Noto Sans KR', sans-serif; padding: 20px 24px; }
|
||
</style>
|
||
</head><body>${content}</body></html>`
|
||
const blob = new Blob([fullHtml], { type: 'text/html; charset=utf-8' })
|
||
const url = URL.createObjectURL(blob)
|
||
const win = window.open(url, '_blank')
|
||
if (win) {
|
||
win.addEventListener('afterprint', () => URL.revokeObjectURL(url))
|
||
setTimeout(() => { win.document.title = 'HNS_물질정보'; win.print() }, 500)
|
||
}
|
||
setTimeout(() => URL.revokeObjectURL(url), 30000)
|
||
}
|
||
|
||
const filtered = substances.filter(s => {
|
||
const matchCategory = selectedCategory === 'all' || s.category === selectedCategory
|
||
const q = searchQuery.toLowerCase()
|
||
const matchSearch = !q || s.name.toLowerCase().includes(q) || s.nameEn.toLowerCase().includes(q) || s.unNumber.toLowerCase().includes(q) || s.casNumber.includes(q) || s.formula.toLowerCase().includes(q)
|
||
return matchCategory && matchSearch
|
||
})
|
||
|
||
/* Detail search filter for Panel 3 (legacy) */
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const detailFiltered = substances.filter(s => {
|
||
const qName = detailSearchName.toLowerCase()
|
||
const qCas = detailSearchCas.toLowerCase()
|
||
const matchName = !qName || s.name.toLowerCase().includes(qName) || s.nameEn.toLowerCase().includes(qName)
|
||
const matchCas = !qCas || s.casNumber.includes(qCas)
|
||
const matchSebc = detailSearchSebc === '전체 거동분류' || s.sebc.includes(detailSearchSebc.split(' ')[0])
|
||
return matchName && matchCas && matchSebc
|
||
})
|
||
|
||
/* Panel 3: HNS API 기반 검색 결과 */
|
||
const HMS_PER_PAGE = 10
|
||
const hmsTotalPages = Math.max(1, Math.ceil(hmsTotal / HMS_PER_PAGE))
|
||
const hmsPageData = hmsResults
|
||
|
||
const tabLabels = [
|
||
{ icon: '📊', label: 'SEBC 거동분류' },
|
||
{ icon: '🧪', label: '주요 물질 특성' },
|
||
{ icon: '⚡', label: '위험도 기준' },
|
||
{ icon: '🔍', label: '물질 상세검색' },
|
||
]
|
||
|
||
return (
|
||
<div className="flex flex-col h-full w-full flex-1 overflow-hidden bg-bg-0">
|
||
<div ref={contentRef} className="flex-1 overflow-y-auto p-5" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||
|
||
{/* 헤더 */}
|
||
<div className="flex items-center justify-between mb-5">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center justify-center text-[20px] shrink-0" style={{ width: 42, height: 42, borderRadius: 10, background: 'linear-gradient(135deg,rgba(249,115,22,.2),rgba(239,68,68,.15))', border: '1px solid rgba(249,115,22,.3)' }}>🧬</div>
|
||
<div>
|
||
<div className="text-base font-bold">HNS 물질정보 데이터베이스</div>
|
||
<div className="text-[10px] text-text-3 mt-0.5">SEBC 거동분류 · CHRIS/CAMEO DB · AEGL/ERPG/IDLH 기준 · 6,500+ 물질</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-1.5" data-html2pdf-ignore>
|
||
<input
|
||
type="text"
|
||
placeholder="물질명 또는 CAS 번호 검색..."
|
||
value={searchQuery}
|
||
onChange={e => setSearchQuery(e.target.value)}
|
||
className="rounded-sm border border-border text-[10px] outline-none bg-bg-3 px-3 py-1.5 w-[200px]"
|
||
/>
|
||
<button
|
||
onClick={handleExportPDF}
|
||
className="rounded-sm text-[10px] font-semibold cursor-pointer text-status-orange px-3.5 py-1.5"
|
||
style={{ border: '1px solid rgba(249,115,22,.3)', background: 'rgba(249,115,22,.08)' }}
|
||
>📥 DB 다운로드</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 서브탭 */}
|
||
<div className="flex rounded-md border border-border mb-5 bg-bg-3 p-1 gap-[3px]" data-html2pdf-ignore>
|
||
{tabLabels.map((tab, idx) => (
|
||
<button
|
||
key={idx}
|
||
onClick={() => setActiveTab(idx)}
|
||
className={activeTab === idx ? 'wx-tbtn on' : 'wx-tbtn'}
|
||
style={{ flex: 1, padding: 8, fontSize: 11 }}
|
||
>{tab.icon} {tab.label}</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* ═══ MAT PANEL 0: SEBC 거동분류 ═══ */}
|
||
{activeTab === 0 && (
|
||
<div>
|
||
<div className="rounded-[12px] p-4 mb-4 relative overflow-hidden" style={{ background: 'linear-gradient(135deg,rgba(249,115,22,.05),rgba(6,182,212,.03))', border: '1px solid rgba(249,115,22,.2)' }}>
|
||
<div className="absolute top-0 left-0 right-0" style={{ height: 3, background: 'linear-gradient(90deg,var(--orange),var(--cyan),var(--green),var(--purple))' }} />
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className="text-[13px] font-bold text-status-orange">SEBC 해양 거동 분류 체계</span>
|
||
<span className="text-[8px] font-semibold text-status-orange py-[2px] px-2 rounded-md" style={{ background: 'rgba(249,115,22,.1)' }}>Standard European Behaviour Classification</span>
|
||
</div>
|
||
<div className="text-[10px] text-text-2 leading-[1.7] mb-[14px]">
|
||
HNS 물질이 해양에 유출되었을 때의 <b className="text-status-orange">물리·화학적 거동</b>에 따라 분류하는 국제 표준 체계입니다. 물질의 밀도, 증기압, 용해도에 따라 5개 주요 거동 유형과 혼합 유형으로 구분되며, 각 유형별로 대응 전략이 달라집니다.
|
||
</div>
|
||
<div className="grid mb-[14px]" style={{ gridTemplateColumns: 'repeat(5,1fr)', gap: 8 }}>
|
||
{/* G: Gas */}
|
||
<div className="p-3 rounded-md text-center" style={{ background: 'rgba(168,85,247,.06)', border: '1px solid rgba(168,85,247,.2)' }}>
|
||
<div className="flex items-center justify-center text-[18px] mx-auto mb-2" style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(168,85,247,.15)' }}>💨</div>
|
||
<div className="text-[13px] font-mono font-extrabold text-primary-purple">G</div>
|
||
<div className="text-[11px] font-bold my-1">Gas</div>
|
||
<div className="text-[8px] text-text-2 leading-normal">기체 상태로 대기 중 확산. 증기압이 높아 빠르게 증발</div>
|
||
<div className="mt-1.5 text-[7px] font-semibold text-primary-purple p-[3px]" style={{ background: 'rgba(168,85,247,.08)', borderRadius: 3 }}>대기확산 모델 적용</div>
|
||
</div>
|
||
{/* E: Evaporator */}
|
||
<div className="p-3 rounded-md text-center" style={{ background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.2)' }}>
|
||
<div className="flex items-center justify-center text-[18px] mx-auto mb-2" style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(239,68,68,.15)' }}>🔥</div>
|
||
<div className="text-[13px] font-mono font-extrabold text-status-red">E</div>
|
||
<div className="text-[11px] font-bold my-1">Evaporator</div>
|
||
<div className="text-[8px] text-text-2 leading-normal">해수면에서 증발. 부유 후 기화하여 독성 가스 생성</div>
|
||
<div className="mt-1.5 text-[7px] font-semibold text-status-red p-[3px]" style={{ background: 'rgba(239,68,68,.08)', borderRadius: 3 }}>대기+해양 복합 대응</div>
|
||
</div>
|
||
{/* F: Floater */}
|
||
<div className="p-3 rounded-md text-center" style={{ background: 'rgba(234,179,8,.06)', border: '1px solid rgba(234,179,8,.2)' }}>
|
||
<div className="flex items-center justify-center text-[18px] mx-auto mb-2" style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(234,179,8,.15)' }}>🟡</div>
|
||
<div className="text-[13px] font-mono font-extrabold text-status-yellow">F</div>
|
||
<div className="text-[11px] font-bold my-1">Floater</div>
|
||
<div className="text-[8px] text-text-2 leading-normal">{'해수면 위에 부유. 비중 < 1.0, 불용성 물질'}</div>
|
||
<div className="mt-1.5 text-[7px] font-semibold text-status-yellow p-[3px]" style={{ background: 'rgba(234,179,8,.08)', borderRadius: 3 }}>오일펜스 유사 봉쇄</div>
|
||
</div>
|
||
{/* D: Dissolver */}
|
||
<div className="p-3 rounded-md text-center" style={{ background: 'rgba(6,182,212,.06)', border: '1px solid rgba(6,182,212,.2)' }}>
|
||
<div className="flex items-center justify-center text-[18px] mx-auto mb-2" style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(6,182,212,.15)' }}>💧</div>
|
||
<div className="text-[13px] font-mono font-extrabold text-primary-cyan">D</div>
|
||
<div className="text-[11px] font-bold my-1">Dissolver</div>
|
||
<div className="text-[8px] text-text-2 leading-normal">해수에 용해. 수중 확산하여 넓은 범위 오염</div>
|
||
<div className="mt-1.5 text-[7px] font-semibold text-primary-cyan p-[3px]" style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}>해양확산 모델 적용</div>
|
||
</div>
|
||
{/* S: Sinker */}
|
||
<div className="p-3 rounded-md text-center" style={{ background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.2)' }}>
|
||
<div className="flex items-center justify-center text-[18px] mx-auto mb-2" style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(34,197,94,.15)' }}>⬇</div>
|
||
<div className="text-[13px] font-mono font-extrabold text-status-green">S</div>
|
||
<div className="text-[11px] font-bold my-1">Sinker</div>
|
||
<div className="text-[8px] text-text-2 leading-normal">{'해저로 침강. 비중 > 1.0, 저층 오염 축적'}</div>
|
||
<div className="mt-1.5 text-[7px] font-semibold text-status-green p-[3px]" style={{ background: 'rgba(34,197,94,.08)', borderRadius: 3 }}>저층 3D 모니터링 필수</div>
|
||
</div>
|
||
</div>
|
||
{/* 복합 거동 */}
|
||
<div className="rounded-md p-3 border border-border bg-bg-3">
|
||
<div className="text-[10px] font-bold mb-2">🔀 복합 거동 유형</div>
|
||
<div className="grid text-center text-[8px]" style={{ gridTemplateColumns: 'repeat(5,1fr)', gap: 6 }}>
|
||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-primary-purple">GD</span><br/><span className="text-text-3">기체+용해</span></div>
|
||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-status-red">ED</span><br/><span className="text-text-3">증발+용해</span></div>
|
||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-status-yellow">FE</span><br/><span className="text-text-3">부유+증발</span></div>
|
||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-primary-cyan">FED</span><br/><span className="text-text-3">부유+증발+용해</span></div>
|
||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-status-green">SD</span><br/><span className="text-text-3">침강+용해</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ═══ MAT PANEL 1: 주요 물질 특성 ═══ */}
|
||
{activeTab === 1 && (
|
||
<div>
|
||
{/* 카테고리 필터 */}
|
||
<div className="flex gap-1.5 mb-[14px]" data-html2pdf-ignore>
|
||
{categories.map(cat => (
|
||
<button key={cat.id} onClick={() => setSelectedCategory(cat.id)}
|
||
className="rounded-sm text-[10px] font-semibold cursor-pointer"
|
||
style={{
|
||
padding: '6px 12px',
|
||
border: selectedCategory === cat.id ? '1px solid var(--orange)' : '1px solid var(--bd)',
|
||
color: selectedCategory === cat.id ? 'var(--orange)' : 'var(--t3)',
|
||
background: selectedCategory === cat.id ? 'rgba(249,115,22,.08)' : 'var(--bg3)',
|
||
}}
|
||
>{cat.icon} {cat.label}</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* 주요 HNS 물질 카드 */}
|
||
<div className="grid grid-cols-2 gap-[14px] mb-4">
|
||
|
||
{/* 암모니아 */}
|
||
{filtered.find(s => s.casNumber === '7664-41-7') && (
|
||
<div className="rounded-[10px] p-[14px] border border-border bg-bg-3" style={{ borderLeft: '4px solid var(--purple)' }}>
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div><span className="text-sm font-mono font-extrabold text-primary-purple">NH₃</span> <span className="text-xs font-bold">암모니아</span></div>
|
||
<div className="flex gap-1">
|
||
<span className="text-[8px] font-semibold text-primary-purple px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(168,85,247,.1)' }}>G/GD</span>
|
||
<span className="text-[8px] font-semibold text-status-red px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(239,68,68,.1)' }}>독성</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">7664-41-7</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">17.03</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">-33.4°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">비중:</span> <span className="font-mono">0.73</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">인화점:</span> <span className="font-mono">N/A (불연)</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">용해도:</span> <span className="font-mono text-primary-cyan">매우 높음</span></div>
|
||
</div>
|
||
<div className="grid text-center text-[7px]" style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
||
<div style={{ padding: 4, background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.12)', borderRadius: 3 }}><span className="text-status-green">AEGL-2</span><br/><b>160 ppm</b></div>
|
||
<div style={{ padding: 4, background: 'rgba(249,115,22,.06)', border: '1px solid rgba(249,115,22,.12)', borderRadius: 3 }}><span className="text-status-orange">ERPG-2</span><br/><b>150 ppm</b></div>
|
||
<div style={{ padding: 4, background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.12)', borderRadius: 3 }}><span className="text-status-red">IDLH</span><br/><b>300 ppm</b></div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-text-3 leading-normal">해상 유출 시 급속 기화 → 독성 가스운 형성. 물에 잘 용해되어 수중 독성도 높음. 해풍 환경에서 확산 범위 확대.</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 메탄올 */}
|
||
{filtered.find(s => s.casNumber === '67-56-1') && (
|
||
<div className="rounded-[10px] p-[14px] border border-border bg-bg-3" style={{ borderLeft: '4px solid var(--cyan)' }}>
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div><span className="text-sm font-mono font-extrabold text-primary-cyan">CH₃OH</span> <span className="text-xs font-bold">메탄올</span></div>
|
||
<div className="flex gap-1">
|
||
<span className="text-[8px] font-semibold text-primary-cyan px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}>ED</span>
|
||
<span className="text-[8px] font-semibold text-status-orange px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(249,115,22,.1)' }}>인화성</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">67-56-1</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">32.04</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">64.7°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">비중:</span> <span className="font-mono">0.79</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">인화점:</span> <span className="font-mono text-status-orange">11°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">용해도:</span> <span className="font-mono text-primary-cyan">완전 혼화</span></div>
|
||
</div>
|
||
<div className="grid text-center text-[7px]" style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
||
<div style={{ padding: 4, background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.12)', borderRadius: 3 }}><span className="text-status-green">AEGL-2</span><br/><b>2,100 ppm</b></div>
|
||
<div style={{ padding: 4, background: 'rgba(249,115,22,.06)', border: '1px solid rgba(249,115,22,.12)', borderRadius: 3 }}><span className="text-status-orange">ERPG-2</span><br/><b>1,000 ppm</b></div>
|
||
<div style={{ padding: 4, background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.12)', borderRadius: 3 }}><span className="text-status-red">IDLH</span><br/><b>6,000 ppm</b></div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-text-3 leading-normal">해수에 완전 용해 → 수질 오염 장기화. 인화점 낮아 화재 위험. 증발 시 독성 증기 발생. 2007 온산항 FODDANGER호 95만L 유출 사고.</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 수소 */}
|
||
{filtered.find(s => s.casNumber === '1333-74-0') && (
|
||
<div className="rounded-[10px] p-[14px] border border-border bg-bg-3" style={{ borderLeft: '4px solid var(--red)' }}>
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div><span className="text-sm font-mono font-extrabold text-status-red">H₂</span> <span className="text-xs font-bold">수소</span></div>
|
||
<div className="flex gap-1">
|
||
<span className="text-[8px] font-semibold text-primary-purple px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(168,85,247,.1)' }}>G</span>
|
||
<span className="text-[8px] font-semibold text-status-red px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(239,68,68,.1)' }}>폭발</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">1333-74-0</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">2.016</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">-252.9°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">비중(기체):</span> <span className="font-mono">0.07</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">LFL:</span> <span className="font-mono text-status-red">4.0%</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">UFL:</span> <span className="font-mono text-status-red">75.0%</span></div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-text-3 leading-normal">폭발 범위 극히 넓음(4~75%). 무색·무취로 감지 불가. 극저온 액화수소 유출 시 BLEVE 위험. 급속 상승 확산.</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* LNG */}
|
||
{filtered.find(s => s.nameEn === 'LPG (Propane/Butane)' || s.casNumber === '74-82-8') && (
|
||
<div className="rounded-[10px] p-[14px] border border-border bg-bg-3" style={{ borderLeft: '4px solid var(--orange)' }}>
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div><span className="text-sm font-mono font-extrabold text-status-orange">CH₄</span> <span className="text-xs font-bold">LNG (메탄)</span></div>
|
||
<div className="flex gap-1">
|
||
<span className="text-[8px] font-semibold text-primary-purple px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(168,85,247,.1)' }}>G</span>
|
||
<span className="text-[8px] font-semibold text-status-orange px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(249,115,22,.1)' }}>인화/폭발</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">74-82-8</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">16.04</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">-161.5°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">비중(액체):</span> <span className="font-mono">0.42</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">LFL:</span> <span className="font-mono text-status-orange">5.0%</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">UFL:</span> <span className="font-mono text-status-orange">15.0%</span></div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-text-3 leading-normal">극저온(-162°C) 유출 시 RPT(급속상변환폭발), Pool Fire 위험. Flash 기화 → 가연성 가스운 형성. 인천·평택항 LNG 물동량 상위.</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 페놀 */}
|
||
{filtered.find(s => s.casNumber === '108-95-2' || s.name === '페놀') && (
|
||
<div className="rounded-[10px] p-[14px] border border-border bg-bg-3" style={{ borderLeft: '4px solid var(--green)' }}>
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div><span className="text-sm font-mono font-extrabold text-status-green">C₆H₅OH</span> <span className="text-xs font-bold">페놀</span></div>
|
||
<div className="flex gap-1">
|
||
<span className="text-[8px] font-semibold text-status-green px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(34,197,94,.1)' }}>S/SD</span>
|
||
<span className="text-[8px] font-semibold text-status-red px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(239,68,68,.1)' }}>독성</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">108-95-2</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">94.11</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono">181.7°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">비중:</span> <span className="font-mono text-status-green">1.07 (침강)</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">인화점:</span> <span className="font-mono text-status-orange">79°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">용해도:</span> <span className="font-mono text-primary-cyan">84 g/L</span></div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-text-3 leading-normal">비중 1.07 → <b className="text-status-green">Sinker 특성</b>으로 저층 축적. ROMS 검증 결과 저층 농도가 표층의 3.5배. 해양산업시설 배출 주요 HNS (31.8kg/일).</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 톨루엔 */}
|
||
{filtered.find(s => s.casNumber === '108-88-3') && (
|
||
<div className="rounded-[10px] p-[14px] border border-border bg-bg-3" style={{ borderLeft: '4px solid var(--yellow)' }}>
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div><span className="text-sm font-mono font-extrabold text-status-yellow">C₇H₈</span> <span className="text-xs font-bold">톨루엔</span></div>
|
||
<div className="flex gap-1">
|
||
<span className="text-[8px] font-semibold text-status-yellow px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(234,179,8,.1)' }}>FE</span>
|
||
<span className="text-[8px] font-semibold text-status-orange px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(249,115,22,.1)' }}>인화성</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">108-88-3</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">92.14</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono">110.6°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">비중:</span> <span className="font-mono text-status-yellow">0.87 (부유)</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">인화점:</span> <span className="font-mono text-status-orange">4°C</span></div>
|
||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">용해도:</span> <span className="font-mono">0.52 g/L</span></div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-text-3 leading-normal">해수면 부유 → 증발. 인화점 극히 낮아(4°C) 화재 위험 상시. 석유화학 산업의 대표적 HNS. 울산항 주요 취급물질.</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ═══ MAT PANEL 2: 위험도 기준 ═══ */}
|
||
{activeTab === 2 && (
|
||
<div>
|
||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||
{/* AEGL 기준 */}
|
||
<div className="rounded-[10px] p-4 border border-border bg-bg-3" style={{ borderTop: '3px solid var(--green)' }}>
|
||
<div className="text-[13px] font-bold text-status-green mb-[10px]">🟢 AEGL (Acute Exposure Guideline Level)</div>
|
||
<div className="text-[9px] text-text-3 mb-[10px]">미국 EPA — 일반인 급성 노출 기준 (10분, 30분, 60분, 4시간, 8시간)</div>
|
||
<div className="flex flex-col gap-1.5">
|
||
<div className="rounded-sm px-2.5 py-2" style={{ background: 'rgba(34,197,94,.04)', border: '1px solid rgba(34,197,94,.12)', borderLeft: '4px solid var(--green)' }}>
|
||
<div className="text-[11px] font-bold text-status-green">AEGL-1 (불쾌감)</div>
|
||
<div className="text-[9px] text-text-2 leading-normal">눈에 띄는 불쾌감, 자극 또는 비감각적 증상. 노출 중단 시 증상 소멸.</div>
|
||
</div>
|
||
<div className="rounded-sm px-2.5 py-2" style={{ background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.12)', borderLeft: '4px solid var(--orange)' }}>
|
||
<div className="text-[11px] font-bold text-status-orange">AEGL-2 (비가역적 영향)</div>
|
||
<div className="text-[9px] text-text-2 leading-normal">비가역적·장기적 건강 영향 또는 대피 능력 저하. <b className="text-status-orange">대피 기준으로 사용</b>.</div>
|
||
</div>
|
||
<div className="rounded-sm px-2.5 py-2" style={{ background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.12)', borderLeft: '4px solid var(--red)' }}>
|
||
<div className="text-[11px] font-bold text-status-red">AEGL-3 (생명 위협)</div>
|
||
<div className="text-[9px] text-text-2 leading-normal">생명을 위협하는 건강 영향 또는 사망. <b className="text-status-red">즉시 대피·격리 구역 설정</b>.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ERPG / IDLH */}
|
||
<div className="rounded-[10px] p-4 border border-border bg-bg-3" style={{ borderTop: '3px solid var(--red)' }}>
|
||
<div className="text-[13px] font-bold text-status-red mb-[10px]">🔴 ERPG & IDLH</div>
|
||
<div className="flex flex-col gap-2">
|
||
<div>
|
||
<div className="text-[10px] font-bold text-status-orange mb-1">ERPG (Emergency Response Planning Guideline)</div>
|
||
<div className="text-[9px] text-text-3 mb-1.5">AIHA 발행 — 1시간 노출 기준</div>
|
||
<div className="flex flex-col gap-1">
|
||
<div className="text-[8px] rounded px-2 py-1.5" style={{ background: 'rgba(34,197,94,.03)', border: '1px solid rgba(34,197,94,.1)', borderLeft: '3px solid var(--green)' }}><b className="text-status-green">ERPG-1</b> — 일시적 건강 영향, 냄새 감지</div>
|
||
<div className="text-[8px] rounded px-2 py-1.5" style={{ background: 'rgba(249,115,22,.03)', border: '1px solid rgba(249,115,22,.1)', borderLeft: '3px solid var(--orange)' }}><b className="text-status-orange">ERPG-2</b> — 비가역적 영향, 대피 판단 기준</div>
|
||
<div className="text-[8px] rounded px-2 py-1.5" style={{ background: 'rgba(239,68,68,.03)', border: '1px solid rgba(239,68,68,.1)', borderLeft: '3px solid var(--red)' }}><b className="text-status-red">ERPG-3</b> — 생명 위협 농도</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1 rounded-sm p-2.5" style={{ background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.15)' }}>
|
||
<div className="text-[10px] font-bold text-status-red mb-1">IDLH (Immediately Dangerous to Life or Health)</div>
|
||
<div className="text-[9px] text-text-3 mb-1">NIOSH — 30분 노출 시 생명·건강에 즉각적 위험</div>
|
||
<div className="text-[9px] text-text-2 leading-normal"><b className="text-status-red">최대 허용 노출 한계</b>로서 이 농도를 초과하면 자급식 호흡장치(SCBA) 없이는 진입 불가. WING 시스템에서 위험구역 자동 설정의 기준값으로 사용.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 물질별 위험도 비교표 */}
|
||
<div className="rounded-[10px] p-4 border border-border bg-bg-3">
|
||
<div className="text-xs font-bold mb-3">📊 주요 HNS 물질별 위험도 기준 비교 (ppm)</div>
|
||
<table className="w-full border-collapse text-[9px]">
|
||
<thead>
|
||
<tr style={{ background: 'rgba(168,85,247,.06)' }}>
|
||
<th className="text-primary-purple text-left" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>물질</th>
|
||
<th className="text-status-green text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>AEGL-1</th>
|
||
<th className="text-status-orange text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>AEGL-2</th>
|
||
<th className="text-status-red text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>AEGL-3</th>
|
||
<th className="text-status-orange text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>ERPG-2</th>
|
||
<th className="text-status-red text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>IDLH</th>
|
||
<th className="text-status-orange text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>LFL(%)</th>
|
||
<th className="text-text-2 text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>SEBC</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||
<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>
|
||
<td className="text-center font-mono">150</td>
|
||
<td className="text-center font-mono text-status-red">300</td>
|
||
<td className="text-center font-mono">15.0</td>
|
||
<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 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-orange">2,100</td>
|
||
<td className="text-center font-mono text-status-red">14,000</td>
|
||
<td className="text-center font-mono">1,000</td>
|
||
<td className="text-center font-mono text-status-red">6,000</td>
|
||
<td className="text-center font-mono">6.0</td>
|
||
<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 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 font-mono font-bold text-status-red">4.0</td>
|
||
<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 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 font-mono text-status-orange">5.0</td>
|
||
<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 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-orange">29</td>
|
||
<td className="text-center font-mono text-status-red">57</td>
|
||
<td className="text-center font-mono">50</td>
|
||
<td className="text-center font-mono text-status-red">250</td>
|
||
<td className="text-center font-mono">1.8</td>
|
||
<td className="text-center font-semibold text-status-green">S/SD</td>
|
||
</tr>
|
||
<tr>
|
||
<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-orange">560</td>
|
||
<td className="text-center font-mono text-status-red">3,700</td>
|
||
<td className="text-center font-mono">300</td>
|
||
<td className="text-center font-mono text-status-red">500</td>
|
||
<td className="text-center font-mono">1.1</td>
|
||
<td className="text-center font-semibold text-status-yellow">FE</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div className="mt-2 text-[7px] text-text-3">※ AEGL: 60분 기준 / ERPG: 1시간 노출 / IDLH: 30분 / LFL: 폭발하한</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ═══ MAT PANEL 3: 물질 상세검색 ═══ */}
|
||
{activeTab === 3 && (
|
||
<div>
|
||
{/* ── 검색창 ── */}
|
||
<div className="rounded-[10px] p-4 mb-4 border border-border bg-bg-3">
|
||
<div className="flex items-center justify-between mb-[14px]">
|
||
<div className="text-[13px] font-bold">🔍 HNS 물질 통합 검색 <span className="text-[9px] font-normal text-text-3">(화물약어·제품명·동의어·CAS·UN번호 통합)</span></div>
|
||
<div className="flex gap-1">
|
||
<button className="text-[9px] font-semibold cursor-pointer text-status-orange rounded px-2.5 py-[5px]" style={{ border: '1px solid rgba(249,115,22,.25)', background: 'rgba(249,115,22,.08)' }}>📥 DB 다운로드</button>
|
||
<button className="text-[9px] font-semibold cursor-pointer text-primary-cyan rounded px-2.5 py-[5px]" style={{ border: '1px solid rgba(6,182,212,.25)', background: 'rgba(6,182,212,.08)' }}>🔗 Port-MIS 연동</button>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2 mb-[10px] items-center">
|
||
<div className="shrink-0 flex items-center gap-1">
|
||
<span className="text-[9px] font-semibold text-text-3">구분:</span>
|
||
<select value={hmsSearchType} onChange={e => { setHmsSearchType(e.target.value as typeof hmsSearchType); setHmsPage(1) }} className="rounded-sm border border-border text-[12px] outline-none bg-bg-0 px-3 py-2" style={{ minWidth: 140 }}>
|
||
<option value="all">전체 통합검색</option>
|
||
<option value="abbr">약자/제품명</option>
|
||
<option value="korName">국문명</option>
|
||
<option value="engName">영문명</option>
|
||
<option value="cas">CAS번호</option>
|
||
<option value="un">UN번호</option>
|
||
</select>
|
||
</div>
|
||
<input type="text" value={hmsSearchInput} onChange={e => { setHmsSearchInput(e.target.value); setHmsPage(1) }} placeholder={hmsSearchType === 'all' ? '물질명, 약자, CAS, UN번호 통합 검색' : hmsSearchType === 'abbr' ? '약자/제품명 입력' : hmsSearchType === 'cas' ? 'CAS번호 입력 (예: 71-43-2)' : hmsSearchType === 'un' ? 'UN번호 입력 (예: 1114)' : '검색어 입력'} className="flex-1 rounded-sm border border-border text-[13px] outline-none bg-bg-0 px-3 py-2" />
|
||
<button onClick={() => setHmsPage(1)} className="text-[13px] font-bold cursor-pointer shrink-0 rounded-sm text-white px-5 py-2" style={{ border: 'none', background: 'linear-gradient(135deg,var(--orange),var(--red))' }}>🔎 검색</button>
|
||
</div>
|
||
<div className="text-[8px] text-text-3 leading-[1.6]">
|
||
※ 국문명·영문명 검색 시 <b className="text-status-orange">동의어까지 검색</b> | 약자/제품명 검색 시 <b className="text-status-orange">부호, 띄어쓰기 제외</b> 후 검색 | 총 <b className="text-primary-cyan">1,222종</b> 등록
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 검색 결과 테이블 ── */}
|
||
<div className="rounded-[10px] p-4 mb-4 border border-border bg-bg-3">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div className="text-[13px] font-bold">📋 검색 결과 <span className="text-[11px] font-normal text-text-3">— {hmsTotal}건 조회</span></div>
|
||
<select value={hmsFilterSebc} onChange={e => { setHmsFilterSebc(e.target.value); setHmsPage(1) }} className="rounded border border-border text-[11px] text-text-2 outline-none bg-bg-0 px-2.5 py-1">
|
||
<option>전체 거동분류</option><option>G (Gas)</option><option>E (Evaporator)</option><option>F (Floater)</option><option>D (Dissolver)</option><option>S (Sinker)</option>
|
||
</select>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full border-collapse text-[11px]">
|
||
<thead>
|
||
<tr style={{ background: 'rgba(249,115,22,.06)' }}>
|
||
<th className="text-status-orange text-left" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)', width: 36 }}>No.</th>
|
||
<th className="text-status-orange text-left" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)' }}>약자/제품명</th>
|
||
<th className="text-text-2 text-left" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)' }}>영문명</th>
|
||
<th className="text-text-2 text-left" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)' }}>영문명 동의어</th>
|
||
<th className="text-primary-cyan text-left" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)' }}>국문명</th>
|
||
<th className="text-text-2 text-left text-[10px]" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)' }}>국문 동의어 / 주요 사용처</th>
|
||
<th className="text-text-2 text-center" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)', width: 68 }}>UN번호</th>
|
||
<th className="text-text-2 text-center" style={{ padding: '8px 8px', borderBottom: '2px solid var(--bdL)', width: 80 }}>CAS번호</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{hmsLoading ? (
|
||
<tr><td colSpan={8} className="text-center text-text-3 py-5 px-2">검색 중...</td></tr>
|
||
) : hmsPageData.length > 0 ? hmsPageData.map((s: HNSSearchSubstance, idx: number) => {
|
||
const isSel = hmsSelectedId === s.id
|
||
return (
|
||
<tr key={s.id} onClick={() => { setHmsSelectedId(isSel ? null : s.id); setHmsDetailTab(0) }}
|
||
style={{ borderBottom: '1px solid rgba(255,255,255,.04)', cursor: 'pointer', background: isSel ? 'rgba(6,182,212,.04)' : undefined }}
|
||
onMouseOver={e => { if (!isSel) e.currentTarget.style.background = 'rgba(249,115,22,.03)' }}
|
||
onMouseOut={e => { if (!isSel) e.currentTarget.style.background = '' }}
|
||
>
|
||
<td className="font-mono text-text-3 px-2 py-2">{(hmsPage - 1) * HMS_PER_PAGE + idx + 1}</td>
|
||
<td className="font-semibold font-mono text-status-orange px-2 py-2">{s.abbreviation}</td>
|
||
<td className="px-2 py-2">{s.nameEn}</td>
|
||
<td className="text-text-3 text-[10px] px-2 py-2">{s.synonymsEn}</td>
|
||
<td className="font-semibold px-2 py-2"><span className="text-primary-cyan underline cursor-pointer" onClick={e => { e.stopPropagation(); setHmsSelectedId(s.id); setHmsDetailTab(0) }}>{s.nameKr}</span></td>
|
||
<td className="text-text-3 text-[10px] px-2 py-2">{s.synonymsKr}</td>
|
||
<td className="text-center font-mono">{s.unNumber}</td>
|
||
<td className="text-center font-mono">{s.casNumber}</td>
|
||
</tr>
|
||
)
|
||
}) : (
|
||
<tr><td colSpan={8} className="text-center text-text-3 py-5 px-2">검색 결과가 없습니다.</td></tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div className="mt-[10px] text-center text-[11px] text-text-3">
|
||
<div className="flex items-center justify-center gap-1 mb-1.5">
|
||
<button onClick={() => setHmsPage(1)} disabled={hmsPage <= 1} className="rounded cursor-pointer border border-border text-[11px] text-text-2 bg-bg-0 px-2.5 py-1" style={{ opacity: hmsPage <= 1 ? 0.4 : 1 }}>◀◀</button>
|
||
<button onClick={() => setHmsPage(p => Math.max(1, p - 1))} disabled={hmsPage <= 1} className="rounded cursor-pointer border border-border text-[11px] text-text-2 bg-bg-0 px-3 py-1" style={{ opacity: hmsPage <= 1 ? 0.4 : 1 }}>◀</button>
|
||
{(() => {
|
||
const range = 2;
|
||
let start = Math.max(1, hmsPage - range);
|
||
let end = Math.min(hmsTotalPages, hmsPage + range);
|
||
if (end - start < range * 2) {
|
||
start = Math.max(1, end - range * 2);
|
||
end = Math.min(hmsTotalPages, start + range * 2);
|
||
}
|
||
const pages = [];
|
||
for (let p = start; p <= end; p++) pages.push(p);
|
||
return pages.map(p => (
|
||
<button key={p} onClick={() => setHmsPage(p)} className="rounded cursor-pointer border border-border text-[11px] px-3 py-1" style={{ background: p === hmsPage ? 'rgba(249,115,22,.15)' : 'var(--bg0)', color: p === hmsPage ? 'var(--orange)' : 'var(--t2)', fontWeight: p === hmsPage ? 700 : 400 }}>{p}</button>
|
||
));
|
||
})()}
|
||
<button onClick={() => setHmsPage(p => Math.min(hmsTotalPages, p + 1))} disabled={hmsPage >= hmsTotalPages} className="rounded cursor-pointer border border-border text-[11px] text-text-2 bg-bg-0 px-3 py-1" style={{ opacity: hmsPage >= hmsTotalPages ? 0.4 : 1 }}>▶</button>
|
||
<button onClick={() => setHmsPage(hmsTotalPages)} disabled={hmsPage >= hmsTotalPages} className="rounded cursor-pointer border border-border text-[11px] text-text-2 bg-bg-0 px-2.5 py-1" style={{ opacity: hmsPage >= hmsTotalPages ? 0.4 : 1 }}>▶▶</button>
|
||
<span className="ml-2 text-text-3">{hmsPage} / {hmsTotalPages} 페이지</span>
|
||
</div>
|
||
<span>총 <b className="text-status-orange">{hmsTotal.toLocaleString()}</b>종 등록</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 물질 상세정보 패널 ── */}
|
||
{hmsSelectedSubstance && <HmsDetailPanel substance={hmsSelectedSubstance} activeTab={hmsDetailTab} onTabChange={setHmsDetailTab} />}
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ═══ HmsDetailPanel: 물질 상세정보 4-tab 패널 ═══ */
|
||
function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: HNSSearchSubstance; activeTab: number; onTabChange: (t: number) => void }) {
|
||
const tabLabels = ['📊 물질특성·위험정보', '🛡 방제거리·PPE·MSDS', '⚓ IBC CODE·EmS 대응', '🔗 화물적부도·항구별 코드']
|
||
const nfpa = s.nfpa
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const sebcColor = s.sebc.startsWith('G') ? 'var(--purple)' : s.sebc.startsWith('E') ? 'var(--red)' : s.sebc.startsWith('F') ? 'var(--yellow)' : s.sebc.startsWith('D') ? 'var(--cyan)' : s.sebc.startsWith('S') ? 'var(--green)' : 'var(--t2)'
|
||
|
||
return (
|
||
<div className="rounded-[10px] overflow-hidden bg-bg-3" style={{ border: '1px solid rgba(6,182,212,.25)', boxShadow: '0 4px 20px rgba(6,182,212,.08)' }}>
|
||
{/* Tab Navigation */}
|
||
<div className="flex border-b border-border" style={{ background: 'linear-gradient(90deg,rgba(6,182,212,.06),rgba(249,115,22,.04))' }}>
|
||
{tabLabels.map((label, i) => (
|
||
<button key={i} onClick={() => onTabChange(i)} style={{ flex: 1, padding: 10, fontSize: 10, fontWeight: activeTab === i ? 700 : 600, cursor: 'pointer', border: 'none', borderBottom: `2px solid ${activeTab === i ? 'var(--cyan)' : 'transparent'}`, background: activeTab === i ? 'rgba(6,182,212,.06)' : 'transparent', color: activeTab === i ? 'var(--cyan)' : 'var(--t3)' }}>{label}</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* TAB 0: 물질특성·위험정보 */}
|
||
{activeTab === 0 && (
|
||
<div className="p-4">
|
||
{/* Header */}
|
||
<div className="flex items-center gap-[14px] mb-4 pb-[14px] border-b border-border">
|
||
<div className="flex items-center justify-center text-[22px] shrink-0" style={{ width: 52, height: 52, borderRadius: 10, background: 'linear-gradient(135deg,rgba(6,182,212,.15),rgba(249,115,22,.1))', border: '1px solid rgba(6,182,212,.25)' }}>🧪</div>
|
||
<div className="flex-1">
|
||
<div style={{ fontSize: 18, fontWeight: 800 }}>{s.nameKr} <span className="text-xs font-normal text-text-3">({s.nameEn})</span></div>
|
||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||
<span className="text-[9px] font-semibold font-mono text-primary-cyan rounded py-[2px] px-2" style={{ background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.2)' }}>CAS: {s.casNumber}</span>
|
||
<span className="text-[9px] font-semibold font-mono text-status-orange rounded py-[2px] px-2" style={{ background: 'rgba(249,115,22,.1)', border: '1px solid rgba(249,115,22,.2)' }}>UN: {s.unNumber}</span>
|
||
<span className="text-[9px] font-semibold text-primary-purple rounded py-[2px] px-2" style={{ background: 'rgba(168,85,247,.1)', border: '1px solid rgba(168,85,247,.2)' }}>운송방법: {s.transportMethod}</span>
|
||
<span className="text-[9px] font-semibold text-status-green rounded py-[2px] px-2" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>SEBC: {s.sebc}</span>
|
||
</div>
|
||
<div className="text-[9px] text-text-3 mt-1"><b>유사명:</b> {s.synonymsKr} | <b>특성:</b> {s.hazardClass}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-[14px]">
|
||
{/* Left: 물리·화학적 특성 */}
|
||
<div>
|
||
<div className="text-[11px] font-bold text-status-orange mb-2">⚗️ 물리·화학적 특성</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[9px]">
|
||
{([
|
||
['용도', s.usage, 'var(--cyan)'],
|
||
['상태', s.state, 'var(--cyan)'],
|
||
['색상', s.color, 'var(--orange)'],
|
||
['냄새', s.odor, 'var(--orange)'],
|
||
['인화점', s.flashPoint, 'var(--red)'],
|
||
['발화점', s.autoIgnition, 'var(--red)'],
|
||
['끓는점', s.boilingPoint, 'var(--purple)'],
|
||
['비중 (물=1)', s.density, 'var(--purple)'],
|
||
['용해도', s.solubility, 'var(--green)'],
|
||
['증기압', s.vaporPressure, 'var(--green)'],
|
||
['증기밀도 (공기=1)', s.vaporDensity, 'var(--yellow)'],
|
||
['폭발범위', s.explosionRange, 'var(--yellow)'],
|
||
] as [string, string, string][]).map(([label, value, borderColor]) => (
|
||
<div key={label} style={{ padding: '6px 8px', background: 'var(--bg0)', borderRadius: 4, borderLeft: `3px solid ${borderColor}` }}>
|
||
<span className="text-text-3">{label}</span><br />
|
||
<b style={{ color: label.includes('인화') || label.includes('폭발') ? 'var(--red)' : label.includes('용해') ? 'var(--cyan)' : 'var(--t1)' }}>{value}</b>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: NFPA + 위험등급 */}
|
||
<div>
|
||
<div className="text-[11px] font-bold text-status-red mb-2">⚠️ 위험등급·농도기준</div>
|
||
<div className="flex gap-[14px] items-start mb-3">
|
||
<div className="relative shrink-0" style={{ width: 80, height: 80 }}>
|
||
<svg viewBox="0 0 100 100" width={80} height={80}>
|
||
<polygon points="50,5 95,50 50,95 5,50" fill="none" stroke="rgba(255,255,255,.15)" strokeWidth={1.5} />
|
||
<polygon points="50,5 72,28 50,50 28,28" fill="rgba(239,68,68,.2)" stroke="rgba(239,68,68,.5)" strokeWidth={1} />
|
||
<text x={50} y={32} textAnchor="middle" fill="#f87171" fontSize={16} fontWeight={800} fontFamily="monospace">{nfpa.health}</text>
|
||
<polygon points="72,28 95,50 72,72 50,50" fill="rgba(234,179,8,.2)" stroke="rgba(234,179,8,.5)" strokeWidth={1} />
|
||
<text x={77} y={55} textAnchor="middle" fill="#fbbf24" fontSize={16} fontWeight={800} fontFamily="monospace">{nfpa.fire}</text>
|
||
<polygon points="50,50 72,72 50,95 28,72" fill="rgba(255,255,255,.08)" stroke="rgba(255,255,255,.2)" strokeWidth={1} />
|
||
<text x={50} y={78} textAnchor="middle" fill="#e2e8f0" fontSize={12} fontWeight={700} fontFamily="sans-serif">{nfpa.special}</text>
|
||
<polygon points="5,50 28,28 50,50 28,72" fill="rgba(59,130,246,.2)" stroke="rgba(59,130,246,.5)" strokeWidth={1} />
|
||
<text x={23} y={55} textAnchor="middle" fill="#60a5fa" fontSize={16} fontWeight={800} fontFamily="monospace">{nfpa.reactivity}</text>
|
||
</svg>
|
||
<div className="text-center text-[7px] font-semibold text-text-3 mt-0.5">NFPA 704</div>
|
||
</div>
|
||
<div className="flex-1 flex flex-col gap-1 text-[8px]">
|
||
<div style={{ padding: '4px 8px', background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.12)', borderRadius: 4 }}><span style={{ color: 'var(--red)', fontWeight: 700 }}>건강(적) {nfpa.health}</span> — {nfpa.health >= 4 ? '치명적' : nfpa.health >= 3 ? '중상' : nfpa.health >= 2 ? '장해' : nfpa.health >= 1 ? '경미한 손상' : '무해'}</div>
|
||
<div style={{ padding: '4px 8px', background: 'rgba(234,179,8,.06)', border: '1px solid rgba(234,179,8,.12)', borderRadius: 4 }}><span style={{ color: '#fbbf24', fontWeight: 700 }}>인화성(황) {nfpa.fire}</span> — {nfpa.fire >= 4 ? '93°F 미만' : nfpa.fire >= 3 ? '100°F 미만' : nfpa.fire >= 2 ? '200°F 미만' : nfpa.fire >= 1 ? '200°F 이상' : '비가연'}</div>
|
||
<div style={{ padding: '4px 8px', background: 'rgba(59,130,246,.06)', border: '1px solid rgba(59,130,246,.12)', borderRadius: 4 }}><span style={{ color: '#60a5fa', fontWeight: 700 }}>반응성(청) {nfpa.reactivity}</span> — {nfpa.reactivity >= 3 ? '폭발 가능' : nfpa.reactivity >= 2 ? '격렬 반응' : nfpa.reactivity >= 1 ? '불안정 가능' : '안정'}</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-col gap-1 text-[9px]">
|
||
<InfoBoxRow label="위험 분류" value={s.hazardClass} bg="rgba(239,68,68,.06)" border="rgba(239,68,68,.12)" labelColor="var(--red)" valueColor="var(--red)" />
|
||
<InfoBoxRow label="ERG 번호" value={s.ergNumber} bg="rgba(249,115,22,.06)" border="rgba(249,115,22,.12)" labelColor="var(--orange)" valueColor="var(--orange)" />
|
||
<InfoBoxRow label="IDLH" value={s.idlh} bg="rgba(168,85,247,.06)" border="rgba(168,85,247,.12)" labelColor="var(--purple)" valueColor="var(--red)" />
|
||
<InfoBoxRow label="AEGL-2 (60분)" value={s.aegl2} bg="rgba(34,197,94,.06)" border="rgba(34,197,94,.12)" labelColor="var(--green)" valueColor="var(--orange)" />
|
||
<InfoBoxRow label="ERPG-2" value={s.erpg2} bg="rgba(6,182,212,.06)" border="rgba(6,182,212,.12)" labelColor="var(--cyan)" valueColor="var(--orange)" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 1: 방제거리·PPE·MSDS */}
|
||
{activeTab === 1 && (
|
||
<div className="p-4">
|
||
<div className="grid grid-cols-2 gap-[14px]">
|
||
{/* 방제거리 */}
|
||
<div className="rounded-md overflow-hidden" style={{ border: '1px solid rgba(239,68,68,.2)' }}>
|
||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(239,68,68,.08),transparent)', borderBottom: '1px solid rgba(239,68,68,.12)' }}>
|
||
<div className="text-xs font-bold text-status-red">🚧 방제거리 (ERG {s.ergNumber})</div>
|
||
</div>
|
||
<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 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 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 leading-[1.6]">{s.marineResponse}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
{/* PPE */}
|
||
<div className="rounded-md overflow-hidden mb-3" style={{ border: '1px solid rgba(34,197,94,.2)' }}>
|
||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(34,197,94,.08),transparent)', borderBottom: '1px solid rgba(34,197,94,.12)' }}>
|
||
<div className="text-xs font-bold text-status-green">🛡 개인보호장구 (PPE) 추천</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1.5 text-[9px] p-3">
|
||
<div className="text-center rounded" style={{ padding: 8, background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.1)' }}>
|
||
<div className="text-base mb-[3px]">🧑🚒</div>
|
||
<div className="font-bold text-status-red">근거리</div>
|
||
<div className="text-[8px] text-text-3 mt-0.5">{s.ppeClose}</div>
|
||
</div>
|
||
<div className="text-center rounded" style={{ padding: 8, background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.1)' }}>
|
||
<div className="text-base mb-[3px]">🦺</div>
|
||
<div className="font-bold text-status-orange">원거리</div>
|
||
<div className="text-[8px] text-text-3 mt-0.5">{s.ppeFar}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* MSDS */}
|
||
<div className="rounded-md overflow-hidden" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
|
||
<div className="flex items-center justify-between" style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(59,130,246,.08),transparent)', borderBottom: '1px solid rgba(59,130,246,.12)' }}>
|
||
<div className="text-xs font-bold" style={{ color: '#60a5fa' }}>📄 MSDS 주요 정보</div>
|
||
<button className="text-[8px] font-semibold cursor-pointer rounded" style={{ padding: '3px 10px', background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)', color: '#60a5fa' }}>📥 전문 다운로드</button>
|
||
</div>
|
||
<div className="text-[8px] text-text-2 leading-[1.7] p-2.5">
|
||
<b>§2 유해성·위험성:</b> {s.msds.hazard}<br />
|
||
<b>§4 응급조치:</b> {s.msds.firstAid}<br />
|
||
<b>§5 소화방법:</b> {s.msds.fireFighting}<br />
|
||
<b>§6 누출대응:</b> {s.msds.spillResponse}<br />
|
||
<b>§8 노출방지:</b> {s.msds.exposure}<br />
|
||
<b>§15 법적규제:</b> {s.msds.regulation}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 2: IBC CODE·EmS 대응 */}
|
||
{activeTab === 2 && (
|
||
<div className="p-4">
|
||
<div className="grid grid-cols-2 gap-[14px]">
|
||
{/* IBC CODE */}
|
||
<div className="rounded-md overflow-hidden" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
|
||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(249,115,22,.08),transparent)', borderBottom: '1px solid rgba(249,115,22,.12)' }}>
|
||
<div className="text-xs font-bold text-status-orange">⚓ IBC CODE 기반 주요 내용</div>
|
||
</div>
|
||
<div className="p-3 flex flex-col gap-2 text-[9px]">
|
||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 4 }}>
|
||
{([
|
||
['위험성', s.ibcHazard],
|
||
['선박형식', s.ibcShipType],
|
||
['탱크형식', s.ibcTankType],
|
||
['탐지장비', s.ibcDetection],
|
||
['소화설비', s.ibcFireFighting],
|
||
['최소적재요건', s.ibcMinRequirement],
|
||
] as [string, string][]).map(([label, value]) => (
|
||
<React.Fragment key={label}>
|
||
<div className="rounded text-text-3" style={{ padding: '6px 8px', background: 'var(--bg0)' }}>{label}</div>
|
||
<div className="rounded font-semibold" style={{ padding: '6px 8px', background: 'var(--bg0)' }}>{value}</div>
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
{/* Tank diagram SVG */}
|
||
<div className="rounded-sm text-center" style={{ padding: 10, background: 'var(--bg0)' }}>
|
||
<svg viewBox="0 0 200 80" width={200} height={80} style={{ display: 'inline-block' }}>
|
||
<rect x={10} y={10} width={180} height={50} rx={4} fill="none" stroke="rgba(249,115,22,.4)" strokeWidth={1.5} />
|
||
<line x1={70} y1={10} x2={70} y2={60} stroke="rgba(249,115,22,.2)" strokeWidth={1} strokeDasharray="3" />
|
||
<line x1={130} y1={10} x2={130} y2={60} stroke="rgba(249,115,22,.2)" strokeWidth={1} strokeDasharray="3" />
|
||
<text x={40} y={38} textAnchor="middle" fill="var(--orange)" fontSize={7} fontFamily="var(--fK)">CARGO</text>
|
||
<text x={40} y={48} textAnchor="middle" fill="var(--t3)" fontSize={6} fontFamily="var(--fM)">Tank 1</text>
|
||
<text x={100} y={38} textAnchor="middle" fill="var(--orange)" fontSize={7} fontFamily="var(--fK)">CARGO</text>
|
||
<text x={100} y={48} textAnchor="middle" fill="var(--t3)" fontSize={6} fontFamily="var(--fM)">Tank 2</text>
|
||
<text x={160} y={38} textAnchor="middle" fill="var(--orange)" fontSize={7} fontFamily="var(--fK)">CARGO</text>
|
||
<text x={160} y={48} textAnchor="middle" fill="var(--t3)" fontSize={6} fontFamily="var(--fM)">Tank 3</text>
|
||
<text x={100} y={75} textAnchor="middle" fill="var(--t3)" fontSize={7} fontFamily="var(--fK)">{s.ibcShipType} — {s.ibcTankType}</text>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* EmS */}
|
||
<div className="rounded-md overflow-hidden" style={{ border: '1px solid rgba(239,68,68,.2)' }}>
|
||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(239,68,68,.08),transparent)', borderBottom: '1px solid rgba(239,68,68,.12)' }}>
|
||
<div className="text-xs font-bold text-status-red">🆘 비상대응핸드북 (EmS) — ERG {s.ergNumber}</div>
|
||
</div>
|
||
<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 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 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 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 3: 화물적부도·항구별 코드 */}
|
||
{activeTab === 3 && (
|
||
<div className="p-4">
|
||
<div className="grid grid-cols-2 gap-[14px]">
|
||
{/* 화물적부도 */}
|
||
<div className="rounded-lg overflow-hidden" style={{ border: '1px solid rgba(168,85,247,.2)' }}>
|
||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(168,85,247,.08),transparent)', borderBottom: '1px solid rgba(168,85,247,.12)' }}>
|
||
<div className="text-[12px] font-bold text-primary-purple">📋 화물적부도 화물코드</div>
|
||
<div className="text-[8px] text-text-3">클릭 시 물질검색창으로 이동</div>
|
||
</div>
|
||
<div className="p-3">
|
||
<table className="w-full border-collapse text-[9px]">
|
||
<thead><tr style={{ background: 'rgba(168,85,247,.05)' }}>
|
||
<th className="text-primary-purple text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>화물코드</th>
|
||
<th className="text-text-2 text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>약자/제품명</th>
|
||
<th className="text-text-2 text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>국적/회사</th>
|
||
<th className="text-text-2 text-center" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>출처</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{s.cargoCodes.map((c, i) => {
|
||
const srcColor = c.source === '적부도' ? 'var(--orange)' : c.source === '용선자' ? 'var(--cyan)' : 'var(--green)'
|
||
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 py-[5px] px-2">{c.code}</td>
|
||
<td className="py-[5px] px-2">{c.name}</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>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 항구별 코드 */}
|
||
<div className="rounded-lg overflow-hidden" style={{ border: '1px solid rgba(6,182,212,.2)' }}>
|
||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(6,182,212,.08),transparent)', borderBottom: '1px solid rgba(6,182,212,.12)' }}>
|
||
<div className="text-[12px] font-bold text-primary-cyan">🏗 항구별 코드</div>
|
||
<div className="text-[8px] text-text-3">Port-MIS 위험물반입신고현황 연동</div>
|
||
</div>
|
||
<div className="p-3">
|
||
<table className="w-full border-collapse text-[9px]">
|
||
<thead><tr style={{ background: 'rgba(6,182,212,.05)' }}>
|
||
<th className="text-primary-cyan text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>항구</th>
|
||
<th className="text-text-2 text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>청코드</th>
|
||
<th className="text-text-2 text-center" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>최근 반입</th>
|
||
<th className="text-text-2 text-center" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>빈도</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{s.portFrequency.map((p, i) => {
|
||
const freqColor = p.frequency === '높음' ? 'var(--red)' : p.frequency === '중간' ? 'var(--orange)' : 'var(--green)'
|
||
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 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>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
<div className="mt-2.5 text-center">
|
||
<button className="text-primary-cyan text-[10px] font-semibold cursor-pointer rounded-sm" style={{ padding: '6px 16px', background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.2)' }}>🔗 Port-MIS 화물적부도 조회</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function InfoBoxRow({ label, value, bg, border, labelColor, valueColor }: { label: string; value: string; bg: string; border: string; labelColor: string; valueColor: string }) {
|
||
return (
|
||
<div className="flex items-center justify-between rounded" style={{ padding: '6px 8px', background: bg, border: `1px solid ${border}` }}>
|
||
<span><b style={{ color: labelColor }}>{label}</b></span>
|
||
<span className="font-mono font-bold" style={{ color: valueColor }}>{value}</span>
|
||
</div>
|
||
)
|
||
}
|