- RescueView: CenterMap을 MapView(useBaseMapStyle) 기반 OSM 지도로 교체 - RescueScenarioView: BASE_STYLE → useBaseMapStyle로 전환하여 OSM 통일 - 긴급구난 시나리오 시드 데이터 10건으로 확장 (모델 이론 기반) - 관리자 비식별화조치 R&D 패널 5종 추가 (HNS대기, KOSPS, POSEIDON, Rescue, 시스템아키텍처) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1545 lines
64 KiB
TypeScript
1545 lines
64 KiB
TypeScript
import { useState } from 'react';
|
||
|
||
type TabId = 'framework' | 'target' | 'interface' | 'heterogeneous' | 'common-features';
|
||
|
||
const TABS: { id: TabId; label: string }[] = [
|
||
{ id: 'framework', label: '표준 프레임워크' },
|
||
{ id: 'target', label: '목표시스템 아키텍쳐' },
|
||
{ id: 'interface', label: '시스템 인터페이스 연계' },
|
||
{ id: 'heterogeneous', label: '이기종시스템연계' },
|
||
{ id: 'common-features', label: '공통기능' },
|
||
];
|
||
|
||
// ─── 기술 스택 테이블 데이터 ──────────────────────────────────────────────────────
|
||
|
||
interface TechStackRow {
|
||
category: string;
|
||
tech: string;
|
||
version: string;
|
||
description: string;
|
||
}
|
||
|
||
const TECH_STACK: TechStackRow[] = [
|
||
{ category: 'Frontend', tech: 'React', version: '19.x', description: '컴포넌트 기반 SPA' },
|
||
{ category: 'Frontend', tech: 'TypeScript', version: '5.9', description: '정적 타입 시스템' },
|
||
{ category: 'Frontend', tech: 'Vite', version: '7.x', description: '빌드 도구 (HMR)' },
|
||
{ category: 'Frontend', tech: 'Tailwind CSS', version: '3.x', description: '유틸리티 기반 CSS' },
|
||
{ category: 'Frontend', tech: 'MapLibre GL', version: '5.x', description: '오픈소스 GIS 엔진' },
|
||
{ category: 'Frontend', tech: 'deck.gl', version: '9.x', description: '대규모 데이터 시각화' },
|
||
{ category: 'Frontend', tech: 'Zustand', version: '-', description: '클라이언트 상태관리' },
|
||
{ category: 'Frontend', tech: 'TanStack Query', version: '-', description: '서버 상태관리/캐싱' },
|
||
{ category: 'Backend', tech: 'Express', version: '4.x', description: 'REST API 서버' },
|
||
{ category: 'Backend', tech: 'Socket.IO', version: '-', description: '실시간 양방향 통신' },
|
||
{ category: 'DB', tech: 'PostgreSQL', version: '16', description: '관계형 데이터베이스' },
|
||
{ category: 'DB', tech: 'PostGIS', version: '-', description: '공간정보 확장' },
|
||
{
|
||
category: '인증',
|
||
tech: 'JWT',
|
||
version: '-',
|
||
description: '토큰 기반 인증 (HttpOnly Cookie)',
|
||
},
|
||
{ category: '인증', tech: 'Google OAuth', version: '2.0', description: 'SSO 연동' },
|
||
{ category: '보안', tech: 'Helmet', version: '-', description: 'HTTP 헤더 보안' },
|
||
{ category: '보안', tech: 'Rate Limiting', version: '-', description: 'API 호출 제한' },
|
||
{ category: 'CI/CD', tech: 'Gitea Actions', version: '-', description: '자동 빌드/배포' },
|
||
];
|
||
|
||
// ─── 탭 모듈 데이터 ───────────────────────────────────────────────────────────────
|
||
|
||
interface TabModuleRow {
|
||
module: string;
|
||
name: string;
|
||
feature: string;
|
||
integration: string;
|
||
}
|
||
|
||
const TAB_MODULES: TabModuleRow[] = [
|
||
{
|
||
module: '확산예측',
|
||
name: 'prediction',
|
||
feature: '유출유 확산 시뮬레이션, 역추적 분석, 오일붐 배치',
|
||
integration: 'KOSPS, 포세이돈 R&D',
|
||
},
|
||
{
|
||
module: 'HNS 분석',
|
||
name: 'hns',
|
||
feature: '화학물질 확산 예측, 물질 DB, 위험도 평가',
|
||
integration: '충북대 R&D, 물질 DB',
|
||
},
|
||
{
|
||
module: '구조 시나리오',
|
||
name: 'rescue',
|
||
feature: '긴급구난 분석, 표류 예측',
|
||
integration: '긴급구난 R&D',
|
||
},
|
||
{
|
||
module: '항공 방제',
|
||
name: 'aerial',
|
||
feature: '위성영상 분석, 드론 영상, 유막 면적 분석',
|
||
integration: '위성/드론 데이터',
|
||
},
|
||
{
|
||
module: '해양 기상',
|
||
name: 'weather',
|
||
feature: '기상·해상 정보, 조위·해류 관측',
|
||
integration: 'KHOA API, 기상청 API',
|
||
},
|
||
{
|
||
module: '사건/사고',
|
||
name: 'incidents',
|
||
feature: '해양오염 사고 등록·관리·이력',
|
||
integration: '해경 사고 DB',
|
||
},
|
||
{
|
||
module: '자산 관리',
|
||
name: 'assets',
|
||
feature: '기관·장비·선박 보험 관리',
|
||
integration: '해경 자산 DB',
|
||
},
|
||
{
|
||
module: 'SCAT 조사',
|
||
name: 'scat',
|
||
feature: 'Pre-SCAT 해안 조사 기록',
|
||
integration: '현장 조사 데이터',
|
||
},
|
||
{
|
||
module: '관리자',
|
||
name: 'admin',
|
||
feature: '사용자/권한/메뉴/설정/연계 관리',
|
||
integration: '전체 시스템',
|
||
},
|
||
];
|
||
|
||
// ─── 연계 인터페이스 데이터 ───────────────────────────────────────────────────────
|
||
|
||
interface InterfaceRow {
|
||
system: string;
|
||
method: string;
|
||
data: string;
|
||
cycle: string;
|
||
protocol: string;
|
||
}
|
||
|
||
const INTERFACES: InterfaceRow[] = [
|
||
{
|
||
system: 'KHOA (해양조사원)',
|
||
method: 'REST API',
|
||
data: '조위, 해류, 수온',
|
||
cycle: '실시간/1시간',
|
||
protocol: 'HTTPS',
|
||
},
|
||
{
|
||
system: '기상청',
|
||
method: 'REST API',
|
||
data: '풍향/풍속, 기압, 기온, 강수',
|
||
cycle: '3시간',
|
||
protocol: 'HTTPS',
|
||
},
|
||
{
|
||
system: 'HYCOM',
|
||
method: '파일 수신',
|
||
data: 'SST, 해류(U/V), SSH',
|
||
cycle: '6시간',
|
||
protocol: 'HTTPS/FTP',
|
||
},
|
||
{
|
||
system: '해경 KBP (인사)',
|
||
method: '배치 수집',
|
||
data: '사용자, 부서, 직위, 조직',
|
||
cycle: '1일 1회',
|
||
protocol: '내부망 API',
|
||
},
|
||
{
|
||
system: 'AIS 선박위치',
|
||
method: '실시간 수집',
|
||
data: '선박 위치, 속도, 방향',
|
||
cycle: '실시간',
|
||
protocol: 'Socket/API',
|
||
},
|
||
{
|
||
system: '포세이돈 R&D',
|
||
method: 'API 연계',
|
||
data: '유출유 확산 예측 결과',
|
||
cycle: '요청 시',
|
||
protocol: 'HTTPS',
|
||
},
|
||
{
|
||
system: 'KOSPS (광주)',
|
||
method: 'DLL 호출',
|
||
data: '유출유 확산 예측 결과',
|
||
cycle: '요청 시',
|
||
protocol: 'HTTPS (Fortran DLL)',
|
||
},
|
||
{
|
||
system: '충북대 HNS',
|
||
method: 'API 호출',
|
||
data: 'HNS 대기확산 결과',
|
||
cycle: '요청 시',
|
||
protocol: 'HTTPS',
|
||
},
|
||
{
|
||
system: '긴급구난 R&D',
|
||
method: '내부 연계',
|
||
data: '구난 분석 결과',
|
||
cycle: '요청 시',
|
||
protocol: '내부망 API',
|
||
},
|
||
];
|
||
|
||
// ─── 탭 1: 표준 프레임워크 ────────────────────────────────────────────────────────
|
||
|
||
function FrameworkTab() {
|
||
return (
|
||
<div className="flex flex-col gap-6 p-5">
|
||
{/* 1. 개발 프레임워크 구성 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 개발 프레임워크 구성</h3>
|
||
<div className="border border-stroke-1 rounded overflow-hidden">
|
||
{/* 프레젠테이션 계층 */}
|
||
<div className="border-b border-stroke-1 p-4 bg-bg-card">
|
||
<p className="text-xs font-semibold text-t2 mb-1">프레젠테이션 계층</p>
|
||
<p className="text-xs text-t3 mb-3">React 19 + TypeScript 5.9 + Tailwind CSS 3</p>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
{[
|
||
{ name: 'MapLibre', sub: 'GL JS 5' },
|
||
{ name: 'deck.gl', sub: '9.x' },
|
||
{ name: 'Zustand', sub: '상태관리' },
|
||
{ name: 'TanStack', sub: 'Query' },
|
||
].map((item) => (
|
||
<div
|
||
key={item.name}
|
||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center"
|
||
>
|
||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||
<p className="text-xs text-t3 mt-0.5">{item.sub}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{/* 비즈니스 로직 계층 */}
|
||
<div className="border-b border-stroke-1 p-4 bg-bg-surface">
|
||
<p className="text-xs font-semibold text-t2 mb-1">비즈니스 로직 계층</p>
|
||
<p className="text-xs text-t3 mb-3">Express 4 + TypeScript</p>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
{[
|
||
{ name: 'JWT 인증', sub: 'OAuth2.0' },
|
||
{ name: 'RBAC', sub: '권한엔진' },
|
||
{ name: 'Socket.IO', sub: '실시간' },
|
||
{ name: 'Helmet', sub: '보안' },
|
||
].map((item) => (
|
||
<div
|
||
key={item.name}
|
||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center"
|
||
>
|
||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||
<p className="text-xs text-t3 mt-0.5">{item.sub}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{/* 데이터 접근 계층 */}
|
||
<div className="p-4 bg-bg-card">
|
||
<p className="text-xs font-semibold text-t2 mb-1">데이터 접근 계층</p>
|
||
<p className="text-xs text-t3 mb-3">PostgreSQL 16 + PostGIS</p>
|
||
<div className="grid grid-cols-3 gap-2 max-w-xs">
|
||
{[
|
||
{ name: 'wing DB', sub: '운영 DB' },
|
||
{ name: 'wing_auth', sub: '인증 DB' },
|
||
{ name: 'PostGIS', sub: '공간정보' },
|
||
].map((item) => (
|
||
<div
|
||
key={item.name}
|
||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center"
|
||
>
|
||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||
<p className="text-xs text-t3 mt-0.5">{item.sub}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 2. 기술 스택 상세 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 기술 스택 상세</h3>
|
||
<div className="overflow-auto">
|
||
<table className="w-full text-xs border-collapse">
|
||
<thead>
|
||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||
{['구분', '기술', '버전', '설명'].map((h) => (
|
||
<th
|
||
key={h}
|
||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||
>
|
||
{h}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{TECH_STACK.map((row, idx) => (
|
||
<tr key={idx} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">
|
||
{row.category}
|
||
</td>
|
||
<td className="px-3 py-2 text-t1 whitespace-nowrap">{row.tech}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">{row.version}</td>
|
||
<td className="px-3 py-2 text-t2">{row.description}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 3. 개발 표준 및 규칙 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 개발 표준 및 규칙</h3>
|
||
<div className="grid grid-cols-1 gap-3">
|
||
{[
|
||
{
|
||
title: 'HTTP 정책',
|
||
content:
|
||
'GET/POST만 사용 (PUT/DELETE/PATCH 금지) — 한국 보안취약점 점검 가이드 준수',
|
||
},
|
||
{
|
||
title: '코드 표준',
|
||
content: 'ESLint + Prettier 적용, TypeScript strict 모드 필수',
|
||
},
|
||
{
|
||
title: '모듈 구조',
|
||
content: '@common/ (공통 모듈) + @tabs/ (업무별 탭) Path Alias 기반 분리',
|
||
},
|
||
{
|
||
title: '보안',
|
||
content: '입력 살균(sanitize), XSS/SQL Injection 방지, CORS 정책, Rate Limiting',
|
||
},
|
||
].map((item) => (
|
||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||
<p className="text-xs font-semibold text-t2 mb-1">{item.title}</p>
|
||
<p className="text-xs text-t2 leading-relaxed">{item.content}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── 탭 2: 목표시스템 아키텍쳐 ───────────────────────────────────────────────────
|
||
|
||
function TargetArchTab() {
|
||
return (
|
||
<div className="flex flex-col gap-6 p-5">
|
||
{/* 1. 시스템 전체 구성도 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 시스템 전체 구성도</h3>
|
||
<div className="flex flex-col items-stretch gap-0 border border-stroke-1 rounded overflow-hidden">
|
||
{/* 사용자 접근 계층 */}
|
||
<div className="p-4 bg-bg-card">
|
||
<p className="text-xs font-semibold text-t2 mb-1">사용자 접근 계층</p>
|
||
<p className="text-xs text-t1 font-medium mb-1">웹 브라우저 (React SPA)</p>
|
||
<p className="text-xs text-t3 leading-relaxed">
|
||
확산예측 | HNS분석 | 구조시나리오 | 항공방제 | 기상정보 | 사고관리 | SCAT조사 |
|
||
자산관리 | 관리자
|
||
</p>
|
||
</div>
|
||
{/* 화살표 + 프로토콜 */}
|
||
<div className="flex flex-col items-center py-2 bg-bg-base border-y border-stroke-1">
|
||
<span className="text-t3 text-lg">↕</span>
|
||
<span className="text-xs text-t3">HTTPS (TLS 1.2+)</span>
|
||
</div>
|
||
{/* API 서버 계층 */}
|
||
<div className="p-4 bg-bg-surface border-b border-stroke-1">
|
||
<p className="text-xs font-semibold text-t2 mb-1">API 서버 계층</p>
|
||
<p className="text-xs text-t1 font-medium mb-2">Express 4 REST API (Port 3001)</p>
|
||
<div className="grid grid-cols-2 gap-1.5 sm:grid-cols-4">
|
||
{[
|
||
'JWT 인증 미들웨어',
|
||
'RBAC 권한 엔진 (permResolver)',
|
||
'감사로그 자동 기록',
|
||
'입력 살균 / Rate Limiting / Helmet',
|
||
].map((item) => (
|
||
<div
|
||
key={item}
|
||
className="bg-bg-elevated border border-stroke-1 rounded px-2 py-1.5 text-center"
|
||
>
|
||
<p className="text-xs text-t2 leading-snug">{item}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{/* 화살표 + 프로토콜 */}
|
||
<div className="flex flex-col items-center py-2 bg-bg-base border-b border-stroke-1">
|
||
<span className="text-t3 text-lg">↕</span>
|
||
<span className="text-xs text-t3">pg connection pool</span>
|
||
</div>
|
||
{/* 데이터 계층 */}
|
||
<div className="p-4 bg-bg-card">
|
||
<p className="text-xs font-semibold text-t2 mb-1">데이터 계층</p>
|
||
<p className="text-xs text-t1 font-medium mb-2">PostgreSQL 16 + PostGIS</p>
|
||
<div className="flex gap-2">
|
||
{[
|
||
{ name: 'wing DB', sub: '운영' },
|
||
{ name: 'wing_auth', sub: '인증' },
|
||
].map((item) => (
|
||
<div
|
||
key={item.name}
|
||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center min-w-24"
|
||
>
|
||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||
<p className="text-xs text-t3 mt-0.5">({item.sub})</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 2. 탭 기반 업무 모듈 구조 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 탭 기반 업무 모듈 구조</h3>
|
||
<div className="overflow-auto">
|
||
<table className="w-full text-xs border-collapse">
|
||
<thead>
|
||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||
{['모듈', '패키지명', '기능', '주요 연계'].map((h) => (
|
||
<th
|
||
key={h}
|
||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||
>
|
||
{h}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{TAB_MODULES.map((row) => (
|
||
<tr key={row.name} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.module}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">{row.name}</td>
|
||
<td className="px-3 py-2 text-t2">{row.feature}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.integration}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 3. RBAC 권한 체계 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">3. RBAC 권한 체계</h3>
|
||
<div className="flex flex-col gap-3">
|
||
{[
|
||
{
|
||
title: '2차원 권한 엔진',
|
||
content:
|
||
'AUTH_PERM OPER_CD 기반: R(조회), C(생성), U(수정), D(삭제) — 역할별 메뉴·기능 접근 제어',
|
||
},
|
||
{
|
||
title: 'permResolver',
|
||
content:
|
||
'역할(Role)과 권한(Permission)의 2차원 매핑으로 메뉴 표시 여부 및 기능 사용 가능 여부를 동적으로 판단',
|
||
},
|
||
{
|
||
title: '감사로그 자동 기록',
|
||
content:
|
||
'누가(사용자) / 언제(타임스탬프) / 무엇을(기능) / 어디서(IP, 메뉴) — 모든 주요 작업 자동 기록',
|
||
},
|
||
].map((item) => (
|
||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||
<p className="text-xs font-semibold text-t2 mb-1">{item.title}</p>
|
||
<p className="text-xs text-t2 leading-relaxed">{item.content}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── 탭 3: 시스템 인터페이스 연계 ────────────────────────────────────────────────
|
||
|
||
function InterfaceTab() {
|
||
const dataFlowSteps = [
|
||
'수집',
|
||
'전처리',
|
||
'저장',
|
||
'분석/예측',
|
||
'시각화',
|
||
'의사결정지원',
|
||
];
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6 p-5">
|
||
{/* 1. 외부 시스템 연계 구성도 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 외부 시스템 연계 구성도</h3>
|
||
<div className="flex items-stretch gap-2">
|
||
{/* 외부 시스템 */}
|
||
<div className="flex-1 bg-bg-card border border-stroke-1 rounded p-3 flex flex-col gap-1.5">
|
||
<p className="text-xs font-semibold text-t2 mb-1 text-center">외부 시스템</p>
|
||
{['KHOA API', '기상청 API', '해경 KBP', 'AIS 선박'].map((item) => (
|
||
<div
|
||
key={item}
|
||
className="bg-bg-elevated border border-stroke-1 rounded px-2 py-1 text-center"
|
||
>
|
||
<p className="text-xs text-t2">{item}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* 화살표 */}
|
||
<div className="flex flex-col items-center justify-center gap-1 shrink-0">
|
||
<span className="text-t3 text-lg">→</span>
|
||
<span className="text-t3 text-lg">←</span>
|
||
</div>
|
||
{/* 통합지원시스템 */}
|
||
<div className="flex-[2] bg-bg-surface border-2 border-cyan-600/40 rounded p-3 flex flex-col gap-2">
|
||
<p className="text-xs font-semibold text-t1 text-center">
|
||
해양환경 위기대응
|
||
<br />
|
||
통합지원시스템
|
||
</p>
|
||
<div className="border border-stroke-1 rounded p-2 bg-bg-elevated">
|
||
<p className="text-xs font-medium text-t2 mb-1 text-center">연계관리 모듈</p>
|
||
<div className="flex flex-col gap-1">
|
||
{['수집자료 관리', '연계 모니터링', '비식별화 조치'].map((item) => (
|
||
<p key={item} className="text-xs text-t3 text-center">
|
||
- {item}
|
||
</p>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* 화살표 */}
|
||
<div className="flex flex-col items-center justify-center gap-1 shrink-0">
|
||
<span className="text-t3 text-lg">←</span>
|
||
<span className="text-t3 text-lg">→</span>
|
||
</div>
|
||
{/* R&D 시스템 */}
|
||
<div className="flex-1 bg-bg-card border border-stroke-1 rounded p-3 flex flex-col gap-1.5">
|
||
<p className="text-xs font-semibold text-t2 mb-1 text-center">R&D 시스템</p>
|
||
{['포세이돈', 'KOSPS', '충북대 HNS', '긴급구난'].map((item) => (
|
||
<div
|
||
key={item}
|
||
className="bg-bg-elevated border border-stroke-1 rounded px-2 py-1 text-center"
|
||
>
|
||
<p className="text-xs text-t2">{item}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 2. 연계 인터페이스 목록 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 연계 인터페이스 목록</h3>
|
||
<div className="overflow-auto">
|
||
<table className="w-full text-xs border-collapse">
|
||
<thead>
|
||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||
{['연계 시스템', '연계 방식', '데이터', '주기', '프로토콜'].map((h) => (
|
||
<th
|
||
key={h}
|
||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||
>
|
||
{h}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{INTERFACES.map((row) => (
|
||
<tr key={row.system} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.system}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.method}</td>
|
||
<td className="px-3 py-2 text-t2">{row.data}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.cycle}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">{row.protocol}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 3. 데이터 흐름도 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 데이터 흐름도</h3>
|
||
<div className="flex items-center gap-1 flex-wrap">
|
||
{dataFlowSteps.map((step, idx) => (
|
||
<div key={step} className="flex items-center gap-1">
|
||
<div className="bg-bg-elevated border border-stroke-1 rounded px-3 py-2 text-center min-w-16">
|
||
<p className="text-xs font-medium text-t1">{step}</p>
|
||
</div>
|
||
{idx < dataFlowSteps.length - 1 && (
|
||
<span className="text-t3 text-lg shrink-0">→</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||
{[
|
||
{ step: '수집', desc: 'KHOA, 기상청, HYCOM, AIS 등 외부 원천 데이터 수신' },
|
||
{ step: '전처리', desc: '포맷 변환, 좌표계 통일, 비식별화, 품질 검사' },
|
||
{ step: '저장', desc: 'PostgreSQL 16 + PostGIS 공간정보 DB 적재' },
|
||
{ step: '분석/예측', desc: 'R&D 모델 연계 (포세이돈, KOSPS, 충북대, 긴급구난)' },
|
||
{ step: '시각화', desc: 'MapLibre GL + deck.gl 기반 지도 레이어 렌더링' },
|
||
{ step: '의사결정지원', desc: '방제작전 시나리오, 구조분석, 경보 발령 지원' },
|
||
].map((item) => (
|
||
<div key={item.step} className="bg-bg-card border border-stroke-1 rounded p-2.5">
|
||
<p className="text-xs font-semibold text-t2 mb-1">{item.step}</p>
|
||
<p className="text-xs text-t2 leading-relaxed">{item.desc}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 4. 연계 장애 대응 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">4. 연계 장애 대응</h3>
|
||
<div className="flex flex-col gap-2">
|
||
{[
|
||
{
|
||
title: '연계 모니터링',
|
||
content: '관리자 > 연계관리 > 연계모니터링에서 실시간 연계 상태 확인',
|
||
},
|
||
{
|
||
title: 'R&D 파이프라인 모니터링',
|
||
content: '관리자 > 연계관리 > R&D과제에서 과제별 데이터 수신 이력 및 처리 현황 확인',
|
||
},
|
||
{
|
||
title: '장애 알림',
|
||
content: '데이터 수신 지연/실패 발생 시 알림 발생 — 운영자 즉시 인지 가능',
|
||
},
|
||
{
|
||
title: '비식별화 조치',
|
||
content: '개인정보 포함 데이터(해경 KBP 인사 등) 수집 시 자동 비식별화 처리 적용',
|
||
},
|
||
].map((item) => (
|
||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||
<p className="text-xs font-semibold text-t2 mb-1">{item.title}</p>
|
||
<p className="text-xs text-t2 leading-relaxed">{item.content}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
// ─── 이기종시스템 연계 데이터 ─────────────────────────────────────────────────────
|
||
|
||
interface HeterogeneousSystemRow {
|
||
system: string;
|
||
lang: string;
|
||
os: string;
|
||
location: string;
|
||
protocol: string;
|
||
description: string;
|
||
}
|
||
|
||
const HETEROGENEOUS_SYSTEMS: HeterogeneousSystemRow[] = [
|
||
{
|
||
system: 'KOSPS',
|
||
lang: 'Fortran',
|
||
os: 'Linux',
|
||
location: '광주',
|
||
protocol: 'HTTPS (REST 래퍼)',
|
||
description: '유출유 확산 예측 — Fortran DLL을 REST API로 래핑하여 연계',
|
||
},
|
||
{
|
||
system: '충북대 HNS',
|
||
lang: 'Python / C++',
|
||
os: 'Linux',
|
||
location: '충북대',
|
||
protocol: 'HTTPS',
|
||
description: 'HNS 대기확산 예측 — Python/C++ 모델을 REST API로 호출',
|
||
},
|
||
{
|
||
system: '긴급구난',
|
||
lang: 'Python',
|
||
os: 'Linux',
|
||
location: '해경 내부',
|
||
protocol: '내부망 API',
|
||
description: '구난 표류 분석 — Python 모델을 내부망 REST API로 연계',
|
||
},
|
||
{
|
||
system: 'HYCOM',
|
||
lang: 'Fortran / NetCDF',
|
||
os: 'Linux HPC',
|
||
location: '미 해군 공개',
|
||
protocol: 'HTTPS / FTP',
|
||
description: '전지구 해류·수온 예측 — NetCDF 파일 수신 후 ETL 전처리',
|
||
},
|
||
{
|
||
system: '기상청',
|
||
lang: '-',
|
||
os: '-',
|
||
location: '기상청 API Hub',
|
||
protocol: 'HTTPS',
|
||
description: '풍향·풍속·기온·강수 등 기상 데이터 REST API 수집',
|
||
},
|
||
{
|
||
system: 'KHOA',
|
||
lang: '-',
|
||
os: '-',
|
||
location: '해양조사원',
|
||
protocol: 'HTTPS',
|
||
description: '조위·해류·수온 등 해양관측 데이터 REST API 수집',
|
||
},
|
||
{
|
||
system: '해경 KBP',
|
||
lang: 'Java 전자정부',
|
||
os: 'Linux',
|
||
location: '해경 내부망',
|
||
protocol: '내부망 API',
|
||
description: '사용자·조직·직위 인사 데이터 배치 수집 (비식별화 적용)',
|
||
},
|
||
{
|
||
system: 'AIS',
|
||
lang: '-',
|
||
os: '-',
|
||
location: '해경 AIS 서버',
|
||
protocol: 'Socket / API',
|
||
description: '선박 위치·속도·방향 실시간 수신',
|
||
},
|
||
];
|
||
|
||
interface HeterogeneousStrategyCard {
|
||
challenge: string;
|
||
solution: string;
|
||
description: string;
|
||
}
|
||
|
||
interface IntegrationPlanItem {
|
||
title: string;
|
||
description: string;
|
||
details?: string[];
|
||
}
|
||
|
||
const INTEGRATION_PLANS: IntegrationPlanItem[] = [
|
||
{
|
||
title: '사용자 정보 연계',
|
||
description:
|
||
'해양경찰청의 인사관리플랫폼과 연계 또는 사용자 정보를 제공받아 구성할 수 있어야 함',
|
||
},
|
||
{
|
||
title: '해양공간 데이터 연계',
|
||
description:
|
||
'해경 해양공간 데이터 구축사업(해양공간정보 활용체계 구축 및 빅데이터 관련 사업)의 \'데이터통합저장소\' 시스템과 연계하여 현장탐색 전자지도 자동표출 기술 등에 영상 및 사진자료를 연계하여 구축',
|
||
},
|
||
{
|
||
title: 'DB 통합설계 기반 맞춤형 인터페이스',
|
||
description:
|
||
'플랫폼 변경 및 신규 통합설계 되는 데이터베이스(DB) 구조 설계를 기반으로 사용자 맞춤형 화면 인터페이스를 구현해야 함',
|
||
details: [
|
||
'DBMS는 분리되어 있는 시스템들을 통합설계를 통하여 공통, 분야별 등으로 설계하여야 함',
|
||
],
|
||
},
|
||
{
|
||
title: '유출유 확산예측 정확성 향상 (KOSPS 연계)',
|
||
description:
|
||
'유출유 확산예측 정확성 향상을 위해, 해양오염방제지원시스템(KOSPS)를 연계·탑재하여야 함',
|
||
details: [
|
||
'다양한 유출유 확산 예측 결과를 사용자가 한눈에 확인 가능하여야 함',
|
||
'확산예측 기반으로 역추적, 최초 유출유 발생지점을 예측할 수 있어야 함',
|
||
'그 밖에 유출유 확산예측 정확성 향상을 위한 대책을 마련하여야 함',
|
||
],
|
||
},
|
||
{
|
||
title: '기타 시스템 연계',
|
||
description:
|
||
'그 밖에 시스템 구축 중 효율적인 사고대응을 위한 타 시스템 연계할 수 있음',
|
||
},
|
||
];
|
||
|
||
const HETEROGENEOUS_STRATEGIES: HeterogeneousStrategyCard[] = [
|
||
{
|
||
challenge: '언어 이질성',
|
||
solution: 'REST API 래퍼 계층',
|
||
description:
|
||
'Fortran, Python, C++, Java 등 각 언어로 작성된 모델을 REST API 래퍼로 감싸 언어·플랫폼 독립적인 표준 인터페이스 제공',
|
||
},
|
||
{
|
||
challenge: '데이터 형식 차이',
|
||
solution: 'ETL 전처리 파이프라인',
|
||
description:
|
||
'NetCDF, CSV, Binary, JSON 등 이기종 포맷을 ETL 파이프라인으로 표준 JSON/GeoJSON 형식으로 변환 후 DB 적재',
|
||
},
|
||
{
|
||
challenge: '네트워크 분리',
|
||
solution: '이중 네트워크 연계',
|
||
description:
|
||
'외부망(인터넷) 연계와 내부망(해경 내부) 연계를 분리 운영하여 보안 정책 준수 및 데이터 안전성 확보',
|
||
},
|
||
{
|
||
challenge: '가용성·장애 대응',
|
||
solution: '연계 모니터링 + 알림',
|
||
description:
|
||
'연계 상태를 실시간 모니터링하고 수신 지연·실패 발생 시 운영자에게 즉시 알림 발송하여 신속 대응',
|
||
},
|
||
{
|
||
challenge: '인증·보안 차이',
|
||
solution: 'API Gateway 패턴',
|
||
description:
|
||
'시스템별 상이한 인증 방식(API Key, JWT, IP 제한 등)을 API Gateway 계층에서 통합 관리하여 단일 보안 정책 적용',
|
||
},
|
||
{
|
||
challenge: '프로토콜 차이',
|
||
solution: '어댑터 패턴 적용',
|
||
description:
|
||
'HTTP REST, FTP, Socket, 배치 파일 등 다양한 프로토콜을 어댑터 패턴으로 추상화하여 표준 인터페이스로 통일',
|
||
},
|
||
];
|
||
|
||
const HETEROGENEOUS_FLOW_STEPS = [
|
||
'원본 데이터',
|
||
'수집 어댑터',
|
||
'ETL 전처리',
|
||
'표준 변환',
|
||
'DB 적재',
|
||
'API 제공',
|
||
];
|
||
|
||
interface SecurityPolicyCard {
|
||
title: string;
|
||
items: string[];
|
||
}
|
||
|
||
const HETEROGENEOUS_SECURITY: SecurityPolicyCard[] = [
|
||
{
|
||
title: '외부망 연계',
|
||
items: [
|
||
'TLS 1.2+ 암호화 통신',
|
||
'API Key / OAuth 인증',
|
||
'IP 화이트리스트 제한',
|
||
'Rate Limiting 적용',
|
||
],
|
||
},
|
||
{
|
||
title: '내부망 연계',
|
||
items: [
|
||
'전용 내부망 구간 분리',
|
||
'상호 인증서 검증',
|
||
'비식별화 자동 처리',
|
||
'접근 이력 감사로그',
|
||
],
|
||
},
|
||
{
|
||
title: '데이터 보호',
|
||
items: [
|
||
'개인정보 수집 최소화',
|
||
'ETL 단계 비식별화',
|
||
'전송 구간 암호화',
|
||
'저장 데이터 접근 제어',
|
||
],
|
||
},
|
||
];
|
||
|
||
// ─── 탭 4: 이기종시스템연계 ───────────────────────────────────────────────────────
|
||
|
||
function HeterogeneousTab() {
|
||
return (
|
||
<div className="p-5 space-y-6">
|
||
{/* 1. 이기종시스템 연계 개요 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 이기종시스템 연계 개요</h3>
|
||
<p className="text-xs text-t2 leading-relaxed mb-4">
|
||
통합지원시스템은 Fortran, Python, C++, Java 등 다양한 언어와 플랫폼으로 구현된 이기종
|
||
시스템과 연계한다. REST API 표준화, ETL 전처리, 어댑터 패턴을 통해 언어·플랫폼 독립적인
|
||
연계 구조를 구현하며, 외부망·내부망 이중 네트워크 정책을 준수한다.
|
||
</p>
|
||
<div className="flex items-stretch gap-2">
|
||
<div className="flex-1 bg-bg-elevated border border-stroke-1 rounded p-3 text-center">
|
||
<p className="text-xs font-semibold text-t2 mb-2">이기종 시스템</p>
|
||
{['Fortran KOSPS', 'Python/C++ 충북대', 'Java 해경KBP', 'NetCDF HYCOM'].map((item) => (
|
||
<p key={item} className="text-xs text-t3 leading-relaxed">
|
||
{item}
|
||
</p>
|
||
))}
|
||
</div>
|
||
<div className="flex flex-col items-center justify-center shrink-0 gap-0.5">
|
||
<span className="text-t3 text-lg">→</span>
|
||
<span className="text-t3 text-lg">←</span>
|
||
</div>
|
||
<div className="flex-1 bg-cyan-600/10 border border-cyan-500/30 rounded p-3 text-center">
|
||
<p className="text-xs font-semibold text-t2 mb-2">연계 어댑터 계층</p>
|
||
{['REST API 래퍼', 'ETL 전처리', '프로토콜 변환', '인증 통합'].map((item) => (
|
||
<p key={item} className="text-xs text-t3 leading-relaxed">
|
||
{item}
|
||
</p>
|
||
))}
|
||
</div>
|
||
<div className="flex flex-col items-center justify-center shrink-0 gap-0.5">
|
||
<span className="text-t3 text-lg">→</span>
|
||
<span className="text-t3 text-lg">←</span>
|
||
</div>
|
||
<div className="flex-1 bg-bg-elevated border border-stroke-1 rounded p-3 text-center">
|
||
<p className="text-xs font-semibold text-t2 mb-2">통합지원시스템</p>
|
||
{['Express REST API', 'PostgreSQL+PostGIS', 'React SPA', '표준 JSON'].map((item) => (
|
||
<p key={item} className="text-xs text-t3 leading-relaxed">
|
||
{item}
|
||
</p>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 2. 이기종 시스템 간의 연계 방안 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 이기종 시스템 간의 연계 방안</h3>
|
||
<div className="flex flex-col gap-2">
|
||
{INTEGRATION_PLANS.map((item, idx) => (
|
||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||
<p className="text-xs font-semibold text-t2 mb-1">
|
||
{idx + 1}. {item.title}
|
||
</p>
|
||
<p className="text-xs text-t2 leading-relaxed">{item.description}</p>
|
||
{item.details && (
|
||
<ul className="mt-1.5 flex flex-col gap-1 pl-3">
|
||
{item.details.map((detail) => (
|
||
<li key={detail} className="text-xs text-t3 leading-relaxed list-disc">
|
||
{detail}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 3. 연계 대상 이기종 시스템 목록 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 연계 대상 이기종 시스템 목록</h3>
|
||
<div className="overflow-auto">
|
||
<table className="w-full text-xs border-collapse">
|
||
<thead>
|
||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||
{['시스템', '구현 언어', 'OS', '위치', '연계 프로토콜', '연계 설명'].map((h) => (
|
||
<th
|
||
key={h}
|
||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||
>
|
||
{h}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{HETEROGENEOUS_SYSTEMS.map((row) => (
|
||
<tr key={row.system} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.system}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">{row.lang}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.os}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.location}</td>
|
||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">{row.protocol}</td>
|
||
<td className="px-3 py-2 text-t2">{row.description}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 4. 이기종 연계 전략 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">4. 이기종 연계 전략</h3>
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{HETEROGENEOUS_STRATEGIES.map((card) => (
|
||
<div key={card.challenge} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||
<div className="flex items-center gap-1.5 mb-1.5">
|
||
<span className="text-xs font-semibold text-red-400">{card.challenge}</span>
|
||
<span className="text-t3 text-xs">→</span>
|
||
<span className="text-xs font-semibold text-cyan-400">{card.solution}</span>
|
||
</div>
|
||
<p className="text-xs text-t2 leading-relaxed">{card.description}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 5. 이기종 데이터 변환 흐름 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">5. 이기종 데이터 변환 흐름</h3>
|
||
<div className="flex items-center gap-1 flex-wrap">
|
||
{HETEROGENEOUS_FLOW_STEPS.map((step, idx) => (
|
||
<div key={step} className="flex items-center gap-1">
|
||
<div className="bg-bg-elevated border border-stroke-1 rounded px-3 py-2 text-center min-w-16">
|
||
<p className="text-xs font-medium text-t1">{step}</p>
|
||
</div>
|
||
{idx < HETEROGENEOUS_FLOW_STEPS.length - 1 && (
|
||
<span className="text-t3 text-lg shrink-0">→</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 6. 이기종 연계 보안 정책 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">6. 이기종 연계 보안 정책</h3>
|
||
<div className="grid grid-cols-3 gap-3">
|
||
{HETEROGENEOUS_SECURITY.map((card) => (
|
||
<div key={card.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||
<p className="text-xs font-semibold text-t2 mb-2">{card.title}</p>
|
||
<ul className="flex flex-col gap-1">
|
||
{card.items.map((item) => (
|
||
<li key={item} className="text-xs text-t2 leading-relaxed">
|
||
· {item}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── 공통기능 탭 데이터 ──────────────────────────────────────────────────────────
|
||
|
||
interface CommonFeatureItem {
|
||
title: string;
|
||
description: string;
|
||
details: string[];
|
||
}
|
||
|
||
const COMMON_FEATURES: CommonFeatureItem[] = [
|
||
{
|
||
title: '인증 시스템',
|
||
description: 'JWT 기반 세션 인증 + Google OAuth 소셜 로그인',
|
||
details: [
|
||
'HttpOnly 쿠키(WING_SESSION) 기반 토큰 관리 — XSS 방어',
|
||
'Access Token(15분) + Refresh Token(7일) 이중 토큰 구조',
|
||
'Google OAuth 2.0 소셜 로그인 지원',
|
||
'Zustand authStore 기반 프론트엔드 인증 상태 통합 관리',
|
||
],
|
||
},
|
||
{
|
||
title: 'RBAC 2차원 권한',
|
||
description: 'AUTH_PERM 기반 기능별·역할별 2차원 권한 엔진',
|
||
details: [
|
||
'OPER_CD (R: 조회, C: 생성, U: 수정, D: 삭제) 4단계 조작 권한',
|
||
'역할(Role) × 기능(Feature) 매트릭스 기반 권한 매핑',
|
||
'permResolver 엔진으로 백엔드·프론트엔드 동시 권한 검증',
|
||
'메뉴 접근, 버튼 노출, API 호출 3중 권한 통제',
|
||
],
|
||
},
|
||
{
|
||
title: 'API 통신 패턴',
|
||
description: 'Axios 기반 공통 API 클라이언트 + 자동 인증·에러 처리',
|
||
details: [
|
||
'GET/POST만 사용 (PUT/DELETE/PATCH 금지 — 보안취약점 점검 가이드 준수)',
|
||
'요청 인터셉터: 쿠키 자동 첨부 (withCredentials)',
|
||
'응답 인터셉터: 401 시 자동 토큰 갱신, 실패 시 로그아웃',
|
||
'TanStack Query 기반 서버 상태 캐싱 및 자동 재검증',
|
||
],
|
||
},
|
||
{
|
||
title: '상태 관리',
|
||
description: 'Zustand(클라이언트) + TanStack Query(서버) 이중 상태 관리',
|
||
details: [
|
||
'Zustand: authStore(인증), menuStore(메뉴) 등 클라이언트 전역 상태',
|
||
'TanStack Query: API 응답 캐싱, 자동 재요청, 낙관적 업데이트',
|
||
'컴포넌트 로컬 상태: useState 활용',
|
||
],
|
||
},
|
||
{
|
||
title: '메뉴 시스템',
|
||
description: 'DB 기반 동적 메뉴 + 권한 연동 자동 필터링',
|
||
details: [
|
||
'DB에서 메뉴 트리 구조를 동적으로 로드',
|
||
'사용자 권한에 따라 메뉴 항목 자동 필터링 (접근 불가 메뉴 미노출)',
|
||
'관리자 화면에서 메뉴 순서·표시 여부·아이콘 실시간 편집',
|
||
'menuStore(Zustand)로 현재 활성 메뉴 상태 전역 관리',
|
||
],
|
||
},
|
||
{
|
||
title: '지도 엔진',
|
||
description: 'MapLibre GL JS 5.x + deck.gl 9.x 기반 GIS 시각화',
|
||
details: [
|
||
'MapLibre GL JS: 오픈소스 벡터 타일 기반 지도 렌더링',
|
||
'deck.gl: 대규모 공간 데이터(파티클, 히트맵, 궤적) 고성능 시각화',
|
||
'PostGIS 공간 쿼리 → GeoJSON → deck.gl 레이어 파이프라인',
|
||
'레이어 트리 UI로 사용자별 레이어 표시·숨김 제어',
|
||
],
|
||
},
|
||
{
|
||
title: '스타일링',
|
||
description: 'Tailwind CSS @layer 아키텍처 + CSS 변수 디자인 시스템',
|
||
details: [
|
||
'@layer base → components → wing 3단계 CSS 계층 구조',
|
||
'CSS 변수 기반 시맨틱 컬러 (bg-bg-base, text-t1, border-stroke-1 등)',
|
||
'다크 모드 기본 적용 — CSS 변수 전환으로 테마 일괄 변경',
|
||
'인라인 스타일 지양, Tailwind 유틸리티 클래스 우선',
|
||
],
|
||
},
|
||
{
|
||
title: '감사 로그',
|
||
description: '사용자 행위 자동 기록 — 접속·조회·변경 이력 추적',
|
||
details: [
|
||
'로그인/로그아웃, 메뉴 접근, 데이터 변경 자동 기록',
|
||
'App.tsx에서 탭 전환 시 감사 로그 자동 전송',
|
||
'관리자 화면에서 사용자별·기간별 감사 로그 조회 가능',
|
||
'IP 주소, User-Agent, 요청 경로 등 부가 정보 기록',
|
||
],
|
||
},
|
||
{
|
||
title: '보안',
|
||
description: '입력 살균·CORS·CSP·Rate Limiting 다층 보안 정책',
|
||
details: [
|
||
'입력 살균(sanitize): XSS·SQL Injection 방어 미들웨어 적용',
|
||
'Helmet: CSP, X-Frame-Options, HSTS 등 보안 헤더 자동 설정',
|
||
'CORS: 허용 오리진 화이트리스트 제한',
|
||
'Rate Limiting: API 요청 빈도 제한으로 DoS 방어',
|
||
],
|
||
},
|
||
];
|
||
|
||
// ─── 방제대응 프로세스 데이터 ─────────────────────────────────────────────────────
|
||
|
||
interface ProcessStep {
|
||
phase: string;
|
||
description: string;
|
||
modules: string[];
|
||
}
|
||
|
||
const RESPONSE_PROCESS: ProcessStep[] = [
|
||
{
|
||
phase: '사고 접수',
|
||
description: '해양오염 사고 신고 접수 및 초동 상황 등록',
|
||
modules: ['사건/사고'],
|
||
},
|
||
{
|
||
phase: '상황 파악',
|
||
description: '사고 현장 기상·해상 조건 확인, 유출원·유출량 파악',
|
||
modules: ['해양기상', '사건/사고'],
|
||
},
|
||
{
|
||
phase: '확산 예측',
|
||
description: '유출유/HNS 확산 시뮬레이션 및 역추적 분석 수행',
|
||
modules: ['확산예측', 'HNS분석'],
|
||
},
|
||
{
|
||
phase: '방제 계획',
|
||
description: '오일붐 배치, 유처리제 살포 구역, 방제선 투입 계획 수립',
|
||
modules: ['확산예측', '자산관리'],
|
||
},
|
||
{
|
||
phase: '구조 작전',
|
||
description: '인명 구조 시나리오 수립, 표류 예측 기반 수색 구역 결정',
|
||
modules: ['구조시나리오'],
|
||
},
|
||
{
|
||
phase: '항공 감시',
|
||
description: '위성·드론 영상으로 유막 면적 모니터링 및 방제 효과 확인',
|
||
modules: ['항공방제'],
|
||
},
|
||
{
|
||
phase: '해안 조사',
|
||
description: 'Pre-SCAT 해안 오염 조사, 피해 범위 기록',
|
||
modules: ['SCAT조사'],
|
||
},
|
||
{
|
||
phase: '상황 종료',
|
||
description: '방제 완료 보고, 감사 이력 정리, 사후 분석',
|
||
modules: ['사건/사고', '관리자'],
|
||
},
|
||
];
|
||
|
||
// ─── 시스템별 기능 유무 매트릭스 데이터 ────────────────────────────────────────────
|
||
|
||
const SYSTEM_MODULES = [
|
||
'확산예측',
|
||
'HNS분석',
|
||
'구조시나리오',
|
||
'항공방제',
|
||
'해양기상',
|
||
'사건/사고',
|
||
'자산관리',
|
||
'SCAT조사',
|
||
'게시판',
|
||
'관리자',
|
||
] as const;
|
||
|
||
interface FeatureMatrixRow {
|
||
feature: string;
|
||
category: '공통기능' | '기본정보관리' | '업무기능';
|
||
integrated: boolean;
|
||
systems: Record<string, boolean>;
|
||
}
|
||
|
||
const FEATURE_MATRIX: FeatureMatrixRow[] = [
|
||
{
|
||
feature: '사용자 인증 (JWT)',
|
||
category: '공통기능',
|
||
integrated: true,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': true, '해양기상': true, '사건/사고': true, '자산관리': true, 'SCAT조사': true, '게시판': true, '관리자': true },
|
||
},
|
||
{
|
||
feature: 'RBAC 권한 제어',
|
||
category: '공통기능',
|
||
integrated: true,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': true, '해양기상': true, '사건/사고': true, '자산관리': true, 'SCAT조사': true, '게시판': true, '관리자': true },
|
||
},
|
||
{
|
||
feature: '감사 로그',
|
||
category: '공통기능',
|
||
integrated: true,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': true, '해양기상': true, '사건/사고': true, '자산관리': true, 'SCAT조사': true, '게시판': true, '관리자': true },
|
||
},
|
||
{
|
||
feature: 'API 통신 (Axios)',
|
||
category: '공통기능',
|
||
integrated: true,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': true, '해양기상': true, '사건/사고': true, '자산관리': true, 'SCAT조사': true, '게시판': true, '관리자': true },
|
||
},
|
||
{
|
||
feature: '입력 살균/보안',
|
||
category: '공통기능',
|
||
integrated: true,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': true, '해양기상': true, '사건/사고': true, '자산관리': true, 'SCAT조사': true, '게시판': true, '관리자': true },
|
||
},
|
||
{
|
||
feature: '사용자 관리',
|
||
category: '기본정보관리',
|
||
integrated: true,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': true },
|
||
},
|
||
{
|
||
feature: '지도 엔진 (MapLibre)',
|
||
category: '기본정보관리',
|
||
integrated: true,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': true, '해양기상': true, '사건/사고': true, '자산관리': false, 'SCAT조사': true, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '레이어 관리',
|
||
category: '기본정보관리',
|
||
integrated: true,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': true, '해양기상': true, '사건/사고': true, '자산관리': false, 'SCAT조사': true, '게시판': false, '관리자': true },
|
||
},
|
||
{
|
||
feature: '메뉴 관리',
|
||
category: '기본정보관리',
|
||
integrated: true,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': true },
|
||
},
|
||
{
|
||
feature: '시스템 설정',
|
||
category: '기본정보관리',
|
||
integrated: true,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': true },
|
||
},
|
||
{
|
||
feature: '확산 시뮬레이션',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': true, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: 'HNS 대기확산',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': false, 'HNS분석': true, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '표류 예측',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': true, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '위성/드론 영상',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': true, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '기상/해상 정보',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': true, 'HNS분석': true, '구조시나리오': true, '항공방제': false, '해양기상': true, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '역추적 분석',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': true, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '사고 등록/이력',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': true, '자산관리': false, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '장비/선박 관리',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': true, 'SCAT조사': false, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '해안 조사',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': true, '게시판': false, '관리자': false },
|
||
},
|
||
{
|
||
feature: '게시판 CRUD',
|
||
category: '업무기능',
|
||
integrated: false,
|
||
systems: { '확산예측': false, 'HNS분석': false, '구조시나리오': false, '항공방제': false, '해양기상': false, '사건/사고': false, '자산관리': false, 'SCAT조사': false, '게시판': true, '관리자': false },
|
||
},
|
||
];
|
||
|
||
const CATEGORY_STYLES: Record<string, string> = {
|
||
'공통기능': 'bg-cyan-600/20 text-cyan-300',
|
||
'기본정보관리': 'bg-emerald-600/20 text-emerald-300',
|
||
'업무기능': 'bg-bg-elevated text-t3',
|
||
};
|
||
|
||
// ─── 탭 5: 공통기능 ─────────────────────────────────────────────────────────────
|
||
|
||
function CommonFeaturesTab() {
|
||
return (
|
||
<div className="flex flex-col gap-6 p-5">
|
||
{/* 1. 방제대응 프로세스 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 방제대응 프로세스</h3>
|
||
<p className="text-xs text-t2 leading-relaxed mb-4">
|
||
해양오염 사고 발생 시 사고 접수부터 상황 종료까지의 단계별 대응 프로세스이며,
|
||
각 단계에서 활용하는 시스템 모듈을 표시한다.
|
||
</p>
|
||
{/* 프로세스 흐름도 */}
|
||
<div className="flex items-start gap-1 flex-wrap mb-4">
|
||
{RESPONSE_PROCESS.map((step, idx) => (
|
||
<div key={step.phase} className="flex items-start gap-1">
|
||
<div className="bg-bg-elevated border border-stroke-1 rounded px-3 py-2 text-center min-w-20">
|
||
<p className="text-xs font-semibold text-t1 mb-1">{step.phase}</p>
|
||
<div className="flex flex-col gap-0.5">
|
||
{step.modules.map((mod) => (
|
||
<span key={mod} className="text-[10px] text-cyan-400">{mod}</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{idx < RESPONSE_PROCESS.length - 1 && (
|
||
<span className="text-t3 text-lg shrink-0 mt-2.5">→</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* 프로세스 상세 */}
|
||
<div className="flex flex-col gap-2">
|
||
{RESPONSE_PROCESS.map((step, idx) => (
|
||
<div key={step.phase} className="bg-bg-card border border-stroke-1 rounded p-3 flex items-start gap-3">
|
||
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-cyan-600 text-white text-xs font-semibold shrink-0 mt-0.5">
|
||
{idx + 1}
|
||
</span>
|
||
<div className="flex-1">
|
||
<p className="text-xs font-semibold text-t1 mb-0.5">{step.phase}</p>
|
||
<p className="text-xs text-t2 leading-relaxed">{step.description}</p>
|
||
</div>
|
||
<div className="flex gap-1 shrink-0">
|
||
{step.modules.map((mod) => (
|
||
<span key={mod} className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-cyan-600/20 text-cyan-300">
|
||
{mod}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 2. 시스템별 기능 유무 매트릭스 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 시스템별 기능 유무 매트릭스</h3>
|
||
<p className="text-xs text-t2 leading-relaxed mb-4">
|
||
각 시스템(업무 모듈)별 기능의 유무를 파악하여 공통기능, 기본정보 관리(사용자, 지도 등) 등
|
||
통합할 수 있는 기능을 표시한다. <span className="text-cyan-400 font-medium">통합 대상</span> 기능은
|
||
공통 모듈로 일원화하여 중복 개발을 방지한다.
|
||
</p>
|
||
<div className="overflow-auto">
|
||
<table className="w-full text-xs border-collapse">
|
||
<thead>
|
||
<tr className="bg-bg-elevated text-t3 tracking-wide">
|
||
<th className="px-2 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap sticky left-0 bg-bg-elevated z-10">
|
||
기능
|
||
</th>
|
||
<th className="px-2 py-2 text-center font-medium border-b border-stroke-1 whitespace-nowrap">
|
||
분류
|
||
</th>
|
||
<th className="px-2 py-2 text-center font-medium border-b border-stroke-1 whitespace-nowrap">
|
||
통합
|
||
</th>
|
||
{SYSTEM_MODULES.map((mod) => (
|
||
<th key={mod} className="px-1.5 py-2 text-center font-medium border-b border-stroke-1 whitespace-nowrap">
|
||
<span className="writing-mode-vertical text-[10px]">{mod}</span>
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{FEATURE_MATRIX.map((row) => (
|
||
<tr key={row.feature} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||
<td className="px-2 py-1.5 font-medium text-t1 whitespace-nowrap sticky left-0 bg-bg-base z-10">
|
||
{row.feature}
|
||
</td>
|
||
<td className="px-2 py-1.5 text-center">
|
||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${CATEGORY_STYLES[row.category]}`}>
|
||
{row.category}
|
||
</span>
|
||
</td>
|
||
<td className="px-2 py-1.5 text-center">
|
||
{row.integrated ? (
|
||
<span className="text-cyan-400 font-semibold">통합</span>
|
||
) : (
|
||
<span className="text-t3">개별</span>
|
||
)}
|
||
</td>
|
||
{SYSTEM_MODULES.map((mod) => (
|
||
<td key={mod} className="px-1.5 py-1.5 text-center">
|
||
{row.systems[mod] ? (
|
||
<span className="text-emerald-400 font-bold">O</span>
|
||
) : (
|
||
<span className="text-t3/30">-</span>
|
||
)}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{/* 범례 */}
|
||
<div className="flex gap-4 mt-3">
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 text-cyan-300">공통기능</span>
|
||
<span className="text-xs text-t3">전 모듈 공통 적용</span>
|
||
</div>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-600/20 text-emerald-300">기본정보관리</span>
|
||
<span className="text-xs text-t3">사용자·지도·메뉴·설정 통합 관리</span>
|
||
</div>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-bg-elevated text-t3">업무기능</span>
|
||
<span className="text-xs text-t3">모듈별 고유 기능</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 3. 공통기능 상세 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 공통기능 상세</h3>
|
||
<div className="flex flex-col gap-3">
|
||
{COMMON_FEATURES.map((feature, idx) => (
|
||
<div key={feature.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||
<div className="flex items-center gap-2 mb-1.5">
|
||
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-cyan-600 text-white text-xs font-semibold shrink-0">
|
||
{idx + 1}
|
||
</span>
|
||
<p className="text-xs font-semibold text-t1">{feature.title}</p>
|
||
</div>
|
||
<p className="text-xs text-t2 leading-relaxed mb-2 pl-7">{feature.description}</p>
|
||
<ul className="flex flex-col gap-1 pl-7">
|
||
{feature.details.map((detail) => (
|
||
<li key={detail} className="text-xs text-t3 leading-relaxed list-disc">
|
||
{detail}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 4. 공통 모듈 구조 */}
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-t1 mb-3">4. 공통 모듈 디렉토리 구조</h3>
|
||
<div className="overflow-auto">
|
||
<table className="w-full text-xs border-collapse">
|
||
<thead>
|
||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||
{['디렉토리', '역할', '주요 파일'].map((h) => (
|
||
<th
|
||
key={h}
|
||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||
>
|
||
{h}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ dir: 'common/components/', role: '공통 UI 컴포넌트', files: 'auth/, layout/, map/, ui/, layer/' },
|
||
{ dir: 'common/hooks/', role: '공통 커스텀 훅', files: 'useLayers, useSubMenu, useFeatureTracking' },
|
||
{ dir: 'common/services/', role: 'API 통신 모듈', files: 'api.ts, authApi.ts, layerService.ts' },
|
||
{ dir: 'common/store/', role: '전역 상태 스토어', files: 'authStore.ts, menuStore.ts' },
|
||
{ dir: 'common/styles/', role: 'CSS @layer 스타일', files: 'base.css, components.css, wing.css' },
|
||
{ dir: 'common/types/', role: '공통 타입 정의', files: 'backtrack, hns, navigation 등' },
|
||
{ dir: 'common/utils/', role: '유틸리티 함수', files: 'coordinates, geo, sanitize, cn.ts' },
|
||
{ dir: 'common/constants/', role: '상수 정의', files: 'featureIds.ts' },
|
||
{ dir: 'common/data/', role: 'UI 데이터', files: 'layerData.ts (레이어 트리)' },
|
||
].map((row) => (
|
||
<tr key={row.dir} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap font-mono">{row.dir}</td>
|
||
<td className="px-3 py-2 text-t2">{row.role}</td>
|
||
<td className="px-3 py-2 text-t3 font-mono">{row.files}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── 메인 패널 ───────────────────────────────────────────────────────────────────
|
||
|
||
export default function SystemArchPanel() {
|
||
const [activeTab, setActiveTab] = useState<TabId>('framework');
|
||
|
||
return (
|
||
<div className="flex flex-col h-full overflow-hidden">
|
||
{/* 헤더 */}
|
||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||
<h2 className="text-sm font-semibold text-t1">시스템구조</h2>
|
||
</div>
|
||
|
||
{/* 탭 버튼 */}
|
||
<div className="flex gap-1.5 px-5 py-2.5 border-b border-stroke-1 shrink-0 bg-bg-base">
|
||
{TABS.map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id)}
|
||
className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
|
||
activeTab === tab.id
|
||
? 'bg-cyan-600 text-white'
|
||
: 'bg-bg-elevated text-t2 hover:bg-bg-card'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* 탭 콘텐츠 */}
|
||
<div className="flex-1 overflow-auto">
|
||
{activeTab === 'framework' && <FrameworkTab />}
|
||
{activeTab === 'target' && <TargetArchTab />}
|
||
{activeTab === 'interface' && <InterfaceTab />}
|
||
{activeTab === 'heterogeneous' && <HeterogeneousTab />}
|
||
{activeTab === 'common-features' && <CommonFeaturesTab />}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|