2859 lines
117 KiB
TypeScript
Executable File
2859 lines
117 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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
{
|
||
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: '#06b6d4',
|
||
},
|
||
];
|
||
|
||
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(--bg-base); color: var(--fg-default); 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-base">
|
||
<div
|
||
ref={contentRef}
|
||
className="flex-1 overflow-y-auto p-5"
|
||
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) 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: 'rgba(6,182,212,.08)',
|
||
border: '1px solid rgba(6,182,212,.3)',
|
||
}}
|
||
>
|
||
🧬
|
||
</div>
|
||
<div>
|
||
<div className="text-base font-bold">HNS 물질정보 데이터베이스</div>
|
||
<div className="text-label-2 text-fg-disabled 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-stroke text-label-2 outline-none bg-bg-card px-3 py-1.5 w-[200px]"
|
||
/>
|
||
<button
|
||
onClick={handleExportPDF}
|
||
className="rounded-sm text-label-2 font-semibold cursor-pointer text-color-accent px-3.5 py-1.5"
|
||
style={{
|
||
border: '1px solid rgba(6,182,212,.3)',
|
||
background: 'rgba(6,182,212,.08)',
|
||
}}
|
||
>
|
||
📥 DB 다운로드
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 서브탭 */}
|
||
<div
|
||
className="flex gap-0.5 rounded-lg p-1 mb-5 bg-bg-card border border-stroke"
|
||
data-html2pdf-ignore
|
||
>
|
||
{tabLabels.map((tab, idx) => (
|
||
<button
|
||
key={idx}
|
||
onClick={() => setActiveTab(idx)}
|
||
className={`flex-1 py-2 px-1 text-label-1 font-medium rounded-md transition-all duration-150 cursor-pointer border ${
|
||
activeTab === idx
|
||
? 'border-stroke-light bg-bg-elevated text-fg'
|
||
: 'border-transparent bg-bg-card text-fg-disabled hover:text-fg-sub'
|
||
}`}
|
||
>
|
||
{tab.icon} {tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* ═══ MAT PANEL 0: SEBC 거동분류 ═══ */}
|
||
{activeTab === 0 && (
|
||
<div>
|
||
<div
|
||
className="rounded-[12px] p-4 mb-4"
|
||
style={{
|
||
background: 'rgba(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.2)',
|
||
}}
|
||
>
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className="text-title-4 font-bold text-color-accent">
|
||
SEBC 해양 거동 분류 체계
|
||
</span>
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent py-[2px] px-2 rounded-md"
|
||
style={{ background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
Standard European Behaviour Classification
|
||
</span>
|
||
</div>
|
||
<div className="text-label-2 text-fg-sub leading-[1.7] mb-[14px]">
|
||
HNS 물질이 해양에 유출되었을 때의{' '}
|
||
<b className="text-color-accent">물리·화학적 거동</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(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.2)',
|
||
}}
|
||
>
|
||
<div
|
||
className="flex items-center justify-center text-title-1 mx-auto mb-2"
|
||
style={{
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: '50%',
|
||
background: 'rgba(6,182,212,.15)',
|
||
}}
|
||
>
|
||
💨
|
||
</div>
|
||
<div className="text-title-4 font-mono font-extrabold text-color-accent">G</div>
|
||
<div className="text-label-2 font-bold my-1">Gas</div>
|
||
<div className="text-[8px] text-fg-sub leading-normal">
|
||
기체 상태로 대기 중 확산. 증기압이 높아 빠르게 증발
|
||
</div>
|
||
<div
|
||
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
|
||
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
|
||
>
|
||
대기확산 모델 적용
|
||
</div>
|
||
</div>
|
||
{/* E: Evaporator */}
|
||
<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-title-1 mx-auto mb-2"
|
||
style={{
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: '50%',
|
||
background: 'rgba(6,182,212,.15)',
|
||
}}
|
||
>
|
||
🔥
|
||
</div>
|
||
<div className="text-title-4 font-mono font-extrabold text-color-accent">E</div>
|
||
<div className="text-label-2 font-bold my-1">Evaporator</div>
|
||
<div className="text-[8px] text-fg-sub leading-normal">
|
||
해수면에서 증발. 부유 후 기화하여 독성 가스 생성
|
||
</div>
|
||
<div
|
||
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
|
||
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
|
||
>
|
||
대기+해양 복합 대응
|
||
</div>
|
||
</div>
|
||
{/* F: Floater */}
|
||
<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-title-1 mx-auto mb-2"
|
||
style={{
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: '50%',
|
||
background: 'rgba(6,182,212,.15)',
|
||
}}
|
||
>
|
||
🟡
|
||
</div>
|
||
<div className="text-title-4 font-mono font-extrabold text-color-accent">F</div>
|
||
<div className="text-label-2 font-bold my-1">Floater</div>
|
||
<div className="text-[8px] text-fg-sub leading-normal">
|
||
{'해수면 위에 부유. 비중 < 1.0, 불용성 물질'}
|
||
</div>
|
||
<div
|
||
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
|
||
style={{ background: 'rgba(6,182,212,.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-title-1 mx-auto mb-2"
|
||
style={{
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: '50%',
|
||
background: 'rgba(6,182,212,.15)',
|
||
}}
|
||
>
|
||
💧
|
||
</div>
|
||
<div className="text-title-4 font-mono font-extrabold text-color-accent">D</div>
|
||
<div className="text-label-2 font-bold my-1">Dissolver</div>
|
||
<div className="text-[8px] text-fg-sub leading-normal">
|
||
해수에 용해. 수중 확산하여 넓은 범위 오염
|
||
</div>
|
||
<div
|
||
className="mt-1.5 text-[7px] font-semibold text-color-accent 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(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.2)',
|
||
}}
|
||
>
|
||
<div
|
||
className="flex items-center justify-center text-title-1 mx-auto mb-2"
|
||
style={{
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: '50%',
|
||
background: 'rgba(6,182,212,.15)',
|
||
}}
|
||
>
|
||
⬇
|
||
</div>
|
||
<div className="text-title-4 font-mono font-extrabold text-color-accent">S</div>
|
||
<div className="text-label-2 font-bold my-1">Sinker</div>
|
||
<div className="text-[8px] text-fg-sub leading-normal">
|
||
{'해저로 침강. 비중 > 1.0, 저층 오염 축적'}
|
||
</div>
|
||
<div
|
||
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
|
||
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
|
||
>
|
||
저층 3D 모니터링 필수
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* 복합 거동 */}
|
||
<div className="rounded-md p-3 border border-stroke bg-bg-card">
|
||
<div className="text-label-2 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-base">
|
||
<span className="font-bold text-color-accent">GD</span>
|
||
<br />
|
||
<span className="text-fg-disabled">기체+용해</span>
|
||
</div>
|
||
<div className="rounded p-1.5 bg-bg-base">
|
||
<span className="font-bold text-color-accent">ED</span>
|
||
<br />
|
||
<span className="text-fg-disabled">증발+용해</span>
|
||
</div>
|
||
<div className="rounded p-1.5 bg-bg-base">
|
||
<span className="font-bold text-color-accent">FE</span>
|
||
<br />
|
||
<span className="text-fg-disabled">부유+증발</span>
|
||
</div>
|
||
<div className="rounded p-1.5 bg-bg-base">
|
||
<span className="font-bold text-color-accent">FED</span>
|
||
<br />
|
||
<span className="text-fg-disabled">부유+증발+용해</span>
|
||
</div>
|
||
<div className="rounded p-1.5 bg-bg-base">
|
||
<span className="font-bold text-color-accent">SD</span>
|
||
<br />
|
||
<span className="text-fg-disabled">침강+용해</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-label-2 font-semibold cursor-pointer"
|
||
style={{
|
||
padding: '6px 12px',
|
||
border:
|
||
selectedCategory === cat.id
|
||
? '1px solid var(--color-accent)'
|
||
: '1px solid var(--stroke-default)',
|
||
color:
|
||
selectedCategory === cat.id ? 'var(--color-accent)' : 'var(--fg-disabled)',
|
||
background:
|
||
selectedCategory === cat.id ? 'rgba(6,182,212,.08)' : 'var(--bg-card)',
|
||
}}
|
||
>
|
||
{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-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div>
|
||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||
NH₃
|
||
</span>{' '}
|
||
<span className="text-label-1 font-bold">암모니아</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
G/GD
|
||
</span>
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
독성
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">CAS:</span>{' '}
|
||
<span className="font-mono">7664-41-7</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">분자량:</span>{' '}
|
||
<span className="font-mono">17.03</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">끓는점:</span>{' '}
|
||
<span className="font-mono text-color-accent">-33.4°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">비중:</span>{' '}
|
||
<span className="font-mono">0.73</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">인화점:</span>{' '}
|
||
<span className="font-mono">N/A (불연)</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">용해도:</span>{' '}
|
||
<span className="font-mono text-color-accent">매우 높음</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="grid text-center text-[7px]"
|
||
style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: 4,
|
||
background: 'rgba(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 3,
|
||
}}
|
||
>
|
||
<span className="text-color-accent">AEGL-2</span>
|
||
<br />
|
||
<b>160 ppm</b>
|
||
</div>
|
||
<div
|
||
style={{
|
||
padding: 4,
|
||
background: 'rgba(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 3,
|
||
}}
|
||
>
|
||
<span className="text-color-accent">ERPG-2</span>
|
||
<br />
|
||
<b>150 ppm</b>
|
||
</div>
|
||
<div
|
||
style={{
|
||
padding: 4,
|
||
background: 'rgba(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 3,
|
||
}}
|
||
>
|
||
<span className="text-color-accent">IDLH</span>
|
||
<br />
|
||
<b>300 ppm</b>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
|
||
해상 유출 시 급속 기화 → 독성 가스운 형성. 물에 잘 용해되어 수중 독성도 높음.
|
||
해풍 환경에서 확산 범위 확대.
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 메탄올 */}
|
||
{filtered.find((s) => s.casNumber === '67-56-1') && (
|
||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div>
|
||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||
CH₃OH
|
||
</span>{' '}
|
||
<span className="text-label-1 font-bold">메탄올</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
ED
|
||
</span>
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
인화성
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">CAS:</span>{' '}
|
||
<span className="font-mono">67-56-1</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">분자량:</span>{' '}
|
||
<span className="font-mono">32.04</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">끓는점:</span>{' '}
|
||
<span className="font-mono text-color-accent">64.7°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">비중:</span>{' '}
|
||
<span className="font-mono">0.79</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">인화점:</span>{' '}
|
||
<span className="font-mono text-color-accent">11°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">용해도:</span>{' '}
|
||
<span className="font-mono text-color-accent">완전 혼화</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="grid text-center text-[7px]"
|
||
style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: 4,
|
||
background: 'rgba(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 3,
|
||
}}
|
||
>
|
||
<span className="text-color-accent">AEGL-2</span>
|
||
<br />
|
||
<b>2,100 ppm</b>
|
||
</div>
|
||
<div
|
||
style={{
|
||
padding: 4,
|
||
background: 'rgba(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 3,
|
||
}}
|
||
>
|
||
<span className="text-color-accent">ERPG-2</span>
|
||
<br />
|
||
<b>1,000 ppm</b>
|
||
</div>
|
||
<div
|
||
style={{
|
||
padding: 4,
|
||
background: 'rgba(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 3,
|
||
}}
|
||
>
|
||
<span className="text-color-accent">IDLH</span>
|
||
<br />
|
||
<b>6,000 ppm</b>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
|
||
해수에 완전 용해 → 수질 오염 장기화. 인화점 낮아 화재 위험. 증발 시 독성 증기
|
||
발생. 2007 온산항 FODDANGER호 95만L 유출 사고.
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 수소 */}
|
||
{filtered.find((s) => s.casNumber === '1333-74-0') && (
|
||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div>
|
||
<span className="text-sm font-mono font-extrabold text-color-accent">H₂</span>{' '}
|
||
<span className="text-label-1 font-bold">수소</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
G
|
||
</span>
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
폭발
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">CAS:</span>{' '}
|
||
<span className="font-mono">1333-74-0</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">분자량:</span>{' '}
|
||
<span className="font-mono">2.016</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">끓는점:</span>{' '}
|
||
<span className="font-mono text-color-accent">-252.9°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">비중(기체):</span>{' '}
|
||
<span className="font-mono">0.07</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">LFL:</span>{' '}
|
||
<span className="font-mono text-color-accent">4.0%</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">UFL:</span>{' '}
|
||
<span className="font-mono text-color-accent">75.0%</span>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-fg-disabled 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-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div>
|
||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||
CH₄
|
||
</span>{' '}
|
||
<span className="text-label-1 font-bold">LNG (메탄)</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
G
|
||
</span>
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
인화/폭발
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">CAS:</span>{' '}
|
||
<span className="font-mono">74-82-8</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">분자량:</span>{' '}
|
||
<span className="font-mono">16.04</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">끓는점:</span>{' '}
|
||
<span className="font-mono text-color-accent">-161.5°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">비중(액체):</span>{' '}
|
||
<span className="font-mono">0.42</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">LFL:</span>{' '}
|
||
<span className="font-mono text-color-accent">5.0%</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">UFL:</span>{' '}
|
||
<span className="font-mono text-color-accent">15.0%</span>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-fg-disabled 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-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div>
|
||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||
C₆H₅OH
|
||
</span>{' '}
|
||
<span className="text-label-1 font-bold">페놀</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
S/SD
|
||
</span>
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
독성
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">CAS:</span>{' '}
|
||
<span className="font-mono">108-95-2</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">분자량:</span>{' '}
|
||
<span className="font-mono">94.11</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">끓는점:</span>{' '}
|
||
<span className="font-mono">181.7°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">비중:</span>{' '}
|
||
<span className="font-mono text-color-accent">1.07 (침강)</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">인화점:</span>{' '}
|
||
<span className="font-mono text-color-accent">79°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">용해도:</span>{' '}
|
||
<span className="font-mono text-color-accent">84 g/L</span>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
|
||
비중 1.07 → <b className="text-color-accent">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-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div>
|
||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||
C₇H₈
|
||
</span>{' '}
|
||
<span className="text-label-1 font-bold">톨루엔</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
FE
|
||
</span>
|
||
<span
|
||
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
|
||
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
|
||
>
|
||
인화성
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">CAS:</span>{' '}
|
||
<span className="font-mono">108-88-3</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">분자량:</span>{' '}
|
||
<span className="font-mono">92.14</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">끓는점:</span>{' '}
|
||
<span className="font-mono">110.6°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">비중:</span>{' '}
|
||
<span className="font-mono text-color-accent">0.87 (부유)</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">인화점:</span>{' '}
|
||
<span className="font-mono text-color-accent">4°C</span>
|
||
</div>
|
||
<div className="rounded bg-bg-base px-1.5 py-1">
|
||
<span className="text-fg-disabled">용해도:</span>{' '}
|
||
<span className="font-mono">0.52 g/L</span>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1.5 text-[8px] text-fg-disabled 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-stroke bg-bg-card">
|
||
<div className="text-title-4 font-bold text-color-accent mb-[10px]">
|
||
🟢 AEGL (Acute Exposure Guideline Level)
|
||
</div>
|
||
<div className="text-caption text-fg-disabled 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(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-2 font-bold text-color-accent">AEGL-1 (불쾌감)</div>
|
||
<div className="text-caption text-fg-sub leading-normal">
|
||
눈에 띄는 불쾌감, 자극 또는 비감각적 증상. 노출 중단 시 증상 소멸.
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="rounded-sm px-2.5 py-2"
|
||
style={{
|
||
background: 'rgba(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-2 font-bold text-color-accent">
|
||
AEGL-2 (비가역적 영향)
|
||
</div>
|
||
<div className="text-caption text-fg-sub leading-normal">
|
||
비가역적·장기적 건강 영향 또는 대피 능력 저하.{' '}
|
||
<b className="text-color-accent">대피 기준으로 사용</b>.
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="rounded-sm px-2.5 py-2"
|
||
style={{
|
||
background: 'rgba(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-2 font-bold text-color-accent">
|
||
AEGL-3 (생명 위협)
|
||
</div>
|
||
<div className="text-caption text-fg-sub leading-normal">
|
||
생명을 위협하는 건강 영향 또는 사망.{' '}
|
||
<b className="text-color-accent">즉시 대피·격리 구역 설정</b>.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ERPG / IDLH */}
|
||
<div className="rounded-[10px] p-4 border border-stroke bg-bg-card">
|
||
<div className="text-title-4 font-bold text-color-accent mb-[10px]">
|
||
🔴 ERPG & IDLH
|
||
</div>
|
||
<div className="flex flex-col gap-2">
|
||
<div>
|
||
<div className="text-label-2 font-bold text-color-accent mb-1">
|
||
ERPG (Emergency Response Planning Guideline)
|
||
</div>
|
||
<div className="text-caption text-fg-disabled 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(6,182,212,.03)',
|
||
border: '1px solid rgba(6,182,212,.1)',
|
||
}}
|
||
>
|
||
<b className="text-color-accent">ERPG-1</b> — 일시적 건강 영향, 냄새 감지
|
||
</div>
|
||
<div
|
||
className="text-[8px] rounded px-2 py-1.5"
|
||
style={{
|
||
background: 'rgba(6,182,212,.03)',
|
||
border: '1px solid rgba(6,182,212,.1)',
|
||
}}
|
||
>
|
||
<b className="text-color-accent">ERPG-2</b> — 비가역적 영향, 대피 판단 기준
|
||
</div>
|
||
<div
|
||
className="text-[8px] rounded px-2 py-1.5"
|
||
style={{
|
||
background: 'rgba(6,182,212,.03)',
|
||
border: '1px solid rgba(6,182,212,.1)',
|
||
}}
|
||
>
|
||
<b className="text-color-accent">ERPG-3</b> — 생명 위협 농도
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="mt-1 rounded-sm p-2.5"
|
||
style={{
|
||
background: 'rgba(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.15)',
|
||
}}
|
||
>
|
||
<div className="text-label-2 font-bold text-color-accent mb-1">
|
||
IDLH (Immediately Dangerous to Life or Health)
|
||
</div>
|
||
<div className="text-caption text-fg-disabled mb-1">
|
||
NIOSH — 30분 노출 시 생명·건강에 즉각적 위험
|
||
</div>
|
||
<div className="text-caption text-fg-sub leading-normal">
|
||
<b className="text-color-accent">최대 허용 노출 한계</b>로서 이 농도를
|
||
초과하면 자급식 호흡장치(SCBA) 없이는 진입 불가. WING 시스템에서 위험구역 자동
|
||
설정의 기준값으로 사용.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 물질별 위험도 비교표 */}
|
||
<div className="rounded-[10px] p-4 border border-stroke bg-bg-card">
|
||
<div className="text-label-1 font-bold mb-3">
|
||
📊 주요 HNS 물질별 위험도 기준 비교 (ppm)
|
||
</div>
|
||
<table className="w-full border-collapse text-caption">
|
||
<thead>
|
||
<tr style={{ background: 'rgba(6,182,212,.06)' }}>
|
||
<th
|
||
className="text-color-accent text-left"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
물질
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-center"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
AEGL-1
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-center"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
AEGL-2
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-center"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
AEGL-3
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-center"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
ERPG-2
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-center"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
IDLH
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-center"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
LFL(%)
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-center"
|
||
style={{ padding: '7px 8px', borderBottom: '2px solid var(--stroke-light)' }}
|
||
>
|
||
SEBC
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||
<td className="font-semibold text-color-accent py-1.5 px-2">NH₃ 암모니아</td>
|
||
<td className="text-center font-mono text-color-accent">30</td>
|
||
<td className="text-center font-mono text-color-accent">160</td>
|
||
<td className="text-center font-mono text-color-accent">1,100</td>
|
||
<td className="text-center font-mono">150</td>
|
||
<td className="text-center font-mono text-color-accent">300</td>
|
||
<td className="text-center font-mono">15.0</td>
|
||
<td className="text-center font-semibold text-color-accent">G/GD</td>
|
||
</tr>
|
||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||
<td className="font-semibold text-color-accent py-1.5 px-2">CH₃OH 메탄올</td>
|
||
<td className="text-center font-mono text-color-accent">530</td>
|
||
<td className="text-center font-mono text-color-accent">2,100</td>
|
||
<td className="text-center font-mono text-color-accent">14,000</td>
|
||
<td className="text-center font-mono">1,000</td>
|
||
<td className="text-center font-mono text-color-accent">6,000</td>
|
||
<td className="text-center font-mono">6.0</td>
|
||
<td className="text-center font-semibold text-color-accent">ED</td>
|
||
</tr>
|
||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||
<td className="font-semibold text-color-accent py-1.5 px-2">H₂ 수소</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center font-mono font-bold text-color-accent">4.0</td>
|
||
<td className="text-center font-semibold text-color-accent">G</td>
|
||
</tr>
|
||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||
<td className="font-semibold text-color-accent py-1.5 px-2">CH₄ LNG</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center text-fg-disabled">-</td>
|
||
<td className="text-center font-mono text-color-accent">5.0</td>
|
||
<td className="text-center font-semibold text-color-accent">G</td>
|
||
</tr>
|
||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||
<td className="font-semibold text-color-accent py-1.5 px-2">C₆H₅OH 페놀</td>
|
||
<td className="text-center font-mono text-color-accent">19</td>
|
||
<td className="text-center font-mono text-color-accent">29</td>
|
||
<td className="text-center font-mono text-color-accent">57</td>
|
||
<td className="text-center font-mono">50</td>
|
||
<td className="text-center font-mono text-color-accent">250</td>
|
||
<td className="text-center font-mono">1.8</td>
|
||
<td className="text-center font-semibold text-color-accent">S/SD</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="font-semibold text-color-accent py-1.5 px-2">C₇H₈ 톨루엔</td>
|
||
<td className="text-center font-mono text-color-accent">67</td>
|
||
<td className="text-center font-mono text-color-accent">560</td>
|
||
<td className="text-center font-mono text-color-accent">3,700</td>
|
||
<td className="text-center font-mono">300</td>
|
||
<td className="text-center font-mono text-color-accent">500</td>
|
||
<td className="text-center font-mono">1.1</td>
|
||
<td className="text-center font-semibold text-color-accent">FE</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div className="mt-2 text-[7px] text-fg-disabled">
|
||
※ 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-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[14px]">
|
||
<div className="text-title-4 font-bold">
|
||
🔍 HNS 물질 통합 검색{' '}
|
||
<span className="text-caption font-normal text-fg-disabled">
|
||
(화물약어·제품명·동의어·CAS·UN번호 통합)
|
||
</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<button
|
||
className="text-caption font-semibold cursor-pointer text-color-accent rounded px-2.5 py-[5px]"
|
||
style={{
|
||
border: '1px solid rgba(6,182,212,.25)',
|
||
background: 'rgba(6,182,212,.08)',
|
||
}}
|
||
>
|
||
📥 DB 다운로드
|
||
</button>
|
||
<button
|
||
className="text-caption font-semibold cursor-pointer text-color-accent 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-caption font-semibold text-fg-disabled">구분:</span>
|
||
<select
|
||
value={hmsSearchType}
|
||
onChange={(e) => {
|
||
setHmsSearchType(e.target.value as typeof hmsSearchType);
|
||
setHmsPage(1);
|
||
}}
|
||
className="rounded-sm border border-stroke text-label-1 outline-none bg-bg-base 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-stroke text-title-4 outline-none bg-bg-base px-3 py-2"
|
||
/>
|
||
<button
|
||
onClick={() => setHmsPage(1)}
|
||
className="text-title-4 font-bold cursor-pointer shrink-0 rounded-sm text-color-accent px-5 py-2"
|
||
style={{
|
||
border: '1px solid rgba(6,182,212,.3)',
|
||
background: 'rgba(6,182,212,.08)',
|
||
}}
|
||
>
|
||
🔎 검색
|
||
</button>
|
||
</div>
|
||
<div className="text-[8px] text-fg-disabled leading-[1.6]">
|
||
※ 국문명·영문명 검색 시 <b className="text-color-accent">동의어까지 검색</b>{' '}
|
||
| 약자/제품명 검색 시{' '}
|
||
<b className="text-color-accent">부호, 띄어쓰기 제외</b> 후 검색 | 총{' '}
|
||
<b className="text-color-accent">1,222종</b> 등록
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 검색 결과 테이블 ── */}
|
||
<div className="rounded-[10px] p-4 mb-4 border border-stroke bg-bg-card">
|
||
<div className="flex items-center justify-between mb-[10px]">
|
||
<div className="text-title-4 font-bold">
|
||
📋 검색 결과{' '}
|
||
<span className="text-label-2 font-normal text-fg-disabled">
|
||
— {hmsTotal}건 조회
|
||
</span>
|
||
</div>
|
||
<select
|
||
value={hmsFilterSebc}
|
||
onChange={(e) => {
|
||
setHmsFilterSebc(e.target.value);
|
||
setHmsPage(1);
|
||
}}
|
||
className="rounded border border-stroke text-label-2 text-fg-sub outline-none bg-bg-base 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-label-2">
|
||
<thead>
|
||
<tr style={{ background: 'rgba(6,182,212,.06)' }}>
|
||
<th
|
||
className="text-color-accent text-left"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
width: 36,
|
||
}}
|
||
>
|
||
No.
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-left"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
약자/제품명
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-left"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
영문명
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-left"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
영문명 동의어
|
||
</th>
|
||
<th
|
||
className="text-color-accent text-left"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
국문명
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-left text-label-2"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
국문 동의어 / 주요 사용처
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-center"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
width: 68,
|
||
}}
|
||
>
|
||
UN번호
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-center"
|
||
style={{
|
||
padding: '8px 8px',
|
||
borderBottom: '2px solid var(--stroke-light)',
|
||
width: 80,
|
||
}}
|
||
>
|
||
CAS번호
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{hmsLoading ? (
|
||
<tr>
|
||
<td colSpan={8} className="text-center text-fg-disabled 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(6,182,212,.03)';
|
||
}}
|
||
onMouseOut={(e) => {
|
||
if (!isSel) e.currentTarget.style.background = '';
|
||
}}
|
||
>
|
||
<td className="font-mono text-fg-disabled px-2 py-2">
|
||
{(hmsPage - 1) * HMS_PER_PAGE + idx + 1}
|
||
</td>
|
||
<td className="font-semibold font-mono text-color-accent px-2 py-2">
|
||
{s.abbreviation}
|
||
</td>
|
||
<td className="px-2 py-2">{s.nameEn}</td>
|
||
<td className="text-fg-disabled text-label-2 px-2 py-2">
|
||
{s.synonymsEn}
|
||
</td>
|
||
<td className="font-semibold px-2 py-2">
|
||
<span
|
||
className="text-color-accent underline cursor-pointer"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setHmsSelectedId(s.id);
|
||
setHmsDetailTab(0);
|
||
}}
|
||
>
|
||
{s.nameKr}
|
||
</span>
|
||
</td>
|
||
<td className="text-fg-disabled text-label-2 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-fg-disabled py-5 px-2">
|
||
검색 결과가 없습니다.
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div className="mt-[10px] text-center text-label-2 text-fg-disabled">
|
||
<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-stroke text-label-2 text-fg-sub bg-bg-base 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-stroke text-label-2 text-fg-sub bg-bg-base 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-stroke text-label-2 px-3 py-1"
|
||
style={{
|
||
background: p === hmsPage ? 'rgba(6,182,212,.15)' : 'var(--bg-base)',
|
||
color: p === hmsPage ? 'var(--color-accent)' : 'var(--fg-sub)',
|
||
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-stroke text-label-2 text-fg-sub bg-bg-base 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-stroke text-label-2 text-fg-sub bg-bg-base px-2.5 py-1"
|
||
style={{ opacity: hmsPage >= hmsTotalPages ? 0.4 : 1 }}
|
||
>
|
||
▶▶
|
||
</button>
|
||
<span className="ml-2 text-fg-disabled">
|
||
{hmsPage} / {hmsTotalPages} 페이지
|
||
</span>
|
||
</div>
|
||
<span>
|
||
총 <b className="text-color-accent">{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(--color-accent)'
|
||
: s.sebc.startsWith('E')
|
||
? 'var(--color-accent)'
|
||
: s.sebc.startsWith('F')
|
||
? 'var(--color-caution)'
|
||
: s.sebc.startsWith('D')
|
||
? 'var(--color-accent)'
|
||
: s.sebc.startsWith('S')
|
||
? 'var(--color-accent)'
|
||
: 'var(--fg-sub)';
|
||
|
||
return (
|
||
<div
|
||
className="rounded-[10px] overflow-hidden bg-bg-card"
|
||
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-stroke" style={{ background: 'rgba(6,182,212,.05)' }}>
|
||
{tabLabels.map((label, i) => (
|
||
<button
|
||
key={i}
|
||
onClick={() => onTabChange(i)}
|
||
className="flex-1 py-2.5 px-1 text-caption cursor-pointer"
|
||
style={{
|
||
fontWeight: activeTab === i ? 700 : 600,
|
||
border: 'none',
|
||
borderBottom: `2px solid ${activeTab === i ? 'var(--color-accent)' : 'transparent'}`,
|
||
background: activeTab === i ? 'rgba(6,182,212,.06)' : 'transparent',
|
||
color: activeTab === i ? 'var(--color-accent)' : 'var(--fg-disabled)',
|
||
}}
|
||
>
|
||
{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-stroke">
|
||
<div
|
||
className="flex items-center justify-center text-[22px] shrink-0"
|
||
style={{
|
||
width: 52,
|
||
height: 52,
|
||
borderRadius: 10,
|
||
background: 'rgba(6,182,212,.12)',
|
||
border: '1px solid rgba(6,182,212,.25)',
|
||
}}
|
||
>
|
||
🧪
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="text-title-2 font-extrabold">
|
||
{s.nameKr}{' '}
|
||
<span className="text-label-1 font-normal text-fg-disabled">({s.nameEn})</span>
|
||
</div>
|
||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||
<span
|
||
className="text-caption font-semibold font-mono text-color-accent 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-caption font-semibold font-mono text-color-accent rounded py-[2px] px-2"
|
||
style={{
|
||
background: 'rgba(6,182,212,.1)',
|
||
border: '1px solid rgba(6,182,212,.2)',
|
||
}}
|
||
>
|
||
UN: {s.unNumber}
|
||
</span>
|
||
<span
|
||
className="text-caption font-semibold text-color-accent rounded py-[2px] px-2"
|
||
style={{
|
||
background: 'rgba(6,182,212,.1)',
|
||
border: '1px solid rgba(6,182,212,.2)',
|
||
}}
|
||
>
|
||
운송방법: {s.transportMethod}
|
||
</span>
|
||
<span
|
||
className="text-caption font-semibold text-color-accent rounded py-[2px] px-2"
|
||
style={{
|
||
background: 'rgba(6,182,212,.1)',
|
||
border: '1px solid rgba(6,182,212,.2)',
|
||
}}
|
||
>
|
||
SEBC: {s.sebc}
|
||
</span>
|
||
</div>
|
||
<div className="text-caption text-fg-disabled 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-label-2 font-bold text-color-accent mb-2">
|
||
⚗️ 물리·화학적 특성
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1 text-caption">
|
||
{(
|
||
[
|
||
['용도', s.usage, 'var(--color-accent)'],
|
||
['상태', s.state, 'var(--color-accent)'],
|
||
['색상', s.color, 'var(--color-accent)'],
|
||
['냄새', s.odor, 'var(--color-accent)'],
|
||
['인화점', s.flashPoint, 'var(--color-accent)'],
|
||
['발화점', s.autoIgnition, 'var(--color-accent)'],
|
||
['끓는점', s.boilingPoint, 'var(--color-accent)'],
|
||
['비중 (물=1)', s.density, 'var(--color-accent)'],
|
||
['용해도', s.solubility, 'var(--color-accent)'],
|
||
['증기압', s.vaporPressure, 'var(--color-accent)'],
|
||
['증기밀도 (공기=1)', s.vaporDensity, 'var(--color-caution)'],
|
||
['폭발범위', s.explosionRange, 'var(--color-caution)'],
|
||
] as [string, string, string][]
|
||
).map(([label, value]) => (
|
||
<div
|
||
key={label}
|
||
style={{
|
||
padding: '6px 8px',
|
||
background: 'var(--bg-base)',
|
||
borderRadius: 4,
|
||
}}
|
||
>
|
||
<span className="text-fg-disabled">{label}</span>
|
||
<br />
|
||
<b
|
||
style={{
|
||
color:
|
||
label.includes('인화') || label.includes('폭발')
|
||
? 'var(--color-accent)'
|
||
: label.includes('용해')
|
||
? 'var(--color-accent)'
|
||
: 'var(--fg-default)',
|
||
}}
|
||
>
|
||
{value}
|
||
</b>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: NFPA + 위험등급 */}
|
||
<div>
|
||
<div className="text-label-2 font-bold text-color-accent 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(6,182,212,.2)"
|
||
stroke="rgba(6,182,212,.5)"
|
||
strokeWidth={1}
|
||
/>
|
||
<text
|
||
x={50}
|
||
y={32}
|
||
textAnchor="middle"
|
||
fill="var(--color-accent)"
|
||
fontSize={16}
|
||
fontWeight={800}
|
||
fontFamily="var(--font-mono)"
|
||
>
|
||
{nfpa.health}
|
||
</text>
|
||
<polygon
|
||
points="72,28 95,50 72,72 50,50"
|
||
fill="rgba(6,182,212,.2)"
|
||
stroke="rgba(6,182,212,.5)"
|
||
strokeWidth={1}
|
||
/>
|
||
<text
|
||
x={77}
|
||
y={55}
|
||
textAnchor="middle"
|
||
fill="var(--color-accent)"
|
||
fontSize={16}
|
||
fontWeight={800}
|
||
fontFamily="var(--font-mono)"
|
||
>
|
||
{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="var(--fg-sub)"
|
||
fontSize={12}
|
||
fontWeight={700}
|
||
fontFamily="var(--font-korean)"
|
||
>
|
||
{nfpa.special}
|
||
</text>
|
||
<polygon
|
||
points="5,50 28,28 50,50 28,72"
|
||
fill="rgba(6,182,212,.2)"
|
||
stroke="rgba(6,182,212,.5)"
|
||
strokeWidth={1}
|
||
/>
|
||
<text
|
||
x={23}
|
||
y={55}
|
||
textAnchor="middle"
|
||
fill="var(--color-accent)"
|
||
fontSize={16}
|
||
fontWeight={800}
|
||
fontFamily="var(--font-mono)"
|
||
>
|
||
{nfpa.reactivity}
|
||
</text>
|
||
</svg>
|
||
<div className="text-center text-[7px] font-semibold text-fg-disabled 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(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 4,
|
||
}}
|
||
>
|
||
<span style={{ color: 'var(--color-accent)', 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(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 4,
|
||
}}
|
||
>
|
||
<span style={{ color: 'var(--color-accent)', 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(6,182,212,.06)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
borderRadius: 4,
|
||
}}
|
||
>
|
||
<span style={{ color: 'var(--color-accent)', 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-caption">
|
||
<InfoBoxRow
|
||
label="위험 분류"
|
||
value={s.hazardClass}
|
||
bg="rgba(6,182,212,.06)"
|
||
border="rgba(6,182,212,.12)"
|
||
labelColor="var(--color-accent)"
|
||
valueColor="var(--color-accent)"
|
||
/>
|
||
<InfoBoxRow
|
||
label="ERG 번호"
|
||
value={s.ergNumber}
|
||
bg="rgba(6,182,212,.06)"
|
||
border="rgba(6,182,212,.12)"
|
||
labelColor="var(--color-accent)"
|
||
valueColor="var(--color-accent)"
|
||
/>
|
||
<InfoBoxRow
|
||
label="IDLH"
|
||
value={s.idlh}
|
||
bg="rgba(6,182,212,.06)"
|
||
border="rgba(6,182,212,.12)"
|
||
labelColor="var(--color-accent)"
|
||
valueColor="var(--color-accent)"
|
||
/>
|
||
<InfoBoxRow
|
||
label="AEGL-2 (60분)"
|
||
value={s.aegl2}
|
||
bg="rgba(6,182,212,.06)"
|
||
border="rgba(6,182,212,.12)"
|
||
labelColor="var(--color-accent)"
|
||
valueColor="var(--color-accent)"
|
||
/>
|
||
<InfoBoxRow
|
||
label="ERPG-2"
|
||
value={s.erpg2}
|
||
bg="rgba(6,182,212,.06)"
|
||
border="rgba(6,182,212,.12)"
|
||
labelColor="var(--color-accent)"
|
||
valueColor="var(--color-accent)"
|
||
/>
|
||
</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(6,182,212,.2)' }}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: '10px 12px',
|
||
background: 'rgba(6,182,212,.06)',
|
||
borderBottom: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-1 font-bold text-color-accent">
|
||
🚧 방제거리 (ERG {s.ergNumber})
|
||
</div>
|
||
</div>
|
||
<div className="p-3 flex flex-col gap-2">
|
||
<div
|
||
className="rounded-sm"
|
||
style={{
|
||
padding: 10,
|
||
background: 'rgba(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-2 font-bold text-color-accent mb-1">🔥 화재 시</div>
|
||
<div className="text-caption text-fg-sub leading-[1.6]">
|
||
격리거리: <b className="text-color-accent">{s.responseDistanceFire}</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-label-2 font-bold text-color-accent mb-1">
|
||
💨 유출 시 (비화재)
|
||
</div>
|
||
<div className="text-caption text-fg-sub leading-[1.6]">
|
||
주간 방호활동거리:{' '}
|
||
<b className="text-color-accent">{s.responseDistanceSpillDay}</b>
|
||
<br />
|
||
야간 방호활동거리:{' '}
|
||
<b className="text-color-accent">{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-label-2 font-bold text-color-accent mb-1">
|
||
🌊 해상 유출 시
|
||
</div>
|
||
<div className="text-caption text-fg-sub leading-[1.6]">{s.marineResponse}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
{/* PPE */}
|
||
<div
|
||
className="rounded-md overflow-hidden mb-3"
|
||
style={{ border: '1px solid rgba(6,182,212,.2)' }}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: '10px 12px',
|
||
background: 'rgba(6,182,212,.06)',
|
||
borderBottom: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-1 font-bold text-color-accent">
|
||
🛡 개인보호장구 (PPE) 추천
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1.5 text-caption p-3">
|
||
<div
|
||
className="text-center rounded"
|
||
style={{
|
||
padding: 8,
|
||
background: 'rgba(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.1)',
|
||
}}
|
||
>
|
||
<div className="text-base mb-[3px]">🧑🚒</div>
|
||
<div className="font-bold text-color-accent">근거리</div>
|
||
<div className="text-[8px] text-fg-disabled mt-0.5">{s.ppeClose}</div>
|
||
</div>
|
||
<div
|
||
className="text-center rounded"
|
||
style={{
|
||
padding: 8,
|
||
background: 'rgba(6,182,212,.04)',
|
||
border: '1px solid rgba(6,182,212,.1)',
|
||
}}
|
||
>
|
||
<div className="text-base mb-[3px]">🦺</div>
|
||
<div className="font-bold text-color-accent">원거리</div>
|
||
<div className="text-[8px] text-fg-disabled mt-0.5">{s.ppeFar}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* MSDS */}
|
||
<div
|
||
className="rounded-md overflow-hidden"
|
||
style={{ border: '1px solid rgba(6,182,212,.2)' }}
|
||
>
|
||
<div
|
||
className="flex items-center justify-between"
|
||
style={{
|
||
padding: '10px 12px',
|
||
background: 'rgba(6,182,212,.06)',
|
||
borderBottom: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-1 font-bold" style={{ color: 'var(--color-accent)' }}>
|
||
📄 MSDS 주요 정보
|
||
</div>
|
||
<button
|
||
className="text-[8px] font-semibold cursor-pointer rounded"
|
||
style={{
|
||
padding: '3px 10px',
|
||
background: 'rgba(6,182,212,.1)',
|
||
border: '1px solid rgba(6,182,212,.2)',
|
||
color: 'var(--color-accent)',
|
||
}}
|
||
>
|
||
📥 전문 다운로드
|
||
</button>
|
||
</div>
|
||
<div className="text-[8px] text-fg-sub 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(6,182,212,.2)' }}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: '10px 12px',
|
||
background: 'rgba(6,182,212,.06)',
|
||
borderBottom: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-1 font-bold text-color-accent">
|
||
⚓ IBC CODE 기반 주요 내용
|
||
</div>
|
||
</div>
|
||
<div className="p-3 flex flex-col gap-2 text-caption">
|
||
<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-fg-disabled"
|
||
style={{ padding: '6px 8px', background: 'var(--bg-base)' }}
|
||
>
|
||
{label}
|
||
</div>
|
||
<div
|
||
className="rounded font-semibold"
|
||
style={{ padding: '6px 8px', background: 'var(--bg-base)' }}
|
||
>
|
||
{value}
|
||
</div>
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
{/* Tank diagram SVG */}
|
||
<div
|
||
className="rounded-sm text-center"
|
||
style={{ padding: 10, background: 'var(--bg-base)' }}
|
||
>
|
||
<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(6,182,212,.4)"
|
||
strokeWidth={1.5}
|
||
/>
|
||
<line
|
||
x1={70}
|
||
y1={10}
|
||
x2={70}
|
||
y2={60}
|
||
stroke="rgba(6,182,212,.2)"
|
||
strokeWidth={1}
|
||
strokeDasharray="3"
|
||
/>
|
||
<line
|
||
x1={130}
|
||
y1={10}
|
||
x2={130}
|
||
y2={60}
|
||
stroke="rgba(6,182,212,.2)"
|
||
strokeWidth={1}
|
||
strokeDasharray="3"
|
||
/>
|
||
<text
|
||
x={40}
|
||
y={38}
|
||
textAnchor="middle"
|
||
fill="var(--color-accent)"
|
||
fontSize={7}
|
||
fontFamily="var(--font-korean)"
|
||
>
|
||
CARGO
|
||
</text>
|
||
<text
|
||
x={40}
|
||
y={48}
|
||
textAnchor="middle"
|
||
fill="var(--fg-disabled)"
|
||
fontSize={6}
|
||
fontFamily="var(--font-mono)"
|
||
>
|
||
Tank 1
|
||
</text>
|
||
<text
|
||
x={100}
|
||
y={38}
|
||
textAnchor="middle"
|
||
fill="var(--color-accent)"
|
||
fontSize={7}
|
||
fontFamily="var(--font-korean)"
|
||
>
|
||
CARGO
|
||
</text>
|
||
<text
|
||
x={100}
|
||
y={48}
|
||
textAnchor="middle"
|
||
fill="var(--fg-disabled)"
|
||
fontSize={6}
|
||
fontFamily="var(--font-mono)"
|
||
>
|
||
Tank 2
|
||
</text>
|
||
<text
|
||
x={160}
|
||
y={38}
|
||
textAnchor="middle"
|
||
fill="var(--color-accent)"
|
||
fontSize={7}
|
||
fontFamily="var(--font-korean)"
|
||
>
|
||
CARGO
|
||
</text>
|
||
<text
|
||
x={160}
|
||
y={48}
|
||
textAnchor="middle"
|
||
fill="var(--fg-disabled)"
|
||
fontSize={6}
|
||
fontFamily="var(--font-mono)"
|
||
>
|
||
Tank 3
|
||
</text>
|
||
<text
|
||
x={100}
|
||
y={75}
|
||
textAnchor="middle"
|
||
fill="var(--fg-disabled)"
|
||
fontSize={7}
|
||
fontFamily="var(--font-korean)"
|
||
>
|
||
{s.ibcShipType} — {s.ibcTankType}
|
||
</text>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* EmS */}
|
||
<div
|
||
className="rounded-md overflow-hidden"
|
||
style={{ border: '1px solid rgba(6,182,212,.2)' }}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: '10px 12px',
|
||
background: 'rgba(6,182,212,.06)',
|
||
borderBottom: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-1 font-bold text-color-accent">
|
||
🆘 비상대응핸드북 (EmS) — ERG {s.ergNumber}
|
||
</div>
|
||
</div>
|
||
<div className="p-3 flex flex-col gap-2 text-caption">
|
||
<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-color-accent mb-[3px]">🔥 화재 대응</div>
|
||
<div className="text-fg-sub leading-[1.6]">{s.emsFire}</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-color-accent mb-[3px]">💧 유출 대응</div>
|
||
<div className="text-fg-sub 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-color-accent mb-[3px]">🏥 응급조치</div>
|
||
<div className="text-fg-sub leading-[1.6]">{s.emsFirstAid}</div>
|
||
</div>
|
||
<div className="text-center">
|
||
<button
|
||
className="text-label-2 font-semibold cursor-pointer rounded-sm text-color-accent"
|
||
style={{
|
||
padding: '6px 16px',
|
||
background: 'rgba(6,182,212,.1)',
|
||
border: '1px solid rgba(6,182,212,.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(6,182,212,.2)' }}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: '10px 12px',
|
||
background: 'rgba(6,182,212,.06)',
|
||
borderBottom: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-1 font-bold text-color-accent">
|
||
📋 화물적부도 화물코드
|
||
</div>
|
||
<div className="text-[8px] text-fg-disabled">클릭 시 물질검색창으로 이동</div>
|
||
</div>
|
||
<div className="p-3">
|
||
<table className="w-full border-collapse text-caption">
|
||
<thead>
|
||
<tr style={{ background: 'rgba(6,182,212,.05)' }}>
|
||
<th
|
||
className="text-color-accent text-left"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
화물코드
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-left"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
약자/제품명
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-left"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
국적/회사
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-center"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
출처
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{s.cargoCodes.map((c, i) => {
|
||
const srcColor =
|
||
c.source === '적부도'
|
||
? 'var(--color-accent)'
|
||
: c.source === '용선자'
|
||
? 'var(--color-accent)'
|
||
: 'var(--color-accent)';
|
||
const srcBg =
|
||
c.source === '적부도'
|
||
? 'rgba(6,182,212,.1)'
|
||
: c.source === '용선자'
|
||
? 'rgba(6,182,212,.1)'
|
||
: 'rgba(6,182,212,.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-color-accent font-semibold cursor-pointer py-[5px] px-2">
|
||
{c.code}
|
||
</td>
|
||
<td className="py-[5px] px-2">{c.name}</td>
|
||
<td className="text-fg-disabled 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: 'rgba(6,182,212,.06)',
|
||
borderBottom: '1px solid rgba(6,182,212,.12)',
|
||
}}
|
||
>
|
||
<div className="text-label-1 font-bold text-color-accent">🏗 항구별 코드</div>
|
||
<div className="text-[8px] text-fg-disabled">Port-MIS 위험물반입신고현황 연동</div>
|
||
</div>
|
||
<div className="p-3">
|
||
<table className="w-full border-collapse text-caption">
|
||
<thead>
|
||
<tr style={{ background: 'rgba(6,182,212,.05)' }}>
|
||
<th
|
||
className="text-color-accent text-left"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
항구
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-left"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
청코드
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-center"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
최근 반입
|
||
</th>
|
||
<th
|
||
className="text-fg-sub text-center"
|
||
style={{
|
||
padding: '6px 8px',
|
||
borderBottom: '1px solid var(--stroke-light)',
|
||
}}
|
||
>
|
||
빈도
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{s.portFrequency.map((p, i) => {
|
||
const freqColor =
|
||
p.frequency === '높음'
|
||
? 'var(--color-accent)'
|
||
: p.frequency === '중간'
|
||
? 'var(--color-accent)'
|
||
: 'var(--color-accent)';
|
||
const freqBg =
|
||
p.frequency === '높음'
|
||
? 'rgba(6,182,212,.1)'
|
||
: p.frequency === '중간'
|
||
? 'rgba(6,182,212,.1)'
|
||
: 'rgba(6,182,212,.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-color-accent 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-color-accent text-label-2 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>
|
||
);
|
||
}
|