Merge pull request 'develop' (#58) from develop into main
All checks were successful
Build and Deploy Wing-Demo / build-and-deploy (push) Successful in 31s
All checks were successful
Build and Deploy Wing-Demo / build-and-deploy (push) Successful in 31s
Reviewed-on: #58
This commit is contained in:
커밋
5150e74a07
@ -1,175 +0,0 @@
|
|||||||
/* Layer tree 3-level styles */
|
|
||||||
.lyr-sw {
|
|
||||||
width: 28px;
|
|
||||||
height: 16px;
|
|
||||||
background: var(--bg0);
|
|
||||||
border-radius: 8px;
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid var(--bd);
|
|
||||||
transition: 0.2s;
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-sw::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
background: var(--t3);
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-sw.on {
|
|
||||||
background: rgba(6, 182, 212, 0.3);
|
|
||||||
border-color: var(--cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-sw.on::after {
|
|
||||||
left: 14px;
|
|
||||||
background: var(--cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-sw:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Level 1 Group */
|
|
||||||
.lyr-g1 {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h1 {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 7px 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--rS);
|
|
||||||
transition: background 0.15s;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--t1);
|
|
||||||
font-family: var(--fK);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h1:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h1 .lyr-arr {
|
|
||||||
font-size: 8px;
|
|
||||||
color: var(--t3);
|
|
||||||
transition: transform 0.2s;
|
|
||||||
width: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h1 .lyr-arr.open {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h1-cnt {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--t3);
|
|
||||||
font-family: var(--fM);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-c1 {
|
|
||||||
padding-left: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-c1.collapsed {
|
|
||||||
max-height: 0 !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Level 2 Group */
|
|
||||||
.lyr-g2 {
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h2 {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 5px 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: background 0.15s;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--t2);
|
|
||||||
font-family: var(--fK);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h2:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h2 .lyr-arr {
|
|
||||||
font-size: 7px;
|
|
||||||
color: var(--t3);
|
|
||||||
transition: transform 0.2s;
|
|
||||||
width: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h2 .lyr-arr.open {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-h2-cnt {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--t3);
|
|
||||||
font-family: var(--fM);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-c2 {
|
|
||||||
padding-left: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-c2.collapsed {
|
|
||||||
max-height: 0 !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Level 3 Toggle */
|
|
||||||
.lyr-t {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--t2);
|
|
||||||
transition: color 0.15s, background 0.15s;
|
|
||||||
font-family: var(--fK);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-t:hover {
|
|
||||||
color: var(--t1);
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyr-cnt {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--t3);
|
|
||||||
font-family: var(--fM);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
@ -55,10 +55,7 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="w-screen h-screen flex overflow-hidden relative bg-[#001028]">
|
||||||
width: '100vw', height: '100vh', display: 'flex',
|
|
||||||
background: '#001028', overflow: 'hidden', position: 'relative',
|
|
||||||
}}>
|
|
||||||
{/* Background image */}
|
{/* Background image */}
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute', inset: 0,
|
position: 'absolute', inset: 0,
|
||||||
@ -78,18 +75,14 @@ export function LoginPage() {
|
|||||||
}} />
|
}} />
|
||||||
|
|
||||||
{/* Center: Login Form */}
|
{/* Center: Login Form */}
|
||||||
<div style={{
|
<div className="w-full flex flex-col items-start justify-center relative z-[1] px-[120px] py-[40px]" style={{ paddingLeft: 120, paddingRight: 50 }}>
|
||||||
width: '100%', display: 'flex', flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start', justifyContent: 'center',
|
|
||||||
padding: '40px 50px 40px 120px', position: 'relative', zIndex: 1,
|
|
||||||
}}>
|
|
||||||
<div style={{ width: '100%', maxWidth: 360 }}>
|
<div style={{ width: '100%', maxWidth: 360 }}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
<div className="text-center mb-9">
|
||||||
<img
|
<img
|
||||||
src="/wing_logo_text_white.svg"
|
src="/wing_logo_text_white.svg"
|
||||||
alt="WING 해양환경 위기대응 통합시스템"
|
alt="WING 해양환경 위기대응 통합시스템"
|
||||||
style={{ height: 28, margin: '0 auto', display: 'block' }}
|
className="h-7 mx-auto block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -104,18 +97,12 @@ export function LoginPage() {
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* User ID */}
|
{/* User ID */}
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div className="mb-4">
|
||||||
<label style={{
|
<label className="block text-[10px] font-semibold text-text-3 mb-1.5" style={{ letterSpacing: '0.3px' }}>
|
||||||
display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)',
|
|
||||||
fontFamily: 'var(--fK)', marginBottom: 6, letterSpacing: '0.3px',
|
|
||||||
}}>
|
|
||||||
아이디
|
아이디
|
||||||
</label>
|
</label>
|
||||||
<div style={{ position: 'relative' }}>
|
<div className="relative">
|
||||||
<span style={{
|
<span className="absolute text-sm text-text-3 pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
|
||||||
position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)',
|
|
||||||
fontSize: 14, color: 'var(--t3)', pointerEvents: 'none',
|
|
||||||
}}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@ -125,11 +112,9 @@ export function LoginPage() {
|
|||||||
placeholder="사용자 아이디 입력"
|
placeholder="사용자 아이디 입력"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
className="w-full bg-bg-2 border border-border rounded-md text-[13px] outline-none"
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '11px 14px 11px 38px',
|
padding: '11px 14px 11px 38px',
|
||||||
background: 'var(--bg2)', border: '1px solid var(--bd)',
|
|
||||||
borderRadius: 8, color: 'var(--t1)', fontSize: 13,
|
|
||||||
fontFamily: 'var(--fK)', outline: 'none',
|
|
||||||
transition: 'border-color 0.2s, box-shadow 0.2s',
|
transition: 'border-color 0.2s, box-shadow 0.2s',
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
@ -145,18 +130,12 @@ export function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password */}
|
{/* Password */}
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div className="mb-5">
|
||||||
<label style={{
|
<label className="block text-[10px] font-semibold text-text-3 mb-1.5" style={{ letterSpacing: '0.3px' }}>
|
||||||
display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)',
|
|
||||||
fontFamily: 'var(--fK)', marginBottom: 6, letterSpacing: '0.3px',
|
|
||||||
}}>
|
|
||||||
비밀번호
|
비밀번호
|
||||||
</label>
|
</label>
|
||||||
<div style={{ position: 'relative' }}>
|
<div className="relative">
|
||||||
<span style={{
|
<span className="absolute text-sm text-text-3 pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
|
||||||
position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)',
|
|
||||||
fontSize: 14, color: 'var(--t3)', pointerEvents: 'none',
|
|
||||||
}}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@ -165,11 +144,9 @@ export function LoginPage() {
|
|||||||
onChange={(e) => { setPassword(e.target.value); clearError() }}
|
onChange={(e) => { setPassword(e.target.value); clearError() }}
|
||||||
placeholder="비밀번호 입력"
|
placeholder="비밀번호 입력"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
|
className="w-full bg-bg-2 border border-border rounded-md text-[13px] outline-none"
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '11px 14px 11px 38px',
|
padding: '11px 14px 11px 38px',
|
||||||
background: 'var(--bg2)', border: '1px solid var(--bd)',
|
|
||||||
borderRadius: 8, color: 'var(--t1)', fontSize: 13,
|
|
||||||
fontFamily: 'var(--fK)', outline: 'none',
|
|
||||||
transition: 'border-color 0.2s, box-shadow 0.2s',
|
transition: 'border-color 0.2s, box-shadow 0.2s',
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
@ -185,27 +162,17 @@ export function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Remember + Forgot */}
|
{/* Remember + Forgot */}
|
||||||
<div style={{
|
<div className="flex items-center justify-between mb-5">
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
<label className="flex items-center gap-1.5 text-[11px] text-text-3 cursor-pointer">
|
||||||
marginBottom: 20,
|
|
||||||
}}>
|
|
||||||
<label style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
fontSize: 11, color: 'var(--t3)', fontFamily: 'var(--fK)', cursor: 'pointer',
|
|
||||||
}}>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={remember}
|
checked={remember}
|
||||||
onChange={(e) => setRemember(e.target.checked)}
|
onChange={(e) => setRemember(e.target.checked)}
|
||||||
style={{ accentColor: 'var(--cyan)' }}
|
className="accent-[var(--cyan)]"
|
||||||
/>
|
/>
|
||||||
아이디 저장
|
아이디 저장
|
||||||
</label>
|
</label>
|
||||||
<button type="button" style={{
|
<button type="button" className="text-[11px] text-primary-cyan cursor-pointer bg-transparent border-none"
|
||||||
fontSize: 11, color: 'var(--cyan)', fontFamily: 'var(--fK)',
|
|
||||||
background: 'none', border: 'none', cursor: 'pointer',
|
|
||||||
textDecoration: 'none',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'}
|
onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
|
onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
|
||||||
>
|
>
|
||||||
@ -215,13 +182,12 @@ export function LoginPage() {
|
|||||||
|
|
||||||
{/* Pending approval */}
|
{/* Pending approval */}
|
||||||
{pendingMessage && (
|
{pendingMessage && (
|
||||||
<div style={{
|
<div className="flex items-start gap-2 text-[11px] rounded-sm mb-4" style={{
|
||||||
padding: '10px 12px', marginBottom: 16, borderRadius: 6,
|
padding: '10px 12px',
|
||||||
background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)',
|
background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)',
|
||||||
fontSize: 11, color: '#67e8f9', fontFamily: 'var(--fK)',
|
color: '#67e8f9',
|
||||||
display: 'flex', alignItems: 'flex-start', gap: 8,
|
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontSize: 14, flexShrink: 0, marginTop: 1 }}>
|
<span className="text-sm shrink-0 mt-px">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<span>{pendingMessage}</span>
|
<span>{pendingMessage}</span>
|
||||||
@ -230,13 +196,12 @@ export function LoginPage() {
|
|||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{
|
<div className="flex items-center gap-1.5 text-[11px] rounded-sm mb-4" style={{
|
||||||
padding: '8px 12px', marginBottom: 16, borderRadius: 6,
|
padding: '8px 12px',
|
||||||
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)',
|
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)',
|
||||||
fontSize: 11, color: '#f87171', fontFamily: 'var(--fK)',
|
color: '#f87171',
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontSize: 13 }}>
|
<span className="text-[13px]">
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>
|
||||||
</span>
|
</span>
|
||||||
{error}
|
{error}
|
||||||
@ -244,18 +209,17 @@ export function LoginPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Login button */}
|
{/* Login button */}
|
||||||
<button type="submit" disabled={isLoading} style={{
|
<button type="submit" disabled={isLoading} className="w-full text-primary-cyan text-sm font-bold rounded-md border"
|
||||||
width: '100%', padding: '12px',
|
style={{
|
||||||
background: isLoading
|
padding: '12px',
|
||||||
? 'rgba(6,182,212,0.15)'
|
background: isLoading
|
||||||
: 'linear-gradient(135deg, rgba(6,182,212,0.2), rgba(59,130,246,0.15))',
|
? 'rgba(6,182,212,0.15)'
|
||||||
border: '1px solid rgba(6,182,212,0.3)',
|
: 'linear-gradient(135deg, rgba(6,182,212,0.2), rgba(59,130,246,0.15))',
|
||||||
borderRadius: 8, color: 'var(--cyan)',
|
borderColor: 'rgba(6,182,212,0.3)',
|
||||||
fontSize: 14, fontWeight: 700, cursor: isLoading ? 'wait' : 'pointer',
|
cursor: isLoading ? 'wait' : 'pointer',
|
||||||
fontFamily: 'var(--fK)',
|
transition: 'all 0.2s',
|
||||||
transition: 'all 0.2s',
|
boxShadow: '0 4px 16px rgba(6,182,212,0.1)',
|
||||||
boxShadow: '0 4px 16px rgba(6,182,212,0.1)',
|
}}
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(6,182,212,0.3), rgba(59,130,246,0.2))'
|
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(6,182,212,0.3), rgba(59,130,246,0.2))'
|
||||||
@ -270,7 +234,7 @@ export function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
|
<span className="flex items-center justify-center gap-2">
|
||||||
<span style={{
|
<span style={{
|
||||||
width: 14, height: 14, border: '2px solid rgba(6,182,212,0.3)',
|
width: 14, height: 14, border: '2px solid rgba(6,182,212,0.3)',
|
||||||
borderTop: '2px solid var(--cyan)', borderRadius: '50%',
|
borderTop: '2px solid var(--cyan)', borderRadius: '50%',
|
||||||
@ -283,21 +247,16 @@ export function LoginPage() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div style={{
|
<div className="flex items-center gap-3 my-6">
|
||||||
display: 'flex', alignItems: 'center', gap: 12, margin: '24px 0',
|
<div className="flex-1 bg-border h-px" />
|
||||||
}}>
|
<span className="text-[9px] text-text-3">또는</span>
|
||||||
<div style={{ flex: 1, height: 1, background: 'var(--bd)' }} />
|
<div className="flex-1 bg-border h-px" />
|
||||||
<span style={{ fontSize: 9, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>또는</span>
|
|
||||||
<div style={{ flex: 1, height: 1, background: 'var(--bd)' }} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Google / Certificate */}
|
{/* Google / Certificate */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div className="flex flex-col gap-2">
|
||||||
{GOOGLE_ENABLED && (
|
{GOOGLE_ENABLED && (
|
||||||
<div style={{
|
<div className="flex justify-center rounded-md overflow-hidden">
|
||||||
display: 'flex', justifyContent: 'center',
|
|
||||||
borderRadius: 8, overflow: 'hidden',
|
|
||||||
}}>
|
|
||||||
<GoogleLogin
|
<GoogleLogin
|
||||||
onSuccess={handleGoogleSuccess}
|
onSuccess={handleGoogleSuccess}
|
||||||
onError={() => { /* 팝업 닫힘 등 — 별도 처리 불필요 */ }}
|
onError={() => { /* 팝업 닫힘 등 — 별도 처리 불필요 */ }}
|
||||||
@ -308,14 +267,8 @@ export function LoginPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button type="button" style={{
|
<button type="button" className="w-full rounded-md bg-bg-3 border border-border text-text-2 text-[11px] font-semibold cursor-pointer flex items-center justify-center gap-1.5 px-[10px] py-[10px]"
|
||||||
width: '100%', padding: '10px', borderRadius: 8,
|
style={{ transition: 'background 0.15s' }}
|
||||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
|
||||||
color: 'var(--t2)', fontSize: 11, fontWeight: 600,
|
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
|
||||||
transition: 'background 0.15s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bgH)'}
|
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bgH)'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--bg3)'}
|
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--bg3)'}
|
||||||
>
|
>
|
||||||
@ -326,29 +279,29 @@ export function LoginPage() {
|
|||||||
|
|
||||||
{/* Demo accounts info (DEV only) */}
|
{/* Demo accounts info (DEV only) */}
|
||||||
{import.meta.env.DEV && (
|
{import.meta.env.DEV && (
|
||||||
<div style={{
|
<div className="rounded-md mt-6" style={{
|
||||||
marginTop: 24, padding: '10px 12px', borderRadius: 8,
|
padding: '10px 12px',
|
||||||
background: 'rgba(6,182,212,0.04)', border: '1px solid rgba(6,182,212,0.08)',
|
background: 'rgba(6,182,212,0.04)', border: '1px solid rgba(6,182,212,0.08)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: 9, fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: 6 }}>
|
<div className="text-[9px] font-bold text-primary-cyan mb-1.5">
|
||||||
데모 계정
|
데모 계정
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<div className="flex flex-col gap-[3px]">
|
||||||
{DEMO_ACCOUNTS.map((acc) => (
|
{DEMO_ACCOUNTS.map((acc) => (
|
||||||
<div key={acc.id}
|
<div key={acc.id}
|
||||||
onClick={() => { setUserId(acc.id); setPassword(acc.password); clearError() }}
|
onClick={() => { setUserId(acc.id); setPassword(acc.password); clearError() }}
|
||||||
|
className="flex justify-between items-center cursor-pointer rounded"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
padding: '4px 6px',
|
||||||
padding: '4px 6px', borderRadius: 4, cursor: 'pointer',
|
|
||||||
transition: 'background 0.15s',
|
transition: 'background 0.15s',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(6,182,212,0.06)'}
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(6,182,212,0.06)'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 9, color: 'var(--t2)', fontFamily: 'var(--fM)' }}>
|
<span className="text-[9px] text-text-2 font-mono">
|
||||||
{acc.id} / {acc.password}
|
{acc.id} / {acc.password}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 8, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<span className="text-[8px] text-text-3">
|
||||||
{acc.label}
|
{acc.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -359,12 +312,9 @@ export function LoginPage() {
|
|||||||
</div>{/* end form card */}
|
</div>{/* end form card */}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{
|
<div className="text-center text-[9px] text-text-3 mt-6" className="leading-[1.6]">
|
||||||
marginTop: 24, textAlign: 'center', fontSize: 9,
|
|
||||||
color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.6,
|
|
||||||
}}>
|
|
||||||
<div>WING V2.0 | 해양경찰청 기동방제과 위기대응 통합시스템</div>
|
<div>WING V2.0 | 해양경찰청 기동방제과 위기대응 통합시스템</div>
|
||||||
<div style={{ marginTop: 2, color: 'rgba(134,144,166,0.6)' }}>
|
<div className="mt-0.5" style={{ color: 'rgba(134,144,166,0.6)' }}>
|
||||||
© 2026 Korea Coast Guard. All rights reserved.
|
© 2026 Korea Coast Guard. All rights reserved.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,19 +25,14 @@ export function LayerTree({ layers, enabledLayers, onToggleLayer, layerColors =
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0 4px' }}>
|
<div className="px-1">
|
||||||
<div style={{
|
<div className="flex items-center justify-between px-2 pt-1 pb-2 mb-1 border-b border-border">
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
<span className="text-[10px] font-semibold text-text-3">
|
||||||
padding: '4px 8px 8px', marginBottom: '4px',
|
|
||||||
borderBottom: '1px solid var(--bd)',
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '10px', fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
|
||||||
전체 레이어
|
전체 레이어
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className={`lyr-sw ${allEnabled ? 'on' : ''}`}
|
className={`lyr-sw ${allEnabled ? 'on' : ''} cursor-pointer`}
|
||||||
onClick={handleToggleAll}
|
onClick={handleToggleAll}
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -125,7 +120,7 @@ function LayerNode({ layer, enabledLayers, onToggleLayer, layerColors, onColorCh
|
|||||||
{hasChildren ? (
|
{hasChildren ? (
|
||||||
<span className={`lyr-arr ${expanded ? 'open' : ''}`}>▶</span>
|
<span className={`lyr-arr ${expanded ? 'open' : ''}`}>▶</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="lyr-arr" style={{ visibility: 'hidden' }}>▶</span>
|
<span className="lyr-arr invisible">▶</span>
|
||||||
)}
|
)}
|
||||||
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} onClick={handleSwitchClick} />
|
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} onClick={handleSwitchClick} />
|
||||||
{layer.icon && <span>{layer.icon}</span>}
|
{layer.icon && <span>{layer.icon}</span>}
|
||||||
@ -153,7 +148,7 @@ function LayerNode({ layer, enabledLayers, onToggleLayer, layerColors, onColorCh
|
|||||||
<div className="lyr-t" onClick={(e) => { if (!(e.target as HTMLElement).closest('.lyr-csw, .lyr-cpop')) handleSwitchClick(e) }}>
|
<div className="lyr-t" onClick={(e) => { if (!(e.target as HTMLElement).closest('.lyr-csw, .lyr-cpop')) handleSwitchClick(e) }}>
|
||||||
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} />
|
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} />
|
||||||
{layer.icon && <span>{layer.icon}</span>}
|
{layer.icon && <span>{layer.icon}</span>}
|
||||||
<span style={{ flex: 1 }}>{layer.name}</span>
|
<span className="flex-1">{layer.name}</span>
|
||||||
{layer.count !== undefined && <span className="lyr-cnt">{layer.count.toLocaleString()}</span>}
|
{layer.count !== undefined && <span className="lyr-cnt">{layer.count.toLocaleString()}</span>}
|
||||||
{onColorChange && (
|
{onColorChange && (
|
||||||
<ColorSwatch color={layerColors[layer.id]} onChange={(c) => onColorChange(layer.id, c)} />
|
<ColorSwatch color={layerColors[layer.id]} onChange={(c) => onColorChange(layer.id, c)} />
|
||||||
@ -168,7 +163,7 @@ function LayerNode({ layer, enabledLayers, onToggleLayer, layerColors, onColorCh
|
|||||||
{hasChildren ? (
|
{hasChildren ? (
|
||||||
<span className={`lyr-arr ${expanded ? 'open' : ''}`}>▶</span>
|
<span className={`lyr-arr ${expanded ? 'open' : ''}`}>▶</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="lyr-arr" style={{ visibility: 'hidden' }}>▶</span>
|
<span className="lyr-arr invisible">▶</span>
|
||||||
)}
|
)}
|
||||||
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} onClick={handleSwitchClick} />
|
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} onClick={handleSwitchClick} />
|
||||||
{layer.icon && <span>{layer.icon}</span>}
|
{layer.icon && <span>{layer.icon}</span>}
|
||||||
@ -192,7 +187,7 @@ function LayerNode({ layer, enabledLayers, onToggleLayer, layerColors, onColorCh
|
|||||||
<div className="lyr-t" onClick={(e) => { if (!(e.target as HTMLElement).closest('.lyr-csw, .lyr-cpop')) handleSwitchClick(e) }}>
|
<div className="lyr-t" onClick={(e) => { if (!(e.target as HTMLElement).closest('.lyr-csw, .lyr-cpop')) handleSwitchClick(e) }}>
|
||||||
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} />
|
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} />
|
||||||
{layer.icon && <span>{layer.icon}</span>}
|
{layer.icon && <span>{layer.icon}</span>}
|
||||||
<span style={{ flex: 1 }}>{layer.name}</span>
|
<span className="flex-1">{layer.name}</span>
|
||||||
{layer.count !== undefined && <span className="lyr-cnt">{layer.count.toLocaleString()}</span>}
|
{layer.count !== undefined && <span className="lyr-cnt">{layer.count.toLocaleString()}</span>}
|
||||||
{onColorChange && (
|
{onColorChange && (
|
||||||
<ColorSwatch color={layerColors[layer.id]} onChange={(c) => onColorChange(layer.id, c)} />
|
<ColorSwatch color={layerColors[layer.id]} onChange={(c) => onColorChange(layer.id, c)} />
|
||||||
@ -204,15 +199,15 @@ function LayerNode({ layer, enabledLayers, onToggleLayer, layerColors, onColorCh
|
|||||||
// depth 2+ with children
|
// depth 2+ with children
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="lyr-t" style={{ gap: '6px' }}>
|
<div className="lyr-t gap-1.5">
|
||||||
<span className={`lyr-arr ${expanded ? 'open' : ''}`} onClick={handleHeaderClick} style={{ cursor: 'pointer', fontSize: '7px', width: '10px', textAlign: 'center' }}>▶</span>
|
<span className={`lyr-arr ${expanded ? 'open' : ''} cursor-pointer text-[7px] w-[10px] text-center`} onClick={handleHeaderClick}>▶</span>
|
||||||
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} onClick={handleSwitchClick} />
|
<div className={`lyr-sw ${isEnabled ? 'on' : ''}`} onClick={handleSwitchClick} />
|
||||||
{layer.icon && <span>{layer.icon}</span>}
|
{layer.icon && <span>{layer.icon}</span>}
|
||||||
<span onClick={handleHeaderClick} style={{ cursor: 'pointer', flex: 1 }}>{layer.name}</span>
|
<span onClick={handleHeaderClick} className="cursor-pointer flex-1">{layer.name}</span>
|
||||||
{layer.count !== undefined && <span className="lyr-cnt">{layer.count.toLocaleString()}</span>}
|
{layer.count !== undefined && <span className="lyr-cnt">{layer.count.toLocaleString()}</span>}
|
||||||
</div>
|
</div>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div style={{ paddingLeft: '16px' }}>
|
<div className="pl-4">
|
||||||
{layer.children!.map(child => (
|
{layer.children!.map(child => (
|
||||||
<LayerNode key={child.id} layer={child} enabledLayers={enabledLayers} onToggleLayer={onToggleLayer} layerColors={layerColors} onColorChange={onColorChange} depth={depth + 1} />
|
<LayerNode key={child.id} layer={child} enabledLayers={enabledLayers} onToggleLayer={onToggleLayer} layerColors={layerColors} onColorChange={onColorChange} depth={depth + 1} />
|
||||||
))}
|
))}
|
||||||
@ -237,7 +232,7 @@ function ColorSwatch({ color, onChange }: { color?: string; onChange: (c: string
|
|||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
|
<div ref={ref} className="relative shrink-0">
|
||||||
<div
|
<div
|
||||||
className={`lyr-csw ${color ? 'has-color' : ''}`}
|
className={`lyr-csw ${color ? 'has-color' : ''}`}
|
||||||
style={color ? { borderColor: color, background: color } : {}}
|
style={color ? { borderColor: color, background: color } : {}}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export function SubMenuBar({ activeMainTab }: SubMenuBarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border bg-bg-1" style={{ flexShrink: 0 }}>
|
<div className="border-b border-border bg-bg-1 shrink-0">
|
||||||
<div className="flex px-5">
|
<div className="flex px-5">
|
||||||
{subMenuConfig.map((item) => (
|
{subMenuConfig.map((item) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showQuickMenu && (
|
{showQuickMenu && (
|
||||||
<div className="absolute top-[44px] right-0 w-[220px] bg-[rgba(18,25,41,0.97)] backdrop-blur-xl border border-border rounded-lg shadow-2xl z-[200] py-2 font-korean" style={{ fontFamily: 'var(--fK)' }}>
|
<div className="absolute top-[44px] right-0 w-[220px] bg-[rgba(18,25,41,0.97)] backdrop-blur-xl border border-border rounded-lg shadow-2xl z-[200] py-2 font-korean">
|
||||||
{/* 거리·면적 계산 */}
|
{/* 거리·면적 계산 */}
|
||||||
<div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
|
<div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
|
||||||
<span>📐</span> 거리·면적 계산
|
<span>📐</span> 거리·면적 계산
|
||||||
|
|||||||
@ -42,28 +42,30 @@ export function BacktrackReplayBar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute', bottom: '80px', left: '50%', transform: 'translateX(-50%)',
|
className="absolute flex flex-col"
|
||||||
minWidth: '480px', maxWidth: '680px', width: '60%',
|
style={{
|
||||||
background: 'rgba(10,15,25,0.92)', backdropFilter: 'blur(12px)',
|
bottom: '80px', left: '50%', transform: 'translateX(-50%)',
|
||||||
border: '1px solid rgba(168,85,247,0.3)', borderRadius: '12px',
|
minWidth: '480px', maxWidth: '680px', width: '60%',
|
||||||
padding: '12px 18px', zIndex: 1200,
|
background: 'rgba(10,15,25,0.92)', backdropFilter: 'blur(12px)',
|
||||||
display: 'flex', flexDirection: 'column', gap: '10px',
|
border: '1px solid rgba(168,85,247,0.3)', borderRadius: '12px',
|
||||||
fontFamily: 'var(--fK)',
|
padding: '12px 18px', zIndex: 1200,
|
||||||
}}>
|
gap: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Header row */}
|
{/* Header row */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div className="flex items-center justify-between">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div className="flex items-center gap-2">
|
||||||
<div style={{
|
<div
|
||||||
width: '8px', height: '8px', borderRadius: '50%',
|
className="w-2 h-2 rounded-full bg-primary-purple"
|
||||||
background: 'var(--purple)', boxShadow: '0 0 8px rgba(168,85,247,0.5)',
|
style={{ boxShadow: '0 0 8px rgba(168,85,247,0.5)' }}
|
||||||
}} />
|
/>
|
||||||
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)' }}>
|
<span className="text-xs font-bold">
|
||||||
역추적 리플레이
|
역추적 리플레이
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="flex items-center gap-1.5">
|
||||||
{/* Speed buttons */}
|
{/* Speed buttons */}
|
||||||
{[1, 2, 4].map((spd) => (
|
{[1, 2, 4].map((spd) => (
|
||||||
<button
|
<button
|
||||||
@ -75,15 +77,15 @@ export function BacktrackReplayBar({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div style={{ width: '8px' }} />
|
<div className="w-2" />
|
||||||
|
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
className="text-status-red cursor-pointer font-bold"
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 10px', borderRadius: '6px', fontSize: '10px', fontWeight: 700,
|
padding: '4px 10px', borderRadius: '6px', fontSize: '10px',
|
||||||
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
|
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
|
||||||
color: 'var(--red)', cursor: 'pointer', fontFamily: 'var(--fK)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
✕ 닫기
|
✕ 닫기
|
||||||
@ -92,52 +94,50 @@ export function BacktrackReplayBar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls row */}
|
{/* Controls row */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div className="flex items-center gap-3">
|
||||||
{/* Play/Pause */}
|
{/* Play/Pause */}
|
||||||
<button
|
<button
|
||||||
onClick={onTogglePlay}
|
onClick={onTogglePlay}
|
||||||
|
className="shrink-0 w-9 h-9 rounded-full flex items-center justify-center text-sm cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
width: '36px', height: '36px', borderRadius: '50%', flexShrink: 0,
|
|
||||||
background: isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.15)',
|
background: isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.15)',
|
||||||
border: `2px solid ${isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.4)'}`,
|
border: `2px solid ${isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.4)'}`,
|
||||||
color: isPlaying ? '#fff' : 'var(--purple)',
|
color: isPlaying ? '#fff' : 'var(--purple)',
|
||||||
fontSize: '14px', cursor: 'pointer',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isPlaying ? '⏸' : '▶'}
|
{isPlaying ? '⏸' : '▶'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Timeline */}
|
{/* Timeline */}
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
<div className="flex-1 flex flex-col gap-1">
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div
|
<div
|
||||||
style={{
|
className="relative h-5 flex items-center cursor-pointer"
|
||||||
position: 'relative', height: '20px', display: 'flex', alignItems: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
onClick={handleSeekClick}
|
onClick={handleSeekClick}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div
|
||||||
width: '100%', height: '4px', background: 'var(--bd)', borderRadius: '2px',
|
className="w-full h-1 bg-border relative overflow-visible"
|
||||||
position: 'relative', overflow: 'visible',
|
style={{ borderRadius: '2px' }}
|
||||||
}}>
|
>
|
||||||
{/* Fill */}
|
{/* Fill */}
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute', top: 0, left: 0,
|
className="absolute top-0 left-0 h-full"
|
||||||
width: `${progress}%`, height: '100%',
|
style={{
|
||||||
background: 'linear-gradient(90deg, var(--purple), var(--cyan))',
|
width: `${progress}%`,
|
||||||
borderRadius: '2px', transition: 'width 0.05s',
|
background: 'linear-gradient(90deg, var(--purple), var(--cyan))',
|
||||||
}} />
|
borderRadius: '2px', transition: 'width 0.05s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Collision marker */}
|
{/* Collision marker */}
|
||||||
{collisionEvent && (
|
{collisionEvent && (
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute', top: '-14px',
|
className="absolute text-[10px] cursor-pointer"
|
||||||
left: `${collisionEvent.progressPercent}%`,
|
style={{
|
||||||
transform: 'translateX(-50%)',
|
top: '-14px',
|
||||||
fontSize: '10px', cursor: 'pointer',
|
left: `${collisionEvent.progressPercent}%`,
|
||||||
}}
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
title={collisionEvent.timeLabel}
|
title={collisionEvent.timeLabel}
|
||||||
>
|
>
|
||||||
💥
|
💥
|
||||||
@ -146,32 +146,33 @@ export function BacktrackReplayBar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Thumb */}
|
{/* Thumb */}
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute', left: `${progress}%`, top: '50%',
|
className="absolute top-1/2 w-3.5 h-3.5 bg-white rounded-full"
|
||||||
transform: 'translate(-50%, -50%)',
|
style={{
|
||||||
width: '14px', height: '14px',
|
left: `${progress}%`,
|
||||||
background: '#fff', borderRadius: '50%',
|
transform: 'translate(-50%, -50%)',
|
||||||
border: '3px solid var(--purple)',
|
border: '3px solid var(--purple)',
|
||||||
boxShadow: '0 0 8px rgba(168,85,247,0.4)',
|
boxShadow: '0 0 8px rgba(168,85,247,0.4)',
|
||||||
zIndex: 2, transition: 'left 0.05s',
|
zIndex: 2, transition: 'left 0.05s',
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time labels */}
|
{/* Time labels */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '9px', fontFamily: 'var(--fM)' }}>
|
<div className="flex justify-between text-[9px] font-mono">
|
||||||
<span style={{ color: 'var(--t3)' }}>18:30</span>
|
<span className="text-text-3">18:30</span>
|
||||||
<span style={{ color: 'var(--purple)', fontWeight: 600 }}>{currentTimeLabel}</span>
|
<span className="font-semibold text-primary-purple">{currentTimeLabel}</span>
|
||||||
<span style={{ color: 'var(--t3)' }}>06:30</span>
|
<span className="text-text-3">06:30</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend row */}
|
{/* Legend row */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '14px', paddingTop: '4px', borderTop: '1px solid var(--bd)' }}>
|
<div className="flex items-center gap-[14px] pt-1 border-t border-border">
|
||||||
{replayShips.map((ship) => (
|
{replayShips.map((ship) => (
|
||||||
<div key={ship.vesselName} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div key={ship.vesselName} className="flex items-center gap-1.5">
|
||||||
<div style={{ width: '16px', height: '3px', background: ship.color, borderRadius: '1px' }} />
|
<div className="w-4 h-[3px]" style={{ background: ship.color, borderRadius: '1px' }} />
|
||||||
<span style={{ fontSize: '9px', color: 'var(--t2)', fontFamily: 'var(--fM)' }}>
|
<span className="text-[9px] text-text-2 font-mono">
|
||||||
{ship.vesselName}
|
{ship.vesselName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -284,7 +284,7 @@ export function MapView({
|
|||||||
longitude: info.coordinate?.[0] ?? 0,
|
longitude: info.coordinate?.[0] ?? 0,
|
||||||
latitude: info.coordinate?.[1] ?? 0,
|
latitude: info.coordinate?.[1] ?? 0,
|
||||||
content: (
|
content: (
|
||||||
<div className="text-xs" style={{ fontFamily: 'var(--fK)', minWidth: '140px' }}>
|
<div className="text-xs" style={{ minWidth: '140px' }}>
|
||||||
<strong style={{ color: PRIORITY_COLORS[d.priority] }}>{d.name}</strong>
|
<strong style={{ color: PRIORITY_COLORS[d.priority] }}>{d.name}</strong>
|
||||||
<br />
|
<br />
|
||||||
우선순위: {PRIORITY_LABELS[d.priority] || d.priority}
|
우선순위: {PRIORITY_LABELS[d.priority] || d.priority}
|
||||||
@ -389,8 +389,8 @@ export function MapView({
|
|||||||
longitude: incidentCoord.lon,
|
longitude: incidentCoord.lon,
|
||||||
latitude: incidentCoord.lat,
|
latitude: incidentCoord.lat,
|
||||||
content: (
|
content: (
|
||||||
<div className="text-xs" style={{ fontFamily: 'var(--fK)' }}>
|
<div className="text-xs">
|
||||||
<strong style={{ color: 'var(--orange)' }}>{d.level}</strong>
|
<strong className="text-status-orange">{d.level}</strong>
|
||||||
<br />
|
<br />
|
||||||
물질: {dispersionResult.substance}
|
물질: {dispersionResult.substance}
|
||||||
<br />
|
<br />
|
||||||
@ -438,12 +438,12 @@ export function MapView({
|
|||||||
longitude: d.lon,
|
longitude: d.lon,
|
||||||
latitude: d.lat,
|
latitude: d.lat,
|
||||||
content: (
|
content: (
|
||||||
<div className="text-xs" style={{ fontFamily: 'var(--fK)', minWidth: '130px' }}>
|
<div className="text-xs" style={{ minWidth: '130px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '4px' }}>
|
<div className="flex items-center gap-1 mb-1">
|
||||||
<span>{SENSITIVE_ICONS[d.type]}</span>
|
<span>{SENSITIVE_ICONS[d.type]}</span>
|
||||||
<strong style={{ color: SENSITIVE_COLORS[d.type] }}>{d.name}</strong>
|
<strong style={{ color: SENSITIVE_COLORS[d.type] }}>{d.name}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '10px', color: '#666' }}>
|
<div className="text-[10px] text-[#666]">
|
||||||
반경: {d.radiusM}m<br />
|
반경: {d.radiusM}m<br />
|
||||||
도달 예상: <strong style={{ color: d.arrivalTimeH <= 6 ? '#ef4444' : '#f97316' }}>{d.arrivalTimeH}h</strong>
|
도달 예상: <strong style={{ color: d.arrivalTimeH <= 6 ? '#ef4444' : '#f97316' }}>{d.arrivalTimeH}h</strong>
|
||||||
</div>
|
</div>
|
||||||
@ -544,7 +544,8 @@ export function MapView({
|
|||||||
zoom: zoom,
|
zoom: zoom,
|
||||||
}}
|
}}
|
||||||
mapStyle={BASE_STYLE}
|
mapStyle={BASE_STYLE}
|
||||||
style={{ width: '100%', height: '100%', cursor: isSelectingLocation ? 'crosshair' : 'grab' }}
|
className="w-full h-full"
|
||||||
|
style={{ cursor: isSelectingLocation ? 'crosshair' : 'grab' }}
|
||||||
onClick={handleMapClick}
|
onClick={handleMapClick}
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
@ -578,9 +579,10 @@ export function MapView({
|
|||||||
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && (
|
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && (
|
||||||
<Marker longitude={incidentCoord.lon} latitude={incidentCoord.lat} anchor="bottom">
|
<Marker longitude={incidentCoord.lon} latitude={incidentCoord.lat} anchor="bottom">
|
||||||
<div
|
<div
|
||||||
|
className="w-6 h-6 bg-primary-cyan border-2 border-white"
|
||||||
style={{
|
style={{
|
||||||
width: 24, height: 24, background: 'var(--cyan)', borderRadius: '50% 50% 50% 0',
|
borderRadius: '50% 50% 50% 0',
|
||||||
transform: 'rotate(-45deg)', border: '2px solid #fff',
|
transform: 'rotate(-45deg)',
|
||||||
boxShadow: '0 2px 8px rgba(6,182,212,0.5)',
|
boxShadow: '0 2px 8px rgba(6,182,212,0.5)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -590,16 +592,16 @@ export function MapView({
|
|||||||
{/* 사고 위치 팝업 (클릭 시) */}
|
{/* 사고 위치 팝업 (클릭 시) */}
|
||||||
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && !popupInfo && (
|
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && !popupInfo && (
|
||||||
<Popup longitude={incidentCoord.lon} latitude={incidentCoord.lat} anchor="bottom" offset={30} closeButton={false} closeOnClick={false}>
|
<Popup longitude={incidentCoord.lon} latitude={incidentCoord.lat} anchor="bottom" offset={30} closeButton={false} closeOnClick={false}>
|
||||||
<div className="text-sm" style={{ color: '#333' }}>
|
<div className="text-sm text-[#333]">
|
||||||
<strong>사고 지점</strong>
|
<strong>사고 지점</strong>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-xs" style={{ color: '#666' }}>
|
<span className="text-xs text-[#666]">
|
||||||
{decimalToDMS(incidentCoord.lat, true)}
|
{decimalToDMS(incidentCoord.lat, true)}
|
||||||
<br />
|
<br />
|
||||||
{decimalToDMS(incidentCoord.lon, false)}
|
{decimalToDMS(incidentCoord.lon, false)}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-xs" style={{ fontFamily: 'monospace', color: '#888' }}>
|
<span className="text-xs font-mono text-[#888]">
|
||||||
({incidentCoord.lat.toFixed(4)}°, {incidentCoord.lon.toFixed(4)}°)
|
({incidentCoord.lat.toFixed(4)}°, {incidentCoord.lon.toFixed(4)}°)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -614,7 +616,7 @@ export function MapView({
|
|||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
onClose={() => setPopupInfo(null)}
|
onClose={() => setPopupInfo(null)}
|
||||||
>
|
>
|
||||||
<div style={{ color: '#333' }}>{popupInfo.content}</div>
|
<div className="text-[#333]">{popupInfo.content}</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -670,7 +672,7 @@ function MapControls({ center, zoom }: { center: [number, number]; zoom: number
|
|||||||
const { current: map } = useMap()
|
const { current: map } = useMap()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'absolute', top: 16, left: 16, zIndex: 10 }}>
|
<div className="absolute top-4 left-4 z-10">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => map?.zoomIn()}
|
onClick={() => map?.zoomIn()}
|
||||||
@ -707,28 +709,28 @@ interface MapLegendProps {
|
|||||||
function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLines = [], selectedModels = new Set(['OpenDrift'] as PredictionModel[]) }: MapLegendProps) {
|
function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLines = [], selectedModels = new Set(['OpenDrift'] as PredictionModel[]) }: MapLegendProps) {
|
||||||
if (dispersionResult && incidentCoord) {
|
if (dispersionResult && incidentCoord) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-border rounded-lg p-3.5 min-w-[200px]" style={{ fontFamily: 'var(--fK)', zIndex: 20 }}>
|
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-border rounded-lg p-3.5 min-w-[200px] z-[20]">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '10px' }}>
|
<div className="flex items-center gap-1.5 mb-2.5">
|
||||||
<div style={{ fontSize: '16px' }}>📍</div>
|
<div className="text-base">📍</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-[11px] font-bold text-primary-orange">사고 위치</h4>
|
<h4 className="text-[11px] font-bold text-primary-orange">사고 위치</h4>
|
||||||
<div className="text-[8px] text-text-3" style={{ fontFamily: 'var(--fM)' }}>
|
<div className="text-[8px] text-text-3 font-mono">
|
||||||
{incidentCoord.lat.toFixed(4)}°N, {incidentCoord.lon.toFixed(4)}°E
|
{incidentCoord.lat.toFixed(4)}°N, {incidentCoord.lon.toFixed(4)}°E
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ background: 'rgba(249,115,22,0.08)', padding: '8px', borderRadius: '6px', marginBottom: '8px', fontSize: '9px', color: 'var(--t2)' }}>
|
<div className="text-[9px] text-text-2 mb-2 rounded" style={{ background: 'rgba(249,115,22,0.08)', padding: '8px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
|
<div className="flex justify-between mb-[3px]">
|
||||||
<span style={{ color: 'var(--t3)' }}>물질</span>
|
<span className="text-text-3">물질</span>
|
||||||
<span style={{ fontWeight: 600, color: 'var(--orange)' }}>{dispersionResult.substance}</span>
|
<span className="font-semibold text-status-orange">{dispersionResult.substance}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
|
<div className="flex justify-between mb-[3px]">
|
||||||
<span style={{ color: 'var(--t3)' }}>풍향</span>
|
<span className="text-text-3">풍향</span>
|
||||||
<span style={{ fontWeight: 600, fontFamily: 'var(--fM)' }}>SW {dispersionResult.windDirection}°</span>
|
<span className="font-semibold font-mono">SW {dispersionResult.windDirection}°</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div className="flex justify-between">
|
||||||
<span style={{ color: 'var(--t3)' }}>확산 구역</span>
|
<span className="text-text-3">확산 구역</span>
|
||||||
<span style={{ fontWeight: 600, color: 'var(--cyan)' }}>{dispersionResult.zones.length}개</span>
|
<span className="font-semibold text-primary-cyan">{dispersionResult.zones.length}개</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -748,8 +750,8 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: '8px', padding: '6px', background: 'rgba(168,85,247,0.08)', borderRadius: '4px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="flex items-center gap-1.5 mt-2 rounded" style={{ padding: '6px', background: 'rgba(168,85,247,0.08)' }}>
|
||||||
<div style={{ fontSize: '12px' }}>🧭</div>
|
<div className="text-xs">🧭</div>
|
||||||
<span className="text-[9px] text-text-3">풍향 (방사형)</span>
|
<span className="text-[9px] text-text-3">풍향 (방사형)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -758,7 +760,7 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
|||||||
|
|
||||||
if (oilTrajectory.length > 0) {
|
if (oilTrajectory.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-md p-3.5 min-w-[180px]" style={{ zIndex: 20 }}>
|
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-md p-3.5 min-w-[180px] z-[20]">
|
||||||
<h4 className="text-[11px] font-bold uppercase tracking-wider text-text-3 mb-2.5">범례</h4>
|
<h4 className="text-[11px] font-bold uppercase tracking-wider text-text-3 mb-2.5">범례</h4>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{Array.from(selectedModels).map(model => (
|
{Array.from(selectedModels).map(model => (
|
||||||
@ -768,28 +770,28 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{selectedModels.size === 3 && (
|
{selectedModels.size === 3 && (
|
||||||
<div className="flex items-center gap-2 text-xs text-text-3" style={{ fontSize: '9px' }}>
|
<div className="flex items-center gap-2 text-[9px] text-text-3">
|
||||||
<span className="font-korean">(앙상블 모드)</span>
|
<span className="font-korean">(앙상블 모드)</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ height: '1px', background: 'var(--bd)', margin: '4px 0' }} />
|
<div className="h-px bg-border my-1" />
|
||||||
<div className="flex items-center gap-2 text-xs text-text-2">
|
<div className="flex items-center gap-2 text-xs text-text-2">
|
||||||
<div className="w-3.5 h-3.5 rounded-full bg-primary-cyan" />
|
<div className="w-3.5 h-3.5 rounded-full bg-primary-cyan" />
|
||||||
<span className="font-korean">사고 지점</span>
|
<span className="font-korean">사고 지점</span>
|
||||||
</div>
|
</div>
|
||||||
{boomLines.length > 0 && (
|
{boomLines.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div style={{ height: '1px', background: 'var(--bd)', margin: '4px 0' }} />
|
<div className="h-px bg-border my-1" />
|
||||||
<div className="flex items-center gap-2 text-xs text-text-2">
|
<div className="flex items-center gap-2 text-xs text-text-2">
|
||||||
<div style={{ width: '14px', height: '3px', background: '#ef4444', borderRadius: '1px' }} />
|
<div className="w-[14px] h-[3px] bg-[#ef4444] rounded-[1px]" />
|
||||||
<span className="font-korean">긴급 오일펜스</span>
|
<span className="font-korean">긴급 오일펜스</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs text-text-2">
|
<div className="flex items-center gap-2 text-xs text-text-2">
|
||||||
<div style={{ width: '14px', height: '3px', background: '#f97316', borderRadius: '1px' }} />
|
<div className="w-[14px] h-[3px] bg-[#f97316] rounded-[1px]" />
|
||||||
<span className="font-korean">중요 오일펜스</span>
|
<span className="font-korean">중요 오일펜스</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs text-text-2">
|
<div className="flex items-center gap-2 text-xs text-text-2">
|
||||||
<div style={{ width: '14px', height: '3px', background: '#eab308', borderRadius: '1px' }} />
|
<div className="w-[14px] h-[3px] bg-[#eab308] rounded-[1px]" />
|
||||||
<span className="font-korean">보통 오일펜스</span>
|
<span className="font-korean">보통 오일펜스</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -866,7 +868,7 @@ function TimelineControl({
|
|||||||
</div>
|
</div>
|
||||||
<div className="tb" onClick={handleForward}>▶▶</div>
|
<div className="tb" onClick={handleForward}>▶▶</div>
|
||||||
<div className="tb" onClick={handleEnd}>⏭</div>
|
<div className="tb" onClick={handleEnd}>⏭</div>
|
||||||
<div style={{ width: '8px' }} />
|
<div className="w-2" />
|
||||||
<div className="tb" onClick={toggleSpeed}>{playbackSpeed}×</div>
|
<div className="tb" onClick={toggleSpeed}>{playbackSpeed}×</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tlt">
|
<div className="tlt">
|
||||||
@ -948,25 +950,33 @@ function WeatherInfoPanel({ position }: { position: [number, number] }) {
|
|||||||
function BacktrackReplayBar({ replayFrame, totalFrames, ships }: { replayFrame: number; totalFrames: number; ships: ReplayShip[] }) {
|
function BacktrackReplayBar({ replayFrame, totalFrames, ships }: { replayFrame: number; totalFrames: number; ships: ReplayShip[] }) {
|
||||||
const progress = (replayFrame / totalFrames) * 100
|
const progress = (replayFrame / totalFrames) * 100
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute', bottom: 80, left: '50%', transform: 'translateX(-50%)',
|
className="absolute flex items-center gap-4"
|
||||||
background: 'rgba(10,14,26,0.92)', backdropFilter: 'blur(12px)',
|
style={{
|
||||||
border: '1px solid var(--bdL)', borderRadius: '10px',
|
bottom: 80, left: '50%', transform: 'translateX(-50%)',
|
||||||
padding: '12px 18px', zIndex: 50,
|
background: 'rgba(10,14,26,0.92)', backdropFilter: 'blur(12px)',
|
||||||
display: 'flex', alignItems: 'center', gap: '16px', minWidth: '340px',
|
border: '1px solid var(--bdL)', borderRadius: '10px',
|
||||||
}}>
|
padding: '12px 18px', zIndex: 50,
|
||||||
<div style={{ fontSize: '14px', color: 'var(--purple)', fontFamily: 'var(--fM)', fontWeight: 700 }}>
|
minWidth: '340px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-sm text-primary-purple font-mono font-bold">
|
||||||
{progress.toFixed(0)}%
|
{progress.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, height: '4px', background: 'var(--bd)', borderRadius: '2px', position: 'relative' }}>
|
<div className="flex-1 h-1 bg-border relative rounded-[2px]">
|
||||||
<div style={{ width: `${progress}%`, height: '100%', background: 'linear-gradient(90deg, var(--purple), var(--cyan))', borderRadius: '2px', transition: 'width 0.05s' }} />
|
<div
|
||||||
|
className="h-full rounded-[2px]"
|
||||||
|
style={{ width: `${progress}%`, background: 'linear-gradient(90deg, var(--purple), var(--cyan))', transition: 'width 0.05s' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '6px' }}>
|
<div className="flex gap-1.5">
|
||||||
{ships.map(s => (
|
{ships.map(s => (
|
||||||
<div key={s.vesselName} style={{
|
<div
|
||||||
width: 8, height: 8, borderRadius: '50%', background: s.color,
|
key={s.vesselName}
|
||||||
border: '1px solid rgba(255,255,255,0.3)',
|
className="w-2 h-2 rounded-full border border-white/30"
|
||||||
}} title={s.vesselName} />
|
style={{ background: s.color }}
|
||||||
|
title={s.vesselName}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,44 +32,34 @@ export function ComboBox({ value, onChange, options, placeholder, className }: C
|
|||||||
const displayText = selectedOption?.label || placeholder || '선택'
|
const displayText = selectedOption?.label || placeholder || '선택'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} style={{ position: 'relative' }}>
|
<div ref={containerRef} className="relative">
|
||||||
<div
|
<div
|
||||||
className={className}
|
className={`cursor-pointer flex items-center justify-between pr-2 ${className ?? ''}`}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingRight: '8px'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span>{displayText}</span>
|
<span>{displayText}</span>
|
||||||
<span style={{
|
<span
|
||||||
fontSize: '8px',
|
className="text-[8px] text-text-3"
|
||||||
color: 'var(--t3)',
|
style={{
|
||||||
transition: 'transform 0.2s',
|
transition: 'transform 0.2s',
|
||||||
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
|
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
▼
|
▼
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute',
|
className="absolute left-0 right-0 bg-bg-0 border border-border overflow-y-auto z-[1000]"
|
||||||
top: 'calc(100% + 2px)',
|
style={{
|
||||||
left: 0,
|
top: 'calc(100% + 2px)',
|
||||||
right: 0,
|
borderRadius: 'var(--rS)',
|
||||||
background: 'var(--bg0)',
|
maxHeight: '200px',
|
||||||
border: '1px solid var(--bd)',
|
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||||
borderRadius: 'var(--rS)',
|
animation: 'fadeSlideDown 0.15s ease-out'
|
||||||
maxHeight: '200px',
|
}}
|
||||||
overflowY: 'auto',
|
>
|
||||||
zIndex: 1000,
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
|
||||||
animation: 'fadeSlideDown 0.15s ease-out'
|
|
||||||
}}>
|
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<div
|
<div
|
||||||
key={option.value}
|
key={option.value}
|
||||||
@ -77,14 +67,12 @@ export function ComboBox({ value, onChange, options, placeholder, className }: C
|
|||||||
onChange(option.value)
|
onChange(option.value)
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}}
|
}}
|
||||||
|
className="text-[11px] cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 10px',
|
padding: '8px 10px',
|
||||||
fontSize: '11px',
|
|
||||||
color: option.value === String(value) ? 'var(--cyan)' : 'var(--t2)',
|
color: option.value === String(value) ? 'var(--cyan)' : 'var(--t2)',
|
||||||
background: option.value === String(value) ? 'rgba(6,182,212,0.1)' : 'transparent',
|
background: option.value === String(value) ? 'rgba(6,182,212,0.1)' : 'transparent',
|
||||||
cursor: 'pointer',
|
|
||||||
transition: '0.1s',
|
transition: '0.1s',
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
borderLeft: option.value === String(value) ? '2px solid var(--cyan)' : '2px solid transparent'
|
borderLeft: option.value === String(value) ? '2px solid var(--cyan)' : '2px solid transparent'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
|
|||||||
54
frontend/src/common/styles/base.css
Normal file
54
frontend/src/common/styles/base.css
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--bg0: #0a0e1a;
|
||||||
|
--bg1: #0f1524;
|
||||||
|
--bg2: #121929;
|
||||||
|
--bg3: #1a2236;
|
||||||
|
--bgH: #1e2844;
|
||||||
|
--bd: #1e2a42;
|
||||||
|
--bdL: #2a3a5c;
|
||||||
|
--t1: #edf0f7;
|
||||||
|
--t2: #b0b8cc;
|
||||||
|
--t3: #8690a6;
|
||||||
|
--blue: #3b82f6;
|
||||||
|
--cyan: #06b6d4;
|
||||||
|
--red: #ef4444;
|
||||||
|
--orange: #f97316;
|
||||||
|
--yellow: #eab308;
|
||||||
|
--green: #22c55e;
|
||||||
|
--purple: #a855f7;
|
||||||
|
--boom: #f59e0b;
|
||||||
|
--boomH: #fbbf24;
|
||||||
|
--fK: Noto Sans KR, sans-serif;
|
||||||
|
--fM: JetBrains Mono, monospace;
|
||||||
|
--rS: 6px;
|
||||||
|
--rM: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Outfit', 'Noto Sans KR', sans-serif;
|
||||||
|
background: var(--bg0);
|
||||||
|
color: var(--t1);
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date input calendar icon — white for dark theme */
|
||||||
|
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||||
|
filter: invert(1);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
1224
frontend/src/common/styles/components.css
Normal file
1224
frontend/src/common/styles/components.css
Normal file
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
242
frontend/src/common/styles/wing.css
Normal file
242
frontend/src/common/styles/wing.css
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
/* ═══ WING Design System ═══ */
|
||||||
|
/* 프로젝트 전체에서 공유하는 공통 컴포넌트 클래스 */
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* ── Layout ── */
|
||||||
|
.wing-panel {
|
||||||
|
@apply flex flex-col h-full overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-panel-scroll {
|
||||||
|
@apply flex-1 overflow-y-auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--bdL) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-header-bar {
|
||||||
|
@apply flex items-center justify-between shrink-0 px-5 border-b border-border;
|
||||||
|
padding-top: 14px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-sidebar {
|
||||||
|
@apply flex flex-col border-r border-border;
|
||||||
|
background: var(--bg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card / Section ── */
|
||||||
|
.wing-card {
|
||||||
|
@apply rounded-md p-4 border border-border;
|
||||||
|
background: var(--bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-card-sm {
|
||||||
|
@apply rounded-sm p-3 border border-border;
|
||||||
|
background: var(--bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-section {
|
||||||
|
@apply rounded-md p-4 mb-3 border border-border;
|
||||||
|
background: var(--bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-section-header {
|
||||||
|
@apply text-[13px] font-bold font-korean mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-section-desc {
|
||||||
|
@apply text-[10px] font-korean leading-relaxed;
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Typography ── */
|
||||||
|
.wing-title {
|
||||||
|
@apply text-[15px] font-bold font-korean;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-subtitle {
|
||||||
|
@apply text-[10px] font-korean mt-0.5;
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-label {
|
||||||
|
@apply text-[11px] font-semibold font-korean;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-value {
|
||||||
|
@apply text-[11px] font-mono font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-meta {
|
||||||
|
@apply text-[9px] font-korean;
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Icon Badge ── */
|
||||||
|
.wing-icon-badge {
|
||||||
|
@apply w-10 h-10 rounded-md flex items-center justify-center text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-icon-badge-sm {
|
||||||
|
@apply w-[38px] h-[38px] rounded-[9px] flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Badge ── */
|
||||||
|
.wing-badge {
|
||||||
|
@apply inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold font-korean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Button ── */
|
||||||
|
.wing-btn {
|
||||||
|
@apply px-3 py-1.5 rounded-sm text-[11px] font-semibold cursor-pointer font-korean border-none;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--cyan), var(--blue));
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-primary:hover {
|
||||||
|
box-shadow: 0 0 16px rgba(6, 182, 212, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-secondary {
|
||||||
|
@apply border border-border;
|
||||||
|
background: var(--bg3);
|
||||||
|
color: var(--t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-secondary:hover {
|
||||||
|
background: var(--bgH);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-outline {
|
||||||
|
@apply bg-transparent border border-border;
|
||||||
|
color: var(--t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-outline:hover {
|
||||||
|
background: var(--bgH);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-pdf {
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-btn-danger {
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Input ── */
|
||||||
|
.wing-input {
|
||||||
|
@apply w-full rounded-sm text-[11px] font-korean outline-none;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--bg0);
|
||||||
|
border: 1px solid var(--bd);
|
||||||
|
color: var(--t1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-input:focus {
|
||||||
|
border-color: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-input::placeholder {
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Table ── */
|
||||||
|
.wing-table {
|
||||||
|
@apply w-full text-[10px] font-korean;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-th {
|
||||||
|
@apply text-left font-semibold;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--t3);
|
||||||
|
background: var(--bg2);
|
||||||
|
border-bottom: 1px solid var(--bd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--t2);
|
||||||
|
border-bottom: 1px solid var(--bd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-tr-hover:hover {
|
||||||
|
background: var(--bgH);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab Bar ── */
|
||||||
|
.wing-tab-bar {
|
||||||
|
@apply flex gap-0.5 rounded-lg p-1 border border-border;
|
||||||
|
background: var(--bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-tab {
|
||||||
|
@apply flex-1 py-2 px-1 text-xs font-semibold rounded-md text-center cursor-pointer font-korean;
|
||||||
|
transition: all 0.15s;
|
||||||
|
color: var(--t3);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-tab:hover {
|
||||||
|
color: var(--t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-tab.active {
|
||||||
|
border-color: rgba(6, 182, 212, 0.3);
|
||||||
|
background: rgba(6, 182, 212, 0.08);
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modal ── */
|
||||||
|
.wing-overlay {
|
||||||
|
@apply fixed inset-0 flex items-center justify-center;
|
||||||
|
z-index: 10000;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-modal {
|
||||||
|
@apply rounded-xl overflow-hidden;
|
||||||
|
background: var(--bg1);
|
||||||
|
border: 1px solid var(--bd);
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-modal-header {
|
||||||
|
@apply flex items-center justify-between px-5 border-b border-border;
|
||||||
|
padding-top: 14px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Utility ── */
|
||||||
|
.wing-divider {
|
||||||
|
@apply w-full;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--bd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-kv-row {
|
||||||
|
@apply flex items-center justify-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-kv-label {
|
||||||
|
@apply text-[10px] font-korean;
|
||||||
|
color: var(--t3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wing-kv-value {
|
||||||
|
@apply text-[11px] font-semibold font-mono;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/src/common/utils/cn.ts
Normal file
7
frontend/src/common/utils/cn.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* className 조합 유틸리티
|
||||||
|
* falsy 값을 필터링하여 className 문자열을 생성한다.
|
||||||
|
*/
|
||||||
|
export function cn(...classes: (string | false | null | undefined)[]): string {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -28,32 +28,30 @@ export function AerialTheoryView() {
|
|||||||
const [activePanel, setActivePanel] = useState(0)
|
const [activePanel, setActivePanel] = useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%', flex: 1, overflow: 'hidden', background: 'var(--bg0)' }}>
|
<div className="flex flex-col h-full w-full flex-1 overflow-hidden bg-bg-0">
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '20px 24px' }}>
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px' }}>
|
<div className="flex items-center justify-between mb-5">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div className="flex items-center gap-3">
|
||||||
<div style={{ width: '42px', height: '42px', borderRadius: '10px', background: 'linear-gradient(135deg,rgba(249,115,22,.2),rgba(234,179,8,.15))', border: '1px solid rgba(249,115,22,.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '20px' }}>📐</div>
|
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center text-xl border" style={{ background: 'linear-gradient(135deg,rgba(249,115,22,.2),rgba(234,179,8,.15))', borderColor: 'rgba(249,115,22,.3)' }}>📐</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '16px', fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>해양 항공탐색 · 원격탐사 이론</div>
|
<div className="text-base font-bold">해양 항공탐색 · 원격탐사 이론</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>유출유 원격탐지 · 항공감시 기법 · ESI 방제정보지도 · 등록특허 10-1567431 기반</div>
|
<div className="text-[10px] text-text-3 mt-0.5">유출유 원격탐지 · 항공감시 기법 · ESI 방제정보지도 · 등록특허 10-1567431 기반</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 내부 네비게이션 */}
|
{/* 내부 네비게이션 */}
|
||||||
<div style={{ display: 'flex', gap: '3px', background: 'var(--bg3)', borderRadius: '8px', padding: '4px', marginBottom: '20px', border: '1px solid var(--bd)' }}>
|
<div className="flex gap-[3px] bg-bg-3 rounded-lg p-1 mb-5 border border-border">
|
||||||
{panels.map(p => (
|
{panels.map(p => (
|
||||||
<button
|
<button
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => setActivePanel(p.id)}
|
onClick={() => setActivePanel(p.id)}
|
||||||
style={{
|
className={`flex-1 py-2 text-[10px] rounded-md border-none cursor-pointer transition-colors ${
|
||||||
flex: 1, padding: '8px', fontSize: '10px', fontFamily: 'var(--fK)',
|
activePanel === p.id
|
||||||
borderRadius: '6px', border: 'none', cursor: 'pointer',
|
? 'bg-[rgba(6,182,212,.15)] text-primary-cyan font-bold'
|
||||||
background: activePanel === p.id ? 'rgba(6,182,212,.15)' : 'transparent',
|
: 'bg-transparent text-text-3 font-normal'
|
||||||
color: activePanel === p.id ? 'var(--cyan)' : 'var(--t3)',
|
}`}
|
||||||
fontWeight: activePanel === p.id ? 700 : 400,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{p.icon} {p.label}
|
{p.icon} {p.label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export function CctvView() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full overflow-hidden" style={{ margin: '-20px -24px', height: 'calc(100% + 40px)' }}>
|
<div className="flex h-full overflow-hidden" style={{ margin: '-20px -24px', height: 'calc(100% + 40px)' }}>
|
||||||
{/* 왼쪽: 목록 패널 */}
|
{/* 왼쪽: 목록 패널 */}
|
||||||
<div className="flex flex-col overflow-hidden bg-bg-1 border-r border-border" style={{ width: 290, minWidth: 290 }}>
|
<div className="flex flex-col overflow-hidden bg-bg-1 border-r border-border w-[290px] min-w-[290px]">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="p-3 pb-2.5 border-b border-border shrink-0 bg-bg-2">
|
<div className="p-3 pb-2.5 border-b border-border shrink-0 bg-bg-2">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
@ -142,7 +142,7 @@ export function CctvView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 가운데: 영상 뷰어 */}
|
{/* 가운데: 영상 뷰어 */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0" style={{ background: '#04070f' }}>
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-[#04070f]">
|
||||||
{/* 뷰어 툴바 */}
|
{/* 뷰어 툴바 */}
|
||||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-bg-2 shrink-0 gap-2.5">
|
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-bg-2 shrink-0 gap-2.5">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
@ -193,26 +193,25 @@ export function CctvView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 영상 그리드 */}
|
{/* 영상 그리드 */}
|
||||||
<div className="flex-1 gap-0.5 p-0.5 overflow-hidden relative" style={{
|
<div className="flex-1 gap-0.5 p-0.5 overflow-hidden relative grid bg-black"
|
||||||
display: 'grid',
|
style={{
|
||||||
gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
|
gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
|
||||||
gridTemplateRows: `repeat(${gridCols}, 1fr)`,
|
gridTemplateRows: `repeat(${gridCols}, 1fr)`,
|
||||||
background: '#000',
|
}}>
|
||||||
}}>
|
|
||||||
{Array.from({ length: totalCells }).map((_, i) => {
|
{Array.from({ length: totalCells }).map((_, i) => {
|
||||||
const cam = activeCells[i]
|
const cam = activeCells[i]
|
||||||
return (
|
return (
|
||||||
<div key={i} className="relative flex items-center justify-center overflow-hidden" style={{ background: '#0a0e18', border: '1px solid rgba(255,255,255,.06)' }}>
|
<div key={i} className="relative flex items-center justify-center overflow-hidden bg-[#0a0e18]" style={{ border: '1px solid rgba(255,255,255,.06)' }}>
|
||||||
{cam ? (
|
{cam ? (
|
||||||
<>
|
<>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-4xl opacity-20">📹</div>
|
<div className="text-4xl opacity-20">📹</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-2 left-2 flex items-center gap-1.5">
|
<div className="absolute top-2 left-2 flex items-center gap-1.5">
|
||||||
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(0,0,0,.7)', color: 'var(--t1)' }}>{cam.cameraNm}</span>
|
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-black/70">{cam.cameraNm}</span>
|
||||||
<span className="text-[8px] font-bold px-1 py-0.5 rounded" style={{ background: 'rgba(239,68,68,.3)', color: '#f87171' }}>● REC</span>
|
<span className="text-[8px] font-bold px-1 py-0.5 rounded text-[#f87171]" style={{ background: 'rgba(239,68,68,.3)' }}>● REC</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-2 left-2 text-[9px] font-mono px-1.5 py-0.5 rounded" style={{ background: 'rgba(0,0,0,.7)', color: 'var(--t3)' }}>
|
<div className="absolute bottom-2 left-2 text-[9px] font-mono px-1.5 py-0.5 rounded text-text-3 bg-black/70">
|
||||||
{cam.coordDc ?? ''} · {cam.sourceNm ?? ''}
|
{cam.coordDc ?? ''} · {cam.sourceNm ?? ''}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 flex items-center justify-center text-[11px] font-korean text-text-3 opacity-60">
|
<div className="absolute inset-0 flex items-center justify-center text-[11px] font-korean text-text-3 opacity-60">
|
||||||
@ -237,14 +236,14 @@ export function CctvView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 오른쪽: 미니맵 + 정보 */}
|
{/* 오른쪽: 미니맵 + 정보 */}
|
||||||
<div className="flex flex-col overflow-hidden bg-bg-1 border-l border-border" style={{ width: 232, minWidth: 232 }}>
|
<div className="flex flex-col overflow-hidden bg-bg-1 border-l border-border w-[232px] min-w-[232px]">
|
||||||
{/* 지도 헤더 */}
|
{/* 지도 헤더 */}
|
||||||
<div className="px-3 py-2 border-b border-border text-[11px] font-bold text-text-1 font-korean bg-bg-2 shrink-0 flex items-center justify-between">
|
<div className="px-3 py-2 border-b border-border text-[11px] font-bold text-text-1 font-korean bg-bg-2 shrink-0 flex items-center justify-between">
|
||||||
<span>🗺 위치 지도</span>
|
<span>🗺 위치 지도</span>
|
||||||
<span className="text-[9px] text-text-3 font-normal">클릭하여 선택</span>
|
<span className="text-[9px] text-text-3 font-normal">클릭하여 선택</span>
|
||||||
</div>
|
</div>
|
||||||
{/* 미니맵 (placeholder) */}
|
{/* 미니맵 (placeholder) */}
|
||||||
<div className="w-full bg-bg-3 flex items-center justify-center shrink-0 relative" style={{ height: 210 }}>
|
<div className="w-full bg-bg-3 flex items-center justify-center shrink-0 relative h-[210px]">
|
||||||
<div className="text-[10px] text-text-3 font-korean opacity-50">지도 영역</div>
|
<div className="text-[10px] text-text-3 font-korean opacity-50">지도 영역</div>
|
||||||
{/* 간략 지도 표현 */}
|
{/* 간략 지도 표현 */}
|
||||||
<div className="absolute inset-2 rounded-md border border-border/30 overflow-hidden" style={{ background: 'linear-gradient(180deg, rgba(6,182,212,.03), rgba(59,130,246,.05))' }}>
|
<div className="absolute inset-2 rounded-md border border-border/30 overflow-hidden" style={{ background: 'linear-gradient(180deg, rgba(6,182,212,.03), rgba(59,130,246,.05))' }}>
|
||||||
@ -327,7 +326,7 @@ export function CctvView() {
|
|||||||
))}
|
))}
|
||||||
<div className="flex items-center justify-between px-2 py-1 bg-bg-3 rounded-[5px]" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
|
<div className="flex items-center justify-between px-2 py-1 bg-bg-3 rounded-[5px]" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
|
||||||
<span className="text-[9px] text-text-2 font-korean">갱신 주기</span>
|
<span className="text-[9px] text-text-2 font-korean">갱신 주기</span>
|
||||||
<span className="text-[9px] font-bold font-mono" style={{ color: 'var(--blue)' }}>1 fps</span>
|
<span className="text-[9px] font-bold font-mono text-primary-blue">1 fps</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-text-3 font-mono text-right mt-0.5">최종갱신: {new Date().toLocaleTimeString('ko-KR')}</div>
|
<div className="text-[9px] text-text-3 font-mono text-right mt-0.5">최종갱신: {new Date().toLocaleTimeString('ko-KR')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -466,7 +466,7 @@ export function RealtimeDrone() {
|
|||||||
zoom: 10,
|
zoom: 10,
|
||||||
}}
|
}}
|
||||||
mapStyle={BASE_STYLE}
|
mapStyle={BASE_STYLE}
|
||||||
style={{ width: '100%', height: '100%' }}
|
className="w-full h-full"
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<DeckGLOverlay layers={deckLayers} />
|
<DeckGLOverlay layers={deckLayers} />
|
||||||
|
|||||||
@ -128,8 +128,8 @@ export function SatelliteRequest() {
|
|||||||
|
|
||||||
// ── 섹션 헤더 헬퍼 (BlackSky 폼) ──
|
// ── 섹션 헤더 헬퍼 (BlackSky 폼) ──
|
||||||
const sectionHeader = (num: number, label: string) => (
|
const sectionHeader = (num: number, label: string) => (
|
||||||
<div className="text-[11px] font-bold font-korean mb-2.5 flex items-center gap-1.5" style={{ color: '#818cf8' }}>
|
<div className="text-[11px] font-bold font-korean mb-2.5 flex items-center gap-1.5 text-[#818cf8]">
|
||||||
<div className="w-[18px] h-[18px] rounded-[5px] flex items-center justify-center text-[9px] font-bold" style={{ background: 'rgba(99,102,241,.12)', color: '#818cf8' }}>{num}</div>
|
<div className="w-[18px] h-[18px] rounded-[5px] flex items-center justify-center text-[9px] font-bold text-[#818cf8]" style={{ background: 'rgba(99,102,241,.12)' }}>{num}</div>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -138,7 +138,7 @@ export function SatelliteRequest() {
|
|||||||
const bsInputStyle = { border: '1px solid #21262d', background: '#161b22', color: '#e2e8f0' }
|
const bsInputStyle = { border: '1px solid #21262d', background: '#161b22', color: '#e2e8f0' }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-auto" style={{ padding: '20px 24px' }}>
|
<div className="overflow-y-auto py-5 px-6">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@ -321,8 +321,8 @@ export function SatelliteRequest() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)', borderColor: 'rgba(99,102,241,.3)' }}>
|
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border border-[rgba(99,102,241,.3)]" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)' }}>
|
||||||
<span className="text-[11px] font-extrabold font-mono" style={{ color: '#818cf8', letterSpacing: '-.5px' }}>B<span style={{ color: '#a78bfa' }}>Sky</span></span>
|
<span className="text-[11px] font-extrabold font-mono text-[#818cf8] tracking-[-0.5px]">B<span className="text-[#a78bfa]">Sky</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-bold text-text-1 font-korean">BlackSky</div>
|
<div className="text-sm font-bold text-text-1 font-korean">BlackSky</div>
|
||||||
@ -330,7 +330,7 @@ export function SatelliteRequest() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)', color: '#22c55e' }}>API 연결됨</span>
|
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API 연결됨</span>
|
||||||
<span className="text-base text-text-3">→</span>
|
<span className="text-base text-text-3">→</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -348,7 +348,7 @@ export function SatelliteRequest() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-text-3 font-korean leading-relaxed">고빈도 소형위성 군집 기반 긴급 촬영. 해양 사고 현장 신속 모니터링에 최적화. Dawn-to-Dusk 촬영 가능.</div>
|
<div className="text-[9px] text-text-3 font-korean leading-relaxed">고빈도 소형위성 군집 기반 긴급 촬영. 해양 사고 현장 신속 모니터링에 최적화. Dawn-to-Dusk 촬영 가능.</div>
|
||||||
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span style={{ color: '#818cf8' }}>eapi.maxar.com/e1so/rapidoc</span></div>
|
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#818cf8]">eapi.maxar.com/e1so/rapidoc</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* UP42 (EO + SAR) */}
|
{/* UP42 (EO + SAR) */}
|
||||||
@ -358,8 +358,8 @@ export function SatelliteRequest() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)', borderColor: 'rgba(59,130,246,.3)' }}>
|
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border border-[rgba(59,130,246,.3)]" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)' }}>
|
||||||
<span className="text-[13px] font-extrabold font-mono" style={{ color: '#60a5fa', letterSpacing: '-.5px' }}>up<sup className="text-[7px] align-super">42</sup></span>
|
<span className="text-[13px] font-extrabold font-mono text-[#60a5fa] tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-bold text-text-1 font-korean">UP42 — EO + SAR</div>
|
<div className="text-sm font-bold text-text-1 font-korean">UP42 — EO + SAR</div>
|
||||||
@ -367,7 +367,7 @@ export function SatelliteRequest() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)', color: '#22c55e' }}>API 연결됨</span>
|
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API 연결됨</span>
|
||||||
<span className="text-base text-text-3">→</span>
|
<span className="text-base text-text-3">→</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -386,7 +386,7 @@ export function SatelliteRequest() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1.5 mb-2.5 flex-wrap">
|
<div className="flex gap-1.5 mb-2.5 flex-wrap">
|
||||||
{['Pléiades Neo', 'SPOT 6/7'].map((t, i) => (
|
{['Pléiades Neo', 'SPOT 6/7'].map((t, i) => (
|
||||||
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(59,130,246,.08)', border: '1px solid rgba(59,130,246,.15)', color: '#60a5fa' }}>{t}</span>
|
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.08)', border: '1px solid rgba(59,130,246,.15)' }}>{t}</span>
|
||||||
))}
|
))}
|
||||||
{['TerraSAR-X', 'Capella SAR', 'ICEYE'].map((t, i) => (
|
{['TerraSAR-X', 'Capella SAR', 'ICEYE'].map((t, i) => (
|
||||||
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.15)', color: 'var(--cyan)' }}>{t}</span>
|
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.15)', color: 'var(--cyan)' }}>{t}</span>
|
||||||
@ -394,36 +394,36 @@ export function SatelliteRequest() {
|
|||||||
<span className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(139,148,158,.08)', border: '1px solid rgba(139,148,158,.15)', color: 'var(--t3)' }}>+11 more</span>
|
<span className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(139,148,158,.08)', border: '1px solid rgba(139,148,158,.15)', color: 'var(--t3)' }}>+11 more</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-text-3 font-korean leading-relaxed">광학(EO) + 합성개구레이더(SAR) 통합 마켓. 야간·악천후 시 SAR 활용. 다중 위성 소스 자동 최적 선택.</div>
|
<div className="text-[9px] text-text-3 font-korean leading-relaxed">광학(EO) + 합성개구레이더(SAR) 통합 마켓. 야간·악천후 시 SAR 활용. 다중 위성 소스 자동 최적 선택.</div>
|
||||||
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span style={{ color: '#60a5fa' }}>up42.com</span></div>
|
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#60a5fa]">up42.com</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 하단 */}
|
{/* 하단 */}
|
||||||
<div className="px-7 pb-5 flex items-center justify-between">
|
<div className="px-7 pb-5 flex items-center justify-between">
|
||||||
<div className="text-[9px] text-text-3 font-korean leading-relaxed">💡 긴급 촬영: BlackSky 권장 (90분 납기) · 야간/악천후: UP42 SAR 권장</div>
|
<div className="text-[9px] text-text-3 font-korean leading-relaxed">💡 긴급 촬영: BlackSky 권장 (90분 납기) · 야간/악천후: UP42 SAR 권장</div>
|
||||||
<button onClick={() => setModalPhase('none')} className="px-4 py-2 rounded-lg border text-[11px] font-semibold cursor-pointer font-korean" style={{ borderColor: 'var(--bd)', background: 'var(--bg3)', color: 'var(--t2)' }}>닫기</button>
|
<button onClick={() => setModalPhase('none')} className="px-4 py-2 rounded-lg border text-[11px] font-semibold cursor-pointer font-korean bg-bg-3 text-text-2 border-border">닫기</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── BlackSky 긴급 촬영 요청 ── */}
|
{/* ── BlackSky 긴급 촬영 요청 ── */}
|
||||||
{modalPhase === 'blacksky' && (
|
{modalPhase === 'blacksky' && (
|
||||||
<div className="border rounded-[14px] w-[860px] max-h-[90vh] flex flex-col overflow-hidden" style={{ background: '#0d1117', borderColor: 'rgba(99,102,241,.3)', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
<div className="border rounded-[14px] w-[860px] max-h-[90vh] flex flex-col overflow-hidden border-[rgba(99,102,241,.3)]" style={{ background: '#0d1117', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="px-6 py-4 border-b flex items-center justify-between shrink-0 relative" style={{ borderColor: '#21262d' }}>
|
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
|
||||||
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#6366f1,#818cf8,#a78bfa)' }} />
|
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#6366f1,#818cf8,#a78bfa)' }} />
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)', borderColor: 'rgba(99,102,241,.3)' }}>
|
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-[rgba(99,102,241,.3)]" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)' }}>
|
||||||
<span className="text-[10px] font-extrabold font-mono" style={{ color: '#818cf8' }}>B<span style={{ color: '#a78bfa' }}>Sky</span></span>
|
<span className="text-[10px] font-extrabold font-mono text-[#818cf8]">B<span className="text-[#a78bfa]">Sky</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[15px] font-bold font-korean" style={{ color: '#e2e8f0' }}>BlackSky — 긴급 위성 촬영 요청</div>
|
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]">BlackSky — 긴급 위성 촬영 요청</div>
|
||||||
<div className="text-[9px] font-korean mt-0.5" style={{ color: '#64748b' }}>Maxar E1SO RapiDoc API · 고빈도 긴급 태스킹</div>
|
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]">Maxar E1SO RapiDoc API · 고빈도 긴급 태스킹</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="px-3 py-1 rounded-md text-[9px] font-semibold font-korean" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.25)', color: '#818cf8' }}>API Docs ↗</span>
|
<span className="px-3 py-1 rounded-md text-[9px] font-semibold font-korean text-[#818cf8]" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.25)' }}>API Docs ↗</span>
|
||||||
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none" style={{ color: '#64748b' }}>✕</button>
|
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-[#64748b]">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -432,9 +432,9 @@ export function SatelliteRequest() {
|
|||||||
{/* API 상태 */}
|
{/* API 상태 */}
|
||||||
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg" style={{ background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.15)' }}>
|
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg" style={{ background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.15)' }}>
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e', boxShadow: '0 0 6px rgba(34,197,94,.5)' }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e', boxShadow: '0 0 6px rgba(34,197,94,.5)' }} />
|
||||||
<span className="text-[10px] font-semibold font-korean" style={{ color: '#22c55e' }}>API Connected</span>
|
<span className="text-[10px] font-semibold font-korean text-green-500">API Connected</span>
|
||||||
<span className="text-[9px] font-mono" style={{ color: '#64748b' }}>eapi.maxar.com/e1so/rapidoc · Latency: 142ms</span>
|
<span className="text-[9px] font-mono text-[#64748b]">eapi.maxar.com/e1so/rapidoc · Latency: 142ms</span>
|
||||||
<span className="ml-auto text-[8px] font-mono" style={{ color: '#64748b' }}>Quota: 47/50 요청 잔여</span>
|
<span className="ml-auto text-[8px] font-mono text-[#64748b]">Quota: 47/50 요청 잔여</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ① 태스킹 유형 */}
|
{/* ① 태스킹 유형 */}
|
||||||
@ -442,7 +442,7 @@ export function SatelliteRequest() {
|
|||||||
{sectionHeader(1, '태스킹 유형 · 우선순위')}
|
{sectionHeader(1, '태스킹 유형 · 우선순위')}
|
||||||
<div className="grid grid-cols-3 gap-2.5">
|
<div className="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 유형 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 유형 <span className="text-red-400">*</span></label>
|
||||||
<select className={bsInput} style={bsInputStyle}>
|
<select className={bsInput} style={bsInputStyle}>
|
||||||
<option>긴급 태스킹 (Emergency)</option>
|
<option>긴급 태스킹 (Emergency)</option>
|
||||||
<option>표준 태스킹 (Standard)</option>
|
<option>표준 태스킹 (Standard)</option>
|
||||||
@ -450,7 +450,7 @@ export function SatelliteRequest() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>우선순위 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">우선순위 <span className="text-red-400">*</span></label>
|
||||||
<select className={bsInput} style={bsInputStyle}>
|
<select className={bsInput} style={bsInputStyle}>
|
||||||
<option>P1 — 긴급 (90분 내)</option>
|
<option>P1 — 긴급 (90분 내)</option>
|
||||||
<option>P2 — 높음 (6시간 내)</option>
|
<option>P2 — 높음 (6시간 내)</option>
|
||||||
@ -458,7 +458,7 @@ export function SatelliteRequest() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 모드</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 모드</label>
|
||||||
<select className={bsInput} style={bsInputStyle}>
|
<select className={bsInput} style={bsInputStyle}>
|
||||||
<option>Single Collect</option>
|
<option>Single Collect</option>
|
||||||
<option>Multi-pass Monitoring</option>
|
<option>Multi-pass Monitoring</option>
|
||||||
@ -473,26 +473,26 @@ export function SatelliteRequest() {
|
|||||||
{sectionHeader(2, '관심 영역 (AOI)')}
|
{sectionHeader(2, '관심 영역 (AOI)')}
|
||||||
<div className="grid grid-cols-3 gap-2.5 items-end">
|
<div className="grid grid-cols-3 gap-2.5 items-end">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>중심 위도 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">중심 위도 <span className="text-red-400">*</span></label>
|
||||||
<input type="text" defaultValue="34.5832" className={bsInput} style={bsInputStyle} />
|
<input type="text" defaultValue="34.5832" className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>중심 경도 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">중심 경도 <span className="text-red-400">*</span></label>
|
||||||
<input type="text" defaultValue="128.4217" className={bsInput} style={bsInputStyle} />
|
<input type="text" defaultValue="128.4217" className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
<button className="px-3.5 py-2 rounded-md text-[10px] font-semibold cursor-pointer font-korean whitespace-nowrap" style={{ border: '1px solid rgba(99,102,241,.3)', background: 'rgba(99,102,241,.08)', color: '#818cf8' }}>📍 지도에서 AOI 그리기</button>
|
<button className="px-3.5 py-2 rounded-md text-[10px] font-semibold cursor-pointer font-korean whitespace-nowrap text-[#818cf8]" style={{ border: '1px solid rgba(99,102,241,.3)', background: 'rgba(99,102,241,.08)' }}>📍 지도에서 AOI 그리기</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 grid grid-cols-3 gap-2.5">
|
<div className="mt-2 grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>AOI 반경 (km)</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">AOI 반경 (km)</label>
|
||||||
<input type="number" defaultValue={10} step={1} min={1} className={bsInput} style={bsInputStyle} />
|
<input type="number" defaultValue={10} step={1} min={1} className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>최대 구름량 (%)</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">최대 구름량 (%)</label>
|
||||||
<input type="number" defaultValue={20} step={5} min={0} max={100} className={bsInput} style={bsInputStyle} />
|
<input type="number" defaultValue={20} step={5} min={0} max={100} className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>최대 Off-nadir (°)</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">최대 Off-nadir (°)</label>
|
||||||
<input type="number" defaultValue={25} step={5} min={0} max={45} className={bsInput} style={bsInputStyle} />
|
<input type="number" defaultValue={25} step={5} min={0} max={45} className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -503,15 +503,15 @@ export function SatelliteRequest() {
|
|||||||
{sectionHeader(3, '촬영 기간 · 반복')}
|
{sectionHeader(3, '촬영 기간 · 반복')}
|
||||||
<div className="grid grid-cols-3 gap-2.5">
|
<div className="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 시작 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 시작 <span className="text-red-400">*</span></label>
|
||||||
<input type="datetime-local" defaultValue="2026-02-26T08:00" className={bsInput} style={bsInputStyle} />
|
<input type="datetime-local" defaultValue="2026-02-26T08:00" className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 종료 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 종료 <span className="text-red-400">*</span></label>
|
||||||
<input type="datetime-local" defaultValue="2026-02-27T20:00" className={bsInput} style={bsInputStyle} />
|
<input type="datetime-local" defaultValue="2026-02-27T20:00" className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>반복 촬영</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">반복 촬영</label>
|
||||||
<select className={bsInput} style={bsInputStyle}>
|
<select className={bsInput} style={bsInputStyle}>
|
||||||
<option>1회 (단일)</option>
|
<option>1회 (단일)</option>
|
||||||
<option>매 패스 (가용 시)</option>
|
<option>매 패스 (가용 시)</option>
|
||||||
@ -528,7 +528,7 @@ export function SatelliteRequest() {
|
|||||||
{sectionHeader(4, '산출물 설정')}
|
{sectionHeader(4, '산출물 설정')}
|
||||||
<div className="grid grid-cols-2 gap-2.5">
|
<div className="grid grid-cols-2 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>산출물 형식 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">산출물 형식 <span className="text-red-400">*</span></label>
|
||||||
<select className={bsInput} style={bsInputStyle}>
|
<select className={bsInput} style={bsInputStyle}>
|
||||||
<option>Ortho-Rectified (정사보정)</option>
|
<option>Ortho-Rectified (정사보정)</option>
|
||||||
<option>Pan-sharpened (팬샤프닝)</option>
|
<option>Pan-sharpened (팬샤프닝)</option>
|
||||||
@ -536,7 +536,7 @@ export function SatelliteRequest() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>파일 포맷</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">파일 포맷</label>
|
||||||
<select className={bsInput} style={bsInputStyle}>
|
<select className={bsInput} style={bsInputStyle}>
|
||||||
<option>GeoTIFF</option>
|
<option>GeoTIFF</option>
|
||||||
<option>NITF</option>
|
<option>NITF</option>
|
||||||
@ -551,7 +551,7 @@ export function SatelliteRequest() {
|
|||||||
{ label: '변화탐지 (Change Detection)', checked: false },
|
{ label: '변화탐지 (Change Detection)', checked: false },
|
||||||
{ label: '웹훅 알림', checked: false },
|
{ label: '웹훅 알림', checked: false },
|
||||||
].map((opt, i) => (
|
].map((opt, i) => (
|
||||||
<label key={i} className="flex items-center gap-1 text-[9px] cursor-pointer font-korean" style={{ color: '#94a3b8' }}>
|
<label key={i} className="flex items-center gap-1 text-[9px] cursor-pointer font-korean text-[#94a3b8]">
|
||||||
<input type="checkbox" defaultChecked={opt.checked} style={{ accentColor: '#818cf8', transform: 'scale(.85)' }} /> {opt.label}
|
<input type="checkbox" defaultChecked={opt.checked} style={{ accentColor: '#818cf8', transform: 'scale(.85)' }} /> {opt.label}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@ -563,7 +563,7 @@ export function SatelliteRequest() {
|
|||||||
{sectionHeader(5, '연계 사고 · 비고')}
|
{sectionHeader(5, '연계 사고 · 비고')}
|
||||||
<div className="grid grid-cols-2 gap-2.5 mb-2">
|
<div className="grid grid-cols-2 gap-2.5 mb-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>연계 사고번호</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">연계 사고번호</label>
|
||||||
<select className={bsInput} style={bsInputStyle}>
|
<select className={bsInput} style={bsInputStyle}>
|
||||||
<option>OIL-2024-0892 · M/V STELLAR DAISY</option>
|
<option>OIL-2024-0892 · M/V STELLAR DAISY</option>
|
||||||
<option>HNS-2024-041 · 울산 온산항 톨루엔 유출</option>
|
<option>HNS-2024-041 · 울산 온산항 톨루엔 유출</option>
|
||||||
@ -572,24 +572,23 @@ export function SatelliteRequest() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>요청자</label>
|
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">요청자</label>
|
||||||
<input type="text" placeholder="소속 / 이름" className={bsInput} style={bsInputStyle} />
|
<input type="text" placeholder="소속 / 이름" className={bsInput} style={bsInputStyle} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="촬영 요청 목적, 특이사항, 관심 대상 등을 기록합니다..."
|
placeholder="촬영 요청 목적, 특이사항, 관심 대상 등을 기록합니다..."
|
||||||
className="w-full h-[50px] px-3 py-2.5 rounded-md text-[10px] font-korean outline-none resize-y leading-relaxed box-border"
|
className="w-full h-[50px] px-3 py-2.5 rounded-md text-[10px] font-korean outline-none resize-y leading-relaxed box-border border border-[#21262d] text-[#e2e8f0] bg-[#161b22]"
|
||||||
style={{ border: '1px solid #21262d', background: '#161b22', color: '#e2e8f0' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 하단 버튼 */}
|
{/* 하단 버튼 */}
|
||||||
<div className="px-6 py-3.5 border-t flex items-center gap-2 shrink-0" style={{ borderColor: '#21262d' }}>
|
<div className="px-6 py-3.5 border-t border-[#21262d] flex items-center gap-2 shrink-0">
|
||||||
<div className="flex-1 text-[9px] font-korean leading-relaxed" style={{ color: '#64748b' }}>
|
<div className="flex-1 text-[9px] font-korean leading-relaxed text-[#64748b]">
|
||||||
<span style={{ color: '#f87171' }}>*</span> 필수 항목 · 긴급 태스킹은 P1 우선순위로 90분 내 최초 영상 수신
|
<span className="text-red-400">*</span> 필수 항목 · 긴급 태스킹은 P1 우선순위로 90분 내 최초 영상 수신
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setModalPhase('provider')} className="px-5 py-2.5 rounded-lg border text-xs font-semibold cursor-pointer font-korean" style={{ borderColor: '#21262d', background: '#161b22', color: '#94a3b8' }}>← 뒤로</button>
|
<button onClick={() => setModalPhase('provider')} className="px-5 py-2.5 rounded-lg border border-[#21262d] text-xs font-semibold cursor-pointer font-korean text-[#94a3b8] bg-[#161b22]">← 뒤로</button>
|
||||||
<button onClick={() => setModalPhase('none')} className="px-7 py-2.5 rounded-lg border-none text-xs font-bold cursor-pointer font-korean text-white" style={{ background: 'linear-gradient(135deg,#6366f1,#818cf8)', boxShadow: '0 4px 16px rgba(99,102,241,.35)' }}>🛰 BlackSky 촬영 요청 제출</button>
|
<button onClick={() => setModalPhase('none')} className="px-7 py-2.5 rounded-lg border-none text-xs font-bold cursor-pointer font-korean text-white" style={{ background: 'linear-gradient(135deg,#6366f1,#818cf8)', boxShadow: '0 4px 16px rgba(99,102,241,.35)' }}>🛰 BlackSky 촬영 요청 제출</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -597,31 +596,31 @@ export function SatelliteRequest() {
|
|||||||
|
|
||||||
{/* ── UP42 카탈로그 주문 ── */}
|
{/* ── UP42 카탈로그 주문 ── */}
|
||||||
{modalPhase === 'up42' && (
|
{modalPhase === 'up42' && (
|
||||||
<div className="border rounded-[14px] w-[920px] max-h-[90vh] flex flex-col overflow-hidden" style={{ background: '#0d1117', borderColor: 'rgba(59,130,246,.3)', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
<div className="border rounded-[14px] w-[920px] max-h-[90vh] flex flex-col overflow-hidden border-[rgba(59,130,246,.3)]" style={{ background: '#0d1117', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="px-6 py-4 border-b flex items-center justify-between shrink-0 relative" style={{ borderColor: '#21262d' }}>
|
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
|
||||||
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#3b82f6,#06b6d4,#22c55e)' }} />
|
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#3b82f6,#06b6d4,#22c55e)' }} />
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)', borderColor: 'rgba(59,130,246,.3)' }}>
|
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-[rgba(59,130,246,.3)]" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)' }}>
|
||||||
<span className="text-[13px] font-extrabold font-mono" style={{ color: '#60a5fa', letterSpacing: '-.5px' }}>up<sup className="text-[7px] align-super">42</sup></span>
|
<span className="text-[13px] font-extrabold font-mono text-[#60a5fa] tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[15px] font-bold font-korean" style={{ color: '#e2e8f0' }}>위성 촬영 요청 — 새 태스킹 주문</div>
|
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]">위성 촬영 요청 — 새 태스킹 주문</div>
|
||||||
<div className="text-[9px] font-korean mt-0.5" style={{ color: '#64748b' }}>관심 지역(AOI)을 그리고 위성 패스를 선택하세요</div>
|
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]">관심 지역(AOI)을 그리고 위성 패스를 선택하세요</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="px-3 py-1 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(234,179,8,.1)', border: '1px solid rgba(234,179,8,.25)', color: '#eab308' }}>⚠ Beijing-3N 납기 지연 2.15–2.23</span>
|
<span className="px-3 py-1 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(234,179,8,.1)', border: '1px solid rgba(234,179,8,.25)', color: '#eab308' }}>⚠ Beijing-3N 납기 지연 2.15–2.23</span>
|
||||||
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none" style={{ color: '#64748b' }}>✕</button>
|
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-[#64748b]">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 본문 (좌: 사이드바, 우: 지도+AOI) */}
|
{/* 본문 (좌: 사이드바, 우: 지도+AOI) */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{/* 왼쪽: 위성 카탈로그 */}
|
{/* 왼쪽: 위성 카탈로그 */}
|
||||||
<div className="flex flex-col overflow-hidden border-r" style={{ width: 320, minWidth: 320, borderColor: '#21262d', background: '#0d1117' }}>
|
<div className="flex flex-col overflow-hidden border-r border-[#21262d]" style={{ width: 320, minWidth: 320, background: '#0d1117' }}>
|
||||||
{/* Optical / SAR / Elevation 탭 */}
|
{/* Optical / SAR / Elevation 탭 */}
|
||||||
<div className="flex border-b shrink-0" style={{ borderColor: '#21262d' }}>
|
<div className="flex border-b border-[#21262d] shrink-0">
|
||||||
{(['optical', 'sar', 'elevation'] as const).map(t => (
|
{(['optical', 'sar', 'elevation'] as const).map(t => (
|
||||||
<button
|
<button
|
||||||
key={t}
|
key={t}
|
||||||
@ -636,15 +635,15 @@ export function SatelliteRequest() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필터 바 */}
|
{/* 필터 바 */}
|
||||||
<div className="flex items-center gap-1.5 px-3 py-2 border-b shrink-0" style={{ borderColor: '#21262d' }}>
|
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-[#21262d] shrink-0">
|
||||||
<span className="px-2 py-0.5 rounded text-[9px] font-semibold" style={{ background: 'rgba(59,130,246,.1)', color: '#60a5fa', border: '1px solid rgba(59,130,246,.2)' }}>Filters ✎</span>
|
<span className="px-2 py-0.5 rounded text-[9px] font-semibold text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)' }}>Filters ✎</span>
|
||||||
<span className="px-2 py-0.5 rounded text-[9px] font-semibold" style={{ background: 'rgba(99,102,241,.1)', color: '#818cf8', border: '1px solid rgba(99,102,241,.2)' }}>☁ 구름 ≤ 20% ✕</span>
|
<span className="px-2 py-0.5 rounded text-[9px] font-semibold text-[#818cf8]" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.2)' }}>☁ 구름 ≤ 20% ✕</span>
|
||||||
<span className="ml-auto text-[9px] font-mono" style={{ color: '#64748b' }}>↕ 해상도 우선</span>
|
<span className="ml-auto text-[9px] font-mono text-[#64748b]">↕ 해상도 우선</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬렉션 수 */}
|
{/* 컬렉션 수 */}
|
||||||
<div className="px-3 py-1.5 border-b text-[9px] font-korean shrink-0" style={{ borderColor: '#21262d', color: '#64748b' }}>
|
<div className="px-3 py-1.5 border-b border-[#21262d] text-[9px] font-korean shrink-0 text-[#64748b]">
|
||||||
이 지역에서 <b style={{ color: '#e2e8f0' }}>{up42Filtered.length}</b>개 컬렉션 사용 가능
|
이 지역에서 <b className="text-[#e2e8f0]">{up42Filtered.length}</b>개 컬렉션 사용 가능
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 위성 목록 */}
|
{/* 위성 목록 */}
|
||||||
@ -653,22 +652,21 @@ export function SatelliteRequest() {
|
|||||||
<div
|
<div
|
||||||
key={sat.id}
|
key={sat.id}
|
||||||
onClick={() => setUp42SelSat(up42SelSat === sat.id ? null : sat.id)}
|
onClick={() => setUp42SelSat(up42SelSat === sat.id ? null : sat.id)}
|
||||||
className="flex items-center gap-2.5 px-3 py-2.5 border-b cursor-pointer transition-colors"
|
className="flex items-center gap-2.5 px-3 py-2.5 border-b border-[#161b22] cursor-pointer transition-colors"
|
||||||
style={{
|
style={{
|
||||||
borderColor: '#161b22',
|
|
||||||
background: up42SelSat === sat.id ? 'rgba(59,130,246,.08)' : 'transparent',
|
background: up42SelSat === sat.id ? 'rgba(59,130,246,.08)' : 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-1 h-8 rounded-full shrink-0" style={{ background: sat.color }} />
|
<div className="w-1 h-8 rounded-full shrink-0" style={{ background: sat.color }} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-[11px] font-semibold truncate font-korean" style={{ color: '#e2e8f0' }}>{sat.name}</div>
|
<div className="text-[11px] font-semibold truncate font-korean text-[#e2e8f0]">{sat.name}</div>
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
<span className="text-[9px] font-bold font-mono" style={{ color: sat.color }}>{sat.res}</span>
|
<span className="text-[9px] font-bold font-mono" style={{ color: sat.color }}>{sat.res}</span>
|
||||||
{sat.cloud > 0 && <span className="text-[8px] font-mono" style={{ color: '#64748b' }}>☁ ≤{sat.cloud}%</span>}
|
{sat.cloud > 0 && <span className="text-[8px] font-mono text-[#64748b]">☁ ≤{sat.cloud}%</span>}
|
||||||
{'delay' in sat && sat.delay && <span className="text-[8px] font-bold" style={{ color: '#eab308' }}>⚠ 지연</span>}
|
{'delay' in sat && sat.delay && <span className="text-[8px] font-bold" style={{ color: '#eab308' }}>⚠ 지연</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{up42SelSat === sat.id && <span className="text-[12px]" style={{ color: '#3b82f6' }}>✓</span>}
|
{up42SelSat === sat.id && <span className="text-[12px] text-blue-500">✓</span>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -679,51 +677,51 @@ export function SatelliteRequest() {
|
|||||||
{/* 지도 영역 (placeholder) */}
|
{/* 지도 영역 (placeholder) */}
|
||||||
<div className="flex-1 relative" style={{ background: '#0a0e18' }}>
|
<div className="flex-1 relative" style={{ background: '#0a0e18' }}>
|
||||||
{/* 검색바 */}
|
{/* 검색바 */}
|
||||||
<div className="absolute top-3 left-3 right-3 flex items-center gap-2 px-3 py-2 rounded-lg z-10" style={{ background: 'rgba(13,17,23,.9)', border: '1px solid #21262d', backdropFilter: 'blur(8px)' }}>
|
<div className="absolute top-3 left-3 right-3 flex items-center gap-2 px-3 py-2 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)', backdropFilter: 'blur(8px)' }}>
|
||||||
<span style={{ color: '#8690a6', fontSize: 13 }}>🔍</span>
|
<span className="text-[#8690a6] text-[13px]">🔍</span>
|
||||||
<input type="text" placeholder="위치 또는 좌표 입력..." className="flex-1 bg-transparent border-none outline-none text-[11px] font-korean" style={{ color: '#e2e8f0' }} />
|
<input type="text" placeholder="위치 또는 좌표 입력..." className="flex-1 bg-transparent border-none outline-none text-[11px] font-korean text-[#e2e8f0]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 지도 placeholder */}
|
{/* 지도 placeholder */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl mb-2 opacity-20">🗺</div>
|
<div className="text-3xl mb-2 opacity-20">🗺</div>
|
||||||
<div className="text-[11px] font-korean opacity-40" style={{ color: '#64748b' }}>지도 영역 — AOI를 그려 위성 패스를 확인하세요</div>
|
<div className="text-[11px] font-korean opacity-40 text-[#64748b]">지도 영역 — AOI를 그려 위성 패스를 확인하세요</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AOI 도구 버튼 (오른쪽 사이드) */}
|
{/* AOI 도구 버튼 (오른쪽 사이드) */}
|
||||||
<div className="absolute top-14 right-3 flex flex-col gap-1 p-1.5 rounded-lg z-10" style={{ background: 'rgba(13,17,23,.9)', border: '1px solid #21262d' }}>
|
<div className="absolute top-14 right-3 flex flex-col gap-1 p-1.5 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)' }}>
|
||||||
<div className="text-[7px] font-bold text-center mb-0.5" style={{ color: '#64748b' }}>ADD</div>
|
<div className="text-[7px] font-bold text-center mb-0.5 text-[#64748b]">ADD</div>
|
||||||
{[
|
{[
|
||||||
{ icon: '⬜', title: '사각형 AOI' },
|
{ icon: '⬜', title: '사각형 AOI' },
|
||||||
{ icon: '🔷', title: '다각형 AOI' },
|
{ icon: '🔷', title: '다각형 AOI' },
|
||||||
{ icon: '⭕', title: '원형 AOI' },
|
{ icon: '⭕', title: '원형 AOI' },
|
||||||
{ icon: '📁', title: '파일 업로드' },
|
{ icon: '📁', title: '파일 업로드' },
|
||||||
].map((t, i) => (
|
].map((t, i) => (
|
||||||
<button key={i} className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#8690a6' }} title={t.title}>{t.icon}</button>
|
<button key={i} className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]" title={t.title}>{t.icon}</button>
|
||||||
))}
|
))}
|
||||||
<div className="h-px my-0.5" style={{ background: '#21262d' }} />
|
<div className="h-px my-0.5 bg-[#21262d]" />
|
||||||
<div className="text-[7px] font-bold text-center mb-0.5" style={{ color: '#64748b' }}>AOI</div>
|
<div className="text-[7px] font-bold text-center mb-0.5 text-[#64748b]">AOI</div>
|
||||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#8690a6' }} title="저장된 AOI">💾</button>
|
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]" title="저장된 AOI">💾</button>
|
||||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#ef4444' }} title="AOI 삭제">🗑</button>
|
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#ef4444]" title="AOI 삭제">🗑</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 줌 컨트롤 */}
|
{/* 줌 컨트롤 */}
|
||||||
<div className="absolute bottom-3 right-3 flex flex-col rounded-md overflow-hidden z-10" style={{ border: '1px solid #21262d' }}>
|
<div className="absolute bottom-3 right-3 flex flex-col rounded-md overflow-hidden z-10 border border-[#21262d]">
|
||||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#8690a6' }}>+</button>
|
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]">+</button>
|
||||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none border-t" style={{ background: '#161b22', color: '#8690a6', borderTopColor: '#21262d' }}>−</button>
|
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none border-t border-t-[#21262d] bg-[#161b22] text-[#8690a6]">−</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 이 지역 검색 버튼 */}
|
{/* 이 지역 검색 버튼 */}
|
||||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10">
|
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10">
|
||||||
<button className="px-4 py-2 rounded-full text-[10px] font-semibold cursor-pointer font-korean" style={{ background: 'rgba(59,130,246,.9)', color: '#fff', border: 'none', boxShadow: '0 2px 12px rgba(59,130,246,.3)' }}>🔍 이 지역 검색</button>
|
<button className="px-4 py-2 rounded-full text-[10px] font-semibold cursor-pointer font-korean text-white border-none" style={{ background: 'rgba(59,130,246,.9)', boxShadow: '0 2px 12px rgba(59,130,246,.3)' }}>🔍 이 지역 검색</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 위성 패스 타임라인 */}
|
{/* 위성 패스 타임라인 */}
|
||||||
<div className="border-t px-4 py-3 shrink-0" style={{ borderColor: '#21262d', background: 'rgba(13,17,23,.95)' }}>
|
<div className="border-t border-[#21262d] px-4 py-3 shrink-0" style={{ background: 'rgba(13,17,23,.95)' }}>
|
||||||
<div className="text-[10px] font-bold font-korean mb-2" style={{ color: '#e2e8f0' }}>🛰 오늘 가용 위성 패스 — 선택된 AOI 통과 예정</div>
|
<div className="text-[10px] font-bold font-korean mb-2 text-[#e2e8f0]">🛰 오늘 가용 위성 패스 — 선택된 AOI 통과 예정</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{up42Passes.map((p, i) => (
|
{up42Passes.map((p, i) => (
|
||||||
<div
|
<div
|
||||||
@ -737,10 +735,10 @@ export function SatelliteRequest() {
|
|||||||
>
|
>
|
||||||
<div className="w-1.5 h-5 rounded-full shrink-0" style={{ background: p.color }} />
|
<div className="w-1.5 h-5 rounded-full shrink-0" style={{ background: p.color }} />
|
||||||
<div className="flex-1 flex items-center gap-3 min-w-0">
|
<div className="flex-1 flex items-center gap-3 min-w-0">
|
||||||
<span className="text-[10px] font-bold font-korean min-w-[100px]" style={{ color: '#e2e8f0' }}>{p.sat}</span>
|
<span className="text-[10px] font-bold font-korean min-w-[100px] text-[#e2e8f0]">{p.sat}</span>
|
||||||
<span className="text-[9px] font-bold font-mono min-w-[110px]" style={{ color: '#60a5fa' }}>{p.time}</span>
|
<span className="text-[9px] font-bold font-mono min-w-[110px] text-[#60a5fa]">{p.time}</span>
|
||||||
<span className="text-[9px] font-mono" style={{ color: '#06b6d4' }}>{p.res}</span>
|
<span className="text-[9px] font-mono" className="text-cyan-500">{p.res}</span>
|
||||||
<span className="text-[8px] font-mono" style={{ color: '#64748b' }}>{p.cloud}</span>
|
<span className="text-[8px] font-mono text-[#64748b]">{p.cloud}</span>
|
||||||
</div>
|
</div>
|
||||||
{p.note && (
|
{p.note && (
|
||||||
<span className="px-1.5 py-px rounded text-[8px] font-bold shrink-0" style={{
|
<span className="px-1.5 py-px rounded text-[8px] font-bold shrink-0" style={{
|
||||||
@ -748,7 +746,7 @@ export function SatelliteRequest() {
|
|||||||
color: p.note === '최우선 추천' ? '#22c55e' : p.note === '초고해상도' ? '#06b6d4' : p.note === 'SAR' ? '#f59e0b' : '#818cf8',
|
color: p.note === '최우선 추천' ? '#22c55e' : p.note === '초고해상도' ? '#06b6d4' : p.note === 'SAR' ? '#f59e0b' : '#818cf8',
|
||||||
}}>{p.note}</span>
|
}}>{p.note}</span>
|
||||||
)}
|
)}
|
||||||
{up42SelPass === i && <span className="text-xs" style={{ color: '#3b82f6' }}>✓</span>}
|
{up42SelPass === i && <span className="text-xs text-blue-500">✓</span>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -757,13 +755,13 @@ export function SatelliteRequest() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 푸터 */}
|
{/* 푸터 */}
|
||||||
<div className="px-6 py-3 border-t flex items-center justify-between shrink-0" style={{ borderColor: '#21262d' }}>
|
<div className="px-6 py-3 border-t border-[#21262d] flex items-center justify-between shrink-0">
|
||||||
<div className="text-[9px] font-korean" style={{ color: '#64748b' }}>원하는 위성을 찾지 못했나요? <span style={{ color: '#60a5fa', cursor: 'pointer' }}>태스킹 주문 생성</span> 또는 <span style={{ color: '#60a5fa', cursor: 'pointer' }}>자세히 보기 ↗</span></div>
|
<div className="text-[9px] font-korean text-[#64748b]">원하는 위성을 찾지 못했나요? <span className="text-[#60a5fa] cursor-pointer">태스킹 주문 생성</span> 또는 <span className="text-[#60a5fa] cursor-pointer">자세히 보기 ↗</span></div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[11px] font-korean mr-1.5" style={{ color: '#8690a6' }}>
|
<span className="text-[11px] font-korean mr-1.5 text-[#8690a6]">
|
||||||
선택: {up42SelSat ? up42Satellites.find(s => s.id === up42SelSat)?.name : '없음'}
|
선택: {up42SelSat ? up42Satellites.find(s => s.id === up42SelSat)?.name : '없음'}
|
||||||
</span>
|
</span>
|
||||||
<button onClick={() => setModalPhase('provider')} className="px-4 py-2 rounded-lg border text-[11px] font-semibold cursor-pointer font-korean" style={{ borderColor: '#21262d', background: '#161b22', color: '#94a3b8' }}>← 뒤로</button>
|
<button onClick={() => setModalPhase('provider')} className="px-4 py-2 rounded-lg border border-[#21262d] text-[11px] font-semibold cursor-pointer font-korean text-[#94a3b8] bg-[#161b22]">← 뒤로</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setModalPhase('none')}
|
onClick={() => setModalPhase('none')}
|
||||||
className="px-6 py-2 rounded-lg border-none text-[11px] font-bold cursor-pointer font-korean text-white transition-opacity"
|
className="px-6 py-2 rounded-lg border-none text-[11px] font-bold cursor-pointer font-korean text-white transition-opacity"
|
||||||
|
|||||||
@ -686,8 +686,8 @@ export function SensorAnalysis() {
|
|||||||
{/* Axis indicator */}
|
{/* Axis indicator */}
|
||||||
<div className="absolute bottom-16 left-4" style={{ fontSize: '9px', fontFamily: 'var(--fM)' }}>
|
<div className="absolute bottom-16 left-4" style={{ fontSize: '9px', fontFamily: 'var(--fM)' }}>
|
||||||
<div style={{ color: '#ef4444' }}>X →</div>
|
<div style={{ color: '#ef4444' }}>X →</div>
|
||||||
<div style={{ color: '#22c55e' }}>Y ↑</div>
|
<div className="text-green-500">Y ↑</div>
|
||||||
<div style={{ color: '#3b82f6' }}>Z ⊙</div>
|
<div className="text-blue-500">Z ⊙</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -351,7 +351,7 @@ function AssetManagement() {
|
|||||||
|
|
||||||
{/* Bottom Actions */}
|
{/* Bottom Actions */}
|
||||||
<div className="p-3.5 border-t border-border flex gap-2">
|
<div className="p-3.5 border-t border-border flex gap-2">
|
||||||
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean text-white border-none cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}>
|
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean text-white border-none cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }} >
|
||||||
📥 다운로드
|
📥 다운로드
|
||||||
</button>
|
</button>
|
||||||
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean bg-bg-3 border border-border text-text-2 cursor-pointer hover:bg-bg-hover transition-colors">
|
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean bg-bg-3 border border-border text-text-2 cursor-pointer hover:bg-bg-hover transition-colors">
|
||||||
|
|||||||
@ -114,7 +114,7 @@ function AssetMap({
|
|||||||
<Map
|
<Map
|
||||||
initialViewState={{ longitude: 127.8, latitude: 35.9, zoom: 7 }}
|
initialViewState={{ longitude: 127.8, latitude: 35.9, zoom: 7 }}
|
||||||
mapStyle={BASE_STYLE}
|
mapStyle={BASE_STYLE}
|
||||||
style={{ width: '100%', height: '100%' }}
|
className="w-full h-full"
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<DeckGLOverlay layers={[markerLayer]} />
|
<DeckGLOverlay layers={[markerLayer]} />
|
||||||
|
|||||||
@ -145,65 +145,54 @@ const TAG_COLORS: Record<string, { bg: string; bd: string; fg: string }> = {
|
|||||||
function TheoryCard({ section }: { section: TheorySection }) {
|
function TheoryCard({ section }: { section: TheorySection }) {
|
||||||
const badgeBg = section.bgTint.replace(/[\d.]+\)$/, '0.15)')
|
const badgeBg = section.bgTint.replace(/[\d.]+\)$/, '0.15)')
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="bg-bg-3 border border-border rounded-md overflow-hidden">
|
||||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
|
||||||
borderRadius: 'var(--rM, 10px)', overflow: 'hidden',
|
|
||||||
}}>
|
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div style={{
|
<div className="px-4 py-3 border-b border-border flex items-center gap-2" style={{ background: section.bgTint }}>
|
||||||
padding: '12px 16px', background: section.bgTint,
|
<span className="text-sm">{section.icon}</span>
|
||||||
borderBottom: '1px solid var(--bd)',
|
<span className="text-xs font-bold" style={{ color: section.color }}>
|
||||||
display: 'flex', alignItems: 'center', gap: '8px',
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '14px' }}>{section.icon}</span>
|
|
||||||
<span style={{ fontSize: '12px', fontWeight: 700, color: section.color, fontFamily: 'var(--fK)' }}>
|
|
||||||
{section.title}
|
{section.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items */}
|
{/* Items */}
|
||||||
<div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '9px', fontFamily: 'var(--fK)' }}>
|
<div className="px-4 py-3.5 flex flex-col gap-2 text-[9px]">
|
||||||
{section.items.map((item, i) => (
|
{section.items.map((item, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
{section.dividerAfter !== undefined && i === section.dividerAfter + 1 && (
|
{section.dividerAfter !== undefined && i === section.dividerAfter + 1 && (
|
||||||
<div style={{ borderTop: '1px dashed var(--bd)', margin: '4px 0 12px', paddingTop: '8px' }}>
|
<div className="mt-1 mb-3 pt-2" style={{ borderTop: '1px dashed var(--bd)' }}>
|
||||||
<div style={{ fontSize: '8px', fontWeight: 700, color: section.color, marginBottom: '6px', opacity: 0.7 }}>
|
<div className="text-[8px] font-bold mb-1.5 opacity-70" style={{ color: section.color }}>
|
||||||
{section.dividerLabel}
|
{section.dividerLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{
|
<div className="grid gap-2 px-2.5 py-2 bg-bg-0 rounded-md" style={{
|
||||||
display: 'grid', gridTemplateColumns: '24px 1fr', gap: '8px',
|
gridTemplateColumns: '24px 1fr',
|
||||||
padding: '8px 10px', background: 'var(--bg0)', borderRadius: '6px',
|
|
||||||
borderLeft: item.highlight ? `2px solid ${section.color}` : undefined,
|
borderLeft: item.highlight ? `2px solid ${section.color}` : undefined,
|
||||||
}}>
|
}}>
|
||||||
{/* Number badge */}
|
{/* Number badge */}
|
||||||
<div style={{
|
<div className="w-5 h-5 rounded flex items-center justify-center text-[9px] shrink-0"
|
||||||
width: '20px', height: '20px', borderRadius: '4px',
|
style={{
|
||||||
background: badgeBg,
|
background: badgeBg,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
fontWeight: item.highlight ? 700 : 400,
|
||||||
fontSize: '9px', flexShrink: 0,
|
color: item.highlight ? section.color : undefined,
|
||||||
fontWeight: item.highlight ? 700 : 400,
|
}}>
|
||||||
color: item.highlight ? section.color : undefined,
|
|
||||||
}}>
|
|
||||||
{['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩'][i]}
|
{['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩'][i]}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: 'var(--t1)', fontWeight: 700, marginBottom: '2px' }}>
|
<div className="font-bold mb-0.5">
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: 'var(--t3)', lineHeight: '1.6' }}>
|
<div className="text-text-3 leading-[1.6]">
|
||||||
{item.source}
|
{item.source}
|
||||||
</div>
|
</div>
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
{item.tags && (
|
{item.tags && (
|
||||||
<div style={{ marginTop: '3px', display: 'flex', flexWrap: 'wrap', gap: '3px' }}>
|
<div className="mt-0.5 flex flex-wrap gap-0.5">
|
||||||
{item.tags.map((tag, ti) => {
|
{item.tags.map((tag, ti) => {
|
||||||
const tc = TAG_COLORS[tag.color] || { bg: 'rgba(107,114,128,0.08)', bd: 'rgba(107,114,128,0.2)', fg: '#6b7280' }
|
const tc = TAG_COLORS[tag.color] || { bg: 'rgba(107,114,128,0.08)', bd: 'rgba(107,114,128,0.2)', fg: '#6b7280' }
|
||||||
return (
|
return (
|
||||||
<span key={ti} style={{
|
<span key={ti} className="px-1 py-px rounded text-[8px]" style={{
|
||||||
padding: '1px 5px', borderRadius: '3px', fontSize: '8px',
|
|
||||||
color: tc.fg, background: tc.bg, border: `1px solid ${tc.bd}`,
|
color: tc.fg, background: tc.bg, border: `1px solid ${tc.bd}`,
|
||||||
}}>
|
}}>
|
||||||
{tag.label}
|
{tag.label}
|
||||||
@ -212,7 +201,7 @@ function TheoryCard({ section }: { section: TheorySection }) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ marginTop: '2px', color: 'var(--t2)' }}>
|
<div className="mt-0.5 text-text-2">
|
||||||
{item.desc}
|
{item.desc}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -226,23 +215,23 @@ function TheoryCard({ section }: { section: TheorySection }) {
|
|||||||
|
|
||||||
function AssetTheory() {
|
function AssetTheory() {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
|
<div className="flex flex-col gap-0">
|
||||||
<div style={{ fontSize: '18px', fontWeight: 700, fontFamily: 'var(--fK)', marginBottom: '4px' }}>
|
<div className="text-[18px] font-bold mb-1">
|
||||||
📚 방제자원 이론
|
📚 방제자원 이론
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '12px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '24px' }}>
|
<div className="text-xs text-text-3 mb-6">
|
||||||
방제자산 운용 기준·성능 이론 및 관련 법령·고시 근거 문헌
|
방제자산 운용 기준·성능 이론 및 관련 법령·고시 근거 문헌
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '18px', alignItems: 'start' }}>
|
<div className="grid gap-[18px] items-start" style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||||
{/* Left column */}
|
{/* Left column */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
<div className="flex flex-col gap-3.5">
|
||||||
{THEORY_SECTIONS.slice(0, 2).map((sec) => (
|
{THEORY_SECTIONS.slice(0, 2).map((sec) => (
|
||||||
<TheoryCard key={sec.title} section={sec} />
|
<TheoryCard key={sec.title} section={sec} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Right column */}
|
{/* Right column */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
<div className="flex flex-col gap-3.5">
|
||||||
{THEORY_SECTIONS.slice(2).map((sec) => (
|
{THEORY_SECTIONS.slice(2).map((sec) => (
|
||||||
<TheoryCard key={sec.title} section={sec} />
|
<TheoryCard key={sec.title} section={sec} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export function AssetsView() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full bg-bg-0">
|
<div className="flex flex-col h-full w-full bg-bg-0">
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<div className="flex items-center justify-between border-b border-border bg-bg-1" style={{ flexShrink: 0 }}>
|
<div className="flex items-center justify-between border-b border-border bg-bg-1 shrink-0">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{([
|
{([
|
||||||
{ id: 'management' as const, icon: '🗂', label: '자산 관리' },
|
{ id: 'management' as const, icon: '🗂', label: '자산 관리' },
|
||||||
|
|||||||
@ -86,50 +86,49 @@ function ShipInsurance() {
|
|||||||
const expiredList = resultData.filter(r => getStatus(r.expiry) === 'expired')
|
const expiredList = resultData.filter(r => getStatus(r.expiry) === 'expired')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, overflow: 'auto' }}>
|
<div className="flex flex-col flex-1 overflow-auto">
|
||||||
|
|
||||||
{/* ── 헤더 ── */}
|
{/* ── 헤더 ── */}
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 20 }}>
|
<div className="flex items-start justify-between mb-5">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
<div className="flex items-center gap-2.5 mb-1">
|
||||||
<div style={{ fontSize: 18, fontWeight: 700, fontFamily: 'var(--fK)' }}>🛡 선박 보험정보 조회</div>
|
<div className="text-[18px] font-bold">🛡 선박 보험정보 조회</div>
|
||||||
<div style={{
|
<div className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[10px] font-bold"
|
||||||
display: 'flex', alignItems: 'center', gap: 5, padding: '3px 10px', borderRadius: 10,
|
style={{
|
||||||
fontSize: 10, fontWeight: 700, fontFamily: 'var(--fK)',
|
background: apiConnected ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
|
||||||
background: apiConnected ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
|
color: apiConnected ? 'var(--green)' : 'var(--red)',
|
||||||
color: apiConnected ? 'var(--green)' : 'var(--red)',
|
border: `1px solid ${apiConnected ? 'rgba(34,197,94,.25)' : 'rgba(239,68,68,.25)'}`,
|
||||||
border: `1px solid ${apiConnected ? 'rgba(34,197,94,.25)' : 'rgba(239,68,68,.25)'}`,
|
}}>
|
||||||
}}>
|
<span className="w-1.5 h-1.5 rounded-full inline-block" style={{ background: apiConnected ? 'var(--green)' : 'var(--red)' }} />
|
||||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: apiConnected ? 'var(--green)' : 'var(--red)', display: 'inline-block' }} />
|
|
||||||
{apiConnected ? 'API 연결됨' : 'API 미연결'}
|
{apiConnected ? 'API 연결됨' : 'API 미연결'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>한국해운조합(KSA) Open API 연동 · 선박 P&I 보험 및 선주 책임보험 실시간 조회</div>
|
<div className="text-xs text-text-3">한국해운조합(KSA) Open API 연동 · 선박 P&I 보험 및 선주 책임보험 실시간 조회</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="flex gap-2">
|
||||||
<button onClick={handleTestConnect} style={{ padding: '8px 16px', background: 'rgba(6,182,212,.12)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,.3)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔌 연결 테스트</button>
|
<button onClick={handleTestConnect} className="px-4 py-2 text-xs font-semibold cursor-pointer rounded-sm" style={{ background: 'rgba(6,182,212,.12)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,.3)' }}>🔌 연결 테스트</button>
|
||||||
<button onClick={() => setShowConfig(v => !v)} style={{ padding: '8px 16px', background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>⚙ API 설정</button>
|
<button onClick={() => setShowConfig(v => !v)} className="px-4 py-2 text-xs font-semibold cursor-pointer rounded-sm bg-bg-3 text-text-2 border border-border">⚙ API 설정</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── API 설정 패널 ── */}
|
{/* ── API 설정 패널 ── */}
|
||||||
{showConfig && (
|
{showConfig && (
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)', padding: '20px 24px', marginBottom: 20 }}>
|
<div className="bg-bg-3 border border-border rounded-md p-5 mb-5">
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, fontFamily: 'var(--fK)', marginBottom: 14, color: 'var(--cyan)' }}>⚙ 한국해운조합 API 연동 설정</div>
|
<div className="text-[13px] font-bold mb-3.5 text-primary-cyan">⚙ 한국해운조합 API 연동 설정</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
<div className="grid gap-3 mb-4" style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}>API Endpoint URL</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">API Endpoint URL</label>
|
||||||
<input type="text" value={configEndpoint} onChange={e => setConfigEndpoint(e.target.value)} placeholder="https://api.haewoon.or.kr/v1/..."
|
<input type="text" value={configEndpoint} onChange={e => setConfigEndpoint(e.target.value)} placeholder="https://api.haewoon.or.kr/v1/..."
|
||||||
style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
|
className="w-full px-3 py-2 bg-bg-0 border border-border rounded-sm font-mono text-xs outline-none box-border" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}>API Key</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">API Key</label>
|
||||||
<input type="password" value={configApiKey} onChange={e => setConfigApiKey(e.target.value)} placeholder="발급받은 API Key 입력"
|
<input type="password" value={configApiKey} onChange={e => setConfigApiKey(e.target.value)} placeholder="발급받은 API Key 입력"
|
||||||
style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
|
className="w-full px-3 py-2 bg-bg-0 border border-border rounded-sm font-mono text-xs outline-none box-border" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}>조회 기본값 — 조회 키 타입</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">조회 기본값 — 조회 키 타입</label>
|
||||||
<select value={configKeyType} onChange={e => setConfigKeyType(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', width: '100%' }}>
|
<select value={configKeyType} onChange={e => setConfigKeyType(e.target.value)} className="prd-i w-full" className="border-border">
|
||||||
<option value="mmsi">MMSI</option>
|
<option value="mmsi">MMSI</option>
|
||||||
<option value="imo">IMO 번호</option>
|
<option value="imo">IMO 번호</option>
|
||||||
<option value="shipname">선박명</option>
|
<option value="shipname">선박명</option>
|
||||||
@ -137,20 +136,20 @@ function ShipInsurance() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}>응답 형식</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">응답 형식</label>
|
||||||
<select value={configRespType} onChange={e => setConfigRespType(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', width: '100%' }}>
|
<select value={configRespType} onChange={e => setConfigRespType(e.target.value)} className="prd-i w-full" className="border-border">
|
||||||
<option value="json">JSON</option>
|
<option value="json">JSON</option>
|
||||||
<option value="xml">XML</option>
|
<option value="xml">XML</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="flex gap-2">
|
||||||
<button onClick={handleSaveConfig} style={{ padding: '9px 20px', background: 'linear-gradient(135deg, var(--cyan), var(--blue))', color: '#fff', border: 'none', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'var(--fK)' }}>💾 저장</button>
|
<button onClick={handleSaveConfig} className="px-5 py-2 text-white border-none rounded-sm text-xs font-bold cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}>💾 저장</button>
|
||||||
<button onClick={() => setShowConfig(false)} style={{ padding: '9px 16px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 12, cursor: 'pointer', fontFamily: 'var(--fK)' }}>취소</button>
|
<button onClick={() => setShowConfig(false)} className="px-4 py-2 bg-bg-0 text-text-2 border border-border rounded-sm text-xs cursor-pointer">취소</button>
|
||||||
</div>
|
</div>
|
||||||
{/* API 연동 안내 */}
|
{/* API 연동 안내 */}
|
||||||
<div style={{ marginTop: 16, padding: '12px 16px', background: 'rgba(6,182,212,.05)', border: '1px solid rgba(6,182,212,.15)', borderRadius: 'var(--rS)', fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.8 }}>
|
<div className="mt-4 px-4 py-3 rounded-sm text-[10px] text-text-3 leading-[1.8]" style={{ background: 'rgba(6,182,212,.05)', border: '1px solid rgba(6,182,212,.15)' }}>
|
||||||
<span style={{ color: 'var(--cyan)', fontWeight: 700 }}>📋 한국해운조합 API 발급 안내</span><br />
|
<span className="text-primary-cyan font-bold">📋 한국해운조합 API 발급 안내</span><br />
|
||||||
• 한국해운조합 공공데이터포털 또는 해운조합 IT지원팀에 API 키 신청<br />
|
• 한국해운조합 공공데이터포털 또는 해운조합 IT지원팀에 API 키 신청<br />
|
||||||
• 해양경찰청 기관 계정으로 신청 시 전용 엔드포인트 및 키 발급<br />
|
• 해양경찰청 기관 계정으로 신청 시 전용 엔드포인트 및 키 발급<br />
|
||||||
• 조회 가능 데이터: P&I 보험, 선주책임보험, 해상보험 가입 여부, 증권번호, 보험기간, 보상한도
|
• 조회 가능 데이터: P&I 보험, 선주책임보험, 해상보험 가입 여부, 증권번호, 보험기간, 보상한도
|
||||||
@ -159,26 +158,26 @@ function ShipInsurance() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── 검색 영역 ── */}
|
{/* ── 검색 영역 ── */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)', padding: '18px 20px', marginBottom: 16 }}>
|
<div className="bg-bg-3 border border-border rounded-md px-5 py-4 mb-4">
|
||||||
<div style={{ fontSize: 12, fontWeight: 700, fontFamily: 'var(--fK)', marginBottom: 12, color: 'var(--t2)' }}>🔍 보험정보 조회</div>
|
<div className="text-xs font-bold mb-3 text-text-2">🔍 보험정보 조회</div>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
<div className="flex gap-2 items-end flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 4 }}>조회 키 타입</label>
|
<label className="block text-[10px] font-semibold text-text-3 mb-1">조회 키 타입</label>
|
||||||
<select value={searchType} onChange={e => setSearchType(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', minWidth: 120 }}>
|
<select value={searchType} onChange={e => setSearchType(e.target.value)} className="prd-i min-w-[120px]" className="border-border">
|
||||||
<option value="mmsi">MMSI</option>
|
<option value="mmsi">MMSI</option>
|
||||||
<option value="imo">IMO 번호</option>
|
<option value="imo">IMO 번호</option>
|
||||||
<option value="shipname">선박명</option>
|
<option value="shipname">선박명</option>
|
||||||
<option value="callsign">호출부호</option>
|
<option value="callsign">호출부호</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 220 }}>
|
<div className="flex-1 min-w-[220px]">
|
||||||
<label style={{ display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 4 }}>조회값</label>
|
<label className="block text-[10px] font-semibold text-text-3 mb-1">조회값</label>
|
||||||
<input type="text" value={searchVal} onChange={e => setSearchVal(e.target.value)} placeholder={placeholderMap[searchType]}
|
<input type="text" value={searchVal} onChange={e => setSearchVal(e.target.value)} placeholder={placeholderMap[searchType]}
|
||||||
style={{ width: '100%', padding: '9px 14px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 13, outline: 'none', boxSizing: 'border-box' }} />
|
className="w-full px-3.5 py-2 bg-bg-0 border border-border rounded-sm font-mono text-[13px] outline-none box-border" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 4 }}>보험 종류</label>
|
<label className="block text-[10px] font-semibold text-text-3 mb-1">보험 종류</label>
|
||||||
<select value={insTypeFilter} onChange={e => setInsTypeFilter(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', minWidth: 140 }}>
|
<select value={insTypeFilter} onChange={e => setInsTypeFilter(e.target.value)} className="prd-i min-w-[140px]" className="border-border">
|
||||||
<option>전체</option>
|
<option>전체</option>
|
||||||
<option>P&I 보험</option>
|
<option>P&I 보험</option>
|
||||||
<option>선주책임보험</option>
|
<option>선주책임보험</option>
|
||||||
@ -186,8 +185,8 @@ function ShipInsurance() {
|
|||||||
<option>방제보증보험</option>
|
<option>방제보증보험</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleQuery} style={{ padding: '9px 24px', background: 'linear-gradient(135deg, var(--cyan), var(--blue))', color: '#fff', border: 'none', borderRadius: 'var(--rS)', fontSize: 13, fontWeight: 700, cursor: 'pointer', fontFamily: 'var(--fK)', flexShrink: 0 }}>🔍 조회</button>
|
<button onClick={handleQuery} className="px-6 py-2 text-white border-none rounded-sm text-[13px] font-bold cursor-pointer shrink-0" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}>🔍 조회</button>
|
||||||
<button onClick={handleBatchQuery} style={{ padding: '9px 18px', background: 'rgba(168,85,247,.12)', color: 'var(--purple)', border: '1px solid rgba(168,85,247,.3)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)', flexShrink: 0 }}>📋 자산목록 일괄조회</button>
|
<button onClick={handleBatchQuery} className="px-4 py-2 text-[12px] font-semibold cursor-pointer shrink-0 rounded-sm" style={{ background: 'rgba(168,85,247,.12)', color: 'var(--purple)', border: '1px solid rgba(168,85,247,.3)' }}>📋 자산목록 일괄조회</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -195,26 +194,26 @@ function ShipInsurance() {
|
|||||||
|
|
||||||
{/* 초기 안내 상태 */}
|
{/* 초기 안내 상태 */}
|
||||||
{viewState === 'empty' && (
|
{viewState === 'empty' && (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '60px 20px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)' }}>
|
<div className="flex flex-col items-center justify-center py-16 px-5 bg-bg-3 border border-border rounded-md">
|
||||||
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }}>🛡</div>
|
<div className="text-[48px] mb-4 opacity-30">🛡</div>
|
||||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 8 }}>한국해운조합 API 연동 대기 중</div>
|
<div className="text-sm font-bold text-text-2 mb-2">한국해운조합 API 연동 대기 중</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--t3)', fontFamily: 'var(--fK)', textAlign: 'center', lineHeight: 1.8 }}>
|
<div className="text-xs text-text-3 text-center leading-[1.8]">
|
||||||
API 설정에서 한국해운조합 API Key를 등록하거나<br />
|
API 설정에서 한국해운조합 API Key를 등록하거나<br />
|
||||||
MMSI·IMO·선박명으로 직접 조회하세요.<br />
|
MMSI·IMO·선박명으로 직접 조회하세요.<br />
|
||||||
<span style={{ color: 'var(--cyan)' }}>자산목록 일괄조회</span> 시 등록된 방제자산 전체의 보험 현황을 한번에 확인할 수 있습니다.
|
<span className="text-primary-cyan">자산목록 일괄조회</span> 시 등록된 방제자산 전체의 보험 현황을 한번에 확인할 수 있습니다.
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 20, display: 'flex', gap: 10 }}>
|
<div className="mt-5 flex gap-2.5">
|
||||||
<button onClick={() => setShowConfig(true)} style={{ padding: '10px 20px', background: 'rgba(6,182,212,.12)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,.3)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>⚙ API 설정</button>
|
<button onClick={() => setShowConfig(true)} className="px-5 py-2.5 text-xs font-semibold cursor-pointer rounded-sm" style={{ background: 'rgba(6,182,212,.12)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,.3)' }}>⚙ API 설정</button>
|
||||||
<button onClick={loadDemoData} style={{ padding: '10px 20px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>📊 샘플 데이터 보기</button>
|
<button onClick={loadDemoData} className="px-5 py-2.5 bg-bg-0 text-text-2 border border-border rounded-sm text-xs font-semibold cursor-pointer">📊 샘플 데이터 보기</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 로딩 */}
|
{/* 로딩 */}
|
||||||
{viewState === 'loading' && (
|
{viewState === 'loading' && (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 60, background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)' }}>
|
<div className="flex flex-col items-center justify-center p-16 bg-bg-3 border border-border rounded-md">
|
||||||
<div style={{ width: 36, height: 36, border: '3px solid var(--bd)', borderTopColor: 'var(--cyan)', borderRadius: '50%', animation: 'spin 0.8s linear infinite', marginBottom: 14 }} />
|
<div className="w-9 h-9 rounded-full mb-3.5" style={{ border: '3px solid var(--bd)', borderTopColor: 'var(--cyan)', animation: 'spin 0.8s linear infinite' }} />
|
||||||
<div style={{ fontSize: 13, color: 'var(--t2)', fontFamily: 'var(--fK)' }}>한국해운조합 API 조회 중...</div>
|
<div className="text-[13px] text-text-2">한국해운조합 API 조회 중...</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -222,33 +221,33 @@ function ShipInsurance() {
|
|||||||
{viewState === 'result' && (
|
{viewState === 'result' && (
|
||||||
<>
|
<>
|
||||||
{/* 요약 카드 */}
|
{/* 요약 카드 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 14 }}>
|
<div className="grid gap-2.5 mb-3.5" style={{ gridTemplateColumns: 'repeat(4, 1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ label: '전체', val: resultData.length, color: 'var(--cyan)', bg: 'rgba(6,182,212,.08)' },
|
{ label: '전체', val: resultData.length, color: 'var(--cyan)', bg: 'rgba(6,182,212,.08)' },
|
||||||
{ label: '유효', val: validCount, color: 'var(--green)', bg: 'rgba(34,197,94,.08)' },
|
{ label: '유효', val: validCount, color: 'var(--green)', bg: 'rgba(34,197,94,.08)' },
|
||||||
{ label: '만료임박(30일)', val: soonList.length, color: 'var(--yellow)', bg: 'rgba(234,179,8,.08)' },
|
{ label: '만료임박(30일)', val: soonList.length, color: 'var(--yellow)', bg: 'rgba(234,179,8,.08)' },
|
||||||
{ label: '만료/미가입', val: resultData.length - validCount, color: 'var(--red)', bg: 'rgba(239,68,68,.08)' },
|
{ label: '만료/미가입', val: resultData.length - validCount, color: 'var(--red)', bg: 'rgba(239,68,68,.08)' },
|
||||||
].map((c, i) => (
|
].map((c, i) => (
|
||||||
<div key={i} style={{ padding: '14px 16px', background: c.bg, border: `1px solid ${c.color}33`, borderRadius: 'var(--rS)', textAlign: 'center' }}>
|
<div key={i} className="px-4 py-3.5 text-center rounded-sm" style={{ background: c.bg, border: `1px solid ${c.color}33` }}>
|
||||||
<div style={{ fontSize: 22, fontWeight: 800, color: c.color, fontFamily: 'var(--fM)' }}>{c.val}</div>
|
<div className="text-[22px] font-extrabold font-mono" style={{ color: c.color }}>{c.val}</div>
|
||||||
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: 2 }}>{c.label}</div>
|
<div className="text-[10px] text-text-3 mt-0.5">{c.label}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)', overflow: 'hidden', marginBottom: 12 }}>
|
<div className="bg-bg-3 border border-border rounded-md overflow-hidden mb-3">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', borderBottom: '1px solid var(--bd)' }}>
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||||
<div style={{ fontSize: 12, fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>조회 결과 <span style={{ color: 'var(--cyan)' }}>{resultData.length}</span>건</div>
|
<div className="text-xs font-bold">조회 결과 <span className="text-primary-cyan">{resultData.length}</span>건</div>
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
<div className="flex gap-1.5">
|
||||||
<button onClick={() => alert('엑셀 내보내기 기능은 실제 API 연동 후 활성화됩니다.')} style={{ padding: '5px 12px', background: 'rgba(34,197,94,.1)', color: 'var(--green)', border: '1px solid rgba(34,197,94,.25)', borderRadius: 'var(--rS)', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>📥 엑셀 내보내기</button>
|
<button onClick={() => alert('엑셀 내보내기 기능은 실제 API 연동 후 활성화됩니다.')} className="px-3 py-1 text-[11px] font-semibold cursor-pointer rounded-sm" style={{ background: 'rgba(34,197,94,.1)', color: 'var(--green)', border: '1px solid rgba(34,197,94,.25)' }}>📥 엑셀 내보내기</button>
|
||||||
<button onClick={handleQuery} style={{ padding: '5px 12px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 11, cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔄 새로고침</button>
|
<button onClick={handleQuery} className="px-3 py-1 bg-bg-0 text-text-2 border border-border rounded-sm text-[11px] cursor-pointer">🔄 새로고침</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div className="overflow-x-auto">
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11, fontFamily: 'var(--fK)' }}>
|
<table className="w-full text-[11px] border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ background: 'var(--bg0)' }}>
|
<tr className="bg-bg-0">
|
||||||
{[
|
{[
|
||||||
{ label: '선박명', align: 'left' },
|
{ label: '선박명', align: 'left' },
|
||||||
{ label: 'MMSI', align: 'center' },
|
{ label: 'MMSI', align: 'center' },
|
||||||
@ -260,7 +259,7 @@ function ShipInsurance() {
|
|||||||
{ label: '보상한도', align: 'right' },
|
{ label: '보상한도', align: 'right' },
|
||||||
{ label: '상태', align: 'center' },
|
{ label: '상태', align: 'center' },
|
||||||
].map((h, i) => (
|
].map((h, i) => (
|
||||||
<th key={i} style={{ padding: '10px 14px', textAlign: h.align as 'left' | 'center' | 'right', fontWeight: 700, color: 'var(--t2)', borderBottom: '1px solid var(--bd)', whiteSpace: 'nowrap' }}>{h.label}</th>
|
<th key={i} className="px-3.5 py-2.5 font-bold text-text-2 border-b border-border whitespace-nowrap" style={{ textAlign: h.align as 'left' | 'center' | 'right' }}>{h.label}</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -270,18 +269,17 @@ function ShipInsurance() {
|
|||||||
const isExp = st === 'expired'
|
const isExp = st === 'expired'
|
||||||
const isSoon = st === 'soon'
|
const isSoon = st === 'soon'
|
||||||
return (
|
return (
|
||||||
<tr key={i} style={{ borderBottom: '1px solid var(--bd)', background: isExp ? 'rgba(239,68,68,.03)' : undefined }}>
|
<tr key={i} className="border-b border-border" style={{ background: isExp ? 'rgba(239,68,68,.03)' : undefined }}>
|
||||||
<td style={{ padding: '10px 14px', fontWeight: 600 }}>{r.shipName}</td>
|
<td className="px-3.5 py-2.5 font-semibold">{r.shipName}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 11 }}>{r.mmsi || '—'}</td>
|
<td className="px-3.5 py-2.5 text-center font-mono text-[11px]">{r.mmsi || '—'}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 11 }}>{r.imo || '—'}</td>
|
<td className="px-3.5 py-2.5 text-center font-mono text-[11px]">{r.imo || '—'}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'center' }}>{r.insType}</td>
|
<td className="px-3.5 py-2.5 text-center">{r.insType}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'center' }}>{r.insurer}</td>
|
<td className="px-3.5 py-2.5 text-center">{r.insurer}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 10, color: 'var(--t3)' }}>{r.policyNo}</td>
|
<td className="px-3.5 py-2.5 text-center font-mono text-[10px] text-text-3">{r.policyNo}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 11, color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : undefined, fontWeight: isExp || isSoon ? 700 : undefined }}>{r.start} ~ {r.expiry}</td>
|
<td className="px-3.5 py-2.5 text-center font-mono text-[11px]" style={{ color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : undefined, fontWeight: isExp || isSoon ? 700 : undefined }}>{r.start} ~ {r.expiry}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'right', fontWeight: 700, fontFamily: 'var(--fM)' }}>{r.limit}</td>
|
<td className="px-3.5 py-2.5 text-right font-bold font-mono">{r.limit}</td>
|
||||||
<td style={{ padding: '10px 14px', textAlign: 'center' }}>
|
<td className="px-3.5 py-2.5 text-center">
|
||||||
<span style={{
|
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-semibold" style={{
|
||||||
padding: '3px 10px', borderRadius: 10, fontSize: 10, fontWeight: 600,
|
|
||||||
background: isExp ? 'rgba(239,68,68,.15)' : isSoon ? 'rgba(234,179,8,.15)' : 'rgba(34,197,94,.15)',
|
background: isExp ? 'rgba(239,68,68,.15)' : isSoon ? 'rgba(234,179,8,.15)' : 'rgba(34,197,94,.15)',
|
||||||
color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : 'var(--green)',
|
color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : 'var(--green)',
|
||||||
}}>
|
}}>
|
||||||
@ -298,12 +296,12 @@ function ShipInsurance() {
|
|||||||
|
|
||||||
{/* 경고 */}
|
{/* 경고 */}
|
||||||
{(expiredList.length > 0 || soonList.length > 0) && (
|
{(expiredList.length > 0 || soonList.length > 0) && (
|
||||||
<div style={{ padding: '12px 16px', background: 'rgba(234,179,8,.06)', border: '1px solid rgba(234,179,8,.25)', borderRadius: 'var(--rS)', fontSize: 12, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 12 }}>
|
<div className="px-4 py-3 text-xs text-text-2 mb-3 rounded-sm" style={{ background: 'rgba(234,179,8,.06)', border: '1px solid rgba(234,179,8,.25)' }}>
|
||||||
{expiredList.length > 0 && (
|
{expiredList.length > 0 && (
|
||||||
<><span style={{ color: 'var(--red)', fontWeight: 700 }}>⛔ 만료 {expiredList.length}건:</span> {expiredList.map(r => r.shipName).join(', ')}<br /></>
|
<><span className="text-status-red font-bold">⛔ 만료 {expiredList.length}건:</span> {expiredList.map(r => r.shipName).join(', ')}<br /></>
|
||||||
)}
|
)}
|
||||||
{soonList.length > 0 && (
|
{soonList.length > 0 && (
|
||||||
<><span style={{ color: 'var(--yellow)', fontWeight: 700 }}>⚠ 만료임박(30일) {soonList.length}건:</span> {soonList.map(r => r.shipName).join(', ')}</>
|
<><span className="font-bold text-status-yellow">⚠ 만료임박(30일) {soonList.length}건:</span> {soonList.map(r => r.shipName).join(', ')}</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -311,15 +309,15 @@ function ShipInsurance() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── API 연동 정보 푸터 ── */}
|
{/* ── API 연동 정보 푸터 ── */}
|
||||||
<div style={{ marginTop: 16, padding: '12px 16px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div className="mt-4 px-4 py-3 bg-bg-3 border border-border rounded-sm flex items-center justify-between">
|
||||||
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.7 }}>
|
<div className="text-[10px] text-text-3 leading-[1.7]">
|
||||||
<span style={{ color: 'var(--t2)', fontWeight: 700 }}>데이터 출처:</span> 한국해운조합(KSA) · haewoon.or.kr<br />
|
<span className="text-text-2 font-bold">데이터 출처:</span> 한국해운조합(KSA) · haewoon.or.kr<br />
|
||||||
<span style={{ color: 'var(--t2)', fontWeight: 700 }}>연동 방식:</span> REST API (JSON) · 실시간 조회 · 캐시 TTL 1시간
|
<span className="text-text-2 font-bold">연동 방식:</span> REST API (JSON) · 실시간 조회 · 캐시 TTL 1시간
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
<div className="flex gap-1.5 items-center">
|
||||||
<span style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>마지막 동기화:</span>
|
<span className="text-[10px] text-text-3">마지막 동기화:</span>
|
||||||
<span style={{ fontSize: 10, color: 'var(--t2)', fontFamily: 'var(--fM)' }}>{lastSync}</span>
|
<span className="text-[10px] text-text-2 font-mono">{lastSync}</span>
|
||||||
<button onClick={handleFullSync} style={{ padding: '4px 10px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 4, fontSize: 10, cursor: 'pointer', fontFamily: 'var(--fK)' }}>전체 동기화</button>
|
<button onClick={handleFullSync} className="px-2.5 py-1 bg-bg-0 text-text-2 border border-border rounded text-[10px] cursor-pointer">전체 동기화</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -204,22 +204,21 @@ export function BoardView() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<div className="flex-1 relative overflow-hidden">
|
<div className="flex-1 relative overflow-hidden">
|
||||||
<div className="flex flex-col h-full" style={{ background: 'var(--bg0)' }}>
|
<div className="flex flex-col h-full bg-bg-0">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between px-8 py-4 border-b" style={{ borderColor: 'var(--bd)', background: 'var(--bg1)' }}>
|
<div className="flex items-center justify-between px-8 py-4 border-b" style={{ borderColor: 'var(--bd)', background: 'var(--bg1)' }}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span style={{ fontSize: 18 }}>📘</span>
|
<span className="text-lg">📘</span>
|
||||||
<span className="text-[15px] font-bold" style={{ fontFamily: 'var(--fK)', color: 'var(--t1)' }}>해경매뉴얼</span>
|
<span className="text-[15px] font-bold">해경매뉴얼</span>
|
||||||
<span className="text-[10px] ml-1" style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>총 {filteredManuals.length}건</span>
|
<span className="text-[10px] ml-1 text-text-3">총 {filteredManuals.length}건</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 ml-4">
|
<div className="flex gap-1 ml-4">
|
||||||
{manualCategories.map(cat => (
|
{manualCategories.map(cat => (
|
||||||
<button key={cat} onClick={() => setManualCategory(cat)}
|
<button key={cat} onClick={() => setManualCategory(cat)}
|
||||||
className="px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all"
|
className="px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--fK)',
|
background: manualCategory === cat ? 'rgba(6,182,212,.15)' : 'var(--bg3)',
|
||||||
background: manualCategory === cat ? 'rgba(6,182,212,.15)' : 'var(--bg3)',
|
|
||||||
border: manualCategory === cat ? '1px solid rgba(6,182,212,.3)' : '1px solid var(--bd)',
|
border: manualCategory === cat ? '1px solid rgba(6,182,212,.3)' : '1px solid var(--bd)',
|
||||||
color: manualCategory === cat ? 'var(--cyan)' : 'var(--t3)',
|
color: manualCategory === cat ? 'var(--cyan)' : 'var(--t3)',
|
||||||
}}>
|
}}>
|
||||||
@ -230,10 +229,10 @@ export function BoardView() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input type="text" placeholder="매뉴얼 검색..." value={manualSearch} onChange={e => setManualSearch(e.target.value)}
|
<input type="text" placeholder="매뉴얼 검색..." value={manualSearch} onChange={e => setManualSearch(e.target.value)}
|
||||||
className="px-4 py-2 text-sm rounded w-64" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fK)', outline: 'none' }} />
|
className="px-4 py-2 text-sm rounded w-64" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', outline: 'none' }} />
|
||||||
<button onClick={() => setShowUploadModal(true)}
|
<button onClick={() => setShowUploadModal(true)}
|
||||||
className="px-5 py-2 text-[12px] font-semibold rounded-md transition-all flex items-center gap-1.5"
|
className="px-5 py-2 text-[12px] font-semibold rounded-md transition-all flex items-center gap-1.5"
|
||||||
style={{ background: 'rgba(6,182,212,.15)', border: '1px solid rgba(6,182,212,.3)', color: '#22d3ee', fontFamily: 'var(--fK)', cursor: 'pointer' }}>
|
style={{ background: 'rgba(6,182,212,.15)', border: '1px solid rgba(6,182,212,.3)', color: '#22d3ee', cursor: 'pointer' }}>
|
||||||
📤 새로 업로드
|
📤 새로 업로드
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -243,7 +242,7 @@ export function BoardView() {
|
|||||||
<div className="flex-1 overflow-auto px-8 py-6">
|
<div className="flex-1 overflow-auto px-8 py-6">
|
||||||
{manualLoading ? (
|
{manualLoading ? (
|
||||||
<div className="text-center py-20">
|
<div className="text-center py-20">
|
||||||
<p className="text-sm" style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>로딩 중...</p>
|
<p className="text-sm text-text-3">로딩 중...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))' }}>
|
<div className="grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))' }}>
|
||||||
@ -265,7 +264,7 @@ export function BoardView() {
|
|||||||
{file.version}
|
{file.version}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[12px] font-bold mb-3 leading-[1.5]" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[12px] font-bold mb-3 leading-[1.5]">
|
||||||
{file.title}
|
{file.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
@ -289,7 +288,7 @@ export function BoardView() {
|
|||||||
setShowUploadModal(true)
|
setShowUploadModal(true)
|
||||||
}}
|
}}
|
||||||
className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all"
|
className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all"
|
||||||
style={{ background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)', color: '#3b82f6', fontFamily: 'var(--fK)', cursor: 'pointer' }}
|
style={{ background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)', color: '#3b82f6', cursor: 'pointer' }}
|
||||||
title="수정">
|
title="수정">
|
||||||
✏️ 수정
|
✏️ 수정
|
||||||
</button>
|
</button>
|
||||||
@ -305,13 +304,13 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all"
|
className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all"
|
||||||
style={{ background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)', color: '#ef4444', fontFamily: 'var(--fK)', cursor: 'pointer' }}
|
style={{ background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)', color: '#ef4444', cursor: 'pointer' }}
|
||||||
title="삭제">
|
title="삭제">
|
||||||
🗑️ 삭제
|
🗑️ 삭제
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between pt-3" style={{ borderTop: '1px solid var(--bd)' }}>
|
<div className="flex items-center justify-between pt-3" style={{ borderTop: '1px solid var(--bd)' }}>
|
||||||
<div className="flex items-center gap-3 text-[10px]" style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<div className="flex items-center gap-3 text-[10px] text-text-3">
|
||||||
<span>{file.authorNm}</span>
|
<span>{file.authorNm}</span>
|
||||||
<span>{new Date(file.regDtm).toLocaleDateString('ko-KR')}</span>
|
<span>{new Date(file.regDtm).toLocaleDateString('ko-KR')}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -353,7 +352,7 @@ export function BoardView() {
|
|||||||
}}
|
}}
|
||||||
className="px-3 py-1 rounded text-[10px] font-semibold transition-all" style={{
|
className="px-3 py-1 rounded text-[10px] font-semibold transition-all" style={{
|
||||||
background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.25)',
|
background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.25)',
|
||||||
color: '#22d3ee', fontFamily: 'var(--fK)', cursor: 'pointer',
|
color: '#22d3ee', cursor: 'pointer',
|
||||||
}}>
|
}}>
|
||||||
📥 다운로드
|
📥 다운로드
|
||||||
</button>
|
</button>
|
||||||
@ -368,7 +367,7 @@ export function BoardView() {
|
|||||||
{!manualLoading && filteredManuals.length === 0 && (
|
{!manualLoading && filteredManuals.length === 0 && (
|
||||||
<div className="text-center py-20">
|
<div className="text-center py-20">
|
||||||
<div style={{ fontSize: 32, opacity: 0.3, marginBottom: 8 }}>📘</div>
|
<div style={{ fontSize: 32, opacity: 0.3, marginBottom: 8 }}>📘</div>
|
||||||
<p className="text-sm" style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>검색 결과가 없습니다.</p>
|
<p className="text-sm text-text-3">검색 결과가 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -376,30 +375,29 @@ export function BoardView() {
|
|||||||
|
|
||||||
{/* 업로드 모달 */}
|
{/* 업로드 모달 */}
|
||||||
{showUploadModal && (
|
{showUploadModal && (
|
||||||
<div style={{ position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,.55)' }}
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,.55)' }}
|
||||||
onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}>
|
onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}>
|
||||||
<div style={{ width: 480, background: 'var(--bg1)', border: '1px solid var(--bd)', borderRadius: 12, overflow: 'hidden' }}
|
<div className="w-[480px] bg-bg-1 border border-border rounded-xl overflow-hidden"
|
||||||
onClick={e => e.stopPropagation()}>
|
onClick={e => e.stopPropagation()}>
|
||||||
<div style={{ padding: '16px 20px', borderBottom: '1px solid var(--bd)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div className="px-5 py-4 border-b border-border flex items-center justify-between">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div className="flex items-center gap-2">
|
||||||
<span style={{ fontSize: 16 }}>{editingManualId ? '✏️' : '📤'}</span>
|
<span className="text-base">{editingManualId ? '✏️' : '📤'}</span>
|
||||||
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>{editingManualId ? '매뉴얼 수정' : '매뉴얼 업로드'}</span>
|
<span className="text-sm font-bold">{editingManualId ? '매뉴얼 수정' : '매뉴얼 업로드'}</span>
|
||||||
</div>
|
</div>
|
||||||
<span onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}
|
<span onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}
|
||||||
style={{ cursor: 'pointer', color: 'var(--t3)', fontSize: 16, lineHeight: 1 }}>✕</span>
|
className="cursor-pointer text-text-3 text-base leading-none">✕</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div className="p-5 flex flex-col gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 6 }}>카테고리</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">카테고리</label>
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
<div className="flex gap-1.5">
|
||||||
{['방제매뉴얼', '대응매뉴얼', '교육자료', '법령·규정'].map(cat => {
|
{['방제매뉴얼', '대응매뉴얼', '교육자료', '법령·규정'].map(cat => {
|
||||||
const cc = catColor(cat)
|
const cc = catColor(cat)
|
||||||
const isActive = uploadForm.category === cat
|
const isActive = uploadForm.category === cat
|
||||||
return (
|
return (
|
||||||
<button key={cat} onClick={() => setUploadForm(prev => ({ ...prev, category: cat }))}
|
<button key={cat} onClick={() => setUploadForm(prev => ({ ...prev, category: cat }))}
|
||||||
|
className="flex-1 py-2 px-1 rounded-md text-[11px] font-semibold cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '8px 4px', borderRadius: 6, fontSize: 11, fontWeight: 600,
|
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
|
||||||
background: isActive ? cc.bg : 'var(--bg3)',
|
background: isActive ? cc.bg : 'var(--bg3)',
|
||||||
border: isActive ? `1px solid ${cc.text}40` : '1px solid var(--bd)',
|
border: isActive ? `1px solid ${cc.text}40` : '1px solid var(--bd)',
|
||||||
color: isActive ? cc.text : 'var(--t3)',
|
color: isActive ? cc.text : 'var(--t3)',
|
||||||
@ -411,23 +409,20 @@ export function BoardView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 6 }}>매뉴얼 제목</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">매뉴얼 제목</label>
|
||||||
<input type="text" placeholder="매뉴얼 제목을 입력하세요" value={uploadForm.title}
|
<input type="text" placeholder="매뉴얼 제목을 입력하세요" value={uploadForm.title}
|
||||||
onChange={e => setUploadForm(prev => ({ ...prev, title: e.target.value }))}
|
onChange={e => setUploadForm(prev => ({ ...prev, title: e.target.value }))}
|
||||||
style={{ width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12, background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fK)', outline: 'none', boxSizing: 'border-box' }} />
|
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-2 border border-border outline-none" style={{ boxSizing: 'border-box' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 6 }}>버전</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">버전</label>
|
||||||
<input type="text" placeholder="예: v1.0" value={uploadForm.version}
|
<input type="text" placeholder="예: v1.0" value={uploadForm.version}
|
||||||
onChange={e => setUploadForm(prev => ({ ...prev, version: e.target.value }))}
|
onChange={e => setUploadForm(prev => ({ ...prev, version: e.target.value }))}
|
||||||
style={{ width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12, background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fK)', outline: 'none', boxSizing: 'border-box' }} />
|
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-2 border border-border outline-none" style={{ boxSizing: 'border-box' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 6 }}>파일 첨부</label>
|
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">파일 첨부</label>
|
||||||
<div style={{
|
<div className="border-2 border-dashed border-border rounded-md py-6 px-4 text-center bg-bg-2 cursor-pointer relative"
|
||||||
border: '2px dashed var(--bd)', borderRadius: 8, padding: '24px 16px', textAlign: 'center',
|
|
||||||
background: 'var(--bg2)', cursor: 'pointer', position: 'relative',
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.createElement('input')
|
const input = document.createElement('input')
|
||||||
input.type = 'file'
|
input.type = 'file'
|
||||||
@ -442,28 +437,28 @@ export function BoardView() {
|
|||||||
input.click()
|
input.click()
|
||||||
}}>
|
}}>
|
||||||
{uploadForm.fileName ? (
|
{uploadForm.fileName ? (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
|
<div className="flex items-center justify-center gap-2">
|
||||||
<span style={{ fontSize: 20 }}>📄</span>
|
<span className="text-xl">📄</span>
|
||||||
<div style={{ textAlign: 'left' }}>
|
<div className="text-left">
|
||||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>{uploadForm.fileName}</div>
|
<div className="text-xs font-semibold">{uploadForm.fileName}</div>
|
||||||
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fM)' }}>{uploadForm.fileSize}</div>
|
<div className="text-[10px] text-text-3 font-mono">{uploadForm.fileSize}</div>
|
||||||
</div>
|
</div>
|
||||||
<span onClick={(e) => { e.stopPropagation(); setUploadForm(prev => ({ ...prev, fileName: '', fileSize: '' })) }}
|
<span onClick={(e) => { e.stopPropagation(); setUploadForm(prev => ({ ...prev, fileName: '', fileSize: '' })) }}
|
||||||
style={{ fontSize: 12, color: 'var(--t3)', cursor: 'pointer', marginLeft: 8 }}>✕</span>
|
className="text-xs text-text-3 cursor-pointer ml-2">✕</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div style={{ fontSize: 28, opacity: 0.3, marginBottom: 6 }}>📁</div>
|
<div className="text-[28px] opacity-30 mb-1.5">📁</div>
|
||||||
<div style={{ fontSize: 11, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>클릭하여 파일을 선택하세요</div>
|
<div className="text-[11px] text-text-3">클릭하여 파일을 선택하세요</div>
|
||||||
<div style={{ fontSize: 9, color: 'var(--t3)', fontFamily: 'var(--fM)', marginTop: 4 }}>PDF, DOC, HWP, XLSX (최대 100MB)</div>
|
<div className="text-[9px] text-text-3 font-mono mt-1">PDF, DOC, HWP, XLSX (최대 100MB)</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '12px 20px', borderTop: '1px solid var(--bd)', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
<div className="px-5 py-3 border-t border-border flex justify-end gap-2">
|
||||||
<button onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}
|
<button onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}
|
||||||
style={{ padding: '8px 20px', borderRadius: 6, fontSize: 12, fontWeight: 600, background: 'var(--bg3)', border: '1px solid var(--bd)', color: 'var(--t3)', fontFamily: 'var(--fK)', cursor: 'pointer' }}>
|
className="px-5 py-2 rounded-md text-xs font-semibold bg-bg-3 border border-border text-text-3 cursor-pointer">
|
||||||
취소
|
취소
|
||||||
</button>
|
</button>
|
||||||
<button onClick={async () => {
|
<button onClick={async () => {
|
||||||
@ -496,7 +491,7 @@ export function BoardView() {
|
|||||||
alert((err as { message?: string })?.message || '저장에 실패했습니다.')
|
alert((err as { message?: string })?.message || '저장에 실패했습니다.')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{ padding: '8px 24px', borderRadius: 6, fontSize: 12, fontWeight: 600, background: 'rgba(6,182,212,.2)', border: '1px solid rgba(6,182,212,.35)', color: '#22d3ee', fontFamily: 'var(--fK)', cursor: 'pointer' }}>
|
className="px-6 py-2 rounded-md text-xs font-semibold cursor-pointer" style={{ background: 'rgba(6,182,212,.2)', border: '1px solid rgba(6,182,212,.35)', color: '#22d3ee' }}>
|
||||||
{editingManualId ? '✏️ 수정' : '📤 업로드'}
|
{editingManualId ? '✏️ 수정' : '📤 업로드'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -55,127 +55,75 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
|||||||
}, [loadData])
|
}, [loadData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="flex flex-col h-full bg-bg-0">
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
background: 'var(--bg0)',
|
|
||||||
fontFamily: 'var(--fK)'
|
|
||||||
}}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div className="px-5 py-4 border-b border-border flex justify-between items-center bg-bg-1">
|
||||||
padding: '16px 20px',
|
<div className="flex items-center gap-3">
|
||||||
borderBottom: '1px solid var(--bd)',
|
<div className="text-base font-bold flex items-center gap-2">
|
||||||
display: 'flex',
|
<span className="text-[18px]">📋</span>
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
background: 'var(--bg1)'
|
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'var(--t1)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px'
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '18px' }}>📋</span>
|
|
||||||
HNS 대기확산 분석 목록
|
HNS 대기확산 분석 목록
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span className="text-[10px] text-text-3 bg-bg-3 font-mono px-[10px] py-1 rounded-xl">
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
background: 'var(--bg3)',
|
|
||||||
padding: '4px 10px',
|
|
||||||
borderRadius: '12px',
|
|
||||||
fontFamily: 'var(--fM)'
|
|
||||||
}}>
|
|
||||||
총 {analyses.length}건
|
총 {analyses.length}건
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
<div className="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="검색..."
|
placeholder="검색..."
|
||||||
style={{
|
className="bg-bg-3 border border-border rounded-sm text-[11px] px-3 py-2 w-[200px]"
|
||||||
padding: '8px 12px',
|
|
||||||
background: 'var(--bg3)',
|
|
||||||
border: '1px solid var(--bd)',
|
|
||||||
borderRadius: '6px',
|
|
||||||
fontSize: '11px',
|
|
||||||
color: 'var(--t1)',
|
|
||||||
width: '200px',
|
|
||||||
fontFamily: 'var(--fK)'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => onTabChange('analysis')}
|
onClick={() => onTabChange('analysis')}
|
||||||
|
className="text-status-orange text-[11px] font-semibold cursor-pointer flex items-center gap-1 px-4 py-2 rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 16px',
|
|
||||||
borderRadius: '6px',
|
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
border: '1px solid rgba(249,115,22,0.3)',
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(239,68,68,0.1))',
|
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(239,68,68,0.1))',
|
||||||
color: 'var(--orange)',
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: '14px' }}>+</span> 새 분석
|
<span className="text-sm">+</span> 새 분석
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
<div className="flex-1 overflow-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '80px 0', color: 'var(--t3)', fontSize: '12px' }}>로딩 중...</div>
|
<div className="text-center text-text-3 text-[12px] py-20">로딩 중...</div>
|
||||||
) : (
|
) : (
|
||||||
<table style={{
|
<table className="w-full text-[11px] border-collapse">
|
||||||
width: '100%',
|
<thead className="sticky top-0 z-10">
|
||||||
borderCollapse: 'collapse',
|
<tr className="bg-bg-2 border-b border-border">
|
||||||
fontSize: '11px'
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[50px]">번호</th>
|
||||||
}}>
|
<th className="text-left text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[180px]">분석명</th>
|
||||||
<thead style={{ position: 'sticky', top: 0, zIndex: 10 }}>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]">물질</th>
|
||||||
<tr style={{
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[130px]">사고일시</th>
|
||||||
background: 'var(--bg2)',
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]">분석날짜</th>
|
||||||
borderBottom: '1px solid var(--bd)'
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]">사고지점</th>
|
||||||
}}>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]">유출량</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', width: '50px' }}>번호</th>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]">알고리즘</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '180px' }}>분석명</th>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">예측시간</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '100px' }}>물질</th>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '130px' }}>사고일시</th>
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '100px' }}>분석날짜</th>
|
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '120px' }}>사고지점</th>
|
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '90px' }}>유출량</th>
|
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '80px' }}>알고리즘</th>
|
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', width: '70px' }}>예측시간</th>
|
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', width: '70px' }}>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
|
||||||
<span>AEGL-3</span>
|
<span>AEGL-3</span>
|
||||||
<span style={{ fontSize: '8px', color: 'var(--t3)' }}>생명위협</span>
|
<span className="text-[8px] text-text-3">생명위협</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', width: '70px' }}>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
<span>AEGL-2</span>
|
<span>AEGL-2</span>
|
||||||
<span style={{ fontSize: '8px', color: 'var(--t3)' }}>건강피해</span>
|
<span className="text-[8px] text-text-3">건강피해</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', width: '70px' }}>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
<span>AEGL-1</span>
|
<span>AEGL-1</span>
|
||||||
<span style={{ fontSize: '8px', color: 'var(--t3)' }}>불쾌감</span>
|
<span className="text-[8px] text-text-3">불쾌감</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '80px' }}>위험등급</th>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]">위험등급</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '90px' }}>피해반경</th>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]">피해반경</th>
|
||||||
<th style={{ padding: '12px 16px', textAlign: 'center', fontSize: '10px', fontWeight: 600, color: 'var(--t2)', minWidth: '120px' }}>분석자</th>
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]">분석자</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -192,90 +140,74 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={item.hnsAnlysSn}
|
key={item.hnsAnlysSn}
|
||||||
|
className="border-b border-border cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
borderBottom: '1px solid var(--bd)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'background 0.15s',
|
transition: 'background 0.15s',
|
||||||
background: index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'
|
background: index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg2)'}
|
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg2)'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.background = index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'}
|
onMouseLeave={(e) => e.currentTarget.style.background = index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'}
|
||||||
>
|
>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t3)', fontFamily: 'var(--fM)' }}>{item.hnsAnlysSn}</td>
|
<td className="text-center text-text-3 font-mono px-4 py-3">{item.hnsAnlysSn}</td>
|
||||||
<td style={{ padding: '12px 16px', color: 'var(--t1)', fontWeight: 500 }}>{item.anlysNm}</td>
|
<td className="font-medium px-4 py-3">{item.anlysNm}</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
<td className="text-center px-4 py-3">
|
||||||
<span style={{
|
<span
|
||||||
padding: '4px 8px',
|
className="text-[9px] font-semibold text-status-orange px-2 py-1 rounded"
|
||||||
borderRadius: '4px',
|
style={{ background: 'rgba(249,115,22,0.12)' }}
|
||||||
fontSize: '9px',
|
>
|
||||||
fontWeight: 600,
|
|
||||||
background: 'rgba(249,115,22,0.12)',
|
|
||||||
color: 'var(--orange)'
|
|
||||||
}}>
|
|
||||||
{substanceTag(item.sbstNm)}
|
{substanceTag(item.sbstNm)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t2)', fontFamily: 'var(--fM)', fontSize: '10px' }}>{formatDate(item.acdntDtm, 'full')}</td>
|
<td className="text-center text-text-2 font-mono text-[10px] px-4 py-3">{formatDate(item.acdntDtm, 'full')}</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t3)', fontFamily: 'var(--fM)', fontSize: '10px' }}>{formatDate(item.regDtm, 'date')}</td>
|
<td className="text-center text-text-3 font-mono text-[10px] px-4 py-3">{formatDate(item.regDtm, 'date')}</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t2)' }}>{item.locNm || '—'}</td>
|
<td className="text-center text-text-2 px-4 py-3">{item.locNm || '—'}</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t2)', fontFamily: 'var(--fM)' }}>{amount}</td>
|
<td className="text-center text-text-2 font-mono px-4 py-3">{amount}</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
<td className="text-center px-4 py-3">
|
||||||
<span style={{
|
<span
|
||||||
padding: '4px 8px',
|
className="text-[9px] font-semibold text-primary-cyan px-2 py-1 rounded"
|
||||||
borderRadius: '4px',
|
style={{ background: 'rgba(6,182,212,0.12)' }}
|
||||||
fontSize: '9px',
|
>
|
||||||
fontWeight: 600,
|
|
||||||
background: 'rgba(6,182,212,0.12)',
|
|
||||||
color: 'var(--cyan)'
|
|
||||||
}}>
|
|
||||||
{item.algoCd || '—'}
|
{item.algoCd || '—'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t2)', fontFamily: 'var(--fM)' }}>{item.fcstHr ? `${item.fcstHr}H` : '—'}</td>
|
<td className="text-center text-text-2 font-mono px-4 py-3">{item.fcstHr ? `${item.fcstHr}H` : '—'}</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
<td className="text-center px-4 py-3">
|
||||||
<div style={{
|
<div
|
||||||
width: '24px',
|
className="w-6 h-6 rounded-full mx-auto"
|
||||||
height: '24px',
|
style={{
|
||||||
borderRadius: '50%',
|
background: aegl3 ? 'rgba(239,68,68,0.8)' : 'rgba(255,255,255,0.1)',
|
||||||
background: aegl3 ? 'rgba(239,68,68,0.8)' : 'rgba(255,255,255,0.1)',
|
border: aegl3 ? 'none' : '1px solid var(--bd)'
|
||||||
margin: '0 auto',
|
}}
|
||||||
border: aegl3 ? 'none' : '1px solid var(--bd)'
|
/>
|
||||||
}} />
|
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
<td className="text-center px-4 py-3">
|
||||||
<div style={{
|
<div
|
||||||
width: '24px',
|
className="w-6 h-6 rounded-full mx-auto"
|
||||||
height: '24px',
|
style={{
|
||||||
borderRadius: '50%',
|
background: aegl2 ? 'rgba(249,115,22,0.8)' : 'rgba(255,255,255,0.1)',
|
||||||
background: aegl2 ? 'rgba(249,115,22,0.8)' : 'rgba(255,255,255,0.1)',
|
border: aegl2 ? 'none' : '1px solid var(--bd)'
|
||||||
margin: '0 auto',
|
}}
|
||||||
border: aegl2 ? 'none' : '1px solid var(--bd)'
|
/>
|
||||||
}} />
|
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
<td className="text-center px-4 py-3">
|
||||||
<div style={{
|
<div
|
||||||
width: '24px',
|
className="w-6 h-6 rounded-full mx-auto"
|
||||||
height: '24px',
|
style={{
|
||||||
borderRadius: '50%',
|
background: aegl1 ? 'rgba(234,179,8,0.8)' : 'rgba(255,255,255,0.1)',
|
||||||
background: aegl1 ? 'rgba(234,179,8,0.8)' : 'rgba(255,255,255,0.1)',
|
border: aegl1 ? 'none' : '1px solid var(--bd)'
|
||||||
margin: '0 auto',
|
}}
|
||||||
border: aegl1 ? 'none' : '1px solid var(--bd)'
|
/>
|
||||||
}} />
|
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
<td className="text-center px-4 py-3">
|
||||||
<span style={{
|
<span
|
||||||
padding: '4px 10px',
|
className="text-[9px] font-semibold px-[10px] py-1 rounded"
|
||||||
borderRadius: '4px',
|
style={{ background: riskStyle.bg, color: riskStyle.color }}
|
||||||
fontSize: '9px',
|
>
|
||||||
fontWeight: 600,
|
|
||||||
background: riskStyle.bg,
|
|
||||||
color: riskStyle.color,
|
|
||||||
}}>
|
|
||||||
{riskLabel}
|
{riskLabel}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t2)', fontFamily: 'var(--fM)' }}>{damageRadius}</td>
|
<td className="text-center text-text-2 font-mono px-4 py-3">{damageRadius}</td>
|
||||||
<td style={{ padding: '12px 16px', textAlign: 'center', color: 'var(--t3)', fontSize: '10px' }}>{item.analystNm || '—'}</td>
|
<td className="text-center text-text-3 text-[10px] px-4 py-3">{item.analystNm || '—'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -284,7 +216,7 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && analyses.length === 0 && (
|
{!loading && analyses.length === 0 && (
|
||||||
<div style={{ textAlign: 'center', padding: '80px 0', color: 'var(--t3)', fontSize: '12px' }}>분석 데이터가 없습니다.</div>
|
<div className="text-center text-text-3 text-[12px] py-20">분석 데이터가 없습니다.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -51,28 +51,24 @@ export function HNSLeftPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-bg-1 border-r border-border overflow-hidden">
|
<div className="flex flex-col h-full bg-bg-1 border-r border-border overflow-hidden">
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent" style={{ background: 'var(--bg0)' }}>
|
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent bg-bg-0">
|
||||||
{activeSubTab === 'analysis' && (
|
{activeSubTab === 'analysis' && (
|
||||||
<div style={{ padding: '16px' }}>
|
<div className="p-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' }}>
|
<div className="flex items-center justify-between mb-[14px]">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div className="flex items-center gap-2.5">
|
||||||
<div style={{
|
<div
|
||||||
width: '36px',
|
className="w-9 h-9 rounded-md flex items-center justify-center text-[18px]"
|
||||||
height: '36px',
|
style={{
|
||||||
borderRadius: '8px',
|
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(168,85,247,0.1))',
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(168,85,247,0.1))',
|
border: '1px solid rgba(249,115,22,0.25)',
|
||||||
border: '1px solid rgba(249,115,22,0.25)',
|
}}
|
||||||
display: 'flex',
|
>🧪</div>
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: '18px'
|
|
||||||
}}>🧪</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '14px', fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>
|
<div className="text-sm font-bold">
|
||||||
HNS 대기확산 예측
|
HNS 대기확산 예측
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[9px] text-text-3">
|
||||||
ALOHA/CAMEO 기반 대기확산 시뮬레이션
|
ALOHA/CAMEO 기반 대기확산 시뮬레이션
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -80,38 +76,36 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Single Column Layout */}
|
{/* Single Column Layout */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div className="flex flex-col gap-3">
|
||||||
|
|
||||||
{/* 사고 기본정보 */}
|
{/* 사고 기본정보 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '10px', padding: '14px' }}>
|
<div className="bg-bg-3 border border-border rounded-md p-[14px]">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="text-[11px] font-bold text-primary-cyan mb-3 flex items-center gap-1.5">
|
||||||
📋 사고 기본정보
|
📋 사고 기본정보
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="flex flex-col gap-2">
|
||||||
{/* 사고명 */}
|
{/* 사고명 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>사고명</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">사고명</label>
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
value={accidentName}
|
value={accidentName}
|
||||||
onChange={(e) => setAccidentName(e.target.value)}
|
onChange={(e) => setAccidentName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사고일시 + 예측시간 */}
|
{/* 사고일시 + 예측시간 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>사고일시</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">사고일시</label>
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
defaultValue="2025-02-11T05:02"
|
defaultValue="2025-02-11T05:02"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>예측시간</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">예측시간</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="hns-inp"
|
className="hns-inp"
|
||||||
value={predictionTime}
|
value={predictionTime}
|
||||||
@ -128,52 +122,46 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사고지점 */}
|
{/* 사고지점 */}
|
||||||
<div style={{ padding: '10px', background: 'var(--bg0)', border: '1px solid rgba(6,182,212,0.2)', borderRadius: '8px' }}>
|
<div
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
className="p-[10px] rounded-md bg-bg-0"
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 0 }}>📍 사고지점</label>
|
style={{ border: '1px solid rgba(6,182,212,0.2)' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="hns-lbl text-[8px] text-text-3">📍 사고지점</label>
|
||||||
<button
|
<button
|
||||||
onClick={onMapSelectClick}
|
onClick={onMapSelectClick}
|
||||||
|
className="text-primary-cyan text-[8px] font-bold cursor-pointer px-[10px] py-1 rounded"
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 10px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid rgba(6,182,212,0.3)',
|
border: '1px solid rgba(6,182,212,0.3)',
|
||||||
background: 'rgba(6,182,212,0.08)',
|
background: 'rgba(6,182,212,0.08)',
|
||||||
color: 'var(--cyan)',
|
|
||||||
fontSize: '8px',
|
|
||||||
fontWeight: 700,
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
transition: '0.15s'
|
transition: '0.15s'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🗺 지도에서 클릭
|
🗺 지도에서 클릭
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px', marginBottom: '6px' }}>
|
<div className="grid grid-cols-2 gap-1.5 mb-1.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>위도</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">위도</label>
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
value={incidentCoord.lat.toFixed(4)}
|
value={incidentCoord.lat.toFixed(4)}
|
||||||
onChange={(e) => onCoordChange({ ...incidentCoord, lat: parseFloat(e.target.value) || 0 })}
|
onChange={(e) => onCoordChange({ ...incidentCoord, lat: parseFloat(e.target.value) || 0 })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>경도</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">경도</label>
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
value={incidentCoord.lon.toFixed(4)}
|
value={incidentCoord.lon.toFixed(4)}
|
||||||
onChange={(e) => onCoordChange({ ...incidentCoord, lon: parseFloat(e.target.value) || 0 })}
|
onChange={(e) => onCoordChange({ ...incidentCoord, lon: parseFloat(e.target.value) || 0 })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>상세 위치</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">상세 위치</label>
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
value={locationName}
|
value={locationName}
|
||||||
onChange={(e) => setLocationName(e.target.value)}
|
onChange={(e) => setLocationName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -181,48 +169,51 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 기상 정보 */}
|
{/* 기상 정보 */}
|
||||||
<div style={{ padding: '10px', background: 'linear-gradient(135deg, rgba(168,85,247,0.04), rgba(6,182,212,0.03))', border: '1px solid rgba(168,85,247,0.15)', borderRadius: '8px' }}>
|
<div
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
className="p-[10px] rounded-md"
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
style={{ background: 'linear-gradient(135deg, rgba(168,85,247,0.04), rgba(6,182,212,0.03))', border: '1px solid rgba(168,85,247,0.15)' }}
|
||||||
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'var(--green)' }}></div>
|
>
|
||||||
<span className="hns-lbl" style={{ fontSize: '8px', color: 'var(--purple)', fontFamily: 'var(--fK)', marginBottom: 0 }}>🌬 기상정보 (자동조회)</span>
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-[5px]">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-status-green"></div>
|
||||||
|
<span className="hns-lbl text-[8px] text-primary-purple">🌬 기상정보 (자동조회)</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: '7px', color: 'var(--t3)', fontFamily: 'var(--fM)' }}>KMA API · 울산 AWS</span>
|
<span className="text-[7px] text-text-3 font-mono">KMA API · 울산 AWS</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '6px', marginBottom: '6px' }}>
|
<div className="grid grid-cols-4 gap-1.5 mb-1.5">
|
||||||
<div style={{ textAlign: 'center', padding: '6px 2px', background: 'var(--bg0)', borderRadius: '4px', border: '1px solid var(--bd)' }}>
|
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 800, fontFamily: 'var(--fM)', color: 'var(--cyan)' }}>5.2</div>
|
<div className="text-[12px] font-extrabold font-mono text-primary-cyan">5.2</div>
|
||||||
<div style={{ fontSize: '7px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>풍속(m/s)</div>
|
<div className="text-[7px] text-text-3">풍속(m/s)</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center', padding: '6px 2px', background: 'var(--bg0)', borderRadius: '4px', border: '1px solid var(--bd)' }}>
|
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 800, fontFamily: 'var(--fM)', color: 'var(--cyan)' }}>SW 225°</div>
|
<div className="text-[12px] font-extrabold font-mono text-primary-cyan">SW 225°</div>
|
||||||
<div style={{ fontSize: '7px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>풍향</div>
|
<div className="text-[7px] text-text-3">풍향</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center', padding: '6px 2px', background: 'var(--bg0)', borderRadius: '4px', border: '1px solid var(--bd)' }}>
|
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 800, fontFamily: 'var(--fM)', color: 'var(--orange)' }}>8.5°C</div>
|
<div className="text-[12px] font-extrabold font-mono text-status-orange">8.5°C</div>
|
||||||
<div style={{ fontSize: '7px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>기온</div>
|
<div className="text-[7px] text-text-3">기온</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center', padding: '6px 2px', background: 'var(--bg0)', borderRadius: '4px', border: '1px solid var(--bd)' }}>
|
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 800, fontFamily: 'var(--fM)', color: 'var(--blue)' }}>62%</div>
|
<div className="text-[12px] font-extrabold font-mono text-primary-blue">62%</div>
|
||||||
<div style={{ fontSize: '7px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>습도</div>
|
<div className="text-[7px] text-text-3">습도</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 6px', background: 'var(--bg0)', borderRadius: '3px', fontSize: '8px', fontFamily: 'var(--fK)' }}>
|
<div className="flex justify-between bg-bg-0 text-[8px] px-1.5 py-[3px] rounded-[3px]">
|
||||||
<span style={{ color: 'var(--t3)' }}>대기안정도</span>
|
<span className="text-text-3">대기안정도</span>
|
||||||
<span style={{ fontWeight: 600, color: 'var(--t1)' }}>D (중립)</span>
|
<span className="font-semibold">D (중립)</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 6px', background: 'var(--bg0)', borderRadius: '3px', fontSize: '8px', fontFamily: 'var(--fK)' }}>
|
<div className="flex justify-between bg-bg-0 text-[8px] px-1.5 py-[3px] rounded-[3px]">
|
||||||
<span style={{ color: 'var(--t3)' }}>지표 조도</span>
|
<span className="text-text-3">지표 조도</span>
|
||||||
<span style={{ fontWeight: 600, color: 'var(--t1)' }}>해안</span>
|
<span className="font-semibold">해안</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 알고리즘 선택 */}
|
{/* 알고리즘 선택 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>예측 알고리즘</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">예측 알고리즘</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="hns-inp"
|
className="hns-inp"
|
||||||
value={algorithm}
|
value={algorithm}
|
||||||
@ -236,7 +227,7 @@ export function HNSLeftPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>확산 등급 기준</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">확산 등급 기준</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="hns-inp"
|
className="hns-inp"
|
||||||
value={criteriaModel}
|
value={criteriaModel}
|
||||||
@ -254,14 +245,14 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 물질 정보 */}
|
{/* 물질 정보 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '10px', padding: '14px' }}>
|
<div className="bg-bg-3 border border-border rounded-md p-[14px]">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fK)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="text-[11px] font-bold text-status-orange mb-3 flex items-center gap-1.5">
|
||||||
🧪 물질 정보
|
🧪 물질 정보
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="flex flex-col gap-2">
|
||||||
{/* 물질 분류 */}
|
{/* 물질 분류 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>물질 분류</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">물질 분류</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="hns-inp"
|
className="hns-inp"
|
||||||
value={materialCategory}
|
value={materialCategory}
|
||||||
@ -279,7 +270,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 물질명 */}
|
{/* 물질명 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>물질명</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">물질명</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="hns-inp"
|
className="hns-inp"
|
||||||
value={substance}
|
value={substance}
|
||||||
@ -297,17 +288,15 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* UN번호 / CAS번호 */}
|
{/* UN번호 / CAS번호 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>UN번호 / CAS번호</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">UN번호 / CAS번호</label>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
value={unNumber}
|
value={unNumber}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
value={casNumber}
|
value={casNumber}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
@ -315,20 +304,19 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 유출량 + 단위 */}
|
{/* 유출량 + 단위 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>유출량</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">유출량</label>
|
||||||
<input
|
<input
|
||||||
className="hns-inp"
|
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||||
type="number"
|
type="number"
|
||||||
style={{ width: '100%', padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}
|
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
step="0.1"
|
step="0.1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>단위</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">단위</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="hns-inp"
|
className="hns-inp"
|
||||||
value={unit}
|
value={unit}
|
||||||
@ -345,7 +333,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 유출 형태 */}
|
{/* 유출 형태 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="hns-lbl" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>유출 형태</label>
|
<label className="hns-lbl text-[8px] text-text-3 block mb-1">유출 형태</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="hns-inp"
|
className="hns-inp"
|
||||||
value={releaseType}
|
value={releaseType}
|
||||||
@ -359,55 +347,61 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 물질 위험 특성 */}
|
{/* 물질 위험 특성 */}
|
||||||
<div style={{ padding: '8px', background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)', borderRadius: '6px', marginTop: '2px' }}>
|
<div
|
||||||
<div style={{ fontSize: '8px', fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fK)', marginBottom: '4px' }}>
|
className="p-2 rounded-sm mt-0.5"
|
||||||
|
style={{ background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)' }}
|
||||||
|
>
|
||||||
|
<div className="text-[8px] font-bold text-status-orange mb-1">
|
||||||
⚠ 물질 위험 특성
|
⚠ 물질 위험 특성
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px', fontSize: '8px', fontFamily: 'var(--fK)' }}>
|
<div className="grid grid-cols-2 gap-[3px] text-[8px]">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div className="flex justify-between">
|
||||||
<span style={{ color: 'var(--t3)' }}>인화점</span>
|
<span className="text-text-3">인화점</span>
|
||||||
<span style={{ color: 'var(--red)', fontWeight: 600, fontFamily: 'var(--fM)' }}>4°C</span>
|
<span className="text-status-red font-semibold font-mono">4°C</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div className="flex justify-between">
|
||||||
<span style={{ color: 'var(--t3)' }}>비중</span>
|
<span className="text-text-3">비중</span>
|
||||||
<span style={{ color: 'var(--t1)', fontFamily: 'var(--fM)' }}>0.867</span>
|
<span className="font-mono">0.867</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div className="flex justify-between">
|
||||||
<span style={{ color: 'var(--t3)' }}>증기압</span>
|
<span className="text-text-3">증기압</span>
|
||||||
<span style={{ color: 'var(--t1)', fontFamily: 'var(--fM)' }}>22 mmHg</span>
|
<span className="font-mono">22 mmHg</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div className="flex justify-between">
|
||||||
<span style={{ color: 'var(--t3)' }}>IDLH</span>
|
<span className="text-text-3">IDLH</span>
|
||||||
<span style={{ color: 'var(--red)', fontWeight: 600, fontFamily: 'var(--fM)' }}>500 ppm</span>
|
<span className="text-status-red font-semibold font-mono">500 ppm</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div className="flex justify-between">
|
||||||
<span style={{ color: 'var(--t3)' }}>TWA</span>
|
<span className="text-text-3">TWA</span>
|
||||||
<span style={{ color: 'var(--t1)', fontFamily: 'var(--fM)' }}>50 ppm</span>
|
<span className="font-mono">50 ppm</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div className="flex justify-between">
|
||||||
<span style={{ color: 'var(--t3)' }}>AEGL-2(1h)</span>
|
<span className="text-text-3">AEGL-2(1h)</span>
|
||||||
<span style={{ color: 'var(--orange)', fontWeight: 600, fontFamily: 'var(--fM)' }}>150 ppm</span>
|
<span className="text-status-orange font-semibold font-mono">150 ppm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AEGL 등급 범례 */}
|
{/* AEGL 등급 범례 */}
|
||||||
<div style={{ padding: '8px', background: 'rgba(168,85,247,0.05)', border: '1px solid rgba(168,85,247,0.12)', borderRadius: '6px' }}>
|
<div
|
||||||
<div style={{ fontSize: '8px', fontWeight: 700, color: 'var(--purple)', fontFamily: 'var(--fK)', marginBottom: '4px' }}>
|
className="p-2 rounded-sm"
|
||||||
|
style={{ background: 'rgba(168,85,247,0.05)', border: '1px solid rgba(168,85,247,0.12)' }}
|
||||||
|
>
|
||||||
|
<div className="text-[8px] font-bold text-primary-purple mb-1">
|
||||||
📊 확산 등급 기준 (AEGL)
|
📊 확산 등급 기준 (AEGL)
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', fontSize: '8px', fontFamily: 'var(--fK)' }}>
|
<div className="flex flex-col gap-0.5 text-[8px]">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div className="flex items-center gap-1">
|
||||||
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: 'rgba(239,68,68,0.7)' }}></div>
|
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(239,68,68,0.7)' }}></div>
|
||||||
<span style={{ color: 'var(--t3)' }}>AEGL-3 (생명위협) — 500 ppm</span>
|
<span className="text-text-3">AEGL-3 (생명위협) — 500 ppm</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div className="flex items-center gap-1">
|
||||||
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: 'rgba(249,115,22,0.7)' }}></div>
|
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(249,115,22,0.7)' }}></div>
|
||||||
<span style={{ color: 'var(--t3)' }}>AEGL-2 (건강피해) — 150 ppm</span>
|
<span className="text-text-3">AEGL-2 (건강피해) — 150 ppm</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div className="flex items-center gap-1">
|
||||||
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: 'rgba(234,179,8,0.7)' }}></div>
|
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(234,179,8,0.7)' }}></div>
|
||||||
<span style={{ color: 'var(--t3)' }}>AEGL-1 (불쾌감) — 37 ppm</span>
|
<span className="text-text-3">AEGL-1 (불쾌감) — 37 ppm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -416,22 +410,17 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 실행 버튼 */}
|
{/* 실행 버튼 */}
|
||||||
<div style={{ display: 'flex', gap: '8px', marginTop: '14px', justifyContent: 'center' }}>
|
<div className="flex gap-2 mt-[14px] justify-center">
|
||||||
<button
|
<button
|
||||||
onClick={onRunPrediction}
|
onClick={onRunPrediction}
|
||||||
disabled={isRunningPrediction}
|
disabled={isRunningPrediction}
|
||||||
|
className="text-white text-[13px] font-bold rounded-md px-[40px] py-3"
|
||||||
style={{
|
style={{
|
||||||
padding: '12px 40px',
|
|
||||||
background: isRunningPrediction
|
background: isRunningPrediction
|
||||||
? 'var(--t4)'
|
? 'var(--t4)'
|
||||||
: 'linear-gradient(135deg, var(--orange), var(--red))',
|
: 'linear-gradient(135deg, var(--orange), var(--red))',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '8px',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: 700,
|
|
||||||
cursor: isRunningPrediction ? 'not-allowed' : 'pointer',
|
cursor: isRunningPrediction ? 'not-allowed' : 'pointer',
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
transition: '0.2s',
|
transition: '0.2s',
|
||||||
boxShadow: isRunningPrediction ? 'none' : '0 4px 16px rgba(249,115,22,0.25)'
|
boxShadow: isRunningPrediction ? 'none' : '0 4px 16px rgba(249,115,22,0.25)'
|
||||||
}}
|
}}
|
||||||
@ -440,17 +429,7 @@ export function HNSLeftPanel({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
style={{
|
className="bg-bg-3 border border-border rounded-md text-text-2 text-[12px] font-semibold cursor-pointer px-6 py-3"
|
||||||
padding: '12px 24px',
|
|
||||||
background: 'var(--bg3)',
|
|
||||||
border: '1px solid var(--bd)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
color: 'var(--t2)',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: 'var(--fK)'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
🔄 초기화
|
🔄 초기화
|
||||||
</button>
|
</button>
|
||||||
@ -459,39 +438,35 @@ export function HNSLeftPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSubTab === 'list' && (
|
{activeSubTab === 'list' && (
|
||||||
<div style={{ padding: '16px' }}>
|
<div className="p-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px' }}>
|
<div className="flex items-center gap-2.5 mb-[14px]">
|
||||||
<div style={{
|
<div
|
||||||
width: '36px',
|
className="w-9 h-9 rounded-md flex items-center justify-center text-[18px]"
|
||||||
height: '36px',
|
style={{
|
||||||
borderRadius: '8px',
|
background: 'linear-gradient(135deg, rgba(6,182,212,0.15), rgba(168,85,247,0.1))',
|
||||||
background: 'linear-gradient(135deg, rgba(6,182,212,0.15), rgba(168,85,247,0.1))',
|
border: '1px solid rgba(6,182,212,0.25)',
|
||||||
border: '1px solid rgba(6,182,212,0.25)',
|
}}
|
||||||
display: 'flex',
|
>📋</div>
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: '18px'
|
|
||||||
}}>📋</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '14px', fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>
|
<div className="text-sm font-bold">
|
||||||
분석 목록
|
분석 목록
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[9px] text-text-3">
|
||||||
저장된 대기확산 예측 결과
|
저장된 대기확산 예측 결과
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필터 섹션 */}
|
{/* 필터 섹션 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '10px', padding: '14px', marginBottom: '12px' }}>
|
<div className="bg-bg-3 border border-border rounded-md p-[14px] mb-3">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="text-[11px] font-bold text-primary-cyan mb-3 flex items-center gap-1.5">
|
||||||
🔍 필터
|
🔍 필터
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="flex flex-col gap-2">
|
||||||
{/* 기간 선택 */}
|
{/* 기간 선택 */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>기간</label>
|
<label className="text-[8px] text-text-3 block mb-1">기간</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value="최근 7일"
|
value="최근 7일"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -506,7 +481,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 물질 분류 */}
|
{/* 물질 분류 */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>물질 분류</label>
|
<label className="text-[8px] text-text-3 block mb-1">물질 분류</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value="전체"
|
value="전체"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -522,7 +497,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 위험도 */}
|
{/* 위험도 */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: '4px' }}>위험도</label>
|
<label className="text-[8px] text-text-3 block mb-1">위험도</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value="전체"
|
value="전체"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -538,22 +513,22 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 통계 요약 */}
|
{/* 통계 요약 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '10px', padding: '14px' }}>
|
<div className="bg-bg-3 border border-border rounded-md p-[14px]">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--purple)', fontFamily: 'var(--fK)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="text-[11px] font-bold text-primary-purple mb-3 flex items-center gap-1.5">
|
||||||
📊 통계
|
📊 통계
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="flex flex-col gap-2">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px', background: 'var(--bg0)', borderRadius: 'var(--rS)' }}>
|
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
|
||||||
<span style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>전체 분석</span>
|
<span className="text-[10px] text-text-3">전체 분석</span>
|
||||||
<span style={{ fontSize: '14px', fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fM)' }}>8건</span>
|
<span className="text-sm font-bold text-primary-cyan font-mono">8건</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px', background: 'var(--bg0)', borderRadius: 'var(--rS)' }}>
|
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
|
||||||
<span style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>고위험 (AEGL-3)</span>
|
<span className="text-[10px] text-text-3">고위험 (AEGL-3)</span>
|
||||||
<span style={{ fontSize: '14px', fontWeight: 700, color: 'var(--red)', fontFamily: 'var(--fM)' }}>3건</span>
|
<span className="text-sm font-bold text-status-red font-mono">3건</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px', background: 'var(--bg0)', borderRadius: 'var(--rS)' }}>
|
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
|
||||||
<span style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>중위험 (AEGL-2)</span>
|
<span className="text-[10px] text-text-3">중위험 (AEGL-2)</span>
|
||||||
<span style={{ fontSize: '14px', fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fM)' }}>5건</span>
|
<span className="text-sm font-bold text-status-orange font-mono">5건</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -79,61 +79,36 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={backdropRef}
|
ref={backdropRef}
|
||||||
style={{
|
className="fixed inset-0 z-[9999] flex items-center justify-center"
|
||||||
position: 'fixed', inset: 0, zIndex: 9999,
|
style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)' }}
|
||||||
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div style={{
|
<div className="w-[400px] max-h-[calc(100vh-100px)] bg-bg-1 border border-border rounded-[14px] overflow-hidden flex flex-col"
|
||||||
width: '400px', maxHeight: 'calc(100vh - 100px)',
|
style={{ boxShadow: '0 20px 60px rgba(0,0,0,0.5)' }}>
|
||||||
background: 'var(--bg1)', border: '1px solid var(--bd)',
|
|
||||||
borderRadius: '14px', overflow: 'hidden',
|
|
||||||
display: 'flex', flexDirection: 'column',
|
|
||||||
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
|
||||||
}}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div className="px-5 py-4 border-b border-border flex items-center gap-3">
|
||||||
padding: '16px 20px', borderBottom: '1px solid var(--bd)',
|
<div className="w-9 h-9 rounded-[10px] border border-[rgba(249,115,22,0.3)] flex items-center justify-center text-base shrink-0"
|
||||||
display: 'flex', alignItems: 'center', gap: '12px',
|
style={{ background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))' }}>🔄</div>
|
||||||
}}>
|
<div className="flex-1">
|
||||||
<div style={{
|
<h2 className="text-[15px] font-bold m-0">
|
||||||
width: '36px', height: '36px', borderRadius: '10px',
|
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))',
|
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px',
|
|
||||||
}}>🔄</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<h2 style={{ fontSize: '15px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', margin: 0 }}>
|
|
||||||
HNS 대기확산 재계산
|
HNS 대기확산 재계산
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>
|
<div className="text-[10px] text-text-3 mt-0.5">
|
||||||
물질·기상조건을 수정하여 대기확산 예측을 재실행합니다
|
물질·기상조건을 수정하여 대기확산 예측을 재실행합니다
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} style={{
|
<button onClick={onClose} className="w-7 h-7 rounded-md border border-border bg-bg-3 text-text-3 text-xs cursor-pointer flex items-center justify-center shrink-0">✕</button>
|
||||||
width: '28px', height: '28px', borderRadius: '6px',
|
|
||||||
border: '1px solid var(--bd)', background: 'var(--bg3)',
|
|
||||||
color: 'var(--t3)', fontSize: '12px', cursor: 'pointer',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}>✕</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div style={{
|
<div className="flex-1 overflow-y-auto px-5 py-4 flex flex-col gap-[14px]"
|
||||||
flex: 1, overflowY: 'auto', padding: '16px 20px',
|
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||||
display: 'flex', flexDirection: 'column', gap: '14px',
|
|
||||||
scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent',
|
|
||||||
}}>
|
|
||||||
{/* 현재 분석 정보 */}
|
{/* 현재 분석 정보 */}
|
||||||
<div style={{
|
<div className="py-2.5 px-3 border border-[rgba(249,115,22,0.15)] rounded-md"
|
||||||
padding: '10px 12px', background: 'rgba(249,115,22,0.04)',
|
style={{ background: 'rgba(249,115,22,0.04)' }}>
|
||||||
border: '1px solid rgba(249,115,22,0.15)', borderRadius: '8px',
|
<div className="text-[9px] font-bold text-status-orange mb-1.5">
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: '9px', fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fK)', marginBottom: '6px' }}>
|
|
||||||
현재 분석 정보
|
현재 분석 정보
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px', fontSize: '9px' }}>
|
<div className="grid grid-cols-2 gap-1 text-[9px]">
|
||||||
<InfoRow label="사고명" value="울산 온산항 톨루엔 유출" />
|
<InfoRow label="사고명" value="울산 온산항 톨루엔 유출" />
|
||||||
<InfoRow label="물질" value="톨루엔 (Toluene)" />
|
<InfoRow label="물질" value="톨루엔 (Toluene)" />
|
||||||
<InfoRow label="유출량" value="2.5 ton" />
|
<InfoRow label="유출량" value="2.5 ton" />
|
||||||
@ -149,16 +124,16 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp
|
|||||||
</FG>
|
</FG>
|
||||||
|
|
||||||
{/* 유출 유형 + 유출량 */}
|
{/* 유출 유형 + 유출량 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
|
<div className="grid grid-cols-2 gap-[10px]">
|
||||||
<FG label="유출 유형">
|
<FG label="유출 유형">
|
||||||
<select className="prd-i" value={releaseType} onChange={e => setReleaseType(e.target.value)}>
|
<select className="prd-i" value={releaseType} onChange={e => setReleaseType(e.target.value)}>
|
||||||
{RELEASE_TYPES.map(r => <option key={r} value={r}>{r}</option>)}
|
{RELEASE_TYPES.map(r => <option key={r} value={r}>{r}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FG>
|
</FG>
|
||||||
<FG label="유출량">
|
<FG label="유출량">
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div className="flex gap-1">
|
||||||
<input className="prd-i" type="number" value={amount} onChange={e => setAmount(Number(e.target.value))} step={0.1} style={{ flex: 1 }} />
|
<input className="prd-i flex-1" type="number" value={amount} onChange={e => setAmount(Number(e.target.value))} step={0.1} />
|
||||||
<select className="prd-i" value={unit} onChange={e => setUnit(e.target.value as typeof unit)} style={{ width: '55px' }}>
|
<select className="prd-i w-[55px]" value={unit} onChange={e => setUnit(e.target.value as typeof unit)}>
|
||||||
{['t', 'kg', 'm³', 'L'].map(u => <option key={u} value={u}>{u}</option>)}
|
{['t', 'kg', 'm³', 'L'].map(u => <option key={u} value={u}>{u}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -166,7 +141,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 풍향 / 풍속 / 기온 */}
|
{/* 풍향 / 풍속 / 기온 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '10px' }}>
|
<div className="grid grid-cols-3 gap-[10px]">
|
||||||
<FG label="풍향">
|
<FG label="풍향">
|
||||||
<select className="prd-i" value={windDir} onChange={e => setWindDir(e.target.value)}>
|
<select className="prd-i" value={windDir} onChange={e => setWindDir(e.target.value)}>
|
||||||
{['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'].map(d => <option key={d} value={d}>{d}</option>)}
|
{['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'].map(d => <option key={d} value={d}>{d}</option>)}
|
||||||
@ -181,7 +156,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 대기안정도 + 확산 모델 */}
|
{/* 대기안정도 + 확산 모델 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
|
<div className="grid grid-cols-2 gap-[10px]">
|
||||||
<FG label="대기안정도 (Pasquill)">
|
<FG label="대기안정도 (Pasquill)">
|
||||||
<select className="prd-i" value={stability} onChange={e => setStability(e.target.value)}>
|
<select className="prd-i" value={stability} onChange={e => setStability(e.target.value)}>
|
||||||
{STABILITIES.map(s => <option key={s} value={s}>{s}</option>)}
|
{STABILITIES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||||
@ -203,37 +178,32 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp
|
|||||||
|
|
||||||
{/* 유출 위치 */}
|
{/* 유출 위치 */}
|
||||||
<FG label="유출 위치 (좌표)">
|
<FG label="유출 위치 (좌표)">
|
||||||
<div style={{ display: 'flex', gap: '6px' }}>
|
<div className="flex gap-1.5">
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '3px' }}>위도 (N)</div>
|
<div className="text-[8px] text-text-3 mb-[3px]">위도 (N)</div>
|
||||||
<input className="prd-i" type="number" value={lat} step={0.0001} onChange={e => setLat(Number(e.target.value))} style={{ fontFamily: 'var(--fM)' }} />
|
<input className="prd-i font-mono" type="number" value={lat} step={0.0001} onChange={e => setLat(Number(e.target.value))} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '3px' }}>경도 (E)</div>
|
<div className="text-[8px] text-text-3 mb-[3px]">경도 (E)</div>
|
||||||
<input className="prd-i" type="number" value={lon} step={0.0001} onChange={e => setLon(Number(e.target.value))} style={{ fontFamily: 'var(--fM)' }} />
|
<input className="prd-i font-mono" type="number" value={lon} step={0.0001} onChange={e => setLon(Number(e.target.value))} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FG>
|
</FG>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{ padding: '14px 20px', borderTop: '1px solid var(--bd)', display: 'flex', gap: '8px' }}>
|
<div className="px-5 py-[14px] border-t border-border flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={phase !== 'editing'}
|
disabled={phase !== 'editing'}
|
||||||
style={{
|
className="flex-1 py-2.5 text-xs font-semibold rounded-md cursor-pointer bg-bg-3 border border-border text-text-2"
|
||||||
flex: 1, padding: '10px', fontSize: '12px', fontWeight: 600,
|
style={{ opacity: phase !== 'editing' ? 0.5 : 1 }}
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer',
|
|
||||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
|
||||||
color: 'var(--t2)', opacity: phase !== 'editing' ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>취소</button>
|
>취소</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleRun}
|
onClick={handleRun}
|
||||||
disabled={phase !== 'editing'}
|
disabled={phase !== 'editing'}
|
||||||
|
className="flex-[2] py-2.5 text-xs font-bold rounded-md"
|
||||||
style={{
|
style={{
|
||||||
flex: 2, padding: '10px', fontSize: '12px', fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px',
|
|
||||||
cursor: phase === 'editing' ? 'pointer' : 'wait',
|
cursor: phase === 'editing' ? 'pointer' : 'wait',
|
||||||
background: phase === 'done'
|
background: phase === 'done'
|
||||||
? 'rgba(34,197,94,0.15)'
|
? 'rgba(34,197,94,0.15)'
|
||||||
@ -263,7 +233,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp
|
|||||||
function FG({ label, children }: { label: string; children: React.ReactNode }) {
|
function FG({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: '6px' }}>{label}</div>
|
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--t2)', marginBottom: '6px' }}>{label}</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -272,8 +242,8 @@ function FG({ label, children }: { label: string; children: React.ReactNode }) {
|
|||||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '2px 0' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '2px 0' }}>
|
||||||
<span style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{label}</span>
|
<span className="text-text-3">{label}</span>
|
||||||
<span style={{ color: 'var(--t1)', fontWeight: 600, fontFamily: 'var(--fM)' }}>{value}</span>
|
<span style={{ fontWeight: 600, fontFamily: 'var(--fM)' }}>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,24 +22,8 @@ interface HNSRightPanelProps {
|
|||||||
export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }: HNSRightPanelProps) {
|
export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }: HNSRightPanelProps) {
|
||||||
if (!dispersionResult) {
|
if (!dispersionResult) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="w-[300px] bg-bg-1 border-l border-border p-4 overflow-auto">
|
||||||
width: '300px',
|
<div className="flex flex-col gap-3 items-center justify-center h-full text-text-3 text-xs">
|
||||||
background: 'var(--bg1)',
|
|
||||||
borderLeft: '1px solid var(--bd)',
|
|
||||||
padding: '16px',
|
|
||||||
overflow: 'auto',
|
|
||||||
fontFamily: 'var(--fK)'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '12px',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: '100%',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
|
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
|
||||||
<div>예측 실행 후 결과가 표시됩니다</div>
|
<div>예측 실행 후 결과가 표시됩니다</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,25 +32,10 @@ export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="w-[300px] bg-bg-1 border-l border-border p-4 overflow-auto flex flex-col gap-4">
|
||||||
width: '300px',
|
|
||||||
background: 'var(--bg1)',
|
|
||||||
borderLeft: '1px solid var(--bd)',
|
|
||||||
padding: '16px',
|
|
||||||
overflow: 'auto',
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '16px'
|
|
||||||
}}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
marginBottom: '8px'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '6px',
|
width: '6px',
|
||||||
height: '6px',
|
height: '6px',
|
||||||
@ -74,165 +43,77 @@ export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }:
|
|||||||
background: 'var(--orange)',
|
background: 'var(--orange)',
|
||||||
animation: 'pulse 1.5s infinite'
|
animation: 'pulse 1.5s infinite'
|
||||||
}}></div>
|
}}></div>
|
||||||
<h3 style={{
|
<h3 className="text-[13px] font-bold m-0">
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'var(--t1)',
|
|
||||||
margin: 0
|
|
||||||
}}>
|
|
||||||
예측 결과
|
예측 결과
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="text-[10px] text-text-3 font-mono">
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
fontFamily: 'var(--fM)'
|
|
||||||
}}>
|
|
||||||
{dispersionResult.substance} · ALOHA v5.4.7
|
{dispersionResult.substance} · ALOHA v5.4.7
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* KPI Cards */}
|
{/* KPI Cards */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="flex flex-col gap-2">
|
||||||
<div style={{
|
<div className="p-3 bg-bg-3 border border-[rgba(6,182,212,0.2)] rounded-[var(--rS)]">
|
||||||
padding: '12px',
|
<div className="text-[10px] text-text-3 mb-1.5">
|
||||||
background: 'var(--bg3)',
|
|
||||||
border: '1px solid rgba(6,182,212,0.2)',
|
|
||||||
borderRadius: 'var(--rS)'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
marginBottom: '6px'
|
|
||||||
}}>
|
|
||||||
평균 확산 면적
|
평균 확산 면적
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="text-[20px] font-bold font-mono text-primary-cyan">
|
||||||
fontSize: '20px',
|
8.2 <span className="text-[10px] font-medium">km²</span>
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fM)',
|
|
||||||
color: 'var(--cyan)'
|
|
||||||
}}>
|
|
||||||
8.2 <span style={{ fontSize: '10px', fontWeight: 500 }}>km²</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div className="p-3 bg-bg-3 border border-[rgba(249,115,22,0.2)] rounded-[var(--rS)]">
|
||||||
padding: '12px',
|
<div className="text-[10px] text-text-3 mb-1.5">
|
||||||
background: 'var(--bg3)',
|
|
||||||
border: '1px solid rgba(249,115,22,0.2)',
|
|
||||||
borderRadius: 'var(--rS)'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
marginBottom: '6px'
|
|
||||||
}}>
|
|
||||||
고위험 구역
|
고위험 구역
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="text-[20px] font-bold font-mono text-status-orange">
|
||||||
fontSize: '20px',
|
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fM)',
|
|
||||||
color: 'var(--orange)'
|
|
||||||
}}>
|
|
||||||
2
|
2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div className="p-3 bg-bg-3 border border-border rounded-[var(--rS)]">
|
||||||
padding: '12px',
|
<div className="text-[10px] text-text-3 mb-1.5">
|
||||||
background: 'var(--bg3)',
|
|
||||||
border: '1px solid var(--bd)',
|
|
||||||
borderRadius: 'var(--rS)'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
marginBottom: '6px'
|
|
||||||
}}>
|
|
||||||
평균 풍속
|
평균 풍속
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="text-[20px] font-bold font-mono">
|
||||||
fontSize: '20px',
|
5.2 <span className="text-[10px] font-medium">m/s</span>
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fM)',
|
|
||||||
color: 'var(--t1)'
|
|
||||||
}}>
|
|
||||||
5.2 <span style={{ fontSize: '10px', fontWeight: 500 }}>m/s</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div className="p-3 bg-bg-3 border border-border rounded-[var(--rS)]">
|
||||||
padding: '12px',
|
<div className="text-[10px] text-text-3 mb-1.5">
|
||||||
background: 'var(--bg3)',
|
|
||||||
border: '1px solid var(--bd)',
|
|
||||||
borderRadius: 'var(--rS)'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
marginBottom: '6px'
|
|
||||||
}}>
|
|
||||||
풍향
|
풍향
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="text-[20px] font-bold font-mono">
|
||||||
fontSize: '20px',
|
SW <span className="text-[10px] font-medium">225°</span>
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fM)',
|
|
||||||
color: 'var(--t1)'
|
|
||||||
}}>
|
|
||||||
SW <span style={{ fontSize: '10px', fontWeight: 500 }}>225°</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zone Details */}
|
{/* Zone Details */}
|
||||||
<div>
|
<div>
|
||||||
<h4 style={{
|
<h4 className="text-[11px] font-semibold text-text-2 mt-0 mb-2.5">
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--t2)',
|
|
||||||
margin: '0 0 10px 0'
|
|
||||||
}}>
|
|
||||||
확산 구역 상세
|
확산 구역 상세
|
||||||
</h4>
|
</h4>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="flex flex-col gap-2">
|
||||||
{dispersionResult.zones.map((zone, idx) => (
|
{dispersionResult.zones.map((zone, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
|
className="py-2.5 px-3 bg-bg-2 rounded-[var(--rS)]"
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 12px',
|
|
||||||
background: 'var(--bg2)',
|
|
||||||
borderRadius: 'var(--rS)',
|
|
||||||
borderLeft: `3px solid ${zone.color.replace('0.4', '1').replace('0.3', '1').replace('0.25', '1')}`
|
borderLeft: `3px solid ${zone.color.replace('0.4', '1').replace('0.3', '1').replace('0.25', '1')}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div className="flex justify-between items-center mb-1">
|
||||||
display: 'flex',
|
<span className="text-[11px] font-semibold">
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: '4px'
|
|
||||||
}}>
|
|
||||||
<span style={{
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--t1)'
|
|
||||||
}}>
|
|
||||||
{zone.level}
|
{zone.level}
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
<span className="text-[10px] font-mono text-text-3">
|
||||||
fontSize: '10px',
|
|
||||||
fontFamily: 'var(--fM)',
|
|
||||||
color: 'var(--t3)'
|
|
||||||
}}>
|
|
||||||
{zone.radius}m
|
{zone.radius}m
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="text-[10px] text-text-3">
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)'
|
|
||||||
}}>
|
|
||||||
{dispersionResult.concentration[zone.level as keyof typeof dispersionResult.concentration]}
|
{dispersionResult.concentration[zone.level as keyof typeof dispersionResult.concentration]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -241,19 +122,12 @@ export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timestamp */}
|
{/* Timestamp */}
|
||||||
<div style={{
|
<div className="mt-auto pt-3 border-t border-border text-[10px] text-text-3 font-mono">
|
||||||
marginTop: 'auto',
|
|
||||||
paddingTop: '12px',
|
|
||||||
borderTop: '1px solid var(--bd)',
|
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
fontFamily: 'var(--fM)'
|
|
||||||
}}>
|
|
||||||
예측 시각: {new Date(dispersionResult.timestamp).toLocaleString('ko-KR')}
|
예측 시각: {new Date(dispersionResult.timestamp).toLocaleString('ko-KR')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Action Buttons */}
|
{/* Bottom Action Buttons */}
|
||||||
<div style={{ display: 'flex', gap: '6px', padding: '12px 0 0', borderTop: '1px solid var(--bd)' }}>
|
<div className="flex gap-1.5 pt-3 border-t border-border">
|
||||||
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean">
|
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean">
|
||||||
💾 저장
|
💾 저장
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -130,29 +130,25 @@ export function HNSScenarioView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, width: '100%', height: '100%', overflow: 'hidden', background: 'var(--bg0)' }}>
|
<div className="flex flex-col flex-1 w-full h-full overflow-hidden bg-bg-0">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div className="flex items-center justify-between shrink-0 border-b border-border px-5 py-[14px] bg-bg-1">
|
||||||
padding: '14px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
<div className="flex items-center gap-2.5">
|
||||||
borderBottom: '1px solid var(--bd)', flexShrink: 0, background: 'var(--bg1)',
|
<span className="text-base">📊</span>
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
||||||
<span style={{ fontSize: '16px' }}>📊</span>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '14px', fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>
|
<div className="text-sm font-bold">
|
||||||
HNS 대기확산 시나리오 관리
|
HNS 대기확산 시나리오 관리
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] text-text-3">
|
||||||
시간·조건별 대기확산 예측 시나리오 비교·검토 및 대응 의사결정 지원
|
시간·조건별 대기확산 예측 시나리오 비교·검토 및 대응 의사결정 지원
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
<div className="flex gap-2 items-center">
|
||||||
<select
|
<select
|
||||||
value={selectedIncident}
|
value={selectedIncident}
|
||||||
onChange={(e) => setSelectedIncident(Number(e.target.value))}
|
onChange={(e) => setSelectedIncident(Number(e.target.value))}
|
||||||
className="prd-i"
|
className="prd-i w-[280px] text-[11px]"
|
||||||
style={{ width: '280px', fontSize: '11px' }}
|
|
||||||
>
|
>
|
||||||
{incidents.length === 0
|
{incidents.length === 0
|
||||||
? <option value={0}>분석 데이터 없음</option>
|
? <option value={0}>분석 데이터 없음</option>
|
||||||
@ -165,11 +161,10 @@ export function HNSScenarioView() {
|
|||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={() => setModalOpen(true)}
|
onClick={() => setModalOpen(true)}
|
||||||
|
className="cursor-pointer whitespace-nowrap font-bold text-status-orange text-[11px] px-[14px] py-1.5 rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 14px', background: 'rgba(249,115,22,0.12)',
|
background: 'rgba(249,115,22,0.12)',
|
||||||
border: '1px solid rgba(249,115,22,0.3)', borderRadius: '6px',
|
border: '1px solid rgba(249,115,22,0.3)',
|
||||||
color: 'var(--orange)', fontSize: '11px', fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer', whiteSpace: 'nowrap',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
+ 신규 시나리오
|
+ 신규 시나리오
|
||||||
@ -178,25 +173,17 @@ export function HNSScenarioView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body: Left list + Right detail */}
|
{/* Body: Left list + Right detail */}
|
||||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* ── Left: Scenario List ── */}
|
{/* ── Left: Scenario List ── */}
|
||||||
<div style={{
|
<div className="flex flex-col overflow-hidden shrink-0 border-r border-border bg-bg-1" style={{ width: '370px', minWidth: '370px' }}>
|
||||||
width: '370px', minWidth: '370px', borderRight: '1px solid var(--bd)',
|
<div className="flex items-center justify-between border-b border-border px-[14px] py-2.5">
|
||||||
display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--bg1)',
|
<span className="text-[11px] font-bold text-text-3">
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
padding: '10px 14px', borderBottom: '1px solid var(--bd)',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '11px', fontWeight: 700, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
|
||||||
시나리오 목록 — 톨루엔 대기확산
|
시나리오 목록 — 톨루엔 대기확산
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div className="flex gap-1">
|
||||||
{['시간순', '위험도순'].map((label, i) => (
|
{['시간순', '위험도순'].map((label, i) => (
|
||||||
<button key={i} style={{
|
<button key={i} className="cursor-pointer px-2 py-[3px] text-[9px] font-semibold rounded-sm border border-border" style={{
|
||||||
padding: '3px 8px', fontSize: '9px', fontWeight: 600,
|
background: i === 0 ? 'rgba(249,115,22,0.08)' : 'var(--bg3)',
|
||||||
borderRadius: '4px', cursor: 'pointer', fontFamily: 'var(--fK)',
|
|
||||||
border: '1px solid var(--bd)', background: i === 0 ? 'rgba(249,115,22,0.08)' : 'var(--bg3)',
|
|
||||||
color: i === 0 ? 'var(--orange)' : 'var(--t3)',
|
color: i === 0 ? 'var(--orange)' : 'var(--t3)',
|
||||||
}}>
|
}}>
|
||||||
{label}
|
{label}
|
||||||
@ -206,11 +193,7 @@ export function HNSScenarioView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable list */}
|
{/* Scrollable list */}
|
||||||
<div style={{
|
<div className="flex-1 overflow-y-auto flex flex-col gap-1.5 p-2" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||||
flex: 1, overflowY: 'auto', padding: '8px',
|
|
||||||
display: 'flex', flexDirection: 'column', gap: '6px',
|
|
||||||
scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent',
|
|
||||||
}}>
|
|
||||||
{scenarios.map((scn, idx) => {
|
{scenarios.map((scn, idx) => {
|
||||||
const sev = SEVERITY_STYLE[scn.severity]
|
const sev = SEVERITY_STYLE[scn.severity]
|
||||||
const isSel = selectedIdx === idx
|
const isSel = selectedIdx === idx
|
||||||
@ -221,8 +204,8 @@ export function HNSScenarioView() {
|
|||||||
onClick={() => { setSelectedIdx(idx); setActiveView(0) }}
|
onClick={() => { setSelectedIdx(idx); setActiveView(0) }}
|
||||||
>
|
>
|
||||||
{/* Title + badge */}
|
{/* Title + badge */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '6px' }}>
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="flex items-center gap-1.5">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked.has(idx)}
|
checked={checked.has(idx)}
|
||||||
@ -230,50 +213,41 @@ export function HNSScenarioView() {
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
style={{ accentColor: 'var(--orange)' }}
|
style={{ accentColor: 'var(--orange)' }}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>
|
<span className="text-[12px] font-bold">
|
||||||
{scn.id} {scn.name}
|
{scn.id} {scn.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span className="font-bold px-2 py-[2px] rounded-lg text-[8px]" style={{ background: sev.bg, color: sev.color }}>
|
||||||
padding: '2px 8px', background: sev.bg, borderRadius: '8px',
|
|
||||||
fontSize: '8px', fontWeight: 700, color: sev.color,
|
|
||||||
}}>
|
|
||||||
{scn.severity}
|
{scn.severity}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time row */}
|
{/* Time row */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '6px' }}>
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<span style={{
|
<span className="font-bold font-mono text-status-orange text-[9px] px-1.5 py-[2px] rounded-[3px]" style={{ background: 'rgba(249,115,22,0.1)' }}>
|
||||||
padding: '2px 6px', background: 'rgba(249,115,22,0.1)', borderRadius: '3px',
|
|
||||||
fontSize: '9px', color: 'var(--orange)', fontWeight: 700, fontFamily: 'var(--fM)',
|
|
||||||
}}>
|
|
||||||
{scn.timeStep}
|
{scn.timeStep}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fM)' }}>{scn.datetime}</span>
|
<span className="text-[9px] text-text-3 font-mono">{scn.datetime}</span>
|
||||||
<span style={{ marginLeft: 'auto', fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{scn.wind}</span>
|
<span className="ml-auto text-text-3 text-[8px]">{scn.wind}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics grid */}
|
{/* Metrics grid */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '4px', fontSize: '8px', fontFamily: 'var(--fM)' }}>
|
<div className="grid grid-cols-4 gap-1 font-mono text-[8px]">
|
||||||
{[
|
{[
|
||||||
{ label: '최대농도', value: scn.maxConc, color: '#f87171' },
|
{ label: '최대농도', value: scn.maxConc, color: '#f87171' },
|
||||||
{ label: 'IDLH반경', value: scn.idlhRadius, color: '#f87171' },
|
{ label: 'IDLH반경', value: scn.idlhRadius, color: '#f87171' },
|
||||||
{ label: 'ERPG-2', value: scn.erpg2, color: '#f97316' },
|
{ label: 'ERPG-2', value: scn.erpg2, color: '#f97316' },
|
||||||
{ label: '영향인구', value: scn.population, color: '#f87171' },
|
{ label: '영향인구', value: scn.population, color: '#f87171' },
|
||||||
].map((m, i) => (
|
].map((m, i) => (
|
||||||
<div key={i} style={{ textAlign: 'center', padding: '3px', background: 'var(--bg0)', borderRadius: '3px' }}>
|
<div key={i} className="text-center p-[3px] bg-bg-0 rounded-[3px]">
|
||||||
<div style={{ color: 'var(--t3)', fontFamily: 'var(--fK)', fontSize: '7px' }}>{m.label}</div>
|
<div className="text-text-3 text-[7px]">{m.label}</div>
|
||||||
<div style={{ color: m.color, fontWeight: 700 }}>{m.value}</div>
|
<div className="font-bold" style={{ color: m.color }}>{m.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div style={{
|
<div className="text-text-2 mt-1.5 text-[8px] leading-[1.4]">
|
||||||
marginTop: '6px', fontSize: '8px', color: 'var(--t2)',
|
|
||||||
fontFamily: 'var(--fK)', lineHeight: 1.4,
|
|
||||||
}}>
|
|
||||||
{scn.description}
|
{scn.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -282,37 +256,27 @@ export function HNSScenarioView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom buttons */}
|
{/* Bottom buttons */}
|
||||||
<div style={{
|
<div className="flex gap-2 border-t border-border px-[14px] py-2.5">
|
||||||
padding: '10px 14px', borderTop: '1px solid var(--bd)',
|
|
||||||
display: 'flex', gap: '8px',
|
|
||||||
}}>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveView(1)}
|
onClick={() => setActiveView(1)}
|
||||||
|
className="flex-1 cursor-pointer font-bold text-status-orange text-[11px] p-2 rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '8px', borderRadius: '6px', cursor: 'pointer',
|
background: 'rgba(249,115,22,0.1)',
|
||||||
background: 'rgba(249,115,22,0.1)', border: '1px solid rgba(249,115,22,0.3)',
|
border: '1px solid rgba(249,115,22,0.3)',
|
||||||
color: 'var(--orange)', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
📊 선택 시나리오 비교
|
📊 선택 시나리오 비교
|
||||||
</button>
|
</button>
|
||||||
<button style={{
|
<button className="cursor-pointer font-semibold text-text-2 text-[11px] px-[14px] py-2 rounded-sm bg-bg-3 border border-border">
|
||||||
padding: '8px 14px', borderRadius: '6px', cursor: 'pointer',
|
|
||||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
|
||||||
color: 'var(--t2)', fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fK)',
|
|
||||||
}}>
|
|
||||||
📄 보고서
|
📄 보고서
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Right: Detail Views ── */}
|
{/* ── Right: Detail Views ── */}
|
||||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
||||||
{/* View Tabs */}
|
{/* View Tabs */}
|
||||||
<div style={{
|
<div className="flex border-b border-border shrink-0 px-4 bg-bg-1">
|
||||||
display: 'flex', borderBottom: '1px solid var(--bd)', flexShrink: 0,
|
|
||||||
padding: '0 16px', background: 'var(--bg1)',
|
|
||||||
}}>
|
|
||||||
{['📋 시나리오 상세', '📊 비교 차트', '🗺 확산범위 오버레이'].map((label, i) => (
|
{['📋 시나리오 상세', '📊 비교 차트', '🗺 확산범위 오버레이'].map((label, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
@ -358,29 +322,27 @@ export function HNSScenarioView() {
|
|||||||
function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||||
const d = scenario.detail
|
const d = scenario.detail
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '14px', scrollbarWidth: 'thin' }}>
|
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 p-4" style={{ scrollbarWidth: 'thin' }}>
|
||||||
{/* Hero card */}
|
{/* Hero card */}
|
||||||
<div style={{
|
<div className="relative overflow-hidden rounded-md p-4" style={{
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.06), rgba(239,68,68,0.04))',
|
background: 'linear-gradient(135deg, rgba(249,115,22,0.06), rgba(239,68,68,0.04))',
|
||||||
border: '1px solid rgba(249,115,22,0.2)', borderRadius: '10px',
|
border: '1px solid rgba(249,115,22,0.2)',
|
||||||
padding: '16px', position: 'relative', overflow: 'hidden',
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, #f97316, #ef4444, #a855f7)' }} />
|
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg, #f97316, #ef4444, #a855f7)' }} />
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span style={{ fontSize: '14px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>
|
<span className="text-sm font-bold">
|
||||||
{scenario.id} {scenario.name}
|
{scenario.id} {scenario.name}
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
<span className="font-bold px-2 py-[2px] rounded-lg text-[9px]" style={{
|
||||||
padding: '2px 8px', borderRadius: '8px', fontSize: '9px', fontWeight: 700,
|
|
||||||
background: SEVERITY_STYLE[scenario.severity].bg, color: SEVERITY_STYLE[scenario.severity].color,
|
background: SEVERITY_STYLE[scenario.severity].bg, color: SEVERITY_STYLE[scenario.severity].color,
|
||||||
}}>
|
}}>
|
||||||
{scenario.severity}
|
{scenario.severity}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ marginLeft: 'auto', fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fM)' }}>
|
<span className="ml-auto text-[10px] text-text-3 font-mono">
|
||||||
{scenario.datetime}
|
{scenario.datetime}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '8px' }}>
|
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ label: '최대농도', value: d.maxConc, color: '#f87171' },
|
{ label: '최대농도', value: d.maxConc, color: '#f87171' },
|
||||||
{ label: 'IDLH 반경', value: d.idlhRadius, color: '#f87171' },
|
{ label: 'IDLH 반경', value: d.idlhRadius, color: '#f87171' },
|
||||||
@ -389,51 +351,45 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
{ label: '영향인구', value: d.population, color: '#f87171' },
|
{ label: '영향인구', value: d.population, color: '#f87171' },
|
||||||
{ label: '유출량', value: d.spillAmount, color: 'var(--orange)' },
|
{ label: '유출량', value: d.spillAmount, color: 'var(--orange)' },
|
||||||
].map((m, i) => (
|
].map((m, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} className="text-center rounded-sm p-2" style={{ background: 'rgba(0,0,0,0.15)' }}>
|
||||||
background: 'rgba(0,0,0,0.15)', borderRadius: '6px', padding: '8px', textAlign: 'center',
|
<div className="text-text-3 text-[8px]">{m.label}</div>
|
||||||
}}>
|
<div className="text-base font-bold font-mono whitespace-pre-line mt-[2px]" style={{ color: m.color, lineHeight: 1.2 }}>{m.value}</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{m.label}</div>
|
|
||||||
<div style={{ fontSize: '16px', fontWeight: 700, color: m.color, fontFamily: 'var(--fM)', whiteSpace: 'pre-line', lineHeight: 1.2, marginTop: '2px' }}>{m.value}</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Two-column section */}
|
{/* Two-column section */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{/* Threat Zones */}
|
{/* Threat Zones */}
|
||||||
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: '8px', padding: '14px' }}>
|
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
|
||||||
<h4 style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>
|
<h4 className="text-[12px] font-bold mb-2.5">
|
||||||
⚠️ 위험 구역
|
⚠️ 위험 구역
|
||||||
</h4>
|
</h4>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div className="flex flex-col gap-1.5">
|
||||||
{[
|
{[
|
||||||
{ label: 'IDLH (즉시위험)', value: scenario.zones.idlh, color: '#ef4444' },
|
{ label: 'IDLH (즉시위험)', value: scenario.zones.idlh, color: '#ef4444' },
|
||||||
{ label: 'ERPG-2 (대피권고)', value: scenario.zones.erpg2, color: '#f97316' },
|
{ label: 'ERPG-2 (대피권고)', value: scenario.zones.erpg2, color: '#f97316' },
|
||||||
{ label: 'ERPG-1 (주의권고)', value: scenario.zones.erpg1, color: '#fbbf24' },
|
{ label: 'ERPG-1 (주의권고)', value: scenario.zones.erpg1, color: '#fbbf24' },
|
||||||
{ label: 'TWA (작업허용)', value: scenario.zones.twa, color: '#22c55e' },
|
{ label: 'TWA (작업허용)', value: scenario.zones.twa, color: '#22c55e' },
|
||||||
].map((z, i) => (
|
].map((z, i) => (
|
||||||
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 8px', background: 'var(--bg0)', borderRadius: '4px', borderLeft: `3px solid ${z.color}` }}>
|
<div key={i} className="flex justify-between items-center bg-bg-0 rounded-sm" style={{ padding: '6px 8px', borderLeft: `3px solid ${z.color}` }}>
|
||||||
<span style={{ fontSize: '10px', color: 'var(--t2)', fontFamily: 'var(--fK)' }}>{z.label}</span>
|
<span className="text-[10px] text-text-2">{z.label}</span>
|
||||||
<span style={{ fontSize: '11px', fontWeight: 700, color: z.color, fontFamily: 'var(--fM)' }}>{z.value}</span>
|
<span className="text-[11px] font-bold font-mono" style={{ color: z.color }}>{z.value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: '8px', padding: '14px' }}>
|
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
|
||||||
<h4 style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>
|
<h4 className="text-[12px] font-bold mb-2.5">
|
||||||
🛡 대응 권고 사항
|
🛡 대응 권고 사항
|
||||||
</h4>
|
</h4>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
<div className="flex flex-col gap-1.5">
|
||||||
{scenario.actions.map((action, i) => (
|
{scenario.actions.map((action, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} className="flex items-start gap-1.5 text-[10px] text-text-2 bg-bg-0 rounded-sm leading-[1.4]" className="py-[5px] px-2">
|
||||||
display: 'flex', alignItems: 'flex-start', gap: '6px',
|
<span className="text-status-orange font-bold shrink-0">•</span>
|
||||||
padding: '5px 8px', background: 'var(--bg0)', borderRadius: '4px',
|
|
||||||
fontSize: '10px', color: 'var(--t2)', fontFamily: 'var(--fK)', lineHeight: 1.4,
|
|
||||||
}}>
|
|
||||||
<span style={{ color: 'var(--orange)', fontWeight: 700, flexShrink: 0 }}>•</span>
|
|
||||||
{action}
|
{action}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -442,11 +398,11 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Weather */}
|
{/* Weather */}
|
||||||
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: '8px', padding: '14px' }}>
|
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
|
||||||
<h4 style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>
|
<h4 className="text-[12px] font-bold mb-2.5">
|
||||||
🌊 기상 조건
|
🌊 기상 조건
|
||||||
</h4>
|
</h4>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '8px' }}>
|
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ label: '풍향', value: scenario.weather.dir, icon: '🌬' },
|
{ label: '풍향', value: scenario.weather.dir, icon: '🌬' },
|
||||||
{ label: '풍속', value: scenario.weather.speed, icon: '💨' },
|
{ label: '풍속', value: scenario.weather.speed, icon: '💨' },
|
||||||
@ -455,10 +411,10 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
{ label: '습도', value: scenario.weather.humidity, icon: '💧' },
|
{ label: '습도', value: scenario.weather.humidity, icon: '💧' },
|
||||||
{ label: '혼합층', value: scenario.weather.mixHeight, icon: '📏' },
|
{ label: '혼합층', value: scenario.weather.mixHeight, icon: '📏' },
|
||||||
].map((w, i) => (
|
].map((w, i) => (
|
||||||
<div key={i} style={{ textAlign: 'center', padding: '8px', background: 'var(--bg0)', borderRadius: '6px' }}>
|
<div key={i} className="text-center p-2 rounded-sm bg-bg-0">
|
||||||
<div style={{ fontSize: '14px', marginBottom: '2px' }}>{w.icon}</div>
|
<div className="text-sm mb-0.5">{w.icon}</div>
|
||||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>{w.value}</div>
|
<div className="text-[12px] font-bold font-mono">{w.value}</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>{w.label}</div>
|
<div className="text-text-3 mt-0.5 text-[8px]">{w.label}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -500,18 +456,18 @@ function ScenarioComparison() {
|
|||||||
const barX = [30, 70, 110, 150, 190]
|
const barX = [30, 70, 110, 150, 190]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: '14px', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 px-5 py-4" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div style={{ fontSize: '13px', fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)', marginBottom: '2px' }}>
|
<div className="text-[13px] font-bold mb-0.5">
|
||||||
📊 시나리오 비교 — 시간대별 대기확산 지표 추이
|
📊 시나리오 비교 — 시간대별 대기확산 지표 추이
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Chart 1: 최대 지표면 농도 추이 (Line + Area) ── */}
|
{/* ── Chart 1: 최대 지표면 농도 추이 (Line + Area) ── */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '8px', padding: '14px' }}>
|
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>
|
<div className="text-[11px] font-bold mb-2.5">
|
||||||
최대 지표면 농도 (ppm) 변화 추이
|
최대 지표면 농도 (ppm) 변화 추이
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 500 140" style={{ width: '100%', height: '130px' }}>
|
<svg viewBox="0 0 500 140" className="w-full" style={{ height: '130px' }}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="hnsGrad" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="hnsGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stopColor="#f97316" stopOpacity={0.12} />
|
<stop offset="0%" stopColor="#f97316" stopOpacity={0.12} />
|
||||||
@ -542,13 +498,13 @@ function ScenarioComparison() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Charts 2 & 3: 2-column grid ── */}
|
{/* ── Charts 2 & 3: 2-column grid ── */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}>
|
<div className="grid grid-cols-2 gap-[14px]">
|
||||||
{/* Chart 2: 위험 반경 변화 (Multi-line) */}
|
{/* Chart 2: 위험 반경 변화 (Multi-line) */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '8px', padding: '14px' }}>
|
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>
|
<div className="text-[11px] font-bold mb-2.5">
|
||||||
위험 반경 (km) 변화
|
위험 반경 (km) 변화
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 260 100" style={{ width: '100%', height: '85px' }}>
|
<svg viewBox="0 0 260 100" className="w-full" style={{ height: '85px' }}>
|
||||||
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
{/* IDLH line (red solid) */}
|
{/* IDLH line (red solid) */}
|
||||||
@ -574,11 +530,11 @@ function ScenarioComparison() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart 3: 영향 인구 변화 (Bar) */}
|
{/* Chart 3: 영향 인구 변화 (Bar) */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '8px', padding: '14px' }}>
|
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>
|
<div className="text-[11px] font-bold mb-2.5">
|
||||||
영향 인구 (명) 변화
|
영향 인구 (명) 변화
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 240 100" style={{ width: '100%', height: '85px' }}>
|
<svg viewBox="0 0 240 100" className="w-full" style={{ height: '85px' }}>
|
||||||
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
{D.map((d, i) => {
|
{D.map((d, i) => {
|
||||||
@ -601,18 +557,15 @@ function ScenarioComparison() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Chart 4: 시나리오 비교표 ── */}
|
{/* ── Chart 4: 시나리오 비교표 ── */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '8px', padding: '14px', overflowX: 'auto' }}>
|
<div className="rounded-md border border-border overflow-x-auto bg-bg-3 p-[14px]">
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>
|
<div className="text-[11px] font-bold mb-2.5">
|
||||||
📋 시나리오 비교표
|
📋 시나리오 비교표
|
||||||
</div>
|
</div>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '10px', fontFamily: 'var(--fK)' }}>
|
<table className="w-full text-[10px] border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ background: 'var(--bg0)' }}>
|
<tr className="bg-bg-0">
|
||||||
{['지표', ...D.map(d => `${d.id} (${d.label})`)].map((h, i) => (
|
{['지표', ...D.map(d => `${d.id} (${d.label})`)].map((h, i) => (
|
||||||
<th key={i} style={{
|
<th key={i} className="text-text-3 border-b border-border text-[9px] px-[10px] py-2" style={{ textAlign: i === 0 ? 'left' : 'center' }}>
|
||||||
padding: '8px 10px', textAlign: i === 0 ? 'left' : 'center',
|
|
||||||
color: 'var(--t3)', fontSize: '9px', borderBottom: '1px solid var(--bd)',
|
|
||||||
}}>
|
|
||||||
{h}
|
{h}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@ -620,45 +573,45 @@ function ScenarioComparison() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{/* 최대농도 */}
|
{/* 최대농도 */}
|
||||||
<tr style={{ borderBottom: '1px solid var(--bd)' }}>
|
<tr className="border-b border-border">
|
||||||
<td style={{ padding: '6px 10px', color: 'var(--t2)' }}>최대농도 (ppm)</td>
|
<td className="text-text-2 px-[10px] py-1.5">최대농도 (ppm)</td>
|
||||||
{D.map(d => (
|
{D.map(d => (
|
||||||
<td key={d.id} style={{ padding: '6px', textAlign: 'center', fontFamily: 'var(--fM)', color: SEV_COLOR[d.severity], fontWeight: 600 }}>{d.conc}</td>
|
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.conc}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
{/* IDLH 반경 */}
|
{/* IDLH 반경 */}
|
||||||
<tr style={{ borderBottom: '1px solid var(--bd)' }}>
|
<tr className="border-b border-border">
|
||||||
<td style={{ padding: '6px 10px', color: 'var(--t2)' }}>IDLH 반경 (km)</td>
|
<td className="text-text-2 px-[10px] py-1.5">IDLH 반경 (km)</td>
|
||||||
{D.map(d => (
|
{D.map(d => (
|
||||||
<td key={d.id} style={{ padding: '6px', textAlign: 'center', fontFamily: 'var(--fM)', color: d.idlh > 0 ? '#f87171' : '#22c55e', fontWeight: 600 }}>{d.idlh || 0}</td>
|
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: d.idlh > 0 ? '#f87171' : '#22c55e' }}>{d.idlh || 0}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
{/* ERPG-2 반경 */}
|
{/* ERPG-2 반경 */}
|
||||||
<tr style={{ borderBottom: '1px solid var(--bd)' }}>
|
<tr className="border-b border-border">
|
||||||
<td style={{ padding: '6px 10px', color: 'var(--t2)' }}>ERPG-2 반경 (km)</td>
|
<td className="text-text-2 px-[10px] py-1.5">ERPG-2 반경 (km)</td>
|
||||||
{D.map(d => (
|
{D.map(d => (
|
||||||
<td key={d.id} style={{ padding: '6px', textAlign: 'center', fontFamily: 'var(--fM)', color: d.erpg2 > 0 ? '#f97316' : '#22c55e', fontWeight: 600 }}>{d.erpg2 || 0}</td>
|
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: d.erpg2 > 0 ? '#f97316' : '#22c55e' }}>{d.erpg2 || 0}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
{/* 영향인구 */}
|
{/* 영향인구 */}
|
||||||
<tr style={{ borderBottom: '1px solid var(--bd)' }}>
|
<tr className="border-b border-border">
|
||||||
<td style={{ padding: '6px 10px', color: 'var(--t2)' }}>영향인구 (명)</td>
|
<td className="text-text-2 px-[10px] py-1.5">영향인구 (명)</td>
|
||||||
{D.map(d => (
|
{D.map(d => (
|
||||||
<td key={d.id} style={{ padding: '6px', textAlign: 'center', fontFamily: 'var(--fM)', color: SEV_COLOR[d.severity], fontWeight: 600 }}>{d.pop.toLocaleString()}</td>
|
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.pop.toLocaleString()}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
{/* 풍향/풍속 */}
|
{/* 풍향/풍속 */}
|
||||||
<tr style={{ borderBottom: '1px solid var(--bd)' }}>
|
<tr className="border-b border-border">
|
||||||
<td style={{ padding: '6px 10px', color: 'var(--t2)' }}>풍향 / 풍속</td>
|
<td className="text-text-2 px-[10px] py-1.5">풍향 / 풍속</td>
|
||||||
{D.map(d => (
|
{D.map(d => (
|
||||||
<td key={d.id} style={{ padding: '6px', textAlign: 'center', fontFamily: 'var(--fM)', color: 'var(--cyan)' }}>{d.wind}</td>
|
<td key={d.id} className="text-center font-mono text-primary-cyan p-1.5">{d.wind}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
{/* 위험 등급 */}
|
{/* 위험 등급 */}
|
||||||
<tr>
|
<tr>
|
||||||
<td style={{ padding: '6px 10px', color: 'var(--t2)' }}>위험 등급</td>
|
<td className="text-text-2 px-[10px] py-1.5">위험 등급</td>
|
||||||
{D.map(d => (
|
{D.map(d => (
|
||||||
<td key={d.id} style={{ padding: '6px', textAlign: 'center', color: SEV_COLOR[d.severity], fontWeight: 700 }}>{d.severity}</td>
|
<td key={d.id} className="text-center font-bold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.severity}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -671,25 +624,22 @@ function ScenarioComparison() {
|
|||||||
// ─── View 2: Map Overlay ─────────────────────────────────
|
// ─── View 2: Map Overlay ─────────────────────────────────
|
||||||
function ScenarioMapOverlay() {
|
function ScenarioMapOverlay() {
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: '16px' }}>
|
<div className="flex-1 flex items-center justify-center flex-col gap-4">
|
||||||
<div style={{
|
<div className="flex items-center justify-center rounded-md border border-border text-text-3 text-[13px] bg-bg-2" style={{
|
||||||
width: '80%', maxWidth: '600px', height: '300px',
|
width: '80%', maxWidth: '600px', height: '300px',
|
||||||
background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: '10px',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
color: 'var(--t3)', fontSize: '13px', fontFamily: 'var(--fK)',
|
|
||||||
}}>
|
}}>
|
||||||
[시나리오별 확산범위 오버레이 지도]
|
[시나리오별 확산범위 오버레이 지도]
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '16px' }}>
|
<div className="flex gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: 'T+0h SW방향', color: '#ef4444' },
|
{ label: 'T+0h SW방향', color: '#ef4444' },
|
||||||
{ label: 'T+1h SE 전환', color: '#f97316' },
|
{ label: 'T+1h SE 전환', color: '#f97316' },
|
||||||
{ label: 'T+3h S방향', color: '#fbbf24' },
|
{ label: 'T+3h S방향', color: '#fbbf24' },
|
||||||
{ label: 'T+6h 차단 후', color: '#22c55e' },
|
{ label: 'T+6h 차단 후', color: '#22c55e' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div key={i} className="flex items-center gap-1.5">
|
||||||
<div style={{ width: '12px', height: '12px', borderRadius: '50%', background: item.color, opacity: 0.5 }} />
|
<div className="w-3 h-3 rounded-full opacity-50" style={{ background: item.color }} />
|
||||||
<span style={{ fontSize: '10px', color: 'var(--t2)', fontFamily: 'var(--fK)' }}>{item.label}</span>
|
<span className="text-[10px] text-text-2">{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -734,56 +684,33 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={backdropRef} style={{
|
<div ref={backdropRef} className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)' }}>
|
||||||
position: 'fixed', inset: 0, zIndex: 9999,
|
<div className="flex flex-col overflow-hidden rounded-[14px] bg-bg-1 border border-border w-[520px] max-h-[calc(100vh-80px)]" style={{ boxShadow: '0 20px 60px rgba(0,0,0,0.5)' }}>
|
||||||
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '520px', maxHeight: 'calc(100vh - 80px)',
|
|
||||||
background: 'var(--bg1)', border: '1px solid var(--bd)',
|
|
||||||
borderRadius: '14px', overflow: 'hidden',
|
|
||||||
display: 'flex', flexDirection: 'column',
|
|
||||||
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
|
||||||
}}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div className="flex items-center gap-3 border-b border-border px-5 py-4">
|
||||||
padding: '16px 20px', borderBottom: '1px solid var(--bd)',
|
<div className="flex items-center justify-center text-base w-9 h-9 rounded-[10px]" style={{
|
||||||
display: 'flex', alignItems: 'center', gap: '12px',
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '36px', height: '36px', borderRadius: '10px',
|
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))',
|
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))',
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
border: '1px solid rgba(249,115,22,0.3)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px',
|
|
||||||
}}>🧪</div>
|
}}>🧪</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<h2 style={{ fontSize: '15px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', margin: 0 }}>
|
<h2 className="text-[15px] font-bold m-0">
|
||||||
신규 HNS 대기확산 시나리오
|
신규 HNS 대기확산 시나리오
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>
|
<div className="text-[10px] text-text-3 mt-0.5">
|
||||||
물질·기상·유출조건을 설정하여 새 시나리오를 생성합니다
|
물질·기상·유출조건을 설정하여 새 시나리오를 생성합니다
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} style={{
|
<button onClick={onClose} className="flex items-center justify-center w-7 h-7 cursor-pointer text-text-3 text-[12px] rounded-sm border border-border bg-bg-3">✕</button>
|
||||||
width: '28px', height: '28px', borderRadius: '6px',
|
|
||||||
border: '1px solid var(--bd)', background: 'var(--bg3)',
|
|
||||||
color: 'var(--t3)', fontSize: '12px', cursor: 'pointer',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}>✕</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable content */}
|
{/* Scrollable content */}
|
||||||
<div style={{
|
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 px-5 py-4">
|
||||||
flex: 1, overflowY: 'auto', padding: '16px 20px',
|
|
||||||
display: 'flex', flexDirection: 'column', gap: '14px',
|
|
||||||
}}>
|
|
||||||
{/* 기본 정보 */}
|
{/* 기본 정보 */}
|
||||||
<ModalSection title="기본 정보">
|
<ModalSection title="기본 정보">
|
||||||
<ModalField label="시나리오명">
|
<ModalField label="시나리오명">
|
||||||
<input className="prd-i" value={name} onChange={e => setName(e.target.value)} placeholder="예: 풍향 변화 시나리오" />
|
<input className="prd-i" value={name} onChange={e => setName(e.target.value)} placeholder="예: 풍향 변화 시나리오" />
|
||||||
</ModalField>
|
</ModalField>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<ModalField label="시간 단계">
|
<ModalField label="시간 단계">
|
||||||
<select className="prd-i" value={timeStep} onChange={e => setTimeStep(e.target.value)}>
|
<select className="prd-i" value={timeStep} onChange={e => setTimeStep(e.target.value)}>
|
||||||
{['T+0h', 'T+1h', 'T+3h', 'T+6h', 'T+12h', 'T+24h'].map(t => <option key={t} value={t}>{t}</option>)}
|
{['T+0h', 'T+1h', 'T+3h', 'T+6h', 'T+12h', 'T+24h'].map(t => <option key={t} value={t}>{t}</option>)}
|
||||||
@ -803,10 +730,10 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
</select>
|
</select>
|
||||||
</ModalField>
|
</ModalField>
|
||||||
{/* Material properties card */}
|
{/* Material properties card */}
|
||||||
<div style={{
|
<div className="grid p-2 rounded-sm" style={{
|
||||||
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '4px',
|
gridTemplateColumns: 'repeat(5, 1fr)', gap: '4px',
|
||||||
padding: '8px', background: 'rgba(249,115,22,0.04)',
|
background: 'rgba(249,115,22,0.04)',
|
||||||
border: '1px solid rgba(249,115,22,0.15)', borderRadius: '6px',
|
border: '1px solid rgba(249,115,22,0.15)',
|
||||||
}}>
|
}}>
|
||||||
{[
|
{[
|
||||||
{ label: 'MW', value: mat.mw },
|
{ label: 'MW', value: mat.mw },
|
||||||
@ -815,13 +742,13 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
{ label: 'IDLH', value: mat.idlh },
|
{ label: 'IDLH', value: mat.idlh },
|
||||||
{ label: 'ERPG-2', value: mat.erpg2 },
|
{ label: 'ERPG-2', value: mat.erpg2 },
|
||||||
].map((p, i) => (
|
].map((p, i) => (
|
||||||
<div key={i} style={{ textAlign: 'center' }}>
|
<div key={i} className="text-center">
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{p.label}</div>
|
<div className="text-text-3 text-[8px]">{p.label}</div>
|
||||||
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fM)' }}>{p.value}</div>
|
<div className="text-[10px] font-bold text-status-orange font-mono">{p.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<ModalField label="유출 유형">
|
<ModalField label="유출 유형">
|
||||||
<select className="prd-i" value={releaseType} onChange={e => setReleaseType(e.target.value)}>
|
<select className="prd-i" value={releaseType} onChange={e => setReleaseType(e.target.value)}>
|
||||||
<option value="instant">순간 유출</option>
|
<option value="instant">순간 유출</option>
|
||||||
@ -830,9 +757,9 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
</select>
|
</select>
|
||||||
</ModalField>
|
</ModalField>
|
||||||
<ModalField label="유출량">
|
<ModalField label="유출량">
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div className="flex gap-1">
|
||||||
<input className="prd-i" type="number" value={amount} onChange={e => setAmount(e.target.value)} style={{ flex: 1 }} />
|
<input className="prd-i flex-1" type="number" value={amount} onChange={e => setAmount(e.target.value)} />
|
||||||
<select className="prd-i" value={unit} onChange={e => setUnit(e.target.value)} style={{ width: '60px' }}>
|
<select className="prd-i w-[60px]" value={unit} onChange={e => setUnit(e.target.value)}>
|
||||||
{['t', 'kg', 'm³', 'L'].map(u => <option key={u} value={u}>{u}</option>)}
|
{['t', 'kg', 'm³', 'L'].map(u => <option key={u} value={u}>{u}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -842,7 +769,7 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
|
|
||||||
{/* 기상 조건 */}
|
{/* 기상 조건 */}
|
||||||
<ModalSection title="기상 조건">
|
<ModalSection title="기상 조건">
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '8px' }}>
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<ModalField label="풍향">
|
<ModalField label="풍향">
|
||||||
<select className="prd-i" value={windDir} onChange={e => setWindDir(e.target.value)}>
|
<select className="prd-i" value={windDir} onChange={e => setWindDir(e.target.value)}>
|
||||||
{['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'].map(d => <option key={d} value={d}>{d}</option>)}
|
{['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'].map(d => <option key={d} value={d}>{d}</option>)}
|
||||||
@ -855,7 +782,7 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
<input className="prd-i" type="number" value={temp} onChange={e => setTemp(e.target.value)} step={0.1} />
|
<input className="prd-i" type="number" value={temp} onChange={e => setTemp(e.target.value)} step={0.1} />
|
||||||
</ModalField>
|
</ModalField>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<ModalField label="대기안정도 (Pasquill)">
|
<ModalField label="대기안정도 (Pasquill)">
|
||||||
<select className="prd-i" value={stability} onChange={e => setStability(e.target.value)}>
|
<select className="prd-i" value={stability} onChange={e => setStability(e.target.value)}>
|
||||||
{['A (매우 불안정)', 'B (불안정)', 'C (약간 불안정)', 'D (중립)', 'E (약간 안정)', 'F (안정)'].map(s => <option key={s[0]} value={s[0]}>{s}</option>)}
|
{['A (매우 불안정)', 'B (불안정)', 'C (약간 불안정)', 'D (중립)', 'E (약간 안정)', 'F (안정)'].map(s => <option key={s[0]} value={s[0]}>{s}</option>)}
|
||||||
@ -876,17 +803,12 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{ padding: '14px 20px', borderTop: '1px solid var(--bd)', display: 'flex', gap: '8px' }}>
|
<div className="flex gap-2 border-t border-border px-5 py-[14px]">
|
||||||
<button onClick={onClose} style={{
|
<button onClick={onClose} className="flex-1 text-[12px] font-semibold cursor-pointer rounded-md text-text-2 p-[10px] bg-bg-3 border border-border">취소</button>
|
||||||
flex: 1, padding: '10px', fontSize: '12px', fontWeight: 600,
|
<button onClick={handleSubmit} className="cursor-pointer rounded-md text-[12px] font-bold text-white p-[10px]" style={{
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer',
|
flex: 2,
|
||||||
background: 'var(--bg3)', border: '1px solid var(--bd)', color: 'var(--t2)',
|
|
||||||
}}>취소</button>
|
|
||||||
<button onClick={handleSubmit} style={{
|
|
||||||
flex: 2, padding: '10px', fontSize: '12px', fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer',
|
|
||||||
background: 'linear-gradient(135deg, var(--orange), #ef4444)',
|
background: 'linear-gradient(135deg, var(--orange), #ef4444)',
|
||||||
border: 'none', color: '#fff',
|
border: 'none',
|
||||||
opacity: name.trim() ? 1 : 0.5,
|
opacity: name.trim() ? 1 : 0.5,
|
||||||
}}>
|
}}>
|
||||||
🧪 시나리오 생성 및 예측 실행
|
🧪 시나리오 생성 및 예측 실행
|
||||||
@ -901,12 +823,8 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
|||||||
function ModalSection({ title, children }: { title: string; children: React.ReactNode }) {
|
function ModalSection({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div className="text-[11px] font-bold text-status-orange mb-2 pb-1" style={{ borderBottom: '1px solid rgba(249,115,22,0.15)' }}>{title}</div>
|
||||||
fontSize: '11px', fontWeight: 700, color: 'var(--orange)',
|
<div className="flex flex-col gap-2">{children}</div>
|
||||||
fontFamily: 'var(--fK)', marginBottom: '8px',
|
|
||||||
paddingBottom: '4px', borderBottom: '1px solid rgba(249,115,22,0.15)',
|
|
||||||
}}>{title}</div>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>{children}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -914,7 +832,7 @@ function ModalSection({ title, children }: { title: string; children: React.Reac
|
|||||||
function ModalField({ label, children }: { label: string; children: React.ReactNode }) {
|
function ModalField({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '9px', fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '4px' }}>{label}</div>
|
<div className="text-[9px] font-semibold text-text-3 mb-1">{label}</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -1476,11 +1476,11 @@ ${styles}
|
|||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<div style={{ width: '42px', height: '42px', borderRadius: '10px', background: 'linear-gradient(135deg,rgba(168,85,247,.2),rgba(59,130,246,.15))', border: '1px solid rgba(168,85,247,.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '20px' }}>📐</div>
|
<div style={{ width: '42px', height: '42px', borderRadius: '10px', background: 'linear-gradient(135deg,rgba(168,85,247,.2),rgba(59,130,246,.15))', border: '1px solid rgba(168,85,247,.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '20px' }}>📐</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '16px', fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>HNS 대기확산 모델 이론 및 검증</div>
|
<div style={{ fontSize: '16px', fontWeight: 700 }}>HNS 대기확산 모델 이론 및 검증</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>WRF-Chem · Gaussian Plume/Puff · ROMS · 해양환경 보정 — Based on Lee Moon-Jin et al.</div>
|
<div style={{ fontSize: '10px', color: 'var(--t3)', marginTop: '2px' }}>WRF-Chem · Gaussian Plume/Puff · ROMS · 해양환경 보정 — Based on Lee Moon-Jin et al.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleExportPDF} data-html2pdf-ignore style={{ padding: '6px 14px', borderRadius: '6px', border: '1px solid rgba(6,182,212,.3)', background: 'rgba(6,182,212,.08)', color: 'var(--cyan)', fontSize: '10px', fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>📤 PDF 내보내기</button>
|
<button onClick={handleExportPDF} data-html2pdf-ignore style={{ padding: '6px 14px', borderRadius: '6px', border: '1px solid rgba(6,182,212,.3)', background: 'rgba(6,182,212,.08)', color: 'var(--cyan)', fontSize: '10px', fontWeight: 600, cursor: 'pointer' }}>📤 PDF 내보내기</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sub-tabs */}
|
{/* Sub-tabs */}
|
||||||
@ -1488,7 +1488,7 @@ ${styles}
|
|||||||
{theoryTabs.map((tab, i) => (
|
{theoryTabs.map((tab, i) => (
|
||||||
<button key={i} onClick={() => setActivePanel(i)}
|
<button key={i} onClick={() => setActivePanel(i)}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '8px', fontSize: '11px', fontFamily: 'var(--fK)', fontWeight: 600, cursor: 'pointer',
|
flex: 1, padding: '8px', fontSize: '11px', fontWeight: 600, cursor: 'pointer',
|
||||||
borderRadius: '6px', border: activePanel === i ? '1px solid var(--cyan)' : '1px solid transparent',
|
borderRadius: '6px', border: activePanel === i ? '1px solid var(--cyan)' : '1px solid transparent',
|
||||||
background: activePanel === i ? 'rgba(6,182,212,.08)' : 'transparent',
|
background: activePanel === i ? 'rgba(6,182,212,.08)' : 'transparent',
|
||||||
color: activePanel === i ? 'var(--cyan)' : 'var(--t3)',
|
color: activePanel === i ? 'var(--cyan)' : 'var(--t3)',
|
||||||
|
|||||||
@ -12,23 +12,22 @@ import { createHnsAnalysis } from '../services/hnsApi'
|
|||||||
|
|
||||||
/* ─── HNS 매뉴얼 뷰어 컴포넌트 ─── */
|
/* ─── HNS 매뉴얼 뷰어 컴포넌트 ─── */
|
||||||
function HNSManualViewer() {
|
function HNSManualViewer() {
|
||||||
const card = 'rounded-[10px] p-4 mb-3'
|
const card = 'rounded-md p-4 mb-3'
|
||||||
const cardBg: React.CSSProperties = { background: 'var(--bg3)', border: '1px solid var(--bd)' }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto" style={{ background: 'var(--bg0)', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
<div className="flex-1 overflow-y-auto bg-bg-0" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||||
<div style={{ padding: '16px 20px', maxWidth: '1200px', margin: '0 auto' }}>
|
<div className="px-5 py-4 max-w-[1200px] mx-auto">
|
||||||
|
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '16px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>📖 해양 HNS 대응 매뉴얼</div>
|
<div className="text-base font-bold">📖 해양 HNS 대응 매뉴얼</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo 2024 한국어판)</div>
|
<div className="text-[10px] text-text-3 mt-0.5">Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo 2024 한국어판)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 목차 카드 그리드 */}
|
{/* 목차 카드 그리드 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: '10px', marginBottom: '20px' }}>
|
<div className="grid mb-5" style={{ gridTemplateColumns: 'repeat(4,1fr)', gap: '10px' }}>
|
||||||
{[
|
{[
|
||||||
{ icon: '📘', title: '1. 서론', desc: 'HNS 정의 · OPRC-HNS 의정서 · HNS 협약 범위 및 목적', color: 'var(--cyan)' },
|
{ icon: '📘', title: '1. 서론', desc: 'HNS 정의 · OPRC-HNS 의정서 · HNS 협약 범위 및 목적', color: 'var(--cyan)' },
|
||||||
{ icon: '⚖️', title: '2. IMO 협약·의정서·규칙', desc: 'SOLAS · MARPOL · IBC Code · IMDG Code · IGC Code', color: 'var(--cyan)' },
|
{ icon: '⚖️', title: '2. IMO 협약·의정서·규칙', desc: 'SOLAS · MARPOL · IBC Code · IMDG Code · IGC Code', color: 'var(--cyan)' },
|
||||||
@ -39,19 +38,19 @@ function HNSManualViewer() {
|
|||||||
{ icon: '📋', title: '7. 사례연구', desc: '실제 HNS 해양사고 사례 분석 및 교훈', color: 'var(--cyan)' },
|
{ icon: '📋', title: '7. 사례연구', desc: '실제 HNS 해양사고 사례 분석 및 교훈', color: 'var(--cyan)' },
|
||||||
{ icon: '📊', title: '8. 자료표', desc: '물질별 데이터시트 · AEGL · 노출 한계값', color: 'var(--cyan)' },
|
{ icon: '📊', title: '8. 자료표', desc: '물질별 데이터시트 · AEGL · 노출 한계값', color: 'var(--cyan)' },
|
||||||
].map(ch => (
|
].map(ch => (
|
||||||
<div key={ch.title} className={card} style={{ ...cardBg, cursor: 'pointer', transition: '.2s' }}>
|
<div key={ch.title} className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', cursor: 'pointer', transition: '.2s' }}>
|
||||||
<div style={{ fontSize: '20px', marginBottom: '6px' }}>{ch.icon}</div>
|
<div className="text-[20px] mb-1.5">{ch.icon}</div>
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>{ch.title}</div>
|
<div className="text-[11px] font-bold">{ch.title}</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '4px', lineHeight: '1.4' }}>{ch.desc}</div>
|
<div className="text-[9px] text-text-3 mt-1 leading-[1.4]">{ch.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SEBC 거동 분류 */}
|
{/* SEBC 거동 분류 */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '13px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>SEBC 거동 분류 (Standard European Behaviour Classification)</div>
|
<div className="text-[13px] font-bold mb-2.5">SEBC 거동 분류 (Standard European Behaviour Classification)</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '10px', lineHeight: '1.5' }}>물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요 범주 + 7가지 하위 범주로 분류</div>
|
<div className="text-[9px] text-text-3 mb-2.5 leading-normal">물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요 범주 + 7가지 하위 범주로 분류</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: '8px' }}>
|
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(5,1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ icon: '💨', label: 'G — 가스', desc: '대기 중 확산\n증기압 > 101.3kPa\n예: 암모니아, 염소', color: 'rgba(139,92,246' },
|
{ icon: '💨', label: 'G — 가스', desc: '대기 중 확산\n증기압 > 101.3kPa\n예: 암모니아, 염소', color: 'rgba(139,92,246' },
|
||||||
{ icon: '🌫️', label: 'E — 증발', desc: '수면→대기 증발\n증기압 > 3kPa\n예: 벤젠, 톨루엔', color: 'rgba(249,115,22' },
|
{ icon: '🌫️', label: 'E — 증발', desc: '수면→대기 증발\n증기압 > 3kPa\n예: 벤젠, 톨루엔', color: 'rgba(249,115,22' },
|
||||||
@ -59,19 +58,19 @@ function HNSManualViewer() {
|
|||||||
{ icon: '💧', label: 'D — 용해', desc: '해수에 용해\n용해도 > 5%\n예: 메탄올, 염산', color: 'rgba(6,182,212' },
|
{ icon: '💧', label: 'D — 용해', desc: '해수에 용해\n용해도 > 5%\n예: 메탄올, 염산', color: 'rgba(6,182,212' },
|
||||||
{ icon: '⬇️', label: 'S — 침강', desc: '해저로 침강\n밀도 > 1.025\n예: EDC, 사염화탄소', color: 'rgba(139,148,158' },
|
{ icon: '⬇️', label: 'S — 침강', desc: '해저로 침강\n밀도 > 1.025\n예: EDC, 사염화탄소', color: 'rgba(139,148,158' },
|
||||||
].map(s => (
|
].map(s => (
|
||||||
<div key={s.label} style={{ textAlign: 'center', padding: '10px 6px', background: `${s.color},.08)`, border: `1px solid ${s.color},.2)`, borderRadius: '6px' }}>
|
<div key={s.label} className="text-center px-[6px] py-[10px] rounded-sm" style={{ background: `${s.color},.08)`, border: `1px solid ${s.color},.2)` }}>
|
||||||
<div style={{ fontSize: '22px', marginBottom: '4px' }}>{s.icon}</div>
|
<div className="text-[22px] mb-1">{s.icon}</div>
|
||||||
<div style={{ fontSize: '11px', fontWeight: 700, color: `${s.color},1)`, fontFamily: 'var(--fK)' }}>{s.label}</div>
|
<div className="text-[11px] font-bold" style={{ color: `${s.color},1)` }}>{s.label}</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '3px', lineHeight: '1.3', whiteSpace: 'pre-line' }}>{s.desc}</div>
|
<div className="text-text-3 whitespace-pre-line text-[8px] mt-[3px] leading-[1.3]">{s.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* IMDG Code 위험물 등급 */}
|
{/* IMDG Code 위험물 등급 */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '13px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>IMDG Code 위험물 등급 (Hazard Classification)</div>
|
<div className="text-[13px] font-bold mb-2.5">IMDG Code 위험물 등급 (Hazard Classification)</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: '6px' }}>
|
<div className="grid gap-1.5" style={{ gridTemplateColumns: 'repeat(3,1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ icon: '💥', label: 'Class 1 — 폭발물', sub: 'Explosives', bg: 'rgba(249,115,22,.15)' },
|
{ icon: '💥', label: 'Class 1 — 폭발물', sub: 'Explosives', bg: 'rgba(249,115,22,.15)' },
|
||||||
{ icon: '🫧', label: 'Class 2 — 가스', sub: '인화성/비인화성/독성', bg: 'rgba(34,197,94,.12)' },
|
{ icon: '🫧', label: 'Class 2 — 가스', sub: '인화성/비인화성/독성', bg: 'rgba(34,197,94,.12)' },
|
||||||
@ -83,11 +82,11 @@ function HNSManualViewer() {
|
|||||||
{ icon: '🧪', label: 'Class 8 — 부식성', sub: 'Corrosive Substances', bg: 'rgba(139,148,158,.12)' },
|
{ icon: '🧪', label: 'Class 8 — 부식성', sub: 'Corrosive Substances', bg: 'rgba(139,148,158,.12)' },
|
||||||
{ icon: '⚠️', label: 'Class 9 — 기타', sub: '환경유해물질 포함', bg: 'rgba(139,148,158,.12)' },
|
{ icon: '⚠️', label: 'Class 9 — 기타', sub: '환경유해물질 포함', bg: 'rgba(139,148,158,.12)' },
|
||||||
].map(c => (
|
].map(c => (
|
||||||
<div key={c.label} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '8px', background: 'var(--bg0)', borderRadius: '5px' }}>
|
<div key={c.label} className="flex items-center gap-2 p-2 bg-bg-0 rounded-[5px]">
|
||||||
<div style={{ width: '28px', height: '28px', background: c.bg, borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', flexShrink: 0 }}>{c.icon}</div>
|
<div className="flex items-center justify-center shrink-0 text-sm w-7 h-7 rounded" style={{ background: c.bg }}>{c.icon}</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>{c.label}</div>
|
<div className="text-[10px] font-bold">{c.label}</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{c.sub}</div>
|
<div className="text-text-3 text-[8px]">{c.sub}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -95,9 +94,9 @@ function HNSManualViewer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HNS 사고 대응 프로세스 */}
|
{/* HNS 사고 대응 프로세스 */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '13px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '10px' }}>HNS 사고 대응 프로세스</div>
|
<div className="text-[13px] font-bold mb-2.5">HNS 사고 대응 프로세스</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'stretch', gap: '4px', fontSize: '9px', fontFamily: 'var(--fK)' }}>
|
<div className="flex items-stretch gap-1 text-[9px]">
|
||||||
{[
|
{[
|
||||||
{ icon: '🚨', step: '1단계: 사고 통보', desc: 'HNS 유출 감지\nGMDSS/DSC 신호\n관계기관 통보', color: 'rgba(239,68,68', textColor: '#f87171' },
|
{ icon: '🚨', step: '1단계: 사고 통보', desc: 'HNS 유출 감지\nGMDSS/DSC 신호\n관계기관 통보', color: 'rgba(239,68,68', textColor: '#f87171' },
|
||||||
{ icon: '🔍', step: '2단계: 상황 평가', desc: '물질 식별 (UN번호)\nSEBC 거동 판단\n위험 구역 설정', color: 'rgba(249,115,22', textColor: '#fb923c' },
|
{ icon: '🔍', step: '2단계: 상황 평가', desc: '물질 식별 (UN번호)\nSEBC 거동 판단\n위험 구역 설정', color: 'rgba(249,115,22', textColor: '#fb923c' },
|
||||||
@ -105,24 +104,24 @@ function HNSManualViewer() {
|
|||||||
{ icon: '⚙️', step: '4단계: 현장 대응', desc: '선박 중심 조치\n오염물질 중심 조치\n모니터링 수행', color: 'rgba(6,182,212', textColor: 'var(--cyan)' },
|
{ icon: '⚙️', step: '4단계: 현장 대응', desc: '선박 중심 조치\n오염물질 중심 조치\n모니터링 수행', color: 'rgba(6,182,212', textColor: 'var(--cyan)' },
|
||||||
{ icon: '🔄', step: '5단계: 유출후 관리', desc: '환경 회복/복원\n비용 문서화\n사고 검토/교훈', color: 'rgba(34,197,94', textColor: '#22c55e' },
|
{ icon: '🔄', step: '5단계: 유출후 관리', desc: '환경 회복/복원\n비용 문서화\n사고 검토/교훈', color: 'rgba(34,197,94', textColor: '#22c55e' },
|
||||||
].map((s, i) => (
|
].map((s, i) => (
|
||||||
<div key={s.step} style={{ display: 'flex', alignItems: 'stretch', flex: 1 }}>
|
<div key={s.step} className="flex items-stretch flex-1">
|
||||||
<div style={{ flex: 1, padding: '10px 8px', background: `${s.color},.08)`, border: `1px solid ${s.color},.2)`, borderRadius: '6px', textAlign: 'center' }}>
|
<div className="flex-1 text-center px-2 py-[10px] rounded-sm" style={{ background: `${s.color},.08)`, border: `1px solid ${s.color},.2)` }}>
|
||||||
<div style={{ fontSize: '16px', marginBottom: '4px' }}>{s.icon}</div>
|
<div className="text-base mb-1">{s.icon}</div>
|
||||||
<div style={{ fontWeight: 700, color: s.textColor, marginBottom: '3px' }}>{s.step}</div>
|
<div className="font-bold mb-0.5" style={{ color: s.textColor }}>{s.step}</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', lineHeight: '1.3', whiteSpace: 'pre-line' }}>{s.desc}</div>
|
<div className="text-text-3 whitespace-pre-line text-[8px] leading-[1.3]">{s.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
{i < 4 && <div style={{ display: 'flex', alignItems: 'center', color: 'var(--bd)', fontSize: '14px', padding: '0 2px' }}>→</div>}
|
{i < 4 && <div className="flex items-center text-sm px-[2px]" style={{ color: 'var(--bd)' }}>→</div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 대응 기술 매트릭스 */}
|
{/* 대응 기술 매트릭스 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px', marginBottom: '14px' }}>
|
<div className="grid gap-[14px] mb-[14px]" style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||||
{/* 선박 중심 조치 */}
|
{/* 선박 중심 조치 */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '8px' }}>🚢 선박 중심 조치</div>
|
<div className="text-[12px] font-bold mb-2">🚢 선박 중심 조치</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
<div className="flex flex-col gap-1">
|
||||||
{[
|
{[
|
||||||
{ icon: '🚁', title: '긴급 승선 (Emergency Boarding)', desc: '전문 대응팀의 사고 선박 접근 및 상황 파악' },
|
{ icon: '🚁', title: '긴급 승선 (Emergency Boarding)', desc: '전문 대응팀의 사고 선박 접근 및 상황 파악' },
|
||||||
{ icon: '🔗', title: '긴급 예인 (Emergency Towing)', desc: '사고 선박을 안전 해역으로 이동' },
|
{ icon: '🔗', title: '긴급 예인 (Emergency Towing)', desc: '사고 선박을 안전 해역으로 이동' },
|
||||||
@ -130,20 +129,20 @@ function HNSManualViewer() {
|
|||||||
{ icon: '🔄', title: '화물 이송 (Cargo Transfer)', desc: '위험 화물을 다른 선박/탱크로 이적' },
|
{ icon: '🔄', title: '화물 이송 (Cargo Transfer)', desc: '위험 화물을 다른 선박/탱크로 이적' },
|
||||||
{ icon: '🔧', title: '밀봉/마개 (Sealing & Plugging)', desc: '유출 지점 임시 차단 및 봉쇄' },
|
{ icon: '🔧', title: '밀봉/마개 (Sealing & Plugging)', desc: '유출 지점 임시 차단 및 봉쇄' },
|
||||||
].map(item => (
|
].map(item => (
|
||||||
<div key={item.title} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', background: 'var(--bg0)', borderRadius: '4px' }}>
|
<div key={item.title} className="flex items-center gap-2 px-2 py-1.5 bg-bg-0 rounded">
|
||||||
<span style={{ fontSize: '12px', width: '20px', textAlign: 'center' }}>{item.icon}</span>
|
<span className="text-[12px] text-center w-5">{item.icon}</span>
|
||||||
<div style={{ fontSize: '9px', fontFamily: 'var(--fK)' }}>
|
<div className="text-[9px]">
|
||||||
<b style={{ color: 'var(--t1)' }}>{item.title}</b><br />
|
<b>{item.title}</b><br />
|
||||||
<span style={{ color: 'var(--t3)' }}>{item.desc}</span>
|
<span className="text-text-3">{item.desc}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 오염물질 중심 조치 */}
|
{/* 오염물질 중심 조치 */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '8px' }}>🧪 오염물질 중심 조치</div>
|
<div className="text-[12px] font-bold mb-2">🧪 오염물질 중심 조치</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
<div className="flex flex-col gap-1">
|
||||||
{[
|
{[
|
||||||
{ icon: '💨', title: '대기 확산 모니터링', desc: '가스/증발 물질의 대기 농도 감시 (ALOHA/CAMEO)' },
|
{ icon: '💨', title: '대기 확산 모니터링', desc: '가스/증발 물질의 대기 농도 감시 (ALOHA/CAMEO)' },
|
||||||
{ icon: '🌊', title: '해수면 회수 (Surface Recovery)', desc: '부유 물질 흡착재/스키머로 회수' },
|
{ icon: '🌊', title: '해수면 회수 (Surface Recovery)', desc: '부유 물질 흡착재/스키머로 회수' },
|
||||||
@ -151,11 +150,11 @@ function HNSManualViewer() {
|
|||||||
{ icon: '⬇️', title: '해저 회수 (Subsea Recovery)', desc: '침강 물질 ROV/잠수 회수' },
|
{ icon: '⬇️', title: '해저 회수 (Subsea Recovery)', desc: '침강 물질 ROV/잠수 회수' },
|
||||||
{ icon: '🔥', title: '제어 연소 (Controlled Burning)', desc: '인화성 물질 현장 소각 처리' },
|
{ icon: '🔥', title: '제어 연소 (Controlled Burning)', desc: '인화성 물질 현장 소각 처리' },
|
||||||
].map(item => (
|
].map(item => (
|
||||||
<div key={item.title} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', background: 'var(--bg0)', borderRadius: '4px' }}>
|
<div key={item.title} className="flex items-center gap-2 px-2 py-1.5 bg-bg-0 rounded">
|
||||||
<span style={{ fontSize: '12px', width: '20px', textAlign: 'center' }}>{item.icon}</span>
|
<span className="text-[12px] text-center w-5">{item.icon}</span>
|
||||||
<div style={{ fontSize: '9px', fontFamily: 'var(--fK)' }}>
|
<div className="text-[9px]">
|
||||||
<b style={{ color: 'var(--t1)' }}>{item.title}</b><br />
|
<b>{item.title}</b><br />
|
||||||
<span style={{ color: 'var(--t3)' }}>{item.desc}</span>
|
<span className="text-text-3">{item.desc}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -164,11 +163,11 @@ function HNSManualViewer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PPE / 안전구역 / 노출한계 */}
|
{/* PPE / 안전구역 / 노출한계 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '14px', marginBottom: '14px' }}>
|
<div className="grid gap-[14px] mb-[14px]" style={{ gridTemplateColumns: '1fr 1fr 1fr' }}>
|
||||||
{/* PPE */}
|
{/* PPE */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '8px' }}>🦺 개인보호장비 (PPE)</div>
|
<div className="text-[12px] font-bold mb-2">🦺 개인보호장비 (PPE)</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '9px', fontFamily: 'var(--fK)' }}>
|
<div className="flex flex-col gap-1 text-[9px]">
|
||||||
{[
|
{[
|
||||||
{ level: 'Level A', desc: '완전 밀폐형 화학보호복 + SCBA\n증기/가스 직접 노출 구역', color: '#ef4444' },
|
{ level: 'Level A', desc: '완전 밀폐형 화학보호복 + SCBA\n증기/가스 직접 노출 구역', color: '#ef4444' },
|
||||||
{ level: 'Level B', desc: '비밀폐형 화학보호복 + SCBA\n액체 스플래시 위험 구역', color: '#f97316' },
|
{ level: 'Level B', desc: '비밀폐형 화학보호복 + SCBA\n액체 스플래시 위험 구역', color: '#f97316' },
|
||||||
@ -177,41 +176,41 @@ function HNSManualViewer() {
|
|||||||
].map(p => (
|
].map(p => (
|
||||||
<div key={p.level} style={{ padding: '6px 8px', background: `color-mix(in srgb,${p.color} 6%,transparent)`, borderLeft: `3px solid ${p.color}`, borderRadius: '0 4px 4px 0' }}>
|
<div key={p.level} style={{ padding: '6px 8px', background: `color-mix(in srgb,${p.color} 6%,transparent)`, borderLeft: `3px solid ${p.color}`, borderRadius: '0 4px 4px 0' }}>
|
||||||
<b style={{ color: p.color }}>{p.level}</b><br />
|
<b style={{ color: p.color }}>{p.level}</b><br />
|
||||||
<span style={{ color: 'var(--t3)', whiteSpace: 'pre-line' }}>{p.desc}</span>
|
<span className="text-text-3 whitespace-pre-line">{p.desc}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 안전구역 */}
|
{/* 안전구역 */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '8px' }}>🔴 안전 구역 설정</div>
|
<div className="text-[12px] font-bold mb-2">🔴 안전 구역 설정</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '9px', fontFamily: 'var(--fK)' }}>
|
<div className="flex flex-col gap-1.5 text-[9px]">
|
||||||
<div style={{ textAlign: 'center', padding: '10px', background: 'rgba(239,68,68,.08)', border: '2px solid rgba(239,68,68,.3)', borderRadius: '50%', aspectRatio: '1', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
<div className="text-center flex flex-col items-center justify-center p-[10px]" style={{ background: 'rgba(239,68,68,.08)', border: '2px solid rgba(239,68,68,.3)', borderRadius: '50%', aspectRatio: '1' }}>
|
||||||
<b style={{ color: '#f87171', fontSize: '11px' }}>HOT ZONE</b>
|
<b style={{ color: '#f87171', fontSize: '11px' }}>HOT ZONE</b>
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '8px' }}>직접 위험구역<br />Level A/B PPE 필수</span>
|
<span className="text-text-3 text-[8px]">직접 위험구역<br />Level A/B PPE 필수</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center', padding: '8px', background: 'rgba(251,191,36,.06)', border: '1.5px solid rgba(251,191,36,.25)', borderRadius: '6px' }}>
|
<div className="text-center p-2 rounded-sm" style={{ background: 'rgba(251,191,36,.06)', border: '1.5px solid rgba(251,191,36,.25)' }}>
|
||||||
<b style={{ color: '#fbbf24', fontSize: '10px' }}>WARM ZONE</b>
|
<b style={{ color: '#fbbf24', fontSize: '10px' }}>WARM ZONE</b>
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '8px' }}> — 오염제거/전환 구역</span>
|
<span className="text-text-3 text-[8px]"> — 오염제거/전환 구역</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center', padding: '8px', background: 'rgba(34,197,94,.06)', border: '1.5px solid rgba(34,197,94,.25)', borderRadius: '6px' }}>
|
<div className="text-center p-2 rounded-sm" style={{ background: 'rgba(34,197,94,.06)', border: '1.5px solid rgba(34,197,94,.25)' }}>
|
||||||
<b style={{ color: '#22c55e', fontSize: '10px' }}>COLD ZONE</b>
|
<b style={{ color: '#22c55e', fontSize: '10px' }}>COLD ZONE</b>
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '8px' }}> — 지휘/지원 구역</span>
|
<span className="text-text-3 text-[8px]"> — 지휘/지원 구역</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 노출한계 (AEGL) */}
|
{/* 노출한계 (AEGL) */}
|
||||||
<div className={card} style={cardBg}>
|
<div className={card} className="bg-bg-3 border border-border">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '8px' }}>📊 노출한계 (AEGL 기준)</div>
|
<div className="text-[12px] font-bold mb-2">📊 노출한계 (AEGL 기준)</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '6px', lineHeight: '1.4' }}>Acute Exposure Guideline Levels (EPA)<br />암모니아(NH₃) 예시 — ppm 기준</div>
|
<div className="text-[9px] text-text-3 mb-1.5 leading-[1.4]">Acute Exposure Guideline Levels (EPA)<br />암모니아(NH₃) 예시 — ppm 기준</div>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '8px', fontFamily: 'var(--fM)' }}>
|
<table className="w-full font-mono border-collapse text-[8px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: '1px solid var(--bd)' }}>
|
<tr className="border-b border-border">
|
||||||
<th style={{ padding: '4px', textAlign: 'left', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>구분</th>
|
<th className="p-1 text-left text-text-3">구분</th>
|
||||||
<th style={{ padding: '4px', textAlign: 'center', color: 'var(--t3)' }}>10분</th>
|
<th className="p-1 text-center text-text-3">10분</th>
|
||||||
<th style={{ padding: '4px', textAlign: 'center', color: 'var(--t3)' }}>30분</th>
|
<th className="p-1 text-center text-text-3">30분</th>
|
||||||
<th style={{ padding: '4px', textAlign: 'center', color: 'var(--t3)' }}>60분</th>
|
<th className="p-1 text-center text-text-3">60분</th>
|
||||||
<th style={{ padding: '4px', textAlign: 'center', color: 'var(--t3)' }}>4시간</th>
|
<th className="p-1 text-center text-text-3">4시간</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -220,16 +219,16 @@ function HNSManualViewer() {
|
|||||||
{ label: 'AEGL-2', color: '#fbbf24', vals: [220, 220, 160, 110] },
|
{ label: 'AEGL-2', color: '#fbbf24', vals: [220, 220, 160, 110] },
|
||||||
{ label: 'AEGL-3', color: '#f87171', vals: [2700, 1600, 1100, 550] },
|
{ label: 'AEGL-3', color: '#f87171', vals: [2700, 1600, 1100, 550] },
|
||||||
].map((row, ri) => (
|
].map((row, ri) => (
|
||||||
<tr key={row.label} style={{ borderBottom: ri < 2 ? '1px solid var(--bd)' : undefined }}>
|
<tr key={row.label} className={ri < 2 ? 'border-b border-border' : ''}>
|
||||||
<td style={{ padding: '4px', color: row.color, fontWeight: 700, fontFamily: 'var(--fK)' }}>{row.label}</td>
|
<td className="p-1 font-bold" style={{ color: row.color }}>{row.label}</td>
|
||||||
{row.vals.map((v, vi) => (
|
{row.vals.map((v, vi) => (
|
||||||
<td key={vi} style={{ padding: '4px', textAlign: 'center', color: 'var(--t2)' }}>{v}</td>
|
<td key={vi} className="p-1 text-center text-text-2">{v}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div style={{ fontSize: '7px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '6px', lineHeight: '1.4' }}>
|
<div className="text-text-3 mt-1.5 text-[7px] leading-[1.4]">
|
||||||
AEGL-1: 불쾌감 (비장애성)<br />
|
AEGL-1: 불쾌감 (비장애성)<br />
|
||||||
AEGL-2: 심각한 건강 영향 (비가역적)<br />
|
AEGL-2: 심각한 건강 영향 (비가역적)<br />
|
||||||
AEGL-3: 생명 위협 또는 사망
|
AEGL-3: 생명 위협 또는 사망
|
||||||
@ -238,7 +237,7 @@ function HNSManualViewer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 출처 */}
|
{/* 출처 */}
|
||||||
<div style={{ padding: '10px', background: 'var(--bg3)', borderRadius: '6px', fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: '1.5' }}>
|
<div className="text-text-3 rounded-sm bg-bg-3 p-[10px] text-[8px] leading-[1.5]">
|
||||||
<b>출처:</b> Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo Project, 2024 한국어판)<br />
|
<b>출처:</b> Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo Project, 2024 한국어판)<br />
|
||||||
번역: 원해민, 이시연, 양보경, 강성길, 이성엽 — KRISO 선박해양플랜트연구소 / NOWPAP MERRAC<br />
|
번역: 원해민, 이시연, 양보경, 강성길, 이성엽 — KRISO 선박해양플랜트연구소 / NOWPAP MERRAC<br />
|
||||||
원본: Alcaro L., Brandt J., Giraud W., Mannozzi M., Nicolas-Kopec A. (2021) ISBN: 978-2-87893-147-1
|
원본: Alcaro L., Brandt J., Giraud W., Mannozzi M., Nicolas-Kopec A. (2021) ISBN: 978-2-87893-147-1
|
||||||
|
|||||||
@ -169,68 +169,69 @@ export function IncidentsLeftPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-bg-1 border-r border-border overflow-hidden" style={{ width: '360px', flexShrink: 0 }}>
|
<div className="flex flex-col bg-bg-1 border-r border-border overflow-hidden shrink-0 w-[360px]">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div style={{ padding: '12px 16px', borderBottom: '1px solid var(--bd)', flexShrink: 0 }}>
|
<div className="px-4 py-3 border-b border-border shrink-0">
|
||||||
<div style={{ position: 'relative' }}>
|
<div className="relative">
|
||||||
<span style={{ position: 'absolute', left: '10px', top: '50%', transform: 'translateY(-50%)', fontSize: '12px' }}>🔍</span>
|
<span className="absolute left-[10px] top-1/2 -translate-y-1/2 text-xs">🔍</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="사고명, 선박명 검색..."
|
placeholder="사고명, 선박명 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => { setSearchTerm(e.target.value); resetPage() }}
|
onChange={(e) => { setSearchTerm(e.target.value); resetPage() }}
|
||||||
style={{
|
className="w-full py-2 pr-3 pl-8 bg-bg-0 border border-border text-xs outline-none"
|
||||||
width: '100%', padding: '8px 12px 8px 32px', background: 'var(--bg0)',
|
style={{ borderRadius: 'var(--rS)' }}
|
||||||
border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)',
|
|
||||||
fontFamily: 'var(--fK)', fontSize: '12px', outline: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date Range */}
|
{/* Date Range */}
|
||||||
<div style={{ padding: '8px 16px', borderBottom: '1px solid var(--bd)', display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0 }}>
|
<div className="flex items-center gap-1.5 px-4 py-2 border-b border-border shrink-0">
|
||||||
<input type="date" value={dateFrom}
|
<input type="date" value={dateFrom}
|
||||||
onChange={(e) => { setDateFrom(e.target.value); setSelectedPeriod(''); resetPage() }}
|
onChange={(e) => { setDateFrom(e.target.value); setSelectedPeriod(''); resetPage() }}
|
||||||
style={{ padding: '5px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: '11px', outline: 'none', flex: 1 }}
|
className="bg-bg-0 border border-border font-mono text-[11px] outline-none flex-1"
|
||||||
|
style={{ padding: '5px 8px', borderRadius: 'var(--rS)' }}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '11px' }}>~</span>
|
<span className="text-text-3 text-[11px]">~</span>
|
||||||
<input type="date" value={dateTo}
|
<input type="date" value={dateTo}
|
||||||
onChange={(e) => { setDateTo(e.target.value); setSelectedPeriod(''); resetPage() }}
|
onChange={(e) => { setDateTo(e.target.value); setSelectedPeriod(''); resetPage() }}
|
||||||
style={{ padding: '5px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: '11px', outline: 'none', flex: 1 }}
|
className="bg-bg-0 border border-border font-mono text-[11px] outline-none flex-1"
|
||||||
|
style={{ padding: '5px 8px', borderRadius: 'var(--rS)' }}
|
||||||
/>
|
/>
|
||||||
<button onClick={resetPage} style={{ padding: '5px 12px', background: 'linear-gradient(135deg,var(--cyan),var(--blue))', color: '#fff', border: 'none', borderRadius: 'var(--rS)', fontSize: '11px', fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)', whiteSpace: 'nowrap' }}>조회</button>
|
<button onClick={resetPage} className="text-[11px] font-semibold cursor-pointer whitespace-nowrap text-white border-none" style={{ padding: '5px 12px', background: 'linear-gradient(135deg,var(--cyan),var(--blue))', borderRadius: 'var(--rS)' }}>조회</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Period Presets */}
|
{/* Period Presets */}
|
||||||
<div style={{ padding: '6px 16px', borderBottom: '1px solid var(--bd)', display: 'flex', gap: '4px', flexShrink: 0 }}>
|
<div className="flex gap-1 px-4 py-1.5 border-b border-border shrink-0">
|
||||||
{PERIOD_PRESETS.map(p => (
|
{PERIOD_PRESETS.map(p => (
|
||||||
<button key={p} onClick={() => handlePeriodClick(p)} style={{
|
<button key={p} onClick={() => handlePeriodClick(p)} className="text-[10px] font-semibold cursor-pointer"
|
||||||
padding: '3px 8px', borderRadius: '14px', border: selectedPeriod === p ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
|
style={{
|
||||||
background: selectedPeriod === p ? 'rgba(6,182,212,0.1)' : 'transparent',
|
padding: '3px 8px', borderRadius: '14px', border: selectedPeriod === p ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
|
||||||
color: selectedPeriod === p ? 'var(--cyan)' : 'var(--t3)', fontSize: '10px', fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)',
|
background: selectedPeriod === p ? 'rgba(6,182,212,0.1)' : 'transparent',
|
||||||
}}>{p}</button>
|
color: selectedPeriod === p ? 'var(--cyan)' : 'var(--t3)',
|
||||||
|
}}>{p}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Today Summary */}
|
{/* Today Summary */}
|
||||||
<div style={{ padding: '10px 16px', borderBottom: '1px solid var(--bd)', background: 'rgba(6,182,212,0.03)', flexShrink: 0 }}>
|
<div className="px-4 py-2.5 border-b border-border shrink-0" style={{ background: 'rgba(6,182,212,0.03)' }}>
|
||||||
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--t3)', letterSpacing: '0.8px', marginBottom: '8px', fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] font-bold text-text-3 mb-2" style={{ letterSpacing: '0.8px' }}>
|
||||||
📅 오늘 ({todayLabel}) 사고 현황
|
📅 오늘 ({todayLabel}) 사고 현황
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
<div className="flex gap-1.5 flex-wrap">
|
||||||
{REGIONS.map(r => {
|
{REGIONS.map(r => {
|
||||||
const count = regionCounts[r] ?? 0
|
const count = regionCounts[r] ?? 0
|
||||||
const isActive = selectedRegion === r
|
const isActive = selectedRegion === r
|
||||||
return (
|
return (
|
||||||
<button key={r} onClick={() => { setSelectedRegion(r); resetPage() }} style={{
|
<button key={r} onClick={() => { setSelectedRegion(r); resetPage() }} className="text-[11px] cursor-pointer"
|
||||||
padding: '4px 10px', borderRadius: 'var(--rS)', fontSize: '11px', fontFamily: 'var(--fK)', cursor: 'pointer',
|
style={{
|
||||||
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg3)',
|
padding: '4px 10px', borderRadius: 'var(--rS)',
|
||||||
border: isActive ? '1px solid rgba(6,182,212,0.25)' : '1px solid var(--bd)',
|
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg3)',
|
||||||
color: isActive ? 'var(--cyan)' : 'var(--t2)', fontWeight: isActive ? 700 : 400,
|
border: isActive ? '1px solid rgba(6,182,212,0.25)' : '1px solid var(--bd)',
|
||||||
}}>
|
color: isActive ? 'var(--cyan)' : 'var(--t2)', fontWeight: isActive ? 700 : 400,
|
||||||
|
}}>
|
||||||
{r === '전체' ? '전체 ' : `${r} `}
|
{r === '전체' ? '전체 ' : `${r} `}
|
||||||
<span style={{ fontWeight: 700, fontFamily: 'var(--fM)', color: isActive ? 'var(--cyan)' : 'var(--t1)' }}>
|
<span className="font-bold font-mono" style={{ color: isActive ? 'var(--cyan)' : undefined }}>
|
||||||
{r === '전체' ? count : `(${count})`}
|
{r === '전체' ? count : `(${count})`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -240,19 +241,19 @@ export function IncidentsLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Filter */}
|
{/* Status Filter */}
|
||||||
<div style={{ display: 'flex', gap: '5px', padding: '8px 16px', borderBottom: '1px solid var(--bd)', flexShrink: 0 }}>
|
<div className="flex gap-[5px] px-4 py-2 border-b border-border shrink-0">
|
||||||
{[
|
{[
|
||||||
{ id: '전체', label: '전체', dot: '' },
|
{ id: '전체', label: '전체', dot: '' },
|
||||||
{ id: 'active', label: `대응중 (${statusCounts.active})`, dot: 'var(--red)' },
|
{ id: 'active', label: `대응중 (${statusCounts.active})`, dot: 'var(--red)' },
|
||||||
{ id: 'investigating', label: `조사중 (${statusCounts.investigating})`, dot: 'var(--orange)' },
|
{ id: 'investigating', label: `조사중 (${statusCounts.investigating})`, dot: 'var(--orange)' },
|
||||||
{ id: 'closed', label: `종료 (${statusCounts.closed})`, dot: 'var(--t3)' },
|
{ id: 'closed', label: `종료 (${statusCounts.closed})`, dot: 'var(--t3)' },
|
||||||
].map(s => (
|
].map(s => (
|
||||||
<button key={s.id} onClick={() => { setSelectedStatus(s.id); resetPage() }} style={{
|
<button key={s.id} onClick={() => { setSelectedStatus(s.id); resetPage() }}
|
||||||
padding: '4px 10px', borderRadius: '12px', border: selectedStatus === s.id ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
|
className="flex items-center gap-1 text-[10px] font-semibold cursor-pointer"
|
||||||
background: 'transparent', color: selectedStatus === s.id ? 'var(--t2)' : 'var(--t3)',
|
style={{
|
||||||
fontSize: '10px', fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)',
|
padding: '4px 10px', borderRadius: '12px', border: selectedStatus === s.id ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
|
||||||
display: 'flex', alignItems: 'center', gap: '4px',
|
background: 'transparent', color: selectedStatus === s.id ? 'var(--t2)' : 'var(--t3)',
|
||||||
}}>
|
}}>
|
||||||
{s.dot && <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: s.dot }} />}
|
{s.dot && <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: s.dot }} />}
|
||||||
{s.label}
|
{s.label}
|
||||||
</button>
|
</button>
|
||||||
@ -260,14 +261,14 @@ export function IncidentsLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Count */}
|
{/* Count */}
|
||||||
<div style={{ padding: '6px 16px', fontSize: '11px', color: 'var(--t3)', fontFamily: 'var(--fK)', borderBottom: '1px solid rgba(30,42,74,0.3)', flexShrink: 0 }}>
|
<div className="px-4 py-1.5 text-[11px] text-text-3 shrink-0 border-b border-border">
|
||||||
총 {filteredIncidents.length}건
|
총 {filteredIncidents.length}건
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Incident List */}
|
{/* Incident List */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin' as const, scrollbarColor: 'var(--bdL) transparent' }}>
|
||||||
{pagedIncidents.length === 0 ? (
|
{pagedIncidents.length === 0 ? (
|
||||||
<div style={{ padding: '40px 16px', textAlign: 'center', color: 'var(--t3)', fontSize: '11px', fontFamily: 'var(--fK)' }}>
|
<div className="px-4 py-10 text-center text-text-3 text-[11px]">
|
||||||
검색 결과가 없습니다.
|
검색 결과가 없습니다.
|
||||||
</div>
|
</div>
|
||||||
) : pagedIncidents.map(inc => {
|
) : pagedIncidents.map(inc => {
|
||||||
@ -288,61 +289,64 @@ export function IncidentsLeftPanel({
|
|||||||
active: '대응중', investigating: '조사중', closed: '종료',
|
active: '대응중', investigating: '조사중', closed: '종료',
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={inc.id} onClick={() => onIncidentSelect(inc.id)} style={{
|
<div key={inc.id} onClick={() => onIncidentSelect(inc.id)}
|
||||||
padding: '12px 16px', borderBottom: '1px solid var(--bd)', cursor: 'pointer',
|
className="px-4 py-3 border-b border-border cursor-pointer"
|
||||||
background: isSel ? 'rgba(6,182,212,0.04)' : undefined,
|
style={{
|
||||||
borderLeft: isSel ? '3px solid var(--cyan)' : '3px solid transparent',
|
background: isSel ? 'rgba(6,182,212,0.04)' : undefined,
|
||||||
transition: 'background 0.15s',
|
borderLeft: isSel ? '3px solid var(--cyan)' : '3px solid transparent',
|
||||||
}}
|
transition: 'background 0.15s',
|
||||||
|
}}
|
||||||
onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = 'rgba(255,255,255,0.02)' }}
|
onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = 'rgba(255,255,255,0.02)' }}
|
||||||
onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = '' }}
|
onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = '' }}
|
||||||
>
|
>
|
||||||
{/* Row 1: name + status */}
|
{/* Row 1: name + status */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '5px' }}>
|
<div className="flex items-center justify-between mb-[5px]">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>
|
<div className="flex items-center gap-1.5 text-xs font-bold">
|
||||||
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: dotStyle[inc.status], boxShadow: dotShadow[inc.status], flexShrink: 0 }} />
|
<span className="shrink-0" style={{ width: '8px', height: '8px', borderRadius: '50%', background: dotStyle[inc.status], boxShadow: dotShadow[inc.status] }} />
|
||||||
{inc.name}
|
{inc.name}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ padding: '2px 10px', borderRadius: '10px', fontSize: '10px', fontWeight: 600, background: stBg[inc.status], color: stColor[inc.status], flexShrink: 0 }}>
|
<span className="shrink-0 text-[10px] font-semibold" style={{ padding: '2px 10px', borderRadius: '10px', background: stBg[inc.status], color: stColor[inc.status] }}>
|
||||||
{stLabel[inc.status]}
|
{stLabel[inc.status]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Row 2: meta */}
|
{/* Row 2: meta */}
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', marginBottom: '5px', fontFamily: 'var(--fK)', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div className="flex items-center gap-2 text-[10px] text-text-3 mb-[5px]">
|
||||||
<span>📅 {inc.date} {inc.time}</span>
|
<span>📅 {inc.date} {inc.time}</span>
|
||||||
<span>🏛 {inc.office}</span>
|
<span>🏛 {inc.office}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Row 3: tags + buttons */}
|
{/* Row 3: tags + buttons */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div className="flex items-center justify-between">
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
<div className="flex flex-wrap gap-1">
|
||||||
{inc.causeType && (
|
{inc.causeType && (
|
||||||
<span style={{ padding: '2px 8px', borderRadius: '3px', fontSize: '10px', fontWeight: 500, fontFamily: 'var(--fK)', background: 'rgba(100,116,139,0.08)', border: '1px solid rgba(100,116,139,0.2)', color: 'var(--t2)' }}>
|
<span className="text-[10px] font-medium text-text-2" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(100,116,139,0.08)', border: '1px solid rgba(100,116,139,0.2)' }}>
|
||||||
{inc.causeType}
|
{inc.causeType}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{inc.oilType && (
|
{inc.oilType && (
|
||||||
<span style={{ padding: '2px 8px', borderRadius: '3px', fontSize: '10px', fontWeight: 500, fontFamily: 'var(--fK)', background: 'rgba(249,115,22,0.08)', border: '1px solid rgba(249,115,22,0.2)', color: 'var(--orange)' }}>
|
<span className="text-[10px] font-medium text-status-orange" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(249,115,22,0.08)', border: '1px solid rgba(249,115,22,0.2)' }}>
|
||||||
{inc.oilType}
|
{inc.oilType}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{inc.prediction && (
|
{inc.prediction && (
|
||||||
<span style={{ padding: '2px 8px', borderRadius: '3px', fontSize: '10px', fontWeight: 500, fontFamily: 'var(--fK)', background: 'rgba(34,197,94,0.08)', border: '1px solid rgba(34,197,94,0.2)', color: 'var(--green)' }}>
|
<span className="text-[10px] font-medium text-status-green" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(34,197,94,0.08)', border: '1px solid rgba(34,197,94,0.2)' }}>
|
||||||
{inc.prediction}
|
{inc.prediction}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div className="flex gap-1">
|
||||||
<button className="inc-wx-btn" onClick={(e) => handleWeatherClick(e, inc.id)} title="사고 위치 기상정보" style={{
|
<button className="inc-wx-btn cursor-pointer text-[11px]" onClick={(e) => handleWeatherClick(e, inc.id)} title="사고 위치 기상정보"
|
||||||
padding: '3px 7px', borderRadius: '4px', fontSize: '11px', cursor: 'pointer', lineHeight: 1,
|
style={{
|
||||||
border: '1px solid rgba(59,130,246,0.25)', background: weatherPopupId === inc.id ? 'rgba(59,130,246,0.18)' : 'rgba(59,130,246,0.08)', color: '#60a5fa',
|
padding: '3px 7px', borderRadius: '4px', lineHeight: 1,
|
||||||
transition: '0.15s',
|
border: '1px solid rgba(59,130,246,0.25)', background: weatherPopupId === inc.id ? 'rgba(59,130,246,0.18)' : 'rgba(59,130,246,0.08)', color: '#60a5fa',
|
||||||
}}>🌤</button>
|
|
||||||
{(inc.mediaCount ?? 0) > 0 && (
|
|
||||||
<button onClick={(e) => { e.stopPropagation(); setMediaModalIncident(inc) }} title="현장정보 조회" style={{
|
|
||||||
padding: '3px 7px', borderRadius: '4px', fontSize: '11px', cursor: 'pointer', lineHeight: 1,
|
|
||||||
border: '1px solid rgba(59,130,246,0.25)', background: 'rgba(59,130,246,0.08)', color: '#60a5fa',
|
|
||||||
transition: '0.15s',
|
transition: '0.15s',
|
||||||
}}>📹 <span style={{ fontSize: '8px' }}>{inc.mediaCount}</span></button>
|
}}>🌤</button>
|
||||||
|
{(inc.mediaCount ?? 0) > 0 && (
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); setMediaModalIncident(inc) }} title="현장정보 조회" className="cursor-pointer text-[11px]"
|
||||||
|
style={{
|
||||||
|
padding: '3px 7px', borderRadius: '4px', lineHeight: 1,
|
||||||
|
border: '1px solid rgba(59,130,246,0.25)', background: 'rgba(59,130,246,0.08)', color: '#60a5fa',
|
||||||
|
transition: '0.15s',
|
||||||
|
}}>📹 <span className="text-[8px]">{inc.mediaCount}</span></button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -367,13 +371,11 @@ export function IncidentsLeftPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div style={{
|
<div className="flex items-center justify-between bg-bg-1 shrink-0 border-t border-border px-3 py-2">
|
||||||
padding: '8px 12px', borderTop: '1px solid var(--bd)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0, background: 'var(--bg1)',
|
<div className="text-[9px] text-text-3">
|
||||||
}}>
|
총 <b>{filteredIncidents.length}</b>건 중 {(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filteredIncidents.length)}
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
|
||||||
총 <b style={{ color: 'var(--t1)' }}>{filteredIncidents.length}</b>건 중 {(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filteredIncidents.length)}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '3px' }}>
|
<div className="flex items-center gap-[3px]">
|
||||||
<PgBtn label="⏮" disabled={safePage <= 1} onClick={() => setCurrentPage(1)} />
|
<PgBtn label="⏮" disabled={safePage <= 1} onClick={() => setCurrentPage(1)} />
|
||||||
<PgBtn label="◀" disabled={safePage <= 1} onClick={() => setCurrentPage(Math.max(1, safePage - 1))} />
|
<PgBtn label="◀" disabled={safePage <= 1} onClick={() => setCurrentPage(Math.max(1, safePage - 1))} />
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).filter(p => Math.abs(p - safePage) <= 2).map(p => (
|
{Array.from({ length: totalPages }, (_, i) => i + 1).filter(p => Math.abs(p - safePage) <= 2).map(p => (
|
||||||
@ -382,9 +384,8 @@ export function IncidentsLeftPanel({
|
|||||||
<PgBtn label="▶" disabled={safePage >= totalPages} onClick={() => setCurrentPage(Math.min(totalPages, safePage + 1))} />
|
<PgBtn label="▶" disabled={safePage >= totalPages} onClick={() => setCurrentPage(Math.min(totalPages, safePage + 1))} />
|
||||||
<PgBtn label="⏭" disabled={safePage >= totalPages} onClick={() => setCurrentPage(totalPages)} />
|
<PgBtn label="⏭" disabled={safePage >= totalPages} onClick={() => setCurrentPage(totalPages)} />
|
||||||
</div>
|
</div>
|
||||||
<select onChange={(e) => { /* page size change placeholder */ void e }} style={{
|
<select onChange={(e) => { /* page size change placeholder */ void e }}
|
||||||
padding: '3px 6px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: '4px', color: 'var(--t2)', fontSize: '9px', fontFamily: 'var(--fK)', outline: 'none',
|
className="bg-bg-0 border border-border text-text-2 text-[9px] outline-none rounded px-1.5 py-[3px]">
|
||||||
}}>
|
|
||||||
<option>6건</option><option>10건</option><option>20건</option>
|
<option>6건</option><option>10건</option><option>20건</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -394,14 +395,16 @@ export function IncidentsLeftPanel({
|
|||||||
|
|
||||||
function PgBtn({ label, active, disabled, onClick }: { label: string; active?: boolean; disabled?: boolean; onClick: () => void }) {
|
function PgBtn({ label, active, disabled, onClick }: { label: string; active?: boolean; disabled?: boolean; onClick: () => void }) {
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} disabled={disabled} style={{
|
<button onClick={onClick} disabled={disabled}
|
||||||
minWidth: '24px', height: '24px', padding: '0 5px', borderRadius: '4px', fontSize: '9px', fontWeight: active ? 700 : 600,
|
className="flex items-center justify-center font-mono text-[9px]"
|
||||||
fontFamily: 'var(--fM)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: disabled ? 'default' : 'pointer',
|
style={{
|
||||||
background: active ? 'rgba(6,182,212,0.15)' : 'var(--bg3)',
|
minWidth: '24px', height: '24px', padding: '0 5px', borderRadius: '4px', fontWeight: active ? 700 : 600,
|
||||||
border: active ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--bd)',
|
background: active ? 'rgba(6,182,212,0.15)' : 'var(--bg3)',
|
||||||
color: active ? 'var(--cyan)' : 'var(--t3)',
|
border: active ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--bd)',
|
||||||
opacity: disabled ? 0.4 : 1, pointerEvents: disabled ? 'none' : undefined,
|
color: active ? 'var(--cyan)' : 'var(--t3)',
|
||||||
}}>{label}</button>
|
opacity: disabled ? 0.4 : 1, pointerEvents: disabled ? 'none' : undefined,
|
||||||
|
cursor: disabled ? 'default' : 'pointer',
|
||||||
|
}}>{label}</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,41 +417,38 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}>(({ data, position, onClose }, ref) => {
|
}>(({ data, position, onClose }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div ref={ref} style={{
|
<div ref={ref} className="fixed overflow-hidden rounded-xl border border-border bg-bg-1" style={{
|
||||||
position: 'fixed', zIndex: 9990, width: 280,
|
zIndex: 9990, width: 280,
|
||||||
top: position.top, left: position.left,
|
top: position.top, left: position.left,
|
||||||
background: 'var(--bg1)', border: '1px solid rgba(59,130,246,0.3)', borderRadius: 12,
|
borderColor: 'rgba(59,130,246,0.3)',
|
||||||
boxShadow: '0 12px 40px rgba(0,0,0,0.5)', overflow: 'hidden', backdropFilter: 'blur(12px)',
|
boxShadow: '0 12px 40px rgba(0,0,0,0.5)', backdropFilter: 'blur(12px)',
|
||||||
}}>
|
}}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div className="flex items-center justify-between border-b border-border px-3.5 py-2.5"
|
||||||
padding: '10px 14px',
|
style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.08), rgba(6,182,212,0.04))' }}>
|
||||||
background: 'linear-gradient(135deg, rgba(59,130,246,0.08), rgba(6,182,212,0.04))',
|
<div className="flex items-center gap-1.5">
|
||||||
borderBottom: '1px solid var(--bd)', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
<span className="text-sm">🌤</span>
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
||||||
<span style={{ fontSize: 14 }}>🌤</span>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>{data.locNm}</div>
|
<div className="text-[11px] font-bold">{data.locNm}</div>
|
||||||
<div style={{ fontSize: 8, color: 'var(--t3)', fontFamily: 'var(--fM)' }}>{data.obsDtm}</div>
|
<div className="text-text-3 font-mono text-[8px]">{data.obsDtm}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span onClick={onClose} style={{ fontSize: 14, cursor: 'pointer', color: 'var(--t3)', padding: 2 }}>✕</span>
|
<span onClick={onClose} className="cursor-pointer text-text-3 text-sm p-0.5">✕</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div style={{ padding: '12px 14px' }}>
|
<div className="px-3.5 py-3">
|
||||||
{/* Main weather */}
|
{/* Main weather */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 }}>
|
<div className="flex items-center gap-3 mb-2.5">
|
||||||
<div style={{ fontSize: 28 }}>{data.icon}</div>
|
<div className="text-[28px]">{data.icon}</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>{data.temp}</div>
|
<div className="font-bold font-mono text-[20px]">{data.temp}</div>
|
||||||
<div style={{ fontSize: 9, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{data.weatherDc}</div>
|
<div className="text-text-3 text-[9px]">{data.weatherDc}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Detail grid */}
|
{/* Detail grid */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, fontSize: 9, fontFamily: 'var(--fK)' }}>
|
<div className="grid grid-cols-2 gap-1.5 text-[9px]">
|
||||||
<WxCell icon="💨" label="풍향/풍속" value={data.wind} />
|
<WxCell icon="💨" label="풍향/풍속" value={data.wind} />
|
||||||
<WxCell icon="🌊" label="파고" value={data.wave} />
|
<WxCell icon="🌊" label="파고" value={data.wave} />
|
||||||
<WxCell icon="💧" label="습도" value={data.humid} />
|
<WxCell icon="💧" label="습도" value={data.humid} />
|
||||||
@ -458,52 +458,46 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tide info */}
|
{/* Tide info */}
|
||||||
<div style={{ marginTop: 8, display: 'flex', gap: 6 }}>
|
<div className="flex gap-1.5 mt-2">
|
||||||
<div style={{
|
<div className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md"
|
||||||
flex: 1, padding: '6px 8px', background: 'rgba(59,130,246,0.06)',
|
style={{ background: 'rgba(59,130,246,0.06)', border: '1px solid rgba(59,130,246,0.1)' }}>
|
||||||
border: '1px solid rgba(59,130,246,0.1)', borderRadius: 6,
|
<span className="text-xs">⬆</span>
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 12 }}>⬆</span>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: 'var(--t3)', fontSize: 7, fontFamily: 'var(--fK)' }}>고조 (만조)</div>
|
<div className="text-text-3 text-[7px]">고조 (만조)</div>
|
||||||
<div style={{ color: '#60a5fa', fontWeight: 700, fontFamily: 'var(--fM)', fontSize: 10 }}>{data.highTide}</div>
|
<div className="font-bold font-mono text-[10px] text-[#60a5fa]">{data.highTide}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md"
|
||||||
flex: 1, padding: '6px 8px', background: 'rgba(6,182,212,0.06)',
|
style={{ background: 'rgba(6,182,212,0.06)', border: '1px solid rgba(6,182,212,0.1)' }}>
|
||||||
border: '1px solid rgba(6,182,212,0.1)', borderRadius: 6,
|
<span className="text-xs">⬇</span>
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 12 }}>⬇</span>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: 'var(--t3)', fontSize: 7, fontFamily: 'var(--fK)' }}>저조 (간조)</div>
|
<div className="text-text-3 text-[7px]">저조 (간조)</div>
|
||||||
<div style={{ color: 'var(--cyan)', fontWeight: 700, fontFamily: 'var(--fM)', fontSize: 10 }}>{data.lowTide}</div>
|
<div className="text-primary-cyan font-bold font-mono text-[10px]">{data.lowTide}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 24h Forecast */}
|
{/* 24h Forecast */}
|
||||||
<div style={{ marginTop: 10, padding: '8px 10px', background: 'var(--bg0)', borderRadius: 6 }}>
|
<div className="bg-bg-0 mt-2.5 px-2.5 py-2 rounded-md">
|
||||||
<div style={{ fontSize: 8, fontWeight: 700, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 6 }}>24h 예보</div>
|
<div className="font-bold text-text-3 text-[8px] mb-1.5">24h 예보</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 8, fontFamily: 'var(--fM)', color: 'var(--t2)' }}>
|
<div className="flex justify-between font-mono text-text-2 text-[8px]">
|
||||||
{data.forecast.map((f, i) => (
|
{data.forecast.map((f, i) => (
|
||||||
<div key={i} style={{ textAlign: 'center' }}>
|
<div key={i} className="text-center">
|
||||||
<div>{f.hour}</div>
|
<div>{f.hour}</div>
|
||||||
<div style={{ fontSize: 12, margin: '2px 0' }}>{f.icon}</div>
|
<div className="text-xs my-0.5">{f.icon}</div>
|
||||||
<div style={{ fontWeight: 600 }}>{f.temp}</div>
|
<div className="font-semibold">{f.temp}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Impact */}
|
{/* Impact */}
|
||||||
<div style={{
|
<div className="mt-2 rounded" style={{
|
||||||
marginTop: 8, padding: '6px 10px',
|
padding: '6px 10px',
|
||||||
background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)', borderRadius: 6,
|
background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: 8, fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fK)', marginBottom: 3 }}>⚠ 방제 작업 영향</div>
|
<div className="font-bold text-status-orange text-[8px] mb-[3px]">⚠ 방제 작업 영향</div>
|
||||||
<div style={{ fontSize: 8, color: 'var(--t2)', fontFamily: 'var(--fK)', lineHeight: 1.5 }}>{data.impactDc}</div>
|
<div className="text-text-2 text-[8px] leading-[1.5]">{data.impactDc}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -513,14 +507,11 @@ WeatherPopup.displayName = 'WeatherPopup'
|
|||||||
|
|
||||||
function WxCell({ icon, label, value }: { icon: string; label: string; value: string }) {
|
function WxCell({ icon, label, value }: { icon: string; label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="flex items-center bg-bg-0 rounded gap-[6px]" className="py-1.5 px-2">
|
||||||
padding: '6px 8px', background: 'var(--bg0)', borderRadius: 6,
|
<span className="text-[12px]">{icon}</span>
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 12 }}>{icon}</span>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: 'var(--t3)', fontSize: 7 }}>{label}</div>
|
<div className="text-text-3 text-[7px]">{label}</div>
|
||||||
<div style={{ color: 'var(--t1)', fontWeight: 600, fontFamily: 'var(--fM)' }}>{value}</div>
|
<div className="font-semibold font-mono">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -124,13 +124,9 @@ export function IncidentsRightPanel({
|
|||||||
|
|
||||||
if (!incident) {
|
if (!incident) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="flex flex-col items-center justify-center bg-bg-1 border-l border-border w-[280px] min-w-[280px]">
|
||||||
width: '280px', minWidth: '280px', background: 'var(--bg1)',
|
<div className="text-center text-text-3 text-[11px]">
|
||||||
borderLeft: '1px solid var(--bd)', display: 'flex', flexDirection: 'column',
|
<div className="text-[32px] mb-2 opacity-30">📊</div>
|
||||||
alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}>
|
|
||||||
<div style={{ textAlign: 'center', color: 'var(--t3)', fontSize: '11px', fontFamily: 'var(--fK)' }}>
|
|
||||||
<div style={{ fontSize: '32px', marginBottom: '8px', opacity: 0.3 }}>📊</div>
|
|
||||||
좌측에서 사고를 선택하면<br />통합분석 조회가 표시됩니다
|
좌측에서 사고를 선택하면<br />통합분석 조회가 표시됩니다
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,86 +134,72 @@ export function IncidentsRightPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden h-full w-[280px] min-w-[280px]">
|
||||||
width: '280px', minWidth: '280px', background: 'var(--bg1)',
|
|
||||||
borderLeft: '1px solid var(--bd)', display: 'flex', flexDirection: 'column',
|
|
||||||
overflow: 'hidden', height: '100%',
|
|
||||||
}}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--bd)', flexShrink: 0 }}>
|
<div className="px-[14px] py-2.5 border-b border-border shrink-0">
|
||||||
<div style={{ fontSize: '12px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '2px' }}>
|
<div className="text-xs font-bold mb-0.5">
|
||||||
🔬 통합분석 조회
|
🔬 통합분석 조회
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[9px] text-text-3">
|
||||||
선택: <b style={{ color: 'var(--cyan)' }}>{incident.name}</b>
|
선택: <b className="text-primary-cyan">{incident.name}</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div style={{
|
<div className="flex-1 h-0 overflow-y-auto flex flex-col gap-2 p-2" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||||
flex: 1, height: 0, overflowY: 'auto', padding: '8px',
|
|
||||||
display: 'flex', flexDirection: 'column', gap: '8px',
|
|
||||||
scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent',
|
|
||||||
}}>
|
|
||||||
{/* Analysis Sections (oil / hns / rsc) */}
|
{/* Analysis Sections (oil / hns / rsc) */}
|
||||||
{sections.map(sec => {
|
{sections.map(sec => {
|
||||||
const checkedCount = sec.items.filter(it => it.checked).length
|
const checkedCount = sec.items.filter(it => it.checked).length
|
||||||
return (
|
return (
|
||||||
<div key={sec.key} style={{
|
<div key={sec.key} className="bg-bg-2 border border-border rounded-md p-2.5">
|
||||||
background: 'var(--bg2)', border: '1px solid var(--bd)',
|
|
||||||
borderRadius: '8px', padding: '10px',
|
|
||||||
}}>
|
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div className="flex items-center gap-1.5">
|
||||||
<span style={{ fontSize: '14px' }}>{sec.icon}</span>
|
<span className="text-sm">{sec.icon}</span>
|
||||||
<span style={{ fontSize: '12px', fontWeight: 700, color: sec.color, fontFamily: 'var(--fK)' }}>
|
<span className="text-xs font-bold" style={{ color: sec.color }}>
|
||||||
{sec.title}
|
{sec.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button style={{
|
<button className="text-[10px] font-semibold cursor-pointer"
|
||||||
padding: '3px 10px', borderRadius: '4px', fontSize: '10px', fontWeight: 600,
|
style={{
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
padding: '3px 10px', borderRadius: '4px',
|
||||||
background: `rgba(${sec.colorRgb},0.1)`,
|
background: `rgba(${sec.colorRgb},0.1)`,
|
||||||
border: `1px solid rgba(${sec.colorRgb},0.25)`,
|
border: `1px solid rgba(${sec.colorRgb},0.25)`,
|
||||||
color: sec.color,
|
color: sec.color,
|
||||||
}}>
|
}}>
|
||||||
📋 조회
|
📋 조회
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items */}
|
{/* Items */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
<div className="flex flex-col gap-1">
|
||||||
{sec.items.map(item => (
|
{sec.items.map(item => (
|
||||||
<div key={item.id} style={{
|
<div key={item.id} className="flex items-center gap-1.5"
|
||||||
display: 'flex', alignItems: 'center', gap: '6px',
|
style={{
|
||||||
padding: '5px 8px',
|
padding: '5px 8px',
|
||||||
background: `rgba(${sec.colorRgb},0.06)`,
|
background: `rgba(${sec.colorRgb},0.06)`,
|
||||||
border: `1px solid rgba(${sec.colorRgb},0.15)`,
|
border: `1px solid rgba(${sec.colorRgb},0.15)`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
}}>
|
}}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={item.checked}
|
checked={item.checked}
|
||||||
onChange={() => toggleItem(sec.key, item.id)}
|
onChange={() => toggleItem(sec.key, item.id)}
|
||||||
style={{ accentColor: sec.color, flexShrink: 0 }}
|
className="shrink-0"
|
||||||
|
style={{ accentColor: sec.color }}
|
||||||
/>
|
/>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1 min-w-0">
|
||||||
<div style={{
|
<div className="text-[10px] font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
fontSize: '10px', fontWeight: 600, color: 'var(--t1)',
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
||||||
}}>
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fM)' }}>
|
<div className="text-text-3 font-mono text-[8px]">
|
||||||
{item.sub}
|
{item.sub}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
onClick={() => removeItem(sec.key, item.id)}
|
onClick={() => removeItem(sec.key, item.id)}
|
||||||
title="제거"
|
title="제거"
|
||||||
style={{ fontSize: '10px', cursor: 'pointer', color: 'var(--t3)', flexShrink: 0 }}
|
className="text-[10px] cursor-pointer text-text-3 shrink-0"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</span>
|
</span>
|
||||||
@ -226,10 +208,7 @@ export function IncidentsRightPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div style={{
|
<div className="flex items-center gap-1.5 mt-1.5 text-[9px] text-text-3">
|
||||||
marginTop: '6px', fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)',
|
|
||||||
display: 'flex', alignItems: 'center', gap: '6px',
|
|
||||||
}}>
|
|
||||||
선택: <b style={{ color: sec.color }}>{checkedCount}건</b> · {sec.totalLabel}
|
선택: <b style={{ color: sec.color }}>{checkedCount}건</b> · {sec.totalLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -237,24 +216,19 @@ export function IncidentsRightPanel({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* 민감자원 */}
|
{/* 민감자원 */}
|
||||||
<div style={{
|
<div className="bg-bg-2 border border-border rounded-md p-2.5">
|
||||||
background: 'var(--bg2)', border: '1px solid var(--bd)',
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
borderRadius: '8px', padding: '10px',
|
<span className="text-sm">🐟</span>
|
||||||
}}>
|
<span className="text-xs font-bold text-[#22c55e]">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '8px' }}>
|
|
||||||
<span style={{ fontSize: '14px' }}>🐟</span>
|
|
||||||
<span style={{ fontSize: '12px', fontWeight: 700, color: '#22c55e', fontFamily: 'var(--fK)' }}>
|
|
||||||
민감자원
|
민감자원
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '3px' }}>
|
<div className="flex flex-col gap-[3px]">
|
||||||
{sensitive.map(res => (
|
{sensitive.map(res => (
|
||||||
<label key={res.id} style={{
|
<label key={res.id} className="flex items-center cursor-pointer text-[9px] gap-[5px] rounded-[3px]"
|
||||||
display: 'flex', alignItems: 'center', gap: '5px',
|
style={{
|
||||||
padding: '4px 6px', background: 'rgba(34,197,94,0.06)',
|
padding: '4px 6px', background: 'rgba(34,197,94,0.06)',
|
||||||
borderRadius: '3px', fontSize: '9px', color: 'var(--t1)',
|
}}>
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
|
||||||
}}>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={res.checked}
|
checked={res.checked}
|
||||||
@ -263,7 +237,7 @@ export function IncidentsRightPanel({
|
|||||||
/>
|
/>
|
||||||
{res.name}
|
{res.name}
|
||||||
{res.area && (
|
{res.area && (
|
||||||
<span style={{ color: 'var(--t3)', fontFamily: 'var(--fM)' }}>({res.area})</span>
|
<span className="text-text-3 font-mono">({res.area})</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@ -271,37 +245,25 @@ export function IncidentsRightPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 근처 방제자원 */}
|
{/* 근처 방제자원 */}
|
||||||
<div style={{
|
<div className="bg-bg-2 border border-border rounded-md p-2.5">
|
||||||
background: 'var(--bg2)', border: '1px solid var(--bd)',
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
borderRadius: '8px', padding: '10px',
|
<span className="text-sm">🛡</span>
|
||||||
}}>
|
<span className="text-xs font-bold text-[#f59e0b]">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '8px' }}>
|
|
||||||
<span style={{ fontSize: '14px' }}>🛡</span>
|
|
||||||
<span style={{ fontSize: '12px', fontWeight: 700, color: '#f59e0b', fontFamily: 'var(--fK)' }}>
|
|
||||||
근처 방제자원
|
근처 방제자원
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
<div style={{
|
<div className="py-2.5 text-center text-text-3 text-[10px] leading-[1.7]">
|
||||||
padding: '10px 0', textAlign: 'center', color: 'var(--t3)',
|
<div className="text-xl mb-1 opacity-40">🚢</div>
|
||||||
fontSize: '10px', fontFamily: 'var(--fK)', lineHeight: 1.7,
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: '20px', marginBottom: '4px', opacity: 0.4 }}>🚢</div>
|
|
||||||
지도에서 선박을 클릭하면<br />부근 방제자원이 표시됩니다
|
지도에서 선박을 클릭하면<br />부근 방제자원이 표시됩니다
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Radius slider */}
|
{/* Radius slider */}
|
||||||
<div style={{
|
<div className="mt-2 pt-2" style={{ borderTop: '1px solid rgba(245,158,11,0.1)' }}>
|
||||||
marginTop: '8px', paddingTop: '8px',
|
<div className="flex items-center justify-between mb-[5px]">
|
||||||
borderTop: '1px solid rgba(245,158,11,0.1)',
|
<span className="text-[9px] text-text-3">탐색 반경</span>
|
||||||
}}>
|
<span className="text-[10px] font-bold font-mono text-[#f59e0b]">
|
||||||
<div style={{
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
||||||
marginBottom: '5px',
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>탐색 반경</span>
|
|
||||||
<span style={{ fontSize: '10px', fontWeight: 700, fontFamily: 'var(--fM)', color: '#f59e0b' }}>
|
|
||||||
{nearbyRadius} nm
|
{nearbyRadius} nm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -312,10 +274,10 @@ export function IncidentsRightPanel({
|
|||||||
value={nearbyRadius}
|
value={nearbyRadius}
|
||||||
step={10}
|
step={10}
|
||||||
onChange={(e) => setNearbyRadius(Number(e.target.value))}
|
onChange={(e) => setNearbyRadius(Number(e.target.value))}
|
||||||
|
className="w-full outline-none cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
width: '100%', height: '4px',
|
height: '4px',
|
||||||
background: 'var(--bd)', borderRadius: '2px',
|
background: 'var(--bd)', borderRadius: '2px',
|
||||||
outline: 'none', cursor: 'pointer',
|
|
||||||
accentColor: '#f59e0b',
|
accentColor: '#f59e0b',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -324,12 +286,9 @@ export function IncidentsRightPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{
|
<div className="flex flex-col gap-1.5 p-2.5 border-t border-border shrink-0">
|
||||||
padding: '10px', borderTop: '1px solid var(--bd)', flexShrink: 0,
|
|
||||||
display: 'flex', flexDirection: 'column', gap: '6px',
|
|
||||||
}}>
|
|
||||||
{/* View Mode */}
|
{/* View Mode */}
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div className="flex gap-1">
|
||||||
{([
|
{([
|
||||||
{ mode: 'overlay' as ViewMode, icon: '🗂', label: '오버레이' },
|
{ mode: 'overlay' as ViewMode, icon: '🗂', label: '오버레이' },
|
||||||
{ mode: 'split2' as ViewMode, icon: '◫', label: '2분할' },
|
{ mode: 'split2' as ViewMode, icon: '◫', label: '2분할' },
|
||||||
@ -337,14 +296,14 @@ export function IncidentsRightPanel({
|
|||||||
]).map(v => {
|
]).map(v => {
|
||||||
const isActive = viewMode === v.mode
|
const isActive = viewMode === v.mode
|
||||||
return (
|
return (
|
||||||
<button key={v.mode} onClick={() => onViewModeChange(v.mode)} style={{
|
<button key={v.mode} onClick={() => onViewModeChange(v.mode)} className="flex-1 text-[10px] cursor-pointer"
|
||||||
flex: 1, padding: '6px', borderRadius: 'var(--rS)',
|
style={{
|
||||||
fontSize: '10px', fontWeight: isActive ? 700 : 600,
|
padding: '6px', borderRadius: 'var(--rS)',
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
fontWeight: isActive ? 700 : 600,
|
||||||
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg3)',
|
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg3)',
|
||||||
border: isActive ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
|
border: isActive ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
|
||||||
color: isActive ? 'var(--cyan)' : 'var(--t3)',
|
color: isActive ? 'var(--cyan)' : 'var(--t3)',
|
||||||
}}>
|
}}>
|
||||||
{v.icon} {v.label}
|
{v.icon} {v.label}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@ -357,17 +316,16 @@ export function IncidentsRightPanel({
|
|||||||
const checkedSections = sections.filter(s => s.items.some(it => it.checked))
|
const checkedSections = sections.filter(s => s.items.some(it => it.checked))
|
||||||
const sensChecked = sensitive.filter(s => s.checked).length
|
const sensChecked = sensitive.filter(s => s.checked).length
|
||||||
onRunAnalysis(checkedSections, sensChecked)
|
onRunAnalysis(checkedSections, sensChecked)
|
||||||
}} style={{
|
}} className="w-full text-[11px] font-bold cursor-pointer"
|
||||||
width: '100%', padding: '8px',
|
style={{
|
||||||
background: analysisActive
|
padding: '8px',
|
||||||
? 'linear-gradient(135deg,rgba(239,68,68,0.15),rgba(239,68,68,0.1))'
|
background: analysisActive
|
||||||
: 'linear-gradient(135deg,rgba(6,182,212,0.15),rgba(59,130,246,0.1))',
|
? 'linear-gradient(135deg,rgba(239,68,68,0.15),rgba(239,68,68,0.1))'
|
||||||
border: analysisActive ? '1px solid rgba(239,68,68,0.3)' : '1px solid rgba(6,182,212,0.3)',
|
: 'linear-gradient(135deg,rgba(6,182,212,0.15),rgba(59,130,246,0.1))',
|
||||||
borderRadius: 'var(--rS)',
|
border: analysisActive ? '1px solid rgba(239,68,68,0.3)' : '1px solid rgba(6,182,212,0.3)',
|
||||||
color: analysisActive ? '#f87171' : 'var(--cyan)',
|
borderRadius: 'var(--rS)',
|
||||||
fontSize: '11px', fontWeight: 700, cursor: 'pointer',
|
color: analysisActive ? '#f87171' : 'var(--cyan)',
|
||||||
fontFamily: 'var(--fK)',
|
}}>
|
||||||
}}>
|
|
||||||
{analysisActive ? '✕ 분석 닫기' : '🔬 통합 분석 비교 실행'}
|
{analysisActive ? '✕ 분석 닫기' : '🔬 통합 분석 비교 실행'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -52,15 +52,21 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
|
|
||||||
if (!media) {
|
if (!media) {
|
||||||
return (
|
return (
|
||||||
<div onClick={(e) => { if (e.target === e.currentTarget) onClose() }} style={{
|
<div
|
||||||
position: 'fixed', inset: 0, zIndex: 10000,
|
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
className="fixed inset-0 z-[10000] flex items-center justify-center"
|
||||||
background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)',
|
style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }}
|
||||||
}}>
|
>
|
||||||
<div style={{
|
<div
|
||||||
width: 300, padding: 40, background: '#0d1117', border: '1px solid #30363d',
|
className="text-center text-[12px] text-[#8b949e]"
|
||||||
borderRadius: 14, textAlign: 'center', color: '#8b949e', fontFamily: 'var(--fK)', fontSize: 12,
|
style={{
|
||||||
}}>
|
width: 300,
|
||||||
|
padding: 40,
|
||||||
|
background: '#0d1117',
|
||||||
|
border: '1px solid #30363d',
|
||||||
|
borderRadius: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
현장정보를 불러오는 중...
|
현장정보를 불러오는 중...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -75,42 +81,50 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
const showCctv = activeTab === 'all' || activeTab === 'cctv'
|
const showCctv = activeTab === 'all' || activeTab === 'cctv'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={(e) => { if (e.target === e.currentTarget) onClose() }} style={{
|
<div
|
||||||
position: 'fixed', inset: 0, zIndex: 10000,
|
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
className="fixed inset-0 z-[10000] flex items-center justify-center"
|
||||||
background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)',
|
style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }}
|
||||||
}}>
|
>
|
||||||
<div style={{
|
<div
|
||||||
width: '95vw', height: '92vh', maxWidth: 1600,
|
className="flex flex-col overflow-hidden"
|
||||||
background: '#0d1117', border: '1px solid #30363d', borderRadius: 14,
|
style={{
|
||||||
overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
width: '95vw',
|
||||||
boxShadow: '0 24px 64px rgba(0,0,0,0.7)',
|
height: '92vh',
|
||||||
}}>
|
maxWidth: 1600,
|
||||||
|
background: '#0d1117',
|
||||||
|
border: '1px solid #30363d',
|
||||||
|
borderRadius: 14,
|
||||||
|
boxShadow: '0 24px 64px rgba(0,0,0,0.7)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* ── Header ─────────────────────────────────── */}
|
{/* ── Header ─────────────────────────────────── */}
|
||||||
<div style={{
|
<div
|
||||||
flexShrink: 0, padding: '12px 20px',
|
className="shrink-0 flex items-center justify-between"
|
||||||
background: 'linear-gradient(135deg,#161b22,#0d1117)',
|
style={{
|
||||||
borderBottom: '1px solid #30363d',
|
padding: '12px 20px',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
background: 'linear-gradient(135deg,#161b22,#0d1117)',
|
||||||
}}>
|
borderBottom: '1px solid #30363d',
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
}}
|
||||||
<span style={{ fontSize: 18 }}>📋</span>
|
>
|
||||||
|
<div className="flex items-center gap-[10px]">
|
||||||
|
<span className="text-lg">📋</span>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 14, fontWeight: 800, color: '#f0f6fc', fontFamily: 'var(--fK)' }}>
|
<div className="text-[14px] font-[800] text-[#f0f6fc]">
|
||||||
현장정보 — {incident.name}
|
현장정보 — {incident.name}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 10, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
<div className="text-[10px] text-[#8b949e] font-mono">
|
||||||
{incident.name} · {incident.date} · 사진 {media.photoCnt} / 영상 {media.videoCnt} / 위성 {media.satCnt} / CCTV {media.cctvCnt}
|
{incident.name} · {incident.date} · 사진 {media.photoCnt} / 영상 {media.videoCnt} / 위성 {media.satCnt} / CCTV {media.cctvCnt}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div className="flex items-center gap-[8px]">
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div style={{ display: 'flex', gap: 2 }}>
|
<div className="flex gap-[2px]">
|
||||||
{MEDIA_TABS.map(t => (
|
{MEDIA_TABS.map(t => (
|
||||||
<button key={t.id} onClick={() => setActiveTab(t.id)} style={{
|
<button key={t.id} onClick={() => setActiveTab(t.id)} style={{
|
||||||
padding: '5px 12px', borderRadius: 6, fontSize: 11, fontWeight: activeTab === t.id ? 700 : 400,
|
padding: '5px 12px', borderRadius: 6, fontSize: 11, fontWeight: activeTab === t.id ? 700 : 400,
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer', border: 'none',
|
cursor: 'pointer', border: 'none',
|
||||||
background: activeTab === t.id ? 'rgba(168,85,247,0.15)' : 'transparent',
|
background: activeTab === t.id ? 'rgba(168,85,247,0.15)' : 'transparent',
|
||||||
color: activeTab === t.id ? '#c084fc' : '#8b949e',
|
color: activeTab === t.id ? '#c084fc' : '#8b949e',
|
||||||
}}>
|
}}>
|
||||||
@ -121,25 +135,26 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
{/* Upload */}
|
{/* Upload */}
|
||||||
<button style={{
|
<button style={{
|
||||||
padding: '5px 14px', borderRadius: 6, fontSize: 11, fontWeight: 700,
|
padding: '5px 14px', borderRadius: 6, fontSize: 11, fontWeight: 700,
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer', border: 'none',
|
cursor: 'pointer', border: 'none',
|
||||||
background: 'linear-gradient(135deg,rgba(168,85,247,0.3),rgba(168,85,247,0.15))',
|
background: 'linear-gradient(135deg,rgba(168,85,247,0.3),rgba(168,85,247,0.15))',
|
||||||
color: '#c084fc',
|
color: '#c084fc',
|
||||||
}}>📤 업로드</button>
|
}}>📤 업로드</button>
|
||||||
{/* Close */}
|
{/* Close */}
|
||||||
<span onClick={onClose} style={{
|
<span
|
||||||
fontSize: 18, cursor: 'pointer', color: '#8b949e', padding: '2px 6px',
|
onClick={onClose}
|
||||||
borderRadius: 4,
|
className="text-[18px] cursor-pointer text-[#8b949e] rounded"
|
||||||
}}>✕</span>
|
style={{ padding: '2px 6px' }}
|
||||||
|
>✕</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Timeline ────────────────────────────────── */}
|
{/* ── Timeline ────────────────────────────────── */}
|
||||||
<div style={{
|
<div
|
||||||
flexShrink: 0, padding: '6px 20px', borderBottom: '1px solid #21262d',
|
className="shrink-0 flex items-center gap-[10px]"
|
||||||
display: 'flex', alignItems: 'center', gap: 10,
|
style={{ padding: '6px 20px', borderBottom: '1px solid #21262d' }}
|
||||||
}}>
|
>
|
||||||
<span style={{ fontSize: 9, color: '#8b949e', fontFamily: 'var(--fK)', whiteSpace: 'nowrap' }}>TIMELINE</span>
|
<span className="text-[9px] text-[#8b949e] whitespace-nowrap">TIMELINE</span>
|
||||||
<div style={{ flex: 1, position: 'relative', height: 16 }}>
|
<div className="flex-1 relative" style={{ height: 16 }}>
|
||||||
<div style={{ position: 'absolute', top: 7, left: 0, right: 0, height: 2, background: '#21262d', borderRadius: 1 }} />
|
<div style={{ position: 'absolute', top: 7, left: 0, right: 0, height: 2, background: '#21262d', borderRadius: 1 }} />
|
||||||
{timelineDots.map((d, i) => (
|
{timelineDots.map((d, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
@ -149,66 +164,67 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
}} />
|
}} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fM)', display: 'flex', gap: 8, whiteSpace: 'nowrap' }}>
|
<div className="flex gap-2 text-[8px] font-mono text-[#8b949e] whitespace-nowrap">
|
||||||
<span style={{ color: '#ef4444' }}>● 초기</span>
|
<span style={{ color: '#ef4444' }}>● 초기</span>
|
||||||
<span style={{ color: '#f59e0b' }}>● 대응</span>
|
<span style={{ color: '#f59e0b' }}>● 대응</span>
|
||||||
<span style={{ color: '#8b949e' }}>● 종료</span>
|
<span className="text-[#8b949e]">● 종료</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 2x2 Grid Content ────────────────────────── */}
|
{/* ── 2x2 Grid Content ────────────────────────── */}
|
||||||
<div style={{
|
<div
|
||||||
flex: 1, display: 'grid', overflow: 'hidden',
|
className="flex-1 overflow-hidden grid"
|
||||||
gridTemplateColumns: (showPhoto || showSat) && (showVideo || showCctv) ? '1fr 1fr' : '1fr',
|
style={{
|
||||||
gridTemplateRows: (showPhoto || showVideo) && (showSat || showCctv) ? '1fr 1fr' : '1fr',
|
gridTemplateColumns: (showPhoto || showSat) && (showVideo || showCctv) ? '1fr 1fr' : '1fr',
|
||||||
gap: 1, background: '#21262d',
|
gridTemplateRows: (showPhoto || showVideo) && (showSat || showCctv) ? '1fr 1fr' : '1fr',
|
||||||
}}>
|
gap: 1,
|
||||||
|
background: '#21262d',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* ── Q1: 현장사진 ──────────────────────────── */}
|
{/* ── Q1: 현장사진 ──────────────────────────── */}
|
||||||
{showPhoto && (
|
{showPhoto && (
|
||||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||||
{/* Section header */}
|
{/* Section header */}
|
||||||
<div style={{
|
<div
|
||||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
className="shrink-0 flex items-center justify-between"
|
||||||
borderBottom: '1px solid #21262d',
|
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||||
}}>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-[6px]">
|
||||||
<span style={{ fontSize: 12 }}>📷</span>
|
<span className="text-[12px]">📷</span>
|
||||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc', fontFamily: 'var(--fK)' }}>
|
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||||
현장사진 — {str(media.photoMeta, 'title', '현장 사진')}
|
현장사진 — {str(media.photoMeta, 'title', '현장 사진')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div className="flex gap-[4px]">
|
||||||
<NavBtn label="◀" /> <NavBtn label="▶" /> <NavBtn label="↗" />
|
<NavBtn label="◀" /> <NavBtn label="▶" /> <NavBtn label="↗" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Photo content */}
|
{/* Photo content */}
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
|
<div className="flex-1 flex items-center justify-center flex-col gap-2">
|
||||||
<div style={{ fontSize: 48, color: '#30363d' }}>📷</div>
|
<div className="text-[48px] text-[#30363d]">📷</div>
|
||||||
<div style={{ fontSize: 12, color: '#c9d1d9', fontWeight: 600, fontFamily: 'var(--fK)' }}>
|
<div className="text-[12px] text-[#c9d1d9] font-semibold">
|
||||||
{incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')} 해상 사진
|
{incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')} 해상 사진
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 9, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||||
{str(media.photoMeta, 'date')} · {str(media.photoMeta, 'by')}
|
{str(media.photoMeta, 'date')} · {str(media.photoMeta, 'by')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Thumbnails */}
|
{/* Thumbnails */}
|
||||||
<div style={{ flexShrink: 0, padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
<div className="shrink-0" style={{ padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
||||||
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
|
<div className="flex gap-1.5" style={{ marginBottom: 6 }}>
|
||||||
{Array.from({ length: Math.min(num(media.photoMeta, 'thumbCount'), 7) }).map((_, i) => (
|
{Array.from({ length: Math.min(num(media.photoMeta, 'thumbCount'), 7) }).map((_, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} className="flex items-center justify-center text-[14px] text-[#30363d] cursor-pointer" style={{
|
||||||
width: 40, height: 36, borderRadius: 4,
|
width: 40, height: 36, borderRadius: 4,
|
||||||
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22',
|
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22',
|
||||||
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid #30363d',
|
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid #30363d',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
fontSize: 14, color: '#30363d', cursor: 'pointer',
|
|
||||||
}}>📷</div>
|
}}>📷</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="flex justify-between items-center">
|
||||||
<span style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fK)' }}>
|
<span className="text-[8px] text-[#8b949e]">
|
||||||
📷 사진 {num(media.photoMeta, 'thumbCount')}장 · {str(media.photoMeta, 'stage')}
|
📷 사진 {num(media.photoMeta, 'thumbCount')}장 · {str(media.photoMeta, 'stage')}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 8, color: '#a78bfa', cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔗 R&D 연계</span>
|
<span className="text-[8px] text-[#a78bfa] cursor-pointer">🔗 R&D 연계</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -216,51 +232,59 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
|
|
||||||
{/* ── Q2: 드론 영상 ─────────────────────────── */}
|
{/* ── Q2: 드론 영상 ─────────────────────────── */}
|
||||||
{showVideo && (
|
{showVideo && (
|
||||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||||
<div style={{
|
<div
|
||||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
className="shrink-0 flex items-center justify-between"
|
||||||
borderBottom: '1px solid #21262d',
|
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||||
}}>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-[6px]">
|
||||||
<span style={{ fontSize: 12 }}>🎬</span>
|
<span className="text-[12px]">🎬</span>
|
||||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc', fontFamily: 'var(--fK)' }}>
|
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||||
드론 영상 — {str(media.droneMeta, 'title', '드론 영상')}
|
드론 영상 — {str(media.droneMeta, 'title', '드론 영상')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span
|
||||||
padding: '2px 8px', borderRadius: 4, fontSize: 9, fontWeight: 700,
|
className="text-[9px] font-bold text-[#ef4444] rounded"
|
||||||
background: 'rgba(239,68,68,0.15)', color: '#ef4444',
|
style={{
|
||||||
}}>● REC</span>
|
padding: '2px 8px',
|
||||||
|
background: 'rgba(239,68,68,0.15)',
|
||||||
|
}}
|
||||||
|
>● REC</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
|
<div className="flex-1 flex items-center justify-center flex-col gap-2">
|
||||||
<div style={{ fontSize: 48, color: '#30363d' }}>🎬</div>
|
<div className="text-[48px] text-[#30363d]">🎬</div>
|
||||||
<div style={{ fontSize: 12, color: '#c9d1d9', fontWeight: 600, fontFamily: 'var(--fK)' }}>
|
<div className="text-[12px] text-[#c9d1d9] font-semibold">
|
||||||
드론 항공 촬영 영상
|
드론 항공 촬영 영상
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 9, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||||
{str(media.droneMeta, 'device')} · {str(media.droneMeta, 'alt')} 고도
|
{str(media.droneMeta, 'device')} · {str(media.droneMeta, 'alt')} 고도
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Video controls */}
|
{/* Video controls */}
|
||||||
<div style={{ flexShrink: 0, padding: '10px 16px', borderTop: '1px solid #21262d', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
className="shrink-0 flex flex-col gap-2"
|
||||||
<span style={{ fontSize: 12, color: '#8b949e', cursor: 'pointer' }}>⏮</span>
|
style={{ padding: '10px 16px', borderTop: '1px solid #21262d' }}
|
||||||
<div style={{
|
>
|
||||||
width: 28, height: 28, borderRadius: '50%', background: 'rgba(168,85,247,0.15)',
|
<div className="flex items-center justify-center gap-3">
|
||||||
border: '1px solid rgba(168,85,247,0.3)',
|
<span className="text-[12px] text-[#8b949e] cursor-pointer">⏮</span>
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
<div
|
||||||
fontSize: 12, color: '#c084fc', cursor: 'pointer',
|
className="flex items-center justify-center text-[12px] text-[#c084fc] cursor-pointer"
|
||||||
}}>▶</div>
|
style={{
|
||||||
<span style={{ fontSize: 12, color: '#8b949e', cursor: 'pointer' }}>⏭</span>
|
width: 28, height: 28, borderRadius: '50%',
|
||||||
<span style={{ fontSize: 10, color: '#8b949e', fontFamily: 'var(--fM)' }}>02:34 / {str(media.droneMeta, 'duration')}</span>
|
background: 'rgba(168,85,247,0.15)',
|
||||||
|
border: '1px solid rgba(168,85,247,0.3)',
|
||||||
|
}}
|
||||||
|
>▶</div>
|
||||||
|
<span className="text-[12px] text-[#8b949e] cursor-pointer">⏭</span>
|
||||||
|
<span className="text-[10px] text-[#8b949e] font-mono">02:34 / {str(media.droneMeta, 'duration')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="flex justify-between items-center">
|
||||||
<span style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fK)' }}>
|
<span className="text-[8px] text-[#8b949e]">
|
||||||
🎬 영상 {num(media.droneMeta, 'videoCount')}건 · {str(media.droneMeta, 'stage')}
|
🎬 영상 {num(media.droneMeta, 'videoCount')}건 · {str(media.droneMeta, 'stage')}
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="flex gap-[8px]">
|
||||||
<span style={{ fontSize: 8, color: '#58a6ff', cursor: 'pointer', fontFamily: 'var(--fK)' }}>📂 전체보기</span>
|
<span className="text-[8px] text-[#58a6ff] cursor-pointer">📂 전체보기</span>
|
||||||
<span style={{ fontSize: 8, color: '#a78bfa', cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔗 R&D 연계</span>
|
<span className="text-[8px] text-[#a78bfa] cursor-pointer">🔗 R&D 연계</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -269,66 +293,67 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
|
|
||||||
{/* ── Q3: 위성영상 ──────────────────────────── */}
|
{/* ── Q3: 위성영상 ──────────────────────────── */}
|
||||||
{showSat && (
|
{showSat && (
|
||||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||||
<div style={{
|
<div
|
||||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
className="shrink-0 flex items-center justify-between"
|
||||||
borderBottom: '1px solid #21262d',
|
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||||
}}>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-[6px]">
|
||||||
<span style={{ fontSize: 12 }}>🛰</span>
|
<span className="text-[12px]">🛰</span>
|
||||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc', fontFamily: 'var(--fK)' }}>
|
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||||
위성영상 — {str(media.satMeta, 'title', '위성영상')}
|
위성영상 — {str(media.satMeta, 'title', '위성영상')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div className="flex gap-[4px]">
|
||||||
<NavBtn label="◀" /> <NavBtn label="▶" /> <NavBtn label="↗" />
|
<NavBtn label="◀" /> <NavBtn label="▶" /> <NavBtn label="↗" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
|
<div className="flex-1 flex items-center justify-center relative">
|
||||||
{str(media.satMeta, 'detection') !== '—' && (
|
{str(media.satMeta, 'detection') !== '—' && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute', top: '15%', left: '10%', width: '55%', height: '60%',
|
position: 'absolute', top: '15%', left: '10%', width: '55%', height: '60%',
|
||||||
border: '2px dashed #ef4444', borderRadius: 4,
|
border: '2px dashed #ef4444', borderRadius: 4,
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6,
|
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ position: 'absolute', top: -10, left: 8, fontSize: 9, fontWeight: 700, color: '#ef4444', fontFamily: 'var(--fM)', background: '#0d1117', padding: '0 4px' }}>
|
<div
|
||||||
|
className="absolute text-[9px] font-bold text-[#ef4444] font-mono bg-[#0d1117]"
|
||||||
|
style={{ top: -10, left: 8, padding: '0 4px' }}
|
||||||
|
>
|
||||||
{str(media.satMeta, 'detection')}
|
{str(media.satMeta, 'detection')}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 40, color: '#30363d' }}>🛰</div>
|
<div className="text-[40px] text-[#30363d]">🛰</div>
|
||||||
<div style={{ fontSize: 11, color: '#c9d1d9', fontWeight: 600, fontFamily: 'var(--fK)' }}>
|
<div className="text-[11px] text-[#c9d1d9] font-semibold">
|
||||||
{str(media.satMeta, 'title', '위성영상')} 위성영상
|
{str(media.satMeta, 'title', '위성영상')} 위성영상
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
<div className="text-[8px] text-[#8b949e] font-mono">
|
||||||
{str(media.satMeta, 'date')} · 해상도 {str(media.satMeta, 'resolution')}
|
{str(media.satMeta, 'date')} · 해상도 {str(media.satMeta, 'resolution')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{str(media.satMeta, 'detection') === '—' && (
|
{str(media.satMeta, 'detection') === '—' && (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div className="text-center">
|
||||||
<div style={{ fontSize: 40, color: '#30363d' }}>🛰</div>
|
<div className="text-[40px] text-[#30363d]">🛰</div>
|
||||||
<div style={{ fontSize: 11, color: '#8b949e', fontFamily: 'var(--fK)', marginTop: 8 }}>위성영상 없음</div>
|
<div className="text-[11px] text-[#8b949e]" style={{ marginTop: 8 }}>위성영상 없음</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flexShrink: 0, padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
<div className="shrink-0" style={{ padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
||||||
{num(media.satMeta, 'thumbCount') > 0 && (
|
{num(media.satMeta, 'thumbCount') > 0 && (
|
||||||
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
|
<div className="flex gap-1.5" style={{ marginBottom: 6 }}>
|
||||||
{Array.from({ length: num(media.satMeta, 'thumbCount') }).map((_, i) => (
|
{Array.from({ length: num(media.satMeta, 'thumbCount') }).map((_, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} className="flex items-center justify-center text-[14px] text-[#30363d] cursor-pointer" style={{
|
||||||
width: 40, height: 36, borderRadius: 4,
|
width: 40, height: 36, borderRadius: 4,
|
||||||
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22',
|
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22',
|
||||||
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid #30363d',
|
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid #30363d',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
fontSize: 14, color: '#30363d', cursor: 'pointer',
|
|
||||||
}}>🛰</div>
|
}}>🛰</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="flex justify-between items-center">
|
||||||
<span style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fK)' }}>
|
<span className="text-[8px] text-[#8b949e]">
|
||||||
🛰 위성 {num(media.satMeta, 'thumbCount')}장 · {str(media.satMeta, 'sensor')}
|
🛰 위성 {num(media.satMeta, 'thumbCount')}장 · {str(media.satMeta, 'sensor')}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 8, color: '#58a6ff', cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔍 편집/측 비교</span>
|
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🔍 편집/측 비교</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -336,44 +361,50 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
|
|
||||||
{/* ── Q4: CCTV ──────────────────────────────── */}
|
{/* ── Q4: CCTV ──────────────────────────────── */}
|
||||||
{showCctv && (
|
{showCctv && (
|
||||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||||
<div style={{
|
<div
|
||||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
className="shrink-0 flex items-center justify-between"
|
||||||
borderBottom: '1px solid #21262d',
|
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||||
}}>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-[6px]">
|
||||||
<span style={{ fontSize: 12 }}>📹</span>
|
<span className="text-[12px]">📹</span>
|
||||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc', fontFamily: 'var(--fK)' }}>
|
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||||
CCTV — {str(media.cctvMeta, 'title', 'CCTV')}
|
CCTV — {str(media.cctvMeta, 'title', 'CCTV')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-[6px]">
|
||||||
{bool(media.cctvMeta, 'live') && (
|
{bool(media.cctvMeta, 'live') && (
|
||||||
<span style={{
|
<span
|
||||||
padding: '2px 8px', borderRadius: 4, fontSize: 9, fontWeight: 700,
|
className="text-[9px] font-bold text-[#22c55e] rounded"
|
||||||
background: 'rgba(34,197,94,0.15)', color: '#22c55e',
|
style={{
|
||||||
}}>● LIVE</span>
|
padding: '2px 8px',
|
||||||
|
background: 'rgba(34,197,94,0.15)',
|
||||||
|
}}
|
||||||
|
>● LIVE</span>
|
||||||
)}
|
)}
|
||||||
<NavBtn label="↗" />
|
<NavBtn label="↗" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8, position: 'relative' }}>
|
<div className="flex-1 flex items-center justify-center flex-col gap-2 relative">
|
||||||
{bool(media.cctvMeta, 'live') && (
|
{bool(media.cctvMeta, 'live') && (
|
||||||
<div style={{ position: 'absolute', top: 10, left: 16, fontSize: 9, fontWeight: 700, color: '#ef4444', fontFamily: 'var(--fM)' }}>
|
<div className="absolute text-[9px] font-bold text-[#ef4444] font-mono" style={{ top: 10, left: 16 }}>
|
||||||
● LIVE {new Date().toLocaleTimeString('ko-KR', { hour12: false })}
|
● LIVE {new Date().toLocaleTimeString('ko-KR', { hour12: false })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ fontSize: 48, color: '#30363d' }}>📹</div>
|
<div className="text-[48px] text-[#30363d]">📹</div>
|
||||||
<div style={{ fontSize: 12, color: '#c9d1d9', fontWeight: 600, fontFamily: 'var(--fK)' }}>
|
<div className="text-[12px] text-[#c9d1d9] font-semibold">
|
||||||
{str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')}
|
{str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 9, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||||
{str(media.cctvMeta, 'ptz')} · {str(media.cctvMeta, 'angle')} · {bool(media.cctvMeta, 'live') ? '실시간 스트리밍' : '녹화 영상'}
|
{str(media.cctvMeta, 'ptz')} · {str(media.cctvMeta, 'angle')} · {bool(media.cctvMeta, 'live') ? '실시간 스트리밍' : '녹화 영상'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* CAM buttons */}
|
{/* CAM buttons */}
|
||||||
<div style={{ flexShrink: 0, padding: '10px 16px', borderTop: '1px solid #21262d', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
className="shrink-0 flex flex-col gap-2"
|
||||||
|
style={{ padding: '10px 16px', borderTop: '1px solid #21262d' }}
|
||||||
|
>
|
||||||
|
<div className="flex gap-[6px]">
|
||||||
{Array.from({ length: num(media.cctvMeta, 'camCount') }).map((_, i) => (
|
{Array.from({ length: num(media.cctvMeta, 'camCount') }).map((_, i) => (
|
||||||
<button key={i} onClick={() => setSelectedCam(i)} style={{
|
<button key={i} onClick={() => setSelectedCam(i)} style={{
|
||||||
padding: '6px 16px', borderRadius: 4, fontSize: 10, fontWeight: 600,
|
padding: '6px 16px', borderRadius: 4, fontSize: 10, fontWeight: 600,
|
||||||
@ -384,13 +415,13 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
}}>CAM{i + 1}</button>
|
}}>CAM{i + 1}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="flex justify-between items-center">
|
||||||
<span style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fK)' }}>
|
<span className="text-[8px] text-[#8b949e]">
|
||||||
📹 CCTV {num(media.cctvMeta, 'camCount')}채널 · {str(media.cctvMeta, 'location')}
|
📹 CCTV {num(media.cctvMeta, 'camCount')}채널 · {str(media.cctvMeta, 'location')}
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="flex gap-[8px]">
|
||||||
<span style={{ fontSize: 8, color: '#ef4444', cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔴 녹화영상</span>
|
<span className="text-[8px] text-[#ef4444] cursor-pointer">🔴 녹화영상</span>
|
||||||
<span style={{ fontSize: 8, color: '#58a6ff', cursor: 'pointer', fontFamily: 'var(--fK)' }}>🎥 PTZ</span>
|
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🎥 PTZ</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -399,19 +430,22 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Bottom Bar ──────────────────────────────── */}
|
{/* ── Bottom Bar ──────────────────────────────── */}
|
||||||
<div style={{
|
<div
|
||||||
flexShrink: 0, padding: '8px 20px',
|
className="shrink-0 flex items-center justify-between"
|
||||||
background: '#161b22', borderTop: '1px solid #30363d',
|
style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
padding: '8px 20px',
|
||||||
}}>
|
background: '#161b22',
|
||||||
<div style={{ display: 'flex', gap: 16, fontSize: 10, fontFamily: 'var(--fM)', color: '#8b949e' }}>
|
borderTop: '1px solid #30363d',
|
||||||
<span>📷 사진 <b style={{ color: '#f0f6fc' }}>{media.photoCnt}</b></span>
|
}}
|
||||||
<span>🎬 영상 <b style={{ color: '#f0f6fc' }}>{media.videoCnt}</b></span>
|
>
|
||||||
<span>🛰 위성 <b style={{ color: '#f0f6fc' }}>{media.satCnt}</b></span>
|
<div className="flex gap-4 text-[10px] font-mono text-[#8b949e]">
|
||||||
<span>📹 CCTV <b style={{ color: '#f0f6fc' }}>{media.cctvCnt}</b></span>
|
<span>📷 사진 <b className="text-[#f0f6fc]">{media.photoCnt}</b></span>
|
||||||
<span>📎 총 <b style={{ color: '#c084fc' }}>{total}건</b></span>
|
<span>🎬 영상 <b className="text-[#f0f6fc]">{media.videoCnt}</b></span>
|
||||||
|
<span>🛰 위성 <b className="text-[#f0f6fc]">{media.satCnt}</b></span>
|
||||||
|
<span>📹 CCTV <b className="text-[#f0f6fc]">{media.cctvCnt}</b></span>
|
||||||
|
<span>📎 총 <b className="text-[#c084fc]">{total}건</b></span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="flex gap-[8px]">
|
||||||
<BottomBtn icon="📥" label="다운로드" bg="rgba(100,116,139,0.1)" bd="rgba(100,116,139,0.2)" fg="#8b949e" />
|
<BottomBtn icon="📥" label="다운로드" bg="rgba(100,116,139,0.1)" bd="rgba(100,116,139,0.2)" fg="#8b949e" />
|
||||||
<BottomBtn icon="📝" label="보고서" bg="rgba(59,130,246,0.1)" bd="rgba(59,130,246,0.2)" fg="#58a6ff" />
|
<BottomBtn icon="📝" label="보고서" bg="rgba(59,130,246,0.1)" bd="rgba(59,130,246,0.2)" fg="#58a6ff" />
|
||||||
<BottomBtn icon="🔗" label="R&D 분석 연계" bg="rgba(168,85,247,0.1)" bd="rgba(168,85,247,0.25)" fg="#c084fc" />
|
<BottomBtn icon="🔗" label="R&D 분석 연계" bg="rgba(168,85,247,0.1)" bd="rgba(168,85,247,0.25)" fg="#c084fc" />
|
||||||
@ -424,21 +458,26 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
|||||||
|
|
||||||
function NavBtn({ label }: { label: string }) {
|
function NavBtn({ label }: { label: string }) {
|
||||||
return (
|
return (
|
||||||
<button style={{
|
<button
|
||||||
width: 22, height: 22, borderRadius: 4, fontSize: 10,
|
className="flex items-center justify-center text-[10px] text-[#8b949e] cursor-pointer rounded bg-[#161b22]"
|
||||||
background: '#161b22', border: '1px solid #30363d', color: '#8b949e',
|
style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
width: 22, height: 22,
|
||||||
}}>{label}</button>
|
border: '1px solid #30363d',
|
||||||
|
}}
|
||||||
|
>{label}</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BottomBtn({ icon, label, bg, bd, fg }: { icon: string; label: string; bg: string; bd: string; fg: string }) {
|
function BottomBtn({ icon, label, bg, bd, fg }: { icon: string; label: string; bg: string; bd: string; fg: string }) {
|
||||||
return (
|
return (
|
||||||
<button style={{
|
<button
|
||||||
padding: '6px 14px', borderRadius: 6, fontSize: 10, fontWeight: 700,
|
className="flex items-center gap-1 text-[10px] font-bold cursor-pointer rounded-sm"
|
||||||
fontFamily: 'var(--fK)', cursor: 'pointer',
|
style={{
|
||||||
background: bg, border: `1px solid ${bd}`, color: fg,
|
padding: '6px 14px',
|
||||||
display: 'flex', alignItems: 'center', gap: 4,
|
background: bg,
|
||||||
}}>{icon} {label}</button>
|
border: `1px solid ${bd}`,
|
||||||
|
color: fg,
|
||||||
|
}}
|
||||||
|
>{icon} {label}</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,42 +36,34 @@ export function BacktrackModal({
|
|||||||
<div
|
<div
|
||||||
ref={backdropRef}
|
ref={backdropRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed', inset: 0, zIndex: 9999,
|
inset: 0,
|
||||||
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
|
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
|
className="fixed z-[9999] flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '580px', maxHeight: 'calc(100vh - 120px)',
|
width: '580px', maxHeight: 'calc(100vh - 120px)',
|
||||||
background: 'var(--bg1)', border: '1px solid var(--bd)',
|
background: 'var(--bg1)',
|
||||||
borderRadius: '14px', overflow: 'hidden',
|
borderRadius: '14px',
|
||||||
display: 'flex', flexDirection: 'column',
|
|
||||||
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||||||
}}>
|
}} className="border border-border overflow-hidden flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '20px 24px', borderBottom: '1px solid var(--bd)',
|
padding: '20px 24px',
|
||||||
display: 'flex', alignItems: 'center', gap: '14px',
|
}} className="border-b border-border flex items-center gap-[14px]">
|
||||||
}}>
|
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '40px', height: '40px', borderRadius: '10px',
|
width: '40px', height: '40px', borderRadius: '10px',
|
||||||
background: 'linear-gradient(135deg, rgba(168,85,247,0.2), rgba(6,182,212,0.2))',
|
background: 'linear-gradient(135deg, rgba(168,85,247,0.2), rgba(6,182,212,0.2))',
|
||||||
border: '1px solid rgba(168,85,247,0.3)',
|
border: '1px solid rgba(168,85,247,0.3)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
}}>
|
}} className="flex items-center justify-center">
|
||||||
🔍
|
🔍
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<h2 style={{
|
<h2 className="text-base font-bold m-0">
|
||||||
fontSize: '16px', fontWeight: 700, color: 'var(--t1)',
|
|
||||||
fontFamily: 'var(--fK)', margin: 0,
|
|
||||||
}}>
|
|
||||||
유출유 역추적 분석
|
유출유 역추적 분석
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{
|
<div className="text-[11px] text-text-3 mt-[2px]">
|
||||||
fontSize: '11px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px',
|
|
||||||
}}>
|
|
||||||
AIS 항적 기반 유출 선박 추정
|
AIS 항적 기반 유출 선박 추정
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -79,10 +71,10 @@ export function BacktrackModal({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
style={{
|
style={{
|
||||||
width: '32px', height: '32px', borderRadius: '8px',
|
width: '32px', height: '32px', borderRadius: '8px',
|
||||||
border: '1px solid var(--bd)', background: 'var(--bg3)',
|
background: 'var(--bg3)',
|
||||||
color: 'var(--t3)', fontSize: '14px', cursor: 'pointer',
|
fontSize: '14px',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
|
className="border border-border text-text-3 cursor-pointer flex items-center justify-center"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -90,15 +82,11 @@ export function BacktrackModal({
|
|||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1, overflowY: 'auto', padding: '20px 24px',
|
padding: '20px 24px',
|
||||||
display: 'flex', flexDirection: 'column', gap: '16px',
|
}} className="flex-1 overflow-y-auto flex flex-col gap-4">
|
||||||
}}>
|
|
||||||
{/* Analysis Conditions */}
|
{/* Analysis Conditions */}
|
||||||
<div>
|
<div>
|
||||||
<h3 style={{
|
<h3 className="text-[12px] font-bold text-text-2 mb-[10px]">
|
||||||
fontSize: '12px', fontWeight: 700, color: 'var(--t2)',
|
|
||||||
fontFamily: 'var(--fK)', marginBottom: '10px',
|
|
||||||
}}>
|
|
||||||
분석 조건
|
분석 조건
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -112,12 +100,12 @@ export function BacktrackModal({
|
|||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
padding: '10px 12px', background: 'var(--bg3)',
|
padding: '10px 12px', background: 'var(--bg3)',
|
||||||
border: '1px solid var(--bd)', borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
}}>
|
}} className="border border-border">
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '4px' }}>
|
<div className="text-[9px] text-text-3 mb-1">
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '12px', fontWeight: 600, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>
|
<div className="text-[12px] font-semibold font-mono">
|
||||||
{item.value}
|
{item.value}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,11 +115,11 @@ export function BacktrackModal({
|
|||||||
border: '1px solid rgba(168,85,247,0.3)', borderRadius: '8px',
|
border: '1px solid rgba(168,85,247,0.3)', borderRadius: '8px',
|
||||||
gridColumn: '1 / -1',
|
gridColumn: '1 / -1',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '4px' }}>
|
<div className="text-[9px] text-text-3 mb-1">
|
||||||
분석 대상 선박
|
분석 대상 선박
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', fontWeight: 700, color: 'var(--purple)', fontFamily: 'var(--fM)' }}>
|
<div className="text-sm font-bold text-primary-purple font-mono">
|
||||||
{conditions.totalVessels}척 <span style={{ fontSize: '10px', fontWeight: 500, color: 'var(--t3)' }}>(AIS 수신)</span>
|
{conditions.totalVessels}척 <span className="text-[10px] font-medium text-text-3">(AIS 수신)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -140,27 +128,19 @@ export function BacktrackModal({
|
|||||||
{/* Results */}
|
{/* Results */}
|
||||||
{phase === 'results' && vessels.length > 0 && (
|
{phase === 'results' && vessels.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div className="flex items-center justify-between mb-3">
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
<h3 className="text-[12px] font-bold text-text-2 m-0">
|
||||||
marginBottom: '12px',
|
|
||||||
}}>
|
|
||||||
<h3 style={{
|
|
||||||
fontSize: '12px', fontWeight: 700, color: 'var(--t2)',
|
|
||||||
fontFamily: 'var(--fK)', margin: 0,
|
|
||||||
}}>
|
|
||||||
분석 결과
|
분석 결과
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '4px 10px', borderRadius: '12px', fontSize: '10px', fontWeight: 700,
|
padding: '4px 10px', borderRadius: '12px',
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
|
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
|
||||||
color: 'var(--red)',
|
}} className="text-[10px] font-bold text-status-red">
|
||||||
}}>
|
|
||||||
{conditions.totalVessels}척 중 {vessels.length}척 의심
|
{conditions.totalVessels}척 중 {vessels.length}척 의심
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
<div className="flex flex-col gap-2.5">
|
||||||
{vessels.map((v) => (
|
{vessels.map((v) => (
|
||||||
<VesselCard key={v.imo} vessel={v} />
|
<VesselCard key={v.imo} vessel={v} />
|
||||||
))}
|
))}
|
||||||
@ -171,18 +151,18 @@ export function BacktrackModal({
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 24px', borderTop: '1px solid var(--bd)',
|
padding: '16px 24px',
|
||||||
display: 'flex', gap: '8px',
|
}} className="border-t border-border flex gap-2">
|
||||||
}}>
|
|
||||||
{phase === 'conditions' && (
|
{phase === 'conditions' && (
|
||||||
<button
|
<button
|
||||||
onClick={onRunAnalysis}
|
onClick={onRunAnalysis}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700,
|
padding: '12px',
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer',
|
borderRadius: '8px',
|
||||||
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
|
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
|
||||||
border: 'none', color: '#fff',
|
border: 'none', color: '#fff',
|
||||||
}}
|
}}
|
||||||
|
className="flex-1 text-[13px] font-bold cursor-pointer"
|
||||||
>
|
>
|
||||||
🔍 역추적 분석 실행
|
🔍 역추적 분석 실행
|
||||||
</button>
|
</button>
|
||||||
@ -191,11 +171,12 @@ export function BacktrackModal({
|
|||||||
<button
|
<button
|
||||||
disabled
|
disabled
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700,
|
padding: '12px',
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
background: 'var(--bg3)',
|
||||||
color: 'var(--purple)', cursor: 'wait',
|
color: 'var(--purple)', cursor: 'wait',
|
||||||
}}
|
}}
|
||||||
|
className="flex-1 text-[13px] font-bold border border-border"
|
||||||
>
|
>
|
||||||
⏳ AIS 항적 분석중...
|
⏳ AIS 항적 분석중...
|
||||||
</button>
|
</button>
|
||||||
@ -204,11 +185,12 @@ export function BacktrackModal({
|
|||||||
<button
|
<button
|
||||||
onClick={onStartReplay}
|
onClick={onStartReplay}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700,
|
padding: '12px',
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer',
|
borderRadius: '8px',
|
||||||
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
|
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
|
||||||
border: 'none', color: '#fff',
|
border: 'none', color: '#fff',
|
||||||
}}
|
}}
|
||||||
|
className="flex-1 text-[13px] font-bold cursor-pointer"
|
||||||
>
|
>
|
||||||
🗺 지도에서 리플레이 보기
|
🗺 지도에서 리플레이 보기
|
||||||
</button>
|
</button>
|
||||||
@ -226,32 +208,31 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '14px', background: 'var(--bg0)',
|
padding: '14px', background: 'var(--bg0)',
|
||||||
border: '1px solid var(--bd)', borderLeft: `4px solid ${vessel.color}`,
|
borderLeft: `4px solid ${vessel.color}`,
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
}}>
|
}} className="border border-border">
|
||||||
{/* Header row */}
|
{/* Header row */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
|
<div className="flex items-center gap-[10px] mb-[10px]">
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '28px', height: '28px', borderRadius: '50%',
|
width: '28px', height: '28px', borderRadius: '50%',
|
||||||
background: `${vessel.color}20`, border: `2px solid ${vessel.color}`,
|
background: `${vessel.color}20`, border: `2px solid ${vessel.color}`,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
fontSize: '12px', fontWeight: 800, color: vessel.color,
|
||||||
fontSize: '12px', fontWeight: 800, color: vessel.color, fontFamily: 'var(--fM)',
|
}} className="flex items-center justify-center font-mono">
|
||||||
}}>
|
|
||||||
{vessel.rank}
|
{vessel.rank}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div style={{ fontSize: '13px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>
|
<div className="text-[13px] font-bold font-mono">
|
||||||
{vessel.name}
|
{vessel.name}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fM)', marginTop: '2px' }}>
|
<div className="text-[9px] text-text-3 font-mono mt-[2px]">
|
||||||
IMO: {vessel.imo} · {vessel.type} · {vessel.flag}
|
IMO: {vessel.imo} · {vessel.type} · {vessel.flag}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
<div style={{ fontSize: '22px', fontWeight: 800, color: probColor, fontFamily: 'var(--fM)', lineHeight: 1 }}>
|
<div style={{ fontSize: '22px', color: probColor, lineHeight: 1 }} className="font-bold font-mono">
|
||||||
{vessel.probability}%
|
{vessel.probability}%
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>유출 확률</div>
|
<div className="text-[8px] text-text-3">유출 확률</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -267,13 +248,12 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
|
|||||||
padding: '6px', background: 'var(--bg3)', borderRadius: '6px',
|
padding: '6px', background: 'var(--bg3)', borderRadius: '6px',
|
||||||
border: s.highlight ? '1px solid rgba(239,68,68,0.3)' : '1px solid var(--bd)',
|
border: s.highlight ? '1px solid rgba(239,68,68,0.3)' : '1px solid var(--bd)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '2px' }}>
|
<div className="text-[8px] text-text-3 mb-[2px]">
|
||||||
{s.label}
|
{s.label}
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '10px', fontWeight: 600, fontFamily: 'var(--fM)',
|
|
||||||
color: s.highlight ? 'var(--red)' : 'var(--t1)',
|
color: s.highlight ? 'var(--red)' : 'var(--t1)',
|
||||||
}}>
|
}} className="text-[10px] font-semibold font-mono">
|
||||||
{s.value}
|
{s.value}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -285,9 +265,8 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
|
|||||||
<div style={{
|
<div style={{
|
||||||
padding: '8px 10px', background: 'rgba(239,68,68,0.05)',
|
padding: '8px 10px', background: 'rgba(239,68,68,0.05)',
|
||||||
border: '1px solid rgba(239,68,68,0.15)', borderRadius: '6px',
|
border: '1px solid rgba(239,68,68,0.15)', borderRadius: '6px',
|
||||||
fontSize: '9px', color: 'var(--t2)', fontFamily: 'var(--fK)',
|
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
}}>
|
}} className="text-[9px] text-text-2">
|
||||||
{vessel.description}
|
{vessel.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -17,42 +17,40 @@ export function BoomDeploymentTheoryView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full overflow-hidden" style={{ background: 'var(--bg0)' }}>
|
<div className="flex flex-col h-full overflow-hidden bg-bg-0">
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-thin p-5">
|
<div className="flex-1 overflow-y-auto scrollbar-thin p-5">
|
||||||
|
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-[38px] h-[38px] rounded-[9px] flex items-center justify-content text-lg"
|
<div className="w-[38px] h-[38px] rounded-[9px] flex items-center justify-center text-lg"
|
||||||
style={{ background: 'linear-gradient(135deg,rgba(249,115,22,.2),rgba(234,179,8,.15))', border: '1px solid rgba(249,115,22,.3)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
style={{ background: 'linear-gradient(135deg,rgba(249,115,22,.2),rgba(234,179,8,.15))', border: '1px solid rgba(249,115,22,.3)' }}>
|
||||||
🛡️
|
🛡️
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[15px] font-bold" style={{ fontFamily: 'var(--fK)', color: 'var(--t1)' }}>오일펜스 배치 최적화 알고리즘 이론</div>
|
<div className="text-[15px] font-bold">오일펜스 배치 최적화 알고리즘 이론</div>
|
||||||
<div className="text-[10px] mt-0.5" style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>Oil Boom Deployment Optimization · 유출유 확산예측 연동 · 방제효율 최대화</div>
|
<div className="text-[10px] mt-0.5 text-text-3">Oil Boom Deployment Optimization · 유출유 확산예측 연동 · 방제효율 최대화</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleExportPDF}
|
<button onClick={handleExportPDF}
|
||||||
className="px-3.5 py-1.5 rounded-md text-[10px] font-semibold cursor-pointer"
|
className="px-3.5 py-1.5 rounded-md text-[10px] font-semibold cursor-pointer text-primary-blue"
|
||||||
style={{ border: '1px solid rgba(59,130,246,.3)', background: 'rgba(59,130,246,.08)', color: 'var(--blue)', fontFamily: 'var(--fK)' }}>
|
style={{ border: '1px solid rgba(59,130,246,.3)', background: 'rgba(59,130,246,.08)' }}>
|
||||||
📤 PDF 내보내기
|
📤 PDF 내보내기
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 내부 네비게이션 */}
|
{/* 내부 네비게이션 */}
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<div className="flex gap-[3px] rounded-lg p-1" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="flex gap-[3px] rounded-lg p-1 bg-bg-3 border border-border">
|
||||||
{boomTabs.map(tab => (
|
{boomTabs.map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setActivePanel(tab.id)}
|
onClick={() => setActivePanel(tab.id)}
|
||||||
className="flex-1 py-2 px-2 text-[12px] font-semibold rounded-md transition-all"
|
className="flex-1 py-2 px-2 text-[12px] font-semibold rounded-md transition-all"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--fK)',
|
background: activePanel === tab.id ? 'rgba(249,115,22,.15)' : undefined,
|
||||||
background: activePanel === tab.id ? 'rgba(249,115,22,.15)' : 'var(--bg3)',
|
|
||||||
color: activePanel === tab.id ? 'var(--orange)' : 'var(--t3)',
|
color: activePanel === tab.id ? 'var(--orange)' : 'var(--t3)',
|
||||||
border: activePanel === tab.id ? '1px solid rgba(249,115,22,.3)' : '1px solid var(--bd)',
|
border: activePanel === tab.id ? '1px solid rgba(249,115,22,.3)' : '1px solid var(--bd)',
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@ -84,14 +82,14 @@ function OverviewPanel() {
|
|||||||
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--orange),var(--yellow),var(--cyan))' }} />
|
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--orange),var(--yellow),var(--cyan))' }} />
|
||||||
<div className="grid grid-cols-2 gap-5">
|
<div className="grid grid-cols-2 gap-5">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[13px] font-bold mb-2" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🛡️ 오일펜스 배치 최적화란?</div>
|
<div className="text-[13px] font-bold mb-2">🛡️ 오일펜스 배치 최적화란?</div>
|
||||||
<div className="text-[11px] leading-[1.8]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[11px] leading-[1.8] text-text-2">
|
||||||
해양 유류오염 발생 시 <b style={{ color: 'var(--orange)' }}>유출유 확산 예측 결과</b>와 실시간 해양환경(조류·풍향·파고)을 연동하여, 제한된 방제자원(오일펜스 길이·방제정 수)으로 <b style={{ color: 'var(--cyan)' }}>오염 확산 차단 효율을 최대화</b>하는 최적 배치 지점·형태·순서를 자동 산출하는 수치 알고리즘 체계입니다.
|
해양 유류오염 발생 시 <b className="text-status-orange">유출유 확산 예측 결과</b>와 실시간 해양환경(조류·풍향·파고)을 연동하여, 제한된 방제자원(오일펜스 길이·방제정 수)으로 <b className="text-primary-cyan">오염 확산 차단 효율을 최대화</b>하는 최적 배치 지점·형태·순서를 자동 산출하는 수치 알고리즘 체계입니다.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[13px] font-bold mb-2" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🎯 WING 최적화 목표</div>
|
<div className="text-[13px] font-bold mb-2">🎯 WING 최적화 목표</div>
|
||||||
<div className="flex flex-col gap-[5px] text-[11px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="flex flex-col gap-[5px] text-[11px] text-text-2">
|
||||||
{[
|
{[
|
||||||
{ num: '①', color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', text: '차단 면적 최대화 — 예측 유출유 확산 경계와 오일펜스 교차 면적 극대화' },
|
{ num: '①', color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', text: '차단 면적 최대화 — 예측 유출유 확산 경계와 오일펜스 교차 면적 극대화' },
|
||||||
{ num: '②', color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', text: '도달시간 최소화 — 유출유 해안·ESI 민감구역 도달 전 선제적 차단선 구축' },
|
{ num: '②', color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', text: '도달시간 최소화 — 유출유 해안·ESI 민감구역 도달 전 선제적 차단선 구축' },
|
||||||
@ -99,7 +97,7 @@ function OverviewPanel() {
|
|||||||
{ num: '④', color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', text: '실패 안전성 확보 — 조류 초과 시 오일펜스 이탈 방지 방향각 자동 보정' },
|
{ num: '④', color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', text: '실패 안전성 확보 — 조류 초과 시 오일펜스 이탈 방지 방향각 자동 보정' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="px-2.5 py-1.5 rounded-md" style={{ background: item.bg, border: `1px solid ${item.bd}` }}>
|
<div key={i} className="px-2.5 py-1.5 rounded-md" style={{ background: item.bg, border: `1px solid ${item.bd}` }}>
|
||||||
<span style={{ color: item.color, fontWeight: 700 }}>{item.num}</span> <b>{item.text}</b>
|
<span className="font-bold" style={{ color: item.color }}>{item.num}</span> <b>{item.text}</b>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -108,8 +106,8 @@ function OverviewPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 전체 흐름도 */}
|
{/* 전체 흐름도 */}
|
||||||
<div className="rounded-[10px] p-4 mb-4" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-4 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3.5" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>⚙️ WING 오일펜스 배치 최적화 전체 흐름</div>
|
<div className="text-xs font-bold mb-3.5">⚙️ WING 오일펜스 배치 최적화 전체 흐름</div>
|
||||||
<div className="flex items-center justify-center gap-0 flex-nowrap overflow-x-auto py-2">
|
<div className="flex items-center justify-center gap-0 flex-nowrap overflow-x-auto py-2">
|
||||||
{[
|
{[
|
||||||
{ icon: '🌊', label: '확산예측', sub: 'KOSPS/POSEIDON\nOpenDrift', color: 'var(--orange)', bg: 'rgba(249,115,22,.08)', bd: 'rgba(249,115,22,.2)' },
|
{ icon: '🌊', label: '확산예측', sub: 'KOSPS/POSEIDON\nOpenDrift', color: 'var(--orange)', bg: 'rgba(249,115,22,.08)', bd: 'rgba(249,115,22,.2)' },
|
||||||
@ -120,16 +118,15 @@ function OverviewPanel() {
|
|||||||
{ icon: '🗺️', label: '지도 표시', sub: 'ESI 중첩\n방제자원 연계', color: 'var(--blue)', bg: 'rgba(59,130,246,.08)', bd: 'rgba(59,130,246,.2)' },
|
{ icon: '🗺️', label: '지도 표시', sub: 'ESI 중첩\n방제자원 연계', color: 'var(--blue)', bg: 'rgba(59,130,246,.08)', bd: 'rgba(59,130,246,.2)' },
|
||||||
].map((step, i) => (
|
].map((step, i) => (
|
||||||
<div key={i} className="flex items-center">
|
<div key={i} className="flex items-center">
|
||||||
<div className="text-center min-w-[80px] rounded-lg px-3 py-2.5" style={{
|
<div className="text-center min-w-[80px] rounded-lg px-3 py-2.5 text-[9px]" style={{
|
||||||
background: step.bg,
|
background: step.bg,
|
||||||
border: `${step.bold ? '2px' : '1px'} solid ${step.bd}`,
|
border: `${step.bold ? '2px' : '1px'} solid ${step.bd}`,
|
||||||
fontFamily: 'var(--fK)', fontSize: '9px'
|
|
||||||
}}>
|
}}>
|
||||||
<div className="text-[15px] mb-1">{step.icon}</div>
|
<div className="text-[15px] mb-1">{step.icon}</div>
|
||||||
<div style={{ fontWeight: 700, color: step.color }}>{step.label}</div>
|
<div className="font-bold" style={{ color: step.color }}>{step.label}</div>
|
||||||
<div style={{ color: 'var(--t3)', whiteSpace: 'pre-line' }}>{step.sub}</div>
|
<div className="text-text-3" style={{ whiteSpace: 'pre-line' }}>{step.sub}</div>
|
||||||
</div>
|
</div>
|
||||||
{i < 5 && <div className="px-1.5" style={{ color: 'var(--t3)', fontSize: '14px' }}>▶</div>}
|
{i < 5 && <div className="px-1.5 text-text-3 text-[14px]">▶</div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -142,12 +139,12 @@ function OverviewPanel() {
|
|||||||
{ icon: '🌊', title: '공기충전식 오일펜스', color: 'var(--blue)', desc: '공기로 부력 확보. 이동·보관 편리. 해상 광역 차단에 주로 사용.', specs: ['내조류 한계: 0.7~1.5 knot', '높이: 45~90cm · 수중 45~90cm', '전개속도: 100~300m/hr'] },
|
{ icon: '🌊', title: '공기충전식 오일펜스', color: 'var(--blue)', desc: '공기로 부력 확보. 이동·보관 편리. 해상 광역 차단에 주로 사용.', specs: ['내조류 한계: 0.7~1.5 knot', '높이: 45~90cm · 수중 45~90cm', '전개속도: 100~300m/hr'] },
|
||||||
{ icon: '🔄', title: '자항식 오일펜스', color: 'var(--green)', desc: '방제정 예인 또는 자체 추진. U형·V형 동적 배치. 강조류 해역 적합.', specs: ['운용수심: 5m 이상', 'U형·V형·J형 동적 형태', '내조류: 조류각도 보정으로 극복'] },
|
{ icon: '🔄', title: '자항식 오일펜스', color: 'var(--green)', desc: '방제정 예인 또는 자체 추진. U형·V형 동적 배치. 강조류 해역 적합.', specs: ['운용수심: 5m 이상', 'U형·V형·J형 동적 형태', '내조류: 조류각도 보정으로 극복'] },
|
||||||
].map((boom, i) => (
|
].map((boom, i) => (
|
||||||
<div key={i} className="rounded-[10px] p-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderTop: `3px solid ${boom.color}` }}>
|
<div key={i} className="rounded-md p-3.5 bg-bg-3 border border-border" style={{ borderTop: `3px solid ${boom.color}` }}>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: boom.color, fontFamily: 'var(--fK)' }}>{boom.icon} {boom.title}</div>
|
<div className="text-[11px] font-bold mb-2" style={{ color: boom.color }}>{boom.icon} {boom.title}</div>
|
||||||
<div className="text-[10px] mb-2 leading-[1.7]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>{boom.desc}</div>
|
<div className="text-[10px] mb-2 leading-[1.7] text-text-2">{boom.desc}</div>
|
||||||
<div className="flex flex-col gap-[3px] text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="flex flex-col gap-[3px] text-[9px] text-text-2">
|
||||||
{boom.specs.map((spec, j) => (
|
{boom.specs.map((spec, j) => (
|
||||||
<div key={j} className="px-[7px] py-[3px] rounded-[3px]" style={{ background: `${boom.color}11`, color: 'var(--t2)' }}>{spec}</div>
|
<div key={j} className="px-[7px] py-[3px] rounded-[3px] text-text-2" style={{ background: `${boom.color}11` }}>{spec}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -155,9 +152,9 @@ function OverviewPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 핵심 제약조건 */}
|
{/* 핵심 제약조건 */}
|
||||||
<div className="rounded-[10px] p-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-[11px] font-bold mb-2.5" style={{ color: 'var(--red)', fontFamily: 'var(--fK)' }}>⚠️ 최적화 핵심 제약조건</div>
|
<div className="text-[11px] font-bold mb-2.5 text-status-red">⚠️ 최적화 핵심 제약조건</div>
|
||||||
<div className="grid grid-cols-3 gap-2.5 text-[10px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="grid grid-cols-3 gap-2.5 text-[10px] text-text-2">
|
||||||
{[
|
{[
|
||||||
{ icon: '🌊', title: '조류 제약', color: 'var(--red)', bg: 'rgba(239,68,68,.05)', bd: 'rgba(239,68,68,.15)', lines: ['조류속도 > 임계유속 시', '오일펜스 하단 통과 발생', 'U<0.7 knot 유지 필수', '임계각도 자동 계산 적용'] },
|
{ icon: '🌊', title: '조류 제약', color: 'var(--red)', bg: 'rgba(239,68,68,.05)', bd: 'rgba(239,68,68,.15)', lines: ['조류속도 > 임계유속 시', '오일펜스 하단 통과 발생', 'U<0.7 knot 유지 필수', '임계각도 자동 계산 적용'] },
|
||||||
{ icon: '📏', title: '자원 제약', color: 'var(--yellow)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.15)', lines: ['가용 오일펜스 총 길이', '방제정 척수·이동시간', '앵커링 가능 수심 조건', '연결부 허용 장력'] },
|
{ icon: '📏', title: '자원 제약', color: 'var(--yellow)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.15)', lines: ['가용 오일펜스 총 길이', '방제정 척수·이동시간', '앵커링 가능 수심 조건', '연결부 허용 장력'] },
|
||||||
@ -165,7 +162,7 @@ function OverviewPanel() {
|
|||||||
].map((c, i) => (
|
].map((c, i) => (
|
||||||
<div key={i} className="p-2.5 rounded-[7px]" style={{ background: c.bg, border: `1px solid ${c.bd}` }}>
|
<div key={i} className="p-2.5 rounded-[7px]" style={{ background: c.bg, border: `1px solid ${c.bd}` }}>
|
||||||
<div className="font-bold mb-[5px]" style={{ color: c.color }}>{c.icon} {c.title}</div>
|
<div className="font-bold mb-[5px]" style={{ color: c.color }}>{c.icon} {c.title}</div>
|
||||||
<div className="leading-[1.7]" style={{ color: 'var(--t2)' }}>
|
<div className="leading-[1.7] text-text-2">
|
||||||
{c.lines.map((l, j) => <span key={j}>{j > 0 && <br />}{l}</span>)}
|
{c.lines.map((l, j) => <span key={j}>{j > 0 && <br />}{l}</span>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -181,46 +178,46 @@ function DeploymentTheoryPanel() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 차단 효율 이론 */}
|
{/* 차단 효율 이론 */}
|
||||||
<div className="rounded-[10px] p-4 mb-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>📐 오일펜스 차단 효율 이론 (Boom Containment Efficiency)</div>
|
<div className="text-xs font-bold mb-3">📐 오일펜스 차단 효율 이론 (Boom Containment Efficiency)</div>
|
||||||
<div className="grid grid-cols-2 gap-3.5">
|
<div className="grid grid-cols-2 gap-3.5">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--orange)', fontFamily: 'var(--fK)' }}>① 차단 효율 함수 E(θ, U)</div>
|
<div className="text-[11px] font-bold mb-2 text-status-orange">① 차단 효율 함수 E(θ, U)</div>
|
||||||
<div className="text-[10px] leading-[1.8] mb-2" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">
|
||||||
오일펜스의 차단 효율은 <b style={{ color: 'var(--t1)' }}>조류 유속(U)</b>과 <b style={{ color: 'var(--t1)' }}>오일펜스 방향각(θ)</b>의 함수입니다. 조류가 오일펜스에 수직으로 입사할수록 차단 효율이 낮아지며, 임계유속 초과 시 기름이 오일펜스 하부로 통과합니다.
|
오일펜스의 차단 효율은 <b>조류 유속(U)</b>과 <b>오일펜스 방향각(θ)</b>의 함수입니다. 조류가 오일펜스에 수직으로 입사할수록 차단 효율이 낮아지며, 임계유속 초과 시 기름이 오일펜스 하부로 통과합니다.
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md p-2.5 text-[10px] leading-[2.1]" style={{ background: 'var(--bg0)', border: '1px solid rgba(249,115,22,.2)', fontFamily: 'var(--fM)', color: 'var(--t1)' }}>
|
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
|
||||||
E(θ,U) = 1 − <span style={{ color: 'var(--red)' }}>F<sub>loss</sub>(U<sub>n</sub>)</span><br />
|
E(θ,U) = 1 − <span className="text-status-red">F<sub>loss</sub>(U<sub>n</sub>)</span><br />
|
||||||
U<sub>n</sub> = U · sin(θ) <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(법선방향 유속)</span><br />
|
U<sub>n</sub> = U · sin(θ) <span className="text-[9px] text-text-3">(법선방향 유속)</span><br />
|
||||||
E = 1 (U<sub>n</sub> ≤ U<sub>c</sub>)<br />
|
E = 1 (U<sub>n</sub> ≤ U<sub>c</sub>)<br />
|
||||||
E = max(0, 1 − (U<sub>n</sub>/U<sub>c</sub>)²) (U<sub>n</sub> > U<sub>c</sub>)<br />
|
E = max(0, 1 − (U<sub>n</sub>/U<sub>c</sub>)²) (U<sub>n</sub> > U<sub>c</sub>)<br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>U<sub>c</sub>: 임계유속(약 0.35m/s = 0.7 knot)</span>
|
<span className="text-[9px] text-text-3">U<sub>c</sub>: 임계유속(약 0.35m/s = 0.7 knot)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--cyan)', fontFamily: 'var(--fK)' }}>② 최적 방향각 θ* 산정</div>
|
<div className="text-[11px] font-bold mb-2 text-primary-cyan">② 최적 방향각 θ* 산정</div>
|
||||||
<div className="text-[10px] leading-[1.8] mb-2" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">
|
||||||
오일펜스 방향각은 조류 방향에 따라 최적화됩니다. 차단 면적과 오일 수집 효율의 <b style={{ color: 'var(--t1)' }}>트레이드오프</b>를 고려하여, 일반적으로 조류에 대해 <b style={{ color: 'var(--cyan)' }}>30°~45° 예각 배치</b>가 최적입니다.
|
오일펜스 방향각은 조류 방향에 따라 최적화됩니다. 차단 면적과 오일 수집 효율의 <b>트레이드오프</b>를 고려하여, 일반적으로 조류에 대해 <b className="text-primary-cyan">30°~45° 예각 배치</b>가 최적입니다.
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md p-2.5 text-[10px] leading-[2.1]" style={{ background: 'var(--bg0)', border: '1px solid rgba(6,182,212,.2)', fontFamily: 'var(--fM)', color: 'var(--t1)' }}>
|
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(6,182,212,.2)' }}>
|
||||||
θ* = arcsin(U<sub>c</sub> / U) <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(임계조건)</span><br />
|
θ* = arcsin(U<sub>c</sub> / U) <span className="text-[9px] text-text-3">(임계조건)</span><br />
|
||||||
θ<sub>opt</sub> = argmax [A<sub>block</sub>(θ) · E(θ,U)]<br />
|
θ<sub>opt</sub> = argmax [A<sub>block</sub>(θ) · E(θ,U)]<br />
|
||||||
실용범위: 15° ≤ θ ≤ 60°<br />
|
실용범위: 15° ≤ θ ≤ 60°<br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>단, θ < arcsin(U<sub>c</sub>/U) 이면 기름 통과 발생</span>
|
<span className="text-[9px] text-text-3">단, θ < arcsin(U<sub>c</sub>/U) 이면 기름 통과 발생</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* V형·U형·J형 배치 패턴 */}
|
{/* V형·U형·J형 배치 패턴 */}
|
||||||
<div className="rounded-[10px] p-4 mb-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🔷 오일펜스 배치 형태별 이론</div>
|
<div className="text-xs font-bold mb-3">🔷 오일펜스 배치 형태별 이론</div>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{/* V형 */}
|
{/* V형 */}
|
||||||
<div className="rounded-lg p-3.5" style={{ background: 'var(--bg0)', border: '1px solid rgba(6,182,212,.2)', borderTop: '3px solid var(--cyan)' }}>
|
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(6,182,212,.2)', borderTop: '3px solid var(--cyan)' }}>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--cyan)', fontFamily: 'var(--fK)' }}>V형 (Chevron)</div>
|
<div className="text-[11px] font-bold mb-2 text-primary-cyan">V형 (Chevron)</div>
|
||||||
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(6,182,212,.04)' }}>
|
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(6,182,212,.04)' }}>
|
||||||
<svg width="180" height="120" viewBox="0 0 100 65" style={{ overflow: 'visible' }}>
|
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
|
||||||
<defs><marker id="arr1" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
<defs><marker id="arr1" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
||||||
<line x1="10" y1="15" x2="50" y2="55" stroke="rgba(249,115,22,.8)" strokeWidth="2.5" />
|
<line x1="10" y1="15" x2="50" y2="55" stroke="rgba(249,115,22,.8)" strokeWidth="2.5" />
|
||||||
<line x1="90" y1="15" x2="50" y2="55" stroke="rgba(249,115,22,.8)" strokeWidth="2.5" />
|
<line x1="90" y1="15" x2="50" y2="55" stroke="rgba(249,115,22,.8)" strokeWidth="2.5" />
|
||||||
@ -230,19 +227,19 @@ function DeploymentTheoryPanel() {
|
|||||||
<text x="58" y="64" fill="rgba(6,182,212,.7)" fontSize="6">조류</text>
|
<text x="58" y="64" fill="rgba(6,182,212,.7)" fontSize="6">조류</text>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] leading-[1.7] mb-[7px]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>조류 방향 정면에서 양측으로 펼친 V형. 기름을 중앙 집유점으로 유도. 회수선 배치 용이.</div>
|
<div className="text-[10px] leading-[1.7] mb-[7px] text-text-2">조류 방향 정면에서 양측으로 펼친 V형. 기름을 중앙 집유점으로 유도. 회수선 배치 용이.</div>
|
||||||
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9]" style={{ fontFamily: 'var(--fM)', color: 'var(--t1)', background: 'rgba(6,182,212,.05)' }}>
|
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9] font-mono" style={{ background: 'rgba(6,182,212,.05)' }}>
|
||||||
A<sub>V</sub> = L²·sin(2α)/2<br />
|
A<sub>V</sub> = L²·sin(2α)/2<br />
|
||||||
<span style={{ color: 'var(--t3)' }}>α: 반개각, L: 편측 길이</span><br />
|
<span className="text-text-3">α: 반개각, L: 편측 길이</span><br />
|
||||||
최적 α = 30°~45°
|
최적 α = 30°~45°
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* U형 */}
|
{/* U형 */}
|
||||||
<div className="rounded-lg p-3.5" style={{ background: 'var(--bg0)', border: '1px solid rgba(34,197,94,.2)', borderTop: '3px solid var(--green)' }}>
|
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(34,197,94,.2)', borderTop: '3px solid var(--green)' }}>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--green)', fontFamily: 'var(--fK)' }}>U형 (Horseshoe)</div>
|
<div className="text-[11px] font-bold mb-2 text-status-green">U형 (Horseshoe)</div>
|
||||||
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(34,197,94,.04)' }}>
|
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(34,197,94,.04)' }}>
|
||||||
<svg width="180" height="120" viewBox="0 0 100 65" style={{ overflow: 'visible' }}>
|
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
|
||||||
<defs><marker id="arr2" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
<defs><marker id="arr2" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
||||||
<path d="M15,10 L15,45 Q50,65 85,45 L85,10" fill="none" stroke="rgba(34,197,94,.8)" strokeWidth="2.5" />
|
<path d="M15,10 L15,45 Q50,65 85,45 L85,10" fill="none" stroke="rgba(34,197,94,.8)" strokeWidth="2.5" />
|
||||||
<circle cx="50" cy="52" r="4" fill="rgba(34,197,94,.5)" />
|
<circle cx="50" cy="52" r="4" fill="rgba(34,197,94,.5)" />
|
||||||
@ -251,19 +248,19 @@ function DeploymentTheoryPanel() {
|
|||||||
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6">조류</text>
|
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6">조류</text>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] leading-[1.7] mb-[7px]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>말굽형으로 기름을 완전 포위. 폐쇄형 구조로 회수 효율 최고. 저조류 해역 적합.</div>
|
<div className="text-[10px] leading-[1.7] mb-[7px] text-text-2">말굽형으로 기름을 완전 포위. 폐쇄형 구조로 회수 효율 최고. 저조류 해역 적합.</div>
|
||||||
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9]" style={{ fontFamily: 'var(--fM)', color: 'var(--t1)', background: 'rgba(34,197,94,.05)' }}>
|
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9] font-mono" style={{ background: 'rgba(34,197,94,.05)' }}>
|
||||||
A<sub>U</sub> = π·r²/2 + 2r·h<br />
|
A<sub>U</sub> = π·r²/2 + 2r·h<br />
|
||||||
<span style={{ color: 'var(--t3)' }}>r: 반경, h: 직선부 길이</span><br />
|
<span className="text-text-3">r: 반경, h: 직선부 길이</span><br />
|
||||||
전제: U < 0.5 knot
|
전제: U < 0.5 knot
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* J형 */}
|
{/* J형 */}
|
||||||
<div className="rounded-lg p-3.5" style={{ background: 'var(--bg0)', border: '1px solid rgba(168,85,247,.2)', borderTop: '3px solid var(--purple)' }}>
|
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(168,85,247,.2)', borderTop: '3px solid var(--purple)' }}>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--purple)', fontFamily: 'var(--fK)' }}>J형 (Skimming)</div>
|
<div className="text-[11px] font-bold mb-2 text-primary-purple">J형 (Skimming)</div>
|
||||||
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(168,85,247,.04)' }}>
|
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(168,85,247,.04)' }}>
|
||||||
<svg width="180" height="120" viewBox="0 0 100 65" style={{ overflow: 'visible' }}>
|
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
|
||||||
<defs><marker id="arr3" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
<defs><marker id="arr3" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
||||||
<line x1="80" y1="8" x2="80" y2="48" stroke="rgba(168,85,247,.8)" strokeWidth="2.5" />
|
<line x1="80" y1="8" x2="80" y2="48" stroke="rgba(168,85,247,.8)" strokeWidth="2.5" />
|
||||||
<path d="M80,48 Q55,60 30,35" fill="none" stroke="rgba(168,85,247,.8)" strokeWidth="2.5" />
|
<path d="M80,48 Q55,60 30,35" fill="none" stroke="rgba(168,85,247,.8)" strokeWidth="2.5" />
|
||||||
@ -273,10 +270,10 @@ function DeploymentTheoryPanel() {
|
|||||||
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6">조류</text>
|
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6">조류</text>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] leading-[1.7] mb-[7px]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>직선+곡선 조합. 기름을 한쪽으로 편향 유도하여 집유. 강조류·연안 배치에 최적.</div>
|
<div className="text-[10px] leading-[1.7] mb-[7px] text-text-2">직선+곡선 조합. 기름을 한쪽으로 편향 유도하여 집유. 강조류·연안 배치에 최적.</div>
|
||||||
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9]" style={{ fontFamily: 'var(--fM)', color: 'var(--t1)', background: 'rgba(168,85,247,.05)' }}>
|
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9] font-mono" style={{ background: 'rgba(168,85,247,.05)' }}>
|
||||||
θ<sub>J</sub> = arcsin(U<sub>c</sub>/U) + δ<br />
|
θ<sub>J</sub> = arcsin(U<sub>c</sub>/U) + δ<br />
|
||||||
<span style={{ color: 'var(--t3)' }}>δ: 안전여유각(5°~10°)</span><br />
|
<span className="text-text-3">δ: 안전여유각(5°~10°)</span><br />
|
||||||
활용: U > 0.7 knot
|
활용: U > 0.7 knot
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -284,24 +281,24 @@ function DeploymentTheoryPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 다단 배치 이론 */}
|
{/* 다단 배치 이론 */}
|
||||||
<div className="rounded-[10px] p-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-[11px] font-bold mb-2.5" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🔢 다단계 차단선(Multi-Boom) 배치 이론</div>
|
<div className="text-[11px] font-bold mb-2.5">🔢 다단계 차단선(Multi-Boom) 배치 이론</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="text-[10px] leading-[1.8]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] leading-[1.8] text-text-2">
|
||||||
단일 오일펜스로 차단 불가한 경우 <b style={{ color: 'var(--t1)' }}>직렬 다단 배치</b>로 누적 차단 효율을 향상합니다. n개 직렬 배치 시 누적 차단 효율:
|
단일 오일펜스로 차단 불가한 경우 <b>직렬 다단 배치</b>로 누적 차단 효율을 향상합니다. n개 직렬 배치 시 누적 차단 효율:
|
||||||
<div className="mt-2 rounded-[5px] p-[9px] leading-[2]" style={{ background: 'var(--bg0)', fontFamily: 'var(--fM)', color: 'var(--t1)' }}>
|
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-0 font-mono">
|
||||||
E<sub>total</sub> = 1 − ∏(1−E<sub>i</sub>)<br />
|
E<sub>total</sub> = 1 − ∏(1−E<sub>i</sub>)<br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>E<sub>i</sub>: i번째 오일펜스 단독 차단효율</span>
|
<span className="text-[9px] text-text-3">E<sub>i</sub>: i번째 오일펜스 단독 차단효율</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[5px] text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="flex flex-col gap-[5px] text-[9px] text-text-2">
|
||||||
{[
|
{[
|
||||||
{ color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', label: '2단 직렬', text: ': E_total = E₁+E₂−E₁·E₂ (예: 70%+70% → 91%)' },
|
{ color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', label: '2단 직렬', text: ': E_total = E₁+E₂−E₁·E₂ (예: 70%+70% → 91%)' },
|
||||||
{ color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', label: '단간 거리', text: ': 부표 집적 방지를 위해 ≥ 200m 이격 권장' },
|
{ color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', label: '단간 거리', text: ': 부표 집적 방지를 위해 ≥ 200m 이격 권장' },
|
||||||
{ color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', label: '배치 우선순위', text: ': ESI 고등급 구역 보호 → 취수원 → 어항 순' },
|
{ color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', label: '배치 우선순위', text: ': ESI 고등급 구역 보호 → 취수원 → 어항 순' },
|
||||||
{ color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', label: '조석 변화', text: ': 창조·낙조 전환 시 오일펜스 방향 재조정 필요' },
|
{ color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', label: '조석 변화', text: ': 창조·낙조 전환 시 오일펜스 방향 재조정 필요' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="px-[9px] py-1.5 rounded-[5px]" style={{ background: item.bg, border: `1px solid ${item.bd}`, color: 'var(--t2)' }}>
|
<div key={i} className="px-[9px] py-1.5 rounded-[5px] text-text-2" style={{ background: item.bg, border: `1px solid ${item.bd}` }}>
|
||||||
<b style={{ color: item.color }}>{item.label}</b>{item.text}
|
<b style={{ color: item.color }}>{item.label}</b>{item.text}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -320,43 +317,43 @@ function OptimizationPanel() {
|
|||||||
<div className="rounded-xl p-4 mb-3.5 relative overflow-hidden"
|
<div className="rounded-xl p-4 mb-3.5 relative overflow-hidden"
|
||||||
style={{ background: 'linear-gradient(135deg,rgba(168,85,247,.06),rgba(59,130,246,.04))', border: '1px solid rgba(168,85,247,.2)' }}>
|
style={{ background: 'linear-gradient(135deg,rgba(168,85,247,.06),rgba(59,130,246,.04))', border: '1px solid rgba(168,85,247,.2)' }}>
|
||||||
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--purple),var(--blue))' }} />
|
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--purple),var(--blue))' }} />
|
||||||
<div className="text-[13px] font-bold mb-2" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>⚙️ 다목적 최적화 문제 (Multi-Objective Optimization)</div>
|
<div className="text-[13px] font-bold mb-2">⚙️ 다목적 최적화 문제 (Multi-Objective Optimization)</div>
|
||||||
<div className="text-[11px] leading-[1.8]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[11px] leading-[1.8] text-text-2">
|
||||||
오일펜스 배치 최적화는 <b style={{ color: 'var(--purple)' }}>상충하는 복수 목적함수</b>를 동시에 만족해야 하는 전형적인 다목적 최적화 문제입니다. 차단 효율 최대화와 자원 사용 최소화는 서로 트레이드오프 관계를 가지며, <b style={{ color: 'var(--cyan)' }}>파레토 최적(Pareto Optimal) 해집합</b>에서 의사결정자가 선택합니다.
|
오일펜스 배치 최적화는 <b className="text-primary-purple">상충하는 복수 목적함수</b>를 동시에 만족해야 하는 전형적인 다목적 최적화 문제입니다. 차단 효율 최대화와 자원 사용 최소화는 서로 트레이드오프 관계를 가지며, <b className="text-primary-cyan">파레토 최적(Pareto Optimal) 해집합</b>에서 의사결정자가 선택합니다.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 목적함수 정의 */}
|
{/* 목적함수 정의 */}
|
||||||
<div className="rounded-[10px] p-4 mb-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>📊 목적함수 및 제약조건 정의</div>
|
<div className="text-xs font-bold mb-3">📊 목적함수 및 제약조건 정의</div>
|
||||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||||
<div className="rounded-lg p-3" style={{ background: 'var(--bg0)', border: '1px solid rgba(34,197,94,.2)' }}>
|
<div className="rounded-lg p-3 bg-bg-0" style={{ border: '1px solid rgba(34,197,94,.2)' }}>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--green)', fontFamily: 'var(--fK)' }}>🎯 목적함수 F(x)</div>
|
<div className="text-[11px] font-bold mb-2 text-status-green">🎯 목적함수 F(x)</div>
|
||||||
<div className="rounded-[5px] p-[9px] text-[10px] leading-[2.2]" style={{ fontFamily: 'var(--fM)', color: 'var(--t1)', background: 'rgba(34,197,94,.04)' }}>
|
<div className="rounded-[5px] p-[9px] text-[10px] leading-[2.2] font-mono" style={{ background: 'rgba(34,197,94,.04)' }}>
|
||||||
<b style={{ color: 'var(--green)' }}>최대화:</b><br />
|
<b className="text-status-green">최대화:</b><br />
|
||||||
f₁(x) = Σ A<sub>blocked,i</sub> · w<sub>ESI,i</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(가중 차단면적)</span><br />
|
f₁(x) = Σ A<sub>blocked,i</sub> · w<sub>ESI,i</sub> <span className="text-[9px] text-text-3">(가중 차단면적)</span><br />
|
||||||
f₂(x) = T<sub>deadline</sub> − T<sub>deploy</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(여유시간)</span><br />
|
f₂(x) = T<sub>deadline</sub> − T<sub>deploy</sub> <span className="text-[9px] text-text-3">(여유시간)</span><br />
|
||||||
<b style={{ color: 'var(--red)' }}>최소화:</b><br />
|
<b className="text-status-red">최소화:</b><br />
|
||||||
f₃(x) = Σ L<sub>boom,j</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(총 오일펜스 사용량)</span><br />
|
f₃(x) = Σ L<sub>boom,j</sub> <span className="text-[9px] text-text-3">(총 오일펜스 사용량)</span><br />
|
||||||
f₄(x) = Σ D<sub>vessel,k</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(방제정 총 이동거리)</span>
|
f₄(x) = Σ D<sub>vessel,k</sub> <span className="text-[9px] text-text-3">(방제정 총 이동거리)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg p-3" style={{ background: 'var(--bg0)', border: '1px solid rgba(239,68,68,.2)' }}>
|
<div className="rounded-lg p-3 bg-bg-0" style={{ border: '1px solid rgba(239,68,68,.2)' }}>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--red)', fontFamily: 'var(--fK)' }}>🚫 제약조건 G(x)</div>
|
<div className="text-[11px] font-bold mb-2 text-status-red">🚫 제약조건 G(x)</div>
|
||||||
<div className="rounded-[5px] p-[9px] text-[10px] leading-[2.2]" style={{ fontFamily: 'var(--fM)', color: 'var(--t1)', background: 'rgba(239,68,68,.04)' }}>
|
<div className="rounded-[5px] p-[9px] text-[10px] leading-[2.2] font-mono" style={{ background: 'rgba(239,68,68,.04)' }}>
|
||||||
g₁: U·sin(θ<sub>i</sub>) ≤ U<sub>c</sub> ∀i <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(임계유속)</span><br />
|
g₁: U·sin(θ<sub>i</sub>) ≤ U<sub>c</sub> ∀i <span className="text-[9px] text-text-3">(임계유속)</span><br />
|
||||||
g₂: Σ L<sub>j</sub> ≤ L<sub>max</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(자원 한계)</span><br />
|
g₂: Σ L<sub>j</sub> ≤ L<sub>max</sub> <span className="text-[9px] text-text-3">(자원 한계)</span><br />
|
||||||
g₃: T<sub>deploy,i</sub> ≤ T<sub>arrive,i</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(시간 제약)</span><br />
|
g₃: T<sub>deploy,i</sub> ≤ T<sub>arrive,i</sub> <span className="text-[9px] text-text-3">(시간 제약)</span><br />
|
||||||
g₄: d(p<sub>i</sub>, shore) ≥ d<sub>min</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(연안 이격)</span><br />
|
g₄: d(p<sub>i</sub>, shore) ≥ d<sub>min</sub> <span className="text-[9px] text-text-3">(연안 이격)</span><br />
|
||||||
g₅: h(p<sub>i</sub>) ≥ h<sub>min</sub> <span className="text-[9px]" style={{ color: 'var(--t3)' }}>(수심 조건)</span>
|
g₅: h(p<sub>i</sub>) ≥ h<sub>min</sub> <span className="text-[9px] text-text-3">(수심 조건)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ESI 가중치 */}
|
{/* ESI 가중치 */}
|
||||||
<div className="rounded-lg p-3" style={{ background: 'var(--bg0)', border: '1px solid rgba(234,179,8,.2)' }}>
|
<div className="rounded-lg p-3 bg-bg-0" style={{ border: '1px solid rgba(234,179,8,.2)' }}>
|
||||||
<div className="text-[10px] font-bold mb-2" style={{ color: 'var(--yellow)', fontFamily: 'var(--fK)' }}>🏖️ ESI 가중치 w<sub>ESI</sub> 설계</div>
|
<div className="text-[10px] font-bold mb-2 text-status-yellow">🏖️ ESI 가중치 w<sub>ESI</sub> 설계</div>
|
||||||
<div className="grid grid-cols-5 gap-[5px] text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="grid grid-cols-5 gap-[5px] text-[9px] text-text-2">
|
||||||
{[
|
{[
|
||||||
{ grade: 'ESI 1~2', desc: '노출암반', w: 'w = 0.2', color: 'var(--green)', bg: 'rgba(34,197,94,.06)' },
|
{ grade: 'ESI 1~2', desc: '노출암반', w: 'w = 0.2', color: 'var(--green)', bg: 'rgba(34,197,94,.06)' },
|
||||||
{ grade: 'ESI 3~4', desc: '모래해변', w: 'w = 0.4', color: 'var(--cyan)', bg: 'rgba(6,182,212,.06)' },
|
{ grade: 'ESI 3~4', desc: '모래해변', w: 'w = 0.4', color: 'var(--cyan)', bg: 'rgba(6,182,212,.06)' },
|
||||||
@ -365,9 +362,9 @@ function OptimizationPanel() {
|
|||||||
{ grade: 'ESI 9~10', desc: '맹그로브·습지', w: 'w = 1.0', color: 'var(--red)', bg: 'rgba(239,68,68,.08)', bd: 'rgba(239,68,68,.2)' },
|
{ grade: 'ESI 9~10', desc: '맹그로브·습지', w: 'w = 1.0', color: 'var(--red)', bg: 'rgba(239,68,68,.08)', bd: 'rgba(239,68,68,.2)' },
|
||||||
].map((esi, i) => (
|
].map((esi, i) => (
|
||||||
<div key={i} className="p-1.5 rounded text-center" style={{ background: esi.bg, border: esi.bd ? `1px solid ${esi.bd}` : undefined }}>
|
<div key={i} className="p-1.5 rounded text-center" style={{ background: esi.bg, border: esi.bd ? `1px solid ${esi.bd}` : undefined }}>
|
||||||
<div style={{ fontWeight: 700, color: esi.color }}>{esi.grade}</div>
|
<div className="font-bold" style={{ color: esi.color }}>{esi.grade}</div>
|
||||||
<div style={{ color: 'var(--t3)' }}>{esi.desc}</div>
|
<div className="text-text-3">{esi.desc}</div>
|
||||||
<div style={{ color: 'var(--t1)', fontWeight: 700 }}>{esi.w}</div>
|
<div className="font-bold">{esi.w}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -375,14 +372,14 @@ function OptimizationPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* NSGA-II 알고리즘 */}
|
{/* NSGA-II 알고리즘 */}
|
||||||
<div className="rounded-[10px] p-4 mb-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3" style={{ color: 'var(--purple)', fontFamily: 'var(--fK)' }}>🧬 NSGA-II (Non-dominated Sorting Genetic Algorithm II)</div>
|
<div className="text-xs font-bold mb-3 text-primary-purple">🧬 NSGA-II (Non-dominated Sorting Genetic Algorithm II)</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] leading-[1.8] mb-2" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">
|
||||||
WING의 오일펜스 배치 최적화는 다목적 유전알고리즘 <b style={{ color: 'var(--purple)' }}>NSGA-II</b>(Deb et al., 2002)를 핵심 엔진으로 사용합니다. 파레토 전면(Pareto Front)을 탐색하여 차단 효율과 자원 효율의 최적 해집합을 제공합니다.
|
WING의 오일펜스 배치 최적화는 다목적 유전알고리즘 <b className="text-primary-purple">NSGA-II</b>(Deb et al., 2002)를 핵심 엔진으로 사용합니다. 파레토 전면(Pareto Front)을 탐색하여 차단 효율과 자원 효율의 최적 해집합을 제공합니다.
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="flex flex-col gap-1 text-[9px] text-text-2">
|
||||||
{[
|
{[
|
||||||
'염색체 구조 : [배치지점 좌표, 방향각θ, 길이L, 형태, 배치순서]',
|
'염색체 구조 : [배치지점 좌표, 방향각θ, 길이L, 형태, 배치순서]',
|
||||||
'집단 크기 : 100~200개체 · 세대수 50~200',
|
'집단 크기 : 100~200개체 · 세대수 50~200',
|
||||||
@ -390,15 +387,15 @@ function OptimizationPanel() {
|
|||||||
'변이 연산 : 다항식 변이(Polynomial Mutation) · ηm=20',
|
'변이 연산 : 다항식 변이(Polynomial Mutation) · ηm=20',
|
||||||
'선택 방식 : 비지배 정렬 + 혼잡도 거리(Crowding Distance)',
|
'선택 방식 : 비지배 정렬 + 혼잡도 거리(Crowding Distance)',
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="px-2 py-[5px] rounded" style={{ background: 'rgba(168,85,247,.05)', border: '1px solid rgba(168,85,247,.12)', color: 'var(--t2)' }}>
|
<div key={i} className="px-2 py-[5px] rounded text-text-2" style={{ background: 'rgba(168,85,247,.05)', border: '1px solid rgba(168,85,247,.12)' }}>
|
||||||
<b style={{ color: 'var(--purple)' }}>{item.split(' : ')[0]}</b> : {item.split(' : ')[1]}
|
<b className="text-primary-purple">{item.split(' : ')[0]}</b> : {item.split(' : ')[1]}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] font-bold mb-[7px]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>NSGA-II 5단계 진화 루프</div>
|
<div className="text-[10px] font-bold mb-[7px] text-text-2">NSGA-II 5단계 진화 루프</div>
|
||||||
<div className="flex flex-col gap-[5px] text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="flex flex-col gap-[5px] text-[9px] text-text-2">
|
||||||
{[
|
{[
|
||||||
{ step: '①', title: '초기 집단 생성', desc: '확산예측 결과 기반 랜덤 + 휴리스틱 배치안 혼합 초기화' },
|
{ step: '①', title: '초기 집단 생성', desc: '확산예측 결과 기반 랜덤 + 휴리스틱 배치안 혼합 초기화' },
|
||||||
{ step: '②', title: '적합도 평가', desc: '유출유 확산 시뮬레이터로 각 배치안의 차단면적·도달시간 계산' },
|
{ step: '②', title: '적합도 평가', desc: '유출유 확산 시뮬레이터로 각 배치안의 차단면적·도달시간 계산' },
|
||||||
@ -407,8 +404,8 @@ function OptimizationPanel() {
|
|||||||
{ step: '⑤', title: '엘리트 선택', desc: '부모+자식 2N 집단에서 비지배 정렬+혼잡도 기준으로 N개 선택 → 수렴까지 반복' },
|
{ step: '⑤', title: '엘리트 선택', desc: '부모+자식 2N 집단에서 비지배 정렬+혼잡도 기준으로 N개 선택 → 수렴까지 반복' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="flex gap-2 px-[9px] py-1.5 rounded-[5px]" style={{ background: i === 4 ? 'rgba(168,85,247,.05)' : 'rgba(168,85,247,.04)', border: i === 4 ? '1px solid rgba(168,85,247,.12)' : undefined }}>
|
<div key={i} className="flex gap-2 px-[9px] py-1.5 rounded-[5px]" style={{ background: i === 4 ? 'rgba(168,85,247,.05)' : 'rgba(168,85,247,.04)', border: i === 4 ? '1px solid rgba(168,85,247,.12)' : undefined }}>
|
||||||
<span className="min-w-[20px] font-extrabold" style={{ color: 'var(--purple)' }}>{item.step}</span>
|
<span className="min-w-[20px] font-extrabold text-primary-purple">{item.step}</span>
|
||||||
<div className="leading-[1.6]" style={{ color: 'var(--t2)' }}><b>{item.title}</b> : {item.desc}</div>
|
<div className="leading-[1.6] text-text-2"><b>{item.title}</b> : {item.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -417,14 +414,14 @@ function OptimizationPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 보조 알고리즘 비교 */}
|
{/* 보조 알고리즘 비교 */}
|
||||||
<div className="rounded-[10px] p-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-[11px] font-bold mb-2.5" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🔬 보조 최적화 알고리즘 비교 적용</div>
|
<div className="text-[11px] font-bold mb-2.5">🔬 보조 최적화 알고리즘 비교 적용</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full" style={{ borderCollapse: 'collapse', fontFamily: 'var(--fK)', fontSize: '10px' }}>
|
<table className="w-full text-[10px] border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ background: 'rgba(255,255,255,.03)', borderBottom: '1px solid var(--bdL)' }}>
|
<tr style={{ background: 'rgba(255,255,255,.03)', borderBottom: '1px solid var(--bdL)' }}>
|
||||||
{['알고리즘', '유형', '장점', '단점', 'WING 활용'].map(h => (
|
{['알고리즘', '유형', '장점', '단점', 'WING 활용'].map(h => (
|
||||||
<th key={h} className="py-[7px] px-2.5 text-left font-semibold" style={{ color: 'var(--t3)', textAlign: h === '알고리즘' ? 'left' : 'center' }}>{h}</th>
|
<th key={h} className="py-[7px] px-2.5 font-semibold text-text-3" style={{ textAlign: h === '알고리즘' ? 'left' : 'center' }}>{h}</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -437,9 +434,9 @@ function OptimizationPanel() {
|
|||||||
].map((row, i) => (
|
].map((row, i) => (
|
||||||
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,.04)', background: i % 2 === 1 ? 'rgba(255,255,255,.01)' : undefined }}>
|
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,.04)', background: i % 2 === 1 ? 'rgba(255,255,255,.01)' : undefined }}>
|
||||||
<td className="py-[7px] px-2.5 font-bold" style={{ color: row.color }}>{row.name}</td>
|
<td className="py-[7px] px-2.5 font-bold" style={{ color: row.color }}>{row.name}</td>
|
||||||
<td className="py-[7px] px-2.5 text-center" style={{ color: 'var(--t2)' }}>{row.type}</td>
|
<td className="py-[7px] px-2.5 text-center text-text-2">{row.type}</td>
|
||||||
<td className="py-[7px] px-2.5 text-center" style={{ color: 'var(--t2)', whiteSpace: 'pre-line' }}>{row.pros}</td>
|
<td className="py-[7px] px-2.5 text-center text-text-2 whitespace-pre-line">{row.pros}</td>
|
||||||
<td className="py-[7px] px-2.5 text-center" style={{ color: 'var(--t2)', whiteSpace: 'pre-line' }}>{row.cons}</td>
|
<td className="py-[7px] px-2.5 text-center text-text-2 whitespace-pre-line">{row.cons}</td>
|
||||||
<td className="py-[7px] px-2.5 text-center" style={{ color: row.wingColor }}>{row.wing}</td>
|
<td className="py-[7px] px-2.5 text-center" style={{ color: row.wingColor }}>{row.wing}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@ -456,47 +453,47 @@ function FluidDynamicsPanel() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 유동 수치 모델 */}
|
{/* 유동 수치 모델 */}
|
||||||
<div className="rounded-[10px] p-4 mb-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🌊 오일펜스 주변 유동 수치 모델</div>
|
<div className="text-xs font-bold mb-3">🌊 오일펜스 주변 유동 수치 모델</div>
|
||||||
<div className="grid grid-cols-2 gap-3.5">
|
<div className="grid grid-cols-2 gap-3.5">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--blue)', fontFamily: 'var(--fK)' }}>① 오일펜스 항력 모델</div>
|
<div className="text-[11px] font-bold mb-2 text-primary-blue">① 오일펜스 항력 모델</div>
|
||||||
<div className="text-[10px] leading-[1.8] mb-2" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>오일펜스에 작용하는 항력은 조류속도의 제곱에 비례합니다. 오일펜스 구조 변형(catenary형태)을 고려한 동적 항력 계산.</div>
|
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">오일펜스에 작용하는 항력은 조류속도의 제곱에 비례합니다. 오일펜스 구조 변형(catenary형태)을 고려한 동적 항력 계산.</div>
|
||||||
<div className="rounded-md p-2.5 text-[10px] leading-[2.1]" style={{ background: 'var(--bg0)', border: '1px solid rgba(59,130,246,.2)', fontFamily: 'var(--fM)', color: 'var(--t1)' }}>
|
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
|
||||||
F<sub>D</sub> = ½ · ρ · C<sub>D</sub> · A · U<sub>n</sub>²<br />
|
F<sub>D</sub> = ½ · ρ · C<sub>D</sub> · A · U<sub>n</sub>²<br />
|
||||||
T = F<sub>D</sub> · L / (2·sin(α))<br />
|
T = F<sub>D</sub> · L / (2·sin(α))<br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>C<sub>D</sub>: 항력계수(≈1.2), A: 수중 투영면적</span><br />
|
<span className="text-[9px] text-text-3">C<sub>D</sub>: 항력계수(≈1.2), A: 수중 투영면적</span><br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>T: 연결부 장력, α: 체인각도</span>
|
<span className="text-[9px] text-text-3">T: 연결부 장력, α: 체인각도</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-bold mb-2" style={{ color: 'var(--orange)', fontFamily: 'var(--fK)' }}>② 기름 통과(Splash-over) 조건</div>
|
<div className="text-[11px] font-bold mb-2 text-status-orange">② 기름 통과(Splash-over) 조건</div>
|
||||||
<div className="text-[10px] leading-[1.8] mb-2" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>조류 유속이 임계값을 초과하면 기름이 파도를 타고 오일펜스를 넘어가는 Splash-over가 발생합니다.</div>
|
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">조류 유속이 임계값을 초과하면 기름이 파도를 타고 오일펜스를 넘어가는 Splash-over가 발생합니다.</div>
|
||||||
<div className="rounded-md p-2.5 text-[10px] leading-[2.1]" style={{ background: 'var(--bg0)', border: '1px solid rgba(249,115,22,.2)', fontFamily: 'var(--fM)', color: 'var(--t1)' }}>
|
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
|
||||||
Fr = U<sub>n</sub> / √(g·Δρ/ρ·h)<br />
|
Fr = U<sub>n</sub> / √(g·Δρ/ρ·h)<br />
|
||||||
Splash-over: Fr > 0.5~0.6<br />
|
Splash-over: Fr > 0.5~0.6<br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>Fr: 수정 Froude수, h: 오일펜스 수중깊이</span><br />
|
<span className="text-[9px] text-text-3">Fr: 수정 Froude수, h: 오일펜스 수중깊이</span><br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>Δρ/ρ: 기름-해수 밀도비 (~0.15)</span>
|
<span className="text-[9px] text-text-3">Δρ/ρ: 기름-해수 밀도비 (~0.15)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Catenary 변형 모델 */}
|
{/* Catenary 변형 모델 */}
|
||||||
<div className="rounded-[10px] p-4 mb-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🔗 오일펜스 현수선(Catenary) 변형 모델</div>
|
<div className="text-xs font-bold mb-3">🔗 오일펜스 현수선(Catenary) 변형 모델</div>
|
||||||
<div className="grid grid-cols-2 gap-3.5">
|
<div className="grid grid-cols-2 gap-3.5">
|
||||||
<div className="text-[10px] leading-[1.8]" style={{ color: 'var(--t2)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] leading-[1.8] text-text-2">
|
||||||
조류와 바람에 의해 오일펜스는 현수선(Catenary) 형태로 변형됩니다. 실제 차단 길이가 설계 길이보다 짧아지며, 최적화 알고리즘에서 <b style={{ color: 'var(--t1)' }}>변형 후 유효 차단 길이</b> L<sub>eff</sub>를 계산합니다.
|
조류와 바람에 의해 오일펜스는 현수선(Catenary) 형태로 변형됩니다. 실제 차단 길이가 설계 길이보다 짧아지며, 최적화 알고리즘에서 <b>변형 후 유효 차단 길이</b> L<sub>eff</sub>를 계산합니다.
|
||||||
<div className="mt-2 rounded-[5px] p-[9px] leading-[2]" style={{ background: 'var(--bg0)', fontFamily: 'var(--fM)' }}>
|
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-0 font-mono">
|
||||||
y(x) = a·cosh(x/a) − a<br />
|
y(x) = a·cosh(x/a) − a<br />
|
||||||
L<sub>arc</sub> = 2a·sinh(L<sub>span</sub>/(2a))<br />
|
L<sub>arc</sub> = 2a·sinh(L<sub>span</sub>/(2a))<br />
|
||||||
L<sub>eff</sub> = L<sub>span</sub> · cos(φ<sub>max</sub>)<br />
|
L<sub>eff</sub> = L<sub>span</sub> · cos(φ<sub>max</sub>)<br />
|
||||||
<span className="text-[9px]" style={{ color: 'var(--t3)' }}>a: catenary 파라미터, φ: 최대 편향각</span>
|
<span className="text-[9px] text-text-3">a: catenary 파라미터, φ: 최대 편향각</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5 text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="flex flex-col gap-1.5 text-[9px] text-text-2">
|
||||||
<div className="text-[10px] font-bold mb-1" style={{ color: 'var(--t2)' }}>변형 단계별 유효 차단 길이 보정</div>
|
<div className="text-[10px] font-bold mb-1 text-text-2">변형 단계별 유효 차단 길이 보정</div>
|
||||||
{[
|
{[
|
||||||
{ cond: 'U < 0.3 knot', result: 'L_eff ≈ L (직선 유지)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)' },
|
{ cond: 'U < 0.3 knot', result: 'L_eff ≈ L (직선 유지)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)' },
|
||||||
{ cond: '0.3~0.7 knot', result: 'L_eff = 0.8~0.95 L (경미 변형)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.12)' },
|
{ cond: '0.3~0.7 knot', result: 'L_eff = 0.8~0.95 L (경미 변형)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.12)' },
|
||||||
@ -512,12 +509,12 @@ function FluidDynamicsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 유막 포집 모델 */}
|
{/* 유막 포집 모델 */}
|
||||||
<div className="rounded-[10px] p-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-[11px] font-bold mb-2.5" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🛢️ 오일펜스 내 유막 포집 동역학</div>
|
<div className="text-[11px] font-bold mb-2.5">🛢️ 오일펜스 내 유막 포집 동역학</div>
|
||||||
<div className="grid grid-cols-2 gap-3 text-[10px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="grid grid-cols-2 gap-3 text-[10px] text-text-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold mb-1.5" style={{ color: 'var(--cyan)' }}>포집 기름 체적 변화율</div>
|
<div className="font-bold mb-1.5 text-primary-cyan">포집 기름 체적 변화율</div>
|
||||||
<div className="rounded-[5px] p-[9px] leading-[2]" style={{ background: 'var(--bg0)', fontFamily: 'var(--fM)' }}>
|
<div className="rounded-[5px] p-[9px] leading-[2] bg-bg-0 font-mono">
|
||||||
dV<sub>oil</sub>/dt = Q<sub>in</sub> − Q<sub>out</sub> − Q<sub>loss</sub><br />
|
dV<sub>oil</sub>/dt = Q<sub>in</sub> − Q<sub>out</sub> − Q<sub>loss</sub><br />
|
||||||
Q<sub>in</sub> = U<sub>oil</sub>·h<sub>oil</sub>·L<sub>eff</sub><br />
|
Q<sub>in</sub> = U<sub>oil</sub>·h<sub>oil</sub>·L<sub>eff</sub><br />
|
||||||
Q<sub>out</sub> = Q<sub>skim</sub> (회수기 흡입량)<br />
|
Q<sub>out</sub> = Q<sub>skim</sub> (회수기 흡입량)<br />
|
||||||
@ -525,8 +522,8 @@ function FluidDynamicsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold mb-1.5" style={{ color: 'var(--orange)' }}>최적 회수 타이밍</div>
|
<div className="font-bold mb-1.5 text-status-orange">최적 회수 타이밍</div>
|
||||||
<div className="text-[10px] leading-[1.7]" style={{ color: 'var(--t2)' }}>포집 기름 체적이 오일펜스 저장 용량의 70~80%에 도달하면 Skimmer 회수 작업을 개시합니다. 이를 초과하면 오일 overflow 발생. WING이 실시간 체적 모니터링 후 회수 알람 발령.</div>
|
<div className="text-[10px] leading-[1.7] text-text-2">포집 기름 체적이 오일펜스 저장 용량의 70~80%에 도달하면 Skimmer 회수 작업을 개시합니다. 이를 초과하면 오일 overflow 발생. WING이 실시간 체적 모니터링 후 회수 알람 발령.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -547,15 +544,15 @@ function FieldApplicationPanel() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 배치 5단계 절차 */}
|
{/* 배치 5단계 절차 */}
|
||||||
<div className="rounded-[10px] p-4 mb-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-xs font-bold mb-3" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>🗺️ WING 오일펜스 배치 의사결정 5단계</div>
|
<div className="text-xs font-bold mb-3">🗺️ WING 오일펜스 배치 의사결정 5단계</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{steps.map(step => (
|
{steps.map(step => (
|
||||||
<div key={step.num} className="flex gap-3 items-start p-3 rounded-lg" style={{ background: step.bg, border: `1px solid ${step.bd}` }}>
|
<div key={step.num} className="flex gap-3 items-start p-3 rounded-lg" style={{ background: step.bg, border: `1px solid ${step.bd}` }}>
|
||||||
<div className="min-w-[36px] h-[36px] rounded-[9px] flex items-center justify-center font-extrabold text-sm flex-shrink-0" style={{ background: step.numBg, border: `1px solid ${step.numBd}`, color: step.color }}>{step.num}</div>
|
<div className="min-w-[36px] h-[36px] rounded-[9px] flex items-center justify-center font-extrabold text-sm flex-shrink-0" style={{ background: step.numBg, border: `1px solid ${step.numBd}`, color: step.color }}>{step.num}</div>
|
||||||
<div className="text-[10px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="text-[10px] text-text-2">
|
||||||
<div className="font-bold mb-1" style={{ color: 'var(--t1)' }}>{step.title}</div>
|
<div className="font-bold mb-1">{step.title}</div>
|
||||||
<div className="leading-[1.7]" style={{ color: 'var(--t2)' }}>{step.desc}</div>
|
<div className="leading-[1.7] text-text-2">{step.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -563,9 +560,9 @@ function FieldApplicationPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 해역별 적용 특성 */}
|
{/* 해역별 적용 특성 */}
|
||||||
<div className="rounded-[10px] p-3.5" style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
|
||||||
<div className="text-[11px] font-bold mb-2.5" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>📍 해역별 적용 특성 및 전략</div>
|
<div className="text-[11px] font-bold mb-2.5">📍 해역별 적용 특성 및 전략</div>
|
||||||
<div className="grid grid-cols-3 gap-2.5 text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="grid grid-cols-3 gap-2.5 text-[9px] text-text-2">
|
||||||
{[
|
{[
|
||||||
{ icon: '🌊', title: '서해 (조차 대형)', color: 'var(--blue)', bg: 'rgba(59,130,246,.05)', bd: 'rgba(59,130,246,.12)', desc: '최대 조차 9m (인천), 조류 최대 3~5 knot. J형 배치 주력. 조석 전환 재배치 필수. 앵커링 수심 급변화 주의.' },
|
{ icon: '🌊', title: '서해 (조차 대형)', color: 'var(--blue)', bg: 'rgba(59,130,246,.05)', bd: 'rgba(59,130,246,.12)', desc: '최대 조차 9m (인천), 조류 최대 3~5 knot. J형 배치 주력. 조석 전환 재배치 필수. 앵커링 수심 급변화 주의.' },
|
||||||
{ icon: '🌿', title: '남해 (다도해)', color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', desc: '복잡한 해안선·섬. 조류 1~2 knot. V형·U형 복합 배치. 좁은 수로 통제 우선. ESI 고등급 갯벌 보호.' },
|
{ icon: '🌿', title: '남해 (다도해)', color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', desc: '복잡한 해안선·섬. 조류 1~2 knot. V형·U형 복합 배치. 좁은 수로 통제 우선. ESI 고등급 갯벌 보호.' },
|
||||||
@ -573,7 +570,7 @@ function FieldApplicationPanel() {
|
|||||||
].map((area, i) => (
|
].map((area, i) => (
|
||||||
<div key={i} className="p-2.5 rounded-[7px]" style={{ background: area.bg, border: `1px solid ${area.bd}` }}>
|
<div key={i} className="p-2.5 rounded-[7px]" style={{ background: area.bg, border: `1px solid ${area.bd}` }}>
|
||||||
<div className="font-bold mb-[5px]" style={{ color: area.color }}>{area.icon} {area.title}</div>
|
<div className="font-bold mb-[5px]" style={{ color: area.color }}>{area.icon} {area.title}</div>
|
||||||
<div className="leading-[1.7]" style={{ color: 'var(--t2)' }}>{area.desc}</div>
|
<div className="leading-[1.7] text-text-2">{area.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -632,15 +629,15 @@ function ReferencesPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-xs font-bold mb-1" style={{ color: 'var(--t1)', fontFamily: 'var(--fK)' }}>📚 오일펜스 배치 최적화 이론 근거 문헌</div>
|
<div className="text-xs font-bold mb-1">📚 오일펜스 배치 최적화 이론 근거 문헌</div>
|
||||||
<div className="text-[10px] mb-3.5" style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>총 12편 · 4개 카테고리</div>
|
<div className="text-[10px] mb-3.5 text-text-3">총 12편 · 4개 카테고리</div>
|
||||||
|
|
||||||
{categories.map((cat, ci) => (
|
{categories.map((cat, ci) => (
|
||||||
<div key={ci} className="mb-4">
|
<div key={ci} className="mb-4">
|
||||||
<div className="text-[10px] font-bold mb-[7px] flex items-center gap-1.5" style={{ color: cat.color, fontFamily: 'var(--fK)' }}>
|
<div className="text-[10px] font-bold mb-[7px] flex items-center gap-1.5" style={{ color: cat.color }}>
|
||||||
<span className="px-[7px] py-0.5 rounded" style={{ background: cat.bg, border: `1px solid ${cat.bd}` }}>{cat.title}</span>
|
<span className="px-[7px] py-0.5 rounded" style={{ background: cat.bg, border: `1px solid ${cat.bd}` }}>{cat.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[5px] text-[9px]" style={{ fontFamily: 'var(--fK)', color: 'var(--t2)' }}>
|
<div className="flex flex-col gap-[5px] text-[9px] text-text-2">
|
||||||
{cat.refs.map((ref, ri) => (
|
{cat.refs.map((ref, ri) => (
|
||||||
<div key={ri} className="p-[9px] px-3 rounded-[7px] grid gap-2" style={{
|
<div key={ri} className="p-[9px] px-3 rounded-[7px] grid gap-2" style={{
|
||||||
gridTemplateColumns: '24px 1fr',
|
gridTemplateColumns: '24px 1fr',
|
||||||
@ -656,9 +653,9 @@ function ReferencesPanel() {
|
|||||||
{ri + 1 === 1 ? '①' : ri + 1 === 2 ? '②' : ri + 1 === 3 ? '③' : '④'}
|
{ri + 1 === 1 ? '①' : ri + 1 === 2 ? '②' : ri + 1 === 3 ? '③' : '④'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold mb-0.5" style={{ color: 'var(--t1)' }}>{ref.title}</div>
|
<div className="font-bold mb-0.5">{ref.title}</div>
|
||||||
<div className="leading-[1.6]" style={{ color: 'var(--t3)' }}>{ref.author}</div>
|
<div className="leading-[1.6] text-text-3">{ref.author}</div>
|
||||||
<div className="mt-0.5" style={{ color: 'var(--t2)' }}>{ref.desc}</div>
|
<div className="mt-0.5 text-text-2">{ref.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -83,8 +83,7 @@ const InfoLayerSection = ({
|
|||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontFamily: 'var(--fK)',
|
border: '1px solid var(--cyan)',
|
||||||
border: '1px solid var(--cyan)',
|
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
color: 'var(--cyan)',
|
color: 'var(--cyan)',
|
||||||
@ -121,8 +120,7 @@ const InfoLayerSection = ({
|
|||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontFamily: 'var(--fK)',
|
border: '1px solid var(--red)',
|
||||||
border: '1px solid var(--red)',
|
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
color: 'var(--red)',
|
color: 'var(--red)',
|
||||||
|
|||||||
@ -52,10 +52,10 @@ const OilBoomSection = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="px-4 pb-4" style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div className="px-4 pb-4 flex flex-col gap-3">
|
||||||
|
|
||||||
{/* Tab Buttons + Reset */}
|
{/* Tab Buttons + Reset */}
|
||||||
<div style={{ display: 'flex', gap: '6px' }}>
|
<div className="flex gap-1.5">
|
||||||
{[
|
{[
|
||||||
{ id: 'ai' as const, label: 'AI 자동 추천' },
|
{ id: 'ai' as const, label: 'AI 자동 추천' },
|
||||||
{ id: 'manual' as const, label: '수동 배치' },
|
{ id: 'manual' as const, label: '수동 배치' },
|
||||||
@ -65,18 +65,14 @@ const OilBoomSection = ({
|
|||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setBoomPlacementTab(tab.id)}
|
onClick={() => setBoomPlacementTab(tab.id)}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
padding: '6px 8px',
|
padding: '6px 8px',
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: 600,
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
border: boomPlacementTab === tab.id ? '1px solid var(--orange)' : '1px solid var(--bd)',
|
border: boomPlacementTab === tab.id ? '1px solid var(--orange)' : '1px solid var(--bd)',
|
||||||
background: boomPlacementTab === tab.id ? 'rgba(245,158,11,0.1)' : 'var(--bg0)',
|
background: boomPlacementTab === tab.id ? 'rgba(245,158,11,0.1)' : 'var(--bg0)',
|
||||||
color: boomPlacementTab === tab.id ? 'var(--orange)' : 'var(--t3)',
|
color: boomPlacementTab === tab.id ? 'var(--orange)' : 'var(--t3)',
|
||||||
cursor: 'pointer',
|
|
||||||
transition: '0.15s'
|
transition: '0.15s'
|
||||||
}}
|
}}
|
||||||
|
className="flex-1 text-[10px] font-semibold cursor-pointer"
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
@ -97,17 +93,14 @@ const OilBoomSection = ({
|
|||||||
disabled={boomLines.length === 0 && !isDrawingBoom && !containmentResult}
|
disabled={boomLines.length === 0 && !isDrawingBoom && !containmentResult}
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 10px',
|
padding: '6px 10px',
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: 600,
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
border: '1px solid var(--bd)',
|
border: '1px solid var(--bd)',
|
||||||
background: 'var(--bg0)',
|
background: 'var(--bg0)',
|
||||||
color: (boomLines.length === 0 && !isDrawingBoom && !containmentResult) ? 'var(--t3)' : 'var(--red)',
|
color: (boomLines.length === 0 && !isDrawingBoom && !containmentResult) ? 'var(--t3)' : 'var(--red)',
|
||||||
cursor: (boomLines.length === 0 && !isDrawingBoom && !containmentResult) ? 'not-allowed' : 'pointer',
|
cursor: (boomLines.length === 0 && !isDrawingBoom && !containmentResult) ? 'not-allowed' : 'pointer',
|
||||||
transition: '0.15s',
|
transition: '0.15s',
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
|
className="text-[10px] font-semibold shrink-0"
|
||||||
>
|
>
|
||||||
초기화
|
초기화
|
||||||
</button>
|
</button>
|
||||||
@ -123,14 +116,13 @@ const OilBoomSection = ({
|
|||||||
<div key={idx} style={{
|
<div key={idx} style={{
|
||||||
padding: '10px 8px',
|
padding: '10px 8px',
|
||||||
background: 'var(--bg0)',
|
background: 'var(--bg0)',
|
||||||
border: '1px solid var(--bd)',
|
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}} className="border border-border">
|
||||||
<div style={{ fontSize: '18px', fontWeight: 700, color: metric.color, fontFamily: 'var(--fM)', marginBottom: '2px' }}>
|
<div style={{ color: metric.color }} className="text-lg font-bold font-mono mb-[2px]">
|
||||||
{metric.value}
|
{metric.value}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<div className="text-[8px] text-text-3">
|
||||||
{metric.label}
|
{metric.label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -146,18 +138,18 @@ const OilBoomSection = ({
|
|||||||
border: '1px solid rgba(245,158,11,0.3)',
|
border: '1px solid rgba(245,158,11,0.3)',
|
||||||
borderRadius: 'var(--rM)'
|
borderRadius: 'var(--rM)'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '8px' }}>
|
<div className="flex items-center gap-1 mb-2">
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--t3)' }} />
|
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--t3)' }} />
|
||||||
<span style={{ fontSize: '10px', fontWeight: 700, color: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<span style={{ color: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--t3)' }} className="text-[10px] font-bold">
|
||||||
{oilTrajectory.length > 0 ? '확산 데이터 준비 완료' : '확산 예측을 먼저 실행하세요'}
|
{oilTrajectory.length > 0 ? '확산 데이터 준비 완료' : '확산 예측을 먼저 실행하세요'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 style={{ fontSize: '13px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: '8px' }}>
|
<h4 className="text-[13px] font-bold mb-2">
|
||||||
확산 예측 기반 최적 배치안
|
확산 예측 기반 최적 배치안
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<p style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: '1.5', marginBottom: '10px' }}>
|
<p className="leading-normal" className="text-[9px] text-text-3 mb-2.5">
|
||||||
{oilTrajectory.length > 0
|
{oilTrajectory.length > 0
|
||||||
? '확산 궤적을 분석하여 해류 직교 방향 1차 방어선, U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 생성합니다.'
|
? '확산 궤적을 분석하여 해류 직교 방향 1차 방어선, U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 생성합니다.'
|
||||||
: '상단에서 확산 예측을 실행한 뒤 AI 배치를 적용할 수 있습니다.'
|
: '상단에서 확산 예측을 실행한 뒤 AI 배치를 적용할 수 있습니다.'
|
||||||
@ -175,11 +167,6 @@ const OilBoomSection = ({
|
|||||||
}}
|
}}
|
||||||
disabled={oilTrajectory.length === 0}
|
disabled={oilTrajectory.length === 0}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
|
||||||
padding: '10px',
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
background: oilTrajectory.length > 0 ? 'rgba(245,158,11,0.15)' : 'var(--bg0)',
|
background: oilTrajectory.length > 0 ? 'rgba(245,158,11,0.15)' : 'var(--bg0)',
|
||||||
border: oilTrajectory.length > 0 ? '2px solid var(--orange)' : '1px solid var(--bd)',
|
border: oilTrajectory.length > 0 ? '2px solid var(--orange)' : '1px solid var(--bd)',
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
@ -187,6 +174,7 @@ const OilBoomSection = ({
|
|||||||
cursor: oilTrajectory.length > 0 ? 'pointer' : 'not-allowed',
|
cursor: oilTrajectory.length > 0 ? 'pointer' : 'not-allowed',
|
||||||
transition: '0.15s'
|
transition: '0.15s'
|
||||||
}}
|
}}
|
||||||
|
className="w-full p-[10px] text-[11px] font-bold"
|
||||||
>
|
>
|
||||||
🛡 추천 배치안 적용하기
|
🛡 추천 배치안 적용하기
|
||||||
</button>
|
</button>
|
||||||
@ -194,10 +182,10 @@ const OilBoomSection = ({
|
|||||||
|
|
||||||
{/* 알고리즘 설정 */}
|
{/* 알고리즘 설정 */}
|
||||||
<div>
|
<div>
|
||||||
<h4 style={{ fontSize: '11px', fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: '8px', letterSpacing: '0.5px' }}>
|
<h4 className="text-[11px] font-bold text-primary-cyan mb-2" style={{ letterSpacing: '0.5px' }}>
|
||||||
📊 배치 알고리즘 설정
|
📊 배치 알고리즘 설정
|
||||||
</h4>
|
</h4>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="flex flex-col gap-2">
|
||||||
{[
|
{[
|
||||||
{ label: '해류 직교 보정', key: 'currentOrthogonalCorrection' as const, unit: '°', value: algorithmSettings.currentOrthogonalCorrection },
|
{ label: '해류 직교 보정', key: 'currentOrthogonalCorrection' as const, unit: '°', value: algorithmSettings.currentOrthogonalCorrection },
|
||||||
{ label: '안전 마진 (도달시간)', key: 'safetyMarginMinutes' as const, unit: '분', value: algorithmSettings.safetyMarginMinutes },
|
{ label: '안전 마진 (도달시간)', key: 'safetyMarginMinutes' as const, unit: '분', value: algorithmSettings.safetyMarginMinutes },
|
||||||
@ -205,11 +193,11 @@ const OilBoomSection = ({
|
|||||||
{ label: '파고 보정 계수', key: 'waveHeightCorrectionFactor' as const, unit: 'x', value: algorithmSettings.waveHeightCorrectionFactor },
|
{ label: '파고 보정 계수', key: 'waveHeightCorrectionFactor' as const, unit: 'x', value: algorithmSettings.waveHeightCorrectionFactor },
|
||||||
].map((setting) => (
|
].map((setting) => (
|
||||||
<div key={setting.key} style={{
|
<div key={setting.key} style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
background: 'var(--bg0)',
|
||||||
padding: '6px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)'
|
borderRadius: 'var(--rS)'
|
||||||
}}>
|
}} className="flex items-center justify-between p-[6px_8px] border border-border">
|
||||||
<span style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>● {setting.label}</span>
|
<span className="text-[9px] text-text-3">● {setting.label}</span>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
|
<div className="flex items-center gap-[2px]">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={setting.value}
|
value={setting.value}
|
||||||
@ -220,7 +208,7 @@ const OilBoomSection = ({
|
|||||||
className="boom-setting-input"
|
className="boom-setting-input"
|
||||||
step={setting.key === 'waveHeightCorrectionFactor' ? 0.1 : 1}
|
step={setting.key === 'waveHeightCorrectionFactor' ? 0.1 : 1}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: '9px', color: 'var(--orange)', fontFamily: 'var(--fK)' }}>{setting.unit}</span>
|
<span className="text-[9px] text-status-orange">{setting.unit}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -233,15 +221,15 @@ const OilBoomSection = ({
|
|||||||
{boomPlacementTab === 'manual' && (
|
{boomPlacementTab === 'manual' && (
|
||||||
<>
|
<>
|
||||||
{/* 드로잉 컨트롤 */}
|
{/* 드로잉 컨트롤 */}
|
||||||
<div style={{ display: 'flex', gap: '6px' }}>
|
<div className="flex gap-1.5">
|
||||||
{!isDrawingBoom ? (
|
{!isDrawingBoom ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => { onDrawingBoomChange(true); onDrawingPointsChange([]) }}
|
onClick={() => { onDrawingBoomChange(true); onDrawingPointsChange([]) }}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '10px', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
||||||
background: 'rgba(245,158,11,0.15)', border: '2px solid var(--orange)',
|
background: 'rgba(245,158,11,0.15)', border: '2px solid var(--orange)',
|
||||||
borderRadius: 'var(--rS)', color: 'var(--orange)', cursor: 'pointer', transition: '0.15s'
|
borderRadius: 'var(--rS)', color: 'var(--orange)', transition: '0.15s'
|
||||||
}}
|
}}
|
||||||
|
className="flex-1 p-[10px] text-[11px] font-bold cursor-pointer"
|
||||||
>
|
>
|
||||||
🛡 배치 시작
|
🛡 배치 시작
|
||||||
</button>
|
</button>
|
||||||
@ -268,23 +256,24 @@ const OilBoomSection = ({
|
|||||||
}}
|
}}
|
||||||
disabled={drawingPoints.length < 2}
|
disabled={drawingPoints.length < 2}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '10px', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
||||||
background: drawingPoints.length >= 2 ? 'rgba(34,197,94,0.15)' : 'var(--bg0)',
|
background: drawingPoints.length >= 2 ? 'rgba(34,197,94,0.15)' : 'var(--bg0)',
|
||||||
border: drawingPoints.length >= 2 ? '2px solid var(--green)' : '1px solid var(--bd)',
|
border: drawingPoints.length >= 2 ? '2px solid var(--green)' : '1px solid var(--bd)',
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
color: drawingPoints.length >= 2 ? 'var(--green)' : 'var(--t3)',
|
color: drawingPoints.length >= 2 ? 'var(--green)' : 'var(--t3)',
|
||||||
cursor: drawingPoints.length >= 2 ? 'pointer' : 'not-allowed', transition: '0.15s'
|
cursor: drawingPoints.length >= 2 ? 'pointer' : 'not-allowed', transition: '0.15s'
|
||||||
}}
|
}}
|
||||||
|
className="flex-1 p-[10px] text-[11px] font-bold"
|
||||||
>
|
>
|
||||||
배치 완료 ({drawingPoints.length}점)
|
배치 완료 ({drawingPoints.length}점)
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onDrawingBoomChange(false); onDrawingPointsChange([]) }}
|
onClick={() => { onDrawingBoomChange(false); onDrawingPointsChange([]) }}
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 14px', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
padding: '10px 14px',
|
||||||
background: 'rgba(239,68,68,0.1)', border: '1px solid var(--red)',
|
background: 'rgba(239,68,68,0.1)', border: '1px solid var(--red)',
|
||||||
borderRadius: 'var(--rS)', color: 'var(--red)', cursor: 'pointer', transition: '0.15s'
|
borderRadius: 'var(--rS)', color: 'var(--red)', transition: '0.15s'
|
||||||
}}
|
}}
|
||||||
|
className="text-[11px] font-bold cursor-pointer"
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</button>
|
</button>
|
||||||
@ -297,29 +286,28 @@ const OilBoomSection = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
padding: '8px 10px', background: 'rgba(245,158,11,0.05)',
|
padding: '8px 10px', background: 'rgba(245,158,11,0.05)',
|
||||||
border: '1px solid rgba(245,158,11,0.3)', borderRadius: 'var(--rS)',
|
border: '1px solid rgba(245,158,11,0.3)', borderRadius: 'var(--rS)',
|
||||||
display: 'flex', gap: '12px', fontSize: '10px', fontFamily: 'var(--fK)', color: 'var(--t2)'
|
}} className="flex gap-3 text-[10px] text-text-2">
|
||||||
}}>
|
<span>포인트: <strong className="text-status-orange font-mono">{drawingPoints.length}</strong></span>
|
||||||
<span>포인트: <strong style={{ color: 'var(--orange)', fontFamily: 'var(--fM)' }}>{drawingPoints.length}</strong></span>
|
<span>길이: <strong className="text-primary-cyan font-mono">{computePolylineLength(drawingPoints).toFixed(0)}m</strong></span>
|
||||||
<span>길이: <strong style={{ color: 'var(--cyan)', fontFamily: 'var(--fM)' }}>{computePolylineLength(drawingPoints).toFixed(0)}m</strong></span>
|
|
||||||
{drawingPoints.length >= 2 && (
|
{drawingPoints.length >= 2 && (
|
||||||
<span>방위각: <strong style={{ color: 'var(--t1)', fontFamily: 'var(--fM)' }}>{computeBearing(drawingPoints[0], drawingPoints[drawingPoints.length - 1]).toFixed(0)}°</strong></span>
|
<span>방위각: <strong className="font-mono">{computeBearing(drawingPoints[0], drawingPoints[drawingPoints.length - 1]).toFixed(0)}°</strong></span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 배치된 라인 목록 */}
|
{/* 배치된 라인 목록 */}
|
||||||
{boomLines.length === 0 ? (
|
{boomLines.length === 0 ? (
|
||||||
<p style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', textAlign: 'center', padding: '16px 0' }}>
|
<p className="text-[10px] text-text-3 text-center py-4">
|
||||||
배치된 오일펜스 라인이 없습니다.
|
배치된 오일펜스 라인이 없습니다.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
boomLines.map((line, idx) => (
|
boomLines.map((line, idx) => (
|
||||||
<div key={line.id} style={{
|
<div key={line.id} style={{
|
||||||
padding: '10px', background: 'var(--bg0)', border: '1px solid var(--bd)',
|
padding: '10px', background: 'var(--bg0)',
|
||||||
borderLeft: `3px solid ${line.priority === 'CRITICAL' ? 'var(--red)' : line.priority === 'HIGH' ? 'var(--orange)' : 'var(--yellow)'}`,
|
borderLeft: `3px solid ${line.priority === 'CRITICAL' ? 'var(--red)' : line.priority === 'HIGH' ? 'var(--orange)' : 'var(--yellow)'}`,
|
||||||
borderRadius: 'var(--rS)'
|
borderRadius: 'var(--rS)'
|
||||||
}}>
|
}} className="border border-border">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '6px' }}>
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={line.name}
|
value={line.name}
|
||||||
@ -328,32 +316,26 @@ const OilBoomSection = ({
|
|||||||
updated[idx] = { ...updated[idx], name: e.target.value }
|
updated[idx] = { ...updated[idx], name: e.target.value }
|
||||||
onBoomLinesChange(updated)
|
onBoomLinesChange(updated)
|
||||||
}}
|
}}
|
||||||
style={{
|
className="flex-1 text-[11px] font-bold bg-transparent border-none outline-none"
|
||||||
flex: 1, fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
||||||
background: 'transparent', border: 'none', color: 'var(--t1)', outline: 'none'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => onBoomLinesChange(boomLines.filter(l => l.id !== line.id))}
|
onClick={() => onBoomLinesChange(boomLines.filter(l => l.id !== line.id))}
|
||||||
style={{
|
className="text-[10px] text-status-red bg-transparent border-none cursor-pointer px-1.5 py-[2px]"
|
||||||
fontSize: '10px', color: 'var(--red)', background: 'none', border: 'none',
|
|
||||||
cursor: 'pointer', padding: '2px 6px'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
삭제
|
삭제
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '6px', fontSize: '9px', fontFamily: 'var(--fK)' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '6px' }} className="text-[9px]">
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: 'var(--t3)' }}>길이</span>
|
<span className="text-text-3">길이</span>
|
||||||
<div style={{ fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>{line.length.toFixed(0)}m</div>
|
<div className="font-bold font-mono">{line.length.toFixed(0)}m</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: 'var(--t3)' }}>각도</span>
|
<span className="text-text-3">각도</span>
|
||||||
<div style={{ fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>{line.angle.toFixed(0)}°</div>
|
<div className="font-bold font-mono">{line.angle.toFixed(0)}°</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: 'var(--t3)' }}>우선순위</span>
|
<span className="text-text-3">우선순위</span>
|
||||||
<select
|
<select
|
||||||
value={line.priority}
|
value={line.priority}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -362,10 +344,11 @@ const OilBoomSection = ({
|
|||||||
onBoomLinesChange(updated)
|
onBoomLinesChange(updated)
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: '100%', fontSize: '10px', fontWeight: 600, fontFamily: 'var(--fK)',
|
background: 'var(--bg0)',
|
||||||
background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
color: 'var(--t1)', padding: '2px', outline: 'none'
|
padding: '2px',
|
||||||
}}
|
}}
|
||||||
|
className="w-full text-[10px] font-semibold border border-border outline-none"
|
||||||
>
|
>
|
||||||
<option value="CRITICAL">긴급</option>
|
<option value="CRITICAL">긴급</option>
|
||||||
<option value="HIGH">중요</option>
|
<option value="HIGH">중요</option>
|
||||||
@ -383,22 +366,20 @@ const OilBoomSection = ({
|
|||||||
{boomPlacementTab === 'simulation' && (
|
{boomPlacementTab === 'simulation' && (
|
||||||
<>
|
<>
|
||||||
{/* 전제조건 체크 */}
|
{/* 전제조건 체크 */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div className="flex flex-col gap-1.5">
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 10px',
|
background: 'var(--bg0)',
|
||||||
background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
fontSize: '10px', fontFamily: 'var(--fK)'
|
}} className="flex items-center gap-1.5 p-[6px_10px] border border-border text-[10px]">
|
||||||
}}>
|
|
||||||
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--red)' }} />
|
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--red)' }} />
|
||||||
<span style={{ color: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--t3)' }}>
|
<span style={{ color: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--t3)' }}>
|
||||||
확산 궤적 데이터 {oilTrajectory.length > 0 ? `(${oilTrajectory.length}개 입자)` : '없음'}
|
확산 궤적 데이터 {oilTrajectory.length > 0 ? `(${oilTrajectory.length}개 입자)` : '없음'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 10px',
|
background: 'var(--bg0)',
|
||||||
background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
fontSize: '10px', fontFamily: 'var(--fK)'
|
}} className="flex items-center gap-1.5 p-[6px_10px] border border-border text-[10px]">
|
||||||
}}>
|
|
||||||
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: boomLines.length > 0 ? 'var(--green)' : 'var(--red)' }} />
|
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: boomLines.length > 0 ? 'var(--green)' : 'var(--red)' }} />
|
||||||
<span style={{ color: boomLines.length > 0 ? 'var(--green)' : 'var(--t3)' }}>
|
<span style={{ color: boomLines.length > 0 ? 'var(--green)' : 'var(--t3)' }}>
|
||||||
오일펜스 라인 {boomLines.length > 0 ? `(${boomLines.length}개 배치)` : '없음'}
|
오일펜스 라인 {boomLines.length > 0 ? `(${boomLines.length}개 배치)` : '없음'}
|
||||||
@ -414,7 +395,6 @@ const OilBoomSection = ({
|
|||||||
}}
|
}}
|
||||||
disabled={oilTrajectory.length === 0 || boomLines.length === 0}
|
disabled={oilTrajectory.length === 0 || boomLines.length === 0}
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '10px', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
||||||
background: (oilTrajectory.length > 0 && boomLines.length > 0) ? 'rgba(6,182,212,0.15)' : 'var(--bg0)',
|
background: (oilTrajectory.length > 0 && boomLines.length > 0) ? 'rgba(6,182,212,0.15)' : 'var(--bg0)',
|
||||||
border: (oilTrajectory.length > 0 && boomLines.length > 0) ? '2px solid var(--cyan)' : '1px solid var(--bd)',
|
border: (oilTrajectory.length > 0 && boomLines.length > 0) ? '2px solid var(--cyan)' : '1px solid var(--bd)',
|
||||||
borderRadius: 'var(--rS)',
|
borderRadius: 'var(--rS)',
|
||||||
@ -422,39 +402,40 @@ const OilBoomSection = ({
|
|||||||
cursor: (oilTrajectory.length > 0 && boomLines.length > 0) ? 'pointer' : 'not-allowed',
|
cursor: (oilTrajectory.length > 0 && boomLines.length > 0) ? 'pointer' : 'not-allowed',
|
||||||
transition: '0.15s'
|
transition: '0.15s'
|
||||||
}}
|
}}
|
||||||
|
className="w-full p-[10px] text-[11px] font-bold"
|
||||||
>
|
>
|
||||||
🔬 차단 시뮬레이션 실행
|
🔬 차단 시뮬레이션 실행
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 시뮬레이션 결과 */}
|
{/* 시뮬레이션 결과 */}
|
||||||
{containmentResult && containmentResult.totalParticles > 0 && (
|
{containmentResult && containmentResult.totalParticles > 0 && (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
<div className="flex flex-col gap-2.5">
|
||||||
{/* 전체 효율 */}
|
{/* 전체 효율 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px', background: 'rgba(6,182,212,0.05)',
|
padding: '16px', background: 'rgba(6,182,212,0.05)',
|
||||||
border: '1px solid rgba(6,182,212,0.3)', borderRadius: 'var(--rM)', textAlign: 'center'
|
border: '1px solid rgba(6,182,212,0.3)', borderRadius: 'var(--rM)', textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '28px', fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fM)' }}>
|
<div className="text-[28px] font-bold text-primary-cyan font-mono">
|
||||||
{containmentResult.overallEfficiency}%
|
{containmentResult.overallEfficiency}%
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>
|
<div className="text-[10px] text-text-3 mt-[2px]">
|
||||||
전체 차단 효율
|
전체 차단 효율
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 차단/통과 카운트 */}
|
{/* 차단/통과 카운트 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
||||||
<div style={{ padding: '10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', textAlign: 'center' }}>
|
<div style={{ padding: '10px', background: 'var(--bg0)', borderRadius: 'var(--rS)', textAlign: 'center' }} className="border border-border">
|
||||||
<div style={{ fontSize: '16px', fontWeight: 700, color: 'var(--green)', fontFamily: 'var(--fM)' }}>
|
<div className="text-base font-bold text-status-green font-mono">
|
||||||
{containmentResult.blockedParticles}
|
{containmentResult.blockedParticles}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>차단 입자</div>
|
<div className="text-[8px] text-text-3">차단 입자</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', textAlign: 'center' }}>
|
<div style={{ padding: '10px', background: 'var(--bg0)', borderRadius: 'var(--rS)', textAlign: 'center' }} className="border border-border">
|
||||||
<div style={{ fontSize: '16px', fontWeight: 700, color: 'var(--red)', fontFamily: 'var(--fM)' }}>
|
<div className="text-base font-bold text-status-red font-mono">
|
||||||
{containmentResult.passedParticles}
|
{containmentResult.passedParticles}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>통과 입자</div>
|
<div className="text-[8px] text-text-3">통과 입자</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -468,18 +449,16 @@ const OilBoomSection = ({
|
|||||||
|
|
||||||
{/* 라인별 분석 */}
|
{/* 라인별 분석 */}
|
||||||
<div>
|
<div>
|
||||||
<h4 style={{ fontSize: '10px', fontWeight: 700, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '6px' }}>
|
<h4 className="text-[10px] font-bold text-text-3 mb-1.5">
|
||||||
라인별 차단 분석
|
라인별 차단 분석
|
||||||
</h4>
|
</h4>
|
||||||
{containmentResult.perLineResults.map((r) => (
|
{containmentResult.perLineResults.map((r) => (
|
||||||
<div key={r.boomLineId} style={{
|
<div key={r.boomLineId} style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
background: 'var(--bg0)',
|
||||||
padding: '6px 8px', marginBottom: '4px',
|
borderRadius: 'var(--rS)',
|
||||||
background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)',
|
}} className="flex items-center justify-between p-[6px_8px] mb-1 border border-border text-[9px]">
|
||||||
fontSize: '9px', fontFamily: 'var(--fK)'
|
<span className="text-text-2 flex-1">{r.boomLineName}</span>
|
||||||
}}>
|
<span style={{ color: r.efficiency >= 50 ? 'var(--green)' : 'var(--orange)', marginLeft: '8px' }} className="font-bold font-mono">
|
||||||
<span style={{ color: 'var(--t2)', flex: 1 }}>{r.boomLineName}</span>
|
|
||||||
<span style={{ fontWeight: 700, color: r.efficiency >= 50 ? 'var(--green)' : 'var(--orange)', fontFamily: 'var(--fM)', marginLeft: '8px' }}>
|
|
||||||
{r.blocked}차단 / {r.efficiency}%
|
{r.blocked}차단 / {r.efficiency}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -498,38 +477,38 @@ const OilBoomSection = ({
|
|||||||
const priorityLabel = line.priority === 'CRITICAL' ? '긴급' : line.priority === 'HIGH' ? '중요' : '보통'
|
const priorityLabel = line.priority === 'CRITICAL' ? '긴급' : line.priority === 'HIGH' ? '중요' : '보통'
|
||||||
return (
|
return (
|
||||||
<div key={line.id} style={{
|
<div key={line.id} style={{
|
||||||
padding: '10px', background: 'var(--bg0)', border: '1px solid var(--bd)',
|
padding: '10px', background: 'var(--bg0)',
|
||||||
borderLeft: `3px solid ${priorityColor}`, borderRadius: 'var(--rS)'
|
borderLeft: `3px solid ${priorityColor}`, borderRadius: 'var(--rS)'
|
||||||
}}>
|
}} className="border border-border">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span style={{ fontSize: '11px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>
|
<span className="text-[11px] font-bold">
|
||||||
🛡 {idx + 1}차 방어선 ({line.type})
|
🛡 {idx + 1}차 방어선 ({line.type})
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
<span style={{
|
||||||
padding: '2px 6px', fontSize: '8px', fontWeight: 700, fontFamily: 'var(--fK)',
|
padding: '2px 6px',
|
||||||
background: `${priorityColor}20`, border: `1px solid ${priorityColor}`,
|
background: `${priorityColor}20`, border: `1px solid ${priorityColor}`,
|
||||||
borderRadius: '3px', color: priorityColor
|
borderRadius: '3px', color: priorityColor
|
||||||
}}>
|
}} className="text-[8px] font-bold">
|
||||||
{priorityLabel}
|
{priorityLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px', marginBottom: '6px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }} className="mb-1.5">
|
||||||
<div>
|
<div>
|
||||||
<span style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>길이</span>
|
<span className="text-[8px] text-text-3">길이</span>
|
||||||
<div style={{ fontSize: '14px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>
|
<div className="text-sm font-bold font-mono">
|
||||||
{line.length.toFixed(0)}m
|
{line.length.toFixed(0)}m
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)' }}>각도</span>
|
<span className="text-[8px] text-text-3">각도</span>
|
||||||
<div style={{ fontSize: '14px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fM)' }}>
|
<div className="text-sm font-bold font-mono">
|
||||||
{line.angle.toFixed(0)}°
|
{line.angle.toFixed(0)}°
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div className="flex items-center gap-1">
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: line.efficiency >= 80 ? 'var(--green)' : 'var(--orange)' }} />
|
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: line.efficiency >= 80 ? 'var(--green)' : 'var(--orange)' }} />
|
||||||
<span style={{ fontSize: '9px', fontWeight: 600, color: line.efficiency >= 80 ? 'var(--green)' : 'var(--orange)', fontFamily: 'var(--fK)' }}>
|
<span style={{ color: line.efficiency >= 80 ? 'var(--green)' : 'var(--orange)' }} className="text-[9px] font-semibold">
|
||||||
차단 효율 {line.efficiency}%
|
차단 효율 {line.efficiency}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -472,14 +472,13 @@ export function OilSpillView() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 타임라인 플레이어 (리플레이 비활성 시) */}
|
{/* 타임라인 플레이어 (리플레이 비활성 시) */}
|
||||||
{!isReplayActive && <div style={{
|
{!isReplayActive && <div className="absolute bottom-0 left-0 right-0 h-[72px] flex items-center px-5 gap-4" style={{
|
||||||
position: 'absolute', bottom: 0, left: 0, right: 0, height: '72px',
|
|
||||||
background: 'rgba(15,21,36,0.95)', backdropFilter: 'blur(16px)',
|
background: 'rgba(15,21,36,0.95)', backdropFilter: 'blur(16px)',
|
||||||
borderTop: '1px solid var(--bd)',
|
borderTop: '1px solid var(--bd)',
|
||||||
display: 'flex', alignItems: 'center', padding: '0 20px', gap: '16px', zIndex: 1100
|
zIndex: 1100
|
||||||
}}>
|
}}>
|
||||||
{/* 컨트롤 버튼 */}
|
{/* 컨트롤 버튼 */}
|
||||||
<div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}>
|
<div className="flex gap-1 shrink-0">
|
||||||
{[
|
{[
|
||||||
{ icon: '⏮', action: () => setTimelinePosition(0) },
|
{ icon: '⏮', action: () => setTimelinePosition(0) },
|
||||||
{ icon: '◀', action: () => setTimelinePosition(Math.max(0, timelinePosition - 100 / 12)) },
|
{ icon: '◀', action: () => setTimelinePosition(Math.max(0, timelinePosition - 100 / 12)) },
|
||||||
@ -510,7 +509,7 @@ export function OilSpillView() {
|
|||||||
cursor: 'pointer', fontSize: '12px', transition: '0.2s'
|
cursor: 'pointer', fontSize: '12px', transition: '0.2s'
|
||||||
}}>{btn.icon}</button>
|
}}>{btn.icon}</button>
|
||||||
))}
|
))}
|
||||||
<div style={{ width: '8px' }} />
|
<div className="w-2" />
|
||||||
<button onClick={() => setPlaySpeed(playSpeed >= 4 ? 1 : playSpeed * 2)} style={{
|
<button onClick={() => setPlaySpeed(playSpeed >= 4 ? 1 : playSpeed * 2)} style={{
|
||||||
width: '34px', height: '34px', borderRadius: 'var(--rS, 4px)',
|
width: '34px', height: '34px', borderRadius: 'var(--rS, 4px)',
|
||||||
border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t2)',
|
border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t2)',
|
||||||
@ -520,9 +519,9 @@ export function OilSpillView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 타임라인 슬라이더 */}
|
{/* 타임라인 슬라이더 */}
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div className="flex-1 flex flex-col gap-1.5">
|
||||||
{/* 시간 라벨 */}
|
{/* 시간 라벨 */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0 4px' }}>
|
<div className="flex justify-between px-1">
|
||||||
{['0h', '6h', '12h', '18h', '24h', '36h', '48h', '60h', '72h'].map((label, i) => {
|
{['0h', '6h', '12h', '18h', '24h', '36h', '48h', '60h', '72h'].map((label, i) => {
|
||||||
const pos = [0, 8.33, 16.67, 25, 33.33, 50, 66.67, 83.33, 100][i]
|
const pos = [0, 8.33, 16.67, 25, 33.33, 50, 66.67, 83.33, 100][i]
|
||||||
const isActive = Math.abs(timelinePosition - pos) < 5
|
const isActive = Math.abs(timelinePosition - pos) < 5
|
||||||
@ -537,7 +536,7 @@ export function OilSpillView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 슬라이더 트랙 */}
|
{/* 슬라이더 트랙 */}
|
||||||
<div style={{ position: 'relative', height: '24px', display: 'flex', alignItems: 'center' }}>
|
<div className="relative h-6 flex items-center">
|
||||||
{/* 트랙 레일 */}
|
{/* 트랙 레일 */}
|
||||||
<div
|
<div
|
||||||
style={{ width: '100%', height: '4px', background: 'var(--bd)', borderRadius: '2px', position: 'relative', cursor: 'pointer' }}
|
style={{ width: '100%', height: '4px', background: 'var(--bd)', borderRadius: '2px', position: 'relative', cursor: 'pointer' }}
|
||||||
@ -603,12 +602,12 @@ export function OilSpillView() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '14px' }}>
|
<div style={{ display: 'flex', gap: '14px' }}>
|
||||||
{[
|
{[
|
||||||
{ label: '풍화율', value: `${Math.min(99, Math.round(timelinePosition * 0.4))}%`, color: 'var(--t1)' },
|
{ label: '풍화율', value: `${Math.min(99, Math.round(timelinePosition * 0.4))}%` },
|
||||||
{ label: '면적', value: `${(timelinePosition * 0.08).toFixed(1)} km²`, color: 'var(--t1)' },
|
{ label: '면적', value: `${(timelinePosition * 0.08).toFixed(1)} km²` },
|
||||||
{ label: '차단율', value: boomLines.length > 0 ? `${Math.min(95, 70 + Math.round(timelinePosition * 0.2))}%` : '—', color: 'var(--boom)' },
|
{ label: '차단율', value: boomLines.length > 0 ? `${Math.min(95, 70 + Math.round(timelinePosition * 0.2))}%` : '—', color: 'var(--boom)' },
|
||||||
].map((s, i) => (
|
].map((s, i) => (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px' }}>
|
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px' }}>
|
||||||
<span style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{s.label}</span>
|
<span className="text-text-3">{s.label}</span>
|
||||||
<span style={{ color: s.color, fontWeight: 600, fontFamily: 'var(--fM)' }}>{s.value}</span>
|
<span style={{ color: s.color, fontWeight: 600, fontFamily: 'var(--fM)' }}>{s.value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -79,26 +79,28 @@ const PredictionInputSection = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="px-4 pb-4" style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div className="px-4 pb-4 flex flex-col gap-[6px]">
|
||||||
{/* Input Mode Selection */}
|
{/* Input Mode Selection */}
|
||||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', fontSize: '11px', color: 'var(--t1)', fontFamily: 'var(--fK)' }}>
|
<div className="flex items-center gap-[10px] text-[11px]">
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '3px', cursor: 'pointer' }}>
|
<label className="flex items-center gap-[3px] cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="prdType"
|
name="prdType"
|
||||||
checked={inputMode === 'direct'}
|
checked={inputMode === 'direct'}
|
||||||
onChange={() => setInputMode('direct')}
|
onChange={() => setInputMode('direct')}
|
||||||
style={{ accentColor: 'var(--cyan)', margin: 0, width: '11px', height: '11px' }}
|
className="m-0 w-[11px] h-[11px]"
|
||||||
|
className="accent-[var(--cyan)]"
|
||||||
/>
|
/>
|
||||||
직접 입력
|
직접 입력
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '3px', cursor: 'pointer' }}>
|
<label className="flex items-center gap-[3px] cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="prdType"
|
name="prdType"
|
||||||
checked={inputMode === 'upload'}
|
checked={inputMode === 'upload'}
|
||||||
onChange={() => setInputMode('upload')}
|
onChange={() => setInputMode('upload')}
|
||||||
style={{ accentColor: 'var(--cyan)', margin: 0, width: '11px', height: '11px' }}
|
className="m-0 w-[11px] h-[11px]"
|
||||||
|
className="accent-[var(--cyan)]"
|
||||||
/>
|
/>
|
||||||
이미지 업로드
|
이미지 업로드
|
||||||
</label>
|
</label>
|
||||||
@ -129,80 +131,55 @@ const PredictionInputSection = ({
|
|||||||
|
|
||||||
{/* Upload Success Message */}
|
{/* Upload Success Message */}
|
||||||
{uploadedImage && (
|
{uploadedImage && (
|
||||||
<div style={{
|
<div className="flex items-center gap-[6px] text-[10px] font-semibold text-[#22c55e] rounded"
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
padding: '6px 8px',
|
||||||
gap: '6px',
|
background: 'rgba(34,197,94,0.1)',
|
||||||
padding: '6px 8px',
|
border: '1px solid rgba(34,197,94,0.3)',
|
||||||
background: 'rgba(34,197,94,0.1)',
|
borderRadius: 'var(--rS)',
|
||||||
border: '1px solid rgba(34,197,94,0.3)',
|
}}>
|
||||||
borderRadius: 'var(--rS)',
|
<span className="text-[12px]">✓</span>
|
||||||
fontSize: '10px',
|
|
||||||
color: '#22c55e',
|
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
fontWeight: 600
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '12px' }}>✓</span>
|
|
||||||
내 이미지가 업로드됨
|
내 이미지가 업로드됨
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* File Upload Area */}
|
{/* File Upload Area */}
|
||||||
{!uploadedImage ? (
|
{!uploadedImage ? (
|
||||||
<label style={{
|
<label className="flex items-center justify-center text-[11px] text-text-3 cursor-pointer"
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
padding: '20px',
|
||||||
justifyContent: 'center',
|
background: 'var(--bg0)',
|
||||||
padding: '20px',
|
border: '2px dashed var(--bd)',
|
||||||
background: 'var(--bg0)',
|
borderRadius: 'var(--rS)',
|
||||||
border: '2px dashed var(--bd)',
|
transition: '0.15s',
|
||||||
borderRadius: 'var(--rS)',
|
}}
|
||||||
cursor: 'pointer',
|
onMouseEnter={(e) => {
|
||||||
transition: '0.15s',
|
e.currentTarget.style.borderColor = 'var(--cyan)'
|
||||||
fontSize: '11px',
|
e.currentTarget.style.background = 'rgba(6,182,212,0.05)'
|
||||||
color: 'var(--t3)',
|
}}
|
||||||
fontFamily: 'var(--fK)'
|
onMouseLeave={(e) => {
|
||||||
}}
|
e.currentTarget.style.borderColor = 'var(--bd)'
|
||||||
onMouseEnter={(e) => {
|
e.currentTarget.style.background = 'var(--bg0)'
|
||||||
e.currentTarget.style.borderColor = 'var(--cyan)'
|
}}>
|
||||||
e.currentTarget.style.background = 'rgba(6,182,212,0.05)'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = 'var(--bd)'
|
|
||||||
e.currentTarget.style.background = 'var(--bg0)'
|
|
||||||
}}>
|
|
||||||
📁 이미지 파일을 선택하세요
|
📁 이미지 파일을 선택하세요
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={handleImageUpload}
|
onChange={handleImageUpload}
|
||||||
style={{ display: 'none' }}
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div className="flex items-center justify-between font-mono text-[10px] bg-bg-0 border border-border"
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
padding: '8px 10px',
|
||||||
justifyContent: 'space-between',
|
borderRadius: 'var(--rS)',
|
||||||
padding: '8px 10px',
|
}}>
|
||||||
background: 'var(--bg0)',
|
<span className="text-text-2">📄 {uploadedFileName || 'example_plot_0.gif'}</span>
|
||||||
border: '1px solid var(--bd)',
|
|
||||||
borderRadius: 'var(--rS)',
|
|
||||||
fontSize: '10px',
|
|
||||||
fontFamily: 'var(--fM)'
|
|
||||||
}}>
|
|
||||||
<span style={{ color: 'var(--t2)' }}>📄 {uploadedFileName || 'example_plot_0.gif'}</span>
|
|
||||||
<button
|
<button
|
||||||
onClick={removeUploadedImage}
|
onClick={removeUploadedImage}
|
||||||
style={{
|
className="text-[10px] text-text-3 bg-transparent border-none cursor-pointer"
|
||||||
padding: '2px 6px',
|
style={{ padding: '2px 6px', transition: '0.15s' }}
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--t3)',
|
|
||||||
background: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: '0.15s'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = 'var(--red)'
|
e.currentTarget.style.color = 'var(--red)'
|
||||||
}}
|
}}
|
||||||
@ -216,7 +193,7 @@ const PredictionInputSection = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dropdowns */}
|
{/* Dropdowns */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}>
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="prd-i"
|
className="prd-i"
|
||||||
value=""
|
value=""
|
||||||
@ -244,8 +221,8 @@ const PredictionInputSection = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Coordinates + Map Button */}
|
{/* Coordinates + Map Button */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
<div className="flex flex-col gap-1">
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '4px', alignItems: 'center' }}>
|
<div className="grid items-center gap-1" style={{ gridTemplateColumns: '1fr 1fr auto' }}>
|
||||||
<input
|
<input
|
||||||
className="prd-i"
|
className="prd-i"
|
||||||
type="number"
|
type="number"
|
||||||
@ -272,22 +249,18 @@ const PredictionInputSection = ({
|
|||||||
</div>
|
</div>
|
||||||
{/* 도분초 표시 */}
|
{/* 도분초 표시 */}
|
||||||
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && (
|
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && (
|
||||||
<div style={{
|
<div className="text-[9px] text-text-3 font-mono border border-border bg-bg-0"
|
||||||
fontSize: '9px',
|
style={{
|
||||||
color: 'var(--t3)',
|
padding: '4px 8px',
|
||||||
fontFamily: 'var(--fM)',
|
borderRadius: 'var(--rS)',
|
||||||
padding: '4px 8px',
|
}}>
|
||||||
background: 'var(--bg0)',
|
|
||||||
borderRadius: 'var(--rS)',
|
|
||||||
border: '1px solid var(--bd)'
|
|
||||||
}}>
|
|
||||||
{decimalToDMS(incidentCoord.lat, true)} / {decimalToDMS(incidentCoord.lon, false)}
|
{decimalToDMS(incidentCoord.lat, true)} / {decimalToDMS(incidentCoord.lon, false)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Oil Type + Oil Kind */}
|
{/* Oil Type + Oil Kind */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}>
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className="prd-i"
|
className="prd-i"
|
||||||
value={spillType}
|
value={spillType}
|
||||||
@ -314,7 +287,7 @@ const PredictionInputSection = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Volume + Unit + Duration */}
|
{/* Volume + Unit + Duration */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 65px 1fr', gap: '4px', alignItems: 'center' }}>
|
<div className="grid items-center gap-1" style={{ gridTemplateColumns: '1fr 65px 1fr' }}>
|
||||||
<input
|
<input
|
||||||
className="prd-i"
|
className="prd-i"
|
||||||
placeholder="유출량"
|
placeholder="유출량"
|
||||||
@ -350,25 +323,22 @@ const PredictionInputSection = ({
|
|||||||
|
|
||||||
{/* Image Analysis Note (Upload Mode Only) */}
|
{/* Image Analysis Note (Upload Mode Only) */}
|
||||||
{inputMode === 'upload' && uploadedImage && (
|
{inputMode === 'upload' && uploadedImage && (
|
||||||
<div style={{
|
<div className="text-[9px] text-text-3 leading-[1.4]"
|
||||||
padding: '8px',
|
style={{
|
||||||
background: 'rgba(59,130,246,0.08)',
|
padding: '8px',
|
||||||
border: '1px solid rgba(59,130,246,0.2)',
|
background: 'rgba(59,130,246,0.08)',
|
||||||
borderRadius: 'var(--rS)',
|
border: '1px solid rgba(59,130,246,0.2)',
|
||||||
fontSize: '9px',
|
borderRadius: 'var(--rS)',
|
||||||
color: 'var(--t3)',
|
}}>
|
||||||
fontFamily: 'var(--fK)',
|
|
||||||
lineHeight: '1.4'
|
|
||||||
}}>
|
|
||||||
📊 이미지 내 확산경로를 분석하였습니다. 각 방제요소 가이드 참고하세요.
|
📊 이미지 내 확산경로를 분석하였습니다. 각 방제요소 가이드 참고하세요.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div style={{ height: '1px', background: 'var(--bd)', margin: '2px 0' }} />
|
<div className="h-px bg-border my-0.5" />
|
||||||
|
|
||||||
{/* Model Selection (다중 선택) */}
|
{/* Model Selection (다중 선택) */}
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '3px' }}>
|
<div className="flex flex-wrap gap-[3px]">
|
||||||
{([
|
{([
|
||||||
{ id: 'KOSPS' as PredictionModel, color: 'var(--cyan)' },
|
{ id: 'KOSPS' as PredictionModel, color: 'var(--cyan)' },
|
||||||
{ id: 'POSEIDON' as PredictionModel, color: 'var(--red)' },
|
{ id: 'POSEIDON' as PredictionModel, color: 'var(--red)' },
|
||||||
@ -376,7 +346,7 @@ const PredictionInputSection = ({
|
|||||||
] as const).map(m => (
|
] as const).map(m => (
|
||||||
<div
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
className={`prd-mc ${selectedModels.has(m.id) ? 'on' : ''}`}
|
className={`prd-mc ${selectedModels.has(m.id) ? 'on' : ''} cursor-pointer`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const next = new Set(selectedModels)
|
const next = new Set(selectedModels)
|
||||||
if (next.has(m.id)) {
|
if (next.has(m.id)) {
|
||||||
@ -386,14 +356,13 @@ const PredictionInputSection = ({
|
|||||||
}
|
}
|
||||||
onModelsChange(next)
|
onModelsChange(next)
|
||||||
}}
|
}}
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
>
|
>
|
||||||
<span className="prd-md" style={{ background: m.color }} />
|
<span className="prd-md" style={{ background: m.color }} />
|
||||||
{m.id}
|
{m.id}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div
|
<div
|
||||||
className={`prd-mc ${selectedModels.size === ALL_MODELS.length ? 'on' : ''}`}
|
className={`prd-mc ${selectedModels.size === ALL_MODELS.length ? 'on' : ''} cursor-pointer`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedModels.size === ALL_MODELS.length) {
|
if (selectedModels.size === ALL_MODELS.length) {
|
||||||
onModelsChange(new Set(['KOSPS']))
|
onModelsChange(new Set(['KOSPS']))
|
||||||
@ -401,7 +370,6 @@ const PredictionInputSection = ({
|
|||||||
onModelsChange(new Set(ALL_MODELS))
|
onModelsChange(new Set(ALL_MODELS))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
>
|
>
|
||||||
<span className="prd-md" style={{ background: 'var(--purple)' }} />
|
<span className="prd-md" style={{ background: 'var(--purple)' }} />
|
||||||
앙상블
|
앙상블
|
||||||
@ -410,8 +378,8 @@ const PredictionInputSection = ({
|
|||||||
|
|
||||||
{/* Run Button */}
|
{/* Run Button */}
|
||||||
<button
|
<button
|
||||||
className="prd-btn pri"
|
className="prd-btn pri mt-0.5"
|
||||||
style={{ padding: '7px', fontSize: '11px', marginTop: '2px' }}
|
style={{ padding: '7px', fontSize: '11px' }}
|
||||||
onClick={onRunSimulation}
|
onClick={onRunSimulation}
|
||||||
disabled={isRunningSimulation}
|
disabled={isRunningSimulation}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -106,42 +106,34 @@ export function RecalcModal({
|
|||||||
<div
|
<div
|
||||||
ref={backdropRef}
|
ref={backdropRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed', inset: 0, zIndex: 9999,
|
inset: 0,
|
||||||
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
|
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
|
className="fixed z-[9999] flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '380px', maxHeight: 'calc(100vh - 120px)',
|
width: '380px', maxHeight: 'calc(100vh - 120px)',
|
||||||
background: 'var(--bg1)', border: '1px solid var(--bd)',
|
background: 'var(--bg1)',
|
||||||
borderRadius: '14px', overflow: 'hidden',
|
borderRadius: '14px',
|
||||||
display: 'flex', flexDirection: 'column',
|
|
||||||
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||||||
}}>
|
}} className="border border-border overflow-hidden flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 20px', borderBottom: '1px solid var(--bd)',
|
padding: '16px 20px',
|
||||||
display: 'flex', alignItems: 'center', gap: '12px',
|
}} className="border-b border-border flex items-center gap-3">
|
||||||
}}>
|
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '36px', height: '36px', borderRadius: '10px',
|
width: '36px', height: '36px', borderRadius: '10px',
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(6,182,212,0.2))',
|
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(6,182,212,0.2))',
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
border: '1px solid rgba(249,115,22,0.3)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
}}>
|
}} className="flex items-center justify-center">
|
||||||
🔄
|
🔄
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<h2 style={{
|
<h2 className="text-[15px] font-bold m-0">
|
||||||
fontSize: '15px', fontWeight: 700, color: 'var(--t1)',
|
|
||||||
fontFamily: 'var(--fK)', margin: 0,
|
|
||||||
}}>
|
|
||||||
확산예측 재계산
|
확산예측 재계산
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{
|
<div className="text-[10px] text-text-3 mt-[2px]">
|
||||||
fontSize: '10px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px',
|
|
||||||
}}>
|
|
||||||
유출유·유출량 등 파라미터를 수정하여 재실행
|
유출유·유출량 등 파라미터를 수정하여 재실행
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -149,10 +141,10 @@ export function RecalcModal({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
style={{
|
style={{
|
||||||
width: '28px', height: '28px', borderRadius: '6px',
|
width: '28px', height: '28px', borderRadius: '6px',
|
||||||
border: '1px solid var(--bd)', background: 'var(--bg3)',
|
background: 'var(--bg3)',
|
||||||
color: 'var(--t3)', fontSize: '12px', cursor: 'pointer',
|
fontSize: '12px',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
|
className="border border-border text-text-3 cursor-pointer flex items-center justify-center"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -160,18 +152,17 @@ export function RecalcModal({
|
|||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1, overflowY: 'auto', padding: '16px 20px',
|
padding: '16px 20px',
|
||||||
display: 'flex', flexDirection: 'column', gap: '14px',
|
}} className="flex-1 overflow-y-auto flex flex-col gap-[14px]">
|
||||||
}}>
|
|
||||||
{/* 현재 분석 정보 */}
|
{/* 현재 분석 정보 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '10px 12px', background: 'rgba(6,182,212,0.04)',
|
padding: '10px 12px', background: 'rgba(6,182,212,0.04)',
|
||||||
border: '1px solid rgba(6,182,212,0.15)', borderRadius: '8px',
|
border: '1px solid rgba(6,182,212,0.15)', borderRadius: '8px',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '9px', fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: '6px' }}>
|
<div className="text-[9px] font-bold text-primary-cyan mb-1.5">
|
||||||
현재 분석 정보
|
현재 분석 정보
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px', fontSize: '9px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }} className="text-[9px]">
|
||||||
<InfoItem label="사고명" value="여수 앞바다 유류오염" />
|
<InfoItem label="사고명" value="여수 앞바다 유류오염" />
|
||||||
<InfoItem label="유종" value={initOilType} />
|
<InfoItem label="유종" value={initOilType} />
|
||||||
<InfoItem label="유출량" value={`${initSpillAmount} kl`} />
|
<InfoItem label="유출량" value={`${initSpillAmount} kl`} />
|
||||||
@ -194,13 +185,12 @@ export function RecalcModal({
|
|||||||
|
|
||||||
{/* 유출량 */}
|
{/* 유출량 */}
|
||||||
<FieldGroup label="유출량">
|
<FieldGroup label="유출량">
|
||||||
<div style={{ display: 'flex', gap: '6px' }}>
|
<div className="flex gap-1.5">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="prd-i"
|
className="prd-i flex-1"
|
||||||
value={spillAmount}
|
value={spillAmount}
|
||||||
onChange={(e) => setSpillAmount(Number(e.target.value))}
|
onChange={(e) => setSpillAmount(Number(e.target.value))}
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
className="prd-i"
|
className="prd-i"
|
||||||
@ -243,31 +233,29 @@ export function RecalcModal({
|
|||||||
|
|
||||||
{/* 유출 위치 */}
|
{/* 유출 위치 */}
|
||||||
<FieldGroup label="유출 위치 (좌표)">
|
<FieldGroup label="유출 위치 (좌표)">
|
||||||
<div style={{ display: 'flex', gap: '6px' }}>
|
<div className="flex gap-1.5">
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '3px' }}>
|
<div className="text-[8px] text-text-3 mb-[3px]">
|
||||||
위도 (N)
|
위도 (N)
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="prd-i"
|
className="prd-i font-mono"
|
||||||
value={lat}
|
value={lat}
|
||||||
step={0.0001}
|
step={0.0001}
|
||||||
onChange={(e) => setLat(Number(e.target.value))}
|
onChange={(e) => setLat(Number(e.target.value))}
|
||||||
style={{ fontFamily: 'var(--fM)' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '3px' }}>
|
<div className="text-[8px] text-text-3 mb-[3px]">
|
||||||
경도 (E)
|
경도 (E)
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="prd-i"
|
className="prd-i font-mono"
|
||||||
value={lon}
|
value={lon}
|
||||||
step={0.0001}
|
step={0.0001}
|
||||||
onChange={(e) => setLon(Number(e.target.value))}
|
onChange={(e) => setLon(Number(e.target.value))}
|
||||||
style={{ fontFamily: 'var(--fM)' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -275,7 +263,7 @@ export function RecalcModal({
|
|||||||
|
|
||||||
{/* 모델 선택 */}
|
{/* 모델 선택 */}
|
||||||
<FieldGroup label="예측 모델 선택">
|
<FieldGroup label="예측 모델 선택">
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{([
|
{([
|
||||||
{ model: 'KOSPS' as PredictionModel, color: '#3b82f6' },
|
{ model: 'KOSPS' as PredictionModel, color: '#3b82f6' },
|
||||||
{ model: 'POSEIDON' as PredictionModel, color: '#22c55e' },
|
{ model: 'POSEIDON' as PredictionModel, color: '#22c55e' },
|
||||||
@ -307,19 +295,18 @@ export function RecalcModal({
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '14px 20px', borderTop: '1px solid var(--bd)',
|
padding: '14px 20px',
|
||||||
display: 'flex', gap: '8px',
|
}} className="border-t border-border flex gap-2">
|
||||||
}}>
|
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={phase !== 'editing'}
|
disabled={phase !== 'editing'}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '10px', fontSize: '12px', fontWeight: 600,
|
padding: '10px',
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer',
|
borderRadius: '8px',
|
||||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
background: 'var(--bg3)',
|
||||||
color: 'var(--t2)',
|
|
||||||
opacity: phase !== 'editing' ? 0.5 : 1,
|
opacity: phase !== 'editing' ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
|
className="flex-1 text-[12px] font-semibold border border-border text-text-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</button>
|
</button>
|
||||||
@ -327,8 +314,9 @@ export function RecalcModal({
|
|||||||
onClick={handleRun}
|
onClick={handleRun}
|
||||||
disabled={phase !== 'editing' || models.size === 0}
|
disabled={phase !== 'editing' || models.size === 0}
|
||||||
style={{
|
style={{
|
||||||
flex: 2, padding: '10px', fontSize: '12px', fontWeight: 700,
|
padding: '10px',
|
||||||
fontFamily: 'var(--fK)', borderRadius: '8px', cursor: phase === 'editing' ? 'pointer' : 'wait',
|
borderRadius: '8px',
|
||||||
|
cursor: phase === 'editing' ? 'pointer' : 'wait',
|
||||||
background: phase === 'done'
|
background: phase === 'done'
|
||||||
? 'rgba(34,197,94,0.15)'
|
? 'rgba(34,197,94,0.15)'
|
||||||
: phase === 'running'
|
: phase === 'running'
|
||||||
@ -346,6 +334,7 @@ export function RecalcModal({
|
|||||||
: '#fff',
|
: '#fff',
|
||||||
opacity: models.size === 0 && phase === 'editing' ? 0.5 : 1,
|
opacity: models.size === 0 && phase === 'editing' ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
|
className="flex-[2] text-[12px] font-bold"
|
||||||
>
|
>
|
||||||
{phase === 'done' ? '✅ 재계산 완료!' : phase === 'running' ? '⏳ 재계산 실행중...' : '🔄 재계산 실행'}
|
{phase === 'done' ? '✅ 재계산 완료!' : phase === 'running' ? '⏳ 재계산 실행중...' : '🔄 재계산 실행'}
|
||||||
</button>
|
</button>
|
||||||
@ -358,10 +347,7 @@ export function RecalcModal({
|
|||||||
function FieldGroup({ label, children }: { label: string; children: React.ReactNode }) {
|
function FieldGroup({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div className="text-[10px] font-bold text-text-2 mb-1.5">
|
||||||
fontSize: '10px', fontWeight: 700, color: 'var(--t2)',
|
|
||||||
fontFamily: 'var(--fK)', marginBottom: '6px',
|
|
||||||
}}>
|
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
@ -371,9 +357,9 @@ function FieldGroup({ label, children }: { label: string; children: React.ReactN
|
|||||||
|
|
||||||
function InfoItem({ label, value }: { label: string; value: string }) {
|
function InfoItem({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '2px 0' }}>
|
<div className="flex justify-between py-[2px]">
|
||||||
<span style={{ color: 'var(--t3)', fontFamily: 'var(--fK)' }}>{label}</span>
|
<span className="text-text-3">{label}</span>
|
||||||
<span style={{ color: 'var(--t1)', fontWeight: 600, fontFamily: 'var(--fM)' }}>{value}</span>
|
<span className="font-semibold font-mono">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
|
|
||||||
{/* 표시 정보 제어 */}
|
{/* 표시 정보 제어 */}
|
||||||
<Section title="표시 정보 제어">
|
<Section title="표시 정보 제어">
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px 10px' }}>
|
<div className="grid grid-cols-2 gap-x-2.5 gap-y-1">
|
||||||
<CheckboxLabel checked>유향/유속</CheckboxLabel>
|
<CheckboxLabel checked>유향/유속</CheckboxLabel>
|
||||||
<CheckboxLabel checked>풍향/풍속</CheckboxLabel>
|
<CheckboxLabel checked>풍향/풍속</CheckboxLabel>
|
||||||
<CheckboxLabel>해안부착</CheckboxLabel>
|
<CheckboxLabel>해안부착</CheckboxLabel>
|
||||||
@ -42,12 +42,12 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
|
|
||||||
{/* 오염 종합 상황 */}
|
{/* 오염 종합 상황 */}
|
||||||
<Section title="오염 종합 상황" badge="위험" badgeColor="red">
|
<Section title="오염 종합 상황" badge="위험" badgeColor="red">
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2px', fontSize: '9px' }}>
|
<div className="grid grid-cols-2 gap-0.5 text-[9px]">
|
||||||
<StatBox label="유출량" value={spill?.volume != null ? spill.volume.toFixed(2) : '—'} unit={spill?.unit || 'kl'} color="var(--t1)" />
|
<StatBox label="유출량" value={spill?.volume != null ? spill.volume.toFixed(2) : '—'} unit={spill?.unit || 'kl'} color="var(--t1)" />
|
||||||
<StatBox label="풍화량" value="0.43" unit="kl" color="var(--orange)" />
|
<StatBox label="풍화량" value="0.43" unit="kl" color="var(--orange)" />
|
||||||
<StatBox label="해상잔존" value="9.57" unit="kl" color="var(--blue)" />
|
<StatBox label="해상잔존" value="9.57" unit="kl" color="var(--blue)" />
|
||||||
<StatBox label="연안부착" value="0.00" unit="kl" color="var(--red)" />
|
<StatBox label="연안부착" value="0.00" unit="kl" color="var(--red)" />
|
||||||
<div style={{ gridColumn: 'span 2' }}>
|
<div className="col-span-2">
|
||||||
<StatBox label="오염해역면적" value="8.56" unit="㎢" color="var(--cyan)" />
|
<StatBox label="오염해역면적" value="8.56" unit="㎢" color="var(--cyan)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +55,7 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
|
|
||||||
{/* 확산 예측 요약 */}
|
{/* 확산 예측 요약 */}
|
||||||
<Section title="확산 예측 요약 (+18h)" badge="위험" badgeColor="red">
|
<Section title="확산 예측 요약 (+18h)" badge="위험" badgeColor="red">
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2px' }}>
|
<div className="grid grid-cols-2 gap-0.5">
|
||||||
<PredictionCard value="4.7 km²" label="영향 면적" color="var(--red)" />
|
<PredictionCard value="4.7 km²" label="영향 면적" color="var(--red)" />
|
||||||
<PredictionCard value="6.2 km" label="최대 확산 거리" color="var(--orange)" />
|
<PredictionCard value="6.2 km" label="최대 확산 거리" color="var(--orange)" />
|
||||||
<PredictionCard value="NE 42°" label="주 확산 방향" color="var(--cyan)" />
|
<PredictionCard value="NE 42°" label="주 확산 방향" color="var(--cyan)" />
|
||||||
@ -65,7 +65,7 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
|
|
||||||
{/* 유출유 풍화 상태 */}
|
{/* 유출유 풍화 상태 */}
|
||||||
<Section title="유출유 풍화 상태">
|
<Section title="유출유 풍화 상태">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '8px' }}>
|
<div className="flex flex-col gap-[3px] text-[8px]">
|
||||||
<ProgressBar label="수면잔류" value={58} color="var(--blue)" />
|
<ProgressBar label="수면잔류" value={58} color="var(--blue)" />
|
||||||
<ProgressBar label="증발" value={22} color="var(--cyan)" />
|
<ProgressBar label="증발" value={22} color="var(--cyan)" />
|
||||||
<ProgressBar label="분산" value={12} color="var(--green)" />
|
<ProgressBar label="분산" value={12} color="var(--green)" />
|
||||||
@ -82,27 +82,14 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* 선박 카드 */}
|
{/* 선박 카드 */}
|
||||||
<div style={{
|
<div className="flex items-center gap-2 p-2 border border-[rgba(6,182,212,0.15)] rounded-md" style={{
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
padding: '8px',
|
|
||||||
background: 'linear-gradient(135deg, rgba(6,182,212,0.06), rgba(168,85,247,0.04))',
|
background: 'linear-gradient(135deg, rgba(6,182,212,0.06), rgba(168,85,247,0.04))',
|
||||||
border: '1px solid rgba(6,182,212,0.15)',
|
|
||||||
borderRadius: '6px'
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div className="w-[30px] h-[30px] rounded-md flex items-center justify-center text-[15px]" style={{
|
||||||
width: '30px',
|
|
||||||
height: '30px',
|
|
||||||
borderRadius: '6px',
|
|
||||||
background: 'rgba(6,182,212,0.1)',
|
background: 'rgba(6,182,212,0.1)',
|
||||||
border: '1px solid rgba(6,182,212,0.2)',
|
border: '1px solid rgba(6,182,212,0.2)',
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: '15px'
|
|
||||||
}}>🚢</div>
|
}}>🚢</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div className="text-[11px] font-bold text-text-1 font-korean">{vessel?.vesselNm || '—'}</div>
|
<div className="text-[11px] font-bold text-text-1 font-korean">{vessel?.vesselNm || '—'}</div>
|
||||||
<div className="text-[8px] text-text-3 font-mono">IMO {vessel?.imoNo || '—'} · {vessel?.vesselTp || '—'}</div>
|
<div className="text-[8px] text-text-3 font-mono">IMO {vessel?.imoNo || '—'} · {vessel?.vesselTp || '—'}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,7 +97,7 @@ export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 제원 */}
|
{/* 제원 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '4px' }}>
|
<div className="grid grid-cols-3 gap-1">
|
||||||
<SpecCard value={vessel?.loaM?.toFixed(1) || '—'} label="전장 LOA(m)" color="var(--purple)" />
|
<SpecCard value={vessel?.loaM?.toFixed(1) || '—'} label="전장 LOA(m)" color="var(--purple)" />
|
||||||
<SpecCard value={vessel?.breadthM?.toFixed(1) || '—'} label="형폭 B(m)" color="var(--cyan)" />
|
<SpecCard value={vessel?.breadthM?.toFixed(1) || '—'} label="형폭 B(m)" color="var(--cyan)" />
|
||||||
<SpecCard value={vessel?.draftM?.toFixed(1) || '—'} label="흘수 d(m)" color="var(--green)" />
|
<SpecCard value={vessel?.draftM?.toFixed(1) || '—'} label="흘수 d(m)" color="var(--green)" />
|
||||||
@ -240,7 +227,7 @@ function CheckboxLabel({ checked, children }: { checked?: boolean; children: str
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={checked}
|
defaultChecked={checked}
|
||||||
className="w-[13px] h-[13px]"
|
className="w-[13px] h-[13px]"
|
||||||
style={{ accentColor: 'var(--cyan)' }}
|
className="accent-[var(--cyan)]"
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</label>
|
</label>
|
||||||
@ -259,21 +246,12 @@ function StatBox({
|
|||||||
color: string
|
color: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex justify-between px-2 py-1 bg-bg-0 border border-border rounded-[3px]">
|
||||||
style={{
|
<span className="text-text-3 font-korean">
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
padding: '4px 8px',
|
|
||||||
background: 'var(--bg0)',
|
|
||||||
borderRadius: '3px',
|
|
||||||
border: '1px solid var(--bd)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ color: 'var(--t3)' }} className="font-korean">
|
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontWeight: 700, color, fontFamily: 'var(--fM)' }}>
|
<span style={{ fontWeight: 700, color, fontFamily: 'var(--fM)' }}>
|
||||||
{value} <small style={{ fontWeight: 400, color: 'var(--t3)' }}>{unit}</small>
|
{value} <small className="font-normal text-text-3">{unit}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -281,22 +259,14 @@ function StatBox({
|
|||||||
|
|
||||||
function PredictionCard({ value, label, color }: { value: string; label: string; color: string }) {
|
function PredictionCard({ value, label, color }: { value: string; label: string; color: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="text-center py-[5px] px-1 bg-bg-0 border border-border rounded-[3px]">
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '5px 4px',
|
|
||||||
background: 'var(--bg0)',
|
|
||||||
borderRadius: '3px',
|
|
||||||
border: '1px solid var(--bd)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{ fontSize: '12px', fontWeight: 800, fontFamily: 'var(--fM)', color }}
|
style={{ color }}
|
||||||
className="font-mono"
|
className="text-xs font-extrabold font-mono"
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '7px', color: 'var(--t3)' }} className="font-korean">
|
<div className="text-[7px] text-text-3 font-korean">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -305,32 +275,21 @@ function PredictionCard({ value, label, color }: { value: string; label: string;
|
|||||||
|
|
||||||
function ProgressBar({ label, value, color }: { label: string; value: number; color: string }) {
|
function ProgressBar({ label, value, color }: { label: string; value: number; color: string }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div className="flex items-center gap-1">
|
||||||
<span style={{ color: 'var(--t3)', minWidth: '38px' }} className="font-korean">
|
<span className="text-text-3 font-korean" style={{ minWidth: '38px' }}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
style={{
|
className="flex-1 h-[5px] overflow-hidden rounded-[3px]"
|
||||||
flex: 1,
|
style={{ background: 'rgba(255,255,255,0.05)' }}
|
||||||
height: '5px',
|
|
||||||
background: 'rgba(255,255,255,0.05)',
|
|
||||||
borderRadius: '3px',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{ height: '100%', width: `${value}%`, background: color, borderRadius: '3px' }}
|
style={{ height: '100%', width: `${value}%`, background: color, borderRadius: '3px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{ color, minWidth: '28px' }}
|
||||||
color,
|
className="font-semibold text-right font-mono"
|
||||||
fontWeight: 600,
|
|
||||||
minWidth: '28px',
|
|
||||||
textAlign: 'right',
|
|
||||||
fontFamily: 'var(--fM)'
|
|
||||||
}}
|
|
||||||
className="font-mono"
|
|
||||||
>
|
>
|
||||||
{value}%
|
{value}%
|
||||||
</span>
|
</span>
|
||||||
@ -365,22 +324,14 @@ function CollapsibleSection({
|
|||||||
|
|
||||||
function SpecCard({ value, label, color }: { value: string; label: string; color: string }) {
|
function SpecCard({ value, label, color }: { value: string; label: string; color: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="text-center py-[6px] px-0.5 bg-bg-0 border border-border rounded-md">
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '6px 2px',
|
|
||||||
background: 'var(--bg0)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid var(--bd)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{ fontSize: '12px', fontWeight: 800, fontFamily: 'var(--fM)', color }}
|
style={{ color }}
|
||||||
className="font-mono"
|
className="text-xs font-extrabold font-mono"
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '7px', color: 'var(--t3)' }} className="font-korean">
|
<div className="text-[7px] text-text-3 font-korean">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -399,23 +350,11 @@ function InfoRow({
|
|||||||
valueColor?: string
|
valueColor?: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex justify-between py-[3px] px-[6px] bg-bg-0 rounded-[3px]">
|
||||||
style={{
|
<span className="text-text-3">{label}</span>
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
padding: '3px 6px',
|
|
||||||
background: 'var(--bg0)',
|
|
||||||
borderRadius: '3px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ color: 'var(--t3)' }}>{label}</span>
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{ color: valueColor || 'var(--t1)' }}
|
||||||
fontWeight: 600,
|
className={`font-semibold${mono ? ' font-mono' : ''}`}
|
||||||
color: valueColor || 'var(--t1)',
|
|
||||||
fontFamily: mono ? 'var(--fM)' : undefined
|
|
||||||
}}
|
|
||||||
className={mono ? 'font-mono' : ''}
|
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
@ -454,21 +393,16 @@ function InsuranceCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="rounded-md"
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 8px',
|
padding: '6px 8px',
|
||||||
border: `1px solid ${colors.border}`,
|
border: `1px solid ${colors.border}`,
|
||||||
borderRadius: '4px',
|
|
||||||
background: colors.bg
|
background: colors.bg
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{ color: colors.text }}
|
||||||
fontSize: '8px',
|
className="text-[8px] font-bold font-korean mb-1"
|
||||||
fontWeight: 700,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: '4px'
|
|
||||||
}}
|
|
||||||
className="font-korean"
|
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
@ -476,16 +410,12 @@ function InsuranceCard({
|
|||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
style={{ display: 'flex', justifyContent: 'space-between', padding: '2px 4px' }}
|
className="flex justify-between py-0.5 px-1"
|
||||||
>
|
>
|
||||||
<span style={{ color: 'var(--t3)' }}>{item.label}</span>
|
<span className="text-text-3">{item.label}</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{ color: item.valueColor || 'var(--t1)' }}
|
||||||
fontWeight: 600,
|
className={`font-semibold${item.mono ? ' font-mono' : ''}`}
|
||||||
color: item.valueColor || 'var(--t1)',
|
|
||||||
fontFamily: item.mono ? 'var(--fM)' : undefined
|
|
||||||
}}
|
|
||||||
className={item.mono ? 'font-mono' : ''}
|
|
||||||
>
|
>
|
||||||
{item.value}
|
{item.value}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -146,7 +146,7 @@ export function createSampleReport(): OilSpillReportData {
|
|||||||
|
|
||||||
// ─── Styles ─────────────────────────────────────────────────
|
// ─── Styles ─────────────────────────────────────────────────
|
||||||
const S = {
|
const S = {
|
||||||
page: { background: 'var(--bg1)', color: 'var(--t1)', padding: '32px 40px', marginBottom: '24px', borderRadius: '6px', border: '1px solid var(--bd)', fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif", fontSize: '12px', lineHeight: '1.6', position: 'relative' as const, width: '100%', boxSizing: 'border-box' as const },
|
page: { background: 'var(--bg1)', padding: '32px 40px', marginBottom: '24px', borderRadius: '6px', border: '1px solid var(--bd)', fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif", fontSize: '12px', lineHeight: '1.6', position: 'relative' as const, width: '100%', boxSizing: 'border-box' as const },
|
||||||
sectionTitle: { background: 'rgba(6,182,212,0.12)', color: 'var(--cyan)', padding: '8px 16px', fontSize: '13px', fontWeight: 700, marginBottom: '12px', borderRadius: '4px', border: '1px solid rgba(6,182,212,0.2)' },
|
sectionTitle: { background: 'rgba(6,182,212,0.12)', color: 'var(--cyan)', padding: '8px 16px', fontSize: '13px', fontWeight: 700, marginBottom: '12px', borderRadius: '4px', border: '1px solid rgba(6,182,212,0.2)' },
|
||||||
subHeader: { fontSize: '14px', fontWeight: 700, color: 'var(--cyan)', marginBottom: '12px', borderBottom: '2px solid var(--bd)', paddingBottom: '6px' },
|
subHeader: { fontSize: '14px', fontWeight: 700, color: 'var(--cyan)', marginBottom: '12px', borderBottom: '2px solid var(--bd)', paddingBottom: '6px' },
|
||||||
table: { width: '100%', tableLayout: 'fixed' as const, borderCollapse: 'collapse' as const, fontSize: '11px', marginBottom: '16px' },
|
table: { width: '100%', tableLayout: 'fixed' as const, borderCollapse: 'collapse' as const, fontSize: '11px', marginBottom: '16px' },
|
||||||
@ -160,7 +160,7 @@ const S = {
|
|||||||
// ─── Editable cell ──────────────────────────────────────────
|
// ─── Editable cell ──────────────────────────────────────────
|
||||||
const inputStyle: React.CSSProperties = {
|
const inputStyle: React.CSSProperties = {
|
||||||
width: '100%', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '3px',
|
width: '100%', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '3px',
|
||||||
padding: '4px 8px', fontSize: '11px', color: 'var(--t1)', outline: 'none', textAlign: 'center',
|
padding: '4px 8px', fontSize: '11px', outline: 'none', textAlign: 'center',
|
||||||
}
|
}
|
||||||
|
|
||||||
function ECell({ value, editing, onChange, align, placeholder }: {
|
function ECell({ value, editing, onChange, align, placeholder }: {
|
||||||
@ -184,10 +184,7 @@ function ECell({ value, editing, onChange, align, placeholder }: {
|
|||||||
|
|
||||||
function AddRowBtn({ onClick, label }: { onClick: () => void; label?: string }) {
|
function AddRowBtn({ onClick, label }: { onClick: () => void; label?: string }) {
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} style={{
|
<button onClick={onClick} className="px-3 py-1 text-[10px] font-semibold text-primary-cyan bg-[rgba(6,182,212,0.08)] border border-dashed border-primary-cyan rounded-sm cursor-pointer mb-3">
|
||||||
padding: '4px 12px', fontSize: '10px', fontWeight: 600, color: '#06b6d4', background: 'rgba(6,182,212,0.08)',
|
|
||||||
border: '1px dashed #06b6d4', borderRadius: '3px', cursor: 'pointer', marginBottom: '12px',
|
|
||||||
}}>
|
|
||||||
+ {label || '행 추가'}
|
+ {label || '행 추가'}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@ -200,8 +197,8 @@ function Page1({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={S.page}>
|
<div style={S.page}>
|
||||||
<div style={{ position: 'absolute', top: 10, right: 16, fontSize: '9px', color: 'var(--t3)', fontWeight: 600 }}>해양오염방제지원시스템</div>
|
<div className="absolute top-2.5 right-4 text-[9px] text-text-3 font-semibold">해양오염방제지원시스템</div>
|
||||||
<div style={{ background: 'rgba(6,182,212,0.1)', color: 'var(--cyan)', padding: '12px 20px', fontSize: '18px', fontWeight: 700, marginBottom: '20px', borderRadius: '4px', letterSpacing: '1px', textAlign: 'center', border: '1px solid rgba(6,182,212,0.2)' }}>
|
<div className="text-primary-cyan text-[18px] font-bold mb-5 rounded px-5 py-3 text-center tracking-wide border" style={{ background: 'rgba(6,182,212,0.1)', border: '1px solid rgba(6,182,212,0.2)' }}>
|
||||||
유류오염사고 대응지원 상황도
|
유류오염사고 대응지원 상황도
|
||||||
</div>
|
</div>
|
||||||
<div style={S.sectionTitle}>1. 사고 정보</div>
|
<div style={S.sectionTitle}>1. 사고 정보</div>
|
||||||
@ -227,7 +224,7 @@ function Page2({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={S.page}>
|
<div style={S.page}>
|
||||||
<div style={{ position: 'absolute', top: 10, right: 16, fontSize: '9px', color: 'var(--t3)', fontWeight: 600 }}>해양오염방제지원시스템</div>
|
<div className="absolute top-2.5 right-4 text-[9px] text-text-3 font-semibold">해양오염방제지원시스템</div>
|
||||||
<div style={S.sectionTitle}>2. 해양기상정보</div>
|
<div style={S.sectionTitle}>2. 해양기상정보</div>
|
||||||
<div style={S.subHeader}>조석 정보</div>
|
<div style={S.subHeader}>조석 정보</div>
|
||||||
<table style={S.table}>
|
<table style={S.table}>
|
||||||
@ -268,7 +265,7 @@ function Page2({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
{editing && <AddRowBtn onClick={() => onChange({ ...data, weather: [...data.weather, { time: '', sunrise: '', sunset: '', windDir: '', windSpeed: '', currentDir: '', currentSpeed: '', waveHeight: '' }] })} />}
|
{editing && <AddRowBtn onClick={() => onChange({ ...data, weather: [...data.weather, { time: '', sunrise: '', sunset: '', windDir: '', windSpeed: '', currentDir: '', currentSpeed: '', waveHeight: '' }] })} />}
|
||||||
|
|
||||||
<div style={S.sectionTitle}>3. 유출유 확산예측</div>
|
<div style={S.sectionTitle}>3. 유출유 확산예측</div>
|
||||||
<div style={{ display: 'flex', gap: '16px', marginBottom: '16px' }}>
|
<div className="flex gap-4 mb-4">
|
||||||
<div style={S.mapPlaceholder}>확산예측 3시간 지도</div>
|
<div style={S.mapPlaceholder}>확산예측 3시간 지도</div>
|
||||||
<div style={S.mapPlaceholder}>확산예측 6시간 지도</div>
|
<div style={S.mapPlaceholder}>확산예측 6시간 지도</div>
|
||||||
</div>
|
</div>
|
||||||
@ -295,14 +292,14 @@ function Page2({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
function Page3({ data, editing, onChange }: { data: OilSpillReportData; editing: boolean; onChange: (d: OilSpillReportData) => void }) {
|
function Page3({ data, editing, onChange }: { data: OilSpillReportData; editing: boolean; onChange: (d: OilSpillReportData) => void }) {
|
||||||
return (
|
return (
|
||||||
<div style={S.page}>
|
<div style={S.page}>
|
||||||
<div style={{ position: 'absolute', top: 10, right: 16, fontSize: '9px', color: 'var(--t3)', fontWeight: 600 }}>해양오염방제지원시스템</div>
|
<div className="absolute top-2.5 right-4 text-[9px] text-text-3 font-semibold">해양오염방제지원시스템</div>
|
||||||
<div style={S.sectionTitle}>분석</div>
|
<div style={S.sectionTitle}>분석</div>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<textarea
|
<textarea
|
||||||
value={data.analysis}
|
value={data.analysis}
|
||||||
onChange={e => onChange({ ...data, analysis: e.target.value })}
|
onChange={e => onChange({ ...data, analysis: e.target.value })}
|
||||||
placeholder="분석 내용을 작성하세요..."
|
placeholder="분석 내용을 작성하세요..."
|
||||||
style={{ width: '100%', minHeight: '300px', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '4px', padding: '16px', fontSize: '13px', color: 'var(--t1)', outline: 'none', resize: 'vertical', lineHeight: '1.8' }}
|
style={{ width: '100%', minHeight: '300px', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '4px', padding: '16px', fontSize: '13px', outline: 'none', resize: 'vertical', lineHeight: '1.8' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ minHeight: '300px', border: data.analysis ? '1px solid var(--bd)' : '2px dashed var(--bd)', borderRadius: '4px', padding: '16px', color: data.analysis ? 'var(--t1)' : 'var(--t3)', fontStyle: data.analysis ? 'normal' : 'italic', fontSize: '13px', whiteSpace: 'pre-wrap', lineHeight: '1.8' }}>
|
<div style={{ minHeight: '300px', border: data.analysis ? '1px solid var(--bd)' : '2px dashed var(--bd)', borderRadius: '4px', padding: '16px', color: data.analysis ? 'var(--t1)' : 'var(--t3)', fontStyle: data.analysis ? 'normal' : 'italic', fontSize: '13px', whiteSpace: 'pre-wrap', lineHeight: '1.8' }}>
|
||||||
@ -320,7 +317,7 @@ function Page4({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={S.page}>
|
<div style={S.page}>
|
||||||
<div style={{ position: 'absolute', top: 10, right: 16, fontSize: '9px', color: 'var(--t3)', fontWeight: 600 }}>해양오염방제지원시스템</div>
|
<div className="absolute top-2.5 right-4 text-[9px] text-text-3 font-semibold">해양오염방제지원시스템</div>
|
||||||
<div style={S.sectionTitle}>4. 민감자원 및 민감도 평가</div>
|
<div style={S.sectionTitle}>4. 민감자원 및 민감도 평가</div>
|
||||||
<div style={S.mapPlaceholder}>민감자원 분포(10km 내) 지도</div>
|
<div style={S.mapPlaceholder}>민감자원 분포(10km 내) 지도</div>
|
||||||
|
|
||||||
@ -337,7 +334,7 @@ function Page4({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
</table>
|
</table>
|
||||||
{editing && <AddRowBtn onClick={() => onChange({ ...data, aquaculture: [...data.aquaculture, { type: '', area: '', distance: '' }] })} />}
|
{editing && <AddRowBtn onClick={() => onChange({ ...data, aquaculture: [...data.aquaculture, { type: '', area: '', distance: '' }] })} />}
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
<div className="grid grid-cols-2 gap-5">
|
||||||
<div>
|
<div>
|
||||||
<div style={S.subHeader}>해수욕장 분포</div>
|
<div style={S.subHeader}>해수욕장 분포</div>
|
||||||
<table style={S.table}>
|
<table style={S.table}>
|
||||||
@ -366,7 +363,7 @@ function Page4({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px', marginTop: '8px' }}>
|
<div className="grid grid-cols-2 gap-5 mt-2">
|
||||||
<div>
|
<div>
|
||||||
<div style={S.subHeader}>해안선(ESI) 분포</div>
|
<div style={S.subHeader}>해안선(ESI) 분포</div>
|
||||||
<table style={S.table}>
|
<table style={S.table}>
|
||||||
@ -411,7 +408,7 @@ function Page5({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
const setSens = (i: number, v: string) => { const s = [...data.sensitivity]; s[i] = { ...s[i], area: v }; onChange({ ...data, sensitivity: s }) }
|
const setSens = (i: number, v: string) => { const s = [...data.sensitivity]; s[i] = { ...s[i], area: v }; onChange({ ...data, sensitivity: s }) }
|
||||||
return (
|
return (
|
||||||
<div style={S.page}>
|
<div style={S.page}>
|
||||||
<div style={{ position: 'absolute', top: 10, right: 16, fontSize: '9px', color: 'var(--t3)', fontWeight: 600 }}>해양오염방제지원시스템</div>
|
<div className="absolute top-2.5 right-4 text-[9px] text-text-3 font-semibold">해양오염방제지원시스템</div>
|
||||||
<div style={S.sectionTitle}>통합민감도 평가 (해당 계절)</div>
|
<div style={S.sectionTitle}>통합민감도 평가 (해당 계절)</div>
|
||||||
<div style={S.mapPlaceholder}>통합민감도 평가 지도</div>
|
<div style={S.mapPlaceholder}>통합민감도 평가 지도</div>
|
||||||
<table style={S.table}>
|
<table style={S.table}>
|
||||||
@ -431,10 +428,10 @@ function Page6({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
const setVessel = (i: number, k: string, v: string) => { const vs = [...data.vessels]; vs[i] = { ...vs[i], [k]: v }; onChange({ ...data, vessels: vs }) }
|
const setVessel = (i: number, k: string, v: string) => { const vs = [...data.vessels]; vs[i] = { ...vs[i], [k]: v }; onChange({ ...data, vessels: vs }) }
|
||||||
return (
|
return (
|
||||||
<div style={S.page}>
|
<div style={S.page}>
|
||||||
<div style={{ position: 'absolute', top: 10, right: 16, fontSize: '9px', color: 'var(--t3)', fontWeight: 600 }}>해양오염방제지원시스템</div>
|
<div className="absolute top-2.5 right-4 text-[9px] text-text-3 font-semibold">해양오염방제지원시스템</div>
|
||||||
<div style={S.sectionTitle}>5. 방제전략 수립·실행</div>
|
<div style={S.sectionTitle}>5. 방제전략 수립·실행</div>
|
||||||
<div style={S.subHeader}>방제자원 배치 현황 (반경 30km내)</div>
|
<div style={S.subHeader}>방제자원 배치 현황 (반경 30km내)</div>
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div className="overflow-x-auto">
|
||||||
<table style={S.table}>
|
<table style={S.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th style={S.th} rowSpan={2}>#</th><th style={S.th} rowSpan={2}>선명</th><th style={S.th} rowSpan={2}>소속</th><th style={S.th} rowSpan={2}>거리(km)</th><th style={S.th} rowSpan={2}>속력(knots)</th><th style={S.th} rowSpan={2}>크기(총톤수)</th><th style={S.th} colSpan={2}>유회수기</th><th style={S.th} colSpan={2}>오일펜스</th></tr>
|
<tr><th style={S.th} rowSpan={2}>#</th><th style={S.th} rowSpan={2}>선명</th><th style={S.th} rowSpan={2}>소속</th><th style={S.th} rowSpan={2}>거리(km)</th><th style={S.th} rowSpan={2}>속력(knots)</th><th style={S.th} rowSpan={2}>크기(총톤수)</th><th style={S.th} colSpan={2}>유회수기</th><th style={S.th} colSpan={2}>오일펜스</th></tr>
|
||||||
@ -459,7 +456,7 @@ function Page6({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
{editing && <AddRowBtn onClick={() => onChange({ ...data, vessels: [...data.vessels, { name: '', org: '', dist: '', speed: '', ton: '', collectorType: '', collectorCap: '', boomType: '', boomLength: '' }] })} />}
|
{editing && <AddRowBtn onClick={() => onChange({ ...data, vessels: [...data.vessels, { name: '', org: '', dist: '', speed: '', ton: '', collectorType: '', collectorCap: '', boomType: '', boomLength: '' }] })} />}
|
||||||
<div style={S.subHeader}>기타 장비</div>
|
<div style={S.subHeader}>기타 장비</div>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<textarea value={data.etcEquipment} onChange={e => onChange({ ...data, etcEquipment: e.target.value })} placeholder="기타 장비 입력" style={{ width: '100%', minHeight: '60px', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '4px', padding: '12px', fontSize: '12px', color: 'var(--t1)', outline: 'none', resize: 'vertical' }} />
|
<textarea value={data.etcEquipment} onChange={e => onChange({ ...data, etcEquipment: e.target.value })} placeholder="기타 장비 입력" style={{ width: '100%', minHeight: '60px', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '4px', padding: '12px', fontSize: '12px', outline: 'none', resize: 'vertical' }} />
|
||||||
) : (
|
) : (
|
||||||
<div style={{ minHeight: '60px', border: data.etcEquipment ? '1px solid var(--bd)' : '2px dashed var(--bd)', borderRadius: '4px', padding: '12px', color: data.etcEquipment ? 'var(--t1)' : 'var(--t3)', fontStyle: data.etcEquipment ? 'normal' : 'italic', fontSize: '12px' }}>{data.etcEquipment || '-'}</div>
|
<div style={{ minHeight: '60px', border: data.etcEquipment ? '1px solid var(--bd)' : '2px dashed var(--bd)', borderRadius: '4px', padding: '12px', color: data.etcEquipment ? 'var(--t1)' : 'var(--t3)', fontStyle: data.etcEquipment ? 'normal' : 'italic', fontSize: '12px' }}>{data.etcEquipment || '-'}</div>
|
||||||
)}
|
)}
|
||||||
@ -472,7 +469,7 @@ function Page7({ data, editing, onChange }: { data: OilSpillReportData; editing:
|
|||||||
const setRes = (k: string, v: string) => onChange({ ...data, result: { ...data.result, [k]: v } })
|
const setRes = (k: string, v: string) => onChange({ ...data, result: { ...data.result, [k]: v } })
|
||||||
return (
|
return (
|
||||||
<div style={S.page}>
|
<div style={S.page}>
|
||||||
<div style={{ position: 'absolute', top: 10, right: 16, fontSize: '9px', color: 'var(--t3)', fontWeight: 600 }}>해양오염방제지원시스템</div>
|
<div className="absolute top-2.5 right-4 text-[9px] text-text-3 font-semibold">해양오염방제지원시스템</div>
|
||||||
<div style={S.sectionTitle}>방제선/자원 동원 결과</div>
|
<div style={S.sectionTitle}>방제선/자원 동원 결과</div>
|
||||||
<div style={S.mapPlaceholder}>방제선/자원 동원 결과 지도</div>
|
<div style={S.mapPlaceholder}>방제선/자원 동원 결과 지도</div>
|
||||||
<div style={S.subHeader}>기름회수량</div>
|
<div style={S.subHeader}>기름회수량</div>
|
||||||
@ -546,57 +543,57 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%' }}>
|
<div className="w-full">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}>
|
<div className="flex items-center justify-between mb-5 flex-wrap gap-3">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div className="flex items-center gap-2.5">
|
||||||
{onBack && <button onClick={onBack} style={{ padding: '6px 12px', fontSize: '12px', fontWeight: 600, color: 'var(--t2)', background: 'none', border: 'none', cursor: 'pointer' }}>← 돌아가기</button>}
|
{onBack && <button onClick={onBack} className="px-3 py-1.5 text-[12px] font-semibold text-text-2 bg-transparent border-none cursor-pointer">← 돌아가기</button>}
|
||||||
<h2 style={{ fontSize: '18px', fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>
|
<h2 className="text-[18px] font-bold">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<input
|
<input
|
||||||
value={data.title}
|
value={data.title}
|
||||||
onChange={e => setData({ ...data, title: e.target.value })}
|
onChange={e => setData({ ...data, title: e.target.value })}
|
||||||
placeholder="보고서 제목 입력"
|
placeholder="보고서 제목 입력"
|
||||||
style={{ fontSize: '18px', fontWeight: 700, color: 'var(--t1)', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '4px', padding: '4px 10px', outline: 'none', width: '100%', maxWidth: '600px' }}
|
className="text-[18px] font-bold bg-bg-0 border border-[var(--bdL)] rounded px-2.5 py-1 outline-none w-full max-w-[600px]"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
data.title || '유류오염사고 대응지원 상황도'
|
data.title || '유류오염사고 대응지원 상황도'
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<span style={{ padding: '3px 10px', fontSize: '10px', fontWeight: 600, borderRadius: '4px', border: '1px solid', ...( editing ? { background: 'rgba(251,191,36,0.15)', color: '#f59e0b', borderColor: 'rgba(251,191,36,0.3)' } : { background: 'rgba(6,182,212,0.15)', color: 'var(--cyan)', borderColor: 'rgba(6,182,212,0.3)' }) }}>
|
<span className="px-2.5 py-[3px] text-[10px] font-semibold rounded border" style={editing ? { background: 'rgba(251,191,36,0.15)', color: '#f59e0b', borderColor: 'rgba(251,191,36,0.3)' } : { background: 'rgba(6,182,212,0.15)', color: 'var(--cyan)', borderColor: 'rgba(6,182,212,0.3)' }}>
|
||||||
{editing ? '편집 중' : mode === 'preview' ? '샘플' : '보기'}
|
{editing ? '편집 중' : mode === 'preview' ? '샘플' : '보기'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={() => setViewMode('all')} style={{ padding: '6px 14px', fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fK)', borderRadius: '4px', border: viewMode === 'all' ? '1px solid var(--cyan)' : '1px solid var(--bd)', background: viewMode === 'all' ? 'rgba(6,182,212,0.1)' : 'var(--bg2)', color: viewMode === 'all' ? 'var(--cyan)' : 'var(--t3)', cursor: 'pointer' }}>전체 보기</button>
|
<button onClick={() => setViewMode('all')} className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer" style={{ border: viewMode === 'all' ? '1px solid var(--cyan)' : '1px solid var(--bd)', background: viewMode === 'all' ? 'rgba(6,182,212,0.1)' : 'var(--bg2)', color: viewMode === 'all' ? 'var(--cyan)' : 'var(--t3)' }}>전체 보기</button>
|
||||||
<button onClick={() => setViewMode('page')} style={{ padding: '6px 14px', fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fK)', borderRadius: '4px', border: viewMode === 'page' ? '1px solid var(--cyan)' : '1px solid var(--bd)', background: viewMode === 'page' ? 'rgba(6,182,212,0.1)' : 'var(--bg2)', color: viewMode === 'page' ? 'var(--cyan)' : 'var(--t3)', cursor: 'pointer' }}>페이지별</button>
|
<button onClick={() => setViewMode('page')} className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer" style={{ border: viewMode === 'page' ? '1px solid var(--cyan)' : '1px solid var(--bd)', background: viewMode === 'page' ? 'rgba(6,182,212,0.1)' : 'var(--bg2)', color: viewMode === 'page' ? 'var(--cyan)' : 'var(--t3)' }}>페이지별</button>
|
||||||
{editing && (
|
{editing && (
|
||||||
<button onClick={handleSave} style={{ padding: '6px 16px', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)', borderRadius: '4px', border: '1px solid #22c55e', background: 'rgba(34,197,94,0.15)', color: '#22c55e', cursor: 'pointer' }}>저장</button>
|
<button onClick={handleSave} className="px-4 py-1.5 text-[11px] font-bold rounded cursor-pointer border border-[#22c55e] bg-[rgba(34,197,94,0.15)] text-status-green">저장</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => window.print()} style={{ padding: '6px 14px', fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fK)', borderRadius: '4px', border: '1px solid var(--red)', background: 'rgba(239,68,68,0.1)', color: '#ef4444', cursor: 'pointer' }}>인쇄 / PDF</button>
|
<button onClick={() => window.print()} className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer border border-[var(--red)] bg-[rgba(239,68,68,0.1)] text-status-red">인쇄 / PDF</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Page tabs */}
|
{/* Page tabs */}
|
||||||
{viewMode === 'page' && (
|
{viewMode === 'page' && (
|
||||||
<div style={{ display: 'flex', gap: '4px', marginBottom: '16px', flexWrap: 'wrap' }}>
|
<div className="flex gap-1 mb-4 flex-wrap">
|
||||||
{pages.map((p, i) => (
|
{pages.map((p, i) => (
|
||||||
<button key={i} onClick={() => setCurrentPage(i)} style={{ padding: '6px 12px', fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fK)', borderRadius: '4px', border: currentPage === i ? '1px solid var(--cyan)' : '1px solid var(--bd)', background: currentPage === i ? 'rgba(6,182,212,0.15)' : 'transparent', color: currentPage === i ? 'var(--cyan)' : 'var(--t3)', cursor: 'pointer' }}>{p.label}</button>
|
<button key={i} onClick={() => setCurrentPage(i)} className="px-3 py-1.5 text-[11px] font-semibold rounded cursor-pointer" style={{ border: currentPage === i ? '1px solid var(--cyan)' : '1px solid var(--bd)', background: currentPage === i ? 'rgba(6,182,212,0.15)' : 'transparent', color: currentPage === i ? 'var(--cyan)' : 'var(--t3)' }}>{p.label}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pages */}
|
{/* Pages */}
|
||||||
<div id="report-print-area" style={{ width: '100%' }}>
|
<div id="report-print-area" className="w-full">
|
||||||
{viewMode === 'all'
|
{viewMode === 'all'
|
||||||
? pages.map((p, i) => <div key={i}>{p.node}</div>)
|
? pages.map((p, i) => <div key={i}>{p.node}</div>)
|
||||||
: (
|
: (
|
||||||
<div>
|
<div>
|
||||||
{pages[currentPage].node}
|
{pages[currentPage].node}
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '16px' }}>
|
<div className="flex justify-center gap-2 mt-4">
|
||||||
<button onClick={() => setCurrentPage(p => Math.max(0, p - 1))} disabled={currentPage === 0} style={{ padding: '8px 20px', fontSize: '12px', fontWeight: 600, borderRadius: '4px', border: '1px solid var(--bd)', background: 'var(--bg2)', color: currentPage === 0 ? 'var(--t3)' : 'var(--t1)', cursor: currentPage === 0 ? 'default' : 'pointer', opacity: currentPage === 0 ? 0.4 : 1 }}>이전</button>
|
<button onClick={() => setCurrentPage(p => Math.max(0, p - 1))} disabled={currentPage === 0} className="px-5 py-2 text-[12px] font-semibold rounded border border-border bg-bg-2 cursor-pointer" style={{ color: currentPage === 0 ? 'var(--t3)' : 'var(--t1)', opacity: currentPage === 0 ? 0.4 : 1 }}>이전</button>
|
||||||
<span style={{ padding: '8px 16px', fontSize: '12px', color: 'var(--t2)' }}>{currentPage + 1} / {pages.length}</span>
|
<span className="px-4 py-2 text-[12px] text-text-2">{currentPage + 1} / {pages.length}</span>
|
||||||
<button onClick={() => setCurrentPage(p => Math.min(pages.length - 1, p + 1))} disabled={currentPage === pages.length - 1} style={{ padding: '8px 20px', fontSize: '12px', fontWeight: 600, borderRadius: '4px', border: '1px solid var(--cyan)', background: 'rgba(6,182,212,0.1)', color: currentPage === pages.length - 1 ? 'var(--t3)' : 'var(--cyan)', cursor: currentPage === pages.length - 1 ? 'default' : 'pointer', opacity: currentPage === pages.length - 1 ? 0.4 : 1 }}>다음</button>
|
<button onClick={() => setCurrentPage(p => Math.min(pages.length - 1, p + 1))} disabled={currentPage === pages.length - 1} className="px-5 py-2 text-[12px] font-semibold rounded cursor-pointer" style={{ border: '1px solid var(--cyan)', background: 'rgba(6,182,212,0.1)', color: currentPage === pages.length - 1 ? 'var(--t3)' : 'var(--cyan)', opacity: currentPage === pages.length - 1 ? 0.4 : 1 }}>다음</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -87,28 +87,24 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
<p className="text-[11px] text-text-3 font-korean mt-1">보고서 유형을 선택하고 포함할 섹션을 구성하여 보고서를 생성합니다.</p>
|
<p className="text-[11px] text-text-3 font-korean mt-1">보고서 유형을 선택하고 포함할 섹션을 구성하여 보고서를 생성합니다.</p>
|
||||||
|
|
||||||
{/* 3 카테고리 카드 */}
|
{/* 3 카테고리 카드 */}
|
||||||
<div style={{ display: 'flex', gap: '14px', marginTop: '16px' }}>
|
<div className="flex gap-3.5 mt-4">
|
||||||
{CATEGORIES.map((c, i) => {
|
{CATEGORIES.map((c, i) => {
|
||||||
const isActive = activeCat === i
|
const isActive = activeCat === i
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => { setActiveCat(i as ReportCategory); setSelectedTemplate(0) }}
|
onClick={() => { setActiveCat(i as ReportCategory); setSelectedTemplate(0) }}
|
||||||
|
className="flex-1 px-4 py-3.5 rounded-[10px] cursor-pointer text-center transition-[0.2s]"
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: '14px 16px', borderRadius: '10px', cursor: 'pointer',
|
|
||||||
textAlign: 'center', transition: '0.2s',
|
|
||||||
border: `1px solid ${isActive ? c.borderColor : 'var(--bd)'}`,
|
border: `1px solid ${isActive ? c.borderColor : 'var(--bd)'}`,
|
||||||
background: isActive ? c.bgActive : 'var(--bg3)',
|
background: isActive ? c.bgActive : 'var(--bg3)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '22px', marginBottom: '4px' }}>{c.icon}</div>
|
<div className="text-[22px] mb-1">{c.icon}</div>
|
||||||
<div style={{
|
<div className="text-[12px] font-bold" style={{ color: isActive ? c.color : 'var(--t3)' }}>
|
||||||
fontSize: '12px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
||||||
color: isActive ? c.color : 'var(--t3)',
|
|
||||||
}}>
|
|
||||||
{c.label}
|
{c.label}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>
|
<div className="text-[9px] text-text-3 mt-0.5">
|
||||||
{c.desc}
|
{c.desc}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -135,11 +131,8 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
background: selectedTemplate === i ? cat.bgActive : 'var(--bg2)',
|
background: selectedTemplate === i ? cat.bgActive : 'var(--bg2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: '14px' }}>{tmpl.icon}</span>
|
<span className="text-[14px]">{tmpl.icon}</span>
|
||||||
<span style={{
|
<span className="text-[11px] font-semibold" style={{ color: selectedTemplate === i ? cat.color : 'var(--t2)' }}>
|
||||||
fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fK)',
|
|
||||||
color: selectedTemplate === i ? cat.color : 'var(--t2)',
|
|
||||||
}}>
|
|
||||||
{tmpl.label}
|
{tmpl.label}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -162,26 +155,24 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
opacity: sec.checked ? 1 : 0.55,
|
opacity: sec.checked ? 1 : 0.55,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div
|
||||||
width: '18px', height: '18px', borderRadius: '4px', flexShrink: 0, marginTop: '1px',
|
className="w-[18px] h-[18px] rounded shrink-0 mt-[1px] flex items-center justify-center text-[10px]"
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px',
|
style={{
|
||||||
background: sec.checked ? cat.color : 'var(--bg3)',
|
background: sec.checked ? cat.color : 'var(--bg3)',
|
||||||
color: sec.checked ? '#fff' : 'transparent',
|
color: sec.checked ? '#fff' : 'transparent',
|
||||||
border: sec.checked ? 'none' : '1px solid var(--bd)',
|
border: sec.checked ? 'none' : '1px solid var(--bd)',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
✓
|
✓
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1 min-w-0">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div className="flex items-center gap-1">
|
||||||
<span style={{ fontSize: '12px' }}>{sec.icon}</span>
|
<span className="text-[12px]">{sec.icon}</span>
|
||||||
<span style={{
|
<span className="text-[11px] font-bold" style={{ color: sec.checked ? 'var(--t1)' : 'var(--t3)' }}>
|
||||||
fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
||||||
color: sec.checked ? 'var(--t1)' : 'var(--t3)',
|
|
||||||
}}>
|
|
||||||
{sec.title}
|
{sec.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>
|
<p className="text-[9px] text-text-3 mt-0.5">
|
||||||
{sec.desc}
|
{sec.desc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -196,10 +187,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
<div className="flex items-center justify-between px-6 py-3 border-b border-border bg-bg-1">
|
<div className="flex items-center justify-between px-6 py-3 border-b border-border bg-bg-1">
|
||||||
<h3 className="text-[13px] font-bold text-text-1 font-korean flex items-center gap-2">
|
<h3 className="text-[13px] font-bold text-text-1 font-korean flex items-center gap-2">
|
||||||
📄 보고서 미리보기
|
📄 보고서 미리보기
|
||||||
<span style={{
|
<span className="text-[10px] font-semibold px-2 py-0.5 rounded" style={{ background: cat.bgActive, color: cat.color }}>
|
||||||
fontSize: '10px', fontWeight: 600, padding: '2px 8px', borderRadius: '4px',
|
|
||||||
background: cat.bgActive, color: cat.color, fontFamily: 'var(--fK)',
|
|
||||||
}}>
|
|
||||||
{cat.templates[selectedTemplate].label}
|
{cat.templates[selectedTemplate].label}
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
@ -223,7 +211,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
{/* Preview Content */}
|
{/* Preview Content */}
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
{/* Report Header */}
|
{/* Report Header */}
|
||||||
<div className="rounded-lg border border-border p-8 mb-6" style={{ background: 'var(--bg2)' }}>
|
<div className="rounded-lg border border-border p-8 mb-6 bg-bg-2">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-[10px] text-text-3 font-korean mb-2">해양환경 위기대응 통합지원시스템</p>
|
<p className="text-[10px] text-text-3 font-korean mb-2">해양환경 위기대응 통합지원시스템</p>
|
||||||
<h2 className="text-[20px] font-bold text-text-1 font-korean mb-2">{cat.reportName}</h2>
|
<h2 className="text-[20px] font-bold text-text-1 font-korean mb-2">{cat.reportName}</h2>
|
||||||
@ -233,7 +221,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
|
|
||||||
{/* Dynamic Sections */}
|
{/* Dynamic Sections */}
|
||||||
{activeSections.map(sec => (
|
{activeSections.map(sec => (
|
||||||
<div key={sec.id} className="rounded-lg border border-border mb-4 overflow-hidden" style={{ background: 'var(--bg2)' }}>
|
<div key={sec.id} className="rounded-lg border border-border mb-4 overflow-hidden bg-bg-2">
|
||||||
<div className="px-5 py-3 border-b border-border">
|
<div className="px-5 py-3 border-b border-border">
|
||||||
<h4 className="text-[13px] font-bold text-text-1 font-korean flex items-center gap-2">
|
<h4 className="text-[13px] font-bold text-text-1 font-korean flex items-center gap-2">
|
||||||
{sec.icon} {sec.title}
|
{sec.icon} {sec.title}
|
||||||
@ -261,7 +249,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{sec.id === 'oil-pollution' && (
|
{sec.id === 'oil-pollution' && (
|
||||||
<table className="w-full table-fixed" style={{ borderCollapse: 'collapse' }}>
|
<table className="w-full table-fixed" className="border-collapse">
|
||||||
<colgroup><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /></colgroup>
|
<colgroup><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /></colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
@ -339,7 +327,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
{ label: '대피 권고 범위', value: sampleHnsData.hazard.evacuation, color: '#a855f7', desc: '안전거리' },
|
{ label: '대피 권고 범위', value: sampleHnsData.hazard.evacuation, color: '#a855f7', desc: '안전거리' },
|
||||||
].map((h, i) => (
|
].map((h, i) => (
|
||||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
||||||
<p className="text-[9px] font-korean mb-1" style={{ color: h.color, fontWeight: 700 }}>{h.label}</p>
|
<p className="text-[9px] font-bold font-korean mb-1" style={{ color: h.color }}>{h.label}</p>
|
||||||
<p className="text-[18px] font-bold font-mono" style={{ color: h.color }}>{h.value}</p>
|
<p className="text-[18px] font-bold font-mono" style={{ color: h.color }}>{h.value}</p>
|
||||||
<p className="text-[8px] text-text-3 font-korean mt-1">{h.desc}</p>
|
<p className="text-[8px] text-text-3 font-korean mt-1">{h.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -347,7 +335,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sec.id === 'hns-substance' && (
|
{sec.id === 'hns-substance' && (
|
||||||
<div className="grid grid-cols-2 gap-2" style={{ fontSize: '11px' }}>
|
<div className="grid grid-cols-2 gap-2 text-[11px]">
|
||||||
{[
|
{[
|
||||||
{ k: '물질명', v: sampleHnsData.substance.name },
|
{ k: '물질명', v: sampleHnsData.substance.name },
|
||||||
{ k: 'UN번호', v: sampleHnsData.substance.un },
|
{ k: 'UN번호', v: sampleHnsData.substance.un },
|
||||||
@ -359,9 +347,9 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
<span className="text-text-1 font-semibold font-mono">{r.v}</span>
|
<span className="text-text-1 font-semibold font-mono">{r.v}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="col-span-2 flex justify-between px-3 py-2 bg-bg-1 rounded border border-border" style={{ borderColor: 'rgba(239,68,68,0.3)' }}>
|
<div className="col-span-2 flex justify-between px-3 py-2 bg-bg-1 rounded border border-[rgba(239,68,68,0.3)]">
|
||||||
<span className="text-text-3 font-korean">독성기준</span>
|
<span className="text-text-3 font-korean">독성기준</span>
|
||||||
<span style={{ color: 'var(--red)', fontWeight: 600, fontFamily: 'var(--fM)', fontSize: '10px' }}>{sampleHnsData.substance.toxicity}</span>
|
<span className="text-[var(--red)] font-semibold font-mono text-[10px]">{sampleHnsData.substance.toxicity}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -382,7 +370,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
{ label: '주변 인구', value: sampleHnsData.facility.population, icon: '👥' },
|
{ label: '주변 인구', value: sampleHnsData.facility.population, icon: '👥' },
|
||||||
].map((f, i) => (
|
].map((f, i) => (
|
||||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
||||||
<div style={{ fontSize: '18px', marginBottom: '4px' }}>{f.icon}</div>
|
<div className="text-[18px] mb-1">{f.icon}</div>
|
||||||
<p className="text-[14px] font-bold text-text-1 font-mono">{f.value}</p>
|
<p className="text-[14px] font-bold text-text-1 font-mono">{f.value}</p>
|
||||||
<p className="text-[9px] text-text-3 font-korean mt-1">{f.label}</p>
|
<p className="text-[9px] text-text-3 font-korean mt-1">{f.label}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -403,7 +391,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
{ label: '기온', value: '8.5°C', icon: '☀️' },
|
{ label: '기온', value: '8.5°C', icon: '☀️' },
|
||||||
].map((w, i) => (
|
].map((w, i) => (
|
||||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
||||||
<div style={{ fontSize: '16px', marginBottom: '2px' }}>{w.icon}</div>
|
<div className="text-[16px] mb-0.5">{w.icon}</div>
|
||||||
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
||||||
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -436,7 +424,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
{ time: '07:15', event: '해경 3009함 현장 도착, 방제 개시', color: '#06b6d4' },
|
{ time: '07:15', event: '해경 3009함 현장 도착, 방제 개시', color: '#06b6d4' },
|
||||||
].map((e, i) => (
|
].map((e, i) => (
|
||||||
<div key={i} className="flex items-center gap-3 px-3 py-2 bg-bg-1 rounded border border-border">
|
<div key={i} className="flex items-center gap-3 px-3 py-2 bg-bg-1 rounded border border-border">
|
||||||
<span className="font-mono text-[11px] font-bold" style={{ color: e.color, minWidth: '40px' }}>{e.time}</span>
|
<span className="font-mono text-[11px] font-bold min-w-[40px]" style={{ color: e.color }}>{e.time}</span>
|
||||||
<span className="text-[11px] text-text-2 font-korean">{e.event}</span>
|
<span className="text-[11px] text-text-2 font-korean">{e.event}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -445,7 +433,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
{sec.id === 'rescue-casualty' && (
|
{sec.id === 'rescue-casualty' && (
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-4 gap-3">
|
||||||
{[
|
{[
|
||||||
{ label: '총원', value: sampleRescueData.casualty.total, color: 'var(--t1)' },
|
{ label: '총원', value: sampleRescueData.casualty.total },
|
||||||
{ label: '구조완료', value: sampleRescueData.casualty.rescued, color: '#22c55e' },
|
{ label: '구조완료', value: sampleRescueData.casualty.rescued, color: '#22c55e' },
|
||||||
{ label: '실종', value: sampleRescueData.casualty.missing, color: '#ef4444' },
|
{ label: '실종', value: sampleRescueData.casualty.missing, color: '#ef4444' },
|
||||||
{ label: '부상', value: sampleRescueData.casualty.injured, color: '#f97316' },
|
{ label: '부상', value: sampleRescueData.casualty.injured, color: '#f97316' },
|
||||||
@ -459,7 +447,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sec.id === 'rescue-resource' && (
|
{sec.id === 'rescue-resource' && (
|
||||||
<table className="w-full" style={{ borderCollapse: 'collapse', fontSize: '11px' }}>
|
<table className="w-full text-[11px]" className="border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border">
|
<tr className="border-b border-border">
|
||||||
<th className="px-3 py-2 text-left text-text-3 font-korean">유형</th>
|
<th className="px-3 py-2 text-left text-text-3 font-korean">유형</th>
|
||||||
@ -510,7 +498,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
{ label: '시정', value: '8 km', icon: '👁' },
|
{ label: '시정', value: '8 km', icon: '👁' },
|
||||||
].map((w, i) => (
|
].map((w, i) => (
|
||||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
||||||
<div style={{ fontSize: '16px', marginBottom: '2px' }}>{w.icon}</div>
|
<div className="text-[16px] mb-0.5">{w.icon}</div>
|
||||||
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
||||||
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -117,7 +117,7 @@ export function ReportsView() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<table className="w-full table-fixed" style={{ borderCollapse: 'collapse' }}>
|
<table className="w-full table-fixed" className="border-collapse">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '3%' }} />
|
<col style={{ width: '3%' }} />
|
||||||
<col style={{ width: '4%' }} />
|
<col style={{ width: '4%' }} />
|
||||||
@ -216,16 +216,16 @@ export function ReportsView() {
|
|||||||
{/* ──── 보고서 미리보기 모달 ──── */}
|
{/* ──── 보고서 미리보기 모달 ──── */}
|
||||||
{previewReport && (
|
{previewReport && (
|
||||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(6px)' }}>
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(6px)' }}>
|
||||||
<div className="flex overflow-hidden" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: '14px', width: 'min(96vw, 1320px)', height: 'min(94vh, 860px)', boxShadow: '0 28px 72px rgba(0,0,0,0.6)' }}>
|
<div className="flex overflow-hidden bg-bg-2 border border-border" style={{ borderRadius: '14px', width: 'min(96vw, 1320px)', height: 'min(94vh, 860px)', boxShadow: '0 28px 72px rgba(0,0,0,0.6)' }}>
|
||||||
|
|
||||||
{/* ── 왼쪽: 메타 + 다운로드 ── */}
|
{/* ── 왼쪽: 메타 + 다운로드 ── */}
|
||||||
<div className="flex flex-col shrink-0" style={{ width: '240px', borderRight: '1px solid var(--bd)', background: 'var(--bg1)' }}>
|
<div className="flex flex-col shrink-0 w-60 border-r border-border bg-bg-1">
|
||||||
{/* 상단 아이콘·제목 */}
|
{/* 상단 아이콘·제목 */}
|
||||||
<div style={{ padding: '20px 18px 16px', borderBottom: '1px solid var(--bd)' }}>
|
<div className="px-[18px] pt-5 pb-4 border-b border-border">
|
||||||
<div className="text-center mb-2.5" style={{ fontSize: '28px' }}>
|
<div className="text-center mb-2.5 text-[28px]">
|
||||||
{({ '초기보고서': '📋', '지휘부 보고': '📊', '예측보고서': '🔬', '종합보고서': '📑', '유출유 보고': '🛢️' } as Record<string, string>)[previewReport.reportType] || '📄'}
|
{({ '초기보고서': '📋', '지휘부 보고': '📊', '예측보고서': '🔬', '종합보고서': '📑', '유출유 보고': '🛢️' } as Record<string, string>)[previewReport.reportType] || '📄'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center font-korean" style={{ fontSize: '13px', fontWeight: 700, color: 'var(--t1)', lineHeight: 1.4, wordBreak: 'keep-all' }}>
|
<div className="text-center font-korean text-[13px] font-bold leading-snug" style={{ wordBreak: 'keep-all' }}>
|
||||||
{previewReport.title || '제목 없음'}
|
{previewReport.title || '제목 없음'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
@ -236,31 +236,30 @@ export function ReportsView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 메타 정보 */}
|
{/* 메타 정보 */}
|
||||||
<div className="flex flex-col gap-2.5 font-korean" style={{ padding: '14px 18px', fontSize: '11px', borderBottom: '1px solid var(--bd)' }}>
|
<div className="flex flex-col gap-2.5 font-korean text-[11px] px-[18px] py-3.5 border-b border-border">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>작성자</span>
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">작성자</span>
|
||||||
<span style={{ color: 'var(--t1)', fontWeight: 600 }}>{previewReport.author || '—'}</span>
|
<span className="font-semibold">{previewReport.author || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>관할</span>
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">관할</span>
|
||||||
<span style={{ color: 'var(--t1)', fontWeight: 600 }}>{previewReport.jurisdiction}</span>
|
<span className="font-semibold">{previewReport.jurisdiction}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>생성일시</span>
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">생성일시</span>
|
||||||
<span className="font-mono" style={{ color: 'var(--t1)', fontWeight: 600 }}>{formatDate(previewReport.createdAt)}</span>
|
<span className="font-mono font-semibold">{formatDate(previewReport.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span style={{ color: 'var(--t3)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>상태</span>
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">상태</span>
|
||||||
<b style={{ color: statusColors[previewReport.status]?.text || 'var(--t1)' }}>{previewReport.status}</b>
|
<b style={{ color: statusColors[previewReport.status]?.text || 'var(--t1)' }}>{previewReport.status}</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 수정 버튼 */}
|
{/* 수정 버튼 */}
|
||||||
<div style={{ padding: '12px 18px', borderBottom: '1px solid var(--bd)' }}>
|
<div className="px-[18px] py-3 border-b border-border">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setPreviewReport(null); setView({ screen: 'edit', data: { ...previewReport } }) }}
|
onClick={() => { setPreviewReport(null); setView({ screen: 'edit', data: { ...previewReport } }) }}
|
||||||
className="w-full font-korean"
|
className="w-full font-korean text-[11px] font-semibold cursor-pointer rounded-md border border-[rgba(6,182,212,0.3)] bg-[rgba(6,182,212,0.08)] text-primary-cyan py-2"
|
||||||
style={{ padding: '8px 0', borderRadius: '6px', border: '1px solid rgba(6,182,212,0.3)', background: 'rgba(6,182,212,0.08)', color: 'var(--cyan)', fontSize: '11px', fontWeight: 600, cursor: 'pointer' }}
|
|
||||||
>
|
>
|
||||||
✏ 수정 모드
|
✏ 수정 모드
|
||||||
</button>
|
</button>
|
||||||
@ -270,8 +269,8 @@ export function ReportsView() {
|
|||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
{/* 하단 다운로드 버튼 */}
|
{/* 하단 다운로드 버튼 */}
|
||||||
<div className="flex flex-col gap-2" style={{ padding: '14px 16px', borderTop: '1px solid var(--bd)' }}>
|
<div className="flex flex-col gap-2 px-4 py-3.5 border-t border-border">
|
||||||
<div className="text-center font-korean mb-0.5" style={{ fontSize: '9px', color: 'var(--t3)' }}>문서 저장</div>
|
<div className="text-center font-korean mb-0.5 text-[9px] text-text-3">문서 저장</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const tpl = templateTypes.find(t => t.id === previewReport.reportType)
|
const tpl = templateTypes.find(t => t.id === previewReport.reportType)
|
||||||
@ -290,8 +289,8 @@ export function ReportsView() {
|
|||||||
exportAsPDF(html, previewReport.title || tpl.label)
|
exportAsPDF(html, previewReport.title || tpl.label)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center justify-center gap-1.5 font-korean"
|
className="w-full flex items-center justify-center gap-1.5 font-korean text-[12px] font-bold cursor-pointer rounded-md py-[11px]"
|
||||||
style={{ padding: '11px 0', borderRadius: '6px', border: '1px solid rgba(239,68,68,0.4)', background: 'rgba(239,68,68,0.1)', color: 'var(--red)', fontSize: '12px', fontWeight: 700, cursor: 'pointer' }}
|
style={{ border: '1px solid rgba(239,68,68,0.4)', background: 'rgba(239,68,68,0.1)', color: 'var(--red)' }}
|
||||||
>
|
>
|
||||||
<span>📄</span> PDF 저장
|
<span>📄</span> PDF 저장
|
||||||
</button>
|
</button>
|
||||||
@ -313,8 +312,8 @@ export function ReportsView() {
|
|||||||
exportAsHWP(html, previewReport.title || tpl.label)
|
exportAsHWP(html, previewReport.title || tpl.label)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center justify-center gap-1.5 font-korean"
|
className="w-full flex items-center justify-center gap-1.5 font-korean text-[12px] font-bold cursor-pointer rounded-md py-[11px]"
|
||||||
style={{ padding: '11px 0', borderRadius: '6px', border: '1px solid rgba(59,130,246,0.4)', background: 'rgba(59,130,246,0.1)', color: 'var(--blue)', fontSize: '12px', fontWeight: 700, cursor: 'pointer' }}
|
style={{ border: '1px solid rgba(59,130,246,0.4)', background: 'rgba(59,130,246,0.1)', color: 'var(--blue)' }}
|
||||||
>
|
>
|
||||||
<span>📝</span> HWP 저장
|
<span>📝</span> HWP 저장
|
||||||
</button>
|
</button>
|
||||||
@ -324,29 +323,28 @@ export function ReportsView() {
|
|||||||
{/* ── 오른쪽: 본문 뷰어 ── */}
|
{/* ── 오른쪽: 본문 뷰어 ── */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between shrink-0" style={{ padding: '14px 20px', borderBottom: '1px solid var(--bd)' }}>
|
<div className="flex items-center justify-between shrink-0 px-5 py-3.5 border-b border-border">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-korean" style={{ fontSize: '9px', padding: '3px 8px', borderRadius: '4px', background: 'rgba(6,182,212,0.1)', color: 'var(--cyan)', fontWeight: 600 }}>보기</span>
|
<span className="font-korean text-[9px] px-2 py-[3px] rounded bg-[rgba(6,182,212,0.1)] text-primary-cyan font-semibold">보기</span>
|
||||||
<span className="font-korean" style={{ fontSize: '12px', color: 'var(--t3)' }}>보고서 내용</span>
|
<span className="font-korean text-[12px] text-text-3">보고서 내용</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
onClick={() => setPreviewReport(null)}
|
onClick={() => setPreviewReport(null)}
|
||||||
style={{ fontSize: '18px', cursor: 'pointer', color: 'var(--t3)', lineHeight: 1 }}
|
className="text-[18px] cursor-pointer text-text-3 leading-none hover:text-text-1 transition-colors"
|
||||||
className="hover:text-text-1 transition-colors"
|
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 본문 스크롤 영역 */}
|
{/* 본문 스크롤 영역 */}
|
||||||
<div className="flex-1 overflow-y-auto" style={{ padding: '24px', scrollbarWidth: 'thin' }}>
|
<div className="flex-1 overflow-y-auto p-6" style={{ scrollbarWidth: 'thin' }}>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* 1. 사고개요 */}
|
{/* 1. 사고개요 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', fontWeight: 700, color: 'var(--cyan)', borderBottom: '1px solid rgba(6,182,212,0.15)', paddingBottom: '4px' }}>
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
||||||
1. 사고개요
|
1. 사고개요
|
||||||
</div>
|
</div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', color: 'var(--t1)', lineHeight: 1.7, whiteSpace: 'pre-wrap', marginTop: '8px' }}>
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
||||||
{[
|
{[
|
||||||
previewReport.incident.name && `사고명: ${previewReport.incident.name}`,
|
previewReport.incident.name && `사고명: ${previewReport.incident.name}`,
|
||||||
previewReport.incident.occurTime && `발생일시: ${previewReport.incident.occurTime}`,
|
previewReport.incident.occurTime && `발생일시: ${previewReport.incident.occurTime}`,
|
||||||
@ -359,10 +357,10 @@ export function ReportsView() {
|
|||||||
|
|
||||||
{/* 2. 유출현황 */}
|
{/* 2. 유출현황 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', fontWeight: 700, color: 'var(--cyan)', borderBottom: '1px solid rgba(6,182,212,0.15)', paddingBottom: '4px' }}>
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
||||||
2. 유출현황
|
2. 유출현황
|
||||||
</div>
|
</div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', color: 'var(--t1)', lineHeight: 1.7, whiteSpace: 'pre-wrap', marginTop: '8px' }}>
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
||||||
{[
|
{[
|
||||||
previewReport.incident.pollutant && `유출유종: ${previewReport.incident.pollutant}`,
|
previewReport.incident.pollutant && `유출유종: ${previewReport.incident.pollutant}`,
|
||||||
previewReport.incident.spillAmount && `유출량: ${previewReport.incident.spillAmount}`,
|
previewReport.incident.spillAmount && `유출량: ${previewReport.incident.spillAmount}`,
|
||||||
@ -373,20 +371,20 @@ export function ReportsView() {
|
|||||||
|
|
||||||
{/* 3. 초동조치 / 대응현황 */}
|
{/* 3. 초동조치 / 대응현황 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', fontWeight: 700, color: 'var(--cyan)', borderBottom: '1px solid rgba(6,182,212,0.15)', paddingBottom: '4px' }}>
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
||||||
3. 초동조치 / 대응현황
|
3. 초동조치 / 대응현황
|
||||||
</div>
|
</div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', color: 'var(--t1)', lineHeight: 1.7, whiteSpace: 'pre-wrap', marginTop: '8px' }}>
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
||||||
{previewReport.analysis || '—'}
|
{previewReport.analysis || '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4. 향후 계획 */}
|
{/* 4. 향후 계획 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', fontWeight: 700, color: 'var(--cyan)', borderBottom: '1px solid rgba(6,182,212,0.15)', paddingBottom: '4px' }}>
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
||||||
4. 향후 계획
|
4. 향후 계획
|
||||||
</div>
|
</div>
|
||||||
<div className="font-korean" style={{ fontSize: '12px', color: 'var(--t1)', lineHeight: 1.7, whiteSpace: 'pre-wrap', marginTop: '8px' }}>
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
||||||
{previewReport.etcEquipment || '—'}
|
{previewReport.etcEquipment || '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -128,7 +128,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
|
|||||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
{template.sections.map((section, sIdx) => (
|
{template.sections.map((section, sIdx) => (
|
||||||
<div key={sIdx} className="mb-6 w-full">
|
<div key={sIdx} className="mb-6 w-full">
|
||||||
<h4 className="text-[13px] font-bold font-korean mb-3" style={{ color: '#06b6d4' }}>{section.title}</h4>
|
<h4 className="text-[13px] font-bold font-korean mb-3" className="text-cyan-500">{section.title}</h4>
|
||||||
<table className="w-full table-fixed border-collapse">
|
<table className="w-full table-fixed border-collapse">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style={{ width: '180px' }} />
|
<col style={{ width: '180px' }} />
|
||||||
@ -235,7 +235,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
|
|||||||
{/* Report Title */}
|
{/* Report Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h2 className="text-[18px] font-bold text-text-1 font-korean mb-1">해양오염방제지원시스템</h2>
|
<h2 className="text-[18px] font-bold text-text-1 font-korean mb-1">해양오염방제지원시스템</h2>
|
||||||
<h3 className="text-[15px] font-semibold font-korean" style={{ color: '#06b6d4' }}>
|
<h3 className="text-[15px] font-semibold font-korean" className="text-cyan-500">
|
||||||
{formData['incident.name'] || template.label}
|
{formData['incident.name'] || template.label}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] text-text-3 font-korean mt-2">
|
<p className="text-[11px] text-text-3 font-korean mt-2">
|
||||||
|
|||||||
@ -169,118 +169,118 @@ export function RescueScenarioView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, width: '100%', height: '100%', overflow: 'hidden', background: 'var(--bg0)' }}>
|
<div className="flex flex-col flex-1 w-full h-full overflow-hidden bg-bg-0">
|
||||||
{/* ── Header ── */}
|
{/* ── Header ── */}
|
||||||
<div style={{ padding: '14px 20px', borderBottom: '1px solid var(--bd)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
|
<div className="px-5 py-3.5 border-b border-border flex items-center justify-between shrink-0">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
<div className="flex items-center gap-3">
|
||||||
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'linear-gradient(135deg,rgba(6,182,212,.2),rgba(59,130,246,.15))', border: '1px solid rgba(6,182,212,.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18 }}>📊</div>
|
<div className="w-10 h-10 rounded-[10px] flex items-center justify-center text-lg border border-[rgba(6,182,212,.3)]" style={{ background: 'linear-gradient(135deg,rgba(6,182,212,.2),rgba(59,130,246,.15))' }}>📊</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 15, fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>긴급구난 시나리오 관리</div>
|
<div className="text-[15px] font-bold">긴급구난 시나리오 관리</div>
|
||||||
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: 2 }}>시간 단계별 시나리오 비교·검토 및 구난 의사결정 지원 (SFR-009)</div>
|
<div className="text-[10px] text-text-3 mt-0.5">시간 단계별 시나리오 비교·검토 및 구난 의사결정 지원 (SFR-009)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div className="flex gap-2 items-center">
|
||||||
<select value={selectedIncident} onChange={e => setSelectedIncident(Number(e.target.value))} style={{ padding: '6px 12px', borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fK)', outline: 'none' }}>
|
<select value={selectedIncident} onChange={e => setSelectedIncident(Number(e.target.value))} className="px-3 py-1.5 rounded-md border border-border bg-bg-3 text-[10px] outline-none">
|
||||||
{ops.map((op, i) => <option key={op.rescueOpsSn} value={i}>{op.opsCd} · {op.vesselNm}</option>)}
|
{ops.map((op, i) => <option key={op.rescueOpsSn} value={i}>{op.opsCd} · {op.vesselNm}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<button onClick={() => setNewScnModalOpen(true)} style={{ padding: '6px 14px', borderRadius: 6, border: 'none', background: 'linear-gradient(135deg,var(--cyan),#3b82f6)', color: '#fff', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'var(--fK)' }}>+ 신규 시나리오</button>
|
<button onClick={() => setNewScnModalOpen(true)} className="px-3.5 py-1.5 rounded-md border-none text-white text-[10px] font-bold cursor-pointer" style={{ background: 'linear-gradient(135deg,var(--cyan),#3b82f6)' }}>+ 신규 시나리오</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Content: Left List + Right Detail ── */}
|
{/* ── Content: Left List + Right Detail ── */}
|
||||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
|
||||||
{/* ═══ LEFT: 시나리오 목록 ═══ */}
|
{/* ═══ LEFT: 시나리오 목록 ═══ */}
|
||||||
<div style={{ width: 360, minWidth: 360, background: 'var(--bg1)', borderRight: '1px solid var(--bd)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="w-[360px] min-w-[360px] bg-bg-1 border-r border-border flex flex-col overflow-hidden">
|
||||||
{/* Sort bar */}
|
{/* Sort bar */}
|
||||||
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--bd)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div className="px-3.5 py-2.5 border-b border-border flex items-center justify-between">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>📋 시나리오 목록 <span style={{ fontWeight: 400, color: 'var(--t3)', fontSize: 9 }}>({scenarios.length}개)</span></div>
|
<div className="text-[11px] font-bold">📋 시나리오 목록 <span className="font-normal text-text-3 text-[9px]">({scenarios.length}개)</span></div>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div className="flex gap-1">
|
||||||
{(['time', 'risk'] as const).map(s => (
|
{(['time', 'risk'] as const).map(s => (
|
||||||
<button key={s} onClick={() => setSortBy(s)} style={{ padding: '3px 8px', borderRadius: 4, border: `1px solid ${sortBy === s ? 'rgba(6,182,212,.4)' : 'var(--bd)'}`, background: sortBy === s ? 'rgba(6,182,212,.08)' : 'var(--bg3)', color: sortBy === s ? 'var(--cyan)' : 'var(--t3)', fontSize: 9, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>{s === 'time' ? '시간순' : '위험도순'}</button>
|
<button key={s} onClick={() => setSortBy(s)} className="px-2 py-px rounded text-[9px] font-semibold cursor-pointer" style={{ border: `1px solid ${sortBy === s ? 'rgba(6,182,212,.4)' : 'var(--bd)'}`, background: sortBy === s ? 'rgba(6,182,212,.08)' : 'var(--bg3)', color: sortBy === s ? 'var(--cyan)' : 'var(--t3)' }}>{s === 'time' ? '시간순' : '위험도순'}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card list */}
|
{/* Card list */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8, scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
<div className="flex-1 overflow-y-auto px-3 py-2.5 flex flex-col gap-2 scrollbar-thin">
|
||||||
{loading && scenarios.length === 0 && (
|
{loading && scenarios.length === 0 && (
|
||||||
<div style={{ textAlign: 'center', padding: '40px 0', fontSize: 11, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>시나리오 로딩 중...</div>
|
<div className="text-center py-10 text-[11px] text-text-3">시나리오 로딩 중...</div>
|
||||||
)}
|
)}
|
||||||
{sorted.map(sc => {
|
{sorted.map(sc => {
|
||||||
const isSel = selectedId === sc.id
|
const isSel = selectedId === sc.id
|
||||||
const sev = SEV_STYLE[sc.severity]
|
const sev = SEV_STYLE[sc.severity]
|
||||||
return (
|
return (
|
||||||
<div key={sc.id} onClick={() => setSelectedId(sc.id)} className={`hns-scn-card${isSel ? ' sel' : ''}`}
|
<div key={sc.id} onClick={() => setSelectedId(sc.id)} className={`hns-scn-card${isSel ? ' sel' : ''} p-3 rounded-md cursor-pointer transition-all`}
|
||||||
style={{ padding: '12px', borderRadius: 8, border: `1px solid ${isSel ? 'rgba(6,182,212,.35)' : 'var(--bd)'}`, background: isSel ? 'rgba(6,182,212,.04)' : 'var(--bg3)', cursor: 'pointer', transition: 'all .15s' }}>
|
style={{ border: `1px solid ${isSel ? 'rgba(6,182,212,.35)' : 'var(--bd)'}`, background: isSel ? 'rgba(6,182,212,.04)' : 'var(--bg3)' }}>
|
||||||
{/* Top: checkbox + ID + severity */}
|
{/* Top: checkbox + ID + severity */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<input type="checkbox" checked={checked.has(sc.id)} onChange={e => { e.stopPropagation(); toggleCheck(sc.id) }} style={{ accentColor: 'var(--cyan)' }} />
|
<input type="checkbox" checked={checked.has(sc.id)} onChange={e => { e.stopPropagation(); toggleCheck(sc.id) }} className="accent-[var(--cyan)]" />
|
||||||
<span style={{ fontSize: 12, fontWeight: 800, fontFamily: 'var(--fM)', color: isSel ? 'var(--cyan)' : 'var(--t1)' }}>{sc.id}</span>
|
<span className="text-xs font-extrabold font-mono" style={{ color: isSel ? 'var(--cyan)' : 'var(--t1)' }}>{sc.id}</span>
|
||||||
<span style={{ padding: '1px 6px', borderRadius: 4, background: sev.bg, color: sev.color, fontSize: 8, fontWeight: 700, fontFamily: 'var(--fM)' }}>{sev.label}</span>
|
<span className="px-1.5 py-px rounded text-[8px] font-bold font-mono" style={{ background: sev.bg, color: sev.color }}>{sev.label}</span>
|
||||||
<span style={{ marginLeft: 'auto', fontSize: 9, color: 'var(--t3)', fontFamily: 'var(--fM)' }}>{sc.timeStep}</span>
|
<span className="ml-auto text-[9px] text-text-3 font-mono">{sc.timeStep}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Name + time */}
|
{/* Name + time */}
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: 4 }}>{sc.name}</div>
|
<div className="text-[11px] font-bold mb-1">{sc.name}</div>
|
||||||
<div style={{ fontSize: 9, color: 'var(--t3)', fontFamily: 'var(--fM)', marginBottom: 8 }}>{sc.datetime}</div>
|
<div className="text-[9px] text-text-3 font-mono mb-2">{sc.datetime}</div>
|
||||||
{/* KPI grid */}
|
{/* KPI grid */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 4, marginBottom: 8, fontSize: 8, fontFamily: 'var(--fK)', textAlign: 'center' }}>
|
<div className="grid grid-cols-4 gap-1 mb-2 text-[8px] text-center">
|
||||||
<div style={{ padding: '4px 2px', background: 'var(--bg0)', borderRadius: 4 }}>
|
<div className="py-1 bg-bg-0 rounded">
|
||||||
<div style={{ color: 'var(--t3)' }}>GM</div>
|
<div className="text-text-3">GM</div>
|
||||||
<div style={{ fontWeight: 700, fontFamily: 'var(--fM)', color: gmColor(parseFloat(sc.gm)) }}>{sc.gm}m</div>
|
<div className="font-bold font-mono" style={{ color: gmColor(parseFloat(sc.gm)) }}>{sc.gm}m</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '4px 2px', background: 'var(--bg0)', borderRadius: 4 }}>
|
<div className="py-1 bg-bg-0 rounded">
|
||||||
<div style={{ color: 'var(--t3)' }}>횡경사</div>
|
<div className="text-text-3">횡경사</div>
|
||||||
<div style={{ fontWeight: 700, fontFamily: 'var(--fM)', color: listColor(parseFloat(sc.list)) }}>{sc.list}°</div>
|
<div className="font-bold font-mono" style={{ color: listColor(parseFloat(sc.list)) }}>{sc.list}°</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '4px 2px', background: 'var(--bg0)', borderRadius: 4 }}>
|
<div className="py-1 bg-bg-0 rounded">
|
||||||
<div style={{ color: 'var(--t3)' }}>부력</div>
|
<div className="text-text-3">부력</div>
|
||||||
<div style={{ fontWeight: 700, fontFamily: 'var(--fM)', color: buoyColor(sc.buoyancy) }}>{sc.buoyancy}%</div>
|
<div className="font-bold font-mono" style={{ color: buoyColor(sc.buoyancy) }}>{sc.buoyancy}%</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '4px 2px', background: 'var(--bg0)', borderRadius: 4 }}>
|
<div className="py-1 bg-bg-0 rounded">
|
||||||
<div style={{ color: 'var(--t3)' }}>유출</div>
|
<div className="text-text-3">유출</div>
|
||||||
<div style={{ fontWeight: 700, fontFamily: 'var(--fM)', color: oilColor(parseFloat(sc.oilRate)) }}>{sc.oilRate.split(' ')[0]}</div>
|
<div className="font-bold font-mono" style={{ color: oilColor(parseFloat(sc.oilRate)) }}>{sc.oilRate.split(' ')[0]}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div style={{ fontSize: 9, color: 'var(--t2)', fontFamily: 'var(--fK)', lineHeight: 1.5 }}>{sc.description}</div>
|
<div className="text-[9px] text-text-2 leading-[1.5]">{sc.description}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom actions */}
|
{/* Bottom actions */}
|
||||||
<div style={{ padding: '10px 14px', borderTop: '1px solid var(--bd)', display: 'flex', gap: 6 }}>
|
<div className="px-3.5 py-2.5 border-t border-border flex gap-1.5">
|
||||||
<button onClick={() => setDetailView(1)} style={{ flex: 1, padding: '8px', borderRadius: 6, border: 'none', background: 'linear-gradient(135deg,var(--cyan),#3b82f6)', color: '#fff', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'var(--fK)' }}>📊 선택 시나리오 비교</button>
|
<button onClick={() => setDetailView(1)} className="flex-1 py-2 rounded-md border-none text-white text-[10px] font-bold cursor-pointer" style={{ background: 'linear-gradient(135deg,var(--cyan),#3b82f6)' }}>📊 선택 시나리오 비교</button>
|
||||||
<button style={{ padding: '8px 14px', borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t2)', fontSize: 10, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>📄 보고서</button>
|
<button className="px-3.5 py-2 rounded-md border border-border bg-bg-3 text-text-2 text-[10px] font-semibold cursor-pointer">📄 보고서</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ═══ RIGHT: 상세/비교 ═══ */}
|
{/* ═══ RIGHT: 상세/비교 ═══ */}
|
||||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
||||||
{/* Detail tabs */}
|
{/* Detail tabs */}
|
||||||
<div style={{ display: 'flex', borderBottom: '1px solid var(--bd)', flexShrink: 0 }}>
|
<div className="flex border-b border-border shrink-0">
|
||||||
{(['📋 시나리오 상세', '📊 비교 차트', '🗺 지도 오버레이'] as const).map((label, i) => (
|
{(['📋 시나리오 상세', '📊 비교 차트', '🗺 지도 오버레이'] as const).map((label, i) => (
|
||||||
<button key={i} onClick={() => setDetailView(i as DetailView)} className="rsc-atab" style={{ flex: 1, padding: '9px 4px', border: 'none', background: detailView === i ? 'rgba(6,182,212,.04)' : 'transparent', color: detailView === i ? 'var(--cyan)' : 'var(--t3)', fontFamily: 'var(--fK)', fontSize: 10, fontWeight: detailView === i ? 700 : 600, cursor: 'pointer', borderBottom: `2px solid ${detailView === i ? 'var(--cyan)' : 'transparent'}`, transition: '.2s' }}>{label}</button>
|
<button key={i} onClick={() => setDetailView(i as DetailView)} className="rsc-atab flex-1 py-2.5 px-1 border-none cursor-pointer transition-all text-[10px]" style={{ background: detailView === i ? 'rgba(6,182,212,.04)' : 'transparent', color: detailView === i ? 'var(--cyan)' : 'var(--t3)', fontWeight: detailView === i ? 700 : 600, borderBottom: `2px solid ${detailView === i ? 'var(--cyan)' : 'transparent'}` }}>{label}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View content */}
|
{/* View content */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
<div className="flex-1 overflow-y-auto scrollbar-thin">
|
||||||
|
|
||||||
{/* ─── VIEW 0: 시나리오 상세 ─── */}
|
{/* ─── VIEW 0: 시나리오 상세 ─── */}
|
||||||
{detailView === 0 && selected && (
|
{detailView === 0 && selected && (
|
||||||
<div style={{ padding: 20 }}>
|
<div className="p-5">
|
||||||
{/* Gradient header */}
|
{/* Gradient header */}
|
||||||
<div style={{ padding: '16px 20px', borderRadius: 10, background: 'linear-gradient(135deg,rgba(6,182,212,.06),rgba(239,68,68,.04))', border: '1px solid rgba(6,182,212,.2)', marginBottom: 16 }}>
|
<div className="px-5 py-4 rounded-[10px] border border-[rgba(6,182,212,.2)] mb-4" style={{ background: 'linear-gradient(135deg,rgba(6,182,212,.06),rgba(239,68,68,.04))' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
<div className="flex items-center gap-2.5 mb-2.5">
|
||||||
<span style={{ fontSize: 16, fontWeight: 800, fontFamily: 'var(--fM)', color: 'var(--cyan)' }}>{selected.id}</span>
|
<span className="text-base font-extrabold font-mono text-primary-cyan">{selected.id}</span>
|
||||||
<span style={{ fontSize: 14, fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>{selected.name}</span>
|
<span className="text-sm font-bold">{selected.name}</span>
|
||||||
<span style={{ padding: '2px 8px', borderRadius: 4, background: SEV_STYLE[selected.severity].bg, color: SEV_STYLE[selected.severity].color, fontSize: 9, fontWeight: 700, fontFamily: 'var(--fM)' }}>{selected.severity}</span>
|
<span className="px-2 py-px rounded text-[9px] font-bold font-mono" style={{ background: SEV_STYLE[selected.severity].bg, color: SEV_STYLE[selected.severity].color }}>{selected.severity}</span>
|
||||||
<span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fM)' }}>{selected.datetime}</span>
|
<span className="ml-auto text-[10px] text-text-3 font-mono">{selected.datetime}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* 6 KPI cards */}
|
{/* 6 KPI cards */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6,1fr)', gap: 8, fontSize: 8, fontFamily: 'var(--fK)', textAlign: 'center' }}>
|
<div className="grid gap-2 text-[8px] text-center" style={{ gridTemplateColumns: 'repeat(6,1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ label: 'GM (복원심)', value: `${selected.gm}m`, color: gmColor(parseFloat(selected.gm)) },
|
{ label: 'GM (복원심)', value: `${selected.gm}m`, color: gmColor(parseFloat(selected.gm)) },
|
||||||
{ label: '횡경사 (List)', value: `${selected.list}°`, color: listColor(parseFloat(selected.list)) },
|
{ label: '횡경사 (List)', value: `${selected.list}°`, color: listColor(parseFloat(selected.list)) },
|
||||||
@ -289,36 +289,36 @@ export function RescueScenarioView() {
|
|||||||
{ label: '유출률', value: selected.oilRate, color: oilColor(parseFloat(selected.oilRate)) },
|
{ label: '유출률', value: selected.oilRate, color: oilColor(parseFloat(selected.oilRate)) },
|
||||||
{ label: 'BM 비율', value: selected.bmRatio, color: parseFloat(selected.bmRatio) > 100 ? 'var(--red)' : parseFloat(selected.bmRatio) > 85 ? 'var(--orange)' : 'var(--green)' },
|
{ label: 'BM 비율', value: selected.bmRatio, color: parseFloat(selected.bmRatio) > 100 ? 'var(--red)' : parseFloat(selected.bmRatio) > 85 ? 'var(--orange)' : 'var(--green)' },
|
||||||
].map(kpi => (
|
].map(kpi => (
|
||||||
<div key={kpi.label} style={{ padding: '8px 4px', background: 'var(--bg3)', borderRadius: 6, border: '1px solid var(--bd)' }}>
|
<div key={kpi.label} className="px-1 py-2 bg-bg-3 rounded-md border border-border">
|
||||||
<div style={{ color: 'var(--t3)', marginBottom: 4 }}>{kpi.label}</div>
|
<div className="text-text-3 mb-1">{kpi.label}</div>
|
||||||
<div style={{ fontSize: 18, fontWeight: 800, fontFamily: 'var(--fM)', color: kpi.color }}>{kpi.value}</div>
|
<div className="text-lg font-extrabold font-mono" style={{ color: kpi.color }}>{kpi.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2-column: 침수구획 + 구난판단 */}
|
{/* 2-column: 침수구획 + 구난판단 */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
{/* 침수 구획 */}
|
{/* 침수 구획 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 8, padding: 14 }}>
|
<div className="bg-bg-3 border border-border rounded-md p-3.5">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: 10 }}>🚢 침수 구획 상태</div>
|
<div className="text-[11px] font-bold text-primary-cyan mb-2.5">🚢 침수 구획 상태</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div className="flex flex-col gap-1.5">
|
||||||
{selected.compartments.map((c, i) => (
|
{selected.compartments.map((c, i) => (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 10px', background: 'var(--bg0)', borderRadius: 4, borderLeft: `3px solid ${c.color}` }}>
|
<div key={i} className="flex items-center justify-between px-2.5 py-1.5 bg-bg-0 rounded" style={{ borderLeft: `3px solid ${c.color}` }}>
|
||||||
<span style={{ fontSize: 9, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>{c.name}</span>
|
<span className="text-[9px]">{c.name}</span>
|
||||||
<span style={{ fontSize: 9, fontFamily: 'var(--fM)', color: c.color, fontWeight: 600 }}>{c.status}</span>
|
<span className="text-[9px] font-semibold font-mono" style={{ color: c.color }}>{c.status}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 구난 판단 */}
|
{/* 구난 판단 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 8, padding: 14 }}>
|
<div className="bg-bg-3 border border-border rounded-md p-3.5">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--red)', fontFamily: 'var(--fK)', marginBottom: 10 }}>⚠️ 구난 판단 요약</div>
|
<div className="text-[11px] font-bold text-status-red mb-2.5">⚠️ 구난 판단 요약</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div className="flex flex-col gap-1.5">
|
||||||
{selected.assessment.map((a, i) => (
|
{selected.assessment.map((a, i) => (
|
||||||
<div key={i} style={{ padding: '8px 10px', background: 'var(--bg0)', borderRadius: 4, borderLeft: `3px solid ${a.color}` }}>
|
<div key={i} className="px-2.5 py-2 bg-bg-0 rounded" style={{ borderLeft: `3px solid ${a.color}` }}>
|
||||||
<div style={{ fontSize: 9, fontWeight: 700, color: a.color, fontFamily: 'var(--fK)' }}>{a.label}</div>
|
<div className="text-[9px] font-bold" style={{ color: a.color }}>{a.label}</div>
|
||||||
<div style={{ fontSize: 9, color: 'var(--t2)', fontFamily: 'var(--fK)', marginTop: 2 }}>{a.value}</div>
|
<div className="text-[9px] text-text-2 mt-0.5">{a.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -326,14 +326,14 @@ export function RescueScenarioView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 대응 조치 이력 */}
|
{/* 대응 조치 이력 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 8, padding: 14 }}>
|
<div className="bg-bg-3 border border-border rounded-md p-3.5">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fK)', marginBottom: 10 }}>📋 대응 조치 이력</div>
|
<div className="text-[11px] font-bold text-status-orange mb-2.5">📋 대응 조치 이력</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div className="flex flex-col gap-1.5">
|
||||||
{selected.actions.map((a, i) => (
|
{selected.actions.map((a, i) => (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px', background: 'var(--bg0)', borderRadius: 4 }}>
|
<div key={i} className="flex items-center gap-2.5 px-2.5 py-1.5 bg-bg-0 rounded">
|
||||||
<span style={{ fontSize: 10, fontWeight: 700, fontFamily: 'var(--fM)', color: a.color, minWidth: 40 }}>{a.time}</span>
|
<span className="text-[10px] font-bold font-mono min-w-[40px]" style={{ color: a.color }}>{a.time}</span>
|
||||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: a.color, flexShrink: 0 }} />
|
<div className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: a.color }} />
|
||||||
<span style={{ fontSize: 9, fontFamily: 'var(--fK)', color: 'var(--t1)' }}>{a.text}</span>
|
<span className="text-[9px]">{a.text}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -346,21 +346,21 @@ export function RescueScenarioView() {
|
|||||||
|
|
||||||
{/* ─── VIEW 2: 지도 오버레이 ─── */}
|
{/* ─── VIEW 2: 지도 오버레이 ─── */}
|
||||||
{detailView === 2 && (
|
{detailView === 2 && (
|
||||||
<div style={{ padding: 20 }}>
|
<div className="p-5">
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 20, textAlign: 'center' }}>
|
<div className="bg-bg-3 border border-border rounded-[10px] p-5 text-center">
|
||||||
<div style={{ fontSize: 32, opacity: 0.3, marginBottom: 10 }}>🗺</div>
|
<div className="text-[32px] opacity-30 mb-2.5">🗺</div>
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)', marginBottom: 6 }}>GIS 기반 시나리오 비교</div>
|
<div className="text-[13px] font-bold mb-1.5">GIS 기반 시나리오 비교</div>
|
||||||
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.6, marginBottom: 16 }}>선택된 시나리오의 침수 구역을 지도 위에 오버레이하여 비교합니다.</div>
|
<div className="text-[10px] text-text-3 leading-relaxed mb-4">선택된 시나리오의 침수 구역을 지도 위에 오버레이하여 비교합니다.</div>
|
||||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}>
|
<div className="flex gap-2 justify-center flex-wrap">
|
||||||
{scenarios.map(sc => (
|
{scenarios.map(sc => (
|
||||||
<div key={sc.id} style={{ padding: '6px 12px', borderRadius: 6, border: `1px solid ${SEV_STYLE[sc.severity].color}40`, background: SEV_STYLE[sc.severity].bg, fontSize: 9, fontFamily: 'var(--fK)' }}>
|
<div key={sc.id} className="px-3 py-1.5 rounded-md text-[9px]" style={{ border: `1px solid ${SEV_STYLE[sc.severity].color}40`, background: SEV_STYLE[sc.severity].bg }}>
|
||||||
<span style={{ fontWeight: 700, color: SEV_STYLE[sc.severity].color }}>{sc.id}</span>
|
<span className="font-bold" style={{ color: SEV_STYLE[sc.severity].color }}>{sc.id}</span>
|
||||||
<span style={{ color: 'var(--t2)', marginLeft: 6 }}>{sc.name}</span>
|
<span className="text-text-2 ml-1.5">{sc.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 16, padding: 30, background: 'var(--bg0)', borderRadius: 8, border: '1px dashed var(--bd)' }}>
|
<div className="mt-4 p-[30px] bg-bg-0 rounded-md border border-dashed border-border">
|
||||||
<div style={{ fontSize: 11, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>지도 뷰 영역 — 구난 분석 지도와 연동하여 침수 구역 오버레이 표시</div>
|
<div className="text-[11px] text-text-3">지도 뷰 영역 — 구난 분석 지도와 연동하여 침수 구역 오버레이 표시</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -386,170 +386,169 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
|||||||
setTimeout(() => { setSubmitting(false); setDone(true) }, 2000)
|
setTimeout(() => { setSubmitting(false); setDone(true) }, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── shared styles ── */
|
/* ── shared className helpers ── */
|
||||||
const labelSt: React.CSSProperties = { fontSize: 9, color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: 4 }
|
const labelCls = 'text-[9px] text-text-3 block mb-1'
|
||||||
const inputSt: React.CSSProperties = { width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 11, fontFamily: 'var(--fK)', outline: 'none', boxSizing: 'border-box' as const }
|
const inputCls = 'w-full px-3 py-2 rounded-md border border-border bg-bg-0 text-[11px] outline-none'
|
||||||
const numSt: React.CSSProperties = { ...inputSt, fontFamily: 'var(--fM)' }
|
const numCls = `${inputCls} font-mono`
|
||||||
const selSt = inputSt
|
|
||||||
const sectionIcon = (n: number) => (
|
const sectionIcon = (n: number) => (
|
||||||
<div style={{ width: 18, height: 18, borderRadius: 5, background: 'rgba(6,182,212,.12)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 9, fontWeight: 700, color: 'var(--cyan)' }}>{n}</div>
|
<div className="w-[18px] h-[18px] rounded-[5px] flex items-center justify-center text-[9px] font-bold text-primary-cyan" style={{ background: 'rgba(6,182,212,.12)' }}>{n}</div>
|
||||||
)
|
)
|
||||||
const sectionTitle: React.CSSProperties = { fontSize: 11, fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }
|
const sectionTitleCls = 'text-[11px] font-bold text-primary-cyan mb-2.5 flex items-center gap-1.5'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={overlayRef} onClick={e => { if (e.target === overlayRef.current) onClose() }}
|
<div ref={overlayRef} onClick={e => { if (e.target === overlayRef.current) onClose() }}
|
||||||
style={{ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,.65)', backdropFilter: 'blur(6px)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,.65)', backdropFilter: 'blur(6px)' }}>
|
||||||
<div style={{ background: 'var(--bg1)', border: '1px solid rgba(6,182,212,.3)', borderRadius: 14, width: 700, maxHeight: '88vh', display: 'flex', flexDirection: 'column', boxShadow: '0 24px 80px rgba(0,0,0,.6)', overflow: 'hidden' }}>
|
<div className="bg-bg-1 border border-[rgba(6,182,212,.3)] rounded-[14px] w-[700px] max-h-[88vh] flex flex-col overflow-hidden" style={{ boxShadow: '0 24px 80px rgba(0,0,0,.6)' }}>
|
||||||
|
|
||||||
{/* ── 헤더 ── */}
|
{/* ── 헤더 ── */}
|
||||||
<div style={{ padding: '20px 24px 16px', borderBottom: '1px solid var(--bd)', flexShrink: 0, position: 'relative', overflow: 'hidden' }}>
|
<div className="px-6 pt-5 pb-4 border-b border-border shrink-0 relative overflow-hidden">
|
||||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(90deg,#06b6d4,#3b82f6,#8b5cf6)' }} />
|
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#06b6d4,#3b82f6,#8b5cf6)' }} />
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div className="flex items-center justify-between">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div className="flex items-center gap-2.5">
|
||||||
<div style={{ width: 36, height: 36, borderRadius: 10, background: 'linear-gradient(135deg,rgba(6,182,212,.15),rgba(59,130,246,.08))', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18 }}>🚨</div>
|
<div className="w-9 h-9 rounded-[10px] flex items-center justify-center text-lg" style={{ background: 'linear-gradient(135deg,rgba(6,182,212,.15),rgba(59,130,246,.08))' }}>🚨</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)' }}>신규 긴급구난 시나리오 생성</div>
|
<div className="text-[15px] font-bold">신규 긴급구난 시나리오 생성</div>
|
||||||
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: 2 }}>선박 사고 조건 및 구난 분석 파라미터를 설정합니다 (SFR-009)</div>
|
<div className="text-[10px] text-text-3 mt-0.5">선박 사고 조건 및 구난 분석 파라미터를 설정합니다 (SFR-009)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span onClick={onClose} style={{ fontSize: 18, cursor: 'pointer', color: 'var(--t3)', padding: 4 }}>✕</span>
|
<span onClick={onClose} className="text-lg cursor-pointer text-text-3 p-1">✕</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 본문 스크롤 ── */}
|
{/* ── 본문 스크롤 ── */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 18, scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-[18px] scrollbar-thin">
|
||||||
|
|
||||||
{/* ① 기본 정보 */}
|
{/* ① 기본 정보 */}
|
||||||
<div>
|
<div>
|
||||||
<div style={sectionTitle}>{sectionIcon(1)} 기본 정보</div>
|
<div className={sectionTitleCls}>{sectionIcon(1)} 기본 정보</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
<div className="grid grid-cols-2 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>시나리오명 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>시나리오명 <span className="text-[#f87171]">*</span></label>
|
||||||
<input type="text" placeholder="예: T+3h 기관실 침수 확대" style={inputSt} />
|
<input type="text" placeholder="예: T+3h 기관실 침수 확대" className={inputCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>연계 사고 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>연계 사고 <span className="text-[#f87171]">*</span></label>
|
||||||
<select defaultValue="0" style={selSt}>
|
<select defaultValue="0" className={inputCls}>
|
||||||
{ops.map((op, i) => <option key={op.rescueOpsSn} value={i}>{op.opsCd} · {op.vesselNm}</option>)}
|
{ops.map((op, i) => <option key={op.rescueOpsSn} value={i}>{op.opsCd} · {op.vesselNm}</option>)}
|
||||||
<option value="new">+ 신규 사고 등록...</option>
|
<option value="new">+ 신규 사고 등록...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>시간 단계 (Time Step) <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>시간 단계 (Time Step) <span className="text-[#f87171]">*</span></label>
|
||||||
<select defaultValue="T+3h" style={selSt}>
|
<select defaultValue="T+3h" className={inputCls}>
|
||||||
{['T+0h (사고 발생 직후)', 'T+1h', 'T+2h', 'T+3h', 'T+6h', 'T+12h', 'T+24h', 'T+48h'].map(t => <option key={t} value={t}>{t}</option>)}
|
{['T+0h (사고 발생 직후)', 'T+1h', 'T+2h', 'T+3h', 'T+6h', 'T+12h', 'T+24h', 'T+48h'].map(t => <option key={t} value={t}>{t}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>기준 시각</label>
|
<label className={labelCls}>기준 시각</label>
|
||||||
<input type="datetime-local" defaultValue="2024-10-27T13:30" style={inputSt} />
|
<input type="datetime-local" defaultValue="2024-10-27T13:30" className={inputCls} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ② 선박 정보 */}
|
{/* ② 선박 정보 */}
|
||||||
<div>
|
<div>
|
||||||
<div style={sectionTitle}>{sectionIcon(2)} 선박 정보</div>
|
<div className={sectionTitleCls}>{sectionIcon(2)} 선박 정보</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
|
<div className="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>선박명 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>선박명 <span className="text-[#f87171]">*</span></label>
|
||||||
<input type="text" defaultValue="M/V SEA GUARDIAN" style={inputSt} />
|
<input type="text" defaultValue="M/V SEA GUARDIAN" className={inputCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>선종 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>선종 <span className="text-[#f87171]">*</span></label>
|
||||||
<select defaultValue="화물선 (Cargo)" style={selSt}>
|
<select defaultValue="화물선 (Cargo)" className={inputCls}>
|
||||||
{['유조선 (Tanker)', '화물선 (Cargo)', '컨테이너선 (Container)', '여객선 (Passenger)', '어선 (Fishing)', 'LNG선', '케미컬선 (Chemical)', '기타'].map(v => <option key={v}>{v}</option>)}
|
{['유조선 (Tanker)', '화물선 (Cargo)', '컨테이너선 (Container)', '여객선 (Passenger)', '어선 (Fishing)', 'LNG선', '케미컬선 (Chemical)', '기타'].map(v => <option key={v}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>총톤수 (GT)</label>
|
<label className={labelCls}>총톤수 (GT)</label>
|
||||||
<input type="number" defaultValue={45000} step={100} min={0} style={numSt} />
|
<input type="number" defaultValue={45000} step={100} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>선체 길이 (m)</label>
|
<label className={labelCls}>선체 길이 (m)</label>
|
||||||
<input type="number" defaultValue={189} step={1} min={0} style={numSt} />
|
<input type="number" defaultValue={189} step={1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>흘수 (m)</label>
|
<label className={labelCls}>흘수 (m)</label>
|
||||||
<input type="number" defaultValue={8.5} step={0.1} min={0} style={numSt} />
|
<input type="number" defaultValue={8.5} step={0.1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>승선 인원</label>
|
<label className={labelCls}>승선 인원</label>
|
||||||
<input type="number" defaultValue={22} step={1} min={0} style={numSt} />
|
<input type="number" defaultValue={22} step={1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 8, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
<div className="mt-2 grid grid-cols-2 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>적재 화물</label>
|
<label className={labelCls}>적재 화물</label>
|
||||||
<input type="text" placeholder="예: 일반화물, 벙커C 450kL" style={inputSt} />
|
<input type="text" placeholder="예: 일반화물, 벙커C 450kL" className={inputCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>연료유 종류 · 탑재량</label>
|
<label className={labelCls}>연료유 종류 · 탑재량</label>
|
||||||
<input type="text" defaultValue="벙커C 450kL / MGO 80kL" style={inputSt} />
|
<input type="text" defaultValue="벙커C 450kL / MGO 80kL" className={inputCls} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ③ 사고 조건 · 선체 상태 */}
|
{/* ③ 사고 조건 · 선체 상태 */}
|
||||||
<div>
|
<div>
|
||||||
<div style={sectionTitle}>{sectionIcon(3)} 사고 조건 · 선체 상태</div>
|
<div className={sectionTitleCls}>{sectionIcon(3)} 사고 조건 · 선체 상태</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
|
<div className="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>사고 유형 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>사고 유형 <span className="text-[#f87171]">*</span></label>
|
||||||
<select defaultValue="충돌 (Collision)" style={selSt}>
|
<select defaultValue="충돌 (Collision)" className={inputCls}>
|
||||||
{['충돌 (Collision)', '좌초 (Grounding)', '침수 (Flooding)', '기관고장 (Engine Failure)', '화재 (Fire)', '전복 (Capsizing)', '구조손상 (Structural)'].map(v => <option key={v}>{v}</option>)}
|
{['충돌 (Collision)', '좌초 (Grounding)', '침수 (Flooding)', '기관고장 (Engine Failure)', '화재 (Fire)', '전복 (Capsizing)', '구조손상 (Structural)'].map(v => <option key={v}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>손상 구역</label>
|
<label className={labelCls}>손상 구역</label>
|
||||||
<select defaultValue="우현 중앙 (Starboard Mid)" style={selSt}>
|
<select defaultValue="우현 중앙 (Starboard Mid)" className={inputCls}>
|
||||||
{['선수부 (Bow)', '우현 중앙 (Starboard Mid)', '좌현 중앙 (Port Mid)', '선미부 (Stern)', '기관실 (Engine Room)', '선저 (Bottom)', '복수 구역'].map(v => <option key={v}>{v}</option>)}
|
{['선수부 (Bow)', '우현 중앙 (Starboard Mid)', '좌현 중앙 (Port Mid)', '선미부 (Stern)', '기관실 (Engine Room)', '선저 (Bottom)', '복수 구역'].map(v => <option key={v}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>파공 크기 (㎡)</label>
|
<label className={labelCls}>파공 크기 (㎡)</label>
|
||||||
<input type="number" defaultValue={2.8} step={0.1} min={0} style={numSt} />
|
<input type="number" defaultValue={2.8} step={0.1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 8, display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10 }}>
|
<div className="mt-2 grid grid-cols-4 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>횡경사 (°)</label>
|
<label className={labelCls}>횡경사 (°)</label>
|
||||||
<input type="number" defaultValue={8.5} step={0.5} style={numSt} />
|
<input type="number" defaultValue={8.5} step={0.5} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>종경사 (°)</label>
|
<label className={labelCls}>종경사 (°)</label>
|
||||||
<input type="number" defaultValue={2.1} step={0.1} style={numSt} />
|
<input type="number" defaultValue={2.1} step={0.1} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>건현 (m)</label>
|
<label className={labelCls}>건현 (m)</label>
|
||||||
<input type="number" defaultValue={3.2} step={0.1} min={0} style={numSt} />
|
<input type="number" defaultValue={3.2} step={0.1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>GM (m)</label>
|
<label className={labelCls}>GM (m)</label>
|
||||||
<input type="number" defaultValue={1.8} step={0.1} style={numSt} />
|
<input type="number" defaultValue={1.8} step={0.1} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 침수 상태 */}
|
{/* 침수 상태 */}
|
||||||
<div style={{ marginTop: 10, padding: '10px 14px', background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.12)', borderRadius: 8 }}>
|
<div className="mt-2.5 px-3.5 py-2.5 rounded-md border border-[rgba(239,68,68,.12)]" style={{ background: 'rgba(239,68,68,.04)' }}>
|
||||||
<div style={{ fontSize: 9, fontWeight: 700, color: '#f87171', fontFamily: 'var(--fK)', marginBottom: 8 }}>💧 침수 상태</div>
|
<div className="text-[9px] font-bold text-[#f87171] mb-2">💧 침수 상태</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 8 }}>
|
<div className="grid grid-cols-4 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: 8, color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: 3 }}>침수 구역 수</label>
|
<label className="text-[8px] text-text-3 block mb-px">침수 구역 수</label>
|
||||||
<select defaultValue="2개" style={{ width: '100%', padding: '6px 8px', borderRadius: 4, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fK)', outline: 'none', boxSizing: 'border-box' as const }}>
|
<select defaultValue="2개" className="w-full px-2 py-1.5 rounded border border-border bg-bg-0 text-[10px] outline-none">
|
||||||
{['1개', '2개', '3개', '4개 이상'].map(v => <option key={v}>{v}</option>)}
|
{['1개', '2개', '3개', '4개 이상'].map(v => <option key={v}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: 8, color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: 3 }}>침수량 (톤)</label>
|
<label className="text-[8px] text-text-3 block mb-px">침수량 (톤)</label>
|
||||||
<input type="number" defaultValue={850} step={50} min={0} style={{ width: '100%', padding: '6px 8px', borderRadius: 4, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fM)', outline: 'none', boxSizing: 'border-box' as const }} />
|
<input type="number" defaultValue={850} step={50} min={0} className="w-full px-2 py-1.5 rounded border border-border bg-bg-0 text-[10px] font-mono outline-none" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: 8, color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: 3 }}>침수 진행률 (t/h)</label>
|
<label className="text-[8px] text-text-3 block mb-px">침수 진행률 (t/h)</label>
|
||||||
<input type="number" defaultValue={120} step={10} min={0} style={{ width: '100%', padding: '6px 8px', borderRadius: 4, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fM)', outline: 'none', boxSizing: 'border-box' as const }} />
|
<input type="number" defaultValue={120} step={10} min={0} className="w-full px-2 py-1.5 rounded border border-border bg-bg-0 text-[10px] font-mono outline-none" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: 8, color: 'var(--t3)', fontFamily: 'var(--fK)', display: 'block', marginBottom: 3 }}>배수 능력 (t/h)</label>
|
<label className="text-[8px] text-text-3 block mb-px">배수 능력 (t/h)</label>
|
||||||
<input type="number" defaultValue={80} step={10} min={0} style={{ width: '100%', padding: '6px 8px', borderRadius: 4, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fM)', outline: 'none', boxSizing: 'border-box' as const }} />
|
<input type="number" defaultValue={80} step={10} min={0} className="w-full px-2 py-1.5 rounded border border-border bg-bg-0 text-[10px] font-mono outline-none" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -557,85 +556,85 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
|||||||
|
|
||||||
{/* ④ 사고 위치 · 해상 조건 */}
|
{/* ④ 사고 위치 · 해상 조건 */}
|
||||||
<div>
|
<div>
|
||||||
<div style={sectionTitle}>{sectionIcon(4)} 사고 위치 · 해상 조건</div>
|
<div className={sectionTitleCls}>{sectionIcon(4)} 사고 위치 · 해상 조건</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 10, alignItems: 'end' }}>
|
<div className="grid gap-2.5 items-end" style={{ gridTemplateColumns: '1fr 1fr auto' }}>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>위도 (Lat)</label>
|
<label className={labelCls}>위도 (Lat)</label>
|
||||||
<input type="text" defaultValue="34.5832" style={{ ...inputSt, fontFamily: 'var(--fM)' }} />
|
<input type="text" defaultValue="34.5832" className={`${inputCls} font-mono`} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>경도 (Lon)</label>
|
<label className={labelCls}>경도 (Lon)</label>
|
||||||
<input type="text" defaultValue="128.4217" style={{ ...inputSt, fontFamily: 'var(--fM)' }} />
|
<input type="text" defaultValue="128.4217" className={`${inputCls} font-mono`} />
|
||||||
</div>
|
</div>
|
||||||
<button style={{ padding: '8px 14px', borderRadius: 6, border: '1px solid rgba(6,182,212,.3)', background: 'rgba(6,182,212,.08)', color: 'var(--cyan)', fontSize: 10, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)', whiteSpace: 'nowrap' }}>📍 지도에서 선택</button>
|
<button className="px-3.5 py-2 rounded-md border border-[rgba(6,182,212,.3)] text-primary-cyan text-[10px] font-semibold cursor-pointer whitespace-nowrap" style={{ background: 'rgba(6,182,212,.08)' }}>📍 지도에서 선택</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 8, display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10 }}>
|
<div className="mt-2 grid grid-cols-4 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>풍향 / 풍속 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>풍향 / 풍속 <span className="text-[#f87171]">*</span></label>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div className="flex gap-1">
|
||||||
<select defaultValue="SW" style={{ flex: 1, padding: 8, borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fK)', outline: 'none' }}>
|
<select defaultValue="SW" className="flex-1 p-2 rounded-md border border-border bg-bg-0 text-[10px] outline-none">
|
||||||
{['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'].map(d => <option key={d}>{d}</option>)}
|
{['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'].map(d => <option key={d}>{d}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input type="number" defaultValue={12.5} step={0.5} min={0} style={{ width: 60, padding: 8, borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fM)', outline: 'none', textAlign: 'center' }} />
|
<input type="number" defaultValue={12.5} step={0.5} min={0} className="w-[60px] p-2 rounded-md border border-border bg-bg-0 text-[10px] font-mono outline-none text-center" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>파고 (m) <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>파고 (m) <span className="text-[#f87171]">*</span></label>
|
||||||
<input type="number" defaultValue={2.5} step={0.1} min={0} style={numSt} />
|
<input type="number" defaultValue={2.5} step={0.1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>조류 방향 / 유속</label>
|
<label className={labelCls}>조류 방향 / 유속</label>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div className="flex gap-1">
|
||||||
<select defaultValue="NE" style={{ flex: 1, padding: 8, borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fK)', outline: 'none' }}>
|
<select defaultValue="NE" className="flex-1 p-2 rounded-md border border-border bg-bg-0 text-[10px] outline-none">
|
||||||
{['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'].map(d => <option key={d}>{d}</option>)}
|
{['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'].map(d => <option key={d}>{d}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input type="number" defaultValue={1.2} step={0.1} min={0} style={{ width: 60, padding: 8, borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fM)', outline: 'none', textAlign: 'center' }} />
|
<input type="number" defaultValue={1.2} step={0.1} min={0} className="w-[60px] p-2 rounded-md border border-border bg-bg-0 text-[10px] font-mono outline-none text-center" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>수심 (m)</label>
|
<label className={labelCls}>수심 (m)</label>
|
||||||
<input type="number" defaultValue={25} step={1} min={0} style={numSt} />
|
<input type="number" defaultValue={25} step={1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 6, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
|
<div className="mt-1.5 grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>시정 (km)</label>
|
<label className={labelCls}>시정 (km)</label>
|
||||||
<input type="number" defaultValue={8} step={1} min={0} style={numSt} />
|
<input type="number" defaultValue={8} step={1} min={0} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>해상 상태 (Douglas)</label>
|
<label className={labelCls}>해상 상태 (Douglas)</label>
|
||||||
<select defaultValue="3 — 거침 (Moderate)" style={selSt}>
|
<select defaultValue="3 — 거침 (Moderate)" className={inputCls}>
|
||||||
{['0 — 평온 (Calm)', '1 — 잔잔 (Smooth)', '2 — 약간 거침 (Slight)', '3 — 거침 (Moderate)', '4 — 다소 높음 (Rough)', '5 — 높음 (Very rough)', '6 — 매우 높음 (High)'].map(v => <option key={v}>{v}</option>)}
|
{['0 — 평온 (Calm)', '1 — 잔잔 (Smooth)', '2 — 약간 거침 (Slight)', '3 — 거침 (Moderate)', '4 — 다소 높음 (Rough)', '5 — 높음 (Very rough)', '6 — 매우 높음 (High)'].map(v => <option key={v}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>수온 (°C)</label>
|
<label className={labelCls}>수온 (°C)</label>
|
||||||
<input type="number" defaultValue={17.2} step={0.5} style={numSt} />
|
<input type="number" defaultValue={17.2} step={0.5} className={numCls} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ⑤ 구난 분석 설정 */}
|
{/* ⑤ 구난 분석 설정 */}
|
||||||
<div>
|
<div>
|
||||||
<div style={sectionTitle}>{sectionIcon(5)} 구난 분석 설정</div>
|
<div className={sectionTitleCls}>{sectionIcon(5)} 구난 분석 설정</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
|
<div className="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>분석 모델 <span style={{ color: '#f87171' }}>*</span></label>
|
<label className={labelCls}>분석 모델 <span className="text-[#f87171]">*</span></label>
|
||||||
<select defaultValue="R&D 긴급구난 종합분석" style={selSt}>
|
<select defaultValue="R&D 긴급구난 종합분석" className={inputCls}>
|
||||||
{['R&D 긴급구난 종합분석', 'HECSALV (Salvage)', 'NAPA Emergency', '수동 입력 (직접 판단)'].map(v => <option key={v}>{v}</option>)}
|
{['R&D 긴급구난 종합분석', 'HECSALV (Salvage)', 'NAPA Emergency', '수동 입력 (직접 판단)'].map(v => <option key={v}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>예측 시간</label>
|
<label className={labelCls}>예측 시간</label>
|
||||||
<select defaultValue="12시간" style={selSt}>
|
<select defaultValue="12시간" className={inputCls}>
|
||||||
{['6시간', '12시간', '24시간', '48시간', '72시간'].map(v => <option key={v}>{v}</option>)}
|
{['6시간', '12시간', '24시간', '48시간', '72시간'].map(v => <option key={v}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelSt}>분석 항목</label>
|
<label className={labelCls}>분석 항목</label>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 2 }}>
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||||
{['복원성', '예인력', '인양력', '유출 위험'].map(item => (
|
{['복원성', '예인력', '인양력', '유출 위험'].map(item => (
|
||||||
<label key={item} style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: 8, color: 'var(--t2)', fontFamily: 'var(--fK)', cursor: 'pointer' }}>
|
<label key={item} className="flex items-center gap-px text-[8px] text-text-2 cursor-pointer">
|
||||||
<input type="checkbox" defaultChecked style={{ accentColor: 'var(--cyan)', transform: 'scale(.85)' }} />{item}
|
<input type="checkbox" defaultChecked style={{ accentColor: 'var(--cyan)', transform: 'scale(.85)' }} />{item}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@ -643,37 +642,37 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* R&D 연계 분석 */}
|
{/* R&D 연계 분석 */}
|
||||||
<div style={{ marginTop: 10, padding: '10px 14px', background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.12)', borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div className="mt-2.5 px-3.5 py-2.5 flex flex-col gap-1.5 rounded-md border border-[rgba(249,115,22,.12)]" style={{ background: 'rgba(249,115,22,.04)' }}>
|
||||||
<div style={{ fontSize: 9, fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fK)' }}>🔗 R&D 연계 분석</div>
|
<div className="text-[9px] font-bold text-status-orange">🔗 R&D 연계 분석</div>
|
||||||
<div style={{ display: 'flex', gap: 12 }}>
|
<div className="flex gap-3">
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--t2)', fontFamily: 'var(--fK)', cursor: 'pointer' }}>
|
<label className="flex items-center gap-1 text-[9px] text-text-2 cursor-pointer">
|
||||||
<input type="checkbox" style={{ accentColor: 'var(--orange)' }} /> 유출유 확산예측 동시 실행
|
<input type="checkbox" style={{ accentColor: 'var(--orange)' }} /> 유출유 확산예측 동시 실행
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--t2)', fontFamily: 'var(--fK)', cursor: 'pointer' }}>
|
<label className="flex items-center gap-1 text-[9px] text-text-2 cursor-pointer">
|
||||||
<input type="checkbox" style={{ accentColor: 'var(--orange)' }} /> HNS 대기확산 연계 분석
|
<input type="checkbox" style={{ accentColor: 'var(--orange)' }} /> HNS 대기확산 연계 분석
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 8, color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.5 }}>화물 유출 가능성이 있는 경우, 긴급구난 분석 결과와 확산예측을 동시에 수행하여 종합 대응 판단을 지원합니다</div>
|
<div className="text-[8px] text-text-3 leading-[1.5]">화물 유출 가능성이 있는 경우, 긴급구난 분석 결과와 확산예측을 동시에 수행하여 종합 대응 판단을 지원합니다</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ⑥ 비고 */}
|
{/* ⑥ 비고 */}
|
||||||
<div>
|
<div>
|
||||||
<div style={sectionTitle}>{sectionIcon(6)} 비고</div>
|
<div className={sectionTitleCls}>{sectionIcon(6)} 비고</div>
|
||||||
<textarea placeholder="시나리오 설명, 가정 조건, 현장 상황 특이사항 등을 기록합니다..." style={{ width: '100%', height: 60, padding: '10px 12px', borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 10, fontFamily: 'var(--fK)', outline: 'none', resize: 'vertical', lineHeight: 1.6, boxSizing: 'border-box' as const, scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }} />
|
<textarea placeholder="시나리오 설명, 가정 조건, 현장 상황 특이사항 등을 기록합니다..." className="w-full h-[60px] px-3 py-2.5 rounded-md border border-border bg-bg-0 text-[10px] outline-none resize-y leading-relaxed scrollbar-thin" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 하단 버튼 ── */}
|
{/* ── 하단 버튼 ── */}
|
||||||
<div style={{ padding: '16px 24px', borderTop: '1px solid var(--bd)', flexShrink: 0, display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div className="px-6 py-4 border-t border-border shrink-0 flex gap-2 items-center">
|
||||||
<div style={{ flex: 1, fontSize: 9, color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.5 }}>
|
<div className="flex-1 text-[9px] text-text-3 leading-[1.5]">
|
||||||
<span style={{ color: '#f87171' }}>*</span> 필수 입력 항목 · 해상 조건은 기상청/조사원 API 연계 시 자동 갱신
|
<span className="text-[#f87171]">*</span> 필수 입력 항목 · 해상 조건은 기상청/조사원 API 연계 시 자동 갱신
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} style={{ padding: '10px 20px', borderRadius: 8, border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t2)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>취소</button>
|
<button onClick={onClose} className="px-5 py-2.5 rounded-md border border-border bg-bg-3 text-text-2 text-xs font-semibold cursor-pointer">취소</button>
|
||||||
{done ? (
|
{done ? (
|
||||||
<button onClick={onClose} style={{ padding: '10px 28px', borderRadius: 8, border: 'none', background: 'linear-gradient(135deg,#22c55e,#10b981)', color: '#fff', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'var(--fK)', boxShadow: '0 4px 16px rgba(34,197,94,.3)' }}>✅ 생성 완료 — 닫기</button>
|
<button onClick={onClose} className="px-7 py-2.5 rounded-md border-none text-white text-xs font-bold cursor-pointer" style={{ background: 'linear-gradient(135deg,#22c55e,#10b981)', boxShadow: '0 4px 16px rgba(34,197,94,.3)' }}>✅ 생성 완료 — 닫기</button>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={handleSubmit} disabled={submitting} style={{ padding: '10px 28px', borderRadius: 8, border: 'none', background: submitting ? 'var(--bg3)' : 'linear-gradient(135deg,#06b6d4,#3b82f6)', color: submitting ? 'var(--t3)' : '#fff', fontSize: 12, fontWeight: 700, cursor: submitting ? 'wait' : 'pointer', fontFamily: 'var(--fK)', boxShadow: submitting ? 'none' : '0 4px 16px rgba(6,182,212,.3)' }}>
|
<button onClick={handleSubmit} disabled={submitting} className="px-7 py-2.5 rounded-md border-none text-xs font-bold" style={{ background: submitting ? 'var(--bg3)' : 'linear-gradient(135deg,#06b6d4,#3b82f6)', color: submitting ? 'var(--t3)' : '#fff', cursor: submitting ? 'wait' : 'pointer', boxShadow: submitting ? 'none' : '0 4px 16px rgba(6,182,212,.3)' }}>
|
||||||
{submitting ? '⏳ 분석 중...' : '🚨 시나리오 생성 · 분석 실행'}
|
{submitting ? '⏳ 분석 중...' : '🚨 시나리오 생성 · 분석 실행'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -691,17 +690,17 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
|||||||
|
|
||||||
if (chartData.length === 0) {
|
if (chartData.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 40, textAlign: 'center', fontSize: 11, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>
|
<div className="p-10 text-center text-[11px] text-text-3">
|
||||||
비교할 시나리오 데이터가 없습니다.
|
비교할 시나리오 데이터가 없습니다.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 20 }}>
|
<div className="p-5">
|
||||||
{/* Chart 1: GM 추이 */}
|
{/* Chart 1: GM 추이 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 16, marginBottom: 16 }}>
|
<div className="bg-bg-3 border border-border rounded-[10px] p-4 mb-4">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--cyan)', fontFamily: 'var(--fK)', marginBottom: 10 }}>📈 GM (복원심) 변화 추이 (m)</div>
|
<div className="text-[11px] font-bold text-primary-cyan mb-2.5">📈 GM (복원심) 변화 추이 (m)</div>
|
||||||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 180 }}>
|
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 180 }}>
|
||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
{[0, 0.5, 1.0, 1.5, 2.0].map(v => {
|
{[0, 0.5, 1.0, 1.5, 2.0].map(v => {
|
||||||
@ -726,10 +725,10 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Charts 2 & 3: 2-column */}
|
{/* Charts 2 & 3: 2-column */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
{/* Chart 2: 횡경사 변화 */}
|
{/* Chart 2: 횡경사 변화 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 16 }}>
|
<div className="bg-bg-3 border border-border rounded-[10px] p-4">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--orange)', fontFamily: 'var(--fK)', marginBottom: 10 }}>📉 횡경사 (List) 변화 (°)</div>
|
<div className="text-[11px] font-bold text-status-orange mb-2.5">📉 횡경사 (List) 변화 (°)</div>
|
||||||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
|
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
|
||||||
{[0, 5, 10, 15, 20, 25].map(v => {
|
{[0, 5, 10, 15, 20, 25].map(v => {
|
||||||
const y = PY + ph - (v / 25) * ph
|
const y = PY + ph - (v / 25) * ph
|
||||||
@ -747,8 +746,8 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart 3: 유출률 변화 (bar) */}
|
{/* Chart 3: 유출률 변화 (bar) */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 16 }}>
|
<div className="bg-bg-3 border border-border rounded-[10px] p-4">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--red)', fontFamily: 'var(--fK)', marginBottom: 10 }}>📊 유출률 변화 (L/min)</div>
|
<div className="text-[11px] font-bold text-status-red mb-2.5">📊 유출률 변화 (L/min)</div>
|
||||||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
|
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
|
||||||
{[0, 50, 100, 150, 200].map(v => {
|
{[0, 50, 100, 150, 200].map(v => {
|
||||||
const y = PY + ph - (v / 200) * ph
|
const y = PY + ph - (v / 200) * ph
|
||||||
@ -770,14 +769,14 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart 4: 비교 테이블 */}
|
{/* Chart 4: 비교 테이블 */}
|
||||||
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 16 }}>
|
<div className="bg-bg-3 border border-border rounded-[10px] p-4">
|
||||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--t1)', fontFamily: 'var(--fK)', marginBottom: 10 }}>📋 시나리오 종합 비교표</div>
|
<div className="text-[11px] font-bold mb-2.5">📋 시나리오 종합 비교표</div>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 9, fontFamily: 'var(--fK)' }}>
|
<table className="w-full border-collapse text-[9px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ background: 'rgba(6,182,212,.06)' }}>
|
<tr style={{ background: 'rgba(6,182,212,.06)' }}>
|
||||||
<th style={{ padding: '7px 8px', textAlign: 'left', borderBottom: '2px solid var(--bdL)', color: 'var(--cyan)' }}>지표</th>
|
<th className="py-[7px] px-2 text-left border-b-2 border-[var(--bdL)] text-primary-cyan">지표</th>
|
||||||
{chartData.map(d => (
|
{chartData.map(d => (
|
||||||
<th key={d.id} style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)', color: SEV_COLOR[d.severity] }}>{d.id}<br /><span style={{ fontWeight: 400, fontSize: 8, color: 'var(--t3)' }}>{d.label}</span></th>
|
<th key={d.id} className="py-[7px] px-2 text-center border-b-2 border-[var(--bdL)]" style={{ color: SEV_COLOR[d.severity] }}>{d.id}<br /><span className="font-normal text-[8px] text-text-3">{d.label}</span></th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -791,9 +790,9 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
|||||||
{ label: '위험 등급', key: 'sev', fmt: (d: ChartDataItem) => d.severity, clr: (d: ChartDataItem) => SEV_COLOR[d.severity] },
|
{ label: '위험 등급', key: 'sev', fmt: (d: ChartDataItem) => d.severity, clr: (d: ChartDataItem) => SEV_COLOR[d.severity] },
|
||||||
].map(row => (
|
].map(row => (
|
||||||
<tr key={row.label} style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
<tr key={row.label} style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||||
<td style={{ padding: '6px 8px', fontWeight: 600, color: 'var(--t2)' }}>{row.label}</td>
|
<td className="py-1.5 px-2 font-semibold text-text-2">{row.label}</td>
|
||||||
{chartData.map(d => (
|
{chartData.map(d => (
|
||||||
<td key={d.id} style={{ padding: '6px 8px', textAlign: 'center', fontFamily: 'var(--fM)', fontWeight: 700, color: row.clr(d) }}>{row.fmt(d)}</td>
|
<td key={d.id} className="py-1.5 px-2 text-center font-mono font-bold" style={{ color: row.clr(d) }}>{row.fmt(d)}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -146,7 +146,7 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
const at = accidentTypes.find(t => t.id === activeType)!
|
const at = accidentTypes.find(t => t.id === activeType)!
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 relative overflow-hidden" style={{ background: '#0a1628' }}>
|
<div className="flex-1 relative overflow-hidden bg-[#0a1628]">
|
||||||
{/* 해양 배경 그라데이션 */}
|
{/* 해양 배경 그라데이션 */}
|
||||||
<div className="absolute inset-0" style={{
|
<div className="absolute inset-0" style={{
|
||||||
background: 'radial-gradient(ellipse at 30% 40%, rgba(6,90,130,.25) 0%, transparent 60%), radial-gradient(ellipse at 70% 60%, rgba(8,60,100,.2) 0%, transparent 50%), linear-gradient(180deg, #0a1628, #0d1f35 50%, #091520)'
|
background: 'radial-gradient(ellipse at 30% 40%, rgba(6,90,130,.25) 0%, transparent 60%), radial-gradient(ellipse at 70% 60%, rgba(8,60,100,.2) 0%, transparent 50%), linear-gradient(180deg, #0a1628, #0d1f35 50%, #091520)'
|
||||||
@ -173,17 +173,16 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 선박 모형 */}
|
{/* 선박 모형 */}
|
||||||
<div className="absolute z-[15]" style={{ top: '42%', left: '46%', transform: 'rotate(-15deg)' }}>
|
<div className="absolute z-[15] top-[42%] left-[46%] -rotate-[15deg]">
|
||||||
<div className="relative" style={{
|
<div className="relative w-[72px] h-5" style={{
|
||||||
width: '72px', height: '20px',
|
|
||||||
background: 'linear-gradient(90deg, #4a3728, #6b4c33)',
|
background: 'linear-gradient(90deg, #4a3728, #6b4c33)',
|
||||||
borderRadius: '3px 10px 10px 3px',
|
borderRadius: '3px 10px 10px 3px',
|
||||||
border: '1px solid rgba(255,150,50,.4)',
|
border: '1px solid rgba(255,150,50,.4)',
|
||||||
boxShadow: '0 0 18px rgba(255,100,0,.2)'
|
boxShadow: '0 0 18px rgba(255,100,0,.2)'
|
||||||
}}>
|
}}>
|
||||||
<div className="absolute" style={{ top: '-3px', left: '60%', width: '2px', height: '7px', background: '#888', borderRadius: '1px' }} />
|
<div className="absolute w-0.5 h-[7px] bg-[#888] rounded-[1px]" style={{ top: '-3px', left: '60%' }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[7px] text-center mt-0.5 font-mono" style={{ color: 'rgba(255,200,150,.5)' }}>M/V SEA GUARDIAN</div>
|
<div className="text-[7px] text-center mt-0.5 font-mono text-[rgba(255,200,150,0.5)]">M/V SEA GUARDIAN</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 예측 구역 원 */}
|
{/* 예측 구역 원 */}
|
||||||
@ -193,25 +192,20 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
border: '1.5px dashed rgba(6,182,212,.2)'
|
border: '1.5px dashed rgba(6,182,212,.2)'
|
||||||
}} />
|
}} />
|
||||||
{/* 구역 라벨 */}
|
{/* 구역 라벨 */}
|
||||||
<div className="absolute z-[6] text-[8px] font-korean font-semibold tracking-wider uppercase" style={{
|
<div className="absolute z-[6] text-[8px] font-korean font-semibold tracking-wider uppercase whitespace-pre-line text-[rgba(6,182,212,0.45)] top-1/2 left-[36%]">
|
||||||
top: '50%', left: '36%', color: 'rgba(6,182,212,.45)', whiteSpace: 'pre-line'
|
|
||||||
}}>
|
|
||||||
{d.zone.replace('\\n', '\n')}
|
{d.zone.replace('\\n', '\n')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SAR 자산 */}
|
{/* SAR 자산 */}
|
||||||
<div className="absolute z-10 text-[7px] font-mono" style={{ top: '10%', left: '42%', color: 'rgba(200,220,255,.35)' }}>ETA 5 MIN ─</div>
|
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[10%] left-[42%]">ETA 5 MIN ─</div>
|
||||||
<div className="absolute z-10 text-[7px] font-mono" style={{ top: '14%', left: '56%', color: 'rgba(200,220,255,.35)' }}>ETA 15 MIN ─</div>
|
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[14%] left-[56%]">ETA 15 MIN ─</div>
|
||||||
<div className="absolute z-[12] text-sm" style={{ top: '7%', left: '52%', opacity: 0.6, transform: 'rotate(-30deg)' }}>🚁</div>
|
<div className="absolute z-[12] text-sm opacity-60 top-[7%] left-[52%] -rotate-[30deg]">🚁</div>
|
||||||
<div className="absolute z-[12] text-[8px] font-mono" style={{ top: '20%', left: '60%', color: 'rgba(200,220,255,.45)' }}>6M</div>
|
<div className="absolute z-[12] text-[8px] font-mono text-[rgba(200,220,255,0.45)] top-[20%] left-[60%]">6M</div>
|
||||||
<div className="absolute z-[12] text-[11px]" style={{ top: '28%', left: '54%', opacity: 0.45 }}>🚢</div>
|
<div className="absolute z-[12] text-[11px] opacity-45 top-[28%] left-[54%]">🚢</div>
|
||||||
|
|
||||||
{/* 환경 민감 구역 */}
|
{/* 환경 민감 구역 */}
|
||||||
<div className="absolute z-10 px-3.5 py-2 rounded" style={{
|
<div className="absolute z-10 px-3.5 py-2 rounded bg-[rgba(34,197,94,0.06)] border border-[rgba(34,197,94,0.2)] bottom-[6%] right-[6%]">
|
||||||
bottom: '6%', right: '6%',
|
<div className="text-[9px] font-bold font-serif uppercase tracking-wider leading-snug text-[rgba(34,197,94,0.55)]">
|
||||||
background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.2)'
|
|
||||||
}}>
|
|
||||||
<div className="text-[9px] font-bold font-serif uppercase tracking-wider leading-snug" style={{ color: 'rgba(34,197,94,.55)' }}>
|
|
||||||
ENVIRONMENTALLY SENSITIVE<br />AREA: AQUACULTURE FARM
|
ENVIRONMENTALLY SENSITIVE<br />AREA: AQUACULTURE FARM
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -226,13 +220,14 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
{/* 스케일 바 */}
|
{/* 스케일 바 */}
|
||||||
<div className="absolute bottom-2.5 left-2.5 z-20 bg-[rgba(13,17,23,0.8)] border border-border rounded px-2.5 py-1 text-[8px] text-text-3 font-mono">
|
<div className="absolute bottom-2.5 left-2.5 z-20 bg-[rgba(13,17,23,0.8)] border border-border rounded px-2.5 py-1 text-[8px] text-text-3 font-mono">
|
||||||
<div className="w-[70px] h-0.5 mb-0.5" style={{ background: 'linear-gradient(90deg, #e4e8f1 50%, var(--bd) 50%)' }} />
|
<div className="w-[70px] h-0.5 mb-0.5" style={{ background: 'linear-gradient(90deg, #e4e8f1 50%, var(--bd) 50%)' }} />
|
||||||
|
|
||||||
5 km · Zoom: 100%
|
5 km · Zoom: 100%
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사고 유형 표시 */}
|
{/* 사고 유형 표시 */}
|
||||||
<div className="absolute bottom-2.5 right-2.5 z-20 bg-[rgba(13,17,23,0.85)] border border-border rounded px-3 py-1.5">
|
<div className="absolute bottom-2.5 right-2.5 z-20 bg-[rgba(13,17,23,0.85)] border border-border rounded px-3 py-1.5">
|
||||||
<div className="text-[8px] text-text-3 font-korean">현재 사고 유형</div>
|
<div className="text-[8px] text-text-3 font-korean">현재 사고 유형</div>
|
||||||
<div className="text-[11px] font-bold font-korean" style={{ color: 'var(--cyan)' }}>{at.icon} {at.label} ({at.eng})</div>
|
<div className="text-[11px] font-bold font-korean text-primary-cyan">{at.icon} {at.label} ({at.eng})</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -502,7 +497,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
<div className="text-[9px] font-bold text-text-1 font-korean mb-1.5">해상 e-Call (GMDSS / VHF-DSC)</div>
|
<div className="text-[9px] font-bold text-text-1 font-korean mb-1.5">해상 e-Call (GMDSS / VHF-DSC)</div>
|
||||||
<div className="flex flex-col gap-0.5 text-[7px] font-mono text-text-3">
|
<div className="flex flex-col gap-0.5 text-[7px] font-mono text-text-3">
|
||||||
{[
|
{[
|
||||||
{ label: 'MMSI', value: '440123456', color: 'var(--t1)' },
|
{ label: 'MMSI', value: '440123456' },
|
||||||
{ label: 'Nature of Distress', value: 'COLLISION', color: 'var(--red)' },
|
{ label: 'Nature of Distress', value: 'COLLISION', color: 'var(--red)' },
|
||||||
{ label: 'DSC Alert', value: 'SENT ✓', color: 'var(--green)' },
|
{ label: 'DSC Alert', value: 'SENT ✓', color: 'var(--green)' },
|
||||||
{ label: 'EPIRB', value: 'ACTIVATED ✓', color: 'var(--green)' },
|
{ label: 'EPIRB', value: 'ACTIVATED ✓', color: 'var(--green)' },
|
||||||
@ -593,7 +588,7 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
<div className="grid grid-cols-2 gap-1 text-[8px] font-mono">
|
<div className="grid grid-cols-2 gap-1 text-[8px] font-mono">
|
||||||
{[
|
{[
|
||||||
{ label: '지반반력', value: '1,240 kN', color: 'var(--yellow)' },
|
{ label: '지반반력', value: '1,240 kN', color: 'var(--yellow)' },
|
||||||
{ label: '접촉 면적', value: '12.5 m²', color: 'var(--t1)' },
|
{ label: '접촉 면적', value: '12.5 m²' },
|
||||||
{ label: '제거력(Removal)', value: '1,850 kN', color: 'var(--red)' },
|
{ label: '제거력(Removal)', value: '1,850 kN', color: 'var(--red)' },
|
||||||
{ label: '좌초 GM', value: '0.65 m', color: 'var(--yellow)' },
|
{ label: '좌초 GM', value: '0.65 m', color: 'var(--yellow)' },
|
||||||
].map((r, i) => (
|
].map((r, i) => (
|
||||||
@ -687,14 +682,14 @@ function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
|||||||
<div className="text-[8px] text-text-3 font-korean">SF 최대/허용 비율</div>
|
<div className="text-[8px] text-text-3 font-korean">SF 최대/허용 비율</div>
|
||||||
<div className="text-lg font-bold text-status-yellow font-mono">88<span className="text-[9px]">%</span></div>
|
<div className="text-lg font-bold text-status-yellow font-mono">88<span className="text-[9px]">%</span></div>
|
||||||
<div className="h-[5px] bg-bg-hover rounded-sm mt-0.5">
|
<div className="h-[5px] bg-bg-hover rounded-sm mt-0.5">
|
||||||
<div className="h-full rounded-sm" style={{ width: '88%', background: 'var(--yellow)' }} />
|
<div className="h-full rounded-sm w-[88%] bg-status-yellow" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||||||
<div className="text-[8px] text-text-3 font-korean">BM 최대/허용 비율</div>
|
<div className="text-[8px] text-text-3 font-korean">BM 최대/허용 비율</div>
|
||||||
<div className="text-lg font-bold text-status-orange font-mono">92<span className="text-[9px]">%</span></div>
|
<div className="text-lg font-bold text-status-orange font-mono">92<span className="text-[9px]">%</span></div>
|
||||||
<div className="h-[5px] bg-bg-hover rounded-sm mt-0.5">
|
<div className="h-[5px] bg-bg-hover rounded-sm mt-0.5">
|
||||||
<div className="h-full rounded-sm" style={{ width: '92%', background: 'var(--orange)' }} />
|
<div className="h-full rounded-sm w-[92%] bg-status-orange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -750,12 +745,12 @@ function BottomBar() {
|
|||||||
<div className="flex-1 overflow-y-auto px-3 py-1 font-mono text-[9px] leading-[1.7] scrollbar-thin">
|
<div className="flex-1 overflow-y-auto px-3 py-1 font-mono text-[9px] leading-[1.7] scrollbar-thin">
|
||||||
{[
|
{[
|
||||||
{ time: '10:35', msg: 'SOS FROM M/V SEA GUARDIAN', color: 'var(--red)', bold: true },
|
{ time: '10:35', msg: 'SOS FROM M/V SEA GUARDIAN', color: 'var(--red)', bold: true },
|
||||||
{ time: '10:35', msg: 'OIL LEAK DETECTED SENSOR #3', color: 'var(--t1)', bold: false },
|
{ time: '10:35', msg: 'OIL LEAK DETECTED SENSOR #3', bold: false },
|
||||||
{ time: '10:40', msg: 'CG HELO DISPATCHED', color: 'var(--red)', bold: true },
|
{ time: '10:40', msg: 'CG HELO DISPATCHED', color: 'var(--red)', bold: true },
|
||||||
{ time: '10:41', msg: 'GM CRITICAL ALERT — DAMAGE STABILITY FAIL', color: 'var(--red)', bold: true },
|
{ time: '10:41', msg: 'GM CRITICAL ALERT — DAMAGE STABILITY FAIL', color: 'var(--red)', bold: true },
|
||||||
{ time: '10:42', msg: 'Coast Guard 123 en route — ETA 15 min', color: 'var(--blue)', bold: false },
|
{ time: '10:42', msg: 'Coast Guard 123 en route — ETA 15 min', color: 'var(--blue)', bold: false },
|
||||||
{ time: '10:43', msg: 'LONGITUDINAL STRENGTH WARNING — BM 92% of LIMIT', color: 'var(--yellow)', bold: false },
|
{ time: '10:43', msg: 'LONGITUDINAL STRENGTH WARNING — BM 92% of LIMIT', color: 'var(--yellow)', bold: false },
|
||||||
{ time: '10:45', msg: 'BALLAST TRANSFER INITIATED — PORT #2 → STBD #3', color: 'var(--t1)', bold: false },
|
{ time: '10:45', msg: 'BALLAST TRANSFER INITIATED — PORT #2 → STBD #3', bold: false },
|
||||||
{ time: '10:48', msg: 'LIST INCREASING — 12° → 15°', color: 'var(--yellow)', bold: false },
|
{ time: '10:48', msg: 'LIST INCREASING — 12° → 15°', color: 'var(--yellow)', bold: false },
|
||||||
{ time: '10:50', msg: 'RESCUE HELO ON SCENE — HOISTING OPS', color: 'var(--blue)', bold: false },
|
{ time: '10:50', msg: 'RESCUE HELO ON SCENE — HOISTING OPS', color: 'var(--blue)', bold: false },
|
||||||
{ time: '10:55', msg: '5 SURVIVORS RECOVERED BY HELO', color: 'var(--green)', bold: false },
|
{ time: '10:55', msg: '5 SURVIVORS RECOVERED BY HELO', color: 'var(--green)', bold: false },
|
||||||
@ -779,9 +774,9 @@ function BottomBar() {
|
|||||||
<span>[+24H]</span>
|
<span>[+24H]</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-1.5 bg-bg-hover rounded-sm mx-1">
|
<div className="relative h-1.5 bg-bg-hover rounded-sm mx-1">
|
||||||
<div className="absolute rounded-full border-2 border-bg-0" style={{
|
<div className="absolute rounded-full border-2 border-bg-0 bg-primary-cyan" style={{
|
||||||
left: '35%', top: '-3px', width: '12px', height: '12px',
|
left: '35%', top: '-3px', width: '12px', height: '12px',
|
||||||
background: 'var(--cyan)', boxShadow: '0 0 8px rgba(6,182,212,.4)'
|
boxShadow: '0 0 8px rgba(6,182,212,.4)'
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-2 mt-0.5">
|
<div className="flex items-center justify-center gap-2 mt-0.5">
|
||||||
@ -790,7 +785,7 @@ function BottomBar() {
|
|||||||
<button className="w-7 h-7 bg-bg-3 border border-border rounded-full text-text-3 text-[11px] flex items-center justify-center cursor-pointer hover:text-text-1">⏭</button>
|
<button className="w-7 h-7 bg-bg-3 border border-border rounded-full text-text-3 text-[11px] flex items-center justify-center cursor-pointer hover:text-text-1">⏭</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-[8px] text-text-3 font-mono">
|
<div className="text-center text-[8px] text-text-3 font-mono">
|
||||||
현재 시간: <b style={{ color: 'var(--cyan)' }}>10:45 KST</b>
|
현재 시간: <b className="text-primary-cyan">10:45 KST</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -238,7 +238,7 @@ function ScatMap({ segments, selectedSeg, onSelectSeg, onOpenPopup }: ScatMapPro
|
|||||||
<Map
|
<Map
|
||||||
initialViewState={{ longitude: 126.55, latitude: 33.38, zoom: 10 }}
|
initialViewState={{ longitude: 126.55, latitude: 33.38, zoom: 10 }}
|
||||||
mapStyle={BASE_STYLE}
|
mapStyle={BASE_STYLE}
|
||||||
style={{ width: '100%', height: '100%' }}
|
className="w-full h-full"
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
onZoom={e => setZoom(e.viewState.zoom)}
|
onZoom={e => setZoom(e.viewState.zoom)}
|
||||||
>
|
>
|
||||||
@ -320,8 +320,8 @@ function ScatMap({ segments, selectedSeg, onSelectSeg, onOpenPopup }: ScatMapPro
|
|||||||
<div className="h-full transition-all duration-500" style={{ width: `${notPct}%`, background: 'var(--bd)' }} />
|
<div className="h-full transition-all duration-500" style={{ width: `${notPct}%`, background: 'var(--bd)' }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-1">
|
<div className="flex justify-between mt-1">
|
||||||
<span className="text-[9px] font-mono" style={{ color: 'var(--green)' }}>완료 {donePct}%</span>
|
<span className="text-[9px] font-mono text-status-green">완료 {donePct}%</span>
|
||||||
<span className="text-[9px] font-mono" style={{ color: 'var(--orange)' }}>진행 {progPct}%</span>
|
<span className="text-[9px] font-mono text-status-orange">진행 {progPct}%</span>
|
||||||
<span className="text-[9px] font-mono text-text-3">미조사 {notPct}%</span>
|
<span className="text-[9px] font-mono text-text-3">미조사 {notPct}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2.5">
|
<div className="mt-2.5">
|
||||||
|
|||||||
@ -121,7 +121,7 @@ function PopupMap({
|
|||||||
key={`${lat}-${lng}`}
|
key={`${lat}-${lng}`}
|
||||||
initialViewState={{ longitude: lng, latitude: lat, zoom: 15 }}
|
initialViewState={{ longitude: lng, latitude: lat, zoom: 15 }}
|
||||||
mapStyle={BASE_STYLE}
|
mapStyle={BASE_STYLE}
|
||||||
style={{ width: '100%', height: '100%' }}
|
className="w-full h-full"
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<DeckGLOverlay layers={deckLayers} />
|
<DeckGLOverlay layers={deckLayers} />
|
||||||
|
|||||||
@ -125,11 +125,11 @@ function ScatTimeline({
|
|||||||
<div className="flex gap-3.5">
|
<div className="flex gap-3.5">
|
||||||
<span className="flex items-center gap-1.5 text-[11px]">
|
<span className="flex items-center gap-1.5 text-[11px]">
|
||||||
<span className="text-text-2 font-korean">완료</span>
|
<span className="text-text-2 font-korean">완료</span>
|
||||||
<span className="text-text-1 font-semibold font-mono" style={{ color: 'var(--green)' }}>{doneCount}/{segments.length}</span>
|
<span className="text-text-1 font-semibold font-mono text-status-green">{doneCount}/{segments.length}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1.5 text-[11px]">
|
<span className="flex items-center gap-1.5 text-[11px]">
|
||||||
<span className="text-text-2 font-korean">진행중</span>
|
<span className="text-text-2 font-korean">진행중</span>
|
||||||
<span className="text-text-1 font-semibold font-mono" style={{ color: 'var(--orange)' }}>{progCount}/{segments.length}</span>
|
<span className="text-text-1 font-semibold font-mono text-status-orange">{progCount}/{segments.length}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1.5 text-[11px]">
|
<span className="flex items-center gap-1.5 text-[11px]">
|
||||||
<span className="text-text-2 font-korean">총 해안선</span>
|
<span className="text-text-2 font-korean">총 해안선</span>
|
||||||
|
|||||||
@ -92,11 +92,8 @@ export function WeatherMapOverlay({
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
transform: `rotate(${station.wind.direction}deg)`,
|
transform: `rotate(${station.wind.direction}deg)`,
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
}}
|
||||||
|
className="flex items-center justify-center cursor-pointer"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
@ -138,70 +135,40 @@ export function WeatherMapOverlay({
|
|||||||
style={{
|
style={{
|
||||||
background: boxBg,
|
background: boxBg,
|
||||||
border: `2px solid ${boxBorder}`,
|
border: `2px solid ${boxBorder}`,
|
||||||
borderRadius: 10,
|
|
||||||
padding: 8,
|
|
||||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
|
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 4,
|
|
||||||
minWidth: 70,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
}}
|
||||||
|
className="rounded-[10px] p-2 flex flex-col gap-1 min-w-[70px] cursor-pointer"
|
||||||
>
|
>
|
||||||
{/* 관측소명 */}
|
{/* 관측소명 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: textColor,
|
color: textColor,
|
||||||
textShadow: '1px 1px 3px rgba(0,0,0,0.7)',
|
textShadow: '1px 1px 3px rgba(0,0,0,0.7)',
|
||||||
paddingBottom: 4,
|
|
||||||
borderBottom: `1px solid ${isSelected ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.3)'}`,
|
borderBottom: `1px solid ${isSelected ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.3)'}`,
|
||||||
marginBottom: 2,
|
|
||||||
}}
|
}}
|
||||||
|
className="text-center text-xs font-bold pb-1 mb-0.5"
|
||||||
>
|
>
|
||||||
{station.name}
|
{station.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 수온 */}
|
{/* 수온 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
style={{
|
className="w-6 h-6 rounded-full flex items-center justify-center text-[11px] text-white font-bold"
|
||||||
background: 'linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%)',
|
style={{ background: 'linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%)', boxShadow: '0 2px 4px rgba(0,0,0,0.15)' }}
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: 11,
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
🌡️
|
🌡️
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 2 }}>
|
<div className="flex items-baseline gap-0.5">
|
||||||
<span
|
<span
|
||||||
style={{
|
className="text-sm font-bold text-white"
|
||||||
fontSize: 14,
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#fff',
|
|
||||||
textShadow: '1px 1px 2px rgba(0,0,0,0.5)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{station.temperature.current.toFixed(1)}
|
{station.temperature.current.toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
className="text-[10px] text-white opacity-90"
|
||||||
fontSize: 10,
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}
|
||||||
color: '#fff',
|
|
||||||
opacity: 0.9,
|
|
||||||
textShadow: '1px 1px 2px rgba(0,0,0,0.5)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
°C
|
°C
|
||||||
</span>
|
</span>
|
||||||
@ -209,42 +176,23 @@ export function WeatherMapOverlay({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 파고 */}
|
{/* 파고 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
style={{
|
className="w-6 h-6 rounded-full flex items-center justify-center text-[11px] text-white font-bold"
|
||||||
background: 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)',
|
style={{ background: 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)', boxShadow: '0 2px 4px rgba(0,0,0,0.15)' }}
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: 11,
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
🌊
|
🌊
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 2 }}>
|
<div className="flex items-baseline gap-0.5">
|
||||||
<span
|
<span
|
||||||
style={{
|
className="text-sm font-bold text-white"
|
||||||
fontSize: 14,
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#fff',
|
|
||||||
textShadow: '1px 1px 2px rgba(0,0,0,0.5)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{station.wave.height.toFixed(1)}
|
{station.wave.height.toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
className="text-[10px] text-white opacity-90"
|
||||||
fontSize: 10,
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}
|
||||||
color: '#fff',
|
|
||||||
opacity: 0.9,
|
|
||||||
textShadow: '1px 1px 2px rgba(0,0,0,0.5)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
m
|
m
|
||||||
</span>
|
</span>
|
||||||
@ -252,42 +200,23 @@ export function WeatherMapOverlay({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 풍속 */}
|
{/* 풍속 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div className="flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
style={{
|
className="w-6 h-6 rounded-full flex items-center justify-center text-[11px] text-white font-bold"
|
||||||
background: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)',
|
style={{ background: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)', boxShadow: '0 2px 4px rgba(0,0,0,0.15)' }}
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: 11,
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
💨
|
💨
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 2 }}>
|
<div className="flex items-baseline gap-0.5">
|
||||||
<span
|
<span
|
||||||
style={{
|
className="text-sm font-bold text-white"
|
||||||
fontSize: 14,
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#fff',
|
|
||||||
textShadow: '1px 1px 2px rgba(0,0,0,0.5)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{station.wind.speed.toFixed(1)}
|
{station.wind.speed.toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
className="text-[10px] text-white opacity-90"
|
||||||
fontSize: 10,
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}
|
||||||
color: '#fff',
|
|
||||||
opacity: 0.9,
|
|
||||||
textShadow: '1px 1px 2px rgba(0,0,0,0.5)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
m/s
|
m/s
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -36,10 +36,7 @@ interface WeatherRightPanelProps {
|
|||||||
export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||||
if (!weatherData) {
|
if (!weatherData) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden w-[380px] shrink-0">
|
||||||
className="flex flex-col bg-bg-1 border-l border-border overflow-hidden"
|
|
||||||
style={{ width: '380px', flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<p className="text-text-3 text-sm">지도에서 해양 지점을 클릭하세요</p>
|
<p className="text-text-3 text-sm">지도에서 해양 지점을 클릭하세요</p>
|
||||||
</div>
|
</div>
|
||||||
@ -55,10 +52,7 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
|||||||
const moonVisibility = '6.7 m'
|
const moonVisibility = '6.7 m'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden w-[380px] shrink-0">
|
||||||
className="flex flex-col bg-bg-1 border-l border-border overflow-hidden"
|
|
||||||
style={{ width: '380px', flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-6 py-4 border-b border-border">
|
<div className="px-6 py-4 border-b border-border">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
|||||||
@ -122,7 +122,7 @@ function WeatherMapControls() {
|
|||||||
const { current: map } = useMap()
|
const { current: map } = useMap()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'absolute', top: 16, right: 16, zIndex: 10 }}>
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => map?.zoomIn()}
|
onClick={() => map?.zoomIn()}
|
||||||
@ -310,7 +310,7 @@ export function WeatherView() {
|
|||||||
{/* Main Map Area */}
|
{/* Main Map Area */}
|
||||||
<div className="flex-1 relative flex flex-col overflow-hidden">
|
<div className="flex-1 relative flex flex-col overflow-hidden">
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<div className="flex items-center border-b border-border bg-bg-1" style={{ flexShrink: 0 }}>
|
<div className="flex items-center border-b border-border bg-bg-1 shrink-0">
|
||||||
<div className="flex items-center gap-2 px-6">
|
<div className="flex items-center gap-2 px-6">
|
||||||
{(['0', '3', '6', '9'] as TimeOffset[]).map((offset) => (
|
{(['0', '3', '6', '9'] as TimeOffset[]).map((offset) => (
|
||||||
<button
|
<button
|
||||||
@ -360,7 +360,7 @@ export function WeatherView() {
|
|||||||
zoom: WEATHER_MAP_ZOOM,
|
zoom: WEATHER_MAP_ZOOM,
|
||||||
}}
|
}}
|
||||||
mapStyle={WEATHER_MAP_STYLE}
|
mapStyle={WEATHER_MAP_STYLE}
|
||||||
style={{ width: '100%', height: '100%' }}
|
className="w-full h-full"
|
||||||
onClick={handleMapClick}
|
onClick={handleMapClick}
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
@ -375,7 +375,7 @@ export function WeatherView() {
|
|||||||
</Map>
|
</Map>
|
||||||
|
|
||||||
{/* 레이어 컨트롤 */}
|
{/* 레이어 컨트롤 */}
|
||||||
<div className="absolute top-6 left-6 bg-bg-1/90 border border-border rounded-lg p-4 backdrop-blur-sm" style={{ zIndex: 10 }}>
|
<div className="absolute top-6 left-6 bg-bg-1/90 border border-border rounded-lg p-4 backdrop-blur-sm z-10">
|
||||||
<div className="text-sm font-semibold text-text-1 mb-3">기상 레이어</div>
|
<div className="text-sm font-semibold text-text-1 mb-3">기상 레이어</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
@ -511,7 +511,7 @@ export function WeatherView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 범례 */}
|
{/* 범례 */}
|
||||||
<div className="absolute bottom-6 left-6 bg-bg-1/90 border border-border rounded-lg p-4 backdrop-blur-sm" style={{ zIndex: 10 }}>
|
<div className="absolute bottom-6 left-6 bg-bg-1/90 border border-border rounded-lg p-4 backdrop-blur-sm z-10">
|
||||||
<div className="text-sm font-semibold text-text-1 mb-3">기상 범례</div>
|
<div className="text-sm font-semibold text-text-1 mb-3">기상 범례</div>
|
||||||
<div className="space-y-3 text-xs">
|
<div className="space-y-3 text-xs">
|
||||||
{/* 바람 (Windy 스타일) */}
|
{/* 바람 (Windy 스타일) */}
|
||||||
@ -527,10 +527,7 @@ export function WeatherView() {
|
|||||||
<div className="flex-1 h-full" style={{ background: '#f05421' }} />
|
<div className="flex-1 h-full" style={{ background: '#f05421' }} />
|
||||||
<div className="flex-1 h-full" style={{ background: '#b41e46' }} />
|
<div className="flex-1 h-full" style={{ background: '#b41e46' }} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="flex justify-between text-text-3 text-[9px]">
|
||||||
className="flex justify-between text-text-3"
|
|
||||||
style={{ fontSize: '9px' }}
|
|
||||||
>
|
|
||||||
<span>3</span>
|
<span>3</span>
|
||||||
<span>5</span>
|
<span>5</span>
|
||||||
<span>7</span>
|
<span>7</span>
|
||||||
|
|||||||
@ -19,9 +19,9 @@ export default {
|
|||||||
light: '#2a3a5c',
|
light: '#2a3a5c',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
1: '#e4e8f1',
|
1: '#edf0f7',
|
||||||
2: '#8892a8',
|
2: '#b0b8cc',
|
||||||
3: '#5a6478',
|
3: '#8690a6',
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
blue: '#3b82f6',
|
blue: '#3b82f6',
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user