wing-ops/frontend/src/tabs/admin/components/SystemArchPanel.tsx
Nan Kyung Lee 387e2a2e40 feat(rescue): 긴급구난/예측도 OSM 지도 적용 및 관리자 패널 추가
- 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>
2026-04-11 19:46:12 +09:00

1545 lines
64 KiB
TypeScript
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}