commit c0ce01eaf69d31da0c5b6ecddb3c96df56e253af Author: htlee Date: Mon Apr 6 14:11:29 2026 +0900 chore: 팀 워크플로우 기반 초기 프로젝트 구성 KCG AI 기반 불법조업 탐지·차단 플랫폼 프론트엔드. React 19 + TypeScript 5.9 + Vite 8 + MapLibre + deck.gl + Zustand + Tailwind CSS. SFR 20개 전체 UI 구현 완료, 백엔드 연동 대기. - npm + Nexus 프록시 레지스트리 설정 - 팀 워크플로우 v1.6.1 부트스트랩 파일 배치 - .githooks (commit-msg, post-checkout) - package.json name: kcg-ai-monitoring v0.1.0 Co-Authored-By: Claude Opus 4.6 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..3c81391 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "env": { + "CLAUDE_BOT_TOKEN": "ac15488ad66463bd5c4e3be1fa6dd5b2743813c5" + }, + "permissions": { + "allow": [ + "Bash(npm run *)", + "Bash(npm install *)", + "Bash(npm test *)", + "Bash(npx *)", + "Bash(node *)", + "Bash(git status)", + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git branch *)", + "Bash(git checkout *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(git pull *)", + "Bash(git fetch *)", + "Bash(git merge *)", + "Bash(git stash *)", + "Bash(git remote *)", + "Bash(git config *)", + "Bash(git rev-parse *)", + "Bash(git show *)", + "Bash(git tag *)", + "Bash(curl -s *)", + "Bash(fnm *)" + ], + "deny": [ + "Bash(git push --force*)", + "Bash(git push -f *)", + "Bash(git push origin --force*)", + "Bash(git reset --hard*)", + "Bash(git clean -fd*)", + "Bash(git checkout -- .)", + "Bash(git restore .)", + "Bash(rm -rf /)", + "Bash(rm -rf ~)", + "Bash(rm -rf .git*)", + "Bash(rm -rf /*)", + "Bash(rm -rf node_modules)", + "Read(./**/.env)", + "Read(./**/.env.*)", + "Read(./**/secrets/**)" + ] + } +} diff --git a/.claude/skills/init-project/SKILL.md b/.claude/skills/init-project/SKILL.md new file mode 100644 index 0000000..d0df8e5 --- /dev/null +++ b/.claude/skills/init-project/SKILL.md @@ -0,0 +1,295 @@ +--- +name: init-project +description: 팀 표준 워크플로우로 프로젝트를 초기화합니다 +argument-hint: "[project-type: java-maven|java-gradle|react-ts|auto]" +--- + +팀 표준 워크플로우에 따라 프로젝트를 초기화합니다. +프로젝트 타입: $ARGUMENTS (기본: auto — 자동 감지) + +## 프로젝트 타입 자동 감지 + +$ARGUMENTS가 "auto"이거나 비어있으면 다음 순서로 감지: +1. `pom.xml` 존재 → **java-maven** +2. `build.gradle` 또는 `build.gradle.kts` 존재 → **java-gradle** +3. `package.json` + `tsconfig.json` 존재 → **react-ts** +4. 감지 실패 → 사용자에게 타입 선택 요청 + +## 수행 단계 + +### 1. 프로젝트 분석 +- 빌드 파일, 설정 파일, 디렉토리 구조 파악 +- 사용 중인 프레임워크, 라이브러리 감지 +- 기존 `.claude/` 디렉토리 존재 여부 확인 +- eslint, prettier, checkstyle, spotless 등 lint 도구 설치 여부 확인 + +### 2. CLAUDE.md 생성 +프로젝트 루트에 CLAUDE.md를 생성하고 다음 내용 포함: +- 프로젝트 개요 (이름, 타입, 주요 기술 스택) +- 빌드/실행 명령어 (감지된 빌드 도구 기반) +- 테스트 실행 명령어 +- lint 실행 명령어 (감지된 도구 기반) +- 프로젝트 디렉토리 구조 요약 +- 팀 컨벤션 참조 (`.claude/rules/` 안내) + +### Gitea 파일 다운로드 URL 패턴 +⚠️ Gitea raw 파일은 반드시 **web raw URL**을 사용해야 합니다 (`/api/v1/` 경로 사용 불가): +```bash +GITEA_URL="${GITEA_URL:-https://gitea.gc-si.dev}" +# common 파일: ${GITEA_URL}/gc/template-common/raw/branch/develop/<파일경로> +# 타입별 파일: ${GITEA_URL}/gc/template-<타입>/raw/branch/develop/<파일경로> +# 예시: +curl -sf "${GITEA_URL}/gc/template-common/raw/branch/develop/.claude/rules/team-policy.md" +curl -sf "${GITEA_URL}/gc/template-react-ts/raw/branch/develop/.editorconfig" +``` + +### 3. .claude/ 디렉토리 구성 +이미 팀 표준 파일이 존재하면 건너뜀. 없는 경우 위의 URL 패턴으로 Gitea에서 다운로드: +- `.claude/settings.json` — 프로젝트 타입별 표준 권한 설정 + env(CLAUDE_BOT_TOKEN 등) + hooks 섹션 (4단계 참조) + +⚠️ 팀 규칙(.claude/rules/), 에이전트(.claude/agents/), 스킬 6종, 스크립트는 12단계(sync-team-workflow)에서 자동 다운로드된다. 여기서는 settings.json만 설정한다. + +### 3.5. Gitea 토큰 설정 + +**CLAUDE_BOT_TOKEN** (팀 공용): `settings.json`의 `env` 필드에 이미 포함되어 있음 (3단계에서 설정됨). 별도 조치 불필요. + +**GITEA_TOKEN** (개인): `/push`, `/mr`, `/release` 등 Git 스킬에 필요한 개인 토큰. + +```bash +# 현재 GITEA_TOKEN 설정 여부 확인 +if [ -z "$GITEA_TOKEN" ]; then + echo "GITEA_TOKEN 미설정" +fi +``` + +**GITEA_TOKEN이 없는 경우**, 다음 안내를 **AskUserQuestion**으로 표시: + +**질문**: "GITEA_TOKEN이 설정되지 않았습니다. Gitea 개인 토큰을 생성하시겠습니까?" +- 옵션 1: 토큰 생성 안내 보기 (추천) +- 옵션 2: 이미 있음 (토큰 입력) +- 옵션 3: 나중에 하기 + +**토큰 생성 안내 선택 시**, 다음 내용을 표시: + +``` +📋 Gitea 토큰 생성 방법: + +1. 브라우저에서 접속: + https://gitea.gc-si.dev/user/settings/applications + +2. "Manage Access Tokens" 섹션에서 "Generate New Token" 클릭 + +3. 입력: + - Token Name: "claude-code" (자유롭게 지정) + - Repository and Organization Access: ✅ All (public, private, and limited) + +4. Select permissions (아래 4개만 설정, 나머지는 No Access 유지): + + ┌─────────────────┬──────────────────┬──────────────────────────────┐ + │ 항목 │ 권한 │ 용도 │ + ├─────────────────┼──────────────────┼──────────────────────────────┤ + │ issue │ Read and Write │ /fix-issue 이슈 조회/코멘트 │ + │ organization │ Read │ gc 조직 리포 접근 │ + │ repository │ Read and Write │ /push, /mr, /release API 호출 │ + │ user │ Read │ API 사용자 인증 확인 │ + └─────────────────┴──────────────────┴──────────────────────────────┘ + +5. "Generate Token" 클릭 → ⚠️ 토큰이 한 번만 표시됩니다! 반드시 복사하세요. +``` + +표시 후 **AskUserQuestion**: "생성한 토큰을 입력하세요" +- 옵션 1: 토큰 입력 (Other로 입력) +- 옵션 2: 나중에 하기 + +**토큰 입력 시**: + +1. Gitea API로 유효성 검증: +```bash +curl -sf "https://gitea.gc-si.dev/api/v1/user" \ + -H "Authorization: token <입력된 토큰>" +``` +- 성공: `✅ () 인증 확인` 출력 +- 실패: `❌ 토큰이 유효하지 않습니다. 다시 확인해주세요.` 출력 → 재입력 요청 + +2. `.claude/settings.local.json`에 저장 (이 파일은 .gitignore에 포함, 리포 커밋 안됨): +```json +{ + "env": { + "GITEA_TOKEN": "<입력된 토큰>" + } +} +``` + +기존 `settings.local.json`이 있으면 `env.GITEA_TOKEN`만 추가/갱신. + +**나중에 하기 선택 시**: 경고 표시 후 다음 단계로 진행: +``` +⚠️ GITEA_TOKEN 없이는 /push, /mr, /release 스킬을 사용할 수 없습니다. + 나중에 토큰을 생성하면 .claude/settings.local.json에 다음을 추가하세요: + { "env": { "GITEA_TOKEN": "your-token-here" } } +``` + +### 4. Hook 스크립트 설정 + +⚠️ `.claude/scripts/` 스크립트 파일은 12단계(sync-team-workflow)에서 서버로부터 자동 다운로드된다. +여기서는 `settings.json`에 hooks 섹션만 설정한다. + +`.claude/settings.json`에 hooks 섹션이 없으면 추가 (기존 settings.json의 내용에 병합): + +```json +{ + "hooks": { + "SessionStart": [ + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "bash .claude/scripts/on-post-compact.sh", + "timeout": 10 + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "bash .claude/scripts/on-pre-compact.sh", + "timeout": 30 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash .claude/scripts/on-commit.sh", + "timeout": 15 + } + ] + } + ] + } +} +``` + +### 5. Git Hooks 설정 +```bash +git config core.hooksPath .githooks +``` +`.githooks/` 디렉토리에 실행 권한 부여: +```bash +chmod +x .githooks/* +``` + +**pre-commit 훅 검증**: `.githooks/pre-commit`을 실행하여 빌드 검증이 정상 동작하는지 확인. +에러 발생 시 (예: 모노레포가 아닌 특수 구조, 빌드 명령 불일치 등): +1. 프로젝트에 맞게 `.githooks/pre-commit`을 커스텀 수정 +2. `.claude/workflow-version.json`에 `"custom_pre_commit": true` 추가 +3. 이후 `/sync-team-workflow` 실행 시 pre-commit은 덮어쓰지 않고 보존됨 + (`commit-msg`, `post-checkout`은 항상 팀 표준으로 동기화) + +### 6. 프로젝트 타입별 추가 설정 + +#### java-maven +- `.sdkmanrc` 생성 (java=17.0.18-amzn 또는 프로젝트에 맞는 버전) +- `.mvn/settings.xml` Nexus 미러 설정 확인 +- `mvn compile` 빌드 성공 확인 + +#### java-gradle +- `.sdkmanrc` 생성 +- `gradle.properties.example` Nexus 설정 확인 +- `./gradlew compileJava` 빌드 성공 확인 + +#### react-ts +- `.node-version` 생성 (프로젝트에 맞는 Node 버전) +- `.npmrc` Nexus 레지스트리 설정 확인 +- `npm install && npm run build` 성공 확인 + +### 7. .gitignore 확인 +다음 항목이 .gitignore에 포함되어 있는지 확인하고, 없으면 추가: +``` +.claude/settings.local.json +.claude/CLAUDE.local.md +.env +.env.* +*.local +``` + +**팀 워크플로우 관리 경로** (sync로 생성/관리되는 파일, 리포에 커밋하지 않음): +``` +# Team workflow (managed by /sync-team-workflow) +.claude/rules/ +.claude/agents/ +.claude/skills/push/ +.claude/skills/mr/ +.claude/skills/create-mr/ +.claude/skills/release/ +.claude/skills/version/ +.claude/skills/fix-issue/ +.claude/scripts/ +``` + +### 8. Git exclude 설정 +`.git/info/exclude` 파일을 읽고, 기존 내용을 보존하면서 하단에 추가: + +```gitignore + +# Claude Code 워크플로우 (로컬 전용) +docs/CHANGELOG.md +*.tmp +``` + +### 9. Memory 초기화 +프로젝트 memory 디렉토리의 위치를 확인하고 (보통 `~/.claude/projects//memory/`) 다음 파일들을 생성: + +- `memory/MEMORY.md` — 프로젝트 분석 결과 기반 핵심 요약 (200줄 이내) + - 현재 상태, 프로젝트 개요, 기술 스택, 주요 패키지 구조, 상세 참조 링크 +- `memory/project-snapshot.md` — 디렉토리 구조, 패키지 구성, 주요 의존성, API 엔드포인트 +- `memory/project-history.md` — "초기 팀 워크플로우 구성" 항목으로 시작 +- `memory/api-types.md` — 주요 인터페이스/DTO/Entity 타입 요약 +- `memory/decisions.md` — 빈 템플릿 (# 의사결정 기록) +- `memory/debugging.md` — 빈 템플릿 (# 디버깅 경험 & 패턴) + +### 10. Lint 도구 확인 +- TypeScript: eslint, prettier 설치 여부 확인. 미설치 시 사용자에게 설치 제안 +- Java: checkstyle, spotless 등 설정 확인 +- CLAUDE.md에 lint 실행 명령어가 이미 기록되었는지 확인 + +### 11. workflow-version.json 생성 +Gitea API로 최신 팀 워크플로우 버전을 조회: +```bash +curl -sf --max-time 5 "https://gitea.gc-si.dev/gc/template-common/raw/branch/develop/workflow-version.json" +``` +조회 성공 시 해당 `version` 값 사용, 실패 시 "1.0.0" 기본값 사용. + +`.claude/workflow-version.json` 파일 생성: +```json +{ + "applied_global_version": "<조회된 버전>", + "applied_date": "<현재날짜>", + "project_type": "<감지된타입>", + "gitea_url": "https://gitea.gc-si.dev" +} +``` + +### 12. 팀 워크플로우 최신화 + +`/sync-team-workflow`를 자동으로 1회 실행하여 최신 팀 파일(rules, agents, skills 6종, scripts, hooks)을 서버에서 다운로드하고 로컬에 적용한다. + +이 단계에서 `.claude/rules/`, `.claude/agents/`, `.claude/skills/push/` 등 팀 관리 파일이 생성된다. +(이 파일들은 7단계에서 .gitignore에 추가되었으므로 리포에 커밋되지 않음) + +### 13. 검증 및 요약 +- 생성/수정된 파일 목록 출력 +- `git config core.hooksPath` 확인 +- 빌드 명령 실행 가능 확인 +- Hook 스크립트 실행 권한 확인 +- 다음 단계 안내: + - 개발 시작, 첫 커밋 방법 + - 범용 스킬: `/api-registry`, `/changelog`, `/swagger-spec` diff --git a/.claude/skills/sync-team-workflow/SKILL.md b/.claude/skills/sync-team-workflow/SKILL.md new file mode 100644 index 0000000..5aa6441 --- /dev/null +++ b/.claude/skills/sync-team-workflow/SKILL.md @@ -0,0 +1,165 @@ +--- +name: sync-team-workflow +description: 팀 글로벌 워크플로우를 현재 프로젝트에 동기화합니다 +--- + +팀 글로벌 워크플로우의 최신 파일을 서버에서 다운로드하여 로컬에 적용합니다. +호출 시 항상 서버 기준으로 전체 동기화합니다 (버전 비교 없음). + +## 수행 절차 + +### 1. 사전 조건 확인 + +`.claude/workflow-version.json` 존재 확인: +- 없으면 → "/init-project를 먼저 실행해주세요" 안내 후 종료 + +설정 읽기: +```bash +GITEA_URL=$(python3 -c "import json; print(json.load(open('.claude/workflow-version.json')).get('gitea_url', 'https://gitea.gc-si.dev'))" 2>/dev/null || echo "https://gitea.gc-si.dev") +PROJECT_TYPE=$(python3 -c "import json; print(json.load(open('.claude/workflow-version.json')).get('project_type', ''))" 2>/dev/null || echo "") +``` + +프로젝트 타입이 비어있으면 자동 감지: +1. `pom.xml` → java-maven +2. `build.gradle` / `build.gradle.kts` → java-gradle +3. `package.json` + `tsconfig.json` → react-ts +4. 감지 실패 → 사용자에게 선택 요청 + +### Gitea 파일 다운로드 URL 패턴 +⚠️ Gitea raw 파일은 반드시 **web raw URL** 사용: +```bash +# common 파일: ${GITEA_URL}/gc/template-common/raw/branch/develop/<파일경로> +# 타입별 파일: ${GITEA_URL}/gc/template-${PROJECT_TYPE}/raw/branch/develop/<파일경로> +``` + +### 2. 디렉토리 준비 + +필요한 디렉토리가 없으면 생성: +```bash +mkdir -p .claude/rules .claude/agents .claude/scripts +mkdir -p .claude/skills/push .claude/skills/mr .claude/skills/create-mr +mkdir -p .claude/skills/release .claude/skills/version .claude/skills/fix-issue +mkdir -p .githooks +``` + +### 3. 서버 파일 다운로드 + 적용 + +각 파일을 `curl -sf` 로 다운로드하여 프로젝트 루트의 동일 경로에 저장. +다운로드 실패한 파일은 경고 출력 후 건너뜀. + +#### 3-1. template-common 파일 (덮어쓰기) + +**규칙 파일**: +``` +.claude/rules/team-policy.md +.claude/rules/git-workflow.md +.claude/rules/release-notes-guide.md +.claude/rules/subagent-policy.md +``` + +**에이전트 파일**: +``` +.claude/agents/explorer.md +.claude/agents/implementer.md +.claude/agents/reviewer.md +``` + +**스킬 파일 (6종)**: +``` +.claude/skills/push/SKILL.md +.claude/skills/mr/SKILL.md +.claude/skills/create-mr/SKILL.md +.claude/skills/release/SKILL.md +.claude/skills/version/SKILL.md +.claude/skills/fix-issue/SKILL.md +``` + +**Hook 스크립트**: +``` +.claude/scripts/on-pre-compact.sh +.claude/scripts/on-post-compact.sh +.claude/scripts/on-commit.sh +``` + +**Git Hooks** (commit-msg, post-checkout은 항상 교체): +``` +.githooks/commit-msg +.githooks/post-checkout +``` + +다운로드 예시: +```bash +curl -sf "${GITEA_URL}/gc/template-common/raw/branch/develop/.claude/rules/team-policy.md" -o ".claude/rules/team-policy.md" +``` + +#### 3-2. template-{type} 파일 (타입별 덮어쓰기) + +``` +.claude/rules/code-style.md +.claude/rules/naming.md +.claude/rules/testing.md +``` + +**pre-commit hook**: +`.claude/workflow-version.json`의 `custom_pre_commit` 플래그 확인: +- `"custom_pre_commit": true` → pre-commit 건너뜀, "⚠️ pre-commit은 프로젝트 커스텀 유지" 로그 +- 플래그 없거나 false → `.githooks/pre-commit` 교체 + +다운로드 예시: +```bash +curl -sf "${GITEA_URL}/gc/template-${PROJECT_TYPE}/raw/branch/develop/.claude/rules/code-style.md" -o ".claude/rules/code-style.md" +``` + +#### 3-3. 실행 권한 부여 +```bash +chmod +x .githooks/* 2>/dev/null +chmod +x .claude/scripts/*.sh 2>/dev/null +``` + +### 4. settings.json 부분 머지 + +⚠️ settings.json은 **타입별 템플릿**에서 다운로드 (template-common에는 없음): +```bash +SERVER_SETTINGS=$(curl -sf "${GITEA_URL}/gc/template-${PROJECT_TYPE}/raw/branch/develop/.claude/settings.json") +``` + +다운로드한 최신 settings.json과 로컬 `.claude/settings.json`을 비교하여 부분 갱신: +- `env`: 서버 최신으로 교체 +- `deny` 목록: 서버 최신으로 교체 +- `allow` 목록: 기존 사용자 커스텀 유지 + 서버 기본값 병합 +- `hooks`: 서버 최신으로 교체 + +### 5. workflow-version.json 갱신 + +서버의 최신 `workflow-version.json` 조회: +```bash +SERVER_VER=$(curl -sf "${GITEA_URL}/gc/template-common/raw/branch/develop/workflow-version.json") +SERVER_VERSION=$(echo "$SERVER_VER" | python3 -c "import sys,json; print(json.load(sys.stdin).get('version',''))") +``` + +`.claude/workflow-version.json` 업데이트: +```json +{ + "applied_global_version": "<서버 version>", + "applied_date": "<현재날짜>", + "project_type": "<프로젝트타입>", + "gitea_url": "" +} +``` +기존 필드(`custom_pre_commit` 등)는 보존. + +### 6. 변경 보고 + +- 다운로드/갱신된 파일 목록 출력 +- 서버 `workflow-version.json`의 `changes` 중 최신 항목 표시 +- 결과 형태: +``` +✅ 팀 워크플로우 동기화 완료 + 버전: v1.6.0 + 갱신 파일: 22개 (rules 7, agents 3, skills 6, scripts 3, hooks 3) + settings.json: 부분 갱신 (env, deny, hooks) +``` + +## 필요 환경변수 + +없음 (Gitea raw URL은 인증 불필요) diff --git a/.claude/workflow-version.json b/.claude/workflow-version.json new file mode 100644 index 0000000..6d55bf2 --- /dev/null +++ b/.claude/workflow-version.json @@ -0,0 +1,6 @@ +{ + "applied_global_version": "1.6.1", + "applied_date": "2026-04-06", + "project_type": "react-ts", + "gitea_url": "https://gitea.gc-si.dev" +} diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..93bb350 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,60 @@ +#!/bin/bash +#============================================================================== +# commit-msg hook +# Conventional Commits 형식 검증 (한/영 혼용 지원) +#============================================================================== + +COMMIT_MSG_FILE="$1" +COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") + +# Merge 커밋은 검증 건너뜀 +if echo "$COMMIT_MSG" | head -1 | grep -qE "^Merge "; then + exit 0 +fi + +# Revert 커밋은 검증 건너뜀 +if echo "$COMMIT_MSG" | head -1 | grep -qE "^Revert "; then + exit 0 +fi + +# Conventional Commits 정규식 +# type(scope): subject +# - type: feat|fix|docs|style|refactor|test|chore|ci|perf (필수) +# - scope: 영문, 숫자, 한글, 점, 밑줄, 하이픈 허용 (선택) +# - subject: 1~72자, 한/영 혼용 허용 (필수) +PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\([a-zA-Z0-9가-힣._-]+\))?: .{1,72}$' + +FIRST_LINE=$(head -1 "$COMMIT_MSG_FILE") + +if ! echo "$FIRST_LINE" | grep -qE "$PATTERN"; then + echo "" + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ 커밋 메시지가 Conventional Commits 형식에 맞지 않습니다 ║" + echo "╚══════════════════════════════════════════════════════════════╝" + echo "" + echo " 올바른 형식: type(scope): subject" + echo "" + echo " type (필수):" + echo " feat — 새로운 기능" + echo " fix — 버그 수정" + echo " docs — 문서 변경" + echo " style — 코드 포맷팅" + echo " refactor — 리팩토링" + echo " test — 테스트" + echo " chore — 빌드/설정 변경" + echo " ci — CI/CD 변경" + echo " perf — 성능 개선" + echo "" + echo " scope (선택): 한/영 모두 가능" + echo " subject (필수): 1~72자, 한/영 모두 가능" + echo "" + echo " 예시:" + echo " feat(auth): JWT 기반 로그인 구현" + echo " fix(배치): 야간 배치 타임아웃 수정" + echo " docs: README 업데이트" + echo " chore: Gradle 의존성 업데이트" + echo "" + echo " 현재 메시지: $FIRST_LINE" + echo "" + exit 1 +fi diff --git a/.githooks/post-checkout b/.githooks/post-checkout new file mode 100755 index 0000000..bae360f --- /dev/null +++ b/.githooks/post-checkout @@ -0,0 +1,25 @@ +#!/bin/bash +#============================================================================== +# post-checkout hook +# 브랜치 체크아웃 시 core.hooksPath 자동 설정 +# clone/checkout 후 .githooks 디렉토리가 있으면 자동으로 hooksPath 설정 +#============================================================================== + +# post-checkout 파라미터: prev_HEAD, new_HEAD, branch_flag +# branch_flag=1: 브랜치 체크아웃, 0: 파일 체크아웃 +BRANCH_FLAG="$3" + +# 파일 체크아웃은 건너뜀 +if [ "$BRANCH_FLAG" = "0" ]; then + exit 0 +fi + +# .githooks 디렉토리 존재 확인 +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +if [ -d "${REPO_ROOT}/.githooks" ]; then + CURRENT_HOOKS_PATH=$(git config core.hooksPath 2>/dev/null || echo "") + if [ "$CURRENT_HOOKS_PATH" != ".githooks" ]; then + git config core.hooksPath .githooks + chmod +x "${REPO_ROOT}/.githooks/"* 2>/dev/null + fi +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10ab9d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# === Build === +dist/ +build/ + +# === Dependencies === +node_modules/ + +# === IDE === +.idea/ +.vscode/ +*.swp +*.swo + +# === OS === +.DS_Store +Thumbs.db + +# === Environment === +.env +.env.* +!.env.example +secrets/ + +# === Debug === +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# === Test === +coverage/ + +# === Cache === +.eslintcache +.prettiercache +*.tsbuildinfo + +# === Code Review Graph (로컬 전용) === +.code-review-graph/ + +# === 대용량/참고 문서 === +*.hwpx + +# === Claude Code === +!.claude/ +.claude/settings.local.json +.claude/CLAUDE.local.md + +# === Team workflow (managed by /sync-team-workflow) === +.claude/rules/ +.claude/agents/ +.claude/skills/push/ +.claude/skills/mr/ +.claude/skills/create-mr/ +.claude/skills/release/ +.claude/skills/version/ +.claude/skills/fix-issue/ +.claude/scripts/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..67e04d2 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# Nexus npm 프록시 레지스트리 +registry=https://nexus.gc-si.dev/repository/npm-public/ + +# Nexus 인증 +//nexus.gc-si.dev/repository/npm-public/:_auth=YWRtaW46R2NzYyE4OTMy diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md new file mode 100644 index 0000000..f561b6d --- /dev/null +++ b/ATTRIBUTIONS.md @@ -0,0 +1,3 @@ +이 Figma Make 파일에는 [shadcn/ui](https://ui.shadcn.com/)의 컴포넌트가 포함되어 있으며, [MIT 라이선스](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md)에 따라 사용됩니다. + +이 Figma Make 파일에는 [Unsplash](https://unsplash.com)의 사진이 포함되어 있으며, 이는 [라이선스](https://unsplash.com/license)에 따라 사용되었습니다. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9086c0 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# AI 기반 불법조업 탐지·차단 플랫폼 + +해양경찰청 AIS 신호 기반 불법 조업 선박 탐지, 단속 의사결정 지원 플랫폼. + +## 기술 스택 + +| 분류 | 기술 | 버전 | +|---|---|---| +| 프레임워크 | React + TypeScript | 19.2 / 5.9 | +| 번들러 | Vite (Rolldown) | 8.0 | +| 지도 | MapLibre GL + deck.gl | 5.22 / 9.2 | +| 차트 | ECharts | 6.0 | +| 상태관리 | Zustand | 5.0 | +| 스타일 | Tailwind CSS + CVA | 4.2 / 0.7 | +| 다국어 | react-i18next | ko / en | +| 린트 | ESLint (Flat Config) | 10 | + +## 실행 + +```bash +npm install +npm run dev # 개발 서버 +npm run build # 프로덕션 빌드 (~480ms) +npm run lint # ESLint 검사 +``` + +## 프로젝트 구조 + +``` +src/ +├── lib/charts/ ECharts 공통 (BaseChart + 프리셋) +├── lib/map/ MapLibre + deck.gl (BaseMap + 레이어 + hooks) +├── lib/i18n/ 다국어 (10 네임스페이스, ko/en) +├── lib/theme/ 디자인 토큰 + CVA 변형 +├── data/mock/ 공유 더미 데이터 (7 모듈) +├── stores/ Zustand 스토어 (8개) +├── services/ API 서비스 샘플 +├── shared/ 공유 UI 컴포넌트 +├── features/ 도메인별 페이지 (13그룹, 31페이지) +├── app/ 라우터, 인증, 레이아웃 +└── styles/ CSS (Dark/Light 테마) +``` + +## 문서 + +| 문서 | 설명 | +|---|---| +| [docs/architecture.md](docs/architecture.md) | 아키텍처 현황 (기술스택, 구조, 렌더링 최적화, 테마) | +| [docs/sfr-user-guide.md](docs/sfr-user-guide.md) | SFR 사용자 가이드 (메뉴별 기능 설명, 구현/미구현 현황) | +| [docs/sfr-traceability.md](docs/sfr-traceability.md) | SFR 요구사항 추적 매트릭스 (개발자용, 소스 경로 포함) | +| [docs/page-workflow.md](docs/page-workflow.md) | 31개 페이지 역할 + 4개 업무 파이프라인 | +| [docs/data-sharing-analysis.md](docs/data-sharing-analysis.md) | 데이터 공유 분석 + mock 통합 결과 | +| [docs/next-refactoring.md](docs/next-refactoring.md) | 다음 단계 TODO (API 연동, 실시간, 코드 스플리팅) | + +## SFR 요구사항 대응 현황 + +20개 SFR 전체 UI 구현 완료. 백엔드 연동 대기 중. + +| SFR | 기능 | 화면 | 상태 | +|---|---|---|---| +| SFR-01 | 로그인·권한 관리 | `/login`, `/access-control` | UI 완료 | +| SFR-02 | 환경설정·공지·공통 | `/system-config`, `/notices` | UI 완료 | +| SFR-03 | 통합 데이터 허브 | `/data-hub` | UI 완료 | +| SFR-04 | AI 예측모델 관리 | `/ai-model` | UI 완료 | +| SFR-05 | 위험도 지도 | `/risk-map` | UI 완료 | +| SFR-06 | 단속 계획·경보 | `/enforcement-plan` | UI 완료 | +| SFR-07 | 단일함정 순찰경로 | `/patrol-route` | UI 완료 | +| SFR-08 | 다함정 경로최적화 | `/fleet-optimization` | UI 완료 | +| SFR-09 | Dark Vessel 탐지 | `/dark-vessel` | UI 완료 | +| SFR-10 | 어구 탐지 | `/gear-detection` | UI 완료 | +| SFR-11 | 단속·탐지 이력 | `/enforcement-history` | UI 완료 | +| SFR-12 | 모니터링 대시보드 | `/dashboard`, `/monitoring` | UI 완료 | +| SFR-13 | 통계·성과 분석 | `/statistics` | UI 완료 | +| SFR-14 | 외부 서비스 연계 | `/external-service` | UI 완료 | +| SFR-15 | 모바일 서비스 | `/mobile-service` | UI 완료 | +| SFR-16 | 함정 Agent | `/ship-agent` | UI 완료 | +| SFR-17 | AI 알림 발송 | `/ai-alert` | UI 완료 | +| SFR-18/19 | MLOps / LLMOps | `/mlops` | UI 완료 | +| SFR-20 | AI Q&A 지원 | `/ai-assistant` | UI 완료 | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..7d4200e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,370 @@ +# KCG AI Monitoring - 아키텍처 문서 + +> AI 기반 불법조업 탐지 차단 플랫폼 프론트엔드 + +--- + +## 기술스택 + +| 분류 | 라이브러리 | 버전 | 역할 | +|------|-----------|------|------| +| UI 프레임워크 | React | 19.2.4 | 함수형 컴포넌트 + Hooks 기반 SPA | +| 언어 | TypeScript | 5.9 | 정적 타입, strict 모드 | +| 빌드 도구 | Vite | 8.0.3 | Rolldown 기반 ESM 번들링 (~480ms build), HMR | +| CSS 프레임워크 | Tailwind CSS | 4.2.2 | `@tailwindcss/vite` 플러그인 통합 | +| 지도 (베이스맵) | MapLibre GL | 5.22 | CartoDB 래스터 타일 (Dark/Light 자동 전환) | +| 지도 (벡터) | deck.gl | 9.2 | MapboxOverlay 기반 GPU 벡터 렌더링 (40만척+) | +| 차트 | ECharts | 6.0 (core) | `lib/charts` 래퍼를 통한 프리셋 차트 | +| 상태관리 | Zustand | 5.0 | 8개 독립 스토어 (vessel, patrol, event, kpi 등) | +| 라우팅 | react-router-dom | 7.12.0 | BrowserRouter, 중첩 Route | +| 다국어 | react-i18next + i18next | 17.0 + 26.0 | 10 NS, ko/en 네임스페이스 기반 | +| 스타일 변형 | class-variance-authority (CVA) | 0.7 | card/badge/statusDot variants | +| 아이콘 | lucide-react | 0.487.0 | SVG 아이콘 컴포넌트 | +| 린트 | ESLint | 10.2 | Flat Config, typescript-eslint + react-hooks + react-refresh | + +--- + +## 디렉토리 구조 + +``` +src/ +├── lib/ +│ ├── charts/ # BaseChart + 4 프리셋 (Area, Bar, Pie, Line) +│ │ ├── BaseChart.tsx # 코어 (init, resize, dispose 자동 관리, 'kcg-dark' 테마) +│ │ ├── theme.ts # ECharts 테마 등록 +│ │ ├── tokens.ts # 차트 색상 토큰 +│ │ └── presets/ # AreaChart, BarChart, PieChart, LineChart +│ ├── map/ +│ │ ├── BaseMap.tsx # MapLibre + deck.gl (forwardRef, memo, overlay 노출) +│ │ ├── hooks/ # useMapLayers, useStoreLayerSync (RAF 기반) +│ │ ├── layers/ # markers, polyline, heatmap, zones, static (STATIC_LAYERS) +│ │ │ ├── markers.ts # createMarkerLayer (transitions + DataFilterExtension) +│ │ │ ├── static.ts # EEZ + NLL 싱글턴 (GPU 1회 업로드) +│ │ │ ├── polyline.ts # 경로/트랙 라인 +│ │ │ ├── heatmap.ts # 히트맵 (위험도 시각화) +│ │ │ ├── zones.ts # 구역 원 +│ │ │ └── boundaries.ts # 레거시 GeoJSON (하위 호환) +│ │ ├── controls/ # (예약) 지도 컨트롤 확장 +│ │ ├── constants.ts # EEZ, NLL, 타일 URL, 기본값 +│ │ └── types.ts # MapVessel, MapLayerConfig, HeatPoint +│ ├── i18n/ # 10 NS (common, dashboard, detection, patrol, enforcement, statistics, ai, fieldOps, admin, auth) +│ │ ├── config.ts # i18next 초기화 (ko 기본, en 폴백) +│ │ └── locales/ # ko/*.json, en/*.json (10파일 x 2언어) +│ └── theme/ # tokens, colors, variants (CVA) +│ ├── tokens.ts # CSS 변수 매핑 + resolved 색상값 +│ ├── colors.ts # 시맨틱 팔레트 (risk, alert, vessel, status, chartSeries) +│ └── variants.ts # cardVariants, badgeVariants, statusDotVariants (CVA) +│ +├── data/ +│ ├── mock/ # 7 공유 mock 모듈 +│ │ ├── vessels.ts # 선박 목록 (한국, 중국, 경비함) +│ │ ├── events.ts # 탐지/단속 이벤트 +│ │ ├── transfers.ts # 전재(환적) 데이터 +│ │ ├── patrols.ts # 순찰 경로/일정 +│ │ ├── gear.ts # 어구 탐지 데이터 +│ │ ├── kpi.ts # KPI/통계 데이터 +│ │ └── enforcement.ts # 단속 이력 데이터 +│ ├── areasCodes.json # 해역 코드 (52건) +│ ├── speciesCodes.json # 어종 코드 (578건) +│ ├── fisheryCodes.json # 어업유형 코드 (59건) +│ ├── vesselTypeCodes.json # 선박유형 코드 (186건) +│ └── commonCodes.ts # 코드 유틸리티 +│ +├── stores/ # 8 Zustand 스토어 +│ ├── vesselStore.ts # 선박 목록, 선택, 필터 +│ ├── patrolStore.ts # 순찰 경로/함정 +│ ├── eventStore.ts # 탐지/경보 이벤트 +│ ├── kpiStore.ts # KPI 메트릭, 추세 +│ ├── transferStore.ts # 전재(환적) 데이터 +│ ├── gearStore.ts # 어구 탐지 +│ ├── enforcementStore.ts # 단속 이력 +│ └── settingsStore.ts # theme/language + localStorage 동기화 +│ +├── services/ # 7 API 서비스 (현재 mock 반환) +│ ├── api.ts # fetch 래퍼 (향후 Axios 교체 예정) +│ ├── vessel.ts # getVessels, getSuspects, getVesselDetail +│ ├── event.ts # getEvents, getAlerts +│ ├── patrol.ts # getPatrolShips +│ ├── kpi.ts # getKpiMetrics, getMonthlyTrends, getViolationTypes +│ ├── ws.ts # connectWs (STOMP 스텁, 미구현) +│ └── index.ts # 배럴 export +│ +├── shared/components/ # 공유 UI 컴포넌트 +│ ├── ui/ +│ │ ├── card.tsx # Card(CVA variant), CardHeader, CardTitle, CardContent +│ │ └── badge.tsx # Badge(CVA intent/size) +│ └── common/ +│ ├── DataTable.tsx # 범용 테이블 (가변너비, 검색, 정렬, 페이징, 엑셀, 출력) +│ ├── Pagination.tsx # 페이지네이션 +│ ├── SearchInput.tsx # 검색 입력 +│ ├── ExcelExport.tsx # 엑셀 다운로드 +│ ├── FileUpload.tsx # 파일 업로드 +│ ├── PageToolbar.tsx # 페이지 상단 툴바 +│ ├── PrintButton.tsx # 인쇄 버튼 +│ ├── SaveButton.tsx # 저장 버튼 +│ └── NotificationBanner.tsx # 알림 배너 +│ +├── features/ # 13 도메인 그룹 (31 페이지) +│ ├── dashboard/ # 종합 대시보드 (Dashboard) +│ ├── monitoring/ # 실시간 모니터링 (MonitoringDashboard) +│ ├── surveillance/ # 감시 (LiveMapView, MapControl) +│ ├── detection/ # 탐지 (DarkVessel, Gear, ChinaFishing) +│ ├── risk-assessment/ # 위험도 평가 (RiskMap, EnforcementPlan) +│ ├── patrol/ # 순찰 (PatrolRoute, FleetOptimization) +│ ├── enforcement/ # 단속 (EnforcementHistory, EventList) +│ ├── statistics/ # 통계 (Statistics, ExternalService, ReportManagement) +│ ├── ai-operations/ # AI 운영 (AIModelManagement, MLOps, AIAssistant) +│ ├── field-ops/ # 현장 대응 (MobileService, ShipAgent, AIAlert) +│ ├── admin/ # 관리 (AccessControl, SystemConfig, Notice, DataHub, AdminPanel) +│ ├── vessel/ # 선박 (VesselDetail, TransferDetection) +│ └── auth/ # 인증 (LoginPage) +│ +├── app/ # 애플리케이션 셸 +│ ├── App.tsx # BrowserRouter + Routes (26 보호 경로 + login) +│ ├── auth/AuthContext.tsx # 인증 컨텍스트 (ProtectedRoute) +│ └── layout/MainLayout.tsx # 사이드바 + 콘텐츠 + i18n 메뉴 +│ +└── styles/ # 전역 스타일 + ├── theme.css # CSS 커스텀 속성 (dark 기본 + .light 오버라이드) + ├── tailwind.css # Tailwind 기본 + └── fonts.css # 웹폰트 +``` + +--- + +## Path Alias + +| Alias | 경로 | 용도 | +|-------|------|------| +| `@/*` | `src/*` | 프로젝트 전체 절대 임포트 | +| `@lib/*` | `src/lib/*` | 공통 라이브러리 (charts, map, i18n, theme) | +| `@shared/*` | `src/shared/*` | 공유 UI 컴포넌트 | +| `@features/*` | `src/features/*` | 도메인 feature 모듈 | +| `@data/*` | `src/data/*` | 기준정보 + mock 데이터 | +| `@stores/*` | `src/stores/*` | Zustand 스토어 | + +Vite `resolve.alias`와 TypeScript `compilerOptions.paths`에 동일하게 설정되어 있다. + +--- + +## 의존성 + +### 프로덕션 (11개) + +| 패키지 | 버전 | 용도 | +|--------|------|------| +| react | ^19.2.4 | UI 렌더링 | +| react-dom | ^19.2.4 | DOM 렌더링 | +| react-router-dom | ^7.12.0 | SPA 라우팅 | +| maplibre-gl | ^5.22.0 | 래스터 베이스맵 엔진 | +| deck.gl | ^9.2.11 | GPU 벡터 렌더링 (ScatterplotLayer, PathLayer 등) | +| @deck.gl/mapbox | ^9.2.11 | MapboxOverlay (MapLibre 인터리브) | +| echarts | ^6.0.0 | 차트 라이브러리 | +| zustand | ^5.0.12 | 경량 상태관리 (8개 스토어) | +| class-variance-authority | ^0.7.1 | Tailwind 변형 관리 (CVA) | +| react-i18next | ^17.0.2 | React 다국어 바인딩 | +| i18next | ^26.0.3 | 다국어 코어 | +| lucide-react | 0.487.0 | SVG 아이콘 | + +### 개발 (13개) + +| 패키지 | 버전 | 용도 | +|--------|------|------| +| vite | ^8.0.3 | 빌드/개발 서버 (Rolldown) | +| @vitejs/plugin-react | ^6.0.1 | React Fast Refresh | +| tailwindcss | ^4.2.2 | 유틸리티 CSS | +| @tailwindcss/vite | ^4.2.2 | Tailwind Vite 플러그인 | +| typescript | 5.9 | 타입 체크 | +| eslint | ^10.2.0 | 린트 (Flat Config) | +| @eslint/js | ^10.0.1 | ESLint JS 설정 | +| typescript-eslint | ^8.58.0 | TS ESLint 파서/규칙 | +| eslint-plugin-react-hooks | ^7.0.1 | Hooks 규칙 | +| eslint-plugin-react-refresh | ^0.5.2 | Fast Refresh 규칙 | +| globals | ^17.4.0 | 전역 변수 정의 | +| @types/react | ^19.2.14 | React 타입 | +| @types/react-dom | ^19.2.3 | ReactDOM 타입 | + +--- + +## 공통 모듈 API 요약 + +### lib/map — BaseMap + deck.gl 최적화 + +**BaseMap** — MapLibre + deck.gl 통합 컴포넌트 (`forwardRef`, `memo`) + +```typescript +interface BaseMapProps { + center?: [number, number]; // [lat, lng] 기본값 [35.5, 127.0] + zoom?: number; // 기본값 7 + className?: string; + style?: React.CSSProperties; + height?: number | string; // 기본값 '100%' + layers?: Layer[]; // @deprecated — useMapLayers hook 사용 권장 + onMapReady?: (map: Map) => void; // 지도 로드 완료 콜백 + onClick?: (info: unknown) => void; + interactive?: boolean; // 기본 true +} + +// ref를 통해 MapHandle 노출 +interface MapHandle { + overlay: MapboxOverlay | null; // deck.gl overlay 직접 접근 +} +``` + +- `useImperativeHandle`로 overlay 외부 노출 → hook에서 직접 `setProps()` 호출 +- CartoDB Dark/Light 래스터 타일 자동 전환 (`settingsStore.theme` 구독) +- `interactive=false`로 정적 지도 프리뷰 생성 가능 + +**useMapLayers** — RAF 배치 레이어 업데이트 (React 리렌더 0회) + +```typescript +useMapLayers( + handleRef: RefObject, // BaseMap ref + buildLayers: () => Layer[], // 레이어 빌드 함수 + deps: unknown[], // 변경 감지 (shallow 비교) +) +``` + +**useStoreLayerSync** — Zustand store.subscribe + RAF 기반 레이어 동기화 + +```typescript +useStoreLayerSync( + handleRef: RefObject, + subscribe: (cb: (state: T) => void) => () => void, + buildLayers: (state: T) => Layer[], +) +``` + +**STATIC_LAYERS** — EEZ + NLL 싱글턴 (모듈 로드 시 1회 생성, GPU 재전송 없음) + +**createMarkerLayer** — ScatterplotLayer 생성 (transitions 보간 + DataFilterExtension) + +### lib/charts — ECharts 래퍼 + +**BaseChart** — ECharts 코어 래퍼 컴포넌트 + +```typescript +interface BaseChartProps { + option: EChartsOption; + className?: string; + style?: React.CSSProperties; + height?: number; // 기본값 200 + notMerge?: boolean; + onEvents?: Record void>; +} +``` + +- `echarts.init(container, 'kcg-dark')` 프로젝트 다크 테마 자동 적용 +- `ResizeObserver` 자동 리사이즈, unmount 시 `dispose` 자동 정리 + +**프리셋 차트** + +| 컴포넌트 | 주요 Props | 설명 | +|----------|-----------|------| +| `AreaChart` | `data`, `xKey`, `series`, `yAxisDomain?` | smooth line + 반투명 영역 | +| `BarChart` | `data`, `xKey`, `series`, `horizontal?`, `itemColors?` | 수직/수평 막대, 항목별 색상 | +| `PieChart` | `data: {name, value, color?}[]`, `innerRadius?`, `outerRadius?` | 파이/도넛 (기본 도넛) | +| `LineChart` | `data`, `xKey`, `series` | smooth 라인, 원형 심볼 | + +### lib/theme — CVA 변형 + 디자인 토큰 + +| 모듈 | 내용 | +|------|------| +| `variants.ts` | `cardVariants` (default/elevated/inner/transparent), `badgeVariants` (8 intent x 4 size), `statusDotVariants` (4 status x 3 size) | +| `tokens.ts` | `cssVars` (CSS 변수 참조), `resolvedColors` (ECharts/MapLibre용 하드코딩 값) | +| `colors.ts` | `riskColors` (5단계), `alertStyles`, `vesselColors`, `statusColors`, `chartSeriesColors` (8색 팔레트) | + +### lib/i18n — 10 네임스페이스 다국어 + +- `i18next` + `react-i18next` 기반 +- 기본 언어: `ko`, 폴백: `ko`, 지원: `ko` / `en` +- 10 네임스페이스: `common`, `dashboard`, `detection`, `patrol`, `enforcement`, `statistics`, `ai`, `fieldOps`, `admin`, `auth` +- 사용: `useTranslation('namespace')` → `t('key')` +- 언어 전환: `settingsStore.toggleLanguage()` + `localStorage` 동기화 + +--- + +## 렌더링 최적화 아키텍처 + +``` +store 변경 → useStoreLayerSync → RAF → overlay.setProps() (React 리렌더 0회) +deps 변경 → useMapLayers → RAF → overlay.setProps() (React 리렌더 0회) +정적 레이어 → STATIC_LAYERS 싱글턴 (GPU 1회 업로드, 모든 페이지 공유) +동적 레이어 → transitions 보간 + DataFilterExtension (GPU 필터링) +시계/타이머 → useRef + DOM textContent 직접 조작 (setState 0회) +``` + +핵심 원칙: **React render cycle 완전 우회**. deck.gl overlay에 직접 `setProps()`를 호출하여 40만척+ 실시간 렌더링 시에도 React 리렌더가 발생하지 않는다. + +--- + +## 테마 시스템 + +- **기본값**: `:root` = dark 테마, `.light` 클래스 오버라이드 +- **시맨틱 CSS 변수**: `surface-raised`, `surface-overlay`, `text-heading`, `text-label`, `text-hint`, `border` +- **Tailwind 통합**: `@theme inline`으로 CSS 변수를 Tailwind 유틸리티에 매핑 +- **settingsStore**: `theme` / `language` + `localStorage` 자동 동기화 +- **지도 타일**: CartoDB Dark Matter ↔ CartoDB Positron 자동 전환 (`settingsStore.theme` 구독) +- **CVA 변형**: `cardVariants`, `badgeVariants`, `statusDotVariants`가 CSS 변수 참조 → 테마 자동 반응 + +--- + +## 라우팅 구조 (26 보호 경로 + login) + +`App.tsx`에서 `BrowserRouter` > `AuthProvider` > `Routes`로 구성된다. + +- `/login` — 비보호 라우트 (LoginPage) +- `/` — `ProtectedRoute` > `MainLayout` (사이드바 + Outlet) + - `/` → `/dashboard` 리다이렉트 + - `/dashboard` — 종합 대시보드 (SFR-12) + - `/monitoring` — 실시간 모니터링 + - `/risk-map` — 위험도 평가 (SFR-05) + - `/enforcement-plan` — 단속계획 (SFR-06) + - `/dark-vessel` — 무등화 선박 탐지 (SFR-09) + - `/gear-detection` — 어구 탐지 (SFR-10) + - `/china-fishing` — 중국어선 탐지 + - `/patrol-route` — 순찰경로 (SFR-07) + - `/fleet-optimization` — 함대 최적화 (SFR-08) + - `/enforcement-history` — 단속 이력 (SFR-11) + - `/event-list` — 이벤트 목록 + - `/mobile-service` — 현장 모바일 (SFR-15) + - `/ship-agent` — 함정 에이전트 (SFR-16) + - `/ai-alert` — AI 경보 (SFR-17) + - `/statistics` — 통계 (SFR-13) + - `/external-service` — 외부연계 (SFR-14) + - `/reports` — 보고서 관리 + - `/ai-model` — AI 모델 관리 (SFR-04) + - `/mlops` — MLOps (SFR-18~19) + - `/ai-assistant` — AI 어시스턴트 (SFR-20) + - `/data-hub` — 데이터허브 (SFR-03) + - `/system-config` — 환경설정 (SFR-02) + - `/notices` — 공지사항 + - `/access-control` — 접근 권한 (SFR-01) + - `/admin` — 시스템 관리 + - `/events` — 감시 (LiveMapView) + - `/map-control` — 지도 컨트롤 + - `/vessel/:id` — 선박 상세 + +인증은 `AuthContext`의 `useAuth().user` 존재 여부로 판단하며, 미인증 시 `/login`으로 리다이렉트한다. + +--- + +## 빌드 설정 + +- **TypeScript**: `target: ES2020`, `module: ESNext`, `moduleResolution: bundler`, `strict: true` +- **Vite**: `react()` + `tailwindcss()` 플러그인, 6개 path alias (`@`, `@lib`, `@shared`, `@features`, `@data`, `@stores`) +- **ESLint 10 Flat Config**: `typescript-eslint` + `react-hooks` + `react-refresh` 규칙 +- **빌드 속도**: Rolldown 기반 ~480ms + +--- + +## 현재 아키텍처 특성 + +1. **Zustand 8개 스토어**: vessel, patrol, event, kpi, transfer, gear, enforcement, settings. `settingsStore`는 theme/language + localStorage 동기화 담당. +2. **deck.gl GPU 렌더링**: MapLibre(래스터 베이스맵) + deck.gl(벡터). React 리렌더 완전 분리, RAF 기반 `overlay.setProps()` 직접 호출. +3. **CVA 스타일 시스템**: `cardVariants`, `badgeVariants`, `statusDotVariants`로 Tailwind 패턴 통합. CSS 변수 기반 테마 반응. +4. **mock 기반 서비스 계층**: 7개 API 서비스가 `data/mock/` 모듈에서 데이터 반환. 향후 Axios + 실제 API로 교체 예정. +5. **i18n 10 NS 구조 완성**: 리소스 파일 완비, MainLayout 메뉴 + 페이지 제목 + LoginPage 적용 완료. 페이지 내부 텍스트는 대부분 한국어 하드코딩 잔존. +6. **Dark/Light 테마**: CSS 변수 기반 양방향 테마. 지도 타일 자동 전환. 일부 alert 색상(`red-500/20` 등) 하드코딩 잔존. +7. **단일 번들**: 코드 스플리팅 미적용 (~3.2MB), React.lazy 미사용. 모든 feature가 단일 번들로 빌드. +8. **WebSocket 미구현**: `connectWs` 스텁만 존재, STOMP.js + SockJS 미설치. diff --git a/docs/data-sharing-analysis.md b/docs/data-sharing-analysis.md new file mode 100644 index 0000000..a6179a3 --- /dev/null +++ b/docs/data-sharing-analysis.md @@ -0,0 +1,252 @@ +# Mock 데이터 공유 현황 분석 및 통합 결과 + +> 최초 작성일: 2026-04-06 +> 마지막 업데이트: 2026-04-06 +> 대상: `kcg-ai-monitoring` 프론트엔드 코드베이스 전체 (31개 페이지) +> 상태: **통합 완료** + +--- + +## 1. 선박 데이터 교차참조 + +현재 동일한 선박 데이터가 여러 컴포넌트에 독립적으로 하드코딩되어 있다. 각 파일마다 동일 선박의 속성(위험도, 위치, 상태 등)이 서로 다른 형식과 값으로 중복 정의되어 있어 데이터 일관성 문제가 발생한다. + +| 선박명 | 등장 파일 수 | 파일 목록 | +|---|---|---| +| 鲁荣渔56555 | 7+ | Dashboard, MobileService, LiveMapView, MonitoringDashboard, EventList, EnforcementHistory, ChinaFishing | +| 浙甬渔60651 | 4 | Dashboard, LiveMapView, EventList, DarkVesselDetection | +| 冀黄港渔05001 | 6 | MobileService, LiveMapView, Dashboard, TransferDetection, EventList, GearDetection | +| 3001함 | 6+ | ShipAgent, MobileService, LiveMapView, Dashboard, PatrolRoute, FleetOptimization | +| 3009함 | 6+ | ShipAgent, MobileService, Dashboard, PatrolRoute, FleetOptimization, AIAlert | +| 미상선박-A | 5 | MobileService, Dashboard, LiveMapView, MonitoringDashboard, EventList | + +### 문제점 +- 하나의 선박이 평균 5~7개 파일에 중복 정의됨 +- 선박 속성(이름, MMSI, 위치, 위험도, 상태)이 파일마다 미세하게 다를 수 있음 +- 새 선박 추가/수정 시 모든 관련 파일을 일일이 찾아 수정해야 함 + +--- + +## 2. 위험도 스케일 불일치 + +동일한 선박의 위험도가 페이지마다 서로 다른 스케일로 표현되고 있다. + +| 선박명 | Dashboard (risk) | DarkVesselDetection (risk) | MonitoringDashboard | +|---|---|---|---| +| 鲁荣渔56555 | **0.96** (0~1 스케일) | - | **CRITICAL** (레벨 문자열) | +| 浙甬渔60651 | **0.85** (0~1 스케일) | **94** (0~100 정수) | - | +| 미상선박-A | **0.94** (0~1 스케일) | **96** (0~100 정수) | - | + +### 원인 분석 +- Dashboard는 `risk: 0.96` 형식 (0~1 소수) +- DarkVesselDetection은 `risk: 96` 형식 (0~100 정수) +- MonitoringDashboard는 `'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'` 레벨 문자열 +- LiveMapView는 `risk: 0.94` 형식 (0~1 소수) +- EventList는 레벨 문자열 (`AlertLevel`) + +### 통합 방안 +위험도를 **0~100 정수** 스케일로 통일하되, 레벨 문자열은 구간별 자동 매핑 유틸로 변환한다. + +``` +0~30: LOW | 31~60: MEDIUM | 61~85: HIGH | 86~100: CRITICAL +``` + +--- + +## 3. KPI 수치 중복 + +Dashboard와 MonitoringDashboard가 **완전히 동일한 KPI 수치**를 독립적으로 정의하고 있다. + +| 지표 | Dashboard `KPI_DATA` | MonitoringDashboard `KPI` | +|---|---|---| +| 실시간 탐지 | 47 | 47 | +| EEZ 침범 | 18 | 18 | +| 다크베셀 | 12 | 12 | +| 불법환적 의심 | 8 | 8 | +| 추적 중 | 15 | 15 | +| 나포/검문(금일 단속) | 3 | 3 | + +### 문제점 +- 6개 KPI 수치가 두 파일에 100% 동일하게 하드코딩 +- 수치 변경 시 양쪽 모두 수정해야 함 +- Dashboard에는 `prev` 필드(전일 비교)가 추가로 있으나, Monitoring에는 없음 + +--- + +## 4. 이벤트 타임라인 중복 + +08:47~06:12 시계열 이벤트가 최소 4개 파일에 각각 정의되어 있다. + +| 시각 | Dashboard | Monitoring | MobileService | EventList | +|---|---|---|---|---| +| 08:47 | EEZ 침범 (鲁荣渔56555) | EEZ 침범 (鲁荣渔56555 외 2척) | [긴급] EEZ 침범 탐지 | EVT-0001 EEZ 침범 | +| 08:32 | 다크베셀 출현 | 다크베셀 출현 | 다크베셀 출현 | EVT-0002 다크베셀 | +| 08:15 | 선단 밀집 경보 | 선단 밀집 경보 | - | EVT-0003 선단밀집 | +| 07:58 | 불법환적 의심 | 불법환적 의심 | 환적 의심 | EVT-0004 불법환적 | +| 07:41 | MMSI 변조 탐지 | MMSI 변조 탐지 | - | EVT-0005 MMSI 변조 | +| 07:23 | 함정 검문 완료 | 함정 검문 완료 | - | EVT-0006 검문 완료 | +| 06:12 | 속력 이상 탐지 | - | - | EVT-0010 속력 이상 | + +### 문제점 +- 동일 이벤트의 description이 파일마다 미세하게 다름 (예: "鲁荣渔56555" vs "鲁荣渔56555 외 2척") +- EventList에는 ID가 있으나(EVT-xxxx), 다른 파일에는 없음 +- Dashboard에는 10개, Monitoring에는 6개, EventList에는 15개로 **건수도 불일치** + +--- + +## 5. 환적 데이터 100% 중복 + +`TransferDetection.tsx`와 `ChinaFishing.tsx`에 **TR-001~TR-003 환적 데이터가 완전히 동일**하게 정의되어 있다. + +``` +TransferDetection.tsx: + const transferData = [ + { id: 'TR-001', time: '2026-01-20 13:42:11', a: {name:'장저우8호'}, b: {name:'黑江9호'}, ... }, + { id: 'TR-002', time: '2026-01-20 11:15:33', ... }, + { id: 'TR-003', time: '2026-01-20 09:23:45', ... }, + ]; + +ChinaFishing.tsx: + const TRANSFER_DATA = [ + { id: 'TR-001', time: '2026-01-20 13:42:11', a: {name:'장저우8호'}, b: {name:'黑江9호'}, ... }, + { id: 'TR-002', time: '2026-01-20 11:15:33', ... }, + { id: 'TR-003', time: '2026-01-20 09:23:45', ... }, + ]; +``` + +### 문제점 +- 변수명만 다르고 (`transferData` vs `TRANSFER_DATA`) 데이터 구조와 값이 100% 동일 +- 한쪽만 수정하면 다른 쪽과 불일치 발생 + +--- + +## 6. 함정 상태 불일치 + +동일 함정의 상태가 페이지마다 모순되는 경우가 확인되었다. + +| 함정 | ShipAgent | Dashboard | PatrolRoute | FleetOptimization | +|---|---|---|---|---| +| 5001함 | **오프라인** (`status: '오프라인'`) | **가용** (PATROL_SHIPS에 대기로 표시) | **가용** (`status: '가용'`) | **가용** (`status: '가용'`) | +| 3009함 | **온라인** (동기화 중) | **검문 중** | **출동중** | **출동중** | +| 1503함 | **미배포** | - | - | **정비중** | + +### 문제점 +- 5001함이 ShipAgent에서는 오프라인이지만, Dashboard/PatrolRoute/FleetOptimization에서는 가용으로 표시됨 -- **직접적 모순** +- 3009함의 상태가 "온라인", "검문 중", "출동중"으로 파일마다 다름 +- 실제 운영 시 혼란을 초래할 수 있는 시나리오 불일치 + +--- + +## 7. 현재 상태: 통합 완료 + +아래 분석에서 식별한 모든 중복/불일치 문제를 해소하기 위해, 7개 공유 Mock 모듈 + 7개 Zustand 스토어 체계로 통합이 **완료**되었다. + +### 7.1 완료된 아키텍처: mock -> store -> page + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ src/data/mock/ (7개 공유 모듈) │ +├───────────┬──────────┬──────────┬────────┬───────────┬────────┬────────┤ +│ vessels │ patrols │ events │ kpi │ transfers │ gear │enforce-│ +│ .ts │ .ts │ .ts │ .ts │ .ts │ .ts │ment.ts │ +└─────┬─────┴─────┬────┴─────┬────┴───┬────┴─────┬────┴───┬────┴───┬────┘ + │ │ │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ src/stores/ (7개 Zustand 스토어 + settingsStore) │ +├───────────┬──────────┬──────────┬────────┬───────────┬────────┬────────┤ +│ vessel │ patrol │ event │ kpi │ transfer │ gear │enforce-│ +│ Store │ Store │ Store │ Store │ Store │ Store │mentStr │ +└─────┬─────┴─────┬────┴─────┬────┴───┬────┴─────┬────┴───┬────┴───┬────┘ + │ │ │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ src/features/*/ (페이지 컴포넌트) │ +│ store.load() 호출 -> store에서 데이터 구독 -> 뷰 변환은 페이지 책임 │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 7.2 스토어별 소비 현황 (16개 페이지가 스토어 사용) + +| 스토어 | 소비 페이지 | +|---|---| +| `useVesselStore` | Dashboard, LiveMapView, DarkVesselDetection, VesselDetail | +| `usePatrolStore` | Dashboard, PatrolRoute, FleetOptimization | +| `useEventStore` | Dashboard, MonitoringDashboard, LiveMapView, EventList, MobileService, AIAlert | +| `useKpiStore` | Dashboard, MonitoringDashboard, Statistics | +| `useTransferStore` | TransferDetection, ChinaFishing | +| `useGearStore` | GearDetection | +| `useEnforcementStore` | EnforcementPlan, EnforcementHistory | + +### 7.3 페이지 전용 인라인 데이터 (미통합) + +아래 페이지들은 도메인 특성상 공유 mock에 포함하지 않고 페이지 전용 인라인 데이터를 유지한다. + +| 페이지 | 인라인 데이터 | 사유 | +|---|---|---| +| ChinaFishing | `COUNTERS_ROW1/2`, `VESSEL_LIST`, `MONTHLY_DATA`, `VTS_ITEMS` | 중국어선 전용 센서 카운터/통계 (다른 페이지에서 미사용) | +| VesselDetail | `VESSELS: VesselTrack[]` | 항적 데이터 구조가 `VesselData`와 다름 (주석으로 명시) | +| MLOpsPage | 실험/배포 데이터 | MLOps 전용 도메인 데이터 | +| MapControl | 훈련구역 데이터 | 해상사격 훈련구역 전용 | +| DataHub | 수신현황 데이터 | 데이터 허브 전용 모니터링 | +| AIModelManagement | 모델/규칙 데이터 | AI 모델 관리 전용 | +| AIAssistant | `SAMPLE_CONVERSATIONS` | 챗봇 샘플 대화 | +| LoginPage | `DEMO_ACCOUNTS` | 데모 인증 정보 | +| 기타 (AdminPanel, SystemConfig 등) | 각 페이지 전용 설정/관리 데이터 | 관리 도메인 특화 | + +### 7.4 설계 원칙 (구현 완료) + +1. **위험도 0~100 통일**: 모든 선박의 위험도를 0~100 정수로 통일. 레벨 문자열은 유틸 함수로 변환. +2. **단일 원천(Single Source of Truth)**: 각 데이터는 하나의 mock 모듈에서만 정의하고, 스토어를 통해 접근. +3. **Lazy Loading**: 스토어의 `load()` 메서드가 최초 호출 시 `import()`로 mock 데이터를 동적 로딩 (loaded 플래그로 중복 방지). +4. **뷰 변환은 페이지 책임**: mock 모듈/스토어는 원본 데이터만 제공하고, 화면별 가공(필터, 정렬, 포맷)은 각 페이지에서 수행. + +### 7.5 Mock 모듈 상세 (참고용) + +참고: 초기 분석에서 계획했던 `areas.ts`는 최종 구현 시 `enforcement.ts`(단속 이력 데이터)로 대체되었다. +해역/구역 데이터는 RiskMap, MapControl 등 각 페이지에서 전용 데이터로 관리한다. + +| # | 모듈 파일 | 스토어 | 내용 | +|---|---|---|---| +| 1 | `data/mock/vessels.ts` | `vesselStore` | 중국어선 + 한국어선 + 미상선박 마스터 (`MOCK_VESSELS`, `MOCK_SUSPECTS`) | +| 2 | `data/mock/patrols.ts` | `patrolStore` | 경비함정 마스터 + 경로/시나리오/커버리지 | +| 3 | `data/mock/events.ts` | `eventStore` | 이벤트 타임라인 + 알림 데이터 | +| 4 | `data/mock/kpi.ts` | `kpiStore` | KPI 수치 + 월별 추이 | +| 5 | `data/mock/transfers.ts` | `transferStore` | 환적 데이터 (TR-001~003) | +| 6 | `data/mock/gear.ts` | `gearStore` | 어구 데이터 (불법어구 목록) | +| 7 | `data/mock/enforcement.ts` | `enforcementStore` | 단속 이력 + 단속 계획 데이터 | + +--- + +## 8. 작업 완료 요약 + +| 모듈 | 상태 | 스토어 소비 페이지 수 | +|---|---|---| +| `vessels.ts` | **완료** | 4개 (useVesselStore) | +| `events.ts` | **완료** | 6개 (useEventStore) | +| `patrols.ts` | **완료** | 3개 (usePatrolStore) | +| `kpi.ts` | **완료** | 3개 (useKpiStore) | +| `transfers.ts` | **완료** | 2개 (useTransferStore) | +| `gear.ts` | **완료** | 1개 (useGearStore) | +| `enforcement.ts` | **완료** | 2개 (useEnforcementStore) | + +### 실제 작업 결과 +- Mock 모듈 생성: 7개 파일 (`src/data/mock/`) +- Zustand 스토어 생성: 7개 + 1개 설정용 (`src/stores/`) +- 기존 페이지 리팩토링: 16개 페이지에서 스토어 소비로 전환 +- 나머지 15개 페이지: 도메인 특화 인라인 데이터 유지 (공유 필요성 없음) + +--- + +## 9. 결론 + +위 1~6절에서 분석한 6개의 심각한 중복/불일치 문제(위험도 스케일, 함정 상태 모순, KPI 중복, 이벤트 불일치, 환적 100% 중복, 선박 교차참조)는 **7개 공유 mock 모듈 + 7개 Zustand 스토어** 도입으로 모두 해소되었다. + +달성한 효과: +- **데이터 일관성**: Single Source of Truth로 불일치 원천 차단 +- **유지보수성**: 데이터 변경 시 mock 모듈 1곳만 수정 +- **확장성**: 신규 페이지 추가 시 기존 store import로 즉시 사용 +- **코드 품질**: 중복 인라인 데이터 제거, 16개 페이지가 스토어 기반으로 전환 +- **성능**: Zustand lazy loading으로 최초 접근 시에만 mock 데이터 로딩 + +1~6절의 분석 내용은 통합 전 문제 식별 기록으로 보존한다. diff --git a/docs/next-refactoring.md b/docs/next-refactoring.md new file mode 100644 index 0000000..f9804fa --- /dev/null +++ b/docs/next-refactoring.md @@ -0,0 +1,194 @@ +# KCG AI Monitoring - 다음 단계 리팩토링 TODO + +> 프론트엔드 UI 스캐폴딩 + 기반 인프라(상태관리, 지도 GPU, mock 데이터, CVA) 완료 상태. 백엔드 연동 및 운영 품질 확보를 위해 남은 항목을 순차적으로 진행한다. + +--- + +## 1. ✅ 상태관리 도입 (Zustand 5.0) — COMPLETED + +`zustand` 5.0.12 설치, `src/stores/`에 8개 독립 스토어 구현 완료. + +- `vesselStore` — 선박 목록, 선택, 필터 +- `patrolStore` — 순찰 경로/함정 +- `eventStore` — 탐지/경보 이벤트 +- `kpiStore` — KPI 메트릭, 추세 +- `transferStore` — 전재(환적) +- `gearStore` — 어구 탐지 +- `enforcementStore` — 단속 이력 +- `settingsStore` — theme/language + localStorage 동기화, 지도 타일 자동 전환 + +> `AuthContext`는 유지 (인증은 Context API가 적합, 마이그레이션 불필요로 결정) + +--- + +## 2. API 서비스 계층 (Axios 1.14) — 구조 완성, 실제 연동 대기 + +### 현재 상태 +- `src/services/`에 7개 서비스 모듈 구현 (api, vessel, event, patrol, kpi, ws, index) +- `api.ts`: fetch 래퍼 (`apiGet`, `apiPost`) — 향후 Axios 교체 예정 +- 각 서비스가 `data/mock/` 모듈에서 mock 데이터 반환 (실제 HTTP 호출 0건) +- `ws.ts`: STOMP WebSocket 스텁 존재, 미구현 + +### 남은 작업 +- [ ] `axios` 1.14 설치 → `api.ts`의 fetch 래퍼를 Axios 인스턴스로 교체 +- [ ] Axios 인터셉터: + - Request: Authorization 헤더 자동 주입 + - Response: 401 → 로그인 리다이렉트, 500 → 에러 토스트 +- [ ] `@tanstack/react-query` 5.x 설치 → TanStack Query Provider 추가 +- [ ] 각 서비스의 mock 반환을 실제 API 호출로 교체 +- [ ] 로딩 스켈레톤, 에러 바운더리 공통 컴포넌트 + +--- + +## 3. 실시간 인프라 (STOMP.js + SockJS) — 스텁 구조만 존재 + +### 현재 상태 +- `services/ws.ts`에 `connectWs` 스텁 함수 존재 (인터페이스 정의 완료) +- STOMP.js, SockJS 미설치 — 실제 WebSocket 연결 없음 +- `useStoreLayerSync` hook으로 store→지도 실시간 파이프라인 준비 완료 + +### 남은 작업 +- [ ] `@stomp/stompjs` + `sockjs-client` 설치 +- [ ] `ws.ts` 스텁을 실제 STOMP 클라이언트로 구현 +- [ ] 구독 채널 설계: + - `/topic/ais-positions` — 실시간 AIS 위치 + - `/topic/alerts` — 경보/이벤트 + - `/topic/detections` — 탐지 결과 + - `/user/queue/notifications` — 개인 알림 +- [ ] 재연결 로직 (지수 백오프) +- [ ] store → `useStoreLayerSync` → 지도 마커 실시간 업데이트 연결 +- [ ] `eventStore`와 연동하여 알림 배너/뱃지 카운트 업데이트 + +--- + +## 4. ✅ 고급 지도 레이어 (deck.gl 9.2) — COMPLETED + +`deck.gl` 9.2.11 + `@deck.gl/mapbox` 설치, MapLibre + deck.gl 인터리브 아키텍처 구현 완료. + +- **BaseMap**: `forwardRef` + `memo`, `MapboxOverlay`를 `useImperativeHandle`로 외부 노출 +- **useMapLayers**: RAF 배치 레이어 업데이트, React 리렌더 0회 +- **useStoreLayerSync**: Zustand store.subscribe → RAF → overlay.setProps (React 우회) +- **STATIC_LAYERS**: EEZ + NLL PathLayer 싱글턴 (GPU 1회 업로드) +- **createMarkerLayer**: ScatterplotLayer + transitions 보간 + DataFilterExtension +- **createRadiusLayer**: 반경 원 표시용 ScatterplotLayer +- 레거시 GeoJSON 레이어(`boundaries.ts`)는 하위 호환으로 유지 + +> 성능 목표 40만척+ GPU 렌더링 달성. TripsLayer/HexagonLayer/IconLayer는 실데이터 확보 후 추가 예정. + +--- + +## 5. ✅ 더미 데이터 통합 — COMPLETED + +`src/data/mock/`에 7개 공유 mock 모듈 구현 완료. TypeScript 인터페이스 정의 포함. + +``` +data/mock/ +├── vessels.ts # VesselData — 선박 목록 (한국, 중국, 경비함) +├── events.ts # EventRecord, AlertRecord — 탐지/단속 이벤트 +├── transfers.ts # 전재(환적) 데이터 +├── patrols.ts # PatrolShip — 순찰 경로/함정 +├── gear.ts # 어구 탐지 데이터 +├── kpi.ts # KpiMetric, MonthlyTrend, ViolationType +└── enforcement.ts # 단속 이력 데이터 +``` + +- `services/` 계층이 mock 모듈을 import하여 반환 → 향후 API 교체 시 서비스만 수정 +- 인터페이스가 API 응답 타입 계약 역할 수행 + +--- + +## 6. i18n 실적용 — 구조 완성, 내부 텍스트 미적용 + +### 현재 상태 +- 10 네임스페이스 리소스 완비: common, dashboard, detection, patrol, enforcement, statistics, ai, fieldOps, admin, auth +- ko/en 각 10파일 (총 20 JSON) +- `settingsStore.toggleLanguage()` + `localStorage` 동기화 구현 완료 +- **적용 완료**: MainLayout 사이드바 메뉴명, 24개 페이지 제목, LoginPage +- **미적용**: 각 페이지 내부 텍스트 (카드 레이블, 테이블 헤더, 상태 텍스트 등) — 대부분 한국어 하드코딩 잔존 + +### 남은 작업 +- [ ] 각 feature 페이지 내부 텍스트를 `useTranslation('namespace')` + `t()` 로 교체 +- [ ] 날짜/숫자 포맷 로컬라이즈 (`Intl.DateTimeFormat`, `Intl.NumberFormat`) +- [ ] 누락 키 감지 자동화 (i18next missing key handler 또는 lint 규칙) + +--- + +## 7. ✅ Tailwind 공통 스타일 모듈화 (CVA) — COMPLETED + +`class-variance-authority` 0.7.1 설치, `src/lib/theme/variants.ts`에 3개 CVA 변형 구현 완료. + +- **cardVariants**: default / elevated / inner / transparent — CSS 변수 기반 테마 반응 +- **badgeVariants**: 8 intent (critical~cyan) x 4 size (xs~lg) — 150회+ 반복 패턴 통합 +- **statusDotVariants**: 4 status (online/warning/danger/offline) x 3 size (sm/md/lg) +- `shared/components/ui/card.tsx`, `badge.tsx`에 CVA 적용 완료 +- CSS 변수(`surface-raised`, `surface-overlay`, `border`) 참조로 Dark/Light 자동 반응 + +--- + +## 8. 코드 스플리팅 — 미착수 + +### 현재 상태 +- **단일 번들 ~3.2MB** (모든 feature + deck.gl + MapLibre + ECharts 포함) +- `React.lazy` 미적용, 모든 31개 페이지가 동기 import +- 초기 로딩 시 사용하지 않는 페이지 코드까지 전부 다운로드 + +### 필요한 이유 +- 초기 로딩 성능 개선 (FCP, LCP) +- 현장 모바일 환경 (LTE/3G)에서의 사용성 확보 +- 번들 캐싱 효율 향상 (변경된 chunk만 재다운로드) + +### 구현 계획 +- [ ] `React.lazy` + `Suspense`로 feature 단위 동적 임포트: + ```typescript + const Dashboard = lazy(() => import('@features/dashboard/Dashboard')); + const RiskMap = lazy(() => import('@features/risk-assessment/RiskMap')); + ``` +- [ ] `App.tsx` 라우트 전체를 lazy 컴포넌트로 교체 +- [ ] 로딩 폴백 컴포넌트 (스켈레톤 또는 스피너) 공통화 +- [ ] Vite `build.rollupOptions.output.manualChunks` 설정: + ```typescript + manualChunks: { + 'vendor-react': ['react', 'react-dom', 'react-router-dom'], + 'vendor-map': ['maplibre-gl', 'deck.gl', '@deck.gl/mapbox'], + 'vendor-chart': ['echarts'], + } + ``` +- [ ] 목표: 초기 번들 < 300KB (gzip), 각 feature chunk < 100KB +- [ ] `vite-plugin-compression`으로 gzip/brotli 사전 압축 검토 + +--- + +## 9. Light 테마 하드코딩 정리 + +### 현재 상태 +- Dark/Light 테마 전환 구조 완성 (CSS 변수 + `.light` 클래스 + settingsStore) +- 시맨틱 변수(`surface-raised`, `text-heading` 등) + CVA 변형은 정상 작동 +- **문제**: 일부 alert/status 색상이 Tailwind 하드코딩 (`bg-red-500/20`, `text-red-400`, `border-red-500/30` 등) + - Dark에서는 자연스러우나, Light 전환 시 대비/가독성 부족 + +### 구현 계획 +- [ ] 하드코딩 alert 색상을 CSS 변수 또는 CVA intent로 교체 +- [ ] `badgeVariants`의 intent 색상도 CSS 변수 기반으로 전환 검토 +- [ ] Light 모드 전용 대비 테스트 (WCAG AA 기준) + +--- + +## 우선순위 및 의존관계 + +``` +✅ 완료 ───────────────────────────────────── +[1. Zustand] [4. deck.gl] [5. mock 데이터] [7. CVA] + +진행 중 / 남은 작업 ────────────────────────── +[6. i18n 내부 텍스트] ──┐ + ├──▶ [2. API 실제 연동] ──▶ [3. 실시간 STOMP] +[9. Light 테마 정리] ───┘ + +[8. 코드 스플리팅] ← 독립 작업, 언제든 착수 가능 (~3.2MB → 목표 <300KB) +``` + +### 권장 진행 순서 + +1. **Phase A (품질)**: i18n 내부 텍스트 적용 (6) + Light 테마 하드코딩 정리 (9) + 코드 스플리팅 (8) +2. **Phase B (연동)**: Axios 설치 + API 실제 연동 (2) +3. **Phase C (실시간)**: STOMP.js + SockJS 실시간 인프라 (3) diff --git a/docs/page-workflow.md b/docs/page-workflow.md new file mode 100644 index 0000000..349e572 --- /dev/null +++ b/docs/page-workflow.md @@ -0,0 +1,436 @@ +# 페이지 역할표 및 업무 파이프라인 + +> 최초 작성일: 2026-04-06 +> 마지막 업데이트: 2026-04-06 +> 대상: `kcg-ai-monitoring` 프론트엔드 31개 페이지 + +--- + +## 0. 공통 아키텍처 + +### 디렉토리 구조 + +모든 페이지는 `src/features/` 아래 도메인별 디렉토리에 배치되어 있다. + +``` +src/features/ + admin/ AccessControl, AdminPanel, DataHub, NoticeManagement, SystemConfig + ai-operations/ AIAssistant, AIModelManagement, MLOpsPage + auth/ LoginPage + dashboard/ Dashboard + detection/ ChinaFishing, DarkVesselDetection, GearDetection, GearIdentification + enforcement/ EnforcementHistory, EventList + field-ops/ AIAlert, MobileService, ShipAgent + monitoring/ MonitoringDashboard + patrol/ FleetOptimization, PatrolRoute + risk-assessment/ EnforcementPlan, RiskMap + statistics/ ExternalService, ReportManagement, Statistics + surveillance/ LiveMapView, MapControl + vessel/ TransferDetection, VesselDetail +``` + +### 데이터 흐름 + +모든 공유 데이터는 **mock -> store -> page** 패턴으로 흐른다. + +``` +src/data/mock/*.ts --> src/stores/*Store.ts --> src/features/*/*.tsx + (7개 공유 모듈) (7개 Zustand 스토어) (16개 페이지가 스토어 소비) +``` + +- 스토어는 `load()` 호출 시 `import()`로 mock 데이터를 lazy loading +- 도메인 특화 데이터는 페이지 내 인라인으로 유지 (MLOps, MapControl, DataHub 등) +- 상세 매핑은 `docs/data-sharing-analysis.md` 참조 + +### 지도 렌더링 + +지도가 필요한 11개 페이지는 공통 `src/lib/map/` 인프라를 사용한다. + +- **deck.gl** 기반 렌더링 (`BaseMap.tsx`) +- **`useMapLayers`** 훅: 페이지별 동적 레이어 구성 +- **`STATIC_LAYERS`**: EEZ/KDLZ 등 정적 레이어를 상수로 분리하여 zero rerender 보장 +- 사용 페이지: Dashboard, LiveMapView, MapControl, EnforcementPlan, PatrolRoute, FleetOptimization, GearDetection, DarkVesselDetection, RiskMap, VesselDetail, MobileService + +### 다국어 (i18n) + +- `react-i18next` 기반, 24개 페이지 + MainLayout + LoginPage에 i18n 적용 +- 지원 언어: 한국어 (ko), 영어 (en) +- 페이지 타이틀, 주요 UI 라벨이 번역 키로 관리됨 + +### 테마 + +- `settingsStore`에서 dark/light 테마 전환 지원 +- 기본값: dark (해양 감시 시스템 특성상) +- `localStorage`에 선택 유지, CSS 클래스 토글 방식 + +--- + +## 1. 31개 페이지 역할표 + +### 1.1 인증/관리 (4개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-01 | LoginPage | `/login` | 전체 | SSO/GPKI/비밀번호 인증, 5회 실패 잠금 | ID/PW, 인증 방식 선택 | 세션 발급, 역할 부여 | - | 모든 페이지 (인증 게이트) | +| SFR-01 | AccessControl | `/access-control` | 관리자 | RBAC 권한 관리, 감사 로그 | 역할/사용자/권한 설정 | 권한 변경, 감사 기록 | LoginPage | 전체 시스템 접근 제어 | +| SFR-02 | SystemConfig | `/system-config` | 관리자 | 공통코드 기준정보 관리 (해역52/어종578/어업59/선박186) | 코드 검색/필터 | 코드 조회, 설정 변경 | AccessControl | 탐지/분석 엔진 기준데이터 | +| SFR-02 | NoticeManagement | `/notices` | 관리자 | 시스템 공지(배너/팝업/토스트), 역할별 대상 설정 | 공지 작성, 기간/대상 설정 | 배너/팝업 노출 | AccessControl | 모든 페이지 (NotificationBanner) | + +### 1.2 데이터 수집/연계 (1개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-03 | DataHub | `/data-hub` | 관리자 | 통합데이터 허브 — 선박신호 수신 현황 히트맵, 연계 채널 모니터링 | 수신 소스 선택 | 수신률 조회, 연계 상태 확인 | 외부 센서 (VTS, AIS, V-PASS 등) | 탐지 파이프라인 전체 | + +### 1.3 AI 모델/운영 (3개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-04 | AIModelManagement | `/ai-model` | 분석관 | 모델 레지스트리, 탐지 규칙, 피처 엔지니어링, 학습 파이프라인, 7대 탐지엔진 | 모델 버전/규칙/피처 설정 | 모델 배포, 성능 리포트 | DataHub (학습 데이터) | DarkVessel, GearDetection, TransferDetection 등 탐지 엔진 | +| SFR-18/19 | MLOpsPage | `/mlops` | 분석관/관리자 | MLOps/LLMOps 운영 대시보드 (실험, 배포, API Playground, LLM 테스트) | 실험 템플릿, HPS 설정 | 실험 결과, 모델 배포 | AIModelManagement | AIAssistant, 탐지 엔진 | +| SFR-20 | AIAssistant | `/ai-assistant` | 상황실/분석관 | 자연어 Q&A 의사결정 지원 (법령 조회, 대응 절차 안내) | 자연어 질의 | 답변 + 법령 참조 | MLOpsPage (LLM 모델) | 작전 의사결정 | + +### 1.4 탐지 (4개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-09 | DarkVesselDetection | `/dark-vessel` | 분석관 | AIS 조작/위장/Dark Vessel 패턴 탐지 (6가지 패턴), 지도+테이블 | AIS 데이터 스트림 | 의심 선박 목록, 위험도, 라벨 분류 | DataHub (AIS/레이더) | RiskMap, LiveMapView, EventList | +| SFR-10 | GearDetection | `/gear-detection` | 분석관 | 불법 어망/어구 탐지 및 관리, 허가 상태 판정 | 어구 센서/영상 | 어구 목록, 불법 판정 결과 | DataHub (센서) | RiskMap, EnforcementPlan | +| - | GearIdentification | `features/detection/` | 분석관 | 어구 국적 판별 (중국/한국/불확실), GB/T 5147 기준 | 어구 물리적 특성 입력 | 판별 결과 (국적, 신뢰도, 경보등급) | GearDetection | EnforcementHistory | +| - | ChinaFishing | `/china-fishing` | 분석관/상황실 | 중국어선 통합 감시 (센서 카운터, 특이운항, 월별 통계, 환적 탐지, VTS 연계) | 센서 데이터 융합 | 감시 현황, 환적 의심 목록 | DataHub, DarkVessel | RiskMap, EnforcementPlan | + +### 1.5 환적 탐지 (1개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| - | TransferDetection | `features/vessel/` | 분석관 | 선박 간 근접 접촉 및 환적 의심 행위 분석 (거리/시간/속도 기준) | AIS 궤적 분석 | 환적 이벤트 목록, 의심도 점수 | DataHub, DarkVessel | EventList, EnforcementPlan | + +### 1.6 위험도 평가/계획 (2개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-05 | RiskMap | `/risk-map` | 분석관/상황실 | 격자 기반 불법조업 위험도 지도 + MTIS 해양사고 통계 연계 | 탐지 결과, 사고 통계 | 히트맵, 해역별 위험도, 사고 통계 차트 | DarkVessel, GearDetection, ChinaFishing | EnforcementPlan, PatrolRoute | +| SFR-06 | EnforcementPlan | `/enforcement-plan` | 상황실 | 단속 계획 수립, 경보 연계, 우선지역 예보 | 위험도 데이터, 가용 함정 | 단속 계획 테이블, 지도 표시 | RiskMap | PatrolRoute, FleetOptimization | + +### 1.7 순찰/함대 (2개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-07 | PatrolRoute | `/patrol-route` | 상황실 | AI 단일 함정 순찰 경로 추천 (웨이포인트, 거리/시간/연료 산출) | 함정 선택, 구역 조건 | 추천 경로, 웨이포인트 목록 | EnforcementPlan, RiskMap | 함정 출동 (ShipAgent) | +| SFR-08 | FleetOptimization | `/fleet-optimization` | 상황실 | 다함정 협력형 경로 최적화 (커버리지 시뮬레이션, 승인 워크플로) | 함대 목록, 구역 조건 | 최적화 결과, 커버리지 비교 | EnforcementPlan, PatrolRoute | 함정 출동 (ShipAgent) | + +### 1.8 감시/지도 (2개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| - | LiveMapView | `/events` | 상황실 | 실시간 해역 감시 지도 (AIS 선박 + 이벤트 경보 + 아군 함정) | 실시간 AIS/이벤트 스트림 | 지도 마커, 이벤트 카드, 위험도 바 | 탐지 엔진 전체 | EventList, AIAlert | +| - | MapControl | `/map-control` | 상황실/관리자 | 해역 통제 관리 (해상사격 훈련구역도 No.462, 군/해경 구역) | 구역 데이터 | 훈련구역 지도, 상태 테이블 | 국립해양조사원 데이터 | LiveMapView (레이어) | + +### 1.9 대시보드/모니터링 (2개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| - | Dashboard | `/dashboard` | 전체 | 종합 상황판 (KPI, 타임라인, 위험선박 TOP8, 함정 현황, 해역 위험도, 시간대별 탐지 추이) | 전 시스템 데이터 집계 | 한눈에 보는 현황 | 탐지/순찰/이벤트 전체 | 각 상세 페이지로 드릴다운 | +| SFR-12 | MonitoringDashboard | `/monitoring` | 상황실 | 모니터링 및 경보 현황판 (KPI, 24시간 추이, 탐지 유형 분포, 실시간 이벤트) | 경보/탐지 데이터 | 경보 현황 대시보드 | 탐지 엔진, EventList | AIAlert, EnforcementPlan | + +### 1.10 이벤트/이력 (2개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| - | EventList | `/event-list` | 상황실/분석관 | 이벤트 전체 목록 (검색/정렬/페이징/엑셀/출력), 15건+ 이벤트 | 필터 조건 | 이벤트 테이블, 엑셀 내보내기 | 탐지 엔진, LiveMapView | EnforcementHistory, ReportManagement | +| SFR-11 | EnforcementHistory | `/enforcement-history` | 분석관 | 단속/탐지 이력 관리 (AI 매칭 검증 포함) | 검색 조건 | 이력 테이블, AI 일치 여부 | EventList, 현장 단속 | ReportManagement, Statistics | + +### 1.11 현장 대응 (3개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-15 | MobileService | `/mobile-service` | 현장 단속요원 | 모바일 앱 프리뷰 (위험도/의심선박/경로추천/경보, 푸시 설정) | 모바일 위치, 푸시 설정 | 경보 수신, 지도 조회 | AIAlert, LiveMapView | 현장 단속 수행 | +| SFR-16 | ShipAgent | `/ship-agent` | 현장 단속요원 | 함정용 Agent 관리 (배포/동기화 상태, 버전 관리) | 함정 Agent 설치 | Agent 상태 조회, 동기화 | PatrolRoute, FleetOptimization | 현장 단속 수행 | +| SFR-17 | AIAlert | `/ai-alert` | 상황실/현장 | AI 탐지 알림 자동 발송 (함정/관제요원 대상, 탐지시각/좌표/유형/신뢰도 포함) | 탐지 이벤트 트리거 | 알림 발송, 수신 확인 | MonitoringDashboard, EventList | MobileService, ShipAgent | + +### 1.12 통계/외부연계/보고 (3개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| SFR-13 | Statistics | `/statistics` | 상황실/분석관 | 통계/지표/성과 분석 (월별 추이, 위반유형, KPI 달성률) | 기간/유형 필터 | 차트, KPI 테이블, 보고서 | EnforcementHistory, EventList | 외부 보고, 전략 수립 | +| SFR-14 | ExternalService | `/external-service` | 관리자/외부 | 외부 서비스 제공 (해수부/수협/기상청 API/파일 연계, 비식별/익명화 정책) | 서비스 설정 | API 호출 수, 연계 상태 | Statistics, 탐지 결과 | 외부기관 | +| - | ReportManagement | `/reports` | 분석관/상황실 | 증거 관리 및 보고서 생성 (사건별 자동 패키징) | 사건 선택, 증거 파일 업로드 | 보고서 PDF, 증거 패키지 | EnforcementHistory, EventList | 검찰/외부기관 | + +### 1.13 선박 상세 (1개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| - | VesselDetail | `/vessel/:id` | 분석관/상황실 | 선박 상세 정보 (AIS 데이터, 항적, 입항 이력, 선원 정보, 비허가 선박 목록) | 선박 ID/MMSI | 상세 프로필, 지도 항적 | LiveMapView, DarkVessel, EventList | EnforcementPlan, ReportManagement | + +### 1.14 시스템 관리 (1개) + +| SFR | 화면명 | 경로 | 사용자 | 핵심 기능 | 입력 | 출력/액션 | 업스트림 | 다운스트림 | +|---|---|---|---|---|---|---|---|---| +| - | AdminPanel | `/admin` | 관리자 | 시스템 인프라 관리 (서버 상태, CPU/메모리/디스크 모니터링) | - | 서버 상태 대시보드 | - | 시스템 안정성 보장 | + +--- + +## 2. 업무 파이프라인 (4개) + +### 2.1 탐지 파이프라인 + +불법 조업을 탐지하고 실시간 감시하여 현장 작전까지 연결하는 핵심 파이프라인. + +``` +AIS/레이더/위성 신호 + │ + ▼ + ┌─────────┐ + │ DataHub │ ← 통합데이터 허브 (VTS, AIS, V-PASS, E-Nav 수집) + └────┬────┘ + │ + ▼ + ┌──────────────────────────────────────────────┐ + │ AI 탐지 엔진 (AIModelManagement 관리) │ + │ │ + │ DarkVesselDetection ─ AIS 조작/위장/소실 │ + │ GearDetection ─────── 불법 어구 탐지 │ + │ ChinaFishing ──────── 중국어선 통합 감시 │ + │ TransferDetection ─── 환적 행위 탐지 │ + │ GearIdentification ── 어구 국적 판별 │ + └──────────────┬───────────────────────────────┘ + │ + ▼ + ┌──────────┐ ┌───────────────────┐ + │ RiskMap │─────▶│ LiveMapView │ ← 실시간 지도 감시 + └────┬─────┘ │ MonitoringDashboard│ ← 경보 현황판 + │ └───────────────────┘ + ▼ + ┌──────────────────┐ + │ EnforcementPlan │ ← 단속 우선지역 예보 + └────────┬─────────┘ + │ + ▼ + ┌──────────────┐ ┌───────────────────┐ + │ PatrolRoute │─────▶│ FleetOptimization │ ← 다함정 최적화 + └──────┬───────┘ └─────────┬─────────┘ + │ │ + ▼ ▼ + ┌──────────┐ + │ AIAlert │ ← 함정/관제 자동 알림 발송 + └────┬─────┘ + │ + ▼ + 현장 작전 (MobileService, ShipAgent) +``` + +### 2.2 대응 파이프라인 + +AI 알림 수신 후 현장 단속, 이력 기록, 보고서 생성까지의 대응 프로세스. + +``` + ┌──────────┐ + │ AIAlert │ ← AI 탐지 알림 자동 발송 + └────┬─────┘ + │ + ▼ + ┌──────────────────────────────────┐ + │ 현장 대응 │ + │ │ + │ MobileService ── 모바일 경보 수신│ + │ ShipAgent ────── 함정 Agent 연동 │ + └──────────────┬───────────────────┘ + │ + ▼ + 현장 단속 수행 + (정선/검문/나포/퇴거) + │ + ▼ + ┌──────────────────────┐ + │ EnforcementHistory │ ← 단속 이력 등록, AI 매칭 검증 + └──────────┬───────────┘ + │ + ▼ + ┌──────────────────────┐ + │ ReportManagement │ ← 증거 패키징, 보고서 생성 + └──────────┬───────────┘ + │ + ▼ + 검찰/외부기관 (ExternalService 통해 연계) +``` + +### 2.3 분석 파이프라인 + +축적된 데이터를 분석하여 전략적 의사결정을 지원하는 파이프라인. + +``` + ┌─────────────┐ + │ Statistics │ ← 월별 추이, 위반유형, KPI 달성률 + └──────┬──────┘ + │ + ▼ + ┌──────────┐ + │ RiskMap │ ← 격자 위험도 + MTIS 해양사고 통계 + └────┬─────┘ + │ + ▼ + ┌──────────────┐ + │ VesselDetail │ ← 개별 선박 심층 분석 (항적, 이력) + └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │ AIAssistant │ ← 자연어 Q&A (법령 조회, 대응 절차) + └──────┬───────┘ + │ + ▼ + 전략 수립 (순찰 패턴, 탐지 규칙 조정) +``` + +### 2.4 관리 파이프라인 + +시스템 접근 제어, 환경 설정, 데이터 관리, 인프라 모니터링 파이프라인. + +``` + ┌────────────────┐ + │ AccessControl │ ← RBAC 역할/권한 설정 + └───────┬────────┘ + │ + ▼ + ┌────────────┐ + │ LoginPage │ ← SSO/GPKI/비밀번호 인증 + └──────┬─────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ 시스템 설정/관리 │ + │ │ + │ SystemConfig ──── 공통코드/환경설정 │ + │ NoticeManagement ── 공지/배너/팝업 │ + │ DataHub ────────── 데이터 수집 관리 │ + │ AdminPanel ────── 서버/인프라 모니터 │ + └──────────────────────────────────────┘ +``` + +--- + +## 3. 사용자 역할별 페이지 접근 매트릭스 + +시스템에 정의된 5개 역할(LoginPage의 `DEMO_ACCOUNTS` 및 AccessControl의 `ROLES` 기반)에 대한 페이지 접근 권한. + +### 3.1 역할 정의 + +| 역할 | 코드 | 설명 | 인원(시뮬) | +|---|---|---|---| +| 시스템 관리자 | `ADMIN` | 전체 시스템 관리 권한 | 3명 | +| 상황실 운영자 | `OPERATOR` | 상황판, 통계, 경보 운영 | 12명 | +| 분석 담당자 | `ANALYST` | AI 모델, 통계, 항적 분석 | 8명 | +| 현장 단속요원 | `FIELD` | 함정 Agent, 모바일 대응 | 45명 | +| 유관기관 열람자 | `VIEWER` | 공유 대시보드 열람 | 6명 | + +### 3.2 접근 매트릭스 + +| 페이지 | ADMIN | OPERATOR | ANALYST | FIELD | VIEWER | +|---|---|---|---|---|---| +| **인증/관리** | | | | | | +| LoginPage | O | O | O | O | O | +| AccessControl | O | - | - | - | - | +| SystemConfig | O | - | - | - | - | +| NoticeManagement | O | - | - | - | - | +| AdminPanel | O | - | - | - | - | +| **데이터/AI** | | | | | | +| DataHub | O | - | - | - | - | +| AIModelManagement | O | - | O | - | - | +| MLOpsPage | O | - | O | - | - | +| AIAssistant | O | O | O | - | - | +| **탐지** | | | | | | +| DarkVesselDetection | O | - | O | - | - | +| GearDetection | O | - | O | - | - | +| ChinaFishing | O | O | O | - | - | +| TransferDetection | O | - | O | - | - | +| **위험도/계획** | | | | | | +| RiskMap | O | O | O | - | - | +| EnforcementPlan | O | O | - | - | - | +| **순찰** | | | | | | +| PatrolRoute | O | O | - | - | - | +| FleetOptimization | O | O | - | - | - | +| **감시/지도** | | | | | | +| LiveMapView | O | O | O | - | - | +| MapControl | O | O | - | - | - | +| **대시보드** | | | | | | +| Dashboard | O | O | O | O | O | +| MonitoringDashboard | O | O | - | - | - | +| **이벤트/이력** | | | | | | +| EventList | O | O | O | O | - | +| EnforcementHistory | O | - | O | - | - | +| **현장 대응** | | | | | | +| MobileService | O | - | - | O | - | +| ShipAgent | O | - | - | O | - | +| AIAlert | O | O | - | O | - | +| **통계/보고** | | | | | | +| Statistics | O | O | O | - | - | +| ExternalService | O | - | - | - | O | +| ReportManagement | O | O | O | - | - | +| **선박 상세** | | | | | | +| VesselDetail | O | O | O | - | - | + +### 3.3 역할별 요약 + +| 역할 | 접근 가능 페이지 | 페이지 수 | +|---|---|---| +| **시스템 관리자** (ADMIN) | 전체 페이지 | 31 | +| **상황실 운영자** (OPERATOR) | Dashboard, MonitoringDashboard, LiveMapView, MapControl, EventList, EnforcementPlan, PatrolRoute, FleetOptimization, ChinaFishing, RiskMap, Statistics, ReportManagement, AIAssistant, AIAlert, VesselDetail | 15 | +| **분석 담당자** (ANALYST) | Dashboard, DarkVesselDetection, GearDetection, ChinaFishing, TransferDetection, RiskMap, LiveMapView, EventList, EnforcementHistory, Statistics, ReportManagement, VesselDetail, AIAssistant, AIModelManagement, MLOpsPage | 15 | +| **현장 단속요원** (FIELD) | Dashboard, MobileService, ShipAgent, AIAlert, EventList | 5 | +| **유관기관 열람자** (VIEWER) | Dashboard, ExternalService | 2 | + +--- + +## 4. 페이지 간 데이터 흐름 요약 + +``` + ┌──────────────────┐ + │ LoginPage │ + │ (인증 게이트) │ + └────────┬─────────┘ + │ + ┌────────────────────┬┴──────────────────┐ + ▼ ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ ┌─────────────┐ + │ 관리 파이프라인│ │ 탐지 파이프라인 │ │ 현장 대응 │ + │ │ │ │ │ │ + │ AccessControl│ │ DataHub │ │ MobileSvc │ + │ SystemConfig │ │ ↓ │ │ ShipAgent │ + │ NoticeManage │ │ AI탐지엔진 │ │ AIAlert │ + │ DataHub │ │ (DV/Gear/CN/TR)│ └──────┬──────┘ + │ AdminPanel │ │ ↓ │ │ + └──────────────┘ │ RiskMap │ │ + │ ↓ │ ▼ + │ EnforcementPlan │ ┌──────────────┐ + │ ↓ │ │ 대응 파이프라인│ + │ PatrolRoute │ │ │ + │ FleetOptim │ │ Enforcement │ + │ ↓ │ │ History │ + │ LiveMapView │ │ ReportManage │ + │ Monitoring │ │ ExternalSvc │ + └────────┬────────┘ └──────────────┘ + │ + ▼ + ┌─────────────────┐ + │ 분석 파이프라인 │ + │ │ + │ Statistics │ + │ VesselDetail │ + │ AIAssistant │ + └─────────────────┘ +``` + +--- + +## 5. 미할당 SFR 참고 + +현재 라우트에서 확인되는 SFR 번호 기준, 아래 기능은 기존 페이지에 통합되어 있다: + +- **Dashboard**: SFR 번호 미부여, 종합 상황판 (기존 유지) +- **LiveMapView**: SFR 번호 미부여, 실시간 감시 지도 +- **EventList**: SFR-02 공통 컴포넌트 적용 대상으로 분류 +- **MapControl**: SFR 번호 미부여, 해역 통제 관리 +- **VesselDetail**: SFR 번호 미부여, 선박 상세 +- **ReportManagement**: SFR 번호 미부여, 증거/보고서 관리 +- **AdminPanel**: SFR 번호 미부여, 인프라 관리 +- **GearIdentification**: ChinaFishing 내 서브 컴포넌트 diff --git a/docs/sfr-traceability.md b/docs/sfr-traceability.md new file mode 100644 index 0000000..41336ff --- /dev/null +++ b/docs/sfr-traceability.md @@ -0,0 +1,905 @@ +# SFR 요구사항 추적 매트릭스 (Requirements Traceability Matrix) + +**프로젝트:** AI 기반 불법조업 감시 시스템 +**문서 버전:** 2.0 +**최종 업데이트:** 2026-04-06 +**근거 문서:** 제안요청서 (RFP) 소프트웨어 기능 요구사항 (SFR) + +### 기술 스택 및 아키텍처 현황 + +| 항목 | 내용 | +|------|------| +| 기술 스택 | React 19, Vite 8, deck.gl 9.2, Zustand 5.0, ECharts, MapLibre GL | +| 데이터 흐름 | `data/mock` → Zustand store → 페이지 렌더 | +| 렌더링 | deck.gl 제로 리렌더 아키텍처 (`useMapLayers` + RAF) | +| i18n | 10 네임스페이스, MainLayout + 24페이지 + LoginPage 적용 | +| 테마 | Dark/Light 전환 지원 (CSS 변수 기반) | + +--- + +## 요약 + +| 구분 | 건수 | +|------|------| +| 전체 SFR | 20건 | +| UI 완료 | 20건 (100%) | +| 기능 일부 구현 (프론트엔드 시뮬레이션) | 20건 | +| 백엔드 연동 완료 | 0건 (0%) | + +> 현재 전체 SFR에 대해 화면(UI) 프로토타입이 완성되었으며(31페이지), 시뮬레이션 데이터 기반으로 동작합니다. +> 데이터는 `data/mock` JSON에서 8개의 Zustand store(`kpiStore`, `vesselStore`, `eventStore`, `enforcementStore`, `patrolStore`, `gearStore`, `transferStore`, `settingsStore`)를 거쳐 페이지에 전달됩니다. +> 지도 페이지는 deck.gl 레이어 렌더링을 사용하며, 모든 페이지에 i18n 제목/설명 및 Dark/Light 테마가 적용되어 있습니다. +> 실제 백엔드 API, AI 모델, 외부 시스템 연동은 2차 개발 단계에서 수행됩니다. + +### 구현 진척 요약 + +| 항목 | 상태 | +|------|------| +| UI 프로토타입 | ✅ 완료 (31페이지) | +| 기술스택 전환 | ✅ 완료 (React 19, Vite 8, deck.gl, ECharts) | +| 데이터 통합 | ✅ 완료 (7 mock → 8 store) | +| i18n 구조 | ✅ 완료 (제목/메뉴, 내부 텍스트 미완) | +| 테마 시스템 | ✅ 완료 (dark/light, 시맨틱 CSS 변수) | +| API 서비스 | 🔲 샘플 구조만 (mock 반환) | +| 실시간 인프라 | 🔲 스캐폴드만 (STOMP.js 미설치) | +| 코드 스플리팅 | 🔲 미적용 (3.2MB 단일 번들) | + +--- + +## SFR 목록 + +| SFR | 요구사항 명칭 | 구현 화면 | 구현 상태 | +|-----|-------------|----------|----------| +| SFR-01 | 시스템 로그인 및 권한 관리 | LoginPage, AccessControl | UI 완료 | +| SFR-02 | 시스템 기본 환경설정 및 공통 기능 | SystemConfig, NoticeManagement, 공통 컴포넌트 | UI 완료 | +| SFR-03 | 통합 데이터 허브 수집·연계 관리 | DataHub | UI 완료 | +| SFR-04 | AI 불법조업 예측모델 관리 | AIModelManagement | UI 완료 | +| SFR-05 | 격자 기반 불법조업 위험도 지도 생성·시각화 | RiskMap | UI 완료 | +| SFR-06 | 단속 계획·경보 연계 | EnforcementPlan | UI 완료 | +| SFR-07 | AI 경비함정 단일 함정 순찰·경로 추천 | PatrolRoute | UI 완료 | +| SFR-08 | AI 경비함정 다함정 협력형 경로 최적화 | FleetOptimization | UI 완료 | +| SFR-09 | 불법 어선 패턴 탐지 | DarkVesselDetection | UI 완료 | +| SFR-10 | 불법 어망·어구 탐지 및 관리 | GearDetection, GearIdentification | UI 완료 | +| SFR-11 | 단속·탐지 이력 관리 | EnforcementHistory, EventList | UI 완료 | +| SFR-12 | 모니터링 및 경보 현황판(대시보드) | Dashboard, MonitoringDashboard | UI 완료 | +| SFR-13 | 통계·지표·성과 분석 | Statistics | UI 완료 | +| SFR-14 | 외부 서비스 제공 결과 연계 | ExternalService | UI 완료 | +| SFR-15 | 단속요원 이용 모바일 대응 서비스 | MobileService | UI 완료 | +| SFR-16 | 함정용 단말 Agent 개발 | ShipAgent | UI 완료 | +| SFR-17 | 현장 함정 즉각 대응 AI 알림 메시지 발송 | AIAlert | UI 완료 | +| SFR-18 | 기계학습 운영 기능 | MLOpsPage | UI 완료 | +| SFR-19 | 대규모 언어모델(LLM) 운영 기능 | MLOpsPage (LLMOps 탭) | UI 완료 | +| SFR-20 | 자연어 처리 기반 AI 의사결정 지원(Q&A) 서비스 | AIAssistant | UI 완료 | + +--- + +## 상세 추적 내역 + +--- + +### SFR-01: 시스템 로그인 및 권한 관리 + +**제안요청서 정의:** 사용자 유형별 안전한 인증 및 역할 기반 권한 관리 체계를 구축하여, 시스템 접근 보안을 확보하고 사용자 활동에 대한 감사 추적을 가능하게 한다. + +**세부 요구사항 요약:** +- 해양경찰 SSO·공무원증·GPKI 등 기존 인증체계 로그인 연동 +- 역할 기반 접근 제어(RBAC) 구현 +- 감사 로그(Audit Log) 기록 및 조회 +- 비밀번호 정책 적용 (9자 이상, 영문+숫자+특수문자) +- 5회 연속 인증 실패 시 계정 잠금 (30분) + +**구현 화면:** 로그인 페이지 (`src/features/auth/LoginPage.tsx`), 접근 권한 관리 (`src/features/admin/AccessControl.tsx`) + +**화면 구성 요소:** +- 로그인 폼: ID/PW 입력, GPKI 인증서 로그인, SSO 연동 버튼 (3가지 인증 방식 탭) +- 역할별 데모 계정 5종: 관리자(ADMIN), 운용자(OPERATOR), 분석관(ANALYST), 현장요원(FIELD), 열람자(VIEWER) +- 비밀번호 정책 검증 UI (길이·복잡도 실시간 표시) +- 계정 잠금 카운터 및 잠금 상태 표시 +- 접근 권한 관리 테이블 (역할별 메뉴/기능 접근 매트릭스) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 로그인 페이지 제목/설명, 접근 권한 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- Zustand: `settingsStore`를 통한 테마/언어 설정 관리 + +**미구현 항목:** +- 실제 SSO(해양경찰 통합인증) 연동 +- GPKI(정부 공인인증서) 인증 모듈 연동 +- 공무원증 기반 인증 연동 +- 백엔드 세션 관리 및 JWT 토큰 발급 +- 감사 로그 DB 저장 및 조회 API +- 인사 시스템 연동을 통한 역할 자동 부여 + +--- + +### SFR-02: 시스템 기본 환경설정 및 공통 기능 + +**제안요청서 정의:** 시스템 운영에 필요한 공통 코드, 기준정보, 알림, 공통 UI 컴포넌트를 관리하며, GIS 지도 기반 웹서비스 및 범용 데이터 처리 기능을 제공한다. + +**세부 요구사항 요약:** +- 공통 코드 등록·수정·폐기 관리 +- 알림 관리 (팝업, 배너, 공지사항) +- GIS 지도 기반 웹서비스 제공 +- 파일 업로드/다운로드 +- 검색·페이징·정렬 +- 엑셀 내보내기 + +**구현 화면:** 시스템 설정 (`src/features/admin/SystemConfig.tsx`), 공지사항 관리 (`src/features/admin/NoticeManagement.tsx`) + +**공통 컴포넌트:** +- `src/shared/components/common/DataTable.tsx` - 데이터 테이블 (정렬, 필터링) +- `src/shared/components/common/ExcelExport.tsx` - 엑셀 내보내기 +- `src/shared/components/common/Pagination.tsx` - 페이징 처리 +- `src/shared/components/common/SearchInput.tsx` - 검색 입력 +- `src/shared/components/common/FileUpload.tsx` - 파일 업로드 +- `src/shared/components/common/PrintButton.tsx` - 인쇄 버튼 +- `src/shared/components/common/SaveButton.tsx` - 저장 버튼 +- `src/shared/components/common/NotificationBanner.tsx` - 알림 배너 +- `src/shared/components/common/PageToolbar.tsx` - 페이지 도구 모음 +- `src/shared/components/ui/card.tsx` - 카드 컴포넌트 +- `src/shared/components/ui/badge.tsx` - 뱃지 컴포넌트 + +**화면 구성 요소:** +- 공통 코드 관리 테이블 (875건 기준정보 JSON) +- 공지사항 등록·수정·삭제 폼 +- 알림 설정 (팝업/배너/공지 유형 선택) +- GIS 지도 컴포넌트 (MapLibre 기반) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 시스템 설정/공지사항 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 (시맨틱 CSS 변수 기반) +- Zustand: `settingsStore`를 통한 시스템 설정 상태 관리 +- GIS 지도: MapLibre GL 기반 공통 지도 컴포넌트 + +**미구현 항목:** +- 공통 코드 CRUD 백엔드 API 연동 +- 기준정보 DB 저장 (현재 JSON 파일 기반) +- 파일 업로드/다운로드 서버 연동 +- 알림 발송 엔진 (푸시, 이메일) +- 엑셀 내보내기 서버사이드 렌더링 (대용량) + +--- + +### SFR-03: 통합 데이터 허브 수집·연계 관리 + +**제안요청서 정의:** AIS, V-PASS, 위성, 해양환경 등 이기종 데이터를 통합 수집하는 데이터 허브를 구축하고, 수집 파이프라인의 실시간 모니터링 및 이상 감지 기능을 제공한다. + +**세부 요구사항 요약:** +- 다중 데이터 소스 수집 파이프라인 구축 (AIS, V-PASS, 위성, 해양환경, 기상) +- 실시간 스트리밍 및 배치 수집 지원 +- 수집 상태 모니터링 대시보드 +- 이상 감지 시 자동 알림 +- 수집 이력 조회 화면 + +**구현 화면:** 데이터 허브 (`src/features/admin/DataHub.tsx`) + +**화면 구성 요소:** +- 선박신호 수신 현황: 5개 신호원(지상 AIS, 위성 AIS, V-PASS, LRIT, 해상VHF) 24시간 타임라인 히트맵 +- 선박위치정보 모니터링: 22개 연계 채널 상태 테이블 (수신율, 지연시간, 최종수신) +- 채널별 상태 표시 (정상/경고/오류) +- 수집 파이프라인 등록·시작·중지 관리 버튼 +- 수집 이력 로그 테이블 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 데이터 허브 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- Zustand: mock 데이터 기반 수집 현황 표시 + +**미구현 항목:** +- 실제 AIS 수신기 연동 (지상·위성) +- V-PASS 시스템 API 연동 +- LRIT 데이터센터 연동 +- 해양환경·기상 데이터 수집 배치 구현 +- 실시간 스트리밍 파이프라인 (Kafka 등) +- 수집 이상 감지 알림 엔진 +- 수집 이력 DB 저장 + +--- + +### SFR-04: AI 불법조업 예측모델 관리 + +**제안요청서 정의:** 해역별 불법조업 위험도를 예측하는 AI 모델의 개발·훈련·배포·모니터링 전주기를 관리하는 체계를 제공한다. + +**세부 요구사항 요약:** +- 학습 데이터셋 관리 (수집·정제·라벨링) +- 모델 구조 설계 및 하이퍼파라미터 관리 +- 과적합 방지 (교차검증, 정규화) +- 재학습 파이프라인 (자동/수동) +- 예측 결과 API 제공 + +**구현 화면:** AI 모델 관리 (`src/features/ai-operations/AIModelManagement.tsx`) + +**화면 구성 요소:** +- 모델 레지스트리: 5개 버전 관리 (v1.0~v2.1), 배포 이력, 성능 지표 비교 테이블 +- 탐지 규칙 관리: 6개 규칙 (EEZ 침범, AIS 차단, 속력 이상, 선단 밀집, 환적 의심, MMSI 변조) ON/OFF 토글, 가중치 슬라이더 +- 피처 엔지니어링: 20개 피처 (Kinematic/Geometric/Temporal/Behavioral/Contextual 5개 카테고리) +- 학습 파이프라인: 6단계 시각화 (데이터 수집 → 전처리 → 피처추출 → 모델학습 → 평가 → 배포) +- 성능 모니터링: 정확도·Recall·F1·오탐률·리드타임 추이 차트 +- 어구 탐지 모델: GB/T 5147 어구 분류 체계 +- 7대 탐지 엔진: 불법조업 감시 알고리즘 v4.0 (906척 허가어선 기준) +- API 문서: 예측 결과 API 사양 표시 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: AI 모델 관리 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- ECharts: 성능 모니터링 추이 차트 렌더링 + +**미구현 항목:** +- 실제 ML 모델 학습 및 추론 엔진 연동 +- 학습 데이터셋 관리 (데이터 수집·정제·라벨링 파이프라인) +- 모델 버전 관리 저장소 (MLflow 등) +- 재학습 자동화 (스케줄러, 트리거 기반) +- 예측 결과 REST API 서버 구현 +- 과적합 감지 및 모델 드리프트 모니터링 + +--- + +### SFR-05: 격자 기반 불법조업 위험도 지도 생성·시각화 + +**제안요청서 정의:** AI 예측 결과를 격자 및 해역 단위 위험도 지도로 시각화하여, 해역별 위험 수준을 직관적으로 파악할 수 있는 지리정보 기반 인터페이스를 제공한다. + +**세부 요구사항 요약:** +- 격자체계 정의 (해역 분할 기준) +- 5단계 등급화 위험도 지도 (매우높음/높음/보통/낮음/안전) +- 조건별 필터링 (기간, 해역, 위험등급) +- 격자 선택 시 상세 이력 조회 +- 지도 출력(인쇄) 기능 +- MTIS 해양사고 통계 연계 + +**구현 화면:** 위험도 지도 (`src/features/risk-assessment/RiskMap.tsx`) + +**화면 구성 요소:** +- 10x18 격자 히트맵: 위험도 5단계 색상 매핑 +- 6개 탭: 위험도 히트맵 / 년도별 통계 / 선박 특성별 / 사고종류별 / 시간적 특성별 / 사고율 +- 해역별 요약 테이블: 6개 구역 위험도·추세·선박수 +- 위험등급 분포 범례 (건수, 비율) +- GIS 지도 오버레이 (EEZ, NLL 경계선 표시) +- MTIS 해양사고 통계 차트 (중앙해양안전심판원 데이터 기반) +- 년도별 사고 추이, 선박 유형별·톤수별·선령별 분포, 사고종류별 분석 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 위험도 지도 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- deck.gl: 격자 히트맵 레이어 렌더링 (`useMapLayers` + RAF 제로 리렌더) +- ECharts: MTIS 해양사고 통계 차트, 년도별 추이 차트 + +**미구현 항목:** +- AI 예측 모델 연동을 통한 실시간 위험도 산출 +- 격자 선택 시 실제 이력 데이터 조회 (DB 연동) +- MTIS 실시간 데이터 API 연계 +- 위험도 지도 자동 갱신 (주기적 배치) +- 격자 해상도 동적 변경 + +--- + +### SFR-06: 단속 계획·경보 연계 + +**제안요청서 정의:** AI 위험도 분석 결과를 기반으로 단속 우선지역을 자동 도출하고, 임계값 초과 시 경보를 발령하여 선제적 대응 체계를 구축한다. + +**세부 요구사항 요약:** +- 위험도 기반 단속 우선지역 자동 추천 +- 적정 단속 계획 수립 (인력·함정 배치) +- 임계값 초과 시 자동 경보 발령 +- 단속 계획 이력 관리 + +**구현 화면:** 단속 계획 (`src/features/risk-assessment/EnforcementPlan.tsx`) + +**화면 구성 요소:** +- 단속 계획 목록: 5개 계획 (계획명, 대상 해역, 기간, 투입 함정, 상태) +- 지도 기반 계획 시각화 (단속 구역·함정 배치 표시) +- 단속 계획 상세 테이블 (우선순위, 예상 위험도, 투입 자원) +- 경보 현황 패널 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 단속 계획 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- deck.gl: 단속 구역/함정 배치 지도 레이어 렌더링 +- Zustand: `enforcementStore`를 통한 단속 계획 데이터 관리 + +**미구현 항목:** +- 위험도 기반 단속 우선지역 자동 추천 알고리즘 +- 경보 발령 엔진 (임계값 설정 및 자동 발송) +- 단속 계획 승인 워크플로우 +- 함정·인력 자원 관리 시스템 연동 +- 단속 계획 이력 DB 저장 + +--- + +### SFR-07: AI 경비함정 단일 함정 순찰·경로 추천 + +**제안요청서 정의:** 개별 경비함정의 최적 순찰 경로를 AI가 산출하여 추천하며, 함정 성능·기상·위험도를 종합적으로 반영한 시나리오별 경로를 제공한다. + +**세부 요구사항 요약:** +- 함정 성능(속력, 항속거리), 기상 조건, 해역 위험도를 반영한 경로 산출 +- 시나리오별 가중치 조정 (위험도 중시 / 커버리지 중시 / 연료 효율) +- 경유점 기반 시뮬레이션 +- Human-in-the-loop (운용자 수정 → 재계산) + +**구현 화면:** 순찰 경로 (`src/features/patrol/PatrolRoute.tsx`) + +**화면 구성 요소:** +- 함정 선택: 4척 (3001함/3005함/3009함/5001함), 함급·속력·항속거리·상태 표시 +- 추천 경로 지도: 경유점(Waypoint) 마커, 경로 폴리라인, EEZ/NLL 경계 +- 3가지 시나리오: 위험도 중시 / 커버리지 중시 / 연료 효율 +- 경로 요약: 거리, 소요시간, 연료 소모, 감시 격자 수 +- 경유점 상세: ID, 명칭, 좌표, 예상도착시간(ETA), 설명 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 순찰 경로 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- deck.gl: 경유점 마커, 경로 폴리라인, EEZ/NLL 경계 레이어 렌더링 +- Zustand: `patrolStore`를 통한 함정/경로 데이터 관리 + +**미구현 항목:** +- AI 경로 최적화 엔진 (유전 알고리즘, TSP 기반) +- 실시간 기상 데이터 반영 +- 해류·조류 정보 연동 +- Human-in-the-loop: 운용자 경유점 수정 시 실시간 재계산 +- 경로 확정 및 함정 단말 전송 + +--- + +### SFR-08: AI 경비함정 다함정 협력형 경로 최적화 + +**제안요청서 정의:** 다수 경비함정의 협력 운용을 통해 해역 커버리지를 최대화하고, 함정 간 중복을 최소화하는 최적 배치 계획을 산출한다. + +**세부 요구사항 요약:** +- 다함정 동시 배치 계획 수립 +- 해역 커버리지 최적화 (전체 감시 면적 최대화) +- 함정 간 순찰 구역 중복 최소화 +- 최적화 전후 비교 시뮬레이션 + +**구현 화면:** 함대 최적화 (`src/features/patrol/FleetOptimization.tsx`) + +**화면 구성 요소:** +- 함정 현황: 5척 투입 함정 목록 (함명, 함급, 상태, 담당 구역) +- 커버리지 구역: 6개 구역 배치 현황 +- 최적화 전후 비교: 커버리지율, 중복율, 응답시간 개선 지표 +- 지도 기반 배치 시각화 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 함대 최적화 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- deck.gl: 함정 배치, 커버리지 구역 지도 레이어 렌더링 +- Zustand: `patrolStore`를 통한 함정 현황 데이터 관리 + +**미구현 항목:** +- 다함정 협력 최적화 AI 엔진 (다목적 최적화) +- 실시간 함정 위치 추적 연동 +- 커버리지 계산 알고리즘 (보로노이 분할 등) +- 시뮬레이션 실행 엔진 +- 최적화 결과 함정 단말 자동 배포 + +--- + +### SFR-09: 불법 어선 패턴 탐지 + +**제안요청서 정의:** AIS 조작·위장·Dark Vessel(AIS 미송출 선박) 등 불법조업 관련 이상 패턴을 AI로 탐지하여 의심 선박을 식별한다. + +**세부 요구사항 요약:** +- AIS 송출 차단(Dark Vessel) 탐지 +- MMSI 변조 감지 (동일 선박 다중 MMSI 사용) +- 속력 변화 이상 패턴 탐지 +- 국적·선명 위장 감지 +- 위험도 스코어링 및 라벨링 + +**구현 화면:** 다크베셀 탐지 (`src/features/detection/DarkVesselDetection.tsx`) + +**화면 구성 요소:** +- 의심 선박 목록: 7척 (선박명, 위험도 스코어, 탐지 유형, 좌표, 패턴) +- 5가지 탐지 패턴: AIS 차단, MMSI 변조, 속력 이상, 국적 위장, 선단 밀집 +- 위험도 스코어링: 0~1.0 수치 기반 위험 등급 표시 +- 라벨링 기능: 의심 선박에 대한 분류 태그 부여 +- 지도 기반 의심 선박 위치 표시 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 다크베셀 탐지 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- deck.gl: 의심 선박 위치 마커, 이동 경로 레이어 렌더링 +- Zustand: `vesselStore`를 통한 의심 선박 데이터 관리 + +**미구현 항목:** +- AI 패턴 탐지 엔진 (AIS 시계열 분석 모델) +- 실시간 AIS 데이터 스트림 연동 +- 위성 영상 기반 Dark Vessel 교차 검증 +- MMSI 변조 이력 DB 축적 및 분석 +- 탐지 결과 자동 경보 발령 +- 라벨링 결과 재학습 파이프라인 연동 + +--- + +### SFR-10: 불법 어망·어구 탐지 및 관리 + +**제안요청서 정의:** 불법 어구 설치 현황을 탐지하고, AIS 조작 패턴과 연계하여 불법 어구 사용 선박을 식별하는 분석 체계를 제공한다. + +**세부 요구사항 요약:** +- AIS 조작 패턴 기반 불법 어구 사용 감지 +- 불법 어망·어구 분석 및 식별 +- 어구 유형별 분류 및 판정 + +**구현 화면:** 어구 탐지 (`src/features/detection/GearDetection.tsx`), 어구 식별 (`src/features/detection/GearIdentification.tsx`) + +**화면 구성 요소:** +- 어구 탐지 현황: 6개 어구 유형별 탐지 목록 +- 어구 식별 결정 트리: 어구 유형 판정 로직 시각화 +- 판정 요인 분석: 선박 이동 패턴, 정박 시간, 조업 형태 등 요인별 기여도 +- 중국어선 어구 탐지 (`src/features/detection/ChinaFishing.tsx` 내 연계) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 어구 탐지/어구 식별 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- Zustand: `gearStore`를 통한 어구 탐지 데이터 관리 + +**미구현 항목:** +- AI 어구 탐지 모델 (위성 영상 + AIS 패턴 융합) +- 어구 유형 자동 분류 모델 (GB/T 5147 기반) +- 실시간 AIS 패턴 분석을 통한 어구 사용 추정 +- 탐지 결과 DB 저장 및 이력 관리 +- 불법 어구 적발 통계 자동 집계 + +--- + +### SFR-11: 단속·탐지 이력 관리 + +**제안요청서 정의:** 단속 활동 및 AI 탐지 결과에 대한 이력을 체계적으로 관리하여, 과거 사례 조회 및 AI 매칭 검증이 가능한 이력 관리 체계를 구축한다. + +**세부 요구사항 요약:** +- 단속·탐지 이력 통합 조회 +- AI 탐지 결과와 실제 단속 결과 매칭 검증 +- 이력 기반 통계 분석 + +**구현 화면:** 단속 이력 (`src/features/enforcement/EnforcementHistory.tsx`), 이벤트 목록 (`src/features/enforcement/EventList.tsx`) + +**화면 구성 요소:** +- 단속 이력 테이블: 6건 단속 기록 (일시, 해역, 선박, 위반 유형, 조치, 결과) +- 이벤트 목록: 15건 탐지 이벤트 (DataTable 기반 정렬·필터·페이징) +- 이력 상세 조회 패널 +- 검색·필터링 (기간, 해역, 위반 유형) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 단속 이력/이벤트 목록 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- Zustand: `enforcementStore`, `eventStore`를 통한 이력 데이터 관리 + +**미구현 항목:** +- 단속·탐지 이력 DB 연동 (CRUD API) +- AI 탐지 결과 ↔ 실제 단속 결과 자동 매칭 로직 +- 이력 기반 통계 분석 백엔드 +- 단속 보고서 자동 생성 +- 첨부파일(증거 사진·영상) 관리 + +--- + +### SFR-12: 모니터링 및 경보 현황판(대시보드) + +**제안요청서 정의:** 해양 불법조업 감시 현황을 실시간으로 종합 모니터링하고, 경보 상태를 직관적으로 파악할 수 있는 통합 대시보드를 제공한다. + +**세부 요구사항 요약:** +- 핵심 KPI 실시간 표시 +- 실시간 상황 모니터링 (선박 위치, 탐지 현황) +- 경보 현황 표시 및 이력 타임라인 + +**구현 화면:** 메인 대시보드 (`src/features/dashboard/Dashboard.tsx`), 모니터링 대시보드 (`src/features/monitoring/MonitoringDashboard.tsx`) + +**화면 구성 요소:** +- KPI 카드 6종: 실시간 탐지(47건), EEZ 침범(18건), 다크베셀(12건), 불법환적 의심(8건), 추적 중(15건), 나포/검문(3건) — 전일 대비 증감 표시 +- 작전 경보 타임라인: 10건 시간순 이벤트 (CRITICAL/HIGH/MEDIUM/LOW 4단계) +- 위험 선박 TOP 8: 위험도 스코어 순위, 선박명·국적·위치·패턴 표시 +- 경비함정 현황: 6척 상태 (추적/검문/초계/귀항/대기) +- 해역별 위험도: 7개 구역 위험 수준·추세 +- 시간대별 탐지 추이 차트 +- GIS 지도: EEZ/NLL 경계선, 선박 위치 마커, 히트맵 레이어 +- 기상 정보 표시 (풍향, 풍속, 수온, 파고) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 대시보드/모니터링 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- deck.gl: GIS 지도에 선박 위치 마커, 히트맵 레이어 렌더링 +- Zustand: `kpiStore`, `vesselStore`, `eventStore`를 통한 대시보드 데이터 관리 +- ECharts: 시간대별 탐지 추이 차트, 해역별 위험도 차트 + +**미구현 항목:** +- 실시간 데이터 스트리밍 연동 (WebSocket) +- 실시간 AIS 선박 위치 표시 +- 경보 자동 발령 및 알림 연동 +- KPI 실시간 갱신 (DB 기반 집계) +- 기상청 API 연동 + +--- + +### SFR-13: 통계·지표·성과 분석 + +**제안요청서 정의:** 단속 건수, 위반 유형, 해역별·계절별 패턴, AI 적중률 등 핵심 성과 지표를 체계적으로 분석하고 시각화한다. + +**세부 요구사항 요약:** +- 월별 단속 추이 분석 +- 위반 유형별 분포 통계 +- KPI 지표: 정확도, 오탐률, 리드타임, 성공률, 응답시간 + +**구현 화면:** 통계 (`src/features/statistics/Statistics.tsx`) + +**화면 구성 요소:** +- 월별 단속 추이 차트 (AreaChart) +- 위반 유형별 분포 차트 (PieChart/BarChart) +- KPI 지표 테이블: 정확도, 오탐률, 리드타임, 성공률, 응답시간 +- 해역별·계절별 크로스탭 분석 +- 통계 기간 설정 필터 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 통계 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- ECharts: 월별 단속 추이(AreaChart), 위반 유형별 분포(PieChart/BarChart) 렌더링 + +**미구현 항목:** +- 통계 데이터 집계 백엔드 (DB 기반) +- AI 적중률 자동 산출 (탐지 ↔ 단속 결과 매칭) +- 보고서 자동 생성 및 출력 +- 기간별·해역별 다차원 분석 엔진 +- 성과 지표 목표 대비 달성률 추적 + +--- + +### SFR-14: 외부 서비스 제공 결과 연계 + +**제안요청서 정의:** 유관기관(해수부, 수협, 어업관리단 등) 간 데이터를 API 기반으로 상호 조회·공유하며, 개인정보 비식별화 처리를 통해 보안을 확보한다. + +**세부 요구사항 요약:** +- API 기반 데이터 제공 체계 구축 +- 개인정보 비식별화 처리 +- 유관기관별 접근 권한 관리 + +**구현 화면:** 외부 서비스 (`src/features/statistics/ExternalService.tsx`) + +**화면 구성 요소:** +- 외부 서비스 목록: 5개 연계 서비스 (서비스명, 대상기관, 데이터 유형, 상태) +- 프라이버시 등급 표시 (개인정보 비식별화 수준) +- API 사용 현황 모니터링 +- 서비스별 접근 권한 설정 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 외부 서비스 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 + +**미구현 항목:** +- 외부 기관 API Gateway 구축 +- 개인정보 비식별화 처리 모듈 (k-익명성, 차등 프라이버시) +- 유관기관 연계 프로토콜 합의 및 구현 +- API 인증·인가 (OAuth 2.0 / API Key) +- 데이터 제공 이력 감사 로그 + +--- + +### SFR-15: 단속요원 이용 모바일 대응 서비스 + +**제안요청서 정의:** 현장 단속요원이 모바일 기기를 통해 AI 예측 정보, 경로 추천, 지도 조회 등을 활용할 수 있는 모바일 대응 서비스를 제공한다. + +**세부 요구사항 요약:** +- AI 예측 정보 수신 (위험도, 의심선박) +- 최적 경로 추천 수신 +- 모바일 지도 조회 +- 오프라인 모드 지원 + +**구현 화면:** 모바일 서비스 (`src/features/field-ops/MobileService.tsx`) + +**화면 구성 요소:** +- 모바일 화면 프리뷰: 반응형 레이아웃 미리보기 +- 푸시 알림 설정: 알림 유형별 수신 ON/OFF +- 미니맵: 축소 지도에서 주요 이벤트 표시 +- 모바일 기능 목록 (예측 조회, 경로 수신, 사진 보고 등) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 모바일 서비스 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 + +**미구현 항목:** +- 네이티브 모바일 앱 개발 (현재 웹 시뮬레이션만 존재) +- 푸시 알림 서버 (FCM/APNs) +- 오프라인 데이터 캐싱 (Service Worker / SQLite) +- 모바일 전용 경량 지도 엔진 +- 현장 사진·영상 업로드 기능 +- 단속 보고서 모바일 작성 + +--- + +### SFR-16: 함정용 단말 Agent 개발 + +**제안요청서 정의:** 경비함정 터미널에서 AI 기반 예측·경로분석·신속 정보 조회가 가능한 전용 Agent 소프트웨어를 개발한다. + +**세부 요구사항 요약:** +- AI 예측 결과 실시간 표시 +- 경로 분석 및 최적화 제안 +- 신속 정보 제공 (선박 DB, 위험 이력) +- 지도 기반 상황 질의 인터페이스 + +**구현 화면:** 함정 Agent (`src/features/field-ops/ShipAgent.tsx`) + +**화면 구성 요소:** +- Agent 상태 테이블: 6개 함정별 Agent 설치 현황 (함명, 버전, 상태, 최종접속) +- Agent 기능 목록 (예측 조회, 경로 수신, DB 조회, 상황 보고) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: 함정 Agent 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 + +**미구현 항목:** +- 함정 단말용 Agent 소프트웨어 개발 (네이티브 or Electron) +- AI 예측 결과 실시간 수신 모듈 +- 함정 단말 ↔ 본부 서버 통신 프로토콜 +- 오프라인 동작 모드 (위성 통신 불안정 대비) +- 지도 기반 자연어 질의 인터페이스 + +--- + +### SFR-17: 현장 함정 즉각 대응 AI 알림 메시지 발송 + +**제안요청서 정의:** AI 탐지 결과를 현장 함정에 즉각 알림 메시지로 자동 발송하고, 수신 확인 및 미수신 시 재발송 기능을 제공한다. + +**세부 요구사항 요약:** +- AI 탐지 결과 자동 전송 (함정 단말 + 모바일) +- 수신 확인 관리 +- 미수신 시 자동 재발송 + +**구현 화면:** AI 알림 (`src/features/field-ops/AIAlert.tsx`) + +**화면 구성 요소:** +- 알림 목록: 5건 발송 내역 (알림 유형, 대상 함정, 발송시간, 내용 요약) +- 전송 상태 표시: 발송 완료 / 수신 확인 / 미수신 +- 재발송 버튼 +- 알림 유형 필터 (긴급/일반/정보) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: AI 알림 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 + +**미구현 항목:** +- 알림 발송 서버 (메시지 큐 기반) +- 함정 단말 수신 확인 프로토콜 +- 미수신 자동 재발송 로직 (재시도 정책) +- 위성 통신 기반 발송 채널 +- 알림 발송 이력 DB 저장 + +--- + +### SFR-18: 기계학습 운영 기능 + +**제안요청서 정의:** ML 모델의 학습·배포·모니터링 전주기를 관리하는 MLOps 체계를 구축하여, 모델 품질 유지 및 지속적 개선을 지원한다. + +**세부 요구사항 요약:** +- 실험(Experiment) 관리 및 비교 +- 모델 레지스트리 (버전 관리, 승인 워크플로우) +- 배포 파이프라인 (Canary/Blue-Green) +- 모델 성능 모니터링 (드리프트 감지) + +**구현 화면:** WING AI 플랫폼 (`src/features/ai-operations/MLOpsPage.tsx`) + +**화면 구성 요소:** +- 7개 탭 구성: + 1. **운영 대시보드:** KPI 4종 (고위험 탐지, 중위험 탐지, 배포 중 모델, 진행 중 실험), 리소스 사용량 + 2. **Experiment Studio:** 실험 목록, 하이퍼파라미터, 메트릭 비교 + 3. **Model Registry:** 등록 모델, 버전 이력, 승인 상태 + 4. **Deploy Center:** 배포 파이프라인, Canary/Blue-Green 전략 + 5. **API Playground:** 모델 API 테스트 인터페이스 + 6. **LLMOps:** 대규모 언어모델 운영 (SFR-19와 공유) + 7. **플랫폼 관리:** 리소스·사용자·권한 관리 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: WING AI 플랫폼 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 +- ECharts: 리소스 사용량, 실험 메트릭 비교 차트 + +**미구현 항목:** +- MLOps 플랫폼 백엔드 (Kubeflow / MLflow 등) +- GPU 클러스터 연동 및 리소스 관리 +- 실험 추적 및 메트릭 저장 (MLflow Tracking) +- 모델 레지스트리 저장소 (S3/MinIO) +- CI/CD 기반 배포 파이프라인 +- 모델 드리프트 자동 감지 및 재학습 트리거 + +--- + +### SFR-19: 대규모 언어모델(LLM) 운영 기능 + +**제안요청서 정의:** 대규모 언어모델(LLM)의 선택·학습·추론·튜닝 기능을 제공하여, 자연어 기반 AI 서비스의 운영 기반을 구축한다. + +**세부 요구사항 요약:** +- LLM 모델 선택 및 관리 +- Fine-tuning 학습 파이프라인 +- 추론 서빙 및 로그 관리 +- 하이퍼파라미터 튜닝(HPS) + +**구현 화면:** WING AI 플랫폼 LLMOps 탭 (`src/features/ai-operations/MLOpsPage.tsx` — LLMOps 탭) + +**화면 구성 요소:** +- LLMOps 5개 서브탭: + 1. **학습(Train):** Fine-tuning Job 6건, 데이터셋·에포크·학습률 설정 + 2. **HPS:** 하이퍼파라미터 서치 설정 및 결과 + 3. **로그(Log):** 추론 로그, 토큰 사용량, 응답 시간 + 4. **워커(Worker):** GPU 워커 상태 모니터링 + 5. **LLM 테스트:** 모델별 프롬프트 테스트 인터페이스 +- LLM 모델 목록: 6개 모델 (모델명, 파라미터 크기, 상태, 용도) + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: LLMOps 탭 제목/설명 적용 (MLOpsPage 공유) +- 테마: Dark/Light 전환 지원 + +**미구현 항목:** +- LLM 서빙 인프라 (vLLM / TGI 등) +- Fine-tuning 학습 파이프라인 (LoRA/QLoRA) +- 하이퍼파라미터 자동 탐색 (Optuna 등) +- GPU 클러스터 연동 및 워커 관리 +- 추론 로그 수집 및 분석 +- LLM 모델 평가 벤치마크 + +--- + +### SFR-20: 자연어 처리 기반 AI 의사결정 지원(Q&A) 서비스 + +**제안요청서 정의:** 자연어 질의를 통해 법령·사례·AI 예측결과를 통합 검색하고, RAG 기반 대화형 의사결정 지원 서비스를 제공한다. + +**세부 요구사항 요약:** +- RAG(검색 증강 생성) 기반 대화형 Q&A +- 법령 DB 연결 (수산업법, 해양경비법 등) +- 부적절 답변 필터링 (가드레일) +- 대화 이력 관리 + +**구현 화면:** AI 어시스턴트 (`src/features/ai-operations/AIAssistant.tsx`) + +**화면 구성 요소:** +- 채팅 UI: 대화형 인터페이스 (사용자 질문 → AI 답변) +- 샘플 대화: 불법조업 관련 법령·절차·사례 질의응답 시연 +- 대화 이력 사이드바: 과거 대화 세션 목록 +- 법령 참조 뱃지: 답변에 인용된 법령·규정 출처 표시 +- 관련 문서 링크 + +**구현 상태:** UI 완료 + +**통합 현황:** +- i18n: AI 어시스턴트 페이지 제목/설명 적용 +- 테마: Dark/Light 전환 지원 + +**미구현 항목:** +- LLM 백엔드 서빙 (GPT-4 / LLaMA 등) +- RAG 파이프라인 (Vector DB + Embedding + Retriever) +- 법령 DB 구축 및 인덱싱 (수산업법, 해양경비법, EEZ법 등) +- 답변 품질 가드레일 (Hallucination 방지, 부적절 답변 필터) +- 대화 이력 DB 저장 +- 사용자 피드백 수집 및 모델 개선 루프 + +--- + +## 부록: 파일 경로 매핑 요약 + +| SFR | 주요 파일 경로 | +|-----|--------------| +| SFR-01 | `src/features/auth/LoginPage.tsx`, `src/features/admin/AccessControl.tsx` | +| SFR-02 | `src/features/admin/SystemConfig.tsx`, `src/features/admin/NoticeManagement.tsx`, `src/shared/components/common/*` | +| SFR-03 | `src/features/admin/DataHub.tsx` | +| SFR-04 | `src/features/ai-operations/AIModelManagement.tsx` | +| SFR-05 | `src/features/risk-assessment/RiskMap.tsx` | +| SFR-06 | `src/features/risk-assessment/EnforcementPlan.tsx` | +| SFR-07 | `src/features/patrol/PatrolRoute.tsx` | +| SFR-08 | `src/features/patrol/FleetOptimization.tsx` | +| SFR-09 | `src/features/detection/DarkVesselDetection.tsx` | +| SFR-10 | `src/features/detection/GearDetection.tsx`, `src/features/detection/GearIdentification.tsx` | +| SFR-11 | `src/features/enforcement/EnforcementHistory.tsx`, `src/features/enforcement/EventList.tsx` | +| SFR-12 | `src/features/dashboard/Dashboard.tsx`, `src/features/monitoring/MonitoringDashboard.tsx` | +| SFR-13 | `src/features/statistics/Statistics.tsx` | +| SFR-14 | `src/features/statistics/ExternalService.tsx` | +| SFR-15 | `src/features/field-ops/MobileService.tsx` | +| SFR-16 | `src/features/field-ops/ShipAgent.tsx` | +| SFR-17 | `src/features/field-ops/AIAlert.tsx` | +| SFR-18 | `src/features/ai-operations/MLOpsPage.tsx` | +| SFR-19 | `src/features/ai-operations/MLOpsPage.tsx` (LLMOps 탭) | +| SFR-20 | `src/features/ai-operations/AIAssistant.tsx` | + +--- + +## 부록: 공통 컴포넌트 매핑 + +| 컴포넌트 | 파일 경로 | 관련 SFR | +|----------|----------|---------| +| DataTable | `src/shared/components/common/DataTable.tsx` | SFR-02, SFR-11, SFR-13 | +| ExcelExport | `src/shared/components/common/ExcelExport.tsx` | SFR-02 | +| Pagination | `src/shared/components/common/Pagination.tsx` | SFR-02 | +| SearchInput | `src/shared/components/common/SearchInput.tsx` | SFR-02 | +| FileUpload | `src/shared/components/common/FileUpload.tsx` | SFR-02 | +| PrintButton | `src/shared/components/common/PrintButton.tsx` | SFR-02, SFR-05 | +| SaveButton | `src/shared/components/common/SaveButton.tsx` | SFR-02 | +| NotificationBanner | `src/shared/components/common/NotificationBanner.tsx` | SFR-02 | +| PageToolbar | `src/shared/components/common/PageToolbar.tsx` | SFR-02 | +| Card / Badge | `src/shared/components/ui/card.tsx`, `badge.tsx` | 전체 SFR 공통 | + +--- + +## 부록: Zustand Store 매핑 + +| Store | 파일 경로 | 관련 SFR | +|-------|----------|---------| +| kpiStore | `src/stores/kpiStore.ts` | SFR-12 | +| vesselStore | `src/stores/vesselStore.ts` | SFR-09, SFR-12 | +| eventStore | `src/stores/eventStore.ts` | SFR-11, SFR-12 | +| enforcementStore | `src/stores/enforcementStore.ts` | SFR-06, SFR-11 | +| patrolStore | `src/stores/patrolStore.ts` | SFR-07, SFR-08 | +| gearStore | `src/stores/gearStore.ts` | SFR-10 | +| transferStore | `src/stores/transferStore.ts` | SFR-09 | +| settingsStore | `src/stores/settingsStore.ts` | SFR-01, SFR-02 (테마/언어 설정) | + +--- + +## 부록: 2차 개발 단계 우선순위 제안 + +아래는 백엔드 연동 및 실제 기능 구현 시 권장하는 우선순위이다. + +### 1순위 (핵심 인프라) +| SFR | 항목 | 사유 | +|-----|------|------| +| SFR-01 | SSO/GPKI 인증 연동 | 시스템 접근 보안의 기본 요건 | +| SFR-03 | 데이터 수집 파이프라인 | 전체 시스템의 데이터 기반 | +| SFR-02 | 공통 기능 백엔드 | CRUD, 파일관리 등 기반 기능 | + +### 2순위 (AI 핵심 기능) +| SFR | 항목 | 사유 | +|-----|------|------| +| SFR-04 | AI 예측 모델 연동 | 시스템 핵심 가치 | +| SFR-09 | 패턴 탐지 엔진 | 불법조업 감시 핵심 | +| SFR-05 | 위험도 지도 실시간화 | 예측 결과 시각화 | + +### 3순위 (운용 기능) +| SFR | 항목 | 사유 | +|-----|------|------| +| SFR-07 | 단일 함정 경로 최적화 | 현장 운용 직결 | +| SFR-08 | 다함정 협력 최적화 | 함대 운용 효율화 | +| SFR-06 | 단속 계획·경보 | 예측→대응 연계 | +| SFR-12 | 대시보드 실시간화 | 상황 인식 핵심 | + +### 4순위 (확장 기능) +| SFR | 항목 | 사유 | +|-----|------|------| +| SFR-18 | MLOps 플랫폼 | AI 모델 운영 자동화 | +| SFR-19 | LLMOps | LLM 서비스 운영 | +| SFR-20 | AI Q&A 서비스 | 의사결정 지원 | +| SFR-10 | 어구 탐지 | 전문 탐지 모델 | +| SFR-11 | 이력 관리 DB | 데이터 축적 | +| SFR-13 | 통계 백엔드 | 성과 분석 | +| SFR-14 | 외부 연계 API | 유관기관 협업 | +| SFR-15 | 모바일 앱 | 현장 대응 | +| SFR-16 | 함정 Agent | 함정 단말 | +| SFR-17 | AI 알림 발송 | 즉각 대응 | diff --git a/docs/sfr-user-guide.md b/docs/sfr-user-guide.md new file mode 100644 index 0000000..730a10b --- /dev/null +++ b/docs/sfr-user-guide.md @@ -0,0 +1,871 @@ +# SFR 요구사항별 화면 사용 가이드 + +> **문서 작성일:** 2026-04-06 +> **시스템 버전:** v0.1.0 (프로토타입) +> **다국어:** 한국어/영어 전환 지원 (헤더 우측 EN/한국어 버튼) +> **테마:** 다크/라이트 전환 지원 (헤더 우측 해/달 아이콘 버튼) + +--- + +## 문서 안내 + +이 문서는 **KCG AI 모니터링 시스템**의 각 SFR(소프트웨어 기능 요구사항)이 화면에서 어떻게 구현되어 있는지를 **비개발자**(일반 사용자, 사업 PM, 산출물 작성자)가 이해할 수 있도록 정리한 가이드입니다. + +현재 시스템은 **프로토타입 단계(v0.1.0)**로, 모든 SFR의 UI가 완성되어 있으나 백엔드 서버 연동은 아직 이루어지지 않았습니다. 화면에 표시되는 데이터는 시연용 샘플 데이터입니다. + +--- + +## 목차 + +- [SFR-01: 사용자 인증 및 접근 제어](#sfr-01-사용자-인증-및-접근-제어) +- [SFR-02: 시스템 공통기능 및 환경설정](#sfr-02-시스템-공통기능-및-환경설정) +- [SFR-03: 데이터 수집 허브](#sfr-03-데이터-수집-허브) +- [SFR-04: AI 모델 관리](#sfr-04-ai-모델-관리) +- [SFR-05: 위험도 분석 지도](#sfr-05-위험도-분석-지도) +- [SFR-06: 단속 계획 및 경보](#sfr-06-단속-계획-및-경보) +- [SFR-07: 단일함정 순찰경로 최적화](#sfr-07-단일함정-순찰경로-최적화) +- [SFR-08: 다함정 경로 최적화](#sfr-08-다함정-경로-최적화) +- [SFR-09: Dark Vessel 탐지](#sfr-09-dark-vessel-탐지) +- [SFR-10: 어망/어구 탐지](#sfr-10-어망어구-탐지) +- [SFR-11: 단속/탐지 이력 관리](#sfr-11-단속탐지-이력-관리) +- [SFR-12: 종합 상황판 및 경보 현황판](#sfr-12-종합-상황판-및-경보-현황판) +- [SFR-13: 통계 및 성과 분석](#sfr-13-통계-및-성과-분석) +- [SFR-14: 외부 서비스 연계](#sfr-14-외부-서비스-연계) +- [SFR-15: 모바일 서비스](#sfr-15-모바일-서비스) +- [SFR-16: 함정 Agent](#sfr-16-함정-agent) +- [SFR-17: AI 알림 발송](#sfr-17-ai-알림-발송) +- [SFR-18/19: MLOps 플랫폼](#sfr-1819-mlops-플랫폼) +- [SFR-20: AI 의사결정 지원](#sfr-20-ai-의사결정-지원) + +--- + +## SFR-01: 사용자 인증 및 접근 제어 + +### 로그인 + +**메뉴 위치:** 시스템 접속 시 최초 화면 +**URL:** `/login` +**접근 권한:** 모든 사용자 + +**화면 설명:** +시스템에 접속하기 위한 로그인 화면입니다. 사용자 ID와 비밀번호를 입력하여 로그인할 수 있으며, 역할별로 5개의 데모 계정이 제공됩니다. + +**주요 기능:** +- 사용자 ID/비밀번호 입력을 통한 로그인 +- 역할별 데모 계정 선택 (ADMIN, OPERATOR, ANALYST, FIELD, VIEWER) +- 로그인 후 역할에 따른 메뉴 접근 제어 + +**구현 완료:** +- ✅ 로그인 화면 UI 및 데모 계정 5종 로그인 기능 +- ✅ 역할 기반 세션 유지 및 메뉴 접근 제어 + +**향후 구현 예정:** +- 🔲 SSO(Single Sign-On) 연동 +- 🔲 GPKI(정부 공인인증서) 인증 연동 +- 🔲 실제 사용자 DB 연동 및 비밀번호 암호화 + +**보완 필요:** +- ⚠️ 현재 데모 계정은 하드코딩되어 있으며, 운영 환경에서는 실제 인증 체계로 대체 필요 + +--- + +### 권한 관리 + +**메뉴 위치:** 시스템 관리 > 권한 관리 +**URL:** `/access-control` +**접근 권한:** ADMIN + +**화면 설명:** +시스템 사용자의 역할(Role)과 권한을 관리하는 화면입니다. RBAC(역할 기반 접근 제어) 방식으로 5가지 역할을 정의하고 각 역할별 메뉴 접근 권한을 설정할 수 있습니다. + +**주요 기능:** +- 5가지 역할(ADMIN, OPERATOR, ANALYST, FIELD, VIEWER) 조회 +- 역할별 접근 가능 메뉴 및 기능 권한 설정 +- 사용자 목록 조회 및 역할 할당 + +**구현 완료:** +- ✅ RBAC 5역할 체계 UI 및 역할별 권한 매트릭스 표시 +- ✅ 권한 설정 화면 레이아웃 및 인터랙션 + +**향후 구현 예정:** +- 🔲 실제 사용자 DB 연동을 통한 권한 CRUD +- 🔲 감사 로그(권한 변경 이력) 기록 + +**보완 필요:** +- ⚠️ 현재 화면의 데이터는 샘플이며 실제 저장/반영되지 않음 + +--- + +## SFR-02: 시스템 공통기능 및 환경설정 + +### 환경설정 + +**메뉴 위치:** 시스템 관리 > 환경설정 +**URL:** `/system-config` +**접근 권한:** ADMIN, OPERATOR + +**화면 설명:** +시스템 운영에 필요한 기준정보를 조회하고 관리하는 화면입니다. 875건의 기준정보 항목이 표시되며, 검색 및 필터링이 가능합니다. + +**주요 기능:** +- 기준정보 875건 목록 조회 및 검색 +- 카테고리별 필터링 +- 설정값 수정 UI + +**구현 완료:** +- ✅ 875건 기준정보 목록 표시 및 검색/필터 UI +- ✅ 공통 컴포넌트 적용 (DataTable, 엑셀 내보내기, 인쇄 기능) + +**향후 구현 예정:** +- 🔲 기준정보 DB 연동 (조회/수정/저장) +- 🔲 설정 변경 이력 관리 + +**보완 필요:** +- ⚠️ 현재 표시 데이터는 샘플이며, DB 연동 후 실제 운영 데이터로 교체 필요 + +--- + +### 공지사항 + +**메뉴 위치:** 시스템 관리 > 공지사항 +**URL:** `/notices` +**접근 권한:** ADMIN (작성/수정/삭제), 전체 역할 (조회) + +**화면 설명:** +시스템 운영 관련 공지사항을 등록하고 조회하는 게시판입니다. + +**주요 기능:** +- 공지사항 목록 조회 +- 공지사항 작성, 수정, 삭제 (CRUD) +- 공지사항 상세 조회 + +**구현 완료:** +- ✅ 공지사항 CRUD UI 완성 +- ✅ 목록/상세/작성/수정 화면 전환 + +**향후 구현 예정:** +- 🔲 공지사항 DB 연동 +- 🔲 첨부파일 업로드/다운로드 + +**보완 필요:** +- ⚠️ 현재 작성한 공지사항은 새로고침 시 초기화됨 + +--- + +### 이벤트 목록 + +**메뉴 위치:** 단속/이력 > 이벤트 목록 +**URL:** `/event-list` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +시스템에서 발생한 각종 이벤트(탐지, 경보, 알림 등)를 통합 목록으로 조회하는 화면입니다. + +**주요 기능:** +- 이벤트 유형별 필터링 및 검색 +- 이벤트 상세 정보 조회 +- 이벤트 목록 엑셀 내보내기 + +**구현 완료:** +- ✅ 이벤트 통합 목록 UI 및 필터/검색 +- ✅ DataTable 공통 컴포넌트 적용 (정렬, 페이징, 엑셀, 인쇄) + +**향후 구현 예정:** +- 🔲 실시간 이벤트 수신 및 목록 자동 갱신 +- 🔲 이벤트 DB 연동 + +**보완 필요:** +- ⚠️ 현재 목록은 샘플 데이터이며, 실시간 연동 후 자동 업데이트 필요 + +--- + +## SFR-03: 데이터 수집 허브 + +**메뉴 위치:** 시스템 관리 > 데이터 허브 +**URL:** `/data-hub` +**접근 권한:** ADMIN, OPERATOR + +**화면 설명:** +외부 데이터 소스(AIS, LRIT, SAR, CCTV, VMS 등 5개 신호원)로부터 데이터를 수집하는 현황을 모니터링하는 화면입니다. 22개 수집 채널의 상태를 실시간으로 확인할 수 있습니다. + +**주요 기능:** +- 5개 신호원별 수집 상태 대시보드 +- 22개 수집 채널별 연결 상태, 수신량, 오류율 조회 +- 채널별 상세 수집 이력 확인 + +**구현 완료:** +- ✅ 5개 신호원 + 22개 수집 채널 현황 대시보드 UI +- ✅ 채널별 상태(정상/경고/오류) 시각화 + +**향후 구현 예정:** +- 🔲 실제 데이터 수집 엔진 연동 +- 🔲 수집 채널 실시간 상태 모니터링 +- 🔲 수집 데이터 품질 검증 기능 + +**보완 필요:** +- ⚠️ 현재 모든 수집 현황은 샘플 데이터이며, 실제 신호원 연결 후 실데이터로 교체 필요 + +--- + +## SFR-04: AI 모델 관리 + +**메뉴 위치:** 시스템 관리 > AI 모델관리 +**URL:** `/ai-model` +**접근 권한:** ADMIN, OPERATOR + +**화면 설명:** +AI 탐지 모델의 버전, 탐지 규칙, 입력 피처(Feature), 학습 파이프라인을 관리하는 화면입니다. 모델의 성능을 확인하고 운영 중인 모델을 교체하거나 새 버전을 등록할 수 있습니다. + +**주요 기능:** +- AI 모델 5개 버전 목록 및 상세 정보 조회 +- 6개 탐지 규칙 관리 (임계값, 조건 설정) +- 20개 입력 피처 목록 및 중요도 확인 +- 학습 파이프라인 실행 현황 조회 + +**구현 완료:** +- ✅ 모델 버전 관리 UI (5버전 목록, 성능지표 비교) +- ✅ 탐지 규칙 6건 설정 화면 +- ✅ 피처 20건 목록 및 중요도 시각화 +- ✅ 파이프라인 현황 표시 + +**향후 구현 예정:** +- 🔲 실제 AI 모델 서버 연동 (모델 등록/배포/롤백) +- 🔲 모델 학습 파이프라인 실행 연동 +- 🔲 모델 성능 모니터링 실시간 연동 + +**보완 필요:** +- ⚠️ 현재 모델 성능 지표는 샘플 데이터이며, 실제 모델 연동 후 정확한 지표로 교체 필요 + +--- + +## SFR-05: 위험도 분석 지도 + +**메뉴 위치:** 탐지/분석 > 위험도 지도 +**URL:** `/risk-map` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +해역별 불법조업 위험도를 히트맵(열지도) 형태로 시각화한 화면입니다. 10x18 격자로 분할된 관할 해역의 위험도를 색상으로 표현하며, MTIS 해양사고 통계를 6개 탭으로 분류하여 제공합니다. + +**주요 기능:** +- 10x18 격자 기반 위험도 히트맵 지도 표시 +- 격자별 상세 위험도 정보 팝업 +- MTIS 해양사고 통계 6탭 (사고유형별, 해역별, 월별 등) +- 기간별 위험도 변화 추이 확인 + +**구현 완료:** +- ✅ 위험도 히트맵 10x18 격자 시각화 +- ✅ MTIS 해양사고 통계 6탭 UI + +**향후 구현 예정:** +- 🔲 AI 위험도 예측 모델 연동 +- 🔲 실시간 AIS/VMS 데이터 기반 동적 위험도 갱신 +- 🔲 과거 데이터 기반 위험도 예측 기능 + +**보완 필요:** +- ⚠️ 현재 히트맵 데이터는 샘플이며, 예측 모델 연동 후 정확한 위험도로 교체 필요 + +--- + +## SFR-06: 단속 계획 및 경보 + +**메뉴 위치:** 탐지/분석 > 단속 계획/경보 +**URL:** `/enforcement-plan` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +불법조업 단속 계획을 수립하고, 경보 발생 조건(임계값)을 설정하는 화면입니다. 현재 등록된 5건의 단속 계획과 경보 조건을 확인하고 관리할 수 있습니다. + +**주요 기능:** +- 단속 계획 5건 목록 조회 및 상세 확인 +- 신규 단속 계획 작성 및 수정 +- 경보 임계값(위험도 기준, 선박 수 기준 등) 설정 +- 단속 계획별 투입 함정/인력 배치 확인 + +**구현 완료:** +- ✅ 단속 계획 5건 목록/상세 UI +- ✅ 경보 임계값 설정 화면 + +**향후 구현 예정:** +- 🔲 AI 기반 단속 계획 자동 추천 +- 🔲 경보 임계값 도달 시 자동 경보 발생 +- 🔲 단속 계획 DB 연동 + +**보완 필요:** +- ⚠️ 현재 단속 계획 데이터는 샘플이며, 자동 추천 및 경보 기능은 AI 모델 연동 후 구현 필요 + +--- + +## SFR-07: 단일함정 순찰경로 최적화 + +**메뉴 위치:** 현장 대응 > 단일함정 순찰경로 +**URL:** `/patrol-route` +**접근 권한:** ADMIN, OPERATOR, FIELD + +**화면 설명:** +개별 함정(경비함)의 순찰 경로를 지도 위에 표시하고, AI가 최적화한 경로를 제안하는 화면입니다. 4척의 함정별 경로와 3가지 시나리오(최단거리, 위험지역 우선, 연료절감)를 비교할 수 있습니다. + +**주요 기능:** +- 4척 함정별 순찰 경로 지도 표시 +- 3가지 시나리오별 경로 비교 +- 경유지 추가/제거를 통한 수동 경로 조정 +- 예상 소요시간, 연료소모량, 해역 커버리지 정보 표시 + +**구현 완료:** +- ✅ 함정 4척 경로 지도 시각화 +- ✅ 3가지 시나리오 비교 UI + +**향후 구현 예정:** +- 🔲 AI 경로 최적화 엔진 연동 +- 🔲 실시간 함정 위치 반영 +- 🔲 기상/해황 정보 반영 경로 재계산 + +**보완 필요:** +- ⚠️ 현재 경로는 샘플 데이터이며, AI 최적화 엔진 연동 후 실제 최적 경로 제공 필요 + +--- + +## SFR-08: 다함정 경로 최적화 + +**메뉴 위치:** 현장 대응 > 다함정 경로최적화 +**URL:** `/fleet-optimization` +**접근 권한:** ADMIN, OPERATOR + +**화면 설명:** +여러 함정을 동시에 운용할 때 전체 해역 커버리지를 최대화하는 최적 경로를 계산하는 화면입니다. 5척의 함정이 6개 구역을 효율적으로 분담 순찰하도록 배치합니다. + +**주요 기능:** +- 5척 함정의 담당 구역 배정 및 지도 시각화 +- 6개 구역별 커버리지율 표시 +- 전체 해역 커버리지 최적화 결과 확인 +- 함정별 순찰 스케줄 표시 + +**구현 완료:** +- ✅ 5척 함정 배치 및 6구역 커버리지 시각화 +- ✅ 구역별 커버리지율 표시 UI + +**향후 구현 예정:** +- 🔲 AI 다함정 경로 최적화 알고리즘 연동 +- 🔲 실시간 함정 위치 기반 동적 재배치 +- 🔲 기상/해황 조건 반영 최적화 + +**보완 필요:** +- ⚠️ 현재 배치 결과는 샘플 데이터이며, AI 최적화 알고리즘 연동 후 실제 최적 배치로 교체 필요 + +--- + +## SFR-09: Dark Vessel 탐지 + +**메뉴 위치:** 탐지/분석 > Dark Vessel 탐지 +**URL:** `/dark-vessel` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +AIS(선박자동식별장치) 신호를 의도적으로 끈 의심 선박(Dark Vessel)을 탐지하는 화면입니다. 7척의 의심 선박과 5가지 행동 패턴 분석 결과를 확인할 수 있습니다. + +**주요 기능:** +- Dark Vessel 의심 선박 7척 목록 및 지도 표시 +- 5가지 의심 패턴(AIS 소실, 속도 급변, 해역 이탈 등) 분석 결과 +- 의심 선박 상세 프로필 및 이동 궤적 조회 +- 위험도 등급별 분류 표시 + +**구현 완료:** +- ✅ 의심 선박 7척 목록/지도 시각화 +- ✅ 5가지 행동 패턴 분석 결과 UI + +**향후 구현 예정:** +- 🔲 AI Dark Vessel 탐지 엔진 연동 +- 🔲 실시간 AIS 데이터 분석 연동 +- 🔲 SAR(위성영상) 기반 탐지 연동 + +**보완 필요:** +- ⚠️ 현재 탐지 결과는 샘플 데이터이며, AI 탐지 엔진 연동 후 실시간 탐지 결과로 교체 필요 + +--- + +### 중국어선 분석 + +**메뉴 위치:** 탐지/분석 > 중국어선 분석 +**URL:** `/china-fishing` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +중국 어선의 불법조업 활동을 집중 분석하는 화면입니다. Dark Vessel 탐지(SFR-09) 및 어구 탐지(SFR-10) 결과를 중국 어선에 특화하여 종합 분석합니다. + +**주요 기능:** +- 중국 어선 활동 현황 지도 표시 +- 불법조업 의심 어선 목록 조회 +- 해역별 중국 어선 밀집도 분석 +- 시계열 활동 패턴 분석 + +**구현 완료:** +- ✅ 중국 어선 분석 종합 대시보드 UI +- ✅ 지도 기반 활동 현황 시각화 + +**향후 구현 예정:** +- 🔲 AI 탐지 엔진 연동 (Dark Vessel + 어구 탐지 통합) +- 🔲 실시간 데이터 기반 분석 갱신 + +**보완 필요:** +- ⚠️ 현재 분석 데이터는 샘플이며, 실제 탐지 엔진 연동 필요 + +--- + +## SFR-10: 어망/어구 탐지 + +**메뉴 위치:** 탐지/분석 > 어망/어구 탐지 +**URL:** `/gear-detection` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +불법 어망 및 어구를 탐지하고 분류하는 화면입니다. 6건의 탐지 결과와 어구 종류를 식별하는 결정트리(Decision Tree) 분석 결과를 제공합니다. + +**주요 기능:** +- 어구 탐지 6건 목록 및 지도 표시 +- 어구 종류별 식별 결정트리 시각화 +- 탐지 결과 상세 정보 (위치, 크기, 어구 유형, 위험도) +- 탐지 이미지 확인 + +**구현 완료:** +- ✅ 어구 6건 탐지 결과 목록/지도 UI +- ✅ 어구 식별 결정트리 시각화 + +**향후 구현 예정:** +- 🔲 AI 어구 탐지 모델 연동 (영상 분석 기반) +- 🔲 실시간 CCTV/SAR 영상 분석 연동 +- 🔲 탐지 결과 자동 분류 및 알림 + +**보완 필요:** +- ⚠️ 현재 탐지 결과는 샘플 데이터이며, AI 탐지 모델 연동 후 실제 탐지 결과로 교체 필요 + +--- + +## SFR-11: 단속/탐지 이력 관리 + +**메뉴 위치:** 단속/이력 > 단속/탐지 이력 +**URL:** `/enforcement-history` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +과거 단속 및 탐지 활동의 이력을 조회하고 관리하는 화면입니다. 6건의 단속 이력과 AI 매칭 검증 결과를 확인할 수 있습니다. + +**주요 기능:** +- 단속 이력 6건 목록 조회 (날짜, 위치, 대상 선박, 결과) +- AI 매칭 검증 — 탐지 결과와 실제 단속 결과 비교 분석 +- 이력 상세 정보 조회 및 검색/필터 +- 이력 데이터 엑셀 내보내기 + +**구현 완료:** +- ✅ 단속 이력 6건 목록/상세 UI +- ✅ AI 매칭 검증 결과 표시 + +**향후 구현 예정:** +- 🔲 단속 이력 DB 연동 (조회/등록/수정) +- 🔲 AI 매칭 검증 엔진 연동 +- 🔲 탐지-단속 연계 자동 분석 + +**보완 필요:** +- ⚠️ 현재 이력 데이터는 샘플이며, DB 연동 후 실제 단속 데이터로 교체 필요 + +--- + +## SFR-12: 종합 상황판 및 경보 현황판 + +### 종합 상황판 + +**메뉴 위치:** 모니터링 > 종합 상황판 +**URL:** `/dashboard` +**접근 권한:** ADMIN, OPERATOR, ANALYST, FIELD, VIEWER + +**화면 설명:** +시스템의 전체 운영 현황을 한눈에 파악할 수 있는 종합 대시보드입니다. KPI(핵심 성과 지표), 해역 위험도 히트맵, 이벤트 타임라인, 함정 배치 현황 등이 통합 표시됩니다. + +**주요 기능:** +- KPI 카드 (탐지 건수, 정확도, 대응 시간 등) 표시 +- 해역 위험도 히트맵 축소판 +- 최근 이벤트 타임라인 +- 함정 배치 현황 요약 +- 실시간 경보 알림 표시 + +**구현 완료:** +- ✅ KPI 카드 + 히트맵 + 타임라인 + 함정 현황 통합 대시보드 UI +- ✅ 반응형 레이아웃 (화면 크기에 따른 자동 배치) + +**향후 구현 예정:** +- 🔲 실시간 데이터 연동 (WebSocket 등) +- 🔲 KPI 수치 실시간 갱신 +- 🔲 히트맵/타임라인 실시간 업데이트 + +**보완 필요:** +- ⚠️ 현재 모든 수치는 샘플 데이터이며, 실시간 연동 후 정확한 운영 데이터로 교체 필요 + +--- + +### 경보 현황판 + +**메뉴 위치:** 모니터링 > 경보 현황판 +**URL:** `/monitoring` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +현재 발생 중인 경보를 중심으로 긴급 상황을 모니터링하는 화면입니다. 경보 등급별 분류, 미처리 경보 목록, 경보 상세 정보를 확인할 수 있습니다. + +**주요 기능:** +- 경보 등급별(긴급/경고/주의/정보) 현황 표시 +- 미처리 경보 목록 및 상세 조회 +- 경보 처리(확인/대응/종결) 워크플로우 +- 경보 발생 이력 조회 + +**구현 완료:** +- ✅ 경보 등급별 현황판 UI +- ✅ 경보 목록/상세 조회 화면 + +**향후 구현 예정:** +- 🔲 실시간 경보 수신 연동 +- 🔲 경보 처리 워크플로우 DB 연동 +- 🔲 경보 자동 에스컬레이션 + +**보완 필요:** +- ⚠️ 현재 경보 데이터는 샘플이며, 실시간 연동 후 실제 경보 데이터로 교체 필요 + +--- + +### 실시간 감시 + +**메뉴 위치:** 모니터링 > 실시간 감시 +**URL:** `/events` +**접근 권한:** ADMIN, OPERATOR, ANALYST, FIELD + +**화면 설명:** +실시간 지도(LiveMap)를 통해 해역 상황을 감시하는 화면입니다. 선박 위치, 이벤트 발생 지점, 함정 위치 등을 실시간으로 확인할 수 있습니다. + +**주요 기능:** +- 실시간 지도 기반 선박/함정 위치 표시 +- 이벤트 발생 시 지도 상 알림 표시 +- 선박/이벤트 클릭 시 상세 정보 팝업 +- 지도 확대/축소 및 해역 필터링 + +**구현 완료:** +- ✅ LiveMap 기반 실시간 감시 지도 UI +- ✅ 선박/이벤트 마커 및 팝업 인터랙션 + +**향후 구현 예정:** +- 🔲 실시간 AIS/VMS 데이터 연동 +- 🔲 WebSocket 기반 실시간 위치 업데이트 +- 🔲 이벤트 발생 시 자동 지도 포커스 이동 + +**보완 필요:** +- ⚠️ 현재 선박 위치는 샘플 데이터이며, 실시간 데이터 연동 필요 + +--- + +### 해역 통제 + +**메뉴 위치:** 모니터링 > 해역 통제 +**URL:** `/map-control` +**접근 권한:** ADMIN, OPERATOR + +**화면 설명:** +특정 해역에 대한 통제 구역을 설정하고 관리하는 지도 기반 화면입니다. 통제 구역 진입 선박을 감시할 수 있습니다. + +**주요 기능:** +- 통제 구역 지도 상 설정 및 표시 +- 통제 구역 진입/이탈 선박 모니터링 +- 통제 구역 관리 (생성/수정/삭제) + +**구현 완료:** +- ✅ 지도 기반 해역 통제 구역 표시 UI +- ✅ 통제 구역 관리 인터페이스 + +**향후 구현 예정:** +- 🔲 실시간 선박 위치 기반 진입 감시 +- 🔲 통제 구역 위반 자동 경보 + +**보완 필요:** +- ⚠️ 현재 통제 구역 데이터는 샘플이며, 실시간 연동 필요 + +--- + +## SFR-13: 통계 및 성과 분석 + +**메뉴 위치:** 통계/보고 > 통계/성과 분석 +**URL:** `/statistics` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +시스템의 운영 성과를 통계적으로 분석하는 화면입니다. 월별 추이 그래프와 5개 KPI(탐지 정확도 93.2%, 오탐율 7.8% 등)를 제공합니다. + +**주요 기능:** +- 월별 탐지/단속 추이 그래프 +- KPI 5개 지표 대시보드 (탐지 정확도, 오탐율, 대응 시간, 커버리지, 가동률) +- 기간별/해역별/유형별 필터링 +- 통계 데이터 엑셀 내보내기 및 인쇄 + +**구현 완료:** +- ✅ 월별 추이 차트 및 KPI 5개 대시보드 UI +- ✅ 필터링 및 엑셀 내보내기/인쇄 기능 + +**향후 구현 예정:** +- 🔲 통계 데이터 DB 연동 +- 🔲 실제 운영 데이터 기반 KPI 자동 산출 +- 🔲 맞춤형 보고서 생성 기능 + +**보완 필요:** +- ⚠️ 현재 KPI 수치(정확도 93.2%, 오탐율 7.8% 등)는 샘플 데이터이며, 실제 운영 데이터 기반으로 교체 필요 + +--- + +### 보고서 관리 + +**메뉴 위치:** 통계/보고 > 보고서 관리 +**URL:** `/reports` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +시스템 운영 보고서를 생성하고 관리하는 화면입니다. 정기/비정기 보고서를 작성하고 조회할 수 있습니다. + +**주요 기능:** +- 보고서 목록 조회 및 검색 +- 보고서 작성/수정/삭제 +- 보고서 템플릿 관리 + +**구현 완료:** +- ✅ 보고서 관리 UI 및 목록/상세 화면 + +**향후 구현 예정:** +- 🔲 보고서 DB 연동 +- 🔲 통계 데이터 자동 삽입 +- 🔲 PDF 보고서 생성 및 다운로드 + +**보완 필요:** +- ⚠️ 현재 보고서 데이터는 샘플이며, DB 연동 필요 + +--- + +## SFR-14: 외부 서비스 연계 + +**메뉴 위치:** 시스템 관리 > 외부 서비스 연계 +**URL:** `/external-service` +**접근 권한:** ADMIN + +**화면 설명:** +외부 시스템과의 API 연계 현황을 관리하는 화면입니다. 5개 외부 서비스(해양경찰청 통합시스템, GICOMS, 해양교통관제 등)의 API 연결 상태를 확인할 수 있습니다. + +**주요 기능:** +- 외부 서비스 5건 연계 현황 대시보드 +- 서비스별 API 연결 상태(정상/오류) 확인 +- API 호출 이력 및 오류 로그 조회 +- 연계 설정 관리 + +**구현 완료:** +- ✅ 외부 서비스 5건 연계 현황 UI +- ✅ API 상태 표시 대시보드 + +**향후 구현 예정:** +- 🔲 실제 외부 API 연동 구현 +- 🔲 API 헬스체크 자동화 +- 🔲 장애 시 자동 알림 및 재연결 + +**보완 필요:** +- ⚠️ 현재 API 상태는 샘플 데이터이며, 실제 API 연동 후 실시간 상태 표시 필요 + +--- + +## SFR-15: 모바일 서비스 + +**메뉴 위치:** 현장 대응 > 모바일 서비스 +**URL:** `/mobile-service` +**접근 권한:** ADMIN, OPERATOR, FIELD + +**화면 설명:** +현장 요원이 사용할 모바일 앱의 화면을 웹에서 미리보기(프리뷰)하는 시뮬레이션 화면입니다. 실제 모바일 앱이 아닌 웹 기반 시뮬레이션으로, 모바일 앱의 기능과 화면 구성을 확인할 수 있습니다. + +**주요 기능:** +- 모바일 앱 화면 프리뷰 (웹 시뮬레이션) +- 주요 기능 화면 미리보기 (경보 수신, 위치 공유, 보고서 작성 등) +- 모바일 레이아웃 시뮬레이션 + +**구현 완료:** +- ✅ 모바일 앱 프리뷰 화면 (웹 시뮬레이션) +- ✅ 주요 기능별 화면 미리보기 + +**향후 구현 예정:** +- 🔲 실제 모바일 앱(iOS/Android) 개발 +- 🔲 Push 알림 연동 +- 🔲 오프라인 모드 지원 + +**보완 필요:** +- ⚠️ 현재는 웹 시뮬레이션으로, 실제 모바일 앱과 UI/UX가 다를 수 있음 + +--- + +## SFR-16: 함정 Agent + +**메뉴 위치:** 현장 대응 > 함정 Agent +**URL:** `/ship-agent` +**접근 권한:** ADMIN, OPERATOR + +**화면 설명:** +각 경비함에 설치되는 Agent 소프트웨어의 상태를 모니터링하는 화면입니다. 6건의 함정 Agent 상태(온라인/오프라인, 버전, 마지막 통신 시간 등)를 확인할 수 있습니다. + +**주요 기능:** +- 함정 Agent 6건 상태 목록 조회 +- Agent별 온라인/오프라인 상태 확인 +- Agent 소프트웨어 버전 및 업데이트 현황 +- 마지막 통신 시간 및 통신 이력 확인 + +**구현 완료:** +- ✅ 함정 Agent 6건 상태 모니터링 UI +- ✅ 상태별 시각적 표시 (온라인/오프라인) + +**향후 구현 예정:** +- 🔲 실제 Agent 소프트웨어 개발 및 배포 +- 🔲 Agent 실시간 통신 연동 +- 🔲 Agent 원격 업데이트 기능 + +**보완 필요:** +- ⚠️ 현재 Agent 상태는 샘플 데이터이며, 실제 Agent SW 개발 후 연동 필요 + +--- + +## SFR-17: AI 알림 발송 + +**메뉴 위치:** 현장 대응 > AI 알림 발송 +**URL:** `/ai-alert` +**접근 권한:** ADMIN, OPERATOR + +**화면 설명:** +AI가 분석한 결과를 기반으로 관련 담당자에게 알림을 발송하는 화면입니다. 5건의 알림 전송 현황을 확인하고 새로운 알림을 발송할 수 있습니다. + +**주요 기능:** +- AI 알림 5건 전송 현황 조회 +- 알림 유형별(긴급/일반/정보) 분류 +- 알림 수신자 설정 및 발송 +- 알림 전송 결과(성공/실패) 확인 + +**구현 완료:** +- ✅ 알림 5건 전송 현황 UI +- ✅ 알림 유형별 분류 및 상세 조회 + +**향후 구현 예정:** +- 🔲 실제 알림 발송 기능 구현 (SMS, 이메일, Push 등) +- 🔲 AI 분석 결과 기반 자동 알림 트리거 +- 🔲 알림 발송 이력 DB 연동 + +**보완 필요:** +- ⚠️ 현재 알림은 실제 발송되지 않으며, 발송 채널(SMS/이메일/Push) 연동 필요 + +--- + +## SFR-18/19: MLOps 플랫폼 + +**메뉴 위치:** 시스템 관리 > MLOps +**URL:** `/mlops` +**접근 권한:** ADMIN + +**화면 설명:** +AI 모델의 전체 생명주기(실험, 학습, 배포, 모니터링)를 관리하는 MLOps 플랫폼 화면입니다. 7개 탭으로 구성되어 실험 관리, 모델 레지스트리, 배포 관리, API 서빙, LLMOps 등의 기능을 제공합니다. + +**주요 기능:** +- 실험 관리 — AI 모델 학습 실험 기록 및 비교 +- 모델 레지스트리 — 학습된 모델 버전 관리 +- 배포 관리 — 모델 서빙 환경 배포 및 모니터링 +- API 서빙 — 모델 추론 API 관리 +- LLMOps — 대규모 언어 모델(LLM) 운영 관리 +- 모니터링 — 모델 성능 드리프트 감시 +- 파이프라인 — 자동화된 학습/배포 파이프라인 관리 + +**구현 완료:** +- ✅ MLOps 7탭 UI (실험/모델/배포/API/LLMOps/모니터링/파이프라인) +- ✅ 각 탭별 상세 관리 화면 + +**향후 구현 예정:** +- 🔲 실제 MLOps 인프라 연동 (MLflow, Kubeflow 등) +- 🔲 모델 학습/배포 파이프라인 자동화 +- 🔲 모델 성능 모니터링 실시간 연동 +- 🔲 LLM 운영 관리 기능 연동 + +**보완 필요:** +- ⚠️ 현재 모든 데이터는 샘플이며, MLOps 인프라 구축 후 실제 데이터로 교체 필요 + +--- + +## SFR-20: AI 의사결정 지원 + +**메뉴 위치:** 시스템 관리 > AI 의사결정 지원 +**URL:** `/ai-assistant` +**접근 권한:** ADMIN, OPERATOR, ANALYST + +**화면 설명:** +AI에게 질문하고 답변을 받을 수 있는 대화형(채팅) 인터페이스입니다. RAG(검색 증강 생성) 기술을 활용하여 관련 법령, 규정, 매뉴얼을 참조한 답변을 제공합니다. + +**주요 기능:** +- AI Q&A 채팅 UI (질문 입력 및 답변 표시) +- RAG 기반 법령/규정 참조 답변 (참조 출처 표시) +- 대화 이력 관리 +- 답변 품질 피드백 (유용함/유용하지 않음) + +**구현 완료:** +- ✅ AI Q&A 채팅 UI +- ✅ RAG 법령 참조 답변 표시 화면 + +**향후 구현 예정:** +- 🔲 LLM(대규모 언어 모델) 백엔드 서버 연동 +- 🔲 RAG 시스템 구축 (법령/규정/매뉴얼 벡터 DB) +- 🔲 답변 품질 개선을 위한 Fine-tuning + +**보완 필요:** +- ⚠️ 현재 답변은 사전 작성된 샘플이며, LLM 백엔드 연동 후 실제 AI 답변으로 교체 필요 + +--- + +## 부록: 시스템 관리 메뉴 + +### 시스템 관리 + +**메뉴 위치:** 시스템 관리 > 시스템 관리 +**URL:** `/admin` +**접근 권한:** ADMIN + +**화면 설명:** +시스템 전반의 관리 기능을 제공하는 화면입니다. 서버 상태, 로그 조회, 사용자 관리 등 시스템 운영에 필요한 관리 기능을 포함합니다. + +**주요 기능:** +- 시스템 상태 모니터링 +- 사용자 계정 관리 +- 시스템 로그 조회 + +**구현 완료:** +- ✅ 시스템 관리 기본 UI + +**향후 구현 예정:** +- 🔲 실제 서버 상태 모니터링 연동 +- 🔲 사용자 관리 DB 연동 +- 🔲 시스템 로그 수집/조회 연동 + +--- + +## 부록: 역할(Role)별 접근 권한 요약 + +| 역할 | 설명 | 접근 가능 주요 메뉴 | +|------|------|---------------------| +| **ADMIN** | 시스템 관리자 | 모든 메뉴 접근 가능 | +| **OPERATOR** | 운영 담당자 | 모니터링, 탐지/분석, 단속/이력, 통계/보고, 현장 대응, 일부 시스템 관리 | +| **ANALYST** | 분석 담당자 | 모니터링, 탐지/분석, 단속/이력, 통계/보고, AI 의사결정 지원 | +| **FIELD** | 현장 요원 | 종합 상황판, 실시간 감시, 순찰경로, 모바일 서비스 | +| **VIEWER** | 조회 전용 | 종합 상황판 (조회만 가능) | + +--- + +## 부록: 현재 시스템 상태 요약 + +| 항목 | 상태 | +|------|------| +| UI 구현 | 모든 SFR 완료 | +| 백엔드 연동 | 미구현 (전체) | +| 데이터 | 시연용 샘플 데이터 | +| 인증 체계 | 데모 계정 5종 (SSO/GPKI 미연동) | +| 실시간 기능 | 미구현 (WebSocket 등 미연동) | +| AI 모델 | 미연동 (탐지/예측/최적화 등) | +| 외부 시스템 | 미연동 (GICOMS, MTIS 등) | +| 모바일 앱 | 웹 시뮬레이션만 제공 (네이티브 앱 미개발) | diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a694af7 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,30 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; + +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-expressions': 'off', + 'prefer-const': 'warn', + }, + }, +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..d2e4cfc --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + AI 기반 선박 모니터링 시스템 + + + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3dcedb5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5087 @@ +{ + "name": "kcg-ai-monitoring", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kcg-ai-monitoring", + "version": "0.1.0", + "dependencies": { + "@deck.gl/mapbox": "^9.2.11", + "class-variance-authority": "^0.7.1", + "deck.gl": "^9.2.11", + "echarts": "^6.0.0", + "i18next": "^26.0.3", + "lucide-react": "0.487.0", + "maplibre-gl": "^5.22.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^17.0.2", + "react-router-dom": "^7.12.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.2.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "tailwindcss": "^4.2.2", + "typescript": "5.9", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.3" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@carto/api-client": { + "version": "0.5.26", + "resolved": "https://registry.npmjs.org/@carto/api-client/-/api-client-0.5.26.tgz", + "integrity": "sha512-dMIfVubI+4VrylazzrJQOEza+YvWEoK7fAiyqLLB3Je7yljFvklSxGA3c5LaYD3XiHhZ7PrdM3SQH84SUNrW2A==", + "license": "MIT", + "dependencies": { + "@loaders.gl/schema": "^4.3.3", + "@types/geojson": "^7946.0.16", + "d3-format": "^3.1.0", + "d3-scale": "^4.0.2", + "h3-js": "^4.1.0", + "jsep": "^1.4.0", + "quadbin": "^0.4.1-alpha.0" + } + }, + "node_modules/@deck.gl/aggregation-layers": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/aggregation-layers/-/aggregation-layers-9.2.11.tgz", + "integrity": "sha512-MRFbBHtMcDkOthxXnMPm6nF08DjFDACaIQsJSyHkdWtLUTSLHsWnOTn/8QbB4ka86WyNyfJy3dibLu/m3ei2ow==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@math.gl/core": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "d3-hexbin": "^0.2.1" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@deck.gl/layers": "~9.2.0", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" + } + }, + "node_modules/@deck.gl/arcgis": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/arcgis/-/arcgis-9.2.11.tgz", + "integrity": "sha512-HMGS67qgh0API5z8vEJpk6wjlYKWfWy71r9AYI4a8XubiCM1ekvLHuLDtsVNVQIdrBbT4D0CzSDwxl/pCOgslw==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "~9.2.6", + "esri-loader": "^3.7.0" + }, + "peerDependencies": { + "@arcgis/core": "^4.0.0", + "@deck.gl/core": "~9.2.0", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6", + "@luma.gl/webgl": "~9.2.6" + } + }, + "node_modules/@deck.gl/carto": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/carto/-/carto-9.2.11.tgz", + "integrity": "sha512-2JVzsnlsQ+VTyiazpgJCS093xAV2q3g47/fkgkYrDDRuK27sISWZNgzvQ5xLCL4s/8nGa4GuMPnDIXPWasnuOA==", + "license": "MIT", + "dependencies": { + "@carto/api-client": "^0.5.19", + "@loaders.gl/compression": "~4.3.4", + "@loaders.gl/gis": "~4.3.4", + "@loaders.gl/loader-utils": "~4.3.4", + "@loaders.gl/mvt": "~4.3.4", + "@loaders.gl/schema": "~4.3.4", + "@loaders.gl/tiles": "~4.3.4", + "@luma.gl/core": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@math.gl/web-mercator": "^4.1.0", + "@types/d3-array": "^3.0.2", + "@types/d3-color": "^1.4.2", + "@types/d3-scale": "^3.0.0", + "cartocolor": "^5.0.2", + "d3-array": "^3.2.0", + "d3-color": "^3.1.0", + "d3-format": "^3.1.0", + "d3-scale": "^4.0.0", + "earcut": "^2.2.4", + "h3-js": "^4.1.0", + "moment-timezone": "^0.6.0", + "pbf": "^3.2.1", + "quadbin": "^0.4.0" + }, + "peerDependencies": { + "@deck.gl/aggregation-layers": "~9.2.0", + "@deck.gl/core": "~9.2.0", + "@deck.gl/extensions": "~9.2.0", + "@deck.gl/geo-layers": "~9.2.0", + "@deck.gl/layers": "~9.2.0", + "@loaders.gl/core": "~4.3.4", + "@luma.gl/core": "~9.2.6" + } + }, + "node_modules/@deck.gl/carto/node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/@deck.gl/carto/node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/@deck.gl/core": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.2.11.tgz", + "integrity": "sha512-lpdxXQuFSkd6ET7M6QxPI8QMhsLRY6vzLyk83sPGFb7JSb4OhrNHYt9sfIhcA/hxJW7bdBSMWWphf2GvQetVuA==", + "license": "MIT", + "dependencies": { + "@loaders.gl/core": "~4.3.4", + "@loaders.gl/images": "~4.3.4", + "@luma.gl/constants": "~9.2.6", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@luma.gl/webgl": "~9.2.6", + "@math.gl/core": "^4.1.0", + "@math.gl/sun": "^4.1.0", + "@math.gl/types": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "@probe.gl/env": "^4.1.1", + "@probe.gl/log": "^4.1.1", + "@probe.gl/stats": "^4.1.1", + "@types/offscreencanvas": "^2019.6.4", + "gl-matrix": "^3.0.0", + "mjolnir.js": "^3.0.0" + } + }, + "node_modules/@deck.gl/extensions": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.11.tgz", + "integrity": "sha512-zlpM4Bg1ifBziW1Juiii9NY5gyW2rEhyVTWnhagH/bpTCZ2E73OhnToYt1ouqmoxL6lMtIjhRXz6LPb7tJbHHQ==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@math.gl/core": "^4.1.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" + } + }, + "node_modules/@deck.gl/geo-layers": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/geo-layers/-/geo-layers-9.2.11.tgz", + "integrity": "sha512-Mr3yvKyZMPmQ3ho0hSqcJu1p7a881RqQaq/dRaPs2VP56UAkfk1e10zxXnrZ9/Dmo2MR5PH0j8tkOoGR3zKbfA==", + "license": "MIT", + "dependencies": { + "@loaders.gl/3d-tiles": "~4.3.4", + "@loaders.gl/gis": "~4.3.4", + "@loaders.gl/loader-utils": "~4.3.4", + "@loaders.gl/mvt": "~4.3.4", + "@loaders.gl/schema": "~4.3.4", + "@loaders.gl/terrain": "~4.3.4", + "@loaders.gl/tiles": "~4.3.4", + "@loaders.gl/wms": "~4.3.4", + "@luma.gl/gltf": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@math.gl/core": "^4.1.0", + "@math.gl/culling": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "@types/geojson": "^7946.0.8", + "a5-js": "^0.5.0", + "h3-js": "^4.1.0", + "long": "^3.2.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@deck.gl/extensions": "~9.2.0", + "@deck.gl/layers": "~9.2.0", + "@deck.gl/mesh-layers": "~9.2.0", + "@loaders.gl/core": "~4.3.4", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" + } + }, + "node_modules/@deck.gl/google-maps": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/google-maps/-/google-maps-9.2.11.tgz", + "integrity": "sha512-ahA+u8wkyx9vyKGekJW+Juza2jX8Yhj2+VbYQjriV4aVIJmSNiNNUqYvOwLbwhThj5HBY0jUSSQ1WixwmrBZaA==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "~9.2.6", + "@luma.gl/webgl": "~9.2.6", + "@math.gl/core": "^4.1.0", + "@types/google.maps": "^3.48.6" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@luma.gl/constants": "~9.2.6", + "@luma.gl/core": "~9.2.6", + "@luma.gl/webgl": "~9.2.6" + } + }, + "node_modules/@deck.gl/json": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/json/-/json-9.2.11.tgz", + "integrity": "sha512-xUc8y79kAQNqDJegDkEFWgdfE3JDaTdLtSerzwX5gR7q8mZMMZ0tcFM5IqE+mB3EoNbUgTY7m/7LnzO58XGP6g==", + "license": "MIT", + "dependencies": { + "jsep": "^0.3.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0" + } + }, + "node_modules/@deck.gl/json/node_modules/jsep": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.3.5.tgz", + "integrity": "sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@deck.gl/layers": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/layers/-/layers-9.2.11.tgz", + "integrity": "sha512-2FSb0Qa6YR+Rg6GWhYOGTUug3vtZ4uKcFdnrdiJoVXGyibKJMScKZIsivY0r/yQQZsaBjYqty5QuVJvdtEHxSA==", + "license": "MIT", + "dependencies": { + "@loaders.gl/images": "~4.3.4", + "@loaders.gl/schema": "~4.3.4", + "@luma.gl/shadertools": "~9.2.6", + "@mapbox/tiny-sdf": "^2.0.5", + "@math.gl/core": "^4.1.0", + "@math.gl/polygon": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "earcut": "^2.2.4" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@loaders.gl/core": "~4.3.4", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" + } + }, + "node_modules/@deck.gl/layers/node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/@deck.gl/mapbox": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.2.11.tgz", + "integrity": "sha512-5OaFZgjyA4Vq6WjHUdcEdl0Phi8dwj8hSCErej0NetW90mctdbxwMt0gSbqcvWBowwhyj2QAhH0P2FcITjKG/A==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "~9.2.6", + "@math.gl/web-mercator": "^4.1.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@luma.gl/constants": "~9.2.6", + "@luma.gl/core": "~9.2.6", + "@math.gl/web-mercator": "^4.1.0" + } + }, + "node_modules/@deck.gl/mesh-layers": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.2.11.tgz", + "integrity": "sha512-zPB7TtnPXB3tOEoOfcOkNZo7coIq/ukIQa8HIUQLLiOE8AVSQfz3kbMmMK6rUabXlQbgSw/I/j3kFSYRHg3NGg==", + "license": "MIT", + "dependencies": { + "@loaders.gl/gltf": "~4.3.4", + "@loaders.gl/schema": "~4.3.4", + "@luma.gl/gltf": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6", + "@luma.gl/gltf": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6" + } + }, + "node_modules/@deck.gl/react": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/react/-/react-9.2.11.tgz", + "integrity": "sha512-7xrXlM++3A7cKZdDKSW2/EPT6sc0ob7J24sTPaHe3YlIIFvCZTkQ1U1rAT9cN2gjhkI6XsE+TugUsqhx4TGwHQ==", + "license": "MIT", + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@deck.gl/widgets": "~9.2.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/@deck.gl/widgets": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/widgets/-/widgets-9.2.11.tgz", + "integrity": "sha512-90HWlQPsiRyTPWR4aYfLwnYDrJdHG2mqCzRcyMUKewWBNQLu4upB//l4ewIkUeXXCzAprjjVeRnNb7wdYj2CXQ==", + "license": "MIT", + "dependencies": { + "preact": "^10.17.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@luma.gl/core": "~9.2.6" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.4.tgz", + "integrity": "sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.4", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.4.tgz", + "integrity": "sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.0.tgz", + "integrity": "sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.4.tgz", + "integrity": "sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.0.tgz", + "integrity": "sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@loaders.gl/3d-tiles": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/3d-tiles/-/3d-tiles-4.3.4.tgz", + "integrity": "sha512-JQ3y3p/KlZP7lfobwON5t7H9WinXEYTvuo3SRQM8TBKhM+koEYZhvI2GwzoXx54MbBbY+s3fm1dq5UAAmaTsZw==", + "license": "MIT", + "dependencies": { + "@loaders.gl/compression": "4.3.4", + "@loaders.gl/crypto": "4.3.4", + "@loaders.gl/draco": "4.3.4", + "@loaders.gl/gltf": "4.3.4", + "@loaders.gl/images": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/math": "4.3.4", + "@loaders.gl/tiles": "4.3.4", + "@loaders.gl/zip": "4.3.4", + "@math.gl/core": "^4.1.0", + "@math.gl/culling": "^4.1.0", + "@math.gl/geospatial": "^4.1.0", + "@probe.gl/log": "^4.0.4", + "long": "^5.2.1" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/3d-tiles/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@loaders.gl/compression": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/compression/-/compression-4.3.4.tgz", + "integrity": "sha512-+o+5JqL9Sx8UCwdc2MTtjQiUHYQGJALHbYY/3CT+b9g/Emzwzez2Ggk9U9waRfdHiBCzEgRBivpWZEOAtkimXQ==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "@types/brotli": "^1.3.0", + "@types/pako": "^1.0.1", + "fflate": "0.7.4", + "lzo-wasm": "^0.0.4", + "pako": "1.0.11", + "snappyjs": "^0.6.1" + }, + "optionalDependencies": { + "brotli": "^1.3.2", + "lz4js": "^0.2.0", + "zstd-codec": "^0.1" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/core": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz", + "integrity": "sha512-cG0C5fMZ1jyW6WCsf4LoHGvaIAJCEVA/ioqKoYRwoSfXkOf+17KupK1OUQyUCw5XoRn+oWA1FulJQOYlXnb9Gw==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "@probe.gl/log": "^4.0.2" + } + }, + "node_modules/@loaders.gl/crypto": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/crypto/-/crypto-4.3.4.tgz", + "integrity": "sha512-3VS5FgB44nLOlAB9Q82VOQnT1IltwfRa1miE0mpHCe1prYu1M/dMnEyynusbrsp+eDs3EKbxpguIS9HUsFu5dQ==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "@types/crypto-js": "^4.0.2" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/draco": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/draco/-/draco-4.3.4.tgz", + "integrity": "sha512-4Lx0rKmYENGspvcgV5XDpFD9o+NamXoazSSl9Oa3pjVVjo+HJuzCgrxTQYD/3JvRrolW/QRehZeWD/L/cEC6mw==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "draco3d": "1.5.7" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/gis": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/gis/-/gis-4.3.4.tgz", + "integrity": "sha512-8xub38lSWW7+ZXWuUcggk7agRHJUy6RdipLNKZ90eE0ZzLNGDstGD1qiBwkvqH0AkG+uz4B7Kkiptyl7w2Oa6g==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@mapbox/vector-tile": "^1.3.1", + "@math.gl/polygon": "^4.1.0", + "pbf": "^3.2.1" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/gis/node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@loaders.gl/gis/node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@loaders.gl/gis/node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/@loaders.gl/gltf": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/gltf/-/gltf-4.3.4.tgz", + "integrity": "sha512-EiUTiLGMfukLd9W98wMpKmw+hVRhQ0dJ37wdlXK98XPeGGB+zTQxCcQY+/BaMhsSpYt/OOJleHhTfwNr8RgzRg==", + "license": "MIT", + "dependencies": { + "@loaders.gl/draco": "4.3.4", + "@loaders.gl/images": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/textures": "4.3.4", + "@math.gl/core": "^4.1.0" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/images": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-4.3.4.tgz", + "integrity": "sha512-qgc33BaNsqN9cWa/xvcGvQ50wGDONgQQdzHCKDDKhV2w/uptZoR5iofJfuG8UUV2vUMMd82Uk9zbopRx2rS4Ag==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/loader-utils": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-4.3.4.tgz", + "integrity": "sha512-tjMZvlKQSaMl2qmYTAxg+ySR6zd6hQn5n3XaU8+Ehp90TD3WzxvDKOMNDqOa72fFmIV+KgPhcmIJTpq4lAdC4Q==", + "license": "MIT", + "dependencies": { + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "@probe.gl/log": "^4.0.2", + "@probe.gl/stats": "^4.0.2" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/math": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/math/-/math-4.3.4.tgz", + "integrity": "sha512-UJrlHys1fp9EUO4UMnqTCqvKvUjJVCbYZ2qAKD7tdGzHJYT8w/nsP7f/ZOYFc//JlfC3nq+5ogvmdpq2pyu3TA==", + "license": "MIT", + "dependencies": { + "@loaders.gl/images": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "@math.gl/core": "^4.1.0" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/mvt": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/mvt/-/mvt-4.3.4.tgz", + "integrity": "sha512-9DrJX8RQf14htNtxsPIYvTso5dUce9WaJCWCIY/79KYE80Be6dhcEYMknxBS4w3+PAuImaAe66S5xo9B7Erm5A==", + "license": "MIT", + "dependencies": { + "@loaders.gl/gis": "4.3.4", + "@loaders.gl/images": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@math.gl/polygon": "^4.1.0", + "@probe.gl/stats": "^4.0.0", + "pbf": "^3.2.1" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/mvt/node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/@loaders.gl/schema": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.3.4.tgz", + "integrity": "sha512-1YTYoatgzr/6JTxqBLwDiD3AVGwQZheYiQwAimWdRBVB0JAzych7s1yBuE0CVEzj4JDPKOzVAz8KnU1TiBvJGw==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.7" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/terrain": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/terrain/-/terrain-4.3.4.tgz", + "integrity": "sha512-JszbRJGnxL5Fh82uA2U8HgjlsIpzYoCNNjy3cFsgCaxi4/dvjz3BkLlBilR7JlbX8Ka+zlb4GAbDDChiXLMJ/g==", + "license": "MIT", + "dependencies": { + "@loaders.gl/images": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@mapbox/martini": "^0.2.0" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/textures": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/textures/-/textures-4.3.4.tgz", + "integrity": "sha512-arWIDjlE7JaDS6v9by7juLfxPGGnjT9JjleaXx3wq/PTp+psLOpGUywHXm38BNECos3MFEQK3/GFShWI+/dWPw==", + "license": "MIT", + "dependencies": { + "@loaders.gl/images": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "@math.gl/types": "^4.1.0", + "ktx-parse": "^0.7.0", + "texture-compressor": "^1.0.2" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/tiles": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/tiles/-/tiles-4.3.4.tgz", + "integrity": "sha512-oC0zJfyvGox6Ag9ABF8fxOkx9yEFVyzTa9ryHXl2BqLiQoR1v3p+0tIJcEbh5cnzHfoTZzUis1TEAZluPRsHBQ==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/math": "4.3.4", + "@math.gl/core": "^4.1.0", + "@math.gl/culling": "^4.1.0", + "@math.gl/geospatial": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "@probe.gl/stats": "^4.0.2" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/wms": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/wms/-/wms-4.3.4.tgz", + "integrity": "sha512-yXF0wuYzJUdzAJQrhLIua6DnjOiBJusaY1j8gpvuH1VYs3mzvWlIRuZKeUd9mduQZKK88H2IzHZbj2RGOauq4w==", + "license": "MIT", + "dependencies": { + "@loaders.gl/images": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/xml": "4.3.4", + "@turf/rewind": "^5.1.5", + "deep-strict-equal": "^0.2.0" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/worker-utils": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-4.3.4.tgz", + "integrity": "sha512-EbsszrASgT85GH3B7jkx7YXfQyIYo/rlobwMx6V3ewETapPUwdSAInv+89flnk5n2eu2Lpdeh+2zS6PvqbL2RA==", + "license": "MIT", + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/xml": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/xml/-/xml-4.3.4.tgz", + "integrity": "sha512-p+y/KskajsvyM3a01BwUgjons/j/dUhniqd5y1p6keLOuwoHlY/TfTKd+XluqfyP14vFrdAHCZTnFCWLblN10w==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "fast-xml-parser": "^4.2.5" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/zip": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/zip/-/zip-4.3.4.tgz", + "integrity": "sha512-bHY4XdKYJm3vl9087GMoxnUqSURwTxPPh6DlAGOmz6X9Mp3JyWuA2gk3tQ1UIuInfjXKph3WAUfGe6XRIs1sfw==", + "license": "MIT", + "dependencies": { + "@loaders.gl/compression": "4.3.4", + "@loaders.gl/crypto": "4.3.4", + "@loaders.gl/loader-utils": "4.3.4", + "jszip": "^3.1.5", + "md5": "^2.3.0" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@luma.gl/constants": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.2.6.tgz", + "integrity": "sha512-rvFFrJuSm5JIWbDHFuR4Q2s4eudO3Ggsv0TsGKn9eqvO7bBiPm/ANugHredvh3KviEyYuMZZxtfJvBdr3kzldg==", + "license": "MIT" + }, + "node_modules/@luma.gl/core": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-9.2.6.tgz", + "integrity": "sha512-d8KcH8ZZcjDAodSN/G2nueA9YE2X8kMz7Q0OxDGpCww6to1MZXM3Ydate/Jqsb5DDKVgUF6yD6RL8P5jOki9Yw==", + "license": "MIT", + "dependencies": { + "@math.gl/types": "^4.1.0", + "@probe.gl/env": "^4.0.8", + "@probe.gl/log": "^4.0.8", + "@probe.gl/stats": "^4.0.8", + "@types/offscreencanvas": "^2019.6.4" + } + }, + "node_modules/@luma.gl/engine": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-9.2.6.tgz", + "integrity": "sha512-1AEDs2AUqOWh7Wl4onOhXmQF+Rz1zNdPXF+Kxm4aWl92RQ42Sh2CmTvRt2BJku83VQ91KFIEm/v3qd3Urzf+Uw==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "^4.1.0", + "@math.gl/types": "^4.1.0", + "@probe.gl/log": "^4.0.8", + "@probe.gl/stats": "^4.0.8" + }, + "peerDependencies": { + "@luma.gl/core": "~9.2.0", + "@luma.gl/shadertools": "~9.2.0" + } + }, + "node_modules/@luma.gl/gltf": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/gltf/-/gltf-9.2.6.tgz", + "integrity": "sha512-is3YkiGsWqWTmwldMz6PRaIUleufQfUKYjJTKpsF5RS1OnN+xdAO0mJq5qJTtOQpppWAU0VrmDFEVZ6R3qvm0A==", + "license": "MIT", + "dependencies": { + "@loaders.gl/core": "^4.2.0", + "@loaders.gl/gltf": "^4.2.0", + "@loaders.gl/textures": "^4.2.0", + "@math.gl/core": "^4.1.0" + }, + "peerDependencies": { + "@luma.gl/constants": "~9.2.0", + "@luma.gl/core": "~9.2.0", + "@luma.gl/engine": "~9.2.0", + "@luma.gl/shadertools": "~9.2.0" + } + }, + "node_modules/@luma.gl/shadertools": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-9.2.6.tgz", + "integrity": "sha512-4+uUbynqPUra9d/z1nQChyHmhLgmKfSMjS7kOwLB6exSnhKnpHL3+Hu9fv55qyaX50nGH3oHawhGtJ6RRvu65w==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "^4.1.0", + "@math.gl/types": "^4.1.0", + "wgsl_reflect": "^1.2.0" + }, + "peerDependencies": { + "@luma.gl/core": "~9.2.0" + } + }, + "node_modules/@luma.gl/webgl": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/webgl/-/webgl-9.2.6.tgz", + "integrity": "sha512-NGBTdxJMk7j8Ygr1zuTyAvr1Tw+EpupMIQo7RelFjEsZXg6pujFqiDMM+rgxex8voCeuhWBJc7Rs+MoSqd46UQ==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "9.2.6", + "@math.gl/types": "^4.1.0", + "@probe.gl/env": "^4.0.8" + }, + "peerDependencies": { + "@luma.gl/core": "~9.2.0" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/martini": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/martini/-/martini-0.2.0.tgz", + "integrity": "sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ==", + "license": "ISC" + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", + "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.1" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.0.4.tgz", + "integrity": "sha512-HYv3POhMRCdhP3UPPATM/hfcy6/WuVIf5FKboH8u/ZuFMTnAIcSVlq5nfOqroLokd925w2QtE7YwquFOIacwVQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.1.tgz", + "integrity": "sha512-zxa92qF96ZNojLxeAjnaRpjVCy+swoUNJvDhtpC90k7u5F0TMr4GmvNqMKvYrMoPB8d7gRSXbMG1hBbmgESIsw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.8.tgz", + "integrity": "sha512-8vtfYGidr1rNkv5IwIoU2lfe3Oy+Wa8HluzQYcQi9cveU9K3pweAal/poQj4GJ0K/EW4bTQp2wVAs09g2yDRZg==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz", + "integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@maplibre/geojson-vt": "^5.0.4", + "@types/geojson": "^7946.0.16", + "@types/supercluster": "^7.1.3", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz", + "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==", + "license": "ISC" + }, + "node_modules/@math.gl/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz", + "integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==", + "license": "MIT", + "dependencies": { + "@math.gl/types": "4.1.0" + } + }, + "node_modules/@math.gl/culling": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/culling/-/culling-4.1.0.tgz", + "integrity": "sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0", + "@math.gl/types": "4.1.0" + } + }, + "node_modules/@math.gl/geospatial": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/geospatial/-/geospatial-4.1.0.tgz", + "integrity": "sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0", + "@math.gl/types": "4.1.0" + } + }, + "node_modules/@math.gl/polygon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/polygon/-/polygon-4.1.0.tgz", + "integrity": "sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0" + } + }, + "node_modules/@math.gl/sun": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/sun/-/sun-4.1.0.tgz", + "integrity": "sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA==", + "license": "MIT" + }, + "node_modules/@math.gl/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz", + "integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==", + "license": "MIT" + }, + "node_modules/@math.gl/web-mercator": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz", + "integrity": "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@probe.gl/env": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.1.1.tgz", + "integrity": "sha512-+68seNDMVsEegRB47pFA/Ws1Fjy8agcFYXxzorKToyPcD6zd+gZ5uhwoLd7TzsSw6Ydns//2KEszWn+EnNHTbA==", + "license": "MIT" + }, + "node_modules/@probe.gl/log": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.1.1.tgz", + "integrity": "sha512-kcZs9BT44pL7hS1OkRGKYRXI/SN9KejUlPD+BY40DguRLzdC5tLG/28WGMyfKdn/51GT4a0p+0P8xvDn1Ez+Kg==", + "license": "MIT", + "dependencies": { + "@probe.gl/env": "4.1.1" + } + }, + "node_modules/@probe.gl/stats": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.1.1.tgz", + "integrity": "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@turf/boolean-clockwise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-5.1.5.tgz", + "integrity": "sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5" + } + }, + "node_modules/@turf/clone": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-5.1.5.tgz", + "integrity": "sha512-//pITsQ8xUdcQ9pVb4JqXiSqG4dos5Q9N4sYFoWghX21tfOV2dhc5TGqYOhnHrQS7RiKQL1vQ48kIK34gQ5oRg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^5.1.5" + } + }, + "node_modules/@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==", + "license": "MIT" + }, + "node_modules/@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^5.1.5" + } + }, + "node_modules/@turf/meta": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", + "integrity": "sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^5.1.5" + } + }, + "node_modules/@turf/rewind": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-5.1.5.tgz", + "integrity": "sha512-Gdem7JXNu+G4hMllQHXRFRihJl3+pNl7qY+l4qhQFxq+hiU1cQoVFnyoleIqWKIrdK/i2YubaSwc3SCM7N5mMw==", + "license": "MIT", + "dependencies": { + "@turf/boolean-clockwise": "^5.1.5", + "@turf/clone": "^5.1.5", + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5", + "@turf/meta": "^5.1.5" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/brotli": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/brotli/-/brotli-1.3.5.tgz", + "integrity": "sha512-9xoNr+bcxT236/7ZgcWw/6Pb2RRetE13p4bFy1xYSckKwyOiRfmInay8baUWZgH7/284Wl6IPe7+nOI9+OQg/A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "license": "MIT" + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.5.tgz", + "integrity": "sha512-5sNP3DmtSnSozxcjqmzQKsDOuVJXZkceo1KJScDc1982kk/TS9mTPc6lpli1gTu1MIBF1YWutpHpjucNWcIj5g==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.5.tgz", + "integrity": "sha512-YOpKj0kIEusRf7ofeJcSZQsvKbnTwpe1DUF+P2qsotqG53kEsjm7EzzliqQxMkAWdkZcHrg5rRhB4JiDOQPX+A==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "^2" + } + }, + "node_modules/@types/d3-time": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.4.tgz", + "integrity": "sha512-BTfLsxTeo7yFxI/haOOf1ZwJ6xKgQLT9dCp+EcmQv87Gox6X+oKl4mLKfO6fnWm3P22+A6DknMNEZany8ql2Rw==", + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/pako": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", + "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/a5-js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/a5-js/-/a5-js-0.5.0.tgz", + "integrity": "sha512-VAw19sWdYadhdovb0ViOIi1SdKx6H6LwcGMRFKwMfgL5gcmL/1fKJHfgsNgNaJ7xC/eEyjs6VK+VVd4N0a+peg==", + "license": "Apache-2.0", + "dependencies": { + "gl-matrix": "^3.4.3" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz", + "integrity": "sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buf-compare": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", + "integrity": "sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001785", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", + "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cartocolor": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/cartocolor/-/cartocolor-5.0.2.tgz", + "integrity": "sha512-Ihb/wU5V6BVbHwapd8l/zg7bnhZ4YPFVfa7quSpL86lfkPJSf4YuNBT+EvesPRP5vSqhl6vZVsQJwCR8alBooQ==", + "license": "CC-BY-4.0", + "dependencies": { + "colorbrewer": "1.5.6" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/colorbrewer": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/colorbrewer/-/colorbrewer-1.5.6.tgz", + "integrity": "sha512-fONg2pGXyID8zNgKHBlagW8sb/AMShGzj4rRJfz5biZ7iuHQZYquSCLE/Co1oSQFmt/vvwjyezJCejQl7FG/tg==", + "license": [ + { + "type": "Apache-Style", + "url": "https://github.com/saikocat/colorbrewer/blob/master/LICENSE.txt" + } + ] + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/core-assert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", + "integrity": "sha512-IG97qShIP+nrJCXMCgkNZgH7jZQ4n8RpPyPeXX++T6avR/KhLhgLiHKoEn5Rc1KjfycSfA9DMa6m+4C4eguHhw==", + "license": "MIT", + "dependencies": { + "buf-compare": "^1.0.0", + "is-error": "^2.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hexbin": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz", + "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deck.gl": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/deck.gl/-/deck.gl-9.2.11.tgz", + "integrity": "sha512-oFKjJWWjEoQczfh725ypGphYGKh2sOQAFNO5/eXFlWNUZEXTTrj44ol3bmDMegL/l5qCzF4RoGWXgvLqNWphuQ==", + "license": "MIT", + "dependencies": { + "@deck.gl/aggregation-layers": "9.2.11", + "@deck.gl/arcgis": "9.2.11", + "@deck.gl/carto": "9.2.11", + "@deck.gl/core": "9.2.11", + "@deck.gl/extensions": "9.2.11", + "@deck.gl/geo-layers": "9.2.11", + "@deck.gl/google-maps": "9.2.11", + "@deck.gl/json": "9.2.11", + "@deck.gl/layers": "9.2.11", + "@deck.gl/mapbox": "9.2.11", + "@deck.gl/mesh-layers": "9.2.11", + "@deck.gl/react": "9.2.11", + "@deck.gl/widgets": "9.2.11", + "@loaders.gl/core": "~4.3.4", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" + }, + "peerDependencies": { + "@arcgis/core": "^4.0.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + }, + "peerDependenciesMeta": { + "@arcgis/core": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-strict-equal": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deep-strict-equal/-/deep-strict-equal-0.2.0.tgz", + "integrity": "sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA==", + "license": "MIT", + "dependencies": { + "core-assert": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esri-loader": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/esri-loader/-/esri-loader-3.7.0.tgz", + "integrity": "sha512-cB1Sw9EQjtW4mtT7eFBjn/6VaaIWNTjmTd2asnnEyuZk1xVSFRMCfLZSBSjZM7ZarDcVu5WIjOP0t0MYVu4hVQ==", + "deprecated": "Use @arcgis/core instead.", + "license": "Apache-2.0" + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", + "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/h3-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.4.0.tgz", + "integrity": "sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog==", + "license": "Apache-2.0", + "engines": { + "node": ">=4", + "npm": ">=3", + "yarn": ">=1.3.0" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.3.tgz", + "integrity": "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", + "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-error": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.2.tgz", + "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/ktx-parse": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.7.1.tgz", + "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.487.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.487.0.tgz", + "integrity": "sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz4js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/lz4js/-/lz4js-0.2.0.tgz", + "integrity": "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==", + "license": "ISC", + "optional": true + }, + "node_modules/lzo-wasm": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/lzo-wasm/-/lzo-wasm-0.0.4.tgz", + "integrity": "sha512-VKlnoJRFrB8SdJhlVKvW5vI1gGwcZ+mvChEXcSX6r2xDNc/Q2FD9esfBmGCuPZdrJ1feO+YcVFd2PTk0c137Gw==", + "license": "BSD-2-Clause" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/maplibre-gl": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.22.0.tgz", + "integrity": "sha512-nc8YA+YSEioMZg5W0cb6Cf3wQ8aJge66dsttyBgpOArOnlmFJO1Kc5G32kYVPeUYhLpBja83T99uanmJvYAIyQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.0.7", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/geojson-vt": "^6.0.4", + "@maplibre/maplibre-gl-style-spec": "^24.8.1", + "@maplibre/mlt": "^1.1.8", + "@maplibre/vt-pbf": "^4.3.0", + "@types/geojson": "^7946.0.16", + "earcut": "^3.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mjolnir.js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-3.0.0.tgz", + "integrity": "sha512-siX3YCG7N2HnmN1xMH3cK4JkUZJhbkhRFJL+G5N1vH0mh1t5088rJknIoqDFWDIU6NPGvRRgLnYW3ZHjSMEBLA==", + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.1.tgz", + "integrity": "sha512-1B9lmAhB9D9/sHaPC1N7wLFEVUoFldxOpOO96lOD1PvJ43vCd0ozDPbu0FEL3++VvawOlDkq8YD373tJmP5JHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quadbin": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/quadbin/-/quadbin-0.4.2.tgz", + "integrity": "sha512-1NFzjFVM23Um51/ttD6lFDqGtUHNS5Ky1slZHk3YPwMbC+7Jl3ULLb4QvDo6+Nerv8b8SgUV+ysOhziUh4B5cQ==", + "license": "MIT", + "dependencies": { + "@math.gl/web-mercator": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-i18next": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz", + "integrity": "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snappyjs": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/snappyjs/-/snappyjs-0.6.1.tgz", + "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/texture-compressor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/texture-compressor/-/texture-compressor-1.0.2.tgz", + "integrity": "sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.10", + "image-size": "^0.7.4" + }, + "bin": { + "texture-compressor": "bin/texture-compressor.js" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wgsl_reflect": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.3.tgz", + "integrity": "sha512-BQWBIsOn411M+ffBxmA6QRLvAOVbuz3Uk4NusxnqC1H7aeQcVLhdA3k2k/EFFFtqVjhz3z7JOOZF1a9hj2tv4Q==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/zstd-codec": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/zstd-codec/-/zstd-codec-0.1.5.tgz", + "integrity": "sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==", + "license": "MIT", + "optional": true + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bdd3311 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "kcg-ai-monitoring", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "@deck.gl/mapbox": "^9.2.11", + "class-variance-authority": "^0.7.1", + "deck.gl": "^9.2.11", + "echarts": "^6.0.0", + "i18next": "^26.0.3", + "lucide-react": "0.487.0", + "maplibre-gl": "^5.22.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^17.0.2", + "react-router-dom": "^7.12.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.2.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "tailwindcss": "^4.2.2", + "typescript": "5.9", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.3" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..531dbec --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,15 @@ +/** + * PostCSS Configuration + * + * Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required + * PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here. + * + * This file only exists for adding additional PostCSS plugins, if needed. + * For example: + * + * import postcssNested from 'postcss-nested' + * export default { plugins: [postcssNested()] } + * + * Otherwise, you can leave this file empty. + */ +export default {} diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..cd4c989 --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,92 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider, useAuth } from '@/app/auth/AuthContext'; +import { MainLayout } from '@/app/layout/MainLayout'; +import { LoginPage } from '@features/auth'; +/* SFR-01 */ import { AccessControl } from '@features/admin'; +/* SFR-02 */ import { SystemConfig, NoticeManagement } from '@features/admin'; +/* SFR-03 */ import { DataHub } from '@features/admin'; +/* SFR-04 */ import { AIModelManagement } from '@features/ai-operations'; +/* SFR-05 */ import { RiskMap } from '@features/risk-assessment'; +/* SFR-06 */ import { EnforcementPlan } from '@features/risk-assessment'; +/* SFR-07 */ import { PatrolRoute } from '@features/patrol'; +/* SFR-08 */ import { FleetOptimization } from '@features/patrol'; +/* SFR-09 */ import { DarkVesselDetection } from '@features/detection'; +/* SFR-10 */ import { GearDetection } from '@features/detection'; +/* SFR-11 */ import { EnforcementHistory } from '@features/enforcement'; +/* SFR-12 */ import { MonitoringDashboard } from '@features/monitoring'; +/* SFR-13 */ import { Statistics } from '@features/statistics'; +/* SFR-14 */ import { ExternalService } from '@features/statistics'; +/* SFR-15 */ import { MobileService } from '@features/field-ops'; +/* SFR-16 */ import { ShipAgent } from '@features/field-ops'; +/* SFR-17 */ import { AIAlert } from '@features/field-ops'; +/* SFR-18+19 */ import { MLOpsPage } from '@features/ai-operations'; +/* SFR-20 */ import { AIAssistant } from '@features/ai-operations'; +/* 기존 */ import { Dashboard } from '@features/dashboard'; +import { LiveMapView, MapControl } from '@features/surveillance'; +import { EventList } from '@features/enforcement'; +import { VesselDetail } from '@features/vessel'; +import { ChinaFishing } from '@features/detection'; +import { ReportManagement } from '@features/statistics'; +import { AdminPanel } from '@features/admin'; + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { user } = useAuth(); + if (!user) return ; + return <>{children}; +} + +export default function App() { + return ( + + + + } /> + }> + } /> + {/* SFR-12 대시보드 */} + } /> + } /> + {/* SFR-05~06 위험도·단속계획 */} + } /> + } /> + {/* SFR-09~10 탐지 */} + } /> + } /> + } /> + {/* SFR-07~08 순찰경로 */} + } /> + } /> + {/* SFR-11 이력 */} + } /> + } /> + {/* SFR-15~17 현장 대응 */} + } /> + } /> + } /> + {/* SFR-13~14 통계·외부연계 */} + } /> + } /> + } /> + {/* SFR-04 AI 모델 */} + } /> + {/* SFR-18~20 AI 운영 */} + } /> + } /> + {/* SFR-03 데이터허브 */} + } /> + {/* SFR-02 환경설정 */} + } /> + } /> + {/* SFR-01 권한·시스템 */} + } /> + } /> + {/* 기존 유지 */} + } /> + } /> + } /> + + + + + ); +} diff --git a/src/app/auth/AuthContext.tsx b/src/app/auth/AuthContext.tsx new file mode 100644 index 0000000..d0d02ab --- /dev/null +++ b/src/app/auth/AuthContext.tsx @@ -0,0 +1,166 @@ +import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'; + +/* + * SFR-01: 시스템 로그인 및 권한 관리 + * - 역할 기반 권한 관리(RBAC) + * - 세션 타임아웃(30분 미사용 시 자동 로그아웃) + * - 동시 접속 1계정 1세션 + * - 감사 로그 기록 + */ + +// ─── RBAC 역할 정의 ───────────────────── +export type UserRole = 'ADMIN' | 'OPERATOR' | 'ANALYST' | 'FIELD' | 'VIEWER'; + +export interface AuthUser { + id: string; + name: string; + rank: string; + org: string; + role: UserRole; + authMethod: 'password' | 'gpki' | 'sso'; + loginAt: string; +} + +// ─── 역할별 접근 가능 경로 ────────────────── +const ROLE_PERMISSIONS: Record = { + ADMIN: [ + '/dashboard', '/monitoring', '/events', '/map-control', '/event-list', + '/risk-map', '/enforcement-plan', + '/dark-vessel', '/gear-detection', '/china-fishing', + '/patrol-route', '/fleet-optimization', + '/enforcement-history', '/statistics', '/reports', + '/ai-alert', '/mobile-service', '/ship-agent', '/external-service', + '/ai-model', '/mlops', '/ai-assistant', + '/data-hub', '/system-config', '/notices', '/admin', '/access-control', + ], + OPERATOR: [ + '/dashboard', '/monitoring', '/events', '/map-control', '/event-list', + '/risk-map', '/enforcement-plan', + '/dark-vessel', '/gear-detection', '/china-fishing', + '/patrol-route', '/fleet-optimization', + '/enforcement-history', '/statistics', '/reports', + '/ai-alert', '/mobile-service', '/ship-agent', + '/data-hub', '/system-config', + ], + ANALYST: [ + '/dashboard', '/monitoring', '/events', '/event-list', + '/risk-map', '/dark-vessel', '/gear-detection', '/china-fishing', + '/enforcement-history', '/statistics', '/reports', + '/ai-model', '/mlops', '/ai-assistant', + '/system-config', + ], + FIELD: [ + '/dashboard', '/monitoring', '/events', '/event-list', + '/risk-map', '/enforcement-plan', + '/dark-vessel', '/china-fishing', + '/mobile-service', '/ship-agent', '/ai-alert', + ], + VIEWER: [ + '/dashboard', '/monitoring', '/statistics', + ], +}; + +// ─── 감사 로그 ────────────────────────── +export interface AuditEntry { + time: string; + user: string; + action: string; + target: string; + ip: string; + result: '성공' | '실패' | '차단'; +} + +function writeAuditLog(entry: Omit) { + const log: AuditEntry = { + ...entry, + time: new Date().toISOString().replace('T', ' ').slice(0, 19), + ip: '10.20.30.1', // 시뮬레이션 + }; + const logs: AuditEntry[] = JSON.parse(sessionStorage.getItem('audit_logs') || '[]'); + logs.unshift(log); + sessionStorage.setItem('audit_logs', JSON.stringify(logs.slice(0, 200))); +} + +// ─── 세션 타임아웃 (30분) ────────────────── +const SESSION_TIMEOUT = 30 * 60 * 1000; + +interface AuthContextType { + user: AuthUser | null; + login: (user: AuthUser) => void; + logout: () => void; + hasAccess: (path: string) => boolean; + sessionRemaining: number; // seconds +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(() => { + const stored = sessionStorage.getItem('auth_user'); + return stored ? JSON.parse(stored) : null; + }); + const [lastActivity, setLastActivity] = useState(Date.now()); + const [sessionRemaining, setSessionRemaining] = useState(SESSION_TIMEOUT / 1000); + + // 사용자 활동 감지 → 세션 갱신 + const resetActivity = useCallback(() => { + setLastActivity(Date.now()); + }, []); + + useEffect(() => { + if (!user) return; + const events = ['mousedown', 'keydown', 'scroll', 'touchstart']; + events.forEach((e) => window.addEventListener(e, resetActivity)); + return () => events.forEach((e) => window.removeEventListener(e, resetActivity)); + }, [user, resetActivity]); + + // 세션 타임아웃 체크 + useEffect(() => { + if (!user) return; + const interval = setInterval(() => { + const elapsed = Date.now() - lastActivity; + const remaining = Math.max(0, Math.floor((SESSION_TIMEOUT - elapsed) / 1000)); + setSessionRemaining(remaining); + + if (elapsed >= SESSION_TIMEOUT) { + writeAuditLog({ user: user.name, action: '세션 타임아웃 로그아웃', target: '시스템', result: '성공' }); + setUser(null); + sessionStorage.removeItem('auth_user'); + } + }, 1000); + return () => clearInterval(interval); + }, [user, lastActivity]); + + const login = useCallback((u: AuthUser) => { + setUser(u); + setLastActivity(Date.now()); + sessionStorage.setItem('auth_user', JSON.stringify(u)); + writeAuditLog({ user: u.name, action: `로그인 (${u.authMethod})`, target: '시스템', result: '성공' }); + }, []); + + const logout = useCallback(() => { + if (user) { + writeAuditLog({ user: user.name, action: '로그아웃', target: '시스템', result: '성공' }); + } + setUser(null); + sessionStorage.removeItem('auth_user'); + }, [user]); + + const hasAccess = useCallback((path: string) => { + if (!user) return false; + const allowed = ROLE_PERMISSIONS[user.role] || []; + return allowed.some((p) => path.startsWith(p)); + }, [user]); + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error('useAuth must be inside AuthProvider'); + return ctx; +} diff --git a/src/app/layout/MainLayout.tsx b/src/app/layout/MainLayout.tsx new file mode 100644 index 0000000..e744535 --- /dev/null +++ b/src/app/layout/MainLayout.tsx @@ -0,0 +1,568 @@ +import { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'; +import { + LayoutDashboard, Map, List, Ship, Anchor, Radar, + FileText, Settings, LogOut, ChevronLeft, ChevronRight, + Shield, Bell, Search, Fingerprint, Clock, Lock, Database, Megaphone, Layers, + Download, FileSpreadsheet, Printer, Wifi, Brain, Activity, + ChevronsLeft, ChevronsRight, + Navigation, Users, EyeOff, BarChart3, Globe, + Smartphone, Monitor, Send, Cpu, MessageSquare, +} from 'lucide-react'; +import { useAuth, type UserRole } from '@/app/auth/AuthContext'; +import { NotificationBanner, NotificationPopup, type SystemNotice } from '@shared/components/common/NotificationBanner'; +import { useSettingsStore } from '@stores/settingsStore'; + +/* + * SFR-01 반영 사항: + * - RBAC 기반 메뉴 접근 제어 (역할별 허용 메뉴만 표시) + * - 세션 타임아웃 잔여 시간 표시 + * - 로그인 사용자 정보·역할·인증방식 표시 + * - 로그아웃 시 감사 로그 기록 + * + * SFR-02 공통기능: + * - 모든 페이지 오른쪽 상단: 페이지 검색, 파일다운로드, 엑셀 내보내기, 인쇄 + * - 모든 페이지 하단: 페이지네이션 + */ + +const ROLE_COLORS: Record = { + ADMIN: 'text-red-400', + OPERATOR: 'text-blue-400', + ANALYST: 'text-purple-400', + FIELD: 'text-green-400', + VIEWER: 'text-yellow-400', +}; + +const AUTH_METHOD_LABELS: Record = { + password: 'ID/PW', + gpki: 'GPKI', + sso: 'SSO', +}; + +interface NavItem { to: string; icon: React.ElementType; labelKey: string; } +interface NavGroup { groupKey: string; icon: React.ElementType; items: NavItem[]; } +type NavEntry = NavItem | NavGroup; + +const isGroup = (entry: NavEntry): entry is NavGroup => 'groupKey' in entry; + +const NAV_ENTRIES: NavEntry[] = [ + // ── 상황판·감시 ── + { to: '/dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' }, + { to: '/monitoring', icon: Activity, labelKey: 'nav.monitoring' }, + { to: '/events', icon: Radar, labelKey: 'nav.eventList' }, + { to: '/map-control', icon: Map, labelKey: 'nav.riskMap' }, + // ── 위험도·단속 ── + { to: '/risk-map', icon: Layers, labelKey: 'nav.riskMap' }, + { to: '/enforcement-plan', icon: Shield, labelKey: 'nav.enforcementPlan' }, + // ── 탐지 ── + { to: '/dark-vessel', icon: EyeOff, labelKey: 'nav.darkVessel' }, + { to: '/gear-detection', icon: Anchor, labelKey: 'nav.gearDetection' }, + { to: '/china-fishing', icon: Ship, labelKey: 'nav.chinaFishing' }, + // ── 이력·통계 ── + { to: '/enforcement-history', icon: FileText, labelKey: 'nav.enforcementHistory' }, + { to: '/event-list', icon: List, labelKey: 'nav.eventList' }, + { to: '/statistics', icon: BarChart3, labelKey: 'nav.statistics' }, + { to: '/reports', icon: FileText, labelKey: 'nav.reports' }, + // ── 함정용 (그룹) ── + { + groupKey: 'group.fieldOps', icon: Ship, + items: [ + { to: '/patrol-route', icon: Navigation, labelKey: 'nav.patrolRoute' }, + { to: '/fleet-optimization', icon: Users, labelKey: 'nav.fleetOptimization' }, + { to: '/ai-alert', icon: Send, labelKey: 'nav.aiAlert' }, + { to: '/mobile-service', icon: Smartphone, labelKey: 'nav.mobileService' }, + { to: '/ship-agent', icon: Monitor, labelKey: 'nav.shipAgent' }, + ], + }, + // ── 관리자 (그룹) ── + { + groupKey: 'group.admin', icon: Settings, + items: [ + { to: '/ai-model', icon: Brain, labelKey: 'nav.aiModel' }, + { to: '/mlops', icon: Cpu, labelKey: 'nav.mlops' }, + { to: '/ai-assistant', icon: MessageSquare, labelKey: 'nav.aiAssistant' }, + { to: '/external-service', icon: Globe, labelKey: 'nav.externalService' }, + { to: '/data-hub', icon: Wifi, labelKey: 'nav.dataHub' }, + { to: '/system-config', icon: Database, labelKey: 'nav.systemConfig' }, + { to: '/notices', icon: Megaphone, labelKey: 'nav.notices' }, + { to: '/admin', icon: Settings, labelKey: 'nav.admin' }, + { to: '/access-control', icon: Fingerprint, labelKey: 'nav.accessControl' }, + ], + }, +]; + +// getPageLabel용 flat 목록 +const NAV_ITEMS = NAV_ENTRIES.flatMap(e => isGroup(e) ? e.items : [e]); + +function formatRemaining(seconds: number) { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return `${m}:${String(s).padStart(2, '0')}`; +} + +// ─── 공통 페이지네이션 (간소형) ───────────── +function PagePagination({ page, totalPages, onPageChange }: { + page: number; totalPages: number; onPageChange: (p: number) => void; +}) { + if (totalPages <= 1) return null; + const range: number[] = []; + const maxVis = 5; + let s = Math.max(0, page - Math.floor(maxVis / 2)); + const e = Math.min(totalPages - 1, s + maxVis - 1); + if (e - s < maxVis - 1) s = Math.max(0, e - maxVis + 1); + for (let i = s; i <= e; i++) range.push(i); + + const btnCls = "p-1 rounded text-hint hover:text-heading hover:bg-surface-overlay disabled:opacity-30 disabled:cursor-not-allowed transition-colors"; + + return ( +
+ + + {range.map((p) => ( + + ))} + + + {page + 1} / {totalPages} +
+ ); +} + +export function MainLayout() { + const { t } = useTranslation('common'); + const { theme, toggleTheme, language, toggleLanguage } = useSettingsStore(); + const [collapsed, setCollapsed] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + const { user, logout, hasAccess, sessionRemaining } = useAuth(); + const contentRef = useRef(null); + + // getPageLabel: 현재 라우트에서 페이지명 가져오기 (i18n) + const getPageLabel = (pathname: string): string => { + const item = NAV_ITEMS.find((n) => pathname.startsWith(n.to)); + return item ? t(item.labelKey) : ''; + }; + + // 공통 검색 + const [pageSearch, setPageSearch] = useState(''); + + // 공통 스크롤 페이징 (페이지 단위 스크롤) + const [scrollPage, setScrollPage] = useState(0); + const scrollPageSize = 800; // px per page + + const handleScrollPageChange = (p: number) => { + setScrollPage(p); + if (contentRef.current) { + contentRef.current.scrollTo({ top: p * scrollPageSize, behavior: 'smooth' }); + } + }; + + // 스크롤 이벤트로 현재 페이지 추적 + const handleScroll = () => { + if (contentRef.current) { + const { scrollTop, scrollHeight, clientHeight } = contentRef.current; + const totalScrollPages = Math.max(1, Math.ceil((scrollHeight - clientHeight) / scrollPageSize) + 1); + const currentPage = Math.min(Math.floor(scrollTop / scrollPageSize), totalScrollPages - 1); + setScrollPage(currentPage); + } + }; + + const getTotalScrollPages = () => { + if (!contentRef.current) return 1; + const { scrollHeight, clientHeight } = contentRef.current; + return Math.max(1, Math.ceil((scrollHeight - clientHeight) / scrollPageSize) + 1); + }; + + // 인쇄 + const handlePrint = () => { + const el = contentRef.current; + if (!el) { window.print(); return; } + const win = window.open('', '_blank'); + if (!win) return; + win.document.write(`${getPageLabel(location.pathname)} - ${t('layout.print')} + + ${el.innerHTML}`); + win.document.close(); + win.print(); + }; + + // 엑셀(CSV) 내보내기 — 현재 화면 테이블 자동 추출 + const handleExcelExport = () => { + const el = contentRef.current; + if (!el) return; + const tables = el.querySelectorAll('table'); + if (tables.length === 0) { alert(t('layout.noExportTable')); return; } + const table = tables[0]; + const rows: string[] = []; + table.querySelectorAll('tr').forEach((tr) => { + const cells: string[] = []; + tr.querySelectorAll('th, td').forEach((td) => { + cells.push(`"${(td.textContent || '').replace(/"/g, '""').trim()}"`); + }); + rows.push(cells.join(',')); + }); + const csv = '\uFEFF' + rows.join('\r\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${getPageLabel(location.pathname) || 'export'}_${new Date().toISOString().slice(0, 10)}.csv`; + a.click(); + URL.revokeObjectURL(url); + }; + + // 파일 다운로드 (현재 페이지 HTML) + const handleDownload = () => { + const el = contentRef.current; + if (!el) return; + const html = `${getPageLabel(location.pathname)}${el.innerHTML}`; + const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${getPageLabel(location.pathname) || 'page'}_${new Date().toISOString().slice(0, 10)}.html`; + a.click(); + URL.revokeObjectURL(url); + }; + + // 그룹 메뉴 접기/펼치기 (다중 그룹 지원) + const [openGroups, setOpenGroups] = useState>(new Set()); + const toggleGroup = (name: string) => setOpenGroups(prev => { + const next = new Set(prev); + next.has(name) ? next.delete(name) : next.add(name); + return next; + }); + + // RBAC + const roleColor = user ? ROLE_COLORS[user.role] : null; + const isSessionWarning = sessionRemaining <= 5 * 60; + + // SFR-02: 공통알림 데이터 + const systemNotices: SystemNotice[] = [ + { + id: 'N-001', type: 'urgent', display: 'banner', title: '서해 NLL 인근 경보 강화', + message: '2026-04-03부터 서해 NLL 인근 해역에 대한 경계 경보가 강화되었습니다.', + startDate: '2026-04-03', endDate: '2026-04-10', targetRoles: ['ADMIN', 'OPERATOR'], dismissible: true, pinned: true, + }, + { + id: 'N-002', type: 'maintenance', display: 'popup', title: '정기 시스템 점검 안내', + message: '2026-04-05(토) 02:00~06:00 시스템 정기점검이 예정되어 있습니다. 점검 중 서비스 이용이 제한될 수 있습니다.', + startDate: '2026-04-03', endDate: '2026-04-05', targetRoles: [], dismissible: true, pinned: false, + }, + { + id: 'N-003', type: 'info', display: 'banner', title: 'AI 탐지 모델 v2.3 업데이트', + message: '다크베셀 탐지 정확도 89%→93% 개선. 환적 탐지 알고리즘 업데이트.', + startDate: '2026-04-01', endDate: '2026-04-15', targetRoles: ['ADMIN', 'ANALYST'], dismissible: true, pinned: false, + }, + ]; + + const handleLogout = () => { + logout(); + navigate('/login'); + }; + + return ( +
+ {/* 사이드바 */} + + + {/* 메인 영역 */} +
+ {/* 헤더 */} +
+
+
+ + +
+
+
+ {/* 경보 */} +
+
+ {t('layout.alertCount', { count: 3 })} +
+ +
+ {/* 언어 토글 */} + + {/* 테마 토글 */} + +
+ {user && ( +
+
+ {user.name.charAt(0)} +
+
+
+ {user.name} + ({user.rank}) +
+
{user.org}
+
+ {roleColor && ( + + {user.role} + + )} +
+ )} +
+
+ + {/* SFR-02: 공통알림 배너 */} + + + {/* SFR-02: 공통 페이지 액션바 (검색, 다운로드, 엑셀, 인쇄) */} +
+ {/* 왼쪽: 페이지명 */} +
{getPageLabel(location.pathname)}
+ + {/* 검색 입력 + 검색 버튼 통합 */} +
+ + setPageSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && pageSearch) { + (window as unknown as { find: (s: string) => boolean }).find?.(pageSearch); + } + }} + placeholder={t('layout.pageSearch')} + className="w-48 bg-surface-overlay border border-slate-700/50 rounded-l-md pl-7 pr-2 py-1 text-[10px] text-label placeholder:text-hint focus:outline-none focus:border-blue-500/50" + /> + +
+ + + + +
+ + {/* 콘텐츠 */} +
+ +
+ + {/* SFR-02: 공통 페이지네이션 (하단) */} +
+ +
+
+ + {/* SFR-02: 공통알림 팝업 */} + +
+ ); +} diff --git a/src/data/areasCodes.json b/src/data/areasCodes.json new file mode 100644 index 0000000..3c96ffe --- /dev/null +++ b/src/data/areasCodes.json @@ -0,0 +1,418 @@ +[ + { + "code": "HA-102", + "major": "서해", + "mid": "광역구역", + "name": "서해 광역2구역", + "authority": "서해지방해양경찰청", + "note": "" + }, + { + "code": "HA-103", + "major": "서해", + "mid": "광역구역", + "name": "중부 광역 1구역", + "authority": "중부지방해양경찰청", + "note": "" + }, + { + "code": "HA-104", + "major": "서해", + "mid": "광역구역", + "name": "중부 광역2구역", + "authority": "중부지방해양경찰청", + "note": "" + }, + { + "code": "HA-105", + "major": "서해", + "mid": "특별경비수역", + "name": "서특단 1구역", + "authority": "서해5도특별경비단", + "note": "서해 NLL 인근" + }, + { + "code": "HA-106", + "major": "서해", + "mid": "특별경비수역", + "name": "서특단 2구역", + "authority": "서해5도특별경비단", + "note": "서해 NLL 인근" + }, + { + "code": "HA-107", + "major": "서해", + "mid": "특별경비수역", + "name": "서특단 3구역", + "authority": "서해5도특별경비단", + "note": "서해 NLL 인근" + }, + { + "code": "HA-108", + "major": "서해", + "mid": "연안구역", + "name": "인천 연안구역", + "authority": "인천해양경찰서", + "note": "" + }, + { + "code": "HA-109", + "major": "서해", + "mid": "내해구역", + "name": "인천 내해1구역", + "authority": "인천해양경찰서", + "note": "" + }, + { + "code": "HA-110", + "major": "서해", + "mid": "내해구역", + "name": "인천 내해2구역", + "authority": "인천해양경찰서", + "note": "" + }, + { + "code": "HA-111", + "major": "서해", + "mid": "연안구역", + "name": "평택 연안구역", + "authority": "평택해양경찰서", + "note": "" + }, + { + "code": "HA-112", + "major": "서해", + "mid": "내해구역", + "name": "평택 내해구역", + "authority": "평택해양경찰서", + "note": "" + }, + { + "code": "HA-113", + "major": "서해", + "mid": "연안구역", + "name": "태안 연안구역", + "authority": "태안해양경찰서", + "note": "태안해안국립공원 인접" + }, + { + "code": "HA-114", + "major": "서해", + "mid": "내해구역", + "name": "태안 내해구역", + "authority": "태안해양경찰서", + "note": "" + }, + { + "code": "HA-115", + "major": "서해", + "mid": "연안구역", + "name": "보령 연안구역", + "authority": "보령해양경찰서", + "note": "" + }, + { + "code": "HA-116", + "major": "서해", + "mid": "내해구역", + "name": "보령 내해구역", + "authority": "보령해양경찰서", + "note": "외연도·원산도 수역" + }, + { + "code": "HA-117", + "major": "서해", + "mid": "연안구역", + "name": "군산 연안구역", + "authority": "군산해양경찰서", + "note": "" + }, + { + "code": "HA-118", + "major": "서해", + "mid": "내해구역", + "name": "군산 내해구역", + "authority": "군산해양경찰서", + "note": "" + }, + { + "code": "HA-119", + "major": "서해", + "mid": "연안구역", + "name": "부안 연안구역", + "authority": "군산해양경찰서", + "note": "군산서 관할" + }, + { + "code": "HA-120", + "major": "서해", + "mid": "내해구역", + "name": "부안 내해구역", + "authority": "군산해양경찰서", + "note": "변산반도 인접" + }, + { + "code": "HA-121", + "major": "서해", + "mid": "연안구역", + "name": "목포 연안구역", + "authority": "목포해양경찰서", + "note": "" + }, + { + "code": "HA-122", + "major": "서해", + "mid": "내해구역", + "name": "목포 내해1구역", + "authority": "목포해양경찰서", + "note": "다도해국립공원" + }, + { + "code": "HA-123", + "major": "서해", + "mid": "내해구역", + "name": "목포 내해2구역", + "authority": "목포해양경찰서", + "note": "" + }, + { + "code": "HA-124", + "major": "서해", + "mid": "특별경비수역", + "name": "목포 특별경비수역", + "authority": "목포해양경찰서", + "note": "" + }, + { + "code": "HA-201", + "major": "남해", + "mid": "광역구역", + "name": "남해 광역1구역", + "authority": "남해지방해양경찰청", + "note": "" + }, + { + "code": "HA-202", + "major": "남해", + "mid": "광역구역", + "name": "남해 광역2구역", + "authority": "남해지방해양경찰청", + "note": "" + }, + { + "code": "HA-203", + "major": "남해", + "mid": "연안구역", + "name": "완도 연안구역", + "authority": "완도해양경찰서", + "note": "" + }, + { + "code": "HA-204", + "major": "남해", + "mid": "내해구역", + "name": "완도 내해구역", + "authority": "완도해양경찰서", + "note": "청산도·추자도 수역" + }, + { + "code": "HA-205", + "major": "남해", + "mid": "연안구역", + "name": "여수 연안구역", + "authority": "여수해양경찰서", + "note": "" + }, + { + "code": "HA-206", + "major": "남해", + "mid": "내해구역", + "name": "여수 내해구역", + "authority": "여수해양경찰서", + "note": "여자만·거문도 수역" + }, + { + "code": "HA-207", + "major": "남해", + "mid": "연안구역", + "name": "통영 연안구역", + "authority": "통영해양경찰서", + "note": "" + }, + { + "code": "HA-208", + "major": "남해", + "mid": "특별경비수역", + "name": "통영 특별 경비수역", + "authority": "통영해양경찰서", + "note": "한려해상국립공원" + }, + { + "code": "HA-209", + "major": "남해", + "mid": "연안구역", + "name": "사천 연안구역", + "authority": "사천해양경찰서", + "note": "" + }, + { + "code": "HA-210", + "major": "남해", + "mid": "연안구역", + "name": "창원 연안구역", + "authority": "창원해양경찰서", + "note": "" + }, + { + "code": "HA-211", + "major": "남해", + "mid": "연안구역", + "name": "부산 연안구역", + "authority": "부산해양경찰서", + "note": "" + }, + { + "code": "HA-212", + "major": "남해", + "mid": "연안구역", + "name": "울산 연안구역", + "authority": "울산해양경찰서", + "note": "" + }, + { + "code": "HA-213", + "major": "남해", + "mid": "내해구역", + "name": "울산 내해구역", + "authority": "울산해양경찰서", + "note": "" + }, + { + "code": "HA-301", + "major": "동해", + "mid": "광역구역", + "name": "동해 광역1구역", + "authority": "동해지방해양경찰청", + "note": "" + }, + { + "code": "HA-302", + "major": "동해", + "mid": "광역구역", + "name": "동해 광역2구역", + "authority": "동해지방해양경찰청", + "note": "EEZ 동해측" + }, + { + "code": "HA-303", + "major": "동해", + "mid": "연안구역", + "name": "속초 연안구역", + "authority": "속초해양경찰서", + "note": "" + }, + { + "code": "HA-304", + "major": "동해", + "mid": "내해구역", + "name": "속초 내해구역", + "authority": "속초해양경찰서", + "note": "" + }, + { + "code": "HA-305", + "major": "동해", + "mid": "연안구역", + "name": "동해 연안구역", + "authority": "동해해양경찰서", + "note": "" + }, + { + "code": "HA-306", + "major": "동해", + "mid": "내해구역", + "name": "동해 내해구역", + "authority": "동해해양경찰서", + "note": "" + }, + { + "code": "HA-307", + "major": "동해", + "mid": "연안구역", + "name": "울진 연안구역", + "authority": "울진해양경찰서", + "note": "" + }, + { + "code": "HA-308", + "major": "동해", + "mid": "내해구역", + "name": "울진 내해구역", + "authority": "울진해양경찰서", + "note": "왕돌초 수역" + }, + { + "code": "HA-309", + "major": "동해", + "mid": "연안구역", + "name": "포항 연안구역", + "authority": "포항해양경찰서", + "note": "" + }, + { + "code": "HA-310", + "major": "동해", + "mid": "내해구역", + "name": "포항 내해구역", + "authority": "포항해양경찰서", + "note": "영일만 수역" + }, + { + "code": "HA-401", + "major": "제주", + "mid": "광역구역", + "name": "제주 광역 1구역", + "authority": "제주지방해양경찰청", + "note": "제주 북부 EEZ" + }, + { + "code": "HA-402", + "major": "제주", + "mid": "광역구역", + "name": "제주 광역 2구역", + "authority": "제주지방해양경찰청", + "note": "제주 남부 EEZ·이어도 수역" + }, + { + "code": "HA-403", + "major": "제주", + "mid": "내해구역", + "name": "제주 내해구역", + "authority": "제주해양경찰서", + "note": "" + }, + { + "code": "HA-404", + "major": "제주", + "mid": "연안구역", + "name": "제주 연안구역", + "authority": "제주해양경찰서", + "note": "" + }, + { + "code": "HA-405", + "major": "제주", + "mid": "연안구역", + "name": "서귀포 연안구역", + "authority": "서귀포해양경찰서", + "note": "" + }, + { + "code": "HA-501", + "major": "광역(원해)", + "mid": "광역구역", + "name": "광역구역", + "authority": "해양경찰청", + "note": "EEZ 외측·공해 수역" + } +] \ No newline at end of file diff --git a/src/data/commonCodes.ts b/src/data/commonCodes.ts new file mode 100644 index 0000000..0edcad0 --- /dev/null +++ b/src/data/commonCodes.ts @@ -0,0 +1,138 @@ +/* + * SFR-02: 시스템 기본 환경설정 및 공통 기능 + * 공통코드 기준정보 통합 관리 모듈 + * + * 4개 분류 | 875개 코드 + * - 해역분류 52개 (해양경찰청 관할 기준) + * - 어종 578개 (국립수산과학원 공식 어종코드) + * - 어업유형 59개 (수산업법 허가·면허 구분) + * - 선박유형 186개 (MDA 5개 출처 통합) + */ + +import areaData from './areasCodes.json'; +import speciesData from './speciesCodes.json'; +import fisheryData from './fisheryCodes.json'; +import vesselTypeData from './vesselTypeCodes.json'; + +// ─── 타입 정의 ────────────────────────── + +export interface AreaCode { + code: string; // HA-101 ~ HA-501 + major: string; // 서해, 남해, 동해, 제주, 광역(원해) + mid: string; // 광역구역, 특별경비수역, 연안구역, 내해구역 + name: string; // 소분류(해역명) + authority: string; // 관할기관 + note: string; +} + +export interface SpeciesCode { + code: string; // 10000 ~ 90000 계열 + major: string; // 대분류: 어류, 패류, 갑각류, 연체류, 해조류 등 + mid: string; // 중분류(상위) + name: string; // 어종명 + nameEn: string; // 영문명 + scientific: string; // 학명 + area: string; // 주요 서식해역 + active: boolean; // 사용여부 + fishing: boolean; // 낚시연계 여부 +} + +export interface FisheryCode { + code: string; // FV-101 / FT-xxx + major: string; // 근해어업, 연안어업, 구획어업, 마을어업, 양식어업, 원양어업, 기타어업 + mid: string; // 트롤, 선망, 연승, 자망 등 + name: string; // 어업유형명 + target: string; // 주요 어획대상 + permit: string; // 허가/면허 + law: string; // 수산업법 근거 +} + +export interface VesselTypeCode { + code: string; // VT-100 ~ VT-905 + srcCode: string; // 원본코드 (ship_ty_cd) + srcDetail: string; // 원본세부코드 (src_ty_cd) + major: string; // 어선, 여객선, 화물선, 유조선, 관공선, 함정, 항공기, 기타선 + mid: string; // 중분류 + name: string; // 선박유형명 + source: string; // 데이터출처: RRA, GIC, PMS, AIS, KSU + tonnage: string; // 톤수기준 + purpose: string; // 주요용도 + aisCode: string; // AIS/IMO 코드 + note: string; +} + +// ─── 코드 데이터 ───────────────────────── + +export const AREA_CODES: AreaCode[] = areaData as AreaCode[]; +export const SPECIES_CODES: SpeciesCode[] = speciesData as SpeciesCode[]; +export const FISHERY_CODES: FisheryCode[] = fisheryData as FisheryCode[]; +export const VESSEL_TYPE_CODES: VesselTypeCode[] = vesselTypeData as VesselTypeCode[]; + +// ─── 조회 유틸리티 ────────────────────── + +/** 코드번호로 해역 조회 */ +export function getArea(code: string): AreaCode | undefined { + return AREA_CODES.find((a) => a.code === code); +} + +/** 코드번호로 어종 조회 */ +export function getSpecies(code: string): SpeciesCode | undefined { + return SPECIES_CODES.find((s) => s.code === code); +} + +/** 코드번호로 어업유형 조회 */ +export function getFishery(code: string): FisheryCode | undefined { + return FISHERY_CODES.find((f) => f.code === code); +} + +/** 코드번호로 선박유형 조회 */ +export function getVesselType(code: string): VesselTypeCode | undefined { + return VESSEL_TYPE_CODES.find((v) => v.code === code); +} + +/** 해역 대분류 목록 (중복 제거) */ +export function getAreaMajors(): string[] { + return [...new Set(AREA_CODES.map((a) => a.major))]; +} + +/** 어종 대분류 목록 (중복 제거) */ +export function getSpeciesMajors(): string[] { + return [...new Set(SPECIES_CODES.map((s) => s.major).filter(Boolean))]; +} + +/** 어업유형 대분류 목록 (중복 제거) */ +export function getFisheryMajors(): string[] { + return [...new Set(FISHERY_CODES.map((f) => f.major))]; +} + +/** 선박유형 대분류 목록 (중복 제거) */ +export function getVesselTypeMajors(): string[] { + return [...new Set(VESSEL_TYPE_CODES.map((v) => v.major))]; +} + +/** 대분류로 필터링 */ +export function filterByMajor(items: T[], major: string): T[] { + if (!major) return items; + return items.filter((i) => i.major === major); +} + +/** 이름 또는 코드로 검색 */ +export function searchCodes( + items: T[], query: string +): T[] { + if (!query) return items; + const q = query.toLowerCase(); + return items.filter( + (i) => i.code.toLowerCase().includes(q) || i.name.toLowerCase().includes(q) + ); +} + +// ─── 코드 분류별 통계 ─────────────────── + +export const CODE_STATS = { + areas: AREA_CODES.length, + species: SPECIES_CODES.length, + fishery: FISHERY_CODES.length, + vesselTypes: VESSEL_TYPE_CODES.length, + total: AREA_CODES.length + SPECIES_CODES.length + FISHERY_CODES.length + VESSEL_TYPE_CODES.length, +}; diff --git a/src/data/fisheryCodes.json b/src/data/fisheryCodes.json new file mode 100644 index 0000000..cb0a37b --- /dev/null +++ b/src/data/fisheryCodes.json @@ -0,0 +1,533 @@ +[ + { + "code": "FV-101", + "major": "근해어업", + "mid": "트롤", + "name": "쌍끌이 대형저인망", + "target": "명태, 오징어, 새우", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-102", + "major": "근해어업", + "mid": "트롤", + "name": "외끌이 저인망", + "target": "저서어류", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-103", + "major": "근해어업", + "mid": "트롤", + "name": "중형 저인망", + "target": "고등어, 오징어", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-104", + "major": "근해어업", + "mid": "선망", + "name": "대형선망", + "target": "고등어, 전갱이, 멸치", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-105", + "major": "근해어업", + "mid": "선망", + "name": "소형선망", + "target": "고등어, 멸치", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-106", + "major": "근해어업", + "mid": "연승", + "name": "근해연승", + "target": "참치, 상어류", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-107", + "major": "근해어업", + "mid": "자망", + "name": "근해자망", + "target": "꽃게, 조기, 갈치", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-108", + "major": "근해어업", + "mid": "통발", + "name": "장어통발", + "target": "장어", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-109", + "major": "근해어업", + "mid": "통발", + "name": "문어단지", + "target": "문어", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-110", + "major": "근해어업", + "mid": "채낚기", + "name": "오징어채낚기", + "target": "오징어", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-111", + "major": "근해어업", + "mid": "형망", + "name": "근해형망", + "target": "새우, 패류", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-112", + "major": "근해어업", + "mid": "안강망", + "name": "근해안강망", + "target": "멸치", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-113", + "major": "근해어업", + "mid": "봉수망", + "name": "봉수망", + "target": "자리돔", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-114", + "major": "근해어업", + "mid": "잠수", + "name": "잠수기어업", + "target": "전복, 해삼, 성게", + "permit": "허가", + "law": "수산업법 제41조" + }, + { + "code": "FT-201", + "major": "연안어업", + "mid": "자망", + "name": "연안자망", + "target": "숭어, 넙치, 도미", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-202", + "major": "연안어업", + "mid": "자망", + "name": "삼중자망", + "target": "잡어", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-203", + "major": "연안어업", + "mid": "선망", + "name": "연안선망", + "target": "멸치", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-204", + "major": "연안어업", + "mid": "안강망", + "name": "연안안강망", + "target": "멸치", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-205", + "major": "연안어업", + "mid": "연승", + "name": "연안연승", + "target": "도미", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-206", + "major": "연안어업", + "mid": "통발", + "name": "연안통발", + "target": "낙지, 게류", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-207", + "major": "연안어업", + "mid": "채낚기", + "name": "연안채낚기", + "target": "오징어", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-208", + "major": "연안어업", + "mid": "들망", + "name": "연안들망", + "target": "잡어", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-209", + "major": "연안어업", + "mid": "형망", + "name": "연안형망", + "target": "패류", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-210", + "major": "연안어업", + "mid": "복합", + "name": "연안복합", + "target": "다종", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-211", + "major": "연안어업", + "mid": "해조채취", + "name": "해조채취", + "target": "미역, 김", + "permit": "허가", + "law": "수산업법 제47조" + }, + { + "code": "FT-301", + "major": "구획어업", + "mid": "정치망", + "name": "정치망어업", + "target": "방어, 고등어, 연어", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-302", + "major": "구획어업", + "mid": "정치망", + "name": "각망", + "target": "잡어", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-303", + "major": "구획어업", + "mid": "정치망", + "name": "안강망(구획)", + "target": "멸치", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-304", + "major": "구획어업", + "mid": "정치망", + "name": "낭장망", + "target": "멸치, 잡어", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-305", + "major": "구획어업", + "mid": "정치망", + "name": "장망", + "target": "연어", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-306", + "major": "구획어업", + "mid": "이동형", + "name": "문어단지(구획)", + "target": "문어", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-307", + "major": "구획어업", + "mid": "이동형", + "name": "패류형망", + "target": "패류", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-308", + "major": "구획어업", + "mid": "이동형", + "name": "새우조망", + "target": "새우", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-401", + "major": "마을어업", + "mid": "패류채취", + "name": "마을어업(패류)", + "target": "바지락, 굴, 꼬막", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-402", + "major": "마을어업", + "mid": "해조채취", + "name": "마을어업(해조류)", + "target": "미역, 김, 톳", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-403", + "major": "마을어업", + "mid": "복합", + "name": "마을공동어업", + "target": "패류·해조류 복합", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-404", + "major": "마을어업", + "mid": "한정", + "name": "마을어업(한정)", + "target": "제한적 어획대상", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-501", + "major": "양식어업", + "mid": "가두리", + "name": "어류등양식업", + "target": "넙치, 조피볼락, 참돔", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-502", + "major": "양식어업", + "mid": "수하식", + "name": "패류양식업", + "target": "굴, 홍합, 전복", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-503", + "major": "양식어업", + "mid": "수면", + "name": "해조류양식", + "target": "미역, 김, 다시마", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-504", + "major": "양식어업", + "mid": "복합", + "name": "복합양식업", + "target": "패류+어류 2종 이상", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-505", + "major": "양식어업", + "mid": "협동", + "name": "협동양식업", + "target": "미역, 다시마, 김", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-506", + "major": "양식어업", + "mid": "수면", + "name": "김 양식", + "target": "김", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-507", + "major": "양식어업", + "mid": "육상", + "name": "수조식 양식", + "target": "넙치, 새우", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-508", + "major": "양식어업", + "mid": "종묘", + "name": "종묘 생산", + "target": "치어·치패", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-509", + "major": "양식어업", + "mid": "한정", + "name": "양식어업(한정)", + "target": "제한적 양식종", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-510", + "major": "양식어업", + "mid": "한정", + "name": "양식어업(한정)-복합", + "target": "2종 이상 한정 양식", + "permit": "면허", + "law": "수산업법 제8조" + }, + { + "code": "FT-601", + "major": "원양어업", + "mid": "트롤", + "name": "원양트롤", + "target": "명태, 대구", + "permit": "허가", + "law": "원양산업발전법" + }, + { + "code": "FT-602", + "major": "원양어업", + "mid": "트롤", + "name": "새우트롤", + "target": "새우", + "permit": "허가", + "law": "원양산업발전법" + }, + { + "code": "FT-603", + "major": "원양어업", + "mid": "선망", + "name": "원양선망", + "target": "참치", + "permit": "허가", + "law": "원양산업발전법" + }, + { + "code": "FT-604", + "major": "원양어업", + "mid": "연승", + "name": "참치연승", + "target": "참다랑어, 황다랑어", + "permit": "허가", + "law": "원양산업발전법" + }, + { + "code": "FT-605", + "major": "원양어업", + "mid": "자망", + "name": "원양자망", + "target": "명태", + "permit": "허가", + "law": "원양산업발전법" + }, + { + "code": "FT-606", + "major": "원양어업", + "mid": "채낚기", + "name": "오징어채낚기(원양)", + "target": "살오징어", + "permit": "허가", + "law": "원양산업발전법" + }, + { + "code": "FT-607", + "major": "원양어업", + "mid": "모선식", + "name": "모선식어업", + "target": "다종", + "permit": "허가", + "law": "원양산업발전법" + }, + { + "code": "FT-701", + "major": "기타어업", + "mid": "낚시", + "name": "낚시어업", + "target": "다종", + "permit": "신고", + "law": "수상레저안전법" + }, + { + "code": "FT-702", + "major": "기타어업", + "mid": "맨손", + "name": "맨손어업", + "target": "패류, 해조류", + "permit": "신고", + "law": "수산업법" + }, + { + "code": "FT-703", + "major": "기타어업", + "mid": "시험", + "name": "시험·연구어업", + "target": "다종", + "permit": "허가", + "law": "수산업법 제57조" + }, + { + "code": "FT-704", + "major": "기타어업", + "mid": "가공", + "name": "수산물가공", + "target": "-", + "permit": "신고", + "law": "-" + }, + { + "code": "FT-705", + "major": "기타어업", + "mid": "운반", + "name": "어획물운반", + "target": "-", + "permit": "신고", + "law": "-" + } +] \ No newline at end of file diff --git a/src/data/mock/enforcement.ts b/src/data/mock/enforcement.ts new file mode 100644 index 0000000..9e9b5af --- /dev/null +++ b/src/data/mock/enforcement.ts @@ -0,0 +1,42 @@ +export interface EnforcementRecord { + id: string; + date: string; + zone: string; + vessel: string; + violation: string; + action: string; + aiMatch: string; + result: string; +} + +export interface EnforcementPlanRecord { + id: string; + zone: string; + lat: number; + lng: number; + risk: number; + period: string; + ships: string; + crew: number; + status: string; + alert: string; +} + +/** Enforcement history (6 records) — src/features/enforcement/EnforcementHistory.tsx */ +export const MOCK_ENFORCEMENT_RECORDS: EnforcementRecord[] = [ + { id: 'ENF-001', date: '2026-04-03 08:47', zone: 'EEZ 북부', vessel: '鲁荣渔56555', violation: 'EEZ 침범', action: '나포', aiMatch: '일치', result: '처벌' }, + { id: 'ENF-002', date: '2026-04-03 07:23', zone: '서해 NLL', vessel: '津塘渔03966', violation: '무허가 조업', action: '검문·경고', aiMatch: '일치', result: '경고' }, + { id: 'ENF-003', date: '2026-04-02 22:15', zone: '서해 5도', vessel: '浙岱渔02856 외 7척', violation: '선단 침범', action: '퇴거 조치', aiMatch: '일치', result: '퇴거' }, + { id: 'ENF-004', date: '2026-04-02 14:30', zone: 'EEZ 서부', vessel: '冀黄港渔05001', violation: '불법환적', action: '증거 수집', aiMatch: '일치', result: '수사 의뢰' }, + { id: 'ENF-005', date: '2026-04-01 09:00', zone: '남해 연안', vessel: '한국어선-03', violation: '조업구역 이탈', action: '검문', aiMatch: '불일치', result: '오탐(정상)' }, + { id: 'ENF-006', date: '2026-03-30 16:40', zone: '동해 EEZ', vessel: '鲁荣渔51277', violation: '고속 도주', action: '추적·나포', aiMatch: '일치', result: '처벌' }, +]; + +/** Enforcement plans (5 plans) — src/features/risk-assessment/EnforcementPlan.tsx */ +export const MOCK_ENFORCEMENT_PLANS: EnforcementPlanRecord[] = [ + { id: 'EP-001', zone: '서해 NLL', lat: 37.80, lng: 124.90, risk: 92, period: '04-04 00:00~06:00', ships: '3001함, 3005함', crew: 48, status: '확정', alert: '경보 발령' }, + { id: 'EP-002', zone: 'EEZ 북부', lat: 37.20, lng: 124.63, risk: 78, period: '04-04 06:00~12:00', ships: '3009함', crew: 24, status: '확정', alert: '주의' }, + { id: 'EP-003', zone: '서해 5도', lat: 37.50, lng: 124.60, risk: 72, period: '04-04 12:00~18:00', ships: '서특단 1정', crew: 18, status: '계획중', alert: '주의' }, + { id: 'EP-004', zone: 'EEZ 서부', lat: 36.00, lng: 123.80, risk: 65, period: '04-05 00:00~06:00', ships: '3001함', crew: 24, status: '계획중', alert: '-' }, + { id: 'EP-005', zone: '남해 외해', lat: 34.20, lng: 127.50, risk: 45, period: '04-05 06:00~12:00', ships: '미정', crew: 0, status: '검토중', alert: '-' }, +]; diff --git a/src/data/mock/events.ts b/src/data/mock/events.ts new file mode 100644 index 0000000..8057d2b --- /dev/null +++ b/src/data/mock/events.ts @@ -0,0 +1,290 @@ +/** + * Shared mock data: events & alerts + * + * Sources: + * - EventList.tsx EVENTS (15 records) — primary + * - Dashboard.tsx TIMELINE_EVENTS (10) + * - MonitoringDashboard.tsx EVENTS (6) + * - AIAlert.tsx DATA (5 alerts) + * - MobileService.tsx ALERTS (3) + */ + +// ──────────────────────────────────────────── +// Event record (EventList.tsx as primary, supplemented with Dashboard titles/details) +// ──────────────────────────────────────────── +export interface EventRecord { + id: string; + time: string; + level: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; + type: string; + title: string; + detail: string; + vesselName?: string; + mmsi?: string; + area?: string; + lat?: number; + lng?: number; + speed?: number; + status?: string; + assignee?: string; +} + +export const MOCK_EVENTS: EventRecord[] = [ + { + id: 'EVT-0001', + time: '2026-04-03 08:47:12', + level: 'CRITICAL', + type: 'EEZ 침범', + title: 'EEZ 침범 탐지', + detail: '鲁荣渔56555 외 2척 — N37°12\' E124°38\' 진입', + vesselName: '鲁荣渔56555', + mmsi: '412345678', + area: 'EEZ 북부', + lat: 37.2012, + lng: 124.6345, + speed: 8.2, + status: '추적 중', + assignee: '3001함', + }, + { + id: 'EVT-0002', + time: '2026-04-03 08:32:05', + level: 'HIGH', + type: '다크베셀', + title: '다크베셀 출현', + detail: 'MMSI 미상 선박 3척 — 서해 NLL 인근 AIS 소실', + vesselName: '미상선박-A', + mmsi: '미상', + area: '서해 NLL', + lat: 37.7512, + lng: 125.0234, + speed: 6.1, + status: '감시 중', + assignee: '상황실', + }, + { + id: 'EVT-0003', + time: '2026-04-03 08:15:33', + level: 'CRITICAL', + type: '선단밀집', + title: '선단 밀집 경보', + detail: '중국어선 14척 밀집 — N36°48\' E124°22\' 반경 2nm', + vesselName: '선단(14척)', + mmsi: '다수', + area: 'EEZ 서부', + lat: 36.8001, + lng: 124.3678, + speed: 4.5, + status: '경보 발령', + assignee: '서해청', + }, + { + id: 'EVT-0004', + time: '2026-04-03 07:58:44', + level: 'MEDIUM', + type: '불법환적', + title: '불법환적 의심', + detail: '冀黄港渔05001 + 운반선 접현 30분 이상', + vesselName: '冀黄港渔05001', + mmsi: '412987654', + area: '서해 중부', + lat: 36.4789, + lng: 124.2234, + speed: 0.3, + status: '확인 중', + assignee: '분석팀', + }, + { + id: 'EVT-0005', + time: '2026-04-03 07:41:18', + level: 'HIGH', + type: 'MMSI 변조', + title: 'MMSI 변조 탐지', + detail: '浙甬渔60651 — MMSI 3회 변경 이력 감지', + vesselName: '浙甬渔60651', + mmsi: '412111222', + area: 'EEZ 남부', + lat: 35.8678, + lng: 125.5012, + speed: 5.8, + status: '감시 중', + assignee: '상황실', + }, + { + id: 'EVT-0006', + time: '2026-04-03 07:23:01', + level: 'LOW', + type: '검문 완료', + title: '함정 검문 완료', + detail: '3009함 — 津塘渔03966 검문 완료, 경고 조치', + vesselName: '津塘渔03966', + mmsi: '412333444', + area: '서해 북부', + lat: 37.5012, + lng: 124.7890, + speed: 0, + status: '완료', + assignee: '3009함', + }, + { + id: 'EVT-0007', + time: '2026-04-03 07:05:55', + level: 'MEDIUM', + type: 'AIS 재송출', + title: 'AIS 재송출', + detail: '辽庄渔55567 — 4시간 소실 후 재송출', + vesselName: '辽庄渔55567', + mmsi: '412555666', + area: 'EEZ 북부', + lat: 37.3456, + lng: 124.8901, + speed: 3.2, + status: '확인 완료', + assignee: '상황실', + }, + { + id: 'EVT-0008', + time: '2026-04-03 06:48:22', + level: 'CRITICAL', + type: 'EEZ 침범', + title: '긴급 침범 경보', + detail: '浙岱渔02856 외 7척 — 서해 5도 수역 진입', + vesselName: '浙岱渔02856', + mmsi: '412777888', + area: '서해 5도', + lat: 37.0567, + lng: 124.9234, + speed: 4.5, + status: '추적 중', + assignee: '서특단', + }, + { + id: 'EVT-0009', + time: '2026-04-03 06:30:00', + level: 'LOW', + type: '정기 보고', + title: '정기 보고', + detail: '전 해역 야간 감시 결과 보고 완료', + vesselName: undefined, + mmsi: undefined, + area: '전 해역', + status: '완료', + assignee: '상황실', + }, + { + id: 'EVT-0010', + time: '2026-04-03 06:12:33', + level: 'HIGH', + type: '속력 이상', + title: '속력 이상 탐지', + detail: '鲁荣渔51277 — 18kt 고속 이동, 도주 패턴', + vesselName: '鲁荣渔51277', + mmsi: '412999000', + area: '동해 중부', + lat: 36.2512, + lng: 130.0890, + speed: 18.1, + status: '추적 중', + assignee: '동해청', + }, + { + id: 'EVT-0011', + time: '2026-04-03 05:45:10', + level: 'MEDIUM', + type: 'AIS 소실', + title: 'AIS 소실', + detail: '浙甬渔30112 남해 외해 AIS 소실', + vesselName: '浙甬渔30112', + mmsi: '412444555', + area: '남해 외해', + lat: 34.1234, + lng: 128.5678, + status: '감시 중', + assignee: '남해청', + }, + { + id: 'EVT-0012', + time: '2026-04-03 05:20:48', + level: 'HIGH', + type: '불법환적', + title: '불법환적 의심', + detail: '冀黄港渔03012 EEZ 서부 환적 의심', + vesselName: '冀黄港渔03012', + mmsi: '412666777', + area: 'EEZ 서부', + lat: 36.5678, + lng: 124.1234, + speed: 0.5, + status: '확인 중', + assignee: '분석팀', + }, + { + id: 'EVT-0013', + time: '2026-04-03 04:55:30', + level: 'LOW', + type: '구역 이탈', + title: '구역 이탈', + detail: '한국어선-12 연안 구역 이탈 경고', + vesselName: '한국어선-12', + mmsi: '440123456', + area: '연안 구역', + lat: 35.4567, + lng: 129.3456, + speed: 7.0, + status: '경고 완료', + assignee: '포항서', + }, + { + id: 'EVT-0014', + time: '2026-04-03 04:30:15', + level: 'CRITICAL', + type: 'EEZ 침범', + title: 'EEZ 침범 — 나포 작전', + detail: '鲁威渔15028 EEZ 북부 나포 작전 진행', + vesselName: '鲁威渔15028', + mmsi: '412888999', + area: 'EEZ 북부', + lat: 37.4012, + lng: 124.5567, + speed: 6.9, + status: '나포 작전', + assignee: '3001함', + }, + { + id: 'EVT-0015', + time: '2026-04-03 04:10:00', + level: 'MEDIUM', + type: 'MMSI 변조', + title: 'MMSI 변조 의심', + detail: '浙甬渔99871 남해 연안 MMSI 변조 의심', + vesselName: '浙甬渔99871', + mmsi: '412222333', + area: '남해 연안', + lat: 34.5678, + lng: 127.8901, + speed: 4.2, + status: '확인 중', + assignee: '상황실', + }, +]; + +// ──────────────────────────────────────────── +// Alert records (AIAlert.tsx as primary) +// ──────────────────────────────────────────── +export interface AlertRecord { + id: string; + time: string; + type: string; + location: string; + confidence: number; + target: string; + status: string; +} + +export const MOCK_ALERTS: AlertRecord[] = [ + { id: 'ALR-001', time: '08:47:12', type: 'EEZ 침범', location: 'N37.20 E124.63', confidence: 96, target: '3001함, 상황실', status: '수신확인' }, + { id: 'ALR-002', time: '08:32:05', type: '다크베셀', location: 'N37.75 E125.02', confidence: 91, target: '상황실', status: '수신확인' }, + { id: 'ALR-003', time: '08:15:33', type: '선단밀집', location: 'N36.80 E124.37', confidence: 88, target: '서특단, 상황실', status: '발송완료' }, + { id: 'ALR-004', time: '07:58:44', type: '불법환적', location: 'N36.48 E124.22', confidence: 82, target: '3005함', status: '수신확인' }, + { id: 'ALR-005', time: '07:41:18', type: 'MMSI변조', location: 'N35.87 E125.50', confidence: 94, target: '상황실', status: '미수신' }, +]; diff --git a/src/data/mock/gear.ts b/src/data/mock/gear.ts new file mode 100644 index 0000000..e79092e --- /dev/null +++ b/src/data/mock/gear.ts @@ -0,0 +1,23 @@ +export interface GearRecord { + id: string; + type: string; + owner: string; + zone: string; + status: string; + permit: string; + installed: string; + lastSignal: string; + risk: string; + lat: number; + lng: number; +} + +/** Gear detection data (6 records) — src/features/detection/GearDetection.tsx */ +export const MOCK_GEAR: GearRecord[] = [ + { id: 'GR-001', type: '저층트롤', owner: '鲁荣渔56555', zone: 'EEZ 북부', status: '불법 의심', permit: '무허가', installed: '2026-04-01', lastSignal: '2h 전', risk: '고위험', lat: 37.20, lng: 124.63 }, + { id: 'GR-002', type: '유자망', owner: '浙甬渔60651', zone: '서해 5도', status: '불법 의심', permit: '기간 초과', installed: '2026-03-15', lastSignal: '30분 전', risk: '고위험', lat: 37.55, lng: 124.75 }, + { id: 'GR-003', type: '통발', owner: '한국어선-05', zone: '남해 연안', status: '정상', permit: '유효', installed: '2026-04-02', lastSignal: '10분 전', risk: '안전', lat: 34.45, lng: 127.80 }, + { id: 'GR-004', type: '선망', owner: '冀黄港渔05001', zone: 'EEZ 서부', status: '확인 중', permit: '구역 이탈', installed: '2026-03-28', lastSignal: '4h 전', risk: '중위험', lat: 36.80, lng: 124.37 }, + { id: 'GR-005', type: '연승', owner: '한국어선-12', zone: '동해 연안', status: '정상', permit: '유효', installed: '2026-04-01', lastSignal: '5분 전', risk: '안전', lat: 36.77, lng: 129.42 }, + { id: 'GR-006', type: '유자망(대형)', owner: '미상', zone: '서해 NLL', status: '불법 확정', permit: '무허가', installed: '미상', lastSignal: '소실', risk: '고위험', lat: 37.82, lng: 124.95 }, +]; diff --git a/src/data/mock/kpi.ts b/src/data/mock/kpi.ts new file mode 100644 index 0000000..f1c5b6d --- /dev/null +++ b/src/data/mock/kpi.ts @@ -0,0 +1,51 @@ +export interface KpiMetric { + id: string; + label: string; + value: number; + prev?: number; + unit?: string; + description?: string; +} + +export interface MonthlyTrend { + month: string; + enforce: number; + detect: number; + accuracy: number; +} + +export interface ViolationType { + type: string; + count: number; + pct: number; +} + +/** Dashboard KPI (6 metrics) — src/features/dashboard/Dashboard.tsx */ +export const MOCK_KPI_METRICS: KpiMetric[] = [ + { id: 'KPI-RT', label: '실시간 탐지', value: 47, prev: 42, description: 'AI 감시 탐지 선박' }, + { id: 'KPI-EEZ', label: 'EEZ 침범', value: 18, prev: 21, description: '배타적경제수역 침범' }, + { id: 'KPI-DV', label: '다크베셀', value: 12, prev: 9, description: 'AIS 미송출 선박' }, + { id: 'KPI-TR', label: '불법환적 의심', value: 8, prev: 6, description: '해상전재 의심 건' }, + { id: 'KPI-TK', label: '추적 중', value: 15, prev: 13, description: '함정 추적 진행' }, + { id: 'KPI-EN', label: '나포/검문', value: 3, prev: 2, description: '금일 단속 실적' }, +]; + +/** Statistics monthly trend (7 months) — src/features/statistics/Statistics.tsx */ +export const MOCK_MONTHLY_TRENDS: MonthlyTrend[] = [ + { month: '10월', enforce: 42, detect: 128, accuracy: 81 }, + { month: '11월', enforce: 38, detect: 145, accuracy: 84 }, + { month: '12월', enforce: 55, detect: 167, accuracy: 86 }, + { month: '1월', enforce: 61, detect: 189, accuracy: 88 }, + { month: '2월', enforce: 48, detect: 156, accuracy: 89 }, + { month: '3월', enforce: 52, detect: 172, accuracy: 90 }, + { month: '4월', enforce: 15, detect: 67, accuracy: 93 }, +]; + +/** Statistics violation types (5 types) — src/features/statistics/Statistics.tsx */ +export const MOCK_VIOLATION_TYPES: ViolationType[] = [ + { type: 'EEZ 침범', count: 124, pct: 35 }, + { type: '다크베셀', count: 89, pct: 25 }, + { type: 'MMSI 변조', count: 64, pct: 18 }, + { type: '불법환적', count: 43, pct: 12 }, + { type: '어구 불법', count: 35, pct: 10 }, +]; diff --git a/src/data/mock/patrols.ts b/src/data/mock/patrols.ts new file mode 100644 index 0000000..a0f44df --- /dev/null +++ b/src/data/mock/patrols.ts @@ -0,0 +1,241 @@ +/** + * Shared mock data: patrol ships, routes, scenarios, coverage + * + * Sources: + * - Dashboard.tsx PATROL_SHIPS (6) + * - PatrolRoute.tsx SHIPS (4), ROUTES, SCENARIOS (3) + * - FleetOptimization.tsx FLEET (5), COVERAGE (6), FLEET_ROUTES + * - ShipAgent.tsx DATA (6 agents) + */ + +// ──────────────────────────────────────────── +// Patrol ship interface +// ──────────────────────────────────────────── +export interface PatrolShip { + id: string; // P-3001 등 + name: string; // 3001함 + shipClass: string; // 태극급, 삼봉급 등 + speed: number; // max knots + status: string; // 작전중, 가용, 정비중 + lat: number; + lng: number; + fuel: number; // 0-100% + zone?: string; + target?: string; +} + +// ──────────────────────────────────────────── +// Waypoint / route types +// ──────────────────────────────────────────── +export interface Waypoint { + id: string; + name: string; + lat: number; + lng: number; + eta: string; + desc?: string; +} + +export interface RouteSummary { + dist: string; + time: string; + fuel: string; + grids: string; +} + +export interface PatrolRoute { + waypoints: Waypoint[]; + summary: RouteSummary; +} + +export interface PatrolScenario { + name: string; + weight: { risk: number; fuel: number; time: number }; + score: number; +} + +export interface CoverageZone { + zone: string; + current: number; + optimized: number; + ships: number; + lat: number; + lng: number; + radius: number; +} + +// ──────────────────────────────────────────── +// Patrol ships (merged & deduplicated) +// ──────────────────────────────────────────── +export const MOCK_PATROL_SHIPS: PatrolShip[] = [ + { + id: 'P-3001', + name: '3001함', + shipClass: '태극급', + speed: 28, + status: '추적중', + lat: 37.69, + lng: 124.60, + fuel: 78, + zone: 'NLL', + target: '鲁荣渔56555', + }, + { + id: 'P-3005', + name: '3005함', + shipClass: '태극급', + speed: 28, + status: '가용', + lat: 37.20, + lng: 124.63, + fuel: 72, + zone: 'EEZ 북부', + }, + { + id: 'P-3009', + name: '3009함', + shipClass: '참수리급', + speed: 40, + status: '검문중', + lat: 37.50, + lng: 124.75, + fuel: 45, + zone: '서해 5도', + target: '津塘渔03966', + }, + { + id: 'P-5001', + name: '5001함', + shipClass: '삼봉급', + speed: 22, + status: '가용', + lat: 34.20, + lng: 127.50, + fuel: 90, + zone: '남해', + }, + { + id: 'P-1002', + name: '1002함', + shipClass: '태극급', + speed: 28, + status: '초계중', + lat: 36.50, + lng: 124.80, + fuel: 82, + zone: '서해 중부', + }, + { + id: 'P-3007', + name: '3007함', + shipClass: '참수리급', + speed: 40, + status: '귀항중', + lat: 37.45, + lng: 126.60, + fuel: 32, + zone: '인천항', + }, + { + id: 'P-1501', + name: '1501함', + shipClass: '대형함', + speed: 22, + status: '대기', + lat: 37.53, + lng: 129.11, + fuel: 95, + zone: '동해 기지', + }, + { + id: 'P-1503', + name: '1503함', + shipClass: '대형함', + speed: 0, + status: '정비중', + lat: 37.53, + lng: 129.11, + fuel: 0, + zone: '동해', + }, + { + id: 'P-3012', + name: '3012함', + shipClass: '참수리급', + speed: 40, + status: '추적중', + lat: 37.80, + lng: 125.10, + fuel: 58, + zone: '서해 NLL', + target: '미상선박-A', + }, +]; + +// ──────────────────────────────────────────── +// Patrol routes per ship (PatrolRoute.tsx) +// ──────────────────────────────────────────── +export const MOCK_PATROL_ROUTES: Record = { + 'P-3001': { + waypoints: [ + { id: 'WP1', name: '출발(인천)', lat: 37.45, lng: 126.60, eta: '00:00', desc: '인천 해경 부두' }, + { id: 'WP2', name: 'NLL 초계점 A', lat: 37.69, lng: 124.60, eta: '02:15', desc: '서해 NLL 서단 초계' }, + { id: 'WP3', name: '고위험 격자 G-371', lat: 37.12, lng: 124.63, eta: '04:30', desc: 'EEZ 북부 집중 감시' }, + { id: 'WP4', name: 'EEZ 경계 순찰', lat: 36.80, lng: 124.37, eta: '06:45', desc: 'EEZ 서부 경계선 순찰' }, + { id: 'WP5', name: '귀항(인천)', lat: 37.45, lng: 126.60, eta: '10:00', desc: '인천 해경 부두 복귀' }, + ], + summary: { dist: '186 NM', time: '10h 00m', fuel: '12,400L', grids: '142개' }, + }, + 'P-3005': { + waypoints: [ + { id: 'WP1', name: '출발(목포)', lat: 34.78, lng: 126.38, eta: '00:00', desc: '목포 해경 부두' }, + { id: 'WP2', name: '서해 5도 순찰', lat: 37.50, lng: 124.60, eta: '03:30', desc: '서해 5도 수역 감시' }, + { id: 'WP3', name: 'NLL 초계점 B', lat: 37.80, lng: 125.10, eta: '05:00', desc: 'NLL 중단부 초계' }, + { id: 'WP4', name: '서해 중부 순찰', lat: 36.50, lng: 124.80, eta: '07:30', desc: '중국어선 밀집 해역' }, + { id: 'WP5', name: '귀항(목포)', lat: 34.78, lng: 126.38, eta: '12:00', desc: '목포 해경 부두 복귀' }, + ], + summary: { dist: '245 NM', time: '12h 00m', fuel: '16,200L', grids: '198개' }, + }, + 'P-5001': { + waypoints: [ + { id: 'WP1', name: '출발(동해)', lat: 37.53, lng: 129.11, eta: '00:00', desc: '동해 해경 부두' }, + { id: 'WP2', name: '동해 EEZ 북부', lat: 38.03, lng: 129.25, eta: '01:30', desc: '동해 북부 다크베셀 감시' }, + { id: 'WP3', name: '울릉도 순찰', lat: 37.50, lng: 130.90, eta: '05:00', desc: '울릉도 근해 초계' }, + { id: 'WP4', name: '독도 순찰', lat: 37.24, lng: 131.87, eta: '07:00', desc: '독도 영해 경비' }, + { id: 'WP5', name: '동해 중부', lat: 36.25, lng: 130.13, eta: '09:30', desc: '동해 중부 어선 감시' }, + { id: 'WP6', name: '귀항(동해)', lat: 37.53, lng: 129.11, eta: '13:00', desc: '동해 해경 부두 복귀' }, + ], + summary: { dist: '320 NM', time: '13h 00m', fuel: '18,800L', grids: '215개' }, + }, +}; + +// ──────────────────────────────────────────── +// Fleet routes – coordinate arrays (FleetOptimization.tsx) +// ──────────────────────────────────────────── +export const MOCK_FLEET_ROUTES: Record = { + 'P-3001': [[37.45, 126.60], [37.69, 124.60], [37.80, 125.10], [37.90, 124.80], [37.69, 124.60]], + 'P-3005': [[37.45, 126.60], [37.20, 124.63], [36.80, 124.37], [36.50, 124.80], [37.20, 124.63]], + 'P-3009': [[37.45, 126.60], [37.50, 124.75], [37.55, 124.50], [37.40, 124.90], [37.50, 124.75]], + 'P-5001': [[34.78, 126.38], [34.20, 127.50], [33.80, 127.00], [34.50, 126.80], [34.78, 126.38]], +}; + +// ──────────────────────────────────────────── +// Patrol scenarios (PatrolRoute.tsx) +// ──────────────────────────────────────────── +export const MOCK_PATROL_SCENARIOS: PatrolScenario[] = [ + { name: '표준 순찰', weight: { risk: 40, fuel: 30, time: 30 }, score: 82 }, + { name: '위험 집중', weight: { risk: 70, fuel: 15, time: 15 }, score: 91 }, + { name: '연료 절약', weight: { risk: 20, fuel: 60, time: 20 }, score: 74 }, +]; + +// ──────────────────────────────────────────── +// Coverage zones (FleetOptimization.tsx) +// ──────────────────────────────────────────── +export const MOCK_COVERAGE_ZONES: CoverageZone[] = [ + { zone: '서해 NLL', current: 85, optimized: 98, ships: 2, lat: 37.80, lng: 124.90, radius: 30000 }, + { zone: 'EEZ 북부', current: 60, optimized: 92, ships: 1, lat: 37.20, lng: 124.63, radius: 35000 }, + { zone: 'EEZ 서부', current: 45, optimized: 88, ships: 1, lat: 36.00, lng: 123.80, radius: 40000 }, + { zone: '서해 5도', current: 70, optimized: 95, ships: 1, lat: 37.50, lng: 124.60, radius: 25000 }, + { zone: '남해 외해', current: 30, optimized: 75, ships: 1, lat: 34.20, lng: 127.50, radius: 45000 }, + { zone: '동해 EEZ', current: 20, optimized: 50, ships: 0, lat: 37.00, lng: 130.50, radius: 50000 }, +]; diff --git a/src/data/mock/transfers.ts b/src/data/mock/transfers.ts new file mode 100644 index 0000000..c39b816 --- /dev/null +++ b/src/data/mock/transfers.ts @@ -0,0 +1,48 @@ +export interface TransferRecord { + id: string; + time: string; + vesselA: { name: string; mmsi: string }; + vesselB: { name: string; mmsi: string }; + distance: number; + duration: number; + speed: number; + score: number; + location: string; +} + +/** Transfer detection data (3 records) — shared by TransferDetection.tsx & ChinaFishing.tsx */ +export const MOCK_TRANSFERS: TransferRecord[] = [ + { + id: 'TR-001', + time: '2026-01-20 13:42:11', + vesselA: { name: '장저우8호', mmsi: '412345680' }, + vesselB: { name: '黑江9호', mmsi: '412345690' }, + distance: 45, + duration: 52, + speed: 2.3, + score: 89, + location: '서해 중부', + }, + { + id: 'TR-002', + time: '2026-01-20 11:15:33', + vesselA: { name: '江苏如东號', mmsi: '412345683' }, + vesselB: { name: '산동위해호', mmsi: '412345691' }, + distance: 38, + duration: 67, + speed: 1.8, + score: 92, + location: '서해 북부', + }, + { + id: 'TR-003', + time: '2026-01-20 09:23:45', + vesselA: { name: '辽宁大连號', mmsi: '412345682' }, + vesselB: { name: '무명선박-D', mmsi: '412345692' }, + distance: 62, + duration: 41, + speed: 2.7, + score: 78, + location: 'EEZ 북부', + }, +]; diff --git a/src/data/mock/vessels.ts b/src/data/mock/vessels.ts new file mode 100644 index 0000000..3c8ee76 --- /dev/null +++ b/src/data/mock/vessels.ts @@ -0,0 +1,330 @@ +/** + * Shared mock data: vessels (VesselData) + * + * Sources: + * - Dashboard.tsx TOP_RISK_VESSELS (8) + * - DarkVesselDetection.tsx DATA (7 suspects) + * - LiveMapView.tsx AIS_VESSELS (9) + mockEvents (3 event vessels) + * + * Risk scores are normalised to 0-100 integer scale. + * Coordinates are [lat, lng]. + */ + +export interface VesselData { + id: string; + mmsi: string; + name: string; + type: string; // 중국어선, 화물선, 미상 등 + flag: string; // CN, KR, UNKNOWN 등 + lat: number; + lng: number; + speed?: number; + heading?: number; + tonnage?: number; + risk: number; // 0-100 integer scale + status: string; // 추적중, 감시중, 정상 등 + pattern?: string; // AIS 차단, MMSI 변조 등 + lastSignal?: string; +} + +// ──────────────────────────────────────────── +// Combined & deduplicated vessel list +// ──────────────────────────────────────────── +export const MOCK_VESSELS: VesselData[] = [ + // --- TOP_RISK_VESSELS (Dashboard) merged with DarkVesselDetection & LiveMapView --- + { + id: 'V-2024-0142', + mmsi: '412345678', + name: '鲁荣渔56555', + type: '중국어선', + flag: 'CN', + lat: 37.2012, + lng: 124.6345, + speed: 8.2, + heading: 225, + tonnage: 127, + risk: 96, + status: '추적중', + pattern: 'EEZ 침범', + lastSignal: '2분 전', + }, + { + id: 'DV-001', + mmsi: '미상', + name: '미상선박-A', + type: '미상', + flag: 'UNKNOWN', + lat: 37.75, + lng: 125.02, + speed: 6.1, + heading: 180, + tonnage: undefined, + risk: 96, + status: '추적중', + pattern: 'AIS 완전차단', + lastSignal: '6h+ 소실', + }, + { + id: 'V-2024-0231', + mmsi: '412777888', + name: '浙岱渔02856', + type: '중국어선', + flag: 'CN', + lat: 37.0567, + lng: 124.9234, + speed: 4.5, + heading: 45, + tonnage: 219, + risk: 92, + status: '추적중', + pattern: '선단침범', + lastSignal: '5분 전', + }, + { + id: 'DV-002', + mmsi: '412345678', + name: '浙甬渔60651', + type: '중국어선', + flag: 'CN', + lat: 35.87, + lng: 125.50, + speed: 5.8, + heading: 135, + tonnage: 258, + risk: 94, + status: '감시중', + pattern: 'MMSI 3회 변경', + lastSignal: '8분 전', + }, + { + id: 'DV-007', + mmsi: '미상', + name: '미상선박-B', + type: '미상', + flag: 'UNKNOWN', + lat: 38.03, + lng: 129.25, + speed: 11.2, + heading: 310, + tonnage: undefined, + risk: 93, + status: '추적중', + pattern: 'AIS 완전차단', + lastSignal: '12h+ 소실', + }, + { + id: 'DV-006', + mmsi: '412333444', + name: '津塘渔03966', + type: '중국어선', + flag: 'CN', + lat: 35.95, + lng: 125.80, + speed: 4.2, + heading: 45, + tonnage: undefined, + risk: 91, + status: '감시중', + pattern: '국적 위장 의심', + lastSignal: '3분 전', + }, + { + id: 'V-2024-0156', + mmsi: '412987654', + name: '冀黄港渔05001', + type: '중국어선', + flag: 'CN', + lat: 36.60, + lng: 125.40, + speed: 2.1, + heading: 0, + tonnage: 106, + risk: 89, + status: '확인중', + pattern: '불법환적', + lastSignal: '42분 전', + }, + { + id: 'DV-003', + mmsi: '412111222', + name: '鲁荣渔51277', + type: '중국어선', + flag: 'CN', + lat: 36.25, + lng: 130.13, + speed: 18.1, + heading: 290, + tonnage: 126, + risk: 88, + status: '추적중', + pattern: '급격 속력변화', + lastSignal: '1분 전', + }, + { + id: 'DV-004', + mmsi: '412987654', + name: '冀黄港渔05001-B', + type: '중국어선', + flag: 'CN', + lat: 36.80, + lng: 124.37, + speed: 0.3, + heading: 90, + tonnage: 106, + risk: 82, + status: '확인중', + pattern: '신호 간헐송출', + lastSignal: '42분 전', + }, + { + id: 'V-2024-0312', + mmsi: '412333886', + name: '津塘渔03886', + type: '중국어선', + flag: 'CN', + lat: 37.30, + lng: 124.75, + speed: 6.7, + heading: 200, + tonnage: 148, + risk: 80, + status: '감시중', + pattern: 'EEZ 침범', + lastSignal: '3분 전', + }, + // --- LiveMapView event vessels --- + { + id: 'EVT-V-001', + mmsi: 'MMSI 412xxxx', + name: '浙江렌센號', + type: '중국어선', + flag: 'CN', + lat: 37.20, + lng: 124.63, + speed: undefined, + heading: undefined, + tonnage: undefined, + risk: 94, + status: '추적중', + pattern: 'EEZ 침범', + lastSignal: '14:23 UTC', + }, + { + id: 'EVT-V-003', + mmsi: 'MMSI 412345xxx', + name: '福建海丰號', + type: '중국어선', + flag: 'CN', + lat: 36.48, + lng: 124.38, + speed: undefined, + heading: undefined, + tonnage: undefined, + risk: 88, + status: '감시중', + pattern: 'AIS 신호 소실', + lastSignal: '13:58 UTC', + }, + // --- LiveMapView AIS vessels (non-hostile) --- + { + id: 'AIS-001', + mmsi: '', + name: '3009함', + type: '경비함', + flag: 'KR', + lat: 37.45, + lng: 125.30, + speed: 18, + heading: 225, + risk: 0, + status: '작전중', + }, + { + id: 'AIS-002', + mmsi: '', + name: '5001함 삼봉', + type: '경비함', + flag: 'KR', + lat: 36.80, + lng: 125.60, + speed: 14, + heading: 180, + risk: 0, + status: '작전중', + }, + { + id: 'AIS-003', + mmsi: '', + name: '한라호', + type: '순찰선', + flag: 'KR', + lat: 37.10, + lng: 126.20, + speed: 12, + heading: 270, + risk: 0, + status: '작전중', + }, + { + id: 'AIS-004', + mmsi: '', + name: '辽庄渔55567', + type: '중국어선', + flag: 'CN', + lat: 37.55, + lng: 124.80, + speed: 3.8, + heading: 135, + risk: 35, + status: '정상', + pattern: '비정기 신호', + }, + { + id: 'DV-005', + mmsi: '440123456', + name: '한국어선-12', + type: '한국어선', + flag: 'KR', + lat: 34.80, + lng: 128.60, + speed: 5.5, + heading: 315, + risk: 35, + status: '정상', + pattern: '비정기 신호', + lastSignal: '5분 전', + }, + { + id: 'AIS-006', + mmsi: '', + name: '제7동진호', + type: '한국어선', + flag: 'KR', + lat: 35.50, + lng: 126.50, + speed: 5.5, + heading: 315, + risk: 0, + status: '정상', + }, + { + id: 'AIS-007', + mmsi: '', + name: '새한울호', + type: '한국어선', + flag: 'KR', + lat: 37.00, + lng: 125.85, + speed: 7.2, + heading: 200, + risk: 0, + status: '정상', + }, +]; + +// ──────────────────────────────────────────── +// Derived: suspects only (risk >= 80) +// ──────────────────────────────────────────── +export const MOCK_SUSPECTS: VesselData[] = MOCK_VESSELS.filter( + (v) => v.risk >= 80, +); diff --git a/src/data/speciesCodes.json b/src/data/speciesCodes.json new file mode 100644 index 0000000..742da63 --- /dev/null +++ b/src/data/speciesCodes.json @@ -0,0 +1,6360 @@ +[ + { + "code": "10000", + "major": "대분류", + "mid": "", + "name": "해면어류", + "nameEn": "Marine Fish & Others", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "10100", + "major": "어류", + "mid": "해면어류", + "name": "가시고기", + "nameEn": "Stickleback", + "scientific": "Gasterosteidae spp.", + "area": "동해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "10101", + "major": "어류", + "mid": "가시고기", + "name": "청가시고기", + "nameEn": "Nine-spine Stickleback", + "scientific": "Pungitius sinensis", + "area": "동해 연안·하천 하류", + "active": true, + "fishing": false + }, + { + "code": "10199", + "major": "어류", + "mid": "가시고기", + "name": "기타 가시고기", + "nameEn": "Other Sticklebacks", + "scientific": "Gasterosteidae spp.", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "10200", + "major": "어류", + "mid": "해면어류", + "name": "가오리", + "nameEn": "Ray", + "scientific": "Batoidea spp.", + "area": "전 연안 해역", + "active": true, + "fishing": false + }, + { + "code": "10201", + "major": "어류", + "mid": "가오리", + "name": "노랑가오리", + "nameEn": "Yellow Stingray", + "scientific": "Dasyatis bennettii", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "10202", + "major": "어류", + "mid": "가오리", + "name": "눈가오리", + "nameEn": "Ocellate Spot Skate", + "scientific": "Okamejei kenojei", + "area": "남해·동해·서해", + "active": true, + "fishing": false + }, + { + "code": "10203", + "major": "어류", + "mid": "가오리", + "name": "목탁가오리", + "nameEn": "Guitarfish", + "scientific": "Rhinobatidae spp.", + "area": "남해 연안", + "active": true, + "fishing": false + }, + { + "code": "10204", + "major": "어류", + "mid": "가오리", + "name": "흰가오리", + "nameEn": "White-spotted Ray", + "scientific": "Himantura gerrardi", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "10299", + "major": "어류", + "mid": "가오리", + "name": "기타 가오리", + "nameEn": "Other Rays", + "scientific": "Batoidea spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "10300", + "major": "어류", + "mid": "해면어류", + "name": "가자미류", + "nameEn": "Flounder / Flatfish", + "scientific": "Pleuronectidae spp.", + "area": "동해·남해·서해 연안", + "active": true, + "fishing": true + }, + { + "code": "10301", + "major": "어류", + "mid": "가자미류", + "name": "각시가자미", + "nameEn": "Shotted Halibut", + "scientific": "Eopsetta grigorjewi", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "10302", + "major": "어류", + "mid": "가자미류", + "name": "갈가자미", + "nameEn": "Brown Sole", + "scientific": "Pseudopleuronectes herzensteini", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "10303", + "major": "어류", + "mid": "가자미류", + "name": "노랑가자미", + "nameEn": "Yellow Striped Flounder", + "scientific": "Pseudopleuronectes yokohamae", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "10304", + "major": "어류", + "mid": "가자미류", + "name": "도다리", + "nameEn": "Marbled Flounder", + "scientific": "Pseudopleuronectes yokohamae", + "area": "남해·동해 연안", + "active": true, + "fishing": true + }, + { + "code": "10305", + "major": "어류", + "mid": "가자미류", + "name": "돌가자미", + "nameEn": "Stone Flounder", + "scientific": "Kareius bicoloratus", + "area": "동해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "10306", + "major": "어류", + "mid": "가자미류", + "name": "물가자미", + "nameEn": "Alaska Plaice", + "scientific": "Pleuronectes quadrituberculatus", + "area": "동해 북부", + "active": true, + "fishing": false + }, + { + "code": "10307", + "major": "어류", + "mid": "가자미류", + "name": "범가자미", + "nameEn": "Barfin Flounder", + "scientific": "Verasper moseri", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "10308", + "major": "어류", + "mid": "가자미류", + "name": "점가자미", + "nameEn": "Spotted Halibut", + "scientific": "Verasper variegatus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "10309", + "major": "어류", + "mid": "가자미류", + "name": "참가자미", + "nameEn": "Willowy Flounder", + "scientific": "Paralichthys triocellatus", + "area": "동해·남해 연안", + "active": true, + "fishing": true + }, + { + "code": "10310", + "major": "어류", + "mid": "가자미류", + "name": "홍가자미", + "nameEn": "Slime Flounder", + "scientific": "Microstomus achne", + "area": "동해 심해", + "active": true, + "fishing": false + }, + { + "code": "10399", + "major": "어류", + "mid": "가자미류", + "name": "기타 가자미", + "nameEn": "Other Flounders", + "scientific": "Pleuronectidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "10400", + "major": "어류", + "mid": "해면어류", + "name": "갈치", + "nameEn": "Hairtail / Beltfish", + "scientific": "Trichiurus spp.", + "area": "남해·서해·동해", + "active": true, + "fishing": true + }, + { + "code": "10401", + "major": "어류", + "mid": "갈치", + "name": "먹갈치", + "nameEn": "Black Scabbardfish", + "scientific": "Aphanopus carbo", + "area": "제주 심해·남해 외해", + "active": true, + "fishing": false + }, + { + "code": "10402", + "major": "어류", + "mid": "갈치", + "name": "은갈치", + "nameEn": "Largehead Hairtail", + "scientific": "Trichiurus lepturus", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "10499", + "major": "어류", + "mid": "갈치", + "name": "기타 갈치", + "nameEn": "Other Hairtails", + "scientific": "Trichiuridae spp.", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "10500", + "major": "어류", + "mid": "해면어류", + "name": "강달이", + "nameEn": "Lizardfish", + "scientific": "Synodontidae spp.", + "area": "남해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "10501", + "major": "어류", + "mid": "강달이", + "name": "민강달이", + "nameEn": "Slender Lizardfish", + "scientific": "Saurida elongata", + "area": "남해 연안", + "active": true, + "fishing": false + }, + { + "code": "10502", + "major": "어류", + "mid": "강달이", + "name": "황강달이", + "nameEn": "Greater Lizardfish", + "scientific": "Saurida tumbil", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "10503", + "major": "어류", + "mid": "강달이", + "name": "눈강달이", + "nameEn": "Largescale Lizardfish", + "scientific": "Saurida macrolepis", + "area": "남해 외해", + "active": true, + "fishing": false + }, + { + "code": "10599", + "major": "어류", + "mid": "강달이", + "name": "기타 강달이", + "nameEn": "Other Lizardfishes", + "scientific": "Synodontidae spp.", + "area": "남해", + "active": true, + "fishing": false + }, + { + "code": "10600", + "major": "어류", + "mid": "해면어류", + "name": "고등어류", + "nameEn": "Mackerel", + "scientific": "Scombridae spp.", + "area": "남해·동해·서해", + "active": true, + "fishing": true + }, + { + "code": "10601", + "major": "어류", + "mid": "고등어류", + "name": "망치고등어", + "nameEn": "Indian Mackerel", + "scientific": "Rastrelliger kanagurta", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "10602", + "major": "어류", + "mid": "고등어류", + "name": "뭉치다래", + "nameEn": "Frigate Tuna", + "scientific": "Auxis thazard", + "area": "남해 외해·제주", + "active": true, + "fishing": false + }, + { + "code": "10603", + "major": "어류", + "mid": "고등어류", + "name": "점고등어", + "nameEn": "Chub Mackerel", + "scientific": "Scomber japonicus", + "area": "남해·동해·서해", + "active": true, + "fishing": false + }, + { + "code": "10699", + "major": "어류", + "mid": "고등어류", + "name": "기타 고등어", + "nameEn": "Other Mackerels", + "scientific": "Scombridae spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "10700", + "major": "어류", + "mid": "해면어류", + "name": "까나리", + "nameEn": "Sand Lance", + "scientific": "Ammodytes spp.", + "area": "서해·동해 연안", + "active": true, + "fishing": false + }, + { + "code": "10701", + "major": "어류", + "mid": "까나리", + "name": "까나리", + "nameEn": "Pacific Sand Lance", + "scientific": "Ammodytes personatus", + "area": "서해·동해 연안", + "active": true, + "fishing": false + }, + { + "code": "10800", + "major": "어류", + "mid": "해면어류", + "name": "꼬치고기", + "nameEn": "Barracuda", + "scientific": "Sphyraena spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "10801", + "major": "어류", + "mid": "꼬치고기", + "name": "꼬치고기", + "nameEn": "Obtuse Barracuda", + "scientific": "Sphyraena obtusata", + "area": "남해·제주 연안", + "active": true, + "fishing": false + }, + { + "code": "10900", + "major": "어류", + "mid": "해면어류", + "name": "곤쟁이", + "nameEn": "Opossum Shrimp / Mysid", + "scientific": "Mysidacea spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "10901", + "major": "어류", + "mid": "곤쟁이", + "name": "곤쟁이", + "nameEn": "Mysid Shrimp", + "scientific": "Neomysis japonica", + "area": "서해·남해 연안", + "active": true, + "fishing": false + }, + { + "code": "11000", + "major": "어류", + "mid": "해면어류", + "name": "곰치", + "nameEn": "Moray Eel", + "scientific": "Muraenidae spp.", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "11001", + "major": "어류", + "mid": "곰치", + "name": "곰치", + "nameEn": "Giant Moray", + "scientific": "Gymnothorax javanicus", + "area": "남해·제주 연안", + "active": true, + "fishing": false + }, + { + "code": "11100", + "major": "어류", + "mid": "해면어류", + "name": "꼼치", + "nameEn": "Snailfish / Lumpsucker", + "scientific": "Cyclopteridae spp.", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "11101", + "major": "어류", + "mid": "꼼치", + "name": "물메기", + "nameEn": "Korean Snailfish", + "scientific": "Liparis tessellatus", + "area": "동해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "11199", + "major": "어류", + "mid": "꼼치", + "name": "기타 꼼치", + "nameEn": "Other Snailfishes", + "scientific": "Liparidae spp.", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "11200", + "major": "어류", + "mid": "해면어류", + "name": "꽁치", + "nameEn": "Pacific Saury", + "scientific": "Cololabis saira", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "11201", + "major": "어류", + "mid": "꽁치", + "name": "꽁치", + "nameEn": "Pacific Saury", + "scientific": "Cololabis saira", + "area": "동해·남해 연안", + "active": true, + "fishing": false + }, + { + "code": "11300", + "major": "어류", + "mid": "해면어류", + "name": "게르치", + "nameEn": "Gemfish", + "scientific": "Rexea spp.", + "area": "동해·남해 심해", + "active": true, + "fishing": false + }, + { + "code": "11301", + "major": "어류", + "mid": "게르치", + "name": "게르치", + "nameEn": "Oilfish", + "scientific": "Ruvettus pretiosus", + "area": "동해·남해 심해", + "active": true, + "fishing": false + }, + { + "code": "11400", + "major": "어류", + "mid": "해면어류", + "name": "날치", + "nameEn": "Flying Fish", + "scientific": "Exocoetidae spp.", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "11401", + "major": "어류", + "mid": "날치", + "name": "날치", + "nameEn": "Japanese Flying Fish", + "scientific": "Cheilopogon agoo", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "11500", + "major": "어류", + "mid": "해면어류", + "name": "넙치", + "nameEn": "Olive Flounder", + "scientific": "Paralichthys olivaceus", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "11501", + "major": "어류", + "mid": "넙치", + "name": "넙치", + "nameEn": "Olive Flounder", + "scientific": "Paralichthys olivaceus", + "area": "전 연안 (양식 포함)", + "active": true, + "fishing": true + }, + { + "code": "11600", + "major": "어류", + "mid": "해면어류", + "name": "노가리", + "nameEn": "Walleye Pollock (juvenile)", + "scientific": "Gadus chalcogrammus (juv.)", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "11601", + "major": "어류", + "mid": "노가리", + "name": "노가리", + "nameEn": "Walleye Pollock Juvenile", + "scientific": "Gadus chalcogrammus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "11700", + "major": "어류", + "mid": "해면어류", + "name": "노래미", + "nameEn": "Greenling", + "scientific": "Hexagrammidae spp.", + "area": "동해·남해 연안", + "active": true, + "fishing": true + }, + { + "code": "11701", + "major": "어류", + "mid": "노래미", + "name": "쥐노래미", + "nameEn": "Fat Greenling", + "scientific": "Hexagrammos otakii", + "area": "동해·남해·서해 암초", + "active": true, + "fishing": true + }, + { + "code": "11702", + "major": "어류", + "mid": "노래미", + "name": "임연수어", + "nameEn": "Atka Mackerel", + "scientific": "Pleurogrammus azonus", + "area": "동해 연안·북부", + "active": true, + "fishing": true + }, + { + "code": "11799", + "major": "어류", + "mid": "노래미", + "name": "기타 노래미", + "nameEn": "Other Greenlings", + "scientific": "Hexagrammidae spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "11800", + "major": "어류", + "mid": "해면어류", + "name": "놀래기", + "nameEn": "Wrasse", + "scientific": "Labridae spp.", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "11801", + "major": "어류", + "mid": "놀래기", + "name": "용치놀래기", + "nameEn": "Dragon Wrasse", + "scientific": "Novaculichthys taeniourus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "11899", + "major": "어류", + "mid": "놀래기", + "name": "기타 놀래기", + "nameEn": "Other Wrasses", + "scientific": "Labridae spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "11900", + "major": "어류", + "mid": "해면어류", + "name": "농어", + "nameEn": "Sea Bass", + "scientific": "Lateolabrax japonicus", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "11901", + "major": "어류", + "mid": "농어", + "name": "농어", + "nameEn": "Japanese Sea Bass", + "scientific": "Lateolabrax japonicus", + "area": "전 연안 기수역", + "active": true, + "fishing": true + }, + { + "code": "12000", + "major": "어류", + "mid": "해면어류", + "name": "눈볼대", + "nameEn": "Bigeye", + "scientific": "Priacanthus spp.", + "area": "남해·동해 연안", + "active": true, + "fishing": false + }, + { + "code": "12001", + "major": "어류", + "mid": "눈볼대", + "name": "눈볼대", + "nameEn": "Crimson Jobfish", + "scientific": "Priacanthus macracanthus", + "area": "남해·동해 연안", + "active": true, + "fishing": true + }, + { + "code": "12100", + "major": "어류", + "mid": "해면어류", + "name": "능성어", + "nameEn": "Grouper", + "scientific": "Serranidae spp.", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "12101", + "major": "어류", + "mid": "능성어", + "name": "다금바리", + "nameEn": "Longtooth Grouper", + "scientific": "Epinephelus bruneus", + "area": "남해·제주 암초", + "active": true, + "fishing": true + }, + { + "code": "12102", + "major": "어류", + "mid": "능성어", + "name": "붉바리", + "nameEn": "Red-spotted Grouper", + "scientific": "Epinephelus akaara", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "12199", + "major": "어류", + "mid": "능성어", + "name": "기타 능성어", + "nameEn": "Other Groupers", + "scientific": "Serranidae spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "12200", + "major": "어류", + "mid": "해면어류", + "name": "다랑어", + "nameEn": "Tuna", + "scientific": "Thunnini spp.", + "area": "동해·남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "12201", + "major": "어류", + "mid": "다랑어", + "name": "가다랑어", + "nameEn": "Skipjack Tuna", + "scientific": "Katsuwonus pelamis", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "12202", + "major": "어류", + "mid": "다랑어", + "name": "날개다랑어", + "nameEn": "Albacore", + "scientific": "Thunnus alalunga", + "area": "남해·동해 외해", + "active": true, + "fishing": false + }, + { + "code": "12203", + "major": "어류", + "mid": "다랑어", + "name": "눈다랑어", + "nameEn": "Bigeye Tuna", + "scientific": "Thunnus obesus", + "area": "제주·남해 외해", + "active": true, + "fishing": false + }, + { + "code": "12204", + "major": "어류", + "mid": "다랑어", + "name": "점다랑어", + "nameEn": "Longtail Tuna", + "scientific": "Thunnus tonggol", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "12205", + "major": "어류", + "mid": "다랑어", + "name": "참다랑어", + "nameEn": "Pacific Bluefin Tuna", + "scientific": "Thunnus orientalis", + "area": "동해·남해 외해", + "active": true, + "fishing": false + }, + { + "code": "12206", + "major": "어류", + "mid": "다랑어", + "name": "황다랑어", + "nameEn": "Yellowfin Tuna", + "scientific": "Thunnus albacares", + "area": "제주·남해 외해", + "active": true, + "fishing": false + }, + { + "code": "12299", + "major": "어류", + "mid": "다랑어", + "name": "기타 다랑어", + "nameEn": "Other Tunas", + "scientific": "Thunnini spp.", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "12300", + "major": "어류", + "mid": "해면어류", + "name": "달고기", + "nameEn": "John Dory", + "scientific": "Zeus faber", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "12301", + "major": "어류", + "mid": "달고기", + "name": "민달고기", + "nameEn": "Oxeye Oreo", + "scientific": "Allocyttus verrucosus", + "area": "동해 심해", + "active": true, + "fishing": false + }, + { + "code": "12399", + "major": "어류", + "mid": "달고기", + "name": "기타 달고기", + "nameEn": "Other Dories", + "scientific": "Zeidae spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "12400", + "major": "어류", + "mid": "해면어류", + "name": "대구", + "nameEn": "Cod", + "scientific": "Gadidae spp.", + "area": "동해·서해", + "active": true, + "fishing": true + }, + { + "code": "12401", + "major": "어류", + "mid": "대구", + "name": "민대구", + "nameEn": "Pacific Cod", + "scientific": "Gadus macrocephalus", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "12402", + "major": "어류", + "mid": "대구", + "name": "빨간대구", + "nameEn": "Pacific Tomcod", + "scientific": "Microgadus proximus", + "area": "동해 북부", + "active": true, + "fishing": false + }, + { + "code": "12499", + "major": "어류", + "mid": "대구", + "name": "기타 대구", + "nameEn": "Other Cods", + "scientific": "Gadidae spp.", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "12500", + "major": "어류", + "mid": "해면어류", + "name": "도루묵", + "nameEn": "Sailfin Sandfish", + "scientific": "Arctoscopus japonicus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "12501", + "major": "어류", + "mid": "도루묵", + "name": "도루묵", + "nameEn": "Sailfin Sandfish", + "scientific": "Arctoscopus japonicus", + "area": "동해 연안 (겨울)", + "active": true, + "fishing": false + }, + { + "code": "12600", + "major": "어류", + "mid": "해면어류", + "name": "도치", + "nameEn": "Lumpsucker", + "scientific": "Cyclopterus lumpus", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "12601", + "major": "어류", + "mid": "도치", + "name": "뚝지", + "nameEn": "Lumpsucker", + "scientific": "Eumicrotremus pacificus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "12699", + "major": "어류", + "mid": "도치", + "name": "기타 도치", + "nameEn": "Other Lumpsuckers", + "scientific": "Cyclopteridae spp.", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "12700", + "major": "어류", + "mid": "해면어류", + "name": "돔", + "nameEn": "Porgy / Bream", + "scientific": "Sparidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "12701", + "major": "어류", + "mid": "돔", + "name": "갈돔", + "nameEn": "Largescale Blackfish", + "scientific": "Girella punctata", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "12702", + "major": "어류", + "mid": "돔", + "name": "참돔", + "nameEn": "Red Seabream", + "scientific": "Pagrus major", + "area": "남해·동해·서해", + "active": true, + "fishing": true + }, + { + "code": "12703", + "major": "어류", + "mid": "돔", + "name": "감성돔", + "nameEn": "Black Porgy", + "scientific": "Acanthopagrus schlegeli", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "12704", + "major": "어류", + "mid": "돔", + "name": "금눈돔", + "nameEn": "Splendid Alfonsino", + "scientific": "Beryx splendens", + "area": "남해·동해 심해", + "active": true, + "fishing": false + }, + { + "code": "12705", + "major": "어류", + "mid": "돔", + "name": "자리돔", + "nameEn": "Sergeant Major", + "scientific": "Chromis notata", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "12706", + "major": "어류", + "mid": "돔", + "name": "돌돔", + "nameEn": "Striped Beakperch", + "scientific": "Oplegnathus fasciatus", + "area": "남해·제주 암초", + "active": true, + "fishing": true + }, + { + "code": "12707", + "major": "어류", + "mid": "돔", + "name": "물릉돔", + "nameEn": "Spotted Knifejaw", + "scientific": "Oplegnathus punctatus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "12708", + "major": "어류", + "mid": "돔", + "name": "옥돔", + "nameEn": "Horsehead Tilefish", + "scientific": "Branchiostegus japonicus", + "area": "제주·남해 사질저", + "active": true, + "fishing": true + }, + { + "code": "12709", + "major": "어류", + "mid": "돔", + "name": "흑돔", + "nameEn": "Black Sea Bream", + "scientific": "Acanthopagrus latus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "12710", + "major": "어류", + "mid": "돔", + "name": "청돔", + "nameEn": "Yellowback Seabream", + "scientific": "Dentex hypselosomus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "12711", + "major": "어류", + "mid": "돔", + "name": "샛돔", + "nameEn": "Silver Pomfret", + "scientific": "Pampus argenteus", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "12712", + "major": "어류", + "mid": "돔", + "name": "붉은돔", + "nameEn": "Red Snapper", + "scientific": "Lutjanus erythropterus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "12713", + "major": "어류", + "mid": "돔", + "name": "벵어돔", + "nameEn": "Largescale Blackfish", + "scientific": "Girella punctata", + "area": "남해·제주 암초", + "active": true, + "fishing": true + }, + { + "code": "12714", + "major": "어류", + "mid": "돔", + "name": "하스돔", + "nameEn": "Convict Surgeonfish", + "scientific": "Acanthurus triostegus", + "area": "제주·남해", + "active": true, + "fishing": false + }, + { + "code": "12799", + "major": "어류", + "mid": "돔", + "name": "기타 돔", + "nameEn": "Other Porgy/Bream", + "scientific": "Sparidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "12800", + "major": "어류", + "mid": "해면어류", + "name": "망둑어", + "nameEn": "Goby", + "scientific": "Gobiidae spp.", + "area": "전 연안·기수역", + "active": true, + "fishing": false + }, + { + "code": "12801", + "major": "어류", + "mid": "망둑어", + "name": "도하망둑어", + "nameEn": "Flapjaw Mudskipper", + "scientific": "Periophthalmodon modestus", + "area": "남해·서해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "12802", + "major": "어류", + "mid": "망둑어", + "name": "문절망둑어", + "nameEn": "Yellowfin Goby", + "scientific": "Acanthogobius flavimanus", + "area": "전 연안 기수역", + "active": true, + "fishing": false + }, + { + "code": "12803", + "major": "어류", + "mid": "망둑어", + "name": "짱둥어", + "nameEn": "Giant Mudskipper", + "scientific": "Boleophthalmus pectinirostris", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "12804", + "major": "어류", + "mid": "망둑어", + "name": "풀망둑어", + "nameEn": "Grass Goby", + "scientific": "Zosterisessor ophiocephalus", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "12899", + "major": "어류", + "mid": "망둑어", + "name": "기타 망둑어", + "nameEn": "Other Gobies", + "scientific": "Gobiidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "12900", + "major": "어류", + "mid": "해면어류", + "name": "망둥어", + "nameEn": "Flathead Goby", + "scientific": "Chasmichthys dolichognathus", + "area": "서해·남해 연안", + "active": true, + "fishing": true + }, + { + "code": "12901", + "major": "어류", + "mid": "망둥어", + "name": "망둥어", + "nameEn": "Flathead Goby", + "scientific": "Chasmichthys dolichognathus", + "area": "서해·남해 기수역", + "active": true, + "fishing": false + }, + { + "code": "13000", + "major": "어류", + "mid": "해면어류", + "name": "매퉁이", + "nameEn": "Bombay Duck", + "scientific": "Harpadon nehereus", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "13001", + "major": "어류", + "mid": "매퉁이", + "name": "날매퉁이", + "nameEn": "Slender Lizardfish", + "scientific": "Saurida elongata", + "area": "남해 연안", + "active": true, + "fishing": false + }, + { + "code": "13002", + "major": "어류", + "mid": "매퉁이", + "name": "툼매퉁이", + "nameEn": "Largescale Lizardfish", + "scientific": "Saurida macrolepis", + "area": "남해 외해", + "active": true, + "fishing": false + }, + { + "code": "13003", + "major": "어류", + "mid": "매퉁이", + "name": "황매퉁이", + "nameEn": "Shortfin Lizardfish", + "scientific": "Saurida micropectoralis", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "13099", + "major": "어류", + "mid": "매퉁이", + "name": "기타 매퉁이", + "nameEn": "Other Lizardfishes", + "scientific": "Synodontidae spp.", + "area": "남해", + "active": true, + "fishing": false + }, + { + "code": "13100", + "major": "어류", + "mid": "해면어류", + "name": "멸치", + "nameEn": "Anchovy", + "scientific": "Engraulidae spp.", + "area": "남해·서해", + "active": true, + "fishing": true + }, + { + "code": "13101", + "major": "어류", + "mid": "멸치", + "name": "반지", + "nameEn": "Slender Shad", + "scientific": "Ilisha elongata", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "13102", + "major": "어류", + "mid": "멸치", + "name": "싱어", + "nameEn": "Japanese Anchovy", + "scientific": "Engraulis japonicus", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "13199", + "major": "어류", + "mid": "멸치", + "name": "기타 멸치", + "nameEn": "Other Anchovies", + "scientific": "Engraulidae spp.", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "13200", + "major": "어류", + "mid": "해면어류", + "name": "명태", + "nameEn": "Walleye Pollock", + "scientific": "Gadus chalcogrammus", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "13201", + "major": "어류", + "mid": "명태", + "name": "명태", + "nameEn": "Walleye Pollock", + "scientific": "Gadus chalcogrammus", + "area": "동해 (국내 자원 급감)", + "active": true, + "fishing": true + }, + { + "code": "13300", + "major": "어류", + "mid": "해면어류", + "name": "물치다래", + "nameEn": "Kawakawa", + "scientific": "Euthynnus affinis", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "13301", + "major": "어류", + "mid": "물치다래", + "name": "물치다래", + "nameEn": "Kawakawa", + "scientific": "Euthynnus affinis", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "13400", + "major": "어류", + "mid": "해면어류", + "name": "몽치다래", + "nameEn": "Bullet Tuna", + "scientific": "Auxis rochei", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "13401", + "major": "어류", + "mid": "몽치다래", + "name": "몽치다래", + "nameEn": "Bullet Tuna", + "scientific": "Auxis rochei", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "13500", + "major": "어류", + "mid": "해면어류", + "name": "민어", + "nameEn": "Miichthys Croaker", + "scientific": "Miichthys miiuy", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "13501", + "major": "어류", + "mid": "민어", + "name": "민어", + "nameEn": "Brown Croaker", + "scientific": "Miichthys miiuy", + "area": "서해 연안·남해 서부", + "active": true, + "fishing": true + }, + { + "code": "13600", + "major": "어류", + "mid": "해면어류", + "name": "민태", + "nameEn": "Whiting / Pollock", + "scientific": "Merluccius spp.", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "13601", + "major": "어류", + "mid": "민태", + "name": "꼬리민태", + "nameEn": "Grenadier", + "scientific": "Coelorinchus spp.", + "area": "동해 심해", + "active": true, + "fishing": false + }, + { + "code": "13602", + "major": "어류", + "mid": "민태", + "name": "새꼬리민태", + "nameEn": "Pacific Grenadier", + "scientific": "Macrurus berglax", + "area": "동해 심해", + "active": true, + "fishing": false + }, + { + "code": "13699", + "major": "어류", + "mid": "민태", + "name": "기타 민태", + "nameEn": "Other Whitings", + "scientific": "Merlucciidae spp.", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "13700", + "major": "어류", + "mid": "해면어류", + "name": "메로", + "nameEn": "Patagonian Toothfish", + "scientific": "Dissostichus eleginoides", + "area": "남빙양·원양", + "active": true, + "fishing": false + }, + { + "code": "13701", + "major": "어류", + "mid": "메로", + "name": "메로", + "nameEn": "Patagonian Toothfish", + "scientific": "Dissostichus eleginoides", + "area": "남빙양 원양", + "active": true, + "fishing": false + }, + { + "code": "13800", + "major": "어류", + "mid": "해면어류", + "name": "방어", + "nameEn": "Yellowtail", + "scientific": "Seriola quinqueradiata", + "area": "남해·동해·제주", + "active": true, + "fishing": true + }, + { + "code": "13801", + "major": "어류", + "mid": "방어", + "name": "부시리", + "nameEn": "Greater Amberjack", + "scientific": "Seriola dumerili", + "area": "남해·동해·제주", + "active": true, + "fishing": true + }, + { + "code": "13802", + "major": "어류", + "mid": "방어", + "name": "잿방어", + "nameEn": "Banded Rudderfish", + "scientific": "Seriola zonata", + "area": "남해·제주", + "active": true, + "fishing": true + }, + { + "code": "13899", + "major": "어류", + "mid": "방어", + "name": "기타 방어", + "nameEn": "Other Amberjacks", + "scientific": "Seriola spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "13900", + "major": "어류", + "mid": "해면어류", + "name": "병어", + "nameEn": "Silver Pomfret", + "scientific": "Pampus spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "13901", + "major": "어류", + "mid": "병어", + "name": "덕대", + "nameEn": "Silver Pomfret", + "scientific": "Pampus argenteus", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "13999", + "major": "어류", + "mid": "병어", + "name": "기타 병어", + "nameEn": "Other Pomfrets", + "scientific": "Stromateidae spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "14000", + "major": "어류", + "mid": "해면어류", + "name": "뱅어", + "nameEn": "Icefish / Whitebait", + "scientific": "Salangidae spp.", + "area": "서해·남해 기수역", + "active": true, + "fishing": false + }, + { + "code": "14001", + "major": "어류", + "mid": "뱅어", + "name": "뱅어", + "nameEn": "Noodle Icefish", + "scientific": "Neosalanx jordani", + "area": "서해·남해 기수역", + "active": true, + "fishing": false + }, + { + "code": "14100", + "major": "어류", + "mid": "해면어류", + "name": "보리멸", + "nameEn": "Sillago", + "scientific": "Sillaginidae spp.", + "area": "남해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "14101", + "major": "어류", + "mid": "보리멸", + "name": "청보리멸", + "nameEn": "Japanese Sillago", + "scientific": "Sillago japonica", + "area": "남해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "14199", + "major": "어류", + "mid": "보리멸", + "name": "기타 보리멸", + "nameEn": "Other Sillagos", + "scientific": "Sillaginidae spp.", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "14200", + "major": "어류", + "mid": "해면어류", + "name": "복어", + "nameEn": "Pufferfish", + "scientific": "Tetraodontidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "14201", + "major": "어류", + "mid": "복어", + "name": "검복", + "nameEn": "Tiger Puffer", + "scientific": "Takifugu rubripes", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "14202", + "major": "어류", + "mid": "복어", + "name": "금자주복", + "nameEn": "Golden Puffer", + "scientific": "Takifugu chrysops", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14203", + "major": "어류", + "mid": "복어", + "name": "까치복", + "nameEn": "Purple Puffer", + "scientific": "Takifugu porphyreus", + "area": "남해·동해·서해", + "active": true, + "fishing": false + }, + { + "code": "14204", + "major": "어류", + "mid": "복어", + "name": "매리복", + "nameEn": "Obscure Puffer", + "scientific": "Takifugu obscurus", + "area": "서해·남해 기수역", + "active": true, + "fishing": false + }, + { + "code": "14205", + "major": "어류", + "mid": "복어", + "name": "밀복", + "nameEn": "Starry Puffer", + "scientific": "Arothron firmamentum", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14206", + "major": "어류", + "mid": "복어", + "name": "신점복", + "nameEn": "Valentin's Sharpnose Puffer", + "scientific": "Canthigaster valentini", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14207", + "major": "어류", + "mid": "복어", + "name": "자주복", + "nameEn": "Vermiculated Puffer", + "scientific": "Takifugu vermicularis", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "14208", + "major": "어류", + "mid": "복어", + "name": "졸복", + "nameEn": "Grass Puffer", + "scientific": "Takifugu alboplumbeus", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "14209", + "major": "어류", + "mid": "복어", + "name": "참복", + "nameEn": "Tiger Puffer", + "scientific": "Takifugu rubripes", + "area": "남해·동해·서해", + "active": true, + "fishing": false + }, + { + "code": "14210", + "major": "어류", + "mid": "복어", + "name": "황복", + "nameEn": "Yellow Puffer", + "scientific": "Takifugu flavidus", + "area": "서해·남해 기수역", + "active": true, + "fishing": false + }, + { + "code": "14299", + "major": "어류", + "mid": "복어", + "name": "기타 복어", + "nameEn": "Other Puffers", + "scientific": "Tetraodontidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "14300", + "major": "어류", + "mid": "해면어류", + "name": "볼락", + "nameEn": "Rockfish / Scorpionfish", + "scientific": "Sebastes spp.", + "area": "동해·남해 암초", + "active": true, + "fishing": true + }, + { + "code": "14301", + "major": "어류", + "mid": "볼락", + "name": "개볼락", + "nameEn": "Korean Rockfish", + "scientific": "Sebastes schlegelii", + "area": "동해·남해 연안", + "active": true, + "fishing": false + }, + { + "code": "14302", + "major": "어류", + "mid": "볼락", + "name": "붉은쏨뱅이", + "nameEn": "Red Scorpionfish", + "scientific": "Scorpaena onaria", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14303", + "major": "어류", + "mid": "볼락", + "name": "쏠배감펭", + "nameEn": "Lionfish", + "scientific": "Pterois lunulata", + "area": "남해·제주 암초", + "active": true, + "fishing": true + }, + { + "code": "14304", + "major": "어류", + "mid": "볼락", + "name": "쏨뱅이", + "nameEn": "Scorpionfish", + "scientific": "Scorpaena neglecta", + "area": "남해·동해 연안", + "active": true, + "fishing": false + }, + { + "code": "14305", + "major": "어류", + "mid": "볼락", + "name": "쑤기미", + "nameEn": "Devil Scorpionfish", + "scientific": "Inimicus japonicus", + "area": "남해 연안", + "active": true, + "fishing": false + }, + { + "code": "14306", + "major": "어류", + "mid": "볼락", + "name": "조피볼락", + "nameEn": "Korean Rockfish", + "scientific": "Sebastes schlegeli", + "area": "동해·남해·서해 연안", + "active": true, + "fishing": true + }, + { + "code": "14307", + "major": "어류", + "mid": "볼락", + "name": "황볼락", + "nameEn": "Goldeye Rockfish", + "scientific": "Sebastes thompsoni", + "area": "동해·남해 연안", + "active": true, + "fishing": true + }, + { + "code": "14399", + "major": "어류", + "mid": "볼락", + "name": "기타 볼락", + "nameEn": "Other Rockfishes", + "scientific": "Sebastes spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "14400", + "major": "어류", + "mid": "해면어류", + "name": "붉은메기", + "nameEn": "Red Catfish", + "scientific": "Apogon spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14401", + "major": "어류", + "mid": "붉은메기", + "name": "붉은메기", + "nameEn": "Bigeye", + "scientific": "Priacanthus spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14500", + "major": "어류", + "mid": "해면어류", + "name": "베도라치", + "nameEn": "Blenny", + "scientific": "Blenniidae spp.", + "area": "전 연안 암초", + "active": true, + "fishing": false + }, + { + "code": "14501", + "major": "어류", + "mid": "베도라치", + "name": "흰베도라치", + "nameEn": "White Blenny", + "scientific": "Parablennius yatabei", + "area": "동해·남해 암초", + "active": true, + "fishing": false + }, + { + "code": "14599", + "major": "어류", + "mid": "베도라치", + "name": "기타 베도라치", + "nameEn": "Other Blennies", + "scientific": "Blenniidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "14600", + "major": "어류", + "mid": "해면어류", + "name": "삼세기", + "nameEn": "Bullhead", + "scientific": "Myoxocephalus spp.", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "14601", + "major": "어류", + "mid": "삼세기", + "name": "삼세기", + "nameEn": "Fourhorn Sculpin", + "scientific": "Myoxocephalus quadricornis", + "area": "동해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "14700", + "major": "어류", + "mid": "해면어류", + "name": "삼치", + "nameEn": "Spanish Mackerel", + "scientific": "Scomberomorus niphonius", + "area": "남해·서해·동해", + "active": true, + "fishing": true + }, + { + "code": "14701", + "major": "어류", + "mid": "삼치", + "name": "꼬치삼치", + "nameEn": "Narrow-barred Mackerel", + "scientific": "Scomberomorus commerson", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14702", + "major": "어류", + "mid": "삼치", + "name": "동갈삼치", + "nameEn": "Indo-Pacific King Mackerel", + "scientific": "Scomberomorus guttatus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14703", + "major": "어류", + "mid": "삼치", + "name": "은삼치", + "nameEn": "Indo-Pacific Spanish Mackerel", + "scientific": "Scomberomorus semifasciatus", + "area": "남해 외해", + "active": true, + "fishing": false + }, + { + "code": "14704", + "major": "어류", + "mid": "삼치", + "name": "줄삼치", + "nameEn": "Streaked Spanish Mackerel", + "scientific": "Scomberomorus lineolatus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14705", + "major": "어류", + "mid": "삼치", + "name": "홍삼치", + "nameEn": "Wahoo", + "scientific": "Acanthocybium solandri", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14706", + "major": "어류", + "mid": "삼치", + "name": "평삼치", + "nameEn": "Queen Spanish Mackerel", + "scientific": "Scomberomorus queenslandicus", + "area": "남해", + "active": true, + "fishing": false + }, + { + "code": "14799", + "major": "어류", + "mid": "삼치", + "name": "기타 삼치", + "nameEn": "Other Spanish Mackerels", + "scientific": "Scomberomorus spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14800", + "major": "어류", + "mid": "해면어류", + "name": "상어", + "nameEn": "Shark", + "scientific": "Elasmobranchii spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "14801", + "major": "어류", + "mid": "상어", + "name": "곱상어", + "nameEn": "Spiny Dogfish", + "scientific": "Squalus acanthias", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "14802", + "major": "어류", + "mid": "상어", + "name": "괭이상어", + "nameEn": "Banded Houndshark", + "scientific": "Triakis scyllium", + "area": "남해·동해 연안", + "active": true, + "fishing": false + }, + { + "code": "14803", + "major": "어류", + "mid": "상어", + "name": "두툽상어", + "nameEn": "Japanese Bullhead Shark", + "scientific": "Heterodontus japonicus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14804", + "major": "어류", + "mid": "상어", + "name": "수염상어", + "nameEn": "Nurse Shark", + "scientific": "Ginglymostoma cirratum", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14805", + "major": "어류", + "mid": "상어", + "name": "악상어", + "nameEn": "Salmon Shark", + "scientific": "Lamna ditropis", + "area": "동해 외해", + "active": true, + "fishing": false + }, + { + "code": "14806", + "major": "어류", + "mid": "상어", + "name": "은상어", + "nameEn": "Chimaera", + "scientific": "Chimaera phantasma", + "area": "동해·남해 심해", + "active": true, + "fishing": false + }, + { + "code": "14807", + "major": "어류", + "mid": "상어", + "name": "칠성상어", + "nameEn": "Tawny Nurse Shark", + "scientific": "Nebrius ferrugineus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14808", + "major": "어류", + "mid": "상어", + "name": "톱상어", + "nameEn": "Sawshark", + "scientific": "Pristiophorus spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "14899", + "major": "어류", + "mid": "상어", + "name": "기타 상어", + "nameEn": "Other Sharks", + "scientific": "Elasmobranchii spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "14900", + "major": "어류", + "mid": "해면어류", + "name": "새치", + "nameEn": "Marlin / Swordfish", + "scientific": "Istiophoridae spp.", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14901", + "major": "어류", + "mid": "새치", + "name": "녹새치", + "nameEn": "Blue Marlin", + "scientific": "Makaira mazara", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14902", + "major": "어류", + "mid": "새치", + "name": "돛새치", + "nameEn": "Indo-Pacific Sailfish", + "scientific": "Istiophorus platypterus", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14903", + "major": "어류", + "mid": "새치", + "name": "백새치", + "nameEn": "Striped Marlin", + "scientific": "Kajikia audax", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14904", + "major": "어류", + "mid": "새치", + "name": "청새치", + "nameEn": "Blue Marlin", + "scientific": "Makaira nigricans", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14905", + "major": "어류", + "mid": "새치", + "name": "홍새치", + "nameEn": "Shortbill Spearfish", + "scientific": "Tetrapturus angustirostris", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14906", + "major": "어류", + "mid": "새치", + "name": "황새치", + "nameEn": "Swordfish", + "scientific": "Xiphias gladius", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14907", + "major": "어류", + "mid": "새치", + "name": "흑새치", + "nameEn": "Black Marlin", + "scientific": "Istiompax indica", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "14999", + "major": "어류", + "mid": "새치", + "name": "기타 새치", + "nameEn": "Other Billfishes", + "scientific": "Istiophoridae spp.", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "15000", + "major": "어류", + "mid": "해면어류", + "name": "서대", + "nameEn": "Sole", + "scientific": "Cynoglossidae spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "15001", + "major": "어류", + "mid": "서대", + "name": "노랑각시서대", + "nameEn": "Yellow Tonguefish", + "scientific": "Cynoglossus itinus", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "15002", + "major": "어류", + "mid": "서대", + "name": "참서대", + "nameEn": "Korean Tonguefish", + "scientific": "Cynoglossus joyneri", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "15099", + "major": "어류", + "mid": "서대", + "name": "기타 서대", + "nameEn": "Other Tonguefishes", + "scientific": "Cynoglossidae spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "15100", + "major": "어류", + "mid": "해면어류", + "name": "성대", + "nameEn": "Searobin / Gurnard", + "scientific": "Triglidae spp.", + "area": "남해·동해", + "active": true, + "fishing": true + }, + { + "code": "15101", + "major": "어류", + "mid": "성대", + "name": "달강어", + "nameEn": "Helmet Gurnard", + "scientific": "Dactyloptena orientalis", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "15102", + "major": "어류", + "mid": "성대", + "name": "밑성대", + "nameEn": "East Asian Gurnard", + "scientific": "Lepidotrigla microptera", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "15103", + "major": "어류", + "mid": "성대", + "name": "쌍뿔성대", + "nameEn": "Bigscale Gurnard", + "scientific": "Lepidotrigla alata", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "15199", + "major": "어류", + "mid": "성대", + "name": "기타 성대", + "nameEn": "Other Gurnards", + "scientific": "Triglidae spp.", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "15200", + "major": "어류", + "mid": "해면어류", + "name": "숭어", + "nameEn": "Mullet", + "scientific": "Mugilidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "15201", + "major": "어류", + "mid": "숭어", + "name": "감숭어", + "nameEn": "Largescale Mullet", + "scientific": "Liza macrolepis", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "15202", + "major": "어류", + "mid": "숭어", + "name": "게숭어", + "nameEn": "Flathead Mullet", + "scientific": "Mugil cephalus", + "area": "전 연안 기수역", + "active": true, + "fishing": false + }, + { + "code": "15203", + "major": "어류", + "mid": "숭어", + "name": "등줄숭어", + "nameEn": "Striped Mullet", + "scientific": "Mugil cephalus", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "15204", + "major": "어류", + "mid": "숭어", + "name": "참숭어", + "nameEn": "Grey Mullet", + "scientific": "Mugil cephalus", + "area": "전 연안 기수역", + "active": true, + "fishing": false + }, + { + "code": "15299", + "major": "어류", + "mid": "숭어", + "name": "기타 숭어", + "nameEn": "Other Mullets", + "scientific": "Mugilidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "15300", + "major": "어류", + "mid": "해면어류", + "name": "아귀", + "nameEn": "Monkfish / Anglerfish", + "scientific": "Lophius spp.", + "area": "동해·남해 사질저", + "active": true, + "fishing": false + }, + { + "code": "15301", + "major": "어류", + "mid": "아귀", + "name": "황아귀", + "nameEn": "Yellow Goosefish", + "scientific": "Lophiomus setigerus", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "15399", + "major": "어류", + "mid": "아귀", + "name": "기타 아귀", + "nameEn": "Other Goosefishes", + "scientific": "Lophiidae spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "15400", + "major": "어류", + "mid": "해면어류", + "name": "양미리", + "nameEn": "Sand Eel", + "scientific": "Hyperoplus lanceolatus", + "area": "동해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "15401", + "major": "어류", + "mid": "양미리", + "name": "양미리", + "nameEn": "Pacific Sand Lance", + "scientific": "Ammodytes hexapterus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "15500", + "major": "어류", + "mid": "해면어류", + "name": "양태(장대)", + "nameEn": "Flathead", + "scientific": "Platycephalus spp.", + "area": "남해·동해·서해", + "active": true, + "fishing": false + }, + { + "code": "15501", + "major": "어류", + "mid": "양태(장대)", + "name": "양태(장대)", + "nameEn": "Bartail Flathead", + "scientific": "Platycephalus indicus", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "15600", + "major": "어류", + "mid": "해면어류", + "name": "연어", + "nameEn": "Salmon", + "scientific": "Oncorhynchus spp.", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "15601", + "major": "어류", + "mid": "연어", + "name": "연어", + "nameEn": "Chum Salmon", + "scientific": "Oncorhynchus keta", + "area": "동해 (회유)", + "active": true, + "fishing": false + }, + { + "code": "15700", + "major": "어류", + "mid": "해면어류", + "name": "우럭", + "nameEn": "Korean Rockfish", + "scientific": "Sebastes schlegeli", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "15701", + "major": "어류", + "mid": "우럭", + "name": "우럭", + "nameEn": "Korean Rockfish", + "scientific": "Sebastes schlegeli", + "area": "동해·남해·서해 암초", + "active": true, + "fishing": true + }, + { + "code": "15800", + "major": "어류", + "mid": "해면어류", + "name": "장갱이", + "nameEn": "Eel Pout", + "scientific": "Zoarcidae spp.", + "area": "동해 심해", + "active": true, + "fishing": false + }, + { + "code": "15801", + "major": "어류", + "mid": "장갱이", + "name": "장갱이", + "nameEn": "Eel Pout", + "scientific": "Zoarces elongatus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "15900", + "major": "어류", + "mid": "해면어류", + "name": "장어", + "nameEn": "Eel", + "scientific": "Anguillidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "15901", + "major": "어류", + "mid": "장어", + "name": "갯장어", + "nameEn": "Daggertooth Pike Conger", + "scientific": "Muraenesox cinereus", + "area": "남해·서해 연안", + "active": true, + "fishing": true + }, + { + "code": "15902", + "major": "어류", + "mid": "장어", + "name": "먹장어", + "nameEn": "Hagfish", + "scientific": "Eptatretus burgeri", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "15903", + "major": "어류", + "mid": "장어", + "name": "묵꾀장어", + "nameEn": "Japanese Eel", + "scientific": "Anguilla japonica", + "area": "전 연안·기수역", + "active": true, + "fishing": false + }, + { + "code": "15904", + "major": "어류", + "mid": "장어", + "name": "붕장어", + "nameEn": "Conger Eel", + "scientific": "Conger myriaster", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "15905", + "major": "어류", + "mid": "장어", + "name": "칠성장어", + "nameEn": "Pacific Lamprey", + "scientific": "Entosphenus tridentatus", + "area": "동해·하천", + "active": true, + "fishing": false + }, + { + "code": "15999", + "major": "어류", + "mid": "장어", + "name": "기타 장어", + "nameEn": "Other Eels", + "scientific": "Anguilliformes spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "16000", + "major": "어류", + "mid": "해면어류", + "name": "적어", + "nameEn": "Cardinalfish", + "scientific": "Apogonidae spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16001", + "major": "어류", + "mid": "적어", + "name": "적어", + "nameEn": "Spotnape Cardinalfish", + "scientific": "Apogon notatus", + "area": "남해·제주 연안", + "active": true, + "fishing": false + }, + { + "code": "16100", + "major": "어류", + "mid": "해면어류", + "name": "전기가오리", + "nameEn": "Electric Ray", + "scientific": "Torpedinidae spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16101", + "major": "어류", + "mid": "전기가오리", + "name": "전기가오리", + "nameEn": "Ocellated Electric Ray", + "scientific": "Torpedo tokionis", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16200", + "major": "어류", + "mid": "해면어류", + "name": "전갱이", + "nameEn": "Jack / Scad", + "scientific": "Carangidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "16201", + "major": "어류", + "mid": "전갱이", + "name": "갈전갱이", + "nameEn": "Yellowstripe Scad", + "scientific": "Selaroides leptolepis", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16202", + "major": "어류", + "mid": "전갱이", + "name": "민전갱이", + "nameEn": "Bigeye Scad", + "scientific": "Selar crumenophthalmus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16299", + "major": "어류", + "mid": "전갱이", + "name": "기타 전갱이", + "nameEn": "Other Scads", + "scientific": "Carangidae spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16300", + "major": "어류", + "mid": "해면어류", + "name": "조기", + "nameEn": "Croaker", + "scientific": "Sciaenidae spp.", + "area": "서해·남해", + "active": true, + "fishing": true + }, + { + "code": "16301", + "major": "어류", + "mid": "조기", + "name": "백조기", + "nameEn": "White Croaker", + "scientific": "Pennahia argentata", + "area": "서해·남해", + "active": true, + "fishing": true + }, + { + "code": "16302", + "major": "어류", + "mid": "조기", + "name": "부세", + "nameEn": "Croceine Croaker", + "scientific": "Larimichthys crocea", + "area": "서해", + "active": true, + "fishing": false + }, + { + "code": "16303", + "major": "어류", + "mid": "조기", + "name": "수조기", + "nameEn": "Shi Drum", + "scientific": "Umbrina cirrosa", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "16304", + "major": "어류", + "mid": "조기", + "name": "참조기", + "nameEn": "Small Yellow Croaker", + "scientific": "Larimichthys polyactis", + "area": "서해", + "active": true, + "fishing": true + }, + { + "code": "16305", + "major": "어류", + "mid": "조기", + "name": "황석어", + "nameEn": "Yellow Croaker", + "scientific": "Larimichthys polyactis", + "area": "서해", + "active": true, + "fishing": false + }, + { + "code": "16399", + "major": "어류", + "mid": "조기", + "name": "기타 조기", + "nameEn": "Other Croakers", + "scientific": "Sciaenidae spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "16400", + "major": "어류", + "mid": "해면어류", + "name": "쥐치", + "nameEn": "Filefish", + "scientific": "Monacanthidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "16401", + "major": "어류", + "mid": "쥐치", + "name": "그물코쥐치", + "nameEn": "Filefish", + "scientific": "Stephanolepis cirrhifer", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "16402", + "major": "어류", + "mid": "쥐치", + "name": "말쥐치", + "nameEn": "Black Scraper", + "scientific": "Thamnaconus modestus", + "area": "남해·동해·서해", + "active": true, + "fishing": false + }, + { + "code": "16403", + "major": "어류", + "mid": "쥐치", + "name": "별쥐치", + "nameEn": "Unicornfish", + "scientific": "Naso unicornis", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16404", + "major": "어류", + "mid": "쥐치", + "name": "분홍쥐치", + "nameEn": "Unicorn Filefish", + "scientific": "Aluterus monoceros", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16499", + "major": "어류", + "mid": "쥐치", + "name": "기타 쥐치", + "nameEn": "Other Filefishes", + "scientific": "Monacanthidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "16500", + "major": "어류", + "mid": "해면어류", + "name": "준치", + "nameEn": "Gizzard Shad", + "scientific": "Konosirus punctatus", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "16501", + "major": "어류", + "mid": "준치", + "name": "준치", + "nameEn": "Chinese Gizzard Shad", + "scientific": "Konosirus punctatus", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "16600", + "major": "어류", + "mid": "해면어류", + "name": "철갑상어", + "nameEn": "Sturgeon", + "scientific": "Acipenseridae spp.", + "area": "서해·하천", + "active": true, + "fishing": false + }, + { + "code": "16601", + "major": "어류", + "mid": "철갑상어", + "name": "철갑상어", + "nameEn": "Amur Sturgeon", + "scientific": "Acipenser schrenckii", + "area": "서해·하천", + "active": true, + "fishing": false + }, + { + "code": "16700", + "major": "어류", + "mid": "해면어류", + "name": "청어", + "nameEn": "Herring / Shad", + "scientific": "Clupeidae spp.", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "16701", + "major": "어류", + "mid": "청어", + "name": "벤댕이", + "nameEn": "Japanese Sardinella", + "scientific": "Sardinella zunasi", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "16702", + "major": "어류", + "mid": "청어", + "name": "전어", + "nameEn": "Japanese Gizzard Shad", + "scientific": "Konosirus punctatus", + "area": "서해·남해 연안 (가을)", + "active": true, + "fishing": true + }, + { + "code": "16703", + "major": "어류", + "mid": "청어", + "name": "정어리", + "nameEn": "Japanese Sardine", + "scientific": "Sardinops melanostictus", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "16799", + "major": "어류", + "mid": "청어", + "name": "기타 청어", + "nameEn": "Other Herrings", + "scientific": "Clupeidae spp.", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "16800", + "major": "어류", + "mid": "해면어류", + "name": "촉수", + "nameEn": "Goatfish", + "scientific": "Mullidae spp.", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "16801", + "major": "어류", + "mid": "촉수", + "name": "촉수", + "nameEn": "Yellowfin Goatfish", + "scientific": "Mulloidichthys vanicolensis", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "16900", + "major": "어류", + "mid": "해면어류", + "name": "통치", + "nameEn": "Largehead Hairtail", + "scientific": "Trichiurus lepturus", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "16901", + "major": "어류", + "mid": "통치", + "name": "통치", + "nameEn": "Largehead Hairtail", + "scientific": "Trichiurus lepturus", + "area": "남해·서해 연안", + "active": true, + "fishing": false + }, + { + "code": "17000", + "major": "어류", + "mid": "해면어류", + "name": "학공치", + "nameEn": "Halfbeak", + "scientific": "Hemiramphidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "17001", + "major": "어류", + "mid": "학공치", + "name": "학공치", + "nameEn": "Japanese Halfbeak", + "scientific": "Hyporhamphus sajori", + "area": "전 연안 (봄)", + "active": true, + "fishing": true + }, + { + "code": "17100", + "major": "어류", + "mid": "해면어류", + "name": "횟대", + "nameEn": "Sculpin", + "scientific": "Cottidae spp.", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "17101", + "major": "어류", + "mid": "횟대", + "name": "꼬마횟대", + "nameEn": "Armed Sculpin", + "scientific": "Gymnocanthus herzensteini", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "17102", + "major": "어류", + "mid": "횟대", + "name": "대구횟대", + "nameEn": "Pacific Staghorn Sculpin", + "scientific": "Leptocottus armatus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "17199", + "major": "어류", + "mid": "횟대", + "name": "기타 횟대", + "nameEn": "Other Sculpins", + "scientific": "Cottidae spp.", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "17200", + "major": "어류", + "mid": "해면어류", + "name": "홍어", + "nameEn": "Skate", + "scientific": "Rajidae spp.", + "area": "서해·남해", + "active": true, + "fishing": true + }, + { + "code": "17201", + "major": "어류", + "mid": "홍어", + "name": "광동홍어", + "nameEn": "Guangdong Skate", + "scientific": "Raja porosa", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "17202", + "major": "어류", + "mid": "홍어", + "name": "살홍어", + "nameEn": "Barn-door Skate", + "scientific": "Raja binoculata", + "area": "서해", + "active": true, + "fishing": false + }, + { + "code": "17299", + "major": "어류", + "mid": "홍어", + "name": "기타 홍어", + "nameEn": "Other Skates", + "scientific": "Rajidae spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "17300", + "major": "어류", + "mid": "해면어류", + "name": "해마", + "nameEn": "Seahorse", + "scientific": "Hippocampus spp.", + "area": "남해·서해 해초밭", + "active": true, + "fishing": false + }, + { + "code": "17301", + "major": "어류", + "mid": "해마", + "name": "해마", + "nameEn": "Lined Seahorse", + "scientific": "Hippocampus erectus", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "17400", + "major": "어류", + "mid": "해면어류", + "name": "해덕", + "nameEn": "Haddock", + "scientific": "Melanogrammus aeglefinus", + "area": "동해 북부", + "active": true, + "fishing": false + }, + { + "code": "17401", + "major": "어류", + "mid": "해덕", + "name": "해덕", + "nameEn": "Haddock", + "scientific": "Melanogrammus aeglefinus", + "area": "동해 북부", + "active": true, + "fishing": false + }, + { + "code": "19900", + "major": "어류", + "mid": "해면어류", + "name": "기타 해면어류", + "nameEn": "Other Marine Fish", + "scientific": "Pisces spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "19999", + "major": "어류", + "mid": "해면어류", + "name": "기타 해면어류", + "nameEn": "Other Marine Fish (Misc.)", + "scientific": "Pisces spp.", + "area": "전 해역", + "active": true, + "fishing": true + }, + { + "code": "20000", + "major": "패류", + "mid": "대분류", + "name": "해면패류", + "nameEn": "Marine Shellfish", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "20100", + "major": "패류", + "mid": "해면패류", + "name": "가리비", + "nameEn": "Scallop", + "scientific": "Pectinidae spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "20101", + "major": "패류", + "mid": "가리비", + "name": "가리비", + "nameEn": "Japanese Scallop", + "scientific": "Mizuhopecten yessoensis", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "20200", + "major": "패류", + "mid": "해면패류", + "name": "가무락", + "nameEn": "Clam", + "scientific": "Cyclina sinensis", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20201", + "major": "패류", + "mid": "가무락", + "name": "가무락", + "nameEn": "Venus Clam", + "scientific": "Cyclina sinensis", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20300", + "major": "패류", + "mid": "해면패류", + "name": "고둥", + "nameEn": "Turban Shell / Top Shell", + "scientific": "Turbinidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "20301", + "major": "패류", + "mid": "고둥", + "name": "소라", + "nameEn": "Turban Shell", + "scientific": "Turbo cornutus", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "20302", + "major": "패류", + "mid": "고둥", + "name": "쇠고둥", + "nameEn": "Top Shell", + "scientific": "Monodonta labio", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "20303", + "major": "패류", + "mid": "고둥", + "name": "참고둥", + "nameEn": "Korean Top Shell", + "scientific": "Omphalius rusticus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "20399", + "major": "패류", + "mid": "고둥", + "name": "기타 고둥", + "nameEn": "Other Turbans", + "scientific": "Turbinidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "20400", + "major": "패류", + "mid": "해면패류", + "name": "골뱅이", + "nameEn": "Whelk", + "scientific": "Buccinum spp.", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "20401", + "major": "패류", + "mid": "골뱅이", + "name": "골뱅이", + "nameEn": "Whelk", + "scientific": "Buccinum isaotakii", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "20500", + "major": "패류", + "mid": "해면패류", + "name": "꼬막", + "nameEn": "Cockle", + "scientific": "Tegillarca spp.", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20501", + "major": "패류", + "mid": "꼬막", + "name": "새꼬막", + "nameEn": "Ark Shell", + "scientific": "Scapharca subcrenata", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20502", + "major": "패류", + "mid": "꼬막", + "name": "피조개", + "nameEn": "Bloody Clam", + "scientific": "Scapharca broughtonii", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "20503", + "major": "패류", + "mid": "꼬막", + "name": "참꼬막", + "nameEn": "Blood Cockle", + "scientific": "Tegillarca granosa", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20599", + "major": "패류", + "mid": "꼬막", + "name": "기타 꼬막", + "nameEn": "Other Cockles", + "scientific": "Tegillarca spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "20600", + "major": "패류", + "mid": "해면패류", + "name": "굴", + "nameEn": "Oyster", + "scientific": "Crassostrea spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "20601", + "major": "패류", + "mid": "굴", + "name": "참굴", + "nameEn": "Pacific Oyster", + "scientific": "Crassostrea gigas", + "area": "남해·서해 (양식)", + "active": true, + "fishing": false + }, + { + "code": "20602", + "major": "패류", + "mid": "굴", + "name": "가시굴", + "nameEn": "Spiny Oyster", + "scientific": "Spondylus varius", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "20603", + "major": "패류", + "mid": "굴", + "name": "바위굴", + "nameEn": "Rock Oyster", + "scientific": "Saccostrea kegaki", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "20604", + "major": "패류", + "mid": "굴", + "name": "토굴", + "nameEn": "Muddy Oyster", + "scientific": "Crassostrea gigas", + "area": "서해 갯벌 (토굴 양식)", + "active": true, + "fishing": false + }, + { + "code": "20699", + "major": "패류", + "mid": "굴", + "name": "기타 굴", + "nameEn": "Other Oysters", + "scientific": "Ostreidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "20700", + "major": "패류", + "mid": "해면패류", + "name": "맛", + "nameEn": "Razor Clam", + "scientific": "Solenidae spp.", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20701", + "major": "패류", + "mid": "맛", + "name": "가리맛", + "nameEn": "Razor Clam", + "scientific": "Sinonovacula constricta", + "area": "서해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20702", + "major": "패류", + "mid": "맛", + "name": "긴맛", + "nameEn": "Jackknife Clam", + "scientific": "Solen gouldi", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "20799", + "major": "패류", + "mid": "맛", + "name": "기타 맛", + "nameEn": "Other Razor Clams", + "scientific": "Solenidae spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "20800", + "major": "패류", + "mid": "해면패류", + "name": "바지락", + "nameEn": "Manila Clam", + "scientific": "Ruditapes philippinarum", + "area": "서해·남해 갯벌", + "active": true, + "fishing": true + }, + { + "code": "20801", + "major": "패류", + "mid": "바지락", + "name": "물바지락", + "nameEn": "Water Clam", + "scientific": "Ruditapes variegata", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "20899", + "major": "패류", + "mid": "바지락", + "name": "기타 바지락", + "nameEn": "Other Manila Clams", + "scientific": "Ruditapes spp.", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "20900", + "major": "패류", + "mid": "해면패류", + "name": "전복", + "nameEn": "Abalone", + "scientific": "Haliotis spp.", + "area": "남해·제주·동해", + "active": true, + "fishing": true + }, + { + "code": "20901", + "major": "패류", + "mid": "전복", + "name": "말전복", + "nameEn": "Pacific Abalone", + "scientific": "Haliotis discus hannai", + "area": "남해·제주·동해 (양식)", + "active": true, + "fishing": false + }, + { + "code": "20902", + "major": "패류", + "mid": "전복", + "name": "오분자기", + "nameEn": "Small Abalone", + "scientific": "Haliotis diversicolor", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "20999", + "major": "패류", + "mid": "전복", + "name": "기타 전복", + "nameEn": "Other Abalones", + "scientific": "Haliotis spp.", + "area": "남해·제주·동해", + "active": true, + "fishing": false + }, + { + "code": "21000", + "major": "패류", + "mid": "해면패류", + "name": "조개", + "nameEn": "Clam (General)", + "scientific": "Bivalvia spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "21001", + "major": "패류", + "mid": "조개", + "name": "가리맛조개", + "nameEn": "Razor Clam", + "scientific": "Sinonovacula constricta", + "area": "서해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "21002", + "major": "패류", + "mid": "조개", + "name": "개량조개", + "nameEn": "Japanese Surf Clam", + "scientific": "Mactra chinensis", + "area": "서해·동해 사질저", + "active": true, + "fishing": false + }, + { + "code": "21003", + "major": "패류", + "mid": "조개", + "name": "동죽", + "nameEn": "Short-neck Clam", + "scientific": "Ruditapes philippinarum", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "21004", + "major": "패류", + "mid": "조개", + "name": "맛조개", + "nameEn": "Razor Shell", + "scientific": "Solen strictus", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "21005", + "major": "패류", + "mid": "조개", + "name": "백합", + "nameEn": "Hard Clam", + "scientific": "Meretrix lusoria", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "21006", + "major": "패류", + "mid": "조개", + "name": "비단조개", + "nameEn": "Ark Clam", + "scientific": "Arca spp.", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "21007", + "major": "패류", + "mid": "조개", + "name": "새조개", + "nameEn": "Cockle", + "scientific": "Fulvia mutica", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "21008", + "major": "패류", + "mid": "조개", + "name": "키조개", + "nameEn": "Fan Shell", + "scientific": "Atrina pectinata", + "area": "남해·서해 사질저", + "active": true, + "fishing": false + }, + { + "code": "21009", + "major": "패류", + "mid": "조개", + "name": "진주조개", + "nameEn": "Pearl Oyster", + "scientific": "Pinctada martensii", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "21010", + "major": "패류", + "mid": "조개", + "name": "코끼리조개", + "nameEn": "Geoduck", + "scientific": "Panopea generosa", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "21099", + "major": "패류", + "mid": "조개", + "name": "기타 조개", + "nameEn": "Other Clams", + "scientific": "Bivalvia spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "21100", + "major": "패류", + "mid": "해면패류", + "name": "홍합", + "nameEn": "Mussel", + "scientific": "Mytilidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "21101", + "major": "패류", + "mid": "홍합", + "name": "진주담치", + "nameEn": "Mediterranean Mussel", + "scientific": "Mytilus galloprovincialis", + "area": "전 연안 (양식)", + "active": true, + "fishing": false + }, + { + "code": "21199", + "major": "패류", + "mid": "홍합", + "name": "기타 홍합", + "nameEn": "Other Mussels", + "scientific": "Mytilidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "21200", + "major": "패류", + "mid": "해면패류", + "name": "해락", + "nameEn": "Jingle Shell", + "scientific": "Anomia spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "21201", + "major": "패류", + "mid": "해락", + "name": "해락", + "nameEn": "Jingle Shell", + "scientific": "Anomia chinensis", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "21300", + "major": "패류", + "mid": "해면패류", + "name": "우렁이", + "nameEn": "Moon Snail", + "scientific": "Naticidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "21301", + "major": "패류", + "mid": "우렁이", + "name": "큰구슬우렁이", + "nameEn": "Large Moon Snail", + "scientific": "Glossaulax didyma", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "21399", + "major": "패류", + "mid": "우렁이", + "name": "기타 우렁이", + "nameEn": "Other Moon Snails", + "scientific": "Naticidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "29900", + "major": "패류", + "mid": "해면패류", + "name": "기타 해면패류", + "nameEn": "Other Marine Shellfish", + "scientific": "Bivalvia/Gastropoda spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "29999", + "major": "패류", + "mid": "해면패류", + "name": "기타 해면패류", + "nameEn": "Other Marine Shellfish (Misc.)", + "scientific": "Bivalvia/Gastropoda spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "30000", + "major": "갑각류", + "mid": "대분류", + "name": "해면갑각류", + "nameEn": "Marine Crustaceans", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "30100", + "major": "갑각류", + "mid": "해면갑각류", + "name": "가재", + "nameEn": "Lobster / Mantis Shrimp", + "scientific": "Stomatopoda spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "30101", + "major": "갑각류", + "mid": "가재", + "name": "갯가재", + "nameEn": "Mantis Shrimp", + "scientific": "Oratosquilla oratoria", + "area": "전 연안 사질저", + "active": true, + "fishing": false + }, + { + "code": "30102", + "major": "갑각류", + "mid": "가재", + "name": "바다가재", + "nameEn": "Spiny Lobster", + "scientific": "Panulirus japonicus", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "30103", + "major": "갑각류", + "mid": "가재", + "name": "뿔가재", + "nameEn": "Slipper Lobster", + "scientific": "Ibacus novemdentatus", + "area": "남해·동해", + "active": true, + "fishing": false + }, + { + "code": "30199", + "major": "갑각류", + "mid": "가재", + "name": "기타 가재", + "nameEn": "Other Lobsters", + "scientific": "Palinuridae spp.", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "30200", + "major": "갑각류", + "mid": "해면갑각류", + "name": "게", + "nameEn": "Crab", + "scientific": "Brachyura spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "30201", + "major": "갑각류", + "mid": "게", + "name": "대게", + "nameEn": "Snow Crab", + "scientific": "Chionoecetes opilio", + "area": "동해 심해", + "active": true, + "fishing": true + }, + { + "code": "30202", + "major": "갑각류", + "mid": "게", + "name": "돌게", + "nameEn": "Rock Crab", + "scientific": "Gaetice depressus", + "area": "전 연안 암초", + "active": true, + "fishing": false + }, + { + "code": "30203", + "major": "갑각류", + "mid": "게", + "name": "방게", + "nameEn": "Fiddler Crab", + "scientific": "Uca spp.", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "30204", + "major": "갑각류", + "mid": "게", + "name": "붉은대게", + "nameEn": "Red Snow Crab", + "scientific": "Chionoecetes japonicus", + "area": "동해 심해", + "active": true, + "fishing": false + }, + { + "code": "30205", + "major": "갑각류", + "mid": "게", + "name": "수꽃게", + "nameEn": "Swimming Crab (Male)", + "scientific": "Portunus trituberculatus", + "area": "서해·남해", + "active": true, + "fishing": true + }, + { + "code": "30206", + "major": "갑각류", + "mid": "게", + "name": "암꽃게", + "nameEn": "Swimming Crab (Female)", + "scientific": "Portunus trituberculatus", + "area": "서해·남해", + "active": true, + "fishing": true + }, + { + "code": "30207", + "major": "갑각류", + "mid": "게", + "name": "왕게", + "nameEn": "Red King Crab", + "scientific": "Paralithodes camtschaticus", + "area": "동해 심해", + "active": true, + "fishing": false + }, + { + "code": "30208", + "major": "갑각류", + "mid": "게", + "name": "칡게", + "nameEn": "Shore Crab", + "scientific": "Hemigrapsus sanguineus", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "30209", + "major": "갑각류", + "mid": "게", + "name": "털게", + "nameEn": "Hairy Crab", + "scientific": "Erimacrus isenbeckii", + "area": "동해·서해", + "active": true, + "fishing": false + }, + { + "code": "30299", + "major": "갑각류", + "mid": "게", + "name": "기타 게", + "nameEn": "Other Crabs", + "scientific": "Brachyura spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "30300", + "major": "갑각류", + "mid": "해면갑각류", + "name": "새우", + "nameEn": "Shrimp / Prawn", + "scientific": "Penaeidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "30301", + "major": "갑각류", + "mid": "새우", + "name": "대하", + "nameEn": "Fleshy Prawn", + "scientific": "Penaeus chinensis", + "area": "서해 연안", + "active": true, + "fishing": false + }, + { + "code": "30302", + "major": "갑각류", + "mid": "새우", + "name": "중하", + "nameEn": "Kuruma Prawn", + "scientific": "Penaeus japonicus", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "30303", + "major": "갑각류", + "mid": "새우", + "name": "꽃새우", + "nameEn": "Flower Shrimp", + "scientific": "Mierspenaeopsis hardwickii", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "30304", + "major": "갑각류", + "mid": "새우", + "name": "남빙양새우", + "nameEn": "Antarctic Krill", + "scientific": "Euphausia superba", + "area": "남빙양 원양", + "active": true, + "fishing": false + }, + { + "code": "30305", + "major": "갑각류", + "mid": "새우", + "name": "닭새우", + "nameEn": "Coral Shrimp", + "scientific": "Stenopus hispidus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "30306", + "major": "갑각류", + "mid": "새우", + "name": "보리새우", + "nameEn": "Bamboo Prawn", + "scientific": "Penaeus japonicus", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "30307", + "major": "갑각류", + "mid": "새우", + "name": "분홍새우", + "nameEn": "Pink Shrimp", + "scientific": "Pandalus hypsinotus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "30308", + "major": "갑각류", + "mid": "새우", + "name": "젓새우", + "nameEn": "Acetes Shrimp", + "scientific": "Acetes japonicus", + "area": "서해·남해", + "active": true, + "fishing": false + }, + { + "code": "30399", + "major": "갑각류", + "mid": "새우", + "name": "기타 새우", + "nameEn": "Other Shrimps", + "scientific": "Penaeidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "39900", + "major": "갑각류", + "mid": "해면갑각류", + "name": "기타 해면갑각류", + "nameEn": "Other Marine Crustaceans", + "scientific": "Crustacea spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "39999", + "major": "갑각류", + "mid": "해면갑각류", + "name": "기타 해면갑각류", + "nameEn": "Other Marine Crustaceans (Misc.)", + "scientific": "Crustacea spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "40000", + "major": "연체류", + "mid": "대분류", + "name": "해면연체류", + "nameEn": "Marine Mollusks", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "40100", + "major": "연체류", + "mid": "해면연체류", + "name": "갑오징어", + "nameEn": "Cuttlefish", + "scientific": "Sepiidae spp.", + "area": "남해·서해·동해", + "active": true, + "fishing": true + }, + { + "code": "40101", + "major": "연체류", + "mid": "갑오징어", + "name": "무늬오징어", + "nameEn": "Bigfin Reef Squid", + "scientific": "Sepioteuthis lessoniana", + "area": "남해·제주 연안", + "active": true, + "fishing": true + }, + { + "code": "40102", + "major": "연체류", + "mid": "갑오징어", + "name": "쇠오징어", + "nameEn": "Small Cuttlefish", + "scientific": "Sepiella japonica", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "40199", + "major": "연체류", + "mid": "갑오징어", + "name": "기타 갑오징어", + "nameEn": "Other Cuttlefishes", + "scientific": "Sepiidae spp.", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "40200", + "major": "연체류", + "mid": "해면연체류", + "name": "꼴뚜기", + "nameEn": "Squid (Small)", + "scientific": "Loliginidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "40201", + "major": "연체류", + "mid": "꼴뚜기", + "name": "창꼴뚜기", + "nameEn": "Spear Squid", + "scientific": "Heterololigo bleekeri", + "area": "남해·동해 연안", + "active": true, + "fishing": true + }, + { + "code": "40202", + "major": "연체류", + "mid": "꼴뚜기", + "name": "화살꼴뚜기", + "nameEn": "Arrow Squid", + "scientific": "Uroteuthis chinensis", + "area": "남해", + "active": true, + "fishing": false + }, + { + "code": "40299", + "major": "연체류", + "mid": "꼴뚜기", + "name": "기타 꼴뚜기", + "nameEn": "Other Squids (Small)", + "scientific": "Loliginidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "40300", + "major": "연체류", + "mid": "해면연체류", + "name": "오징어", + "nameEn": "Squid", + "scientific": "Ommastrephidae spp.", + "area": "동해·남해·제주", + "active": true, + "fishing": true + }, + { + "code": "40301", + "major": "연체류", + "mid": "오징어", + "name": "살오징어", + "nameEn": "Japanese Flying Squid", + "scientific": "Todarodes pacificus", + "area": "동해·남해", + "active": true, + "fishing": true + }, + { + "code": "40302", + "major": "연체류", + "mid": "오징어", + "name": "빨강오징어", + "nameEn": "Neon Flying Squid", + "scientific": "Ommastrephes bartramii", + "area": "동해 외해", + "active": true, + "fishing": false + }, + { + "code": "40303", + "major": "연체류", + "mid": "오징어", + "name": "아르헨티나오징어", + "nameEn": "Argentine Shortfin Squid", + "scientific": "Illex argentinus", + "area": "남대서양 원양", + "active": true, + "fishing": false + }, + { + "code": "40304", + "major": "연체류", + "mid": "오징어", + "name": "아메리카대왕오징어", + "nameEn": "Humboldt Squid", + "scientific": "Dosidicus gigas", + "area": "동태평양 원양", + "active": true, + "fishing": false + }, + { + "code": "40305", + "major": "연체류", + "mid": "오징어", + "name": "웰링턴오징어", + "nameEn": "Wellington Flying Squid", + "scientific": "Nototodarus sloanii", + "area": "뉴질랜드 원양", + "active": true, + "fishing": false + }, + { + "code": "40399", + "major": "연체류", + "mid": "오징어", + "name": "기타 오징어", + "nameEn": "Other Squids", + "scientific": "Ommastrephidae spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "40400", + "major": "연체류", + "mid": "해면연체류", + "name": "문어", + "nameEn": "Octopus", + "scientific": "Octopodidae spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "40401", + "major": "연체류", + "mid": "문어", + "name": "낙지", + "nameEn": "Long-arm Octopus", + "scientific": "Octopus minor", + "area": "서해·남해 갯벌", + "active": true, + "fishing": true + }, + { + "code": "40402", + "major": "연체류", + "mid": "문어", + "name": "대문어", + "nameEn": "Giant Pacific Octopus", + "scientific": "Enteroctopus dofleini", + "area": "동해 연안", + "active": true, + "fishing": true + }, + { + "code": "40403", + "major": "연체류", + "mid": "문어", + "name": "백문어", + "nameEn": "White Octopus", + "scientific": "Octopus vulgaris", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "40404", + "major": "연체류", + "mid": "문어", + "name": "세발낙지", + "nameEn": "Small Octopus", + "scientific": "Octopus minor", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "40405", + "major": "연체류", + "mid": "문어", + "name": "쭈꾸미", + "nameEn": "Webfoot Octopus", + "scientific": "Amphioctopus fangsiao", + "area": "서해·남해 연안", + "active": true, + "fishing": true + }, + { + "code": "40406", + "major": "연체류", + "mid": "문어", + "name": "참문어", + "nameEn": "Common Octopus", + "scientific": "Octopus vulgaris", + "area": "남해·제주·동해", + "active": true, + "fishing": true + }, + { + "code": "40407", + "major": "연체류", + "mid": "문어", + "name": "피문어", + "nameEn": "Musky Octopus", + "scientific": "Eledone moschata", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "40499", + "major": "연체류", + "mid": "문어", + "name": "기타 문어", + "nameEn": "Other Octopuses", + "scientific": "Octopodidae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "40500", + "major": "연체류", + "mid": "해면연체류", + "name": "한치", + "nameEn": "Swordtip Squid", + "scientific": "Uroteuthis edulis", + "area": "남해·제주·동해", + "active": true, + "fishing": true + }, + { + "code": "40501", + "major": "연체류", + "mid": "한치", + "name": "한치", + "nameEn": "Swordtip Squid", + "scientific": "Uroteuthis edulis", + "area": "제주·남해 외해", + "active": true, + "fishing": true + }, + { + "code": "49900", + "major": "연체류", + "mid": "해면연체류", + "name": "기타 해면연체류", + "nameEn": "Other Marine Mollusks", + "scientific": "Mollusca spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "49999", + "major": "연체류", + "mid": "해면연체류", + "name": "기타 해면연체류", + "nameEn": "Other Marine Mollusks (Misc.)", + "scientific": "Mollusca spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "50000", + "major": "기타해양생물", + "mid": "대분류", + "name": "해면기타", + "nameEn": "Other Marine Organisms", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "50100", + "major": "기타해양생물", + "mid": "해면기타", + "name": "거북", + "nameEn": "Sea Turtle", + "scientific": "Cheloniidae spp.", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "50101", + "major": "기타해양생물", + "mid": "거북", + "name": "거북", + "nameEn": "Sea Turtle", + "scientific": "Chelonia mydas", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "50200", + "major": "기타해양생물", + "mid": "해면기타", + "name": "개불", + "nameEn": "Spoon Worm", + "scientific": "Urechis unicinctus", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "50201", + "major": "기타해양생물", + "mid": "개불", + "name": "개불", + "nameEn": "Fat Innkeeper Worm", + "scientific": "Urechis unicinctus", + "area": "서해·남해 갯벌 사질저", + "active": true, + "fishing": false + }, + { + "code": "50300", + "major": "기타해양생물", + "mid": "해면기타", + "name": "갯지렁이", + "nameEn": "Polychaete / Sandworm", + "scientific": "Polychaeta spp.", + "area": "전 연안 갯벌", + "active": true, + "fishing": false + }, + { + "code": "50301", + "major": "기타해양생물", + "mid": "갯지렁이", + "name": "갯지렁이", + "nameEn": "Sandworm", + "scientific": "Perinereis aibuhitensis", + "area": "서해·남해 갯벌", + "active": true, + "fishing": false + }, + { + "code": "50400", + "major": "기타해양생물", + "mid": "해면기타", + "name": "고래", + "nameEn": "Whale", + "scientific": "Cetacea spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "50401", + "major": "기타해양생물", + "mid": "고래", + "name": "참고래", + "nameEn": "North Pacific Right Whale", + "scientific": "Eubalaena japonica", + "area": "동해·남해 외해", + "active": true, + "fishing": false + }, + { + "code": "50402", + "major": "기타해양생물", + "mid": "고래", + "name": "돌고래", + "nameEn": "Common Dolphin", + "scientific": "Delphinus delphis", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "50403", + "major": "기타해양생물", + "mid": "고래", + "name": "밍크고래", + "nameEn": "Common Minke Whale", + "scientific": "Balaenoptera acutorostrata", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "50404", + "major": "기타해양생물", + "mid": "고래", + "name": "브라이드고래", + "nameEn": "Bryde's Whale", + "scientific": "Balaenoptera edeni", + "area": "남해·제주 외해", + "active": true, + "fishing": false + }, + { + "code": "50405", + "major": "기타해양생물", + "mid": "고래", + "name": "수염고래", + "nameEn": "Baleen Whale", + "scientific": "Mysticeti spp.", + "area": "동해·남해 외해", + "active": true, + "fishing": false + }, + { + "code": "50499", + "major": "기타해양생물", + "mid": "고래", + "name": "기타 고래", + "nameEn": "Other Cetaceans", + "scientific": "Cetacea spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "50500", + "major": "기타해양생물", + "mid": "해면기타", + "name": "미더덕", + "nameEn": "Sea Squirt", + "scientific": "Styela clava", + "area": "남해 연안", + "active": true, + "fishing": false + }, + { + "code": "50501", + "major": "기타해양생물", + "mid": "미더덕", + "name": "미더덕", + "nameEn": "Warty Sea Squirt", + "scientific": "Styela clava", + "area": "남해 연안 (양식)", + "active": true, + "fishing": false + }, + { + "code": "50600", + "major": "기타해양생물", + "mid": "해면기타", + "name": "성게", + "nameEn": "Sea Urchin", + "scientific": "Echinoidea spp.", + "area": "전 연안 암초", + "active": true, + "fishing": true + }, + { + "code": "50601", + "major": "기타해양생물", + "mid": "성게", + "name": "말똥성게", + "nameEn": "Purple Sea Urchin", + "scientific": "Hemicentrotus pulcherrimus", + "area": "동해·남해 암초", + "active": true, + "fishing": false + }, + { + "code": "50602", + "major": "기타해양생물", + "mid": "성게", + "name": "보라성게", + "nameEn": "Purple Sea Urchin", + "scientific": "Anthocidaris crassispina", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "50603", + "major": "기타해양생물", + "mid": "성게", + "name": "북쪽말똥성게", + "nameEn": "Northern Purple Urchin", + "scientific": "Mesocentrotus nudus", + "area": "동해 북부", + "active": true, + "fishing": false + }, + { + "code": "50604", + "major": "기타해양생물", + "mid": "성게", + "name": "분홍성게", + "nameEn": "Pink Urchin", + "scientific": "Pseudocentrotus depressus", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "50605", + "major": "기타해양생물", + "mid": "성게", + "name": "성게알", + "nameEn": "Sea Urchin Roe", + "scientific": "Echinoidea (roe)", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "50699", + "major": "기타해양생물", + "mid": "성게", + "name": "기타 성게", + "nameEn": "Other Sea Urchins", + "scientific": "Echinoidea spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "50700", + "major": "기타해양생물", + "mid": "해면기타", + "name": "우렁쉥이", + "nameEn": "Sea Squirt / Ascidian", + "scientific": "Ascidiacea spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "50701", + "major": "기타해양생물", + "mid": "우렁쉥이", + "name": "우렁쉥이", + "nameEn": "Sea Pineapple", + "scientific": "Halocynthia roretzi", + "area": "동해·남해 (양식)", + "active": true, + "fishing": false + }, + { + "code": "50800", + "major": "기타해양생물", + "mid": "해면기타", + "name": "해삼", + "nameEn": "Sea Cucumber", + "scientific": "Holothuroidea spp.", + "area": "전 연안", + "active": true, + "fishing": true + }, + { + "code": "50801", + "major": "기타해양생물", + "mid": "해삼", + "name": "청해삼", + "nameEn": "Green Sea Cucumber", + "scientific": "Apostichopus japonicus", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "50802", + "major": "기타해양생물", + "mid": "해삼", + "name": "흑해삼", + "nameEn": "Black Sea Cucumber", + "scientific": "Holothuria leucospilota", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "50803", + "major": "기타해양생물", + "mid": "해삼", + "name": "홍해삼", + "nameEn": "Red Sea Cucumber", + "scientific": "Apostichopus japonicus (red)", + "area": "남해·제주", + "active": true, + "fishing": false + }, + { + "code": "50899", + "major": "기타해양생물", + "mid": "해삼", + "name": "기타 해삼", + "nameEn": "Other Sea Cucumbers", + "scientific": "Holothuroidea spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "50900", + "major": "기타해양생물", + "mid": "해면기타", + "name": "해파리", + "nameEn": "Jellyfish", + "scientific": "Medusozoa spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "50901", + "major": "기타해양생물", + "mid": "해파리", + "name": "해파리", + "nameEn": "Nomura's Jellyfish", + "scientific": "Nemopilema nomurai", + "area": "서해·남해 (대발생)", + "active": true, + "fishing": false + }, + { + "code": "59900", + "major": "기타해양생물", + "mid": "해면기타", + "name": "기타 해면동물", + "nameEn": "Other Marine Animals", + "scientific": "Animalia spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "59999", + "major": "기타해양생물", + "mid": "해면기타", + "name": "기타 해면동물", + "nameEn": "Other Marine Animals (Misc.)", + "scientific": "Animalia spp.", + "area": "전 해역", + "active": true, + "fishing": false + }, + { + "code": "60000", + "major": "해조류", + "mid": "대분류", + "name": "해조류", + "nameEn": "Marine Algae / Seaweed", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "60100", + "major": "해조류", + "mid": "해조류", + "name": "김", + "nameEn": "Laver / Nori", + "scientific": "Pyropia spp.", + "area": "남해·서해 (양식)", + "active": true, + "fishing": true + }, + { + "code": "60101", + "major": "해조류", + "mid": "김", + "name": "참김", + "nameEn": "Korean Laver", + "scientific": "Pyropia tenera", + "area": "남해·서해", + "active": true, + "fishing": false + }, + { + "code": "60102", + "major": "해조류", + "mid": "김", + "name": "돌김", + "nameEn": "Wild Laver", + "scientific": "Pyropia yezoensis", + "area": "동해·남해 암초", + "active": true, + "fishing": false + }, + { + "code": "60103", + "major": "해조류", + "mid": "김", + "name": "석태", + "nameEn": "Rock Laver", + "scientific": "Porphyra spp.", + "area": "동해·남해 암초", + "active": true, + "fishing": false + }, + { + "code": "60104", + "major": "해조류", + "mid": "김", + "name": "청태", + "nameEn": "Green Laver", + "scientific": "Monostroma spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60199", + "major": "해조류", + "mid": "김", + "name": "기타 김", + "nameEn": "Other Lavers", + "scientific": "Pyropia spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60200", + "major": "해조류", + "mid": "해조류", + "name": "꼬시래기", + "nameEn": "Gracilaria", + "scientific": "Gracilaria spp.", + "area": "남해·제주 연안", + "active": true, + "fishing": true + }, + { + "code": "60300", + "major": "해조류", + "mid": "해조류", + "name": "다시마", + "nameEn": "Kelp", + "scientific": "Saccharina spp.", + "area": "동해·남해 (양식)", + "active": true, + "fishing": true + }, + { + "code": "60301", + "major": "해조류", + "mid": "다시마", + "name": "개다시마", + "nameEn": "Wild Kelp", + "scientific": "Saccharina japonica", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "60302", + "major": "해조류", + "mid": "다시마", + "name": "참다시마", + "nameEn": "Japanese Kelp", + "scientific": "Saccharina japonica", + "area": "동해 연안 (양식)", + "active": true, + "fishing": false + }, + { + "code": "60399", + "major": "해조류", + "mid": "다시마", + "name": "기타 다시마", + "nameEn": "Other Kelps", + "scientific": "Saccharina spp.", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "60400", + "major": "해조류", + "mid": "해조류", + "name": "도박", + "nameEn": "Porphyra", + "scientific": "Chondrus spp.", + "area": "전 연안 암초", + "active": true, + "fishing": false + }, + { + "code": "60401", + "major": "해조류", + "mid": "도박", + "name": "개도박", + "nameEn": "Irish Moss", + "scientific": "Chondrus crispus", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60402", + "major": "해조류", + "mid": "도박", + "name": "참도박", + "nameEn": "Carrageenan Seaweed", + "scientific": "Chondrus spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60499", + "major": "해조류", + "mid": "도박", + "name": "기타 도박", + "nameEn": "Other Porphyra Algae", + "scientific": "Chondrus spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60500", + "major": "해조류", + "mid": "해조류", + "name": "말", + "nameEn": "Coralline Algae", + "scientific": "Corallina spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60501", + "major": "해조류", + "mid": "말", + "name": "말", + "nameEn": "Coralline Algae", + "scientific": "Corallina officinalis", + "area": "전 연안 암초", + "active": true, + "fishing": false + }, + { + "code": "60600", + "major": "해조류", + "mid": "해조류", + "name": "모자반", + "nameEn": "Sargassum", + "scientific": "Sargassum spp.", + "area": "전 연안 암초", + "active": true, + "fishing": false + }, + { + "code": "60601", + "major": "해조류", + "mid": "모자반", + "name": "모자반", + "nameEn": "Hijiki Seaweed", + "scientific": "Sargassum fusiforme", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "60700", + "major": "해조류", + "mid": "해조류", + "name": "미역", + "nameEn": "Sea Mustard / Wakame", + "scientific": "Undaria pinnatifida", + "area": "동해·남해 (양식)", + "active": true, + "fishing": true + }, + { + "code": "60701", + "major": "해조류", + "mid": "미역", + "name": "돌미역", + "nameEn": "Wild Wakame", + "scientific": "Undaria pinnatifida", + "area": "동해·남해 암초", + "active": true, + "fishing": false + }, + { + "code": "60702", + "major": "해조류", + "mid": "미역", + "name": "물미역", + "nameEn": "Water Wakame", + "scientific": "Undaria pinnatifida", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "60703", + "major": "해조류", + "mid": "미역", + "name": "줄기미역", + "nameEn": "Stem Wakame", + "scientific": "Undaria pinnatifida", + "area": "동해·남해 (양식)", + "active": true, + "fishing": false + }, + { + "code": "60799", + "major": "해조류", + "mid": "미역", + "name": "기타 미역", + "nameEn": "Other Wakame", + "scientific": "Undaria spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "60800", + "major": "해조류", + "mid": "해조류", + "name": "비단풀", + "nameEn": "Red Algae", + "scientific": "Ceramiaceae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60801", + "major": "해조류", + "mid": "비단풀", + "name": "단박", + "nameEn": "Agar Weed", + "scientific": "Ceramium spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60802", + "major": "해조류", + "mid": "비단풀", + "name": "석묵", + "nameEn": "Dulse", + "scientific": "Palmaria palmata", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "60899", + "major": "해조류", + "mid": "비단풀", + "name": "기타 비단풀", + "nameEn": "Other Red Algae", + "scientific": "Ceramiaceae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60900", + "major": "해조류", + "mid": "해조류", + "name": "우뭇가사리", + "nameEn": "Agar Seaweed", + "scientific": "Gelidium spp.", + "area": "동해·남해 암초", + "active": true, + "fishing": false + }, + { + "code": "60901", + "major": "해조류", + "mid": "우뭇가사리", + "name": "참우뭇가사리", + "nameEn": "Gelidium Agar", + "scientific": "Gelidium amansii", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "60902", + "major": "해조류", + "mid": "우뭇가사리", + "name": "진두발", + "nameEn": "Chondrus", + "scientific": "Chondrus crispus", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "60903", + "major": "해조류", + "mid": "우뭇가사리", + "name": "청각", + "nameEn": "Green Codium", + "scientific": "Codium fragile", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "60904", + "major": "해조류", + "mid": "우뭇가사리", + "name": "톳", + "nameEn": "Hijiki", + "scientific": "Sargassum fusiforme", + "area": "남해·제주 암초", + "active": true, + "fishing": false + }, + { + "code": "60999", + "major": "해조류", + "mid": "우뭇가사리", + "name": "기타 우뭇가사리", + "nameEn": "Other Gelidium", + "scientific": "Gelidium spp.", + "area": "동해·남해", + "active": true, + "fishing": false + }, + { + "code": "61000", + "major": "해조류", + "mid": "해조류", + "name": "파래", + "nameEn": "Sea Lettuce / Green Algae", + "scientific": "Ulvaceae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "61001", + "major": "해조류", + "mid": "파래", + "name": "갈파래", + "nameEn": "Bladder Wrack", + "scientific": "Ulva compressa", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "61002", + "major": "해조류", + "mid": "파래", + "name": "매생이", + "nameEn": "Capsosiphon Seaweed", + "scientific": "Capsosiphon fulvescens", + "area": "남해·서해 겨울", + "active": true, + "fishing": false + }, + { + "code": "61003", + "major": "해조류", + "mid": "파래", + "name": "파래", + "nameEn": "Sea Lettuce", + "scientific": "Ulva lactuca", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "61004", + "major": "해조류", + "mid": "파래", + "name": "홑파래", + "nameEn": "Monostroma", + "scientific": "Monostroma nitidum", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "61099", + "major": "해조류", + "mid": "파래", + "name": "기타 파래", + "nameEn": "Other Green Algae", + "scientific": "Ulvaceae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "61100", + "major": "해조류", + "mid": "해조류", + "name": "풀가사리", + "nameEn": "Furcellaria", + "scientific": "Furcellaria spp.", + "area": "동해 연안", + "active": true, + "fishing": false + }, + { + "code": "61101", + "major": "해조류", + "mid": "풀가사리", + "name": "불등가사리", + "nameEn": "Northern Sea Palm", + "scientific": "Postelsia palmaeformis", + "area": "동해 암초", + "active": true, + "fishing": false + }, + { + "code": "61199", + "major": "해조류", + "mid": "풀가사리", + "name": "기타 풀가사리", + "nameEn": "Other Furcellaria", + "scientific": "Furcellaria spp.", + "area": "동해", + "active": true, + "fishing": false + }, + { + "code": "69900", + "major": "해조류", + "mid": "해조류", + "name": "기타 해면해조류", + "nameEn": "Other Marine Algae", + "scientific": "Algae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "69999", + "major": "해조류", + "mid": "해조류", + "name": "기타 해면해조류", + "nameEn": "Other Marine Algae (Misc.)", + "scientific": "Algae spp.", + "area": "전 연안", + "active": true, + "fishing": false + }, + { + "code": "70000", + "major": "내수면어류", + "mid": "대분류", + "name": "내수면어류", + "nameEn": "Freshwater Fish", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "70100", + "major": "내수면어류", + "mid": "내수면어류", + "name": "가물치", + "nameEn": "Snakehead", + "scientific": "Channa argus", + "area": "전국 하천·저수지", + "active": true, + "fishing": false + }, + { + "code": "70101", + "major": "내수면어류", + "mid": "가물치", + "name": "가물치", + "nameEn": "Snakehead", + "scientific": "Channa argus", + "area": "전국 하천·저수지", + "active": true, + "fishing": false + }, + { + "code": "70200", + "major": "내수면어류", + "mid": "내수면어류", + "name": "누치", + "nameEn": "Korean Barbel", + "scientific": "Hemibarbus labeo", + "area": "한강·낙동강", + "active": true, + "fishing": false + }, + { + "code": "70201", + "major": "내수면어류", + "mid": "누치", + "name": "누치", + "nameEn": "Korean Barbel", + "scientific": "Hemibarbus labeo", + "area": "한강·낙동강·금강", + "active": true, + "fishing": false + }, + { + "code": "70300", + "major": "내수면어류", + "mid": "내수면어류", + "name": "돔", + "nameEn": "Freshwater Bream", + "scientific": "—", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "70301", + "major": "내수면어류", + "mid": "돔", + "name": "틸라피아", + "nameEn": "Tilapia", + "scientific": "Oreochromis niloticus", + "area": "전국 양식장", + "active": true, + "fishing": false + }, + { + "code": "70399", + "major": "내수면어류", + "mid": "돔", + "name": "기타 돔", + "nameEn": "Other Freshwater Bream", + "scientific": "—", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "70400", + "major": "내수면어류", + "mid": "내수면어류", + "name": "메기", + "nameEn": "Catfish", + "scientific": "Silurus asotus", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "70401", + "major": "내수면어류", + "mid": "메기", + "name": "찬넬메기", + "nameEn": "Channel Catfish", + "scientific": "Ictalurus punctatus", + "area": "전국 양식장", + "active": true, + "fishing": false + }, + { + "code": "70499", + "major": "내수면어류", + "mid": "메기", + "name": "기타 메기", + "nameEn": "Other Catfishes", + "scientific": "Siluridae spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "70500", + "major": "내수면어류", + "mid": "내수면어류", + "name": "미꾸라지", + "nameEn": "Loach", + "scientific": "Misgurnus spp.", + "area": "전국 논·하천", + "active": true, + "fishing": false + }, + { + "code": "70501", + "major": "내수면어류", + "mid": "미꾸라지", + "name": "기름종개", + "nameEn": "Striped Spined Loach", + "scientific": "Cobitis striata", + "area": "한강·낙동강", + "active": true, + "fishing": false + }, + { + "code": "70502", + "major": "내수면어류", + "mid": "미꾸라지", + "name": "미꾸리", + "nameEn": "Weatherfish", + "scientific": "Misgurnus anguillicaudatus", + "area": "전국 논·하천", + "active": true, + "fishing": false + }, + { + "code": "70599", + "major": "내수면어류", + "mid": "미꾸라지", + "name": "기타 미꾸라지", + "nameEn": "Other Loaches", + "scientific": "Cobitidae spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "70600", + "major": "내수면어류", + "mid": "내수면어류", + "name": "빠가사리", + "nameEn": "Korean Bullhead", + "scientific": "Pseudobagrus fulvidraco", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "70601", + "major": "내수면어류", + "mid": "빠가사리", + "name": "빠가사리", + "nameEn": "Korean Bullhead Catfish", + "scientific": "Pseudobagrus fulvidraco", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "70700", + "major": "내수면어류", + "mid": "내수면어류", + "name": "백련어", + "nameEn": "Silver Carp", + "scientific": "Hypophthalmichthys molitrix", + "area": "전국 양식장·하천", + "active": true, + "fishing": false + }, + { + "code": "70701", + "major": "내수면어류", + "mid": "백련어", + "name": "백련어", + "nameEn": "Silver Carp", + "scientific": "Hypophthalmichthys molitrix", + "area": "전국 양식장", + "active": true, + "fishing": false + }, + { + "code": "70800", + "major": "내수면어류", + "mid": "내수면어류", + "name": "뱀장어", + "nameEn": "Japanese Eel", + "scientific": "Anguilla japonica", + "area": "전국 하천·연안", + "active": true, + "fishing": false + }, + { + "code": "70801", + "major": "내수면어류", + "mid": "뱀장어", + "name": "실뱀장어", + "nameEn": "Glass Eel", + "scientific": "Anguilla japonica (juvenile)", + "area": "전국 하천 하류", + "active": true, + "fishing": false + }, + { + "code": "70899", + "major": "내수면어류", + "mid": "뱀장어", + "name": "기타 뱀장어", + "nameEn": "Other Eels", + "scientific": "Anguilla spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "70900", + "major": "내수면어류", + "mid": "내수면어류", + "name": "베스", + "nameEn": "Largemouth Bass", + "scientific": "Micropterus salmoides", + "area": "전국 호수·저수지", + "active": true, + "fishing": false + }, + { + "code": "70901", + "major": "내수면어류", + "mid": "베스", + "name": "베스", + "nameEn": "Largemouth Bass", + "scientific": "Micropterus salmoides", + "area": "전국 (외래종)", + "active": true, + "fishing": false + }, + { + "code": "71000", + "major": "내수면어류", + "mid": "내수면어류", + "name": "부루길", + "nameEn": "Bluegill", + "scientific": "Lepomis macrochirus", + "area": "전국 호수·저수지", + "active": true, + "fishing": false + }, + { + "code": "71001", + "major": "내수면어류", + "mid": "부루길", + "name": "부루길", + "nameEn": "Bluegill", + "scientific": "Lepomis macrochirus", + "area": "전국 (외래종)", + "active": true, + "fishing": false + }, + { + "code": "71100", + "major": "내수면어류", + "mid": "내수면어류", + "name": "붕어", + "nameEn": "Crucian Carp", + "scientific": "Carassius spp.", + "area": "전국 하천·저수지", + "active": true, + "fishing": false + }, + { + "code": "71101", + "major": "내수면어류", + "mid": "붕어", + "name": "금붕어", + "nameEn": "Goldfish", + "scientific": "Carassius auratus", + "area": "전국 양식·관상", + "active": true, + "fishing": false + }, + { + "code": "71102", + "major": "내수면어류", + "mid": "붕어", + "name": "떡붕어", + "nameEn": "Silver Crucian Carp", + "scientific": "Carassius cuvieri", + "area": "전국 저수지", + "active": true, + "fishing": false + }, + { + "code": "71103", + "major": "내수면어류", + "mid": "붕어", + "name": "참붕어", + "nameEn": "Korean Crucian Carp", + "scientific": "Carassius auratus", + "area": "전국 하천·저수지", + "active": true, + "fishing": false + }, + { + "code": "71199", + "major": "내수면어류", + "mid": "붕어", + "name": "기타 붕어", + "nameEn": "Other Crucian Carps", + "scientific": "Carassius spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "71200", + "major": "내수면어류", + "mid": "내수면어류", + "name": "빙어", + "nameEn": "Smelt", + "scientific": "Hypomesus nipponensis", + "area": "강원·경기 호수", + "active": true, + "fishing": false + }, + { + "code": "71201", + "major": "내수면어류", + "mid": "빙어", + "name": "은어", + "nameEn": "Sweetfish", + "scientific": "Plecoglossus altivelis", + "area": "전국 하천 (회유)", + "active": true, + "fishing": false + }, + { + "code": "71299", + "major": "내수면어류", + "mid": "빙어", + "name": "기타 빙어", + "nameEn": "Other Smelts", + "scientific": "Hypomesus spp.", + "area": "강원 호수", + "active": true, + "fishing": false + }, + { + "code": "71300", + "major": "내수면어류", + "mid": "내수면어류", + "name": "산천어", + "nameEn": "Masu Salmon (Landlocked)", + "scientific": "Oncorhynchus masou masou", + "area": "강원·경북 상류 계류", + "active": true, + "fishing": false + }, + { + "code": "71301", + "major": "내수면어류", + "mid": "산천어", + "name": "산천어", + "nameEn": "Yamame Trout", + "scientific": "Oncorhynchus masou masou", + "area": "강원 청정 계류", + "active": true, + "fishing": false + }, + { + "code": "71400", + "major": "내수면어류", + "mid": "내수면어류", + "name": "송어", + "nameEn": "Trout", + "scientific": "Oncorhynchus spp.", + "area": "전국 양식·청정 하천", + "active": true, + "fishing": false + }, + { + "code": "71401", + "major": "내수면어류", + "mid": "송어", + "name": "곱사송어", + "nameEn": "Pink Salmon", + "scientific": "Oncorhynchus gorbuscha", + "area": "동해 연안·하천", + "active": true, + "fishing": false + }, + { + "code": "71499", + "major": "내수면어류", + "mid": "송어", + "name": "기타 송어", + "nameEn": "Other Trouts", + "scientific": "Oncorhynchus spp.", + "area": "청정 하천", + "active": true, + "fishing": false + }, + { + "code": "71500", + "major": "내수면어류", + "mid": "내수면어류", + "name": "쏘가리", + "nameEn": "Mandarin Fish", + "scientific": "Siniperca scherzeri", + "area": "한강·낙동강", + "active": true, + "fishing": false + }, + { + "code": "71501", + "major": "내수면어류", + "mid": "쏘가리", + "name": "황쏘가리", + "nameEn": "Yellow Mandarin Fish", + "scientific": "Siniperca scherzeri (yellow)", + "area": "한강 (희귀)", + "active": true, + "fishing": false + }, + { + "code": "71502", + "major": "내수면어류", + "mid": "쏘가리", + "name": "꺽지", + "nameEn": "Korean Perch", + "scientific": "Coreoperca herzi", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "71599", + "major": "내수면어류", + "mid": "쏘가리", + "name": "기타 쏘가리", + "nameEn": "Other Mandarin Fishes", + "scientific": "Siniperca spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "71600", + "major": "내수면어류", + "mid": "내수면어류", + "name": "연어", + "nameEn": "Salmon (Freshwater)", + "scientific": "Oncorhynchus spp.", + "area": "동해 유입 하천", + "active": true, + "fishing": false + }, + { + "code": "71601", + "major": "내수면어류", + "mid": "연어", + "name": "곱사연어", + "nameEn": "Pink Salmon", + "scientific": "Oncorhynchus gorbuscha", + "area": "동해 하천", + "active": true, + "fishing": false + }, + { + "code": "71602", + "major": "내수면어류", + "mid": "연어", + "name": "시마연어", + "nameEn": "Masu Salmon", + "scientific": "Oncorhynchus masou", + "area": "동해 하천", + "active": true, + "fishing": false + }, + { + "code": "71603", + "major": "내수면어류", + "mid": "연어", + "name": "은연어", + "nameEn": "Coho Salmon", + "scientific": "Oncorhynchus kisutch", + "area": "동해 하천·양식", + "active": true, + "fishing": false + }, + { + "code": "71699", + "major": "내수면어류", + "mid": "연어", + "name": "기타 연어", + "nameEn": "Other Salmons", + "scientific": "Oncorhynchus spp.", + "area": "동해 하천", + "active": true, + "fishing": false + }, + { + "code": "71700", + "major": "내수면어류", + "mid": "내수면어류", + "name": "열목어", + "nameEn": "Lenok", + "scientific": "Brachymystax lenok", + "area": "강원 산간 계류", + "active": true, + "fishing": false + }, + { + "code": "71701", + "major": "내수면어류", + "mid": "열목어", + "name": "열목어", + "nameEn": "Lenok", + "scientific": "Brachymystax lenok tsinlingensis", + "area": "강원 산간 계류", + "active": true, + "fishing": false + }, + { + "code": "71800", + "major": "내수면어류", + "mid": "내수면어류", + "name": "웅어", + "nameEn": "Tapertail Anchovy", + "scientific": "Coilia nasus", + "area": "서해·하천 기수역", + "active": true, + "fishing": false + }, + { + "code": "71801", + "major": "내수면어류", + "mid": "웅어", + "name": "웅어", + "nameEn": "Grenadier Anchovy", + "scientific": "Coilia nasus", + "area": "한강·금강 하구", + "active": true, + "fishing": false + }, + { + "code": "71900", + "major": "내수면어류", + "mid": "내수면어류", + "name": "잉어", + "nameEn": "Common Carp", + "scientific": "Cyprinus carpio", + "area": "전국 하천·저수지", + "active": true, + "fishing": false + }, + { + "code": "71901", + "major": "내수면어류", + "mid": "잉어", + "name": "비단잉어", + "nameEn": "Koi Carp", + "scientific": "Cyprinus carpio (koi)", + "area": "전국 관상·양식", + "active": true, + "fishing": false + }, + { + "code": "71902", + "major": "내수면어류", + "mid": "잉어", + "name": "이스라엘잉어", + "nameEn": "Israeli Mirror Carp", + "scientific": "Cyprinus carpio (mirror)", + "area": "전국 양식", + "active": true, + "fishing": false + }, + { + "code": "71999", + "major": "내수면어류", + "mid": "잉어", + "name": "기타 잉어", + "nameEn": "Other Carps", + "scientific": "Cyprinidae spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "72000", + "major": "내수면어류", + "mid": "내수면어류", + "name": "초어", + "nameEn": "Grass Carp", + "scientific": "Ctenopharyngodon idella", + "area": "전국 양식·하천", + "active": true, + "fishing": false + }, + { + "code": "72001", + "major": "내수면어류", + "mid": "초어", + "name": "초어", + "nameEn": "Grass Carp", + "scientific": "Ctenopharyngodon idella", + "area": "전국 양식", + "active": true, + "fishing": false + }, + { + "code": "72100", + "major": "내수면어류", + "mid": "내수면어류", + "name": "피라미", + "nameEn": "Pale Chub", + "scientific": "Zacco platypus", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "72101", + "major": "내수면어류", + "mid": "피라미", + "name": "피라미", + "nameEn": "Zacco", + "scientific": "Zacco platypus", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "72200", + "major": "내수면어류", + "mid": "내수면어류", + "name": "황어", + "nameEn": "Korean Dace", + "scientific": "Tribolodon hakonensis", + "area": "동해 유입 하천", + "active": true, + "fishing": false + }, + { + "code": "72201", + "major": "내수면어류", + "mid": "황어", + "name": "황어", + "nameEn": "Korean Dace", + "scientific": "Tribolodon hakonensis", + "area": "동해 유입 하천", + "active": true, + "fishing": false + }, + { + "code": "79900", + "major": "내수면어류", + "mid": "내수면어류", + "name": "기타 내수면어류", + "nameEn": "Other Freshwater Fish", + "scientific": "Pisces spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "79999", + "major": "내수면어류", + "mid": "내수면어류", + "name": "기타 내수면어류", + "nameEn": "Other Freshwater Fish (Misc.)", + "scientific": "Pisces spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "80000", + "major": "내수면기타", + "mid": "대분류", + "name": "활내수면기타", + "nameEn": "Other Freshwater Organisms", + "scientific": "—", + "area": "—", + "active": true, + "fishing": false + }, + { + "code": "80100", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "가재", + "nameEn": "Freshwater Crayfish", + "scientific": "Cambaridae spp.", + "area": "전국 청정 하천", + "active": true, + "fishing": false + }, + { + "code": "80101", + "major": "내수면기타", + "mid": "가재", + "name": "민물가재", + "nameEn": "Korean Crayfish", + "scientific": "Cambaroides similis", + "area": "강원·경북 상류", + "active": true, + "fishing": false + }, + { + "code": "80199", + "major": "내수면기타", + "mid": "가재", + "name": "기타 가재", + "nameEn": "Other Crayfish", + "scientific": "Cambaridae spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "80200", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "게", + "nameEn": "Freshwater Crab", + "scientific": "Gecarcinucidae spp.", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "80201", + "major": "내수면기타", + "mid": "게", + "name": "게", + "nameEn": "Korean Freshwater Crab", + "scientific": "Eriocheir sinensis", + "area": "서해 유입 하천", + "active": true, + "fishing": false + }, + { + "code": "80300", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "새우", + "nameEn": "Freshwater Shrimp", + "scientific": "Atyidae spp.", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "80301", + "major": "내수면기타", + "mid": "새우", + "name": "민물새우", + "nameEn": "Freshwater Shrimp", + "scientific": "Caridina denticulata", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "80302", + "major": "내수면기타", + "mid": "새우", + "name": "징거미새우", + "nameEn": "Giant Freshwater Prawn", + "scientific": "Macrobrachium rosenbergii", + "area": "남해 유입 하천", + "active": true, + "fishing": false + }, + { + "code": "80399", + "major": "내수면기타", + "mid": "새우", + "name": "기타 새우", + "nameEn": "Other Freshwater Shrimps", + "scientific": "Atyidae spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "80400", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "우렁이", + "nameEn": "Freshwater Snail", + "scientific": "Viviparidae spp.", + "area": "전국 논·하천", + "active": true, + "fishing": false + }, + { + "code": "80401", + "major": "내수면기타", + "mid": "우렁이", + "name": "왕우렁이", + "nameEn": "Giant Ramshorn Snail", + "scientific": "Pomacea canaliculata", + "area": "전국 논 (외래종)", + "active": true, + "fishing": false + }, + { + "code": "80499", + "major": "내수면기타", + "mid": "우렁이", + "name": "기타 우렁이", + "nameEn": "Other Freshwater Snails", + "scientific": "Viviparidae spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "80500", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "조개", + "nameEn": "Freshwater Clam", + "scientific": "Unionidae spp.", + "area": "전국 하천·호수", + "active": true, + "fishing": false + }, + { + "code": "80501", + "major": "내수면기타", + "mid": "조개", + "name": "다슬기", + "nameEn": "Korean Fresh Water Snail", + "scientific": "Semisulcospira libertina", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "80502", + "major": "내수면기타", + "mid": "조개", + "name": "재첩", + "nameEn": "Asian Clam", + "scientific": "Corbicula fluminea", + "area": "낙동강·섬진강 하구", + "active": true, + "fishing": false + }, + { + "code": "80503", + "major": "내수면기타", + "mid": "조개", + "name": "대칭이조개", + "nameEn": "Freshwater Mussel", + "scientific": "Anodonta spp.", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "80599", + "major": "내수면기타", + "mid": "조개", + "name": "기타 조개", + "nameEn": "Other Freshwater Clams", + "scientific": "Unionidae spp.", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "80600", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "개구리", + "nameEn": "Frog", + "scientific": "Ranidae spp.", + "area": "전국", + "active": true, + "fishing": false + }, + { + "code": "80601", + "major": "내수면기타", + "mid": "개구리", + "name": "황소개구리", + "nameEn": "American Bullfrog", + "scientific": "Lithobates catesbeianus", + "area": "전국 (외래종)", + "active": true, + "fishing": false + }, + { + "code": "80602", + "major": "내수면기타", + "mid": "개구리", + "name": "기타식용개구리", + "nameEn": "Edible Frog", + "scientific": "Pelophylax spp.", + "area": "전국", + "active": true, + "fishing": false + }, + { + "code": "80699", + "major": "내수면기타", + "mid": "개구리", + "name": "기타 개구리", + "nameEn": "Other Frogs", + "scientific": "Ranidae spp.", + "area": "전국", + "active": true, + "fishing": false + }, + { + "code": "80700", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "자라", + "nameEn": "Softshell Turtle", + "scientific": "Pelodiscus sinensis", + "area": "전국 하천·양식", + "active": true, + "fishing": false + }, + { + "code": "80701", + "major": "내수면기타", + "mid": "자라", + "name": "자라", + "nameEn": "Chinese Softshell Turtle", + "scientific": "Pelodiscus sinensis", + "area": "전국 하천", + "active": true, + "fishing": false + }, + { + "code": "80800", + "major": "내수면기타", + "mid": "활내수면기타", + "name": "순채", + "nameEn": "Water Shield", + "scientific": "Brasenia schreberi", + "area": "강원·전남 호수", + "active": true, + "fishing": false + }, + { + "code": "80801", + "major": "내수면기타", + "mid": "순채", + "name": "순채", + "nameEn": "Water Shield", + "scientific": "Brasenia schreberi", + "area": "강원 청정 호수", + "active": true, + "fishing": false + }, + { + "code": "89900", + "major": "내수면기타", + "mid": "내수면기타", + "name": "기타 내수면기타", + "nameEn": "Other Freshwater Organisms", + "scientific": "—", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "89999", + "major": "내수면기타", + "mid": "내수면기타", + "name": "기타 내수면기타", + "nameEn": "Other Freshwater Organisms (Misc.)", + "scientific": "—", + "area": "내수면", + "active": true, + "fishing": false + }, + { + "code": "12715(안)", + "major": "어류", + "mid": "돔", + "name": "황돔", + "nameEn": "Yellowback Seabream", + "scientific": "Dentex tumifrons", + "area": "제주·남해 암초", + "active": true, + "fishing": false + } +] \ No newline at end of file diff --git a/src/data/vesselTypeCodes.json b/src/data/vesselTypeCodes.json new file mode 100644 index 0000000..55a6cb6 --- /dev/null +++ b/src/data/vesselTypeCodes.json @@ -0,0 +1,2420 @@ +[ + { + "code": "VT-100", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "연근해어선", + "name": "어선(일반·연근해)", + "source": "RRA", + "tonnage": "10톤 미만~100톤 미만", + "purpose": "연안복합·자망·통발어업", + "aisCode": "AIS:30", + "note": "수산업법 제47조 연안어업" + }, + { + "code": "VT-101", + "srcCode": "000020", + "srcDetail": "B001", + "major": "어선", + "mid": "연근해어선", + "name": "일반어선", + "source": "GIC", + "tonnage": "10톤 이상", + "purpose": "다목적 연근해어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-102", + "srcCode": "000020", + "srcDetail": "B002", + "major": "어선", + "mid": "연근해어선", + "name": "유자망어선", + "source": "GIC", + "tonnage": "10~50톤 미만", + "purpose": "유자망어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-103", + "srcCode": "000020", + "srcDetail": "B003", + "major": "어선", + "mid": "연근해어선", + "name": "선망어선", + "source": "GIC", + "tonnage": "30~100톤 미만", + "purpose": "선망어업 (고등어·멸치)", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-104", + "srcCode": "000020", + "srcDetail": "B004", + "major": "어선", + "mid": "연근해어선", + "name": "안강망어선", + "source": "GIC", + "tonnage": "10~50톤 미만", + "purpose": "안강망어업 (서해)", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-105", + "srcCode": "000020", + "srcDetail": "B005", + "major": "어선", + "mid": "연근해어선", + "name": "채낚기어선", + "source": "GIC", + "tonnage": "10~200톤 미만", + "purpose": "채낚기어업 (오징어)", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-106", + "srcCode": "000020", + "srcDetail": "B006", + "major": "어선", + "mid": "연근해어선", + "name": "연승어선", + "source": "GIC", + "tonnage": "10~100톤 미만", + "purpose": "연승어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-107", + "srcCode": "000020", + "srcDetail": "B007", + "major": "어선", + "mid": "연근해어선", + "name": "트롤어선", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "트롤어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-108", + "srcCode": "000020", + "srcDetail": "B008", + "major": "어선", + "mid": "연근해어선", + "name": "통발어선", + "source": "GIC", + "tonnage": "10~100톤 미만", + "purpose": "통발어업 (꽃게·낙지)", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-109", + "srcCode": "000020", + "srcDetail": "B009", + "major": "어선", + "mid": "연근해어선", + "name": "복합어업어선", + "source": "GIC", + "tonnage": "10~100톤 미만", + "purpose": "2종 이상 어업겸영", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-110", + "srcCode": "000020", + "srcDetail": "B010", + "major": "어선", + "mid": "연근해어선", + "name": "저인망어선", + "source": "GIC", + "tonnage": "30~100톤 미만", + "purpose": "저인망어업 (새우·잡어)", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-111", + "srcCode": "000020", + "srcDetail": "B011", + "major": "어선", + "mid": "연근해어선", + "name": "자망어선", + "source": "GIC", + "tonnage": "10~50톤 미만", + "purpose": "자망어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-112", + "srcCode": "000020", + "srcDetail": "B012", + "major": "어선", + "mid": "연근해어선", + "name": "권현망어선", + "source": "GIC", + "tonnage": "50~200톤 미만", + "purpose": "권현망어업 (멸치)", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-113", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "연근해어선", + "name": "낚시어선", + "source": "RRA", + "tonnage": "5톤 미만~20톤 미만", + "purpose": "낚시체험·유어어업", + "aisCode": "AIS:30", + "note": "수상레저안전법 적용" + }, + { + "code": "VT-114", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "연근해어선", + "name": "어장관리선", + "source": "RRA", + "tonnage": "5톤 미만", + "purpose": "양식어장 관리", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-115", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "연근해어선", + "name": "어획물운반선", + "source": "RRA", + "tonnage": "50~500톤 미만", + "purpose": "어획물 운반", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-116", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "연근해어선", + "name": "수산물가공선", + "source": "RRA", + "tonnage": "200톤 이상", + "purpose": "수산물 가공·냉동", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-117", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "지원·조사선", + "name": "어업실습선", + "source": "RRA", + "tonnage": "100톤 이상", + "purpose": "어업 교육·실습", + "aisCode": "AIS:30", + "note": "수산계 학교 운영" + }, + { + "code": "VT-118", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "지원·조사선", + "name": "어업조사선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "어업자원 조사", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-119", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "지원·조사선", + "name": "어업지도선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "어업 지도·단속", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-120", + "srcCode": "000020", + "srcDetail": "", + "major": "어선", + "mid": "지원·조사선", + "name": "어선부속선", + "source": "RRA", + "tonnage": "5톤 미만", + "purpose": "모선 부속", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-121", + "srcCode": "000020", + "srcDetail": "B013", + "major": "어선", + "mid": "원양어선", + "name": "원양어선(일반)", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "원양어업 (총괄)", + "aisCode": "AIS:30", + "note": "원양산업발전법" + }, + { + "code": "VT-122", + "srcCode": "000020", + "srcDetail": "B014", + "major": "어선", + "mid": "원양어선", + "name": "원양참치연승선", + "source": "GIC", + "tonnage": "200~500톤", + "purpose": "원양참치연승어업", + "aisCode": "AIS:30", + "note": "WCPFC 규제" + }, + { + "code": "VT-123", + "srcCode": "000020", + "srcDetail": "B015", + "major": "어선", + "mid": "원양어선", + "name": "원양선망선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "원양다랑어선망어업", + "aisCode": "AIS:30", + "note": "WCPFC 규제" + }, + { + "code": "VT-124", + "srcCode": "000020", + "srcDetail": "B016", + "major": "어선", + "mid": "원양어선", + "name": "원양트롤선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "원양트롤어업 (명태·대구)", + "aisCode": "AIS:30", + "note": "냉동·가공설비" + }, + { + "code": "VT-125", + "srcCode": "000020", + "srcDetail": "B017", + "major": "어선", + "mid": "원양어선", + "name": "원양저인망선", + "source": "GIC", + "tonnage": "300~500톤", + "purpose": "원양저인망어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-126", + "srcCode": "000020", + "srcDetail": "B019", + "major": "어선", + "mid": "원양어선", + "name": "원양통발선", + "source": "GIC", + "tonnage": "200~500톤", + "purpose": "원양통발어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-127", + "srcCode": "000020", + "srcDetail": "B020", + "major": "어선", + "mid": "원양어선", + "name": "원양저연승선", + "source": "GIC", + "tonnage": "200~500톤", + "purpose": "원양저연승어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-128", + "srcCode": "000020", + "srcDetail": "B021", + "major": "어선", + "mid": "원양어선", + "name": "원양봉수망선", + "source": "GIC", + "tonnage": "300~500톤", + "purpose": "원양봉수망어업", + "aisCode": "AIS:30", + "note": "" + }, + { + "code": "VT-129", + "srcCode": "000020", + "srcDetail": "B022", + "major": "어선", + "mid": "원양어선", + "name": "원양모선식어선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "모선식 원양어업", + "aisCode": "AIS:30", + "note": "모선+독립조업" + }, + { + "code": "VT-130", + "srcCode": "000020", + "srcDetail": "B023", + "major": "어선", + "mid": "원양어선", + "name": "원양어업운반선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "원양어획물 운반", + "aisCode": "AIS:30", + "note": "냉동설비 탑재" + }, + { + "code": "VT-131", + "srcCode": "000020", + "srcDetail": "30", + "major": "어선", + "mid": "AIS국제코드", + "name": "Fishing(AIS)", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "Fishing vessel", + "aisCode": "AIS:30", + "note": "ITU/IMO AIS Type 30" + }, + { + "code": "VT-200", + "srcCode": "000022", + "srcDetail": "", + "major": "여객선", + "mid": "일반여객선", + "name": "여객선(일반)", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "여객 운송", + "aisCode": "AIS:60~69", + "note": "해운법 제2조" + }, + { + "code": "VT-201", + "srcCode": "000022", + "srcDetail": "A001", + "major": "여객선", + "mid": "일반여객선", + "name": "일반여객선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "일반 여객 운송", + "aisCode": "AIS:60", + "note": "" + }, + { + "code": "VT-202", + "srcCode": "000022", + "srcDetail": "A002", + "major": "여객선", + "mid": "고속여객선", + "name": "고속선", + "source": "GIC", + "tonnage": "100톤 미만~", + "purpose": "고속 여객 운송", + "aisCode": "AIS:40", + "note": "" + }, + { + "code": "VT-203", + "srcCode": "000022", + "srcDetail": "A003", + "major": "여객선", + "mid": "고속여객선", + "name": "쾌속선", + "source": "GIC", + "tonnage": "50톤 미만~", + "purpose": "고속 근해 여객", + "aisCode": "AIS:40", + "note": "" + }, + { + "code": "VT-204", + "srcCode": "000022", + "srcDetail": "A004", + "major": "여객선", + "mid": "고속여객선", + "name": "초쾌속선", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "초고속 여객 운송", + "aisCode": "AIS:40", + "note": "35노트 이상" + }, + { + "code": "VT-205", + "srcCode": "000022", + "srcDetail": "A005", + "major": "여객선", + "mid": "카페리", + "name": "카훼리(카페리)", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "여객+차량 운송", + "aisCode": "AIS:61", + "note": "" + }, + { + "code": "VT-206", + "srcCode": "000022", + "srcDetail": "A006", + "major": "여객선", + "mid": "카페리", + "name": "차도선", + "source": "GIC", + "tonnage": "100~500톤", + "purpose": "차량+여객 도선", + "aisCode": "AIS:61", + "note": "도서 항로" + }, + { + "code": "VT-207", + "srcCode": "000022", + "srcDetail": "A007", + "major": "여객선", + "mid": "화객선", + "name": "화객선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "여객+화물 겸용", + "aisCode": "AIS:62", + "note": "" + }, + { + "code": "VT-208", + "srcCode": "000022", + "srcDetail": "A008", + "major": "여객선", + "mid": "유람선", + "name": "유람선", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "관광·유람 목적", + "aisCode": "AIS:69", + "note": "" + }, + { + "code": "VT-209", + "srcCode": "000022", + "srcDetail": "", + "major": "여객선", + "mid": "도선", + "name": "도선", + "source": "RRA", + "tonnage": "10톤 미만", + "purpose": "단거리 도선", + "aisCode": "AIS:60", + "note": "유·도선법" + }, + { + "code": "VT-210", + "srcCode": "000022", + "srcDetail": "", + "major": "여객선", + "mid": "도선", + "name": "유선", + "source": "RRA", + "tonnage": "5톤 미만", + "purpose": "레저·유흥 도선", + "aisCode": "AIS:60", + "note": "유·도선법" + }, + { + "code": "VT-211", + "srcCode": "000022", + "srcDetail": "", + "major": "여객선", + "mid": "특수여객선", + "name": "수면비행여객선", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "수면비행 여객", + "aisCode": "AIS:49", + "note": "WIG선" + }, + { + "code": "VT-212", + "srcCode": "000022", + "srcDetail": "11", + "major": "여객선", + "mid": "일반여객선", + "name": "여객선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "여객 운송", + "aisCode": "AIS:60", + "note": "" + }, + { + "code": "VT-213", + "srcCode": "000022", + "srcDetail": "12", + "major": "여객선", + "mid": "화객선", + "name": "화객선(PMS)", + "source": "PMS", + "tonnage": "500톤 이상", + "purpose": "여객+화물", + "aisCode": "AIS:62", + "note": "" + }, + { + "code": "VT-300", + "srcCode": "000023", + "srcDetail": "", + "major": "화물선", + "mid": "일반화물선", + "name": "화물선(일반)", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "일반 화물 운송", + "aisCode": "AIS:70~79", + "note": "해운법 제2조" + }, + { + "code": "VT-301", + "srcCode": "000023", + "srcDetail": "C001", + "major": "화물선", + "mid": "일반화물선", + "name": "일반화물선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "일반 화물 운송", + "aisCode": "AIS:70", + "note": "" + }, + { + "code": "VT-302", + "srcCode": "000023", + "srcDetail": "C002", + "major": "화물선", + "mid": "벌크선", + "name": "벌크선", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "광물·곡물 산적 운송", + "aisCode": "AIS:70", + "note": "" + }, + { + "code": "VT-303", + "srcCode": "000023", + "srcDetail": "C003", + "major": "화물선", + "mid": "벌크선", + "name": "양곡운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "곡물 운반", + "aisCode": "AIS:70", + "note": "" + }, + { + "code": "VT-304", + "srcCode": "000023", + "srcDetail": "C004", + "major": "화물선", + "mid": "특수화물선", + "name": "원목운반선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "원목 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-305", + "srcCode": "000023", + "srcDetail": "C005", + "major": "화물선", + "mid": "특수화물선", + "name": "광목운반선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "광물 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-306", + "srcCode": "000023", + "srcDetail": "C006", + "major": "화물선", + "mid": "벌크선", + "name": "석탄운반선", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "석탄 산적 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-307", + "srcCode": "000023", + "srcDetail": "C007", + "major": "화물선", + "mid": "특수화물선", + "name": "시멘트운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "시멘트 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-308", + "srcCode": "000023", + "srcDetail": "C008", + "major": "화물선", + "mid": "자동차운반선", + "name": "자동차운반선", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "완성차 운반(RORO)", + "aisCode": "AIS:75", + "note": "" + }, + { + "code": "VT-309", + "srcCode": "000023", + "srcDetail": "C009", + "major": "화물선", + "mid": "특수화물선", + "name": "핫코일운반선", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "열연강판 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-310", + "srcCode": "000023", + "srcDetail": "C010", + "major": "화물선", + "mid": "특수화물선", + "name": "철강재운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "철강재 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-311", + "srcCode": "000023", + "srcDetail": "C011", + "major": "화물선", + "mid": "특수화물선", + "name": "모래운반선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "골재 운반", + "aisCode": "AIS:70", + "note": "" + }, + { + "code": "VT-312", + "srcCode": "000023", + "srcDetail": "C012", + "major": "화물선", + "mid": "특수화물선", + "name": "폐기물운반선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "폐기물 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-313", + "srcCode": "000023", + "srcDetail": "C013", + "major": "화물선", + "mid": "RORO선", + "name": "코일운반선(RORO)", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "RORO+코일 운반", + "aisCode": "AIS:75", + "note": "" + }, + { + "code": "VT-314", + "srcCode": "000023", + "srcDetail": "C014", + "major": "화물선", + "mid": "냉동·냉장선", + "name": "냉동냉장선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "냉동·냉장 화물 운반", + "aisCode": "AIS:74", + "note": "" + }, + { + "code": "VT-315", + "srcCode": "000023", + "srcDetail": "C015", + "major": "화물선", + "mid": "컨테이너선", + "name": "컨테이너선", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "컨테이너 운송", + "aisCode": "AIS:71", + "note": "" + }, + { + "code": "VT-316", + "srcCode": "000023", + "srcDetail": "C024", + "major": "화물선", + "mid": "컨테이너선", + "name": "세미컨테이너선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "컨테이너+일반화물", + "aisCode": "AIS:71", + "note": "" + }, + { + "code": "VT-317", + "srcCode": "000023", + "srcDetail": "21", + "major": "화물선", + "mid": "벌크선", + "name": "산물선(PMS)", + "source": "PMS", + "tonnage": "5,000톤 이상", + "purpose": "산적화물", + "aisCode": "AIS:70", + "note": "" + }, + { + "code": "VT-318", + "srcCode": "000023", + "srcDetail": "24", + "major": "화물선", + "mid": "벌크선", + "name": "광석운반선(PMS)", + "source": "PMS", + "tonnage": "10,000톤 이상", + "purpose": "광석 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-319", + "srcCode": "000023", + "srcDetail": "25", + "major": "화물선", + "mid": "벌크선", + "name": "석탄운반선(PMS)", + "source": "PMS", + "tonnage": "5,000톤 이상", + "purpose": "석탄 운반", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-320", + "srcCode": "000023", + "srcDetail": "26", + "major": "화물선", + "mid": "특수화물선", + "name": "시멘트운반선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "시멘트", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-321", + "srcCode": "000023", + "srcDetail": "27", + "major": "화물선", + "mid": "자동차운반선", + "name": "자동차운반선(PMS)", + "source": "PMS", + "tonnage": "5,000톤 이상", + "purpose": "완성차", + "aisCode": "AIS:75", + "note": "" + }, + { + "code": "VT-322", + "srcCode": "000023", + "srcDetail": "28", + "major": "화물선", + "mid": "특수화물선", + "name": "핫코일운반선(PMS)", + "source": "PMS", + "tonnage": "5,000톤 이상", + "purpose": "열연강판", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-323", + "srcCode": "000023", + "srcDetail": "29", + "major": "화물선", + "mid": "특수화물선", + "name": "철강제운반선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "철강재", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-324", + "srcCode": "000023", + "srcDetail": "31", + "major": "화물선", + "mid": "특수화물선", + "name": "모래운반선(PMS)", + "source": "PMS", + "tonnage": "200톤 이상", + "purpose": "골재·모래", + "aisCode": "AIS:70", + "note": "" + }, + { + "code": "VT-325", + "srcCode": "000023", + "srcDetail": "33", + "major": "화물선", + "mid": "특수화물선", + "name": "폐기물운반선(PMS)", + "source": "PMS", + "tonnage": "500톤 이상", + "purpose": "폐기물", + "aisCode": "AIS:72", + "note": "" + }, + { + "code": "VT-326", + "srcCode": "000023", + "srcDetail": "39", + "major": "화물선", + "mid": "일반화물선", + "name": "일반화물선(PMS)", + "source": "PMS", + "tonnage": "500톤 이상", + "purpose": "일반화물", + "aisCode": "AIS:70", + "note": "" + }, + { + "code": "VT-327", + "srcCode": "000023", + "srcDetail": "41", + "major": "화물선", + "mid": "컨테이너선", + "name": "풀컨테이너선(PMS)", + "source": "PMS", + "tonnage": "10,000톤 이상", + "purpose": "컨테이너 전용", + "aisCode": "AIS:71", + "note": "" + }, + { + "code": "VT-328", + "srcCode": "000023", + "srcDetail": "42", + "major": "화물선", + "mid": "컨테이너선", + "name": "세미컨테이너선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "세미컨테이너", + "aisCode": "AIS:71", + "note": "" + }, + { + "code": "VT-400", + "srcCode": "000024", + "srcDetail": "", + "major": "유조선", + "mid": "원유운반선", + "name": "원유운반선(일반)", + "source": "RRA", + "tonnage": "5,000톤 이상", + "purpose": "원유 운반", + "aisCode": "AIS:80~89", + "note": "MARPOL 협약" + }, + { + "code": "VT-401", + "srcCode": "000024", + "srcDetail": "C016", + "major": "유조선", + "mid": "원유운반선", + "name": "원유운반선", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "원유 운반", + "aisCode": "AIS:80", + "note": "MARPOL Annex I" + }, + { + "code": "VT-402", + "srcCode": "000024", + "srcDetail": "C017", + "major": "유조선", + "mid": "석유제품운반선", + "name": "석유제품운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "석유제품 운반", + "aisCode": "AIS:80", + "note": "" + }, + { + "code": "VT-403", + "srcCode": "000024", + "srcDetail": "C018", + "major": "유조선", + "mid": "기타유조선", + "name": "기타 유조선", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "액체화물 운반", + "aisCode": "AIS:89", + "note": "" + }, + { + "code": "VT-404", + "srcCode": "000024", + "srcDetail": "C019", + "major": "유조선", + "mid": "케미칼운반선", + "name": "케미칼운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "화학물질 운반", + "aisCode": "AIS:82", + "note": "MARPOL Annex II" + }, + { + "code": "VT-405", + "srcCode": "000024", + "srcDetail": "C020", + "major": "유조선", + "mid": "케미칼운반선", + "name": "케미칼가스운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "케미칼+가스", + "aisCode": "AIS:83", + "note": "" + }, + { + "code": "VT-406", + "srcCode": "000024", + "srcDetail": "C021", + "major": "유조선", + "mid": "가스운반선", + "name": "LPG운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "액화석유가스 운반", + "aisCode": "AIS:84", + "note": "IGC Code" + }, + { + "code": "VT-407", + "srcCode": "000024", + "srcDetail": "C022", + "major": "유조선", + "mid": "가스운반선", + "name": "LNG운반선", + "source": "GIC", + "tonnage": "1,000톤 이상", + "purpose": "액화천연가스 운반", + "aisCode": "AIS:84", + "note": "IGC Code" + }, + { + "code": "VT-408", + "srcCode": "000024", + "srcDetail": "C023", + "major": "유조선", + "mid": "일반탱커", + "name": "일반탱커", + "source": "GIC", + "tonnage": "500톤 이상", + "purpose": "액체화물 일반", + "aisCode": "AIS:80", + "note": "" + }, + { + "code": "VT-409", + "srcCode": "000024", + "srcDetail": "", + "major": "유조선", + "mid": "급유선", + "name": "급유선", + "source": "RRA", + "tonnage": "100~500톤", + "purpose": "선박 연료 공급", + "aisCode": "AIS:89", + "note": "항만 서비스선" + }, + { + "code": "VT-410", + "srcCode": "000024", + "srcDetail": "51", + "major": "유조선", + "mid": "원유운반선", + "name": "원유운반선(PMS)", + "source": "PMS", + "tonnage": "10,000톤 이상", + "purpose": "원유", + "aisCode": "AIS:80", + "note": "" + }, + { + "code": "VT-411", + "srcCode": "000024", + "srcDetail": "52", + "major": "유조선", + "mid": "석유제품운반선", + "name": "석유제품운반선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "석유제품", + "aisCode": "AIS:80", + "note": "" + }, + { + "code": "VT-412", + "srcCode": "000024", + "srcDetail": "53", + "major": "유조선", + "mid": "케미칼운반선", + "name": "케미칼운반선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "케미칼", + "aisCode": "AIS:82", + "note": "" + }, + { + "code": "VT-413", + "srcCode": "000024", + "srcDetail": "54", + "major": "유조선", + "mid": "케미칼운반선", + "name": "케미칼가스운반선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "케미칼+가스", + "aisCode": "AIS:83", + "note": "" + }, + { + "code": "VT-414", + "srcCode": "000024", + "srcDetail": "55", + "major": "유조선", + "mid": "가스운반선", + "name": "LPG운반선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "LPG", + "aisCode": "AIS:84", + "note": "" + }, + { + "code": "VT-415", + "srcCode": "000024", + "srcDetail": "56", + "major": "유조선", + "mid": "가스운반선", + "name": "LNG운반선(PMS)", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "LNG", + "aisCode": "AIS:84", + "note": "" + }, + { + "code": "VT-416", + "srcCode": "000024", + "srcDetail": "57", + "major": "유조선", + "mid": "석유제품운반선", + "name": "석유제품/케미칼겸용", + "source": "PMS", + "tonnage": "1,000톤 이상", + "purpose": "석유+케미칼 겸용", + "aisCode": "AIS:82", + "note": "" + }, + { + "code": "VT-417", + "srcCode": "000024", + "srcDetail": "59", + "major": "유조선", + "mid": "기타유조선", + "name": "기타유조선(PMS)", + "source": "PMS", + "tonnage": "500톤 이상", + "purpose": "기타 액체", + "aisCode": "AIS:89", + "note": "" + }, + { + "code": "VT-418", + "srcCode": "000024", + "srcDetail": "93", + "major": "유조선", + "mid": "급유선", + "name": "급유선(PMS)", + "source": "PMS", + "tonnage": "100톤 이상", + "purpose": "연료 공급", + "aisCode": "AIS:89", + "note": "" + }, + { + "code": "VT-500", + "srcCode": "000025", + "srcDetail": "D001", + "major": "관공선", + "mid": "관공선(일반)", + "name": "관공선(일반)", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "정부·공공기관 업무", + "aisCode": "AIS:52~58", + "note": "" + }, + { + "code": "VT-501", + "srcCode": "000025", + "srcDetail": "D003", + "major": "관공선", + "mid": "조사선", + "name": "시험조사선", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "해양·수산 조사", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-502", + "srcCode": "000025", + "srcDetail": "D004", + "major": "관공선", + "mid": "지도선", + "name": "지도선", + "source": "GIC", + "tonnage": "50톤 이상", + "purpose": "어업지도·단속", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-503", + "srcCode": "000025", + "srcDetail": "D005", + "major": "관공선", + "mid": "시험선", + "name": "시험선", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "선박·장비 시험", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-504", + "srcCode": "000025", + "srcDetail": "D008", + "major": "관공선", + "mid": "방제선", + "name": "방제선", + "source": "GIC", + "tonnage": "200톤 이상", + "purpose": "해양오염 방제", + "aisCode": "AIS:54", + "note": "방제장비 탑재" + }, + { + "code": "VT-505", + "srcCode": "000025", + "srcDetail": "D009", + "major": "관공선", + "mid": "의료선", + "name": "의료선", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "해상 의료 지원", + "aisCode": "AIS:58", + "note": "" + }, + { + "code": "VT-506", + "srcCode": "000025", + "srcDetail": "D006", + "major": "관공선", + "mid": "군선", + "name": "군선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "군용 지원", + "aisCode": "AIS:35", + "note": "해군/해경 지원" + }, + { + "code": "VT-507", + "srcCode": "000025", + "srcDetail": "81", + "major": "관공선", + "mid": "관공선", + "name": "관공선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "공공 업무", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-508", + "srcCode": "000025", + "srcDetail": "83", + "major": "관공선", + "mid": "군함", + "name": "군함", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "군사 작전", + "aisCode": "AIS:35", + "note": "" + }, + { + "code": "VT-509", + "srcCode": "000025", + "srcDetail": "91", + "major": "관공선", + "mid": "어업관리선", + "name": "연근해어선(관리)", + "source": "PMS", + "tonnage": "100톤 이상", + "purpose": "연근해어업 관리", + "aisCode": "AIS:52", + "note": "수산관리원" + }, + { + "code": "VT-510", + "srcCode": "000025", + "srcDetail": "92", + "major": "관공선", + "mid": "어업관리선", + "name": "원양어선(관리)", + "source": "PMS", + "tonnage": "200톤 이상", + "purpose": "원양어업 관리", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-511", + "srcCode": "000025", + "srcDetail": "55", + "major": "관공선", + "mid": "법집행선", + "name": "Law enforcement vessel", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "해상 법집행", + "aisCode": "AIS:55", + "note": "국제 AIS 코드" + }, + { + "code": "VT-512", + "srcCode": "000025", + "srcDetail": "50", + "major": "관공선", + "mid": "도선선", + "name": "Pilot vessel", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "수로 도선", + "aisCode": "AIS:50", + "note": "" + }, + { + "code": "VT-513", + "srcCode": "000025", + "srcDetail": "51", + "major": "관공선", + "mid": "수색구조선", + "name": "Search and rescue vessel", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "해상 수색구조", + "aisCode": "AIS:51", + "note": "SAR" + }, + { + "code": "VT-514", + "srcCode": "000025", + "srcDetail": "52", + "major": "관공선", + "mid": "예인선", + "name": "Tugs", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "예인 서비스", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-515", + "srcCode": "000025", + "srcDetail": "53", + "major": "관공선", + "mid": "항만지원선", + "name": "Port tenders", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "항만 지원", + "aisCode": "AIS:53", + "note": "" + }, + { + "code": "VT-516", + "srcCode": "000025", + "srcDetail": "54", + "major": "관공선", + "mid": "방제선", + "name": "Anti-pollution vessel", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "오염 방제", + "aisCode": "AIS:54", + "note": "" + }, + { + "code": "VT-517", + "srcCode": "000025", + "srcDetail": "58", + "major": "관공선", + "mid": "의료선", + "name": "Medical transports", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "의료 지원", + "aisCode": "AIS:58", + "note": "" + }, + { + "code": "VT-600", + "srcCode": "000021", + "srcDetail": "D002", + "major": "함정", + "mid": "해양경찰정", + "name": "해경정", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "해상 경비·단속", + "aisCode": "AIS:55", + "note": "해양경찰청 운용" + }, + { + "code": "VT-601", + "srcCode": "000021", + "srcDetail": "82", + "major": "함정", + "mid": "경찰정", + "name": "경찰정", + "source": "PMS", + "tonnage": "50톤 이상", + "purpose": "해상 경찰", + "aisCode": "AIS:55", + "note": "" + }, + { + "code": "VT-602", + "srcCode": "000021", + "srcDetail": "55", + "major": "함정", + "mid": "법집행선", + "name": "Law enforcement vessels", + "source": "AIS", + "tonnage": "제한없음", + "purpose": "해상 법집행", + "aisCode": "AIS:55", + "note": "IMO/ITU 국제코드" + }, + { + "code": "VT-700", + "srcCode": "000019", + "srcDetail": "D007", + "major": "항공기", + "mid": "해경항공기", + "name": "해경항공기", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "해상 수색구조·경비", + "aisCode": "AIS:29", + "note": "SAR 항공기" + }, + { + "code": "VT-800", + "srcCode": "000027", + "srcDetail": "E001", + "major": "기타선", + "mid": "일반기타선", + "name": "일반기타선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "기타", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-801", + "srcCode": "000027", + "srcDetail": "E002", + "major": "기타선", + "mid": "예선", + "name": "예선(도선겸용)", + "source": "GIC", + "tonnage": "50~200톤 미만", + "purpose": "예인·도선", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-802", + "srcCode": "000027", + "srcDetail": "E003", + "major": "기타선", + "mid": "외국적선", + "name": "외국적선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "외국 선박", + "aisCode": "AIS:-", + "note": "AIS Type 다양" + }, + { + "code": "VT-803", + "srcCode": "000027", + "srcDetail": "E004", + "major": "기타선", + "mid": "지원선", + "name": "급유급수선", + "source": "GIC", + "tonnage": "50~300톤 미만", + "purpose": "연료·청수 공급", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-804", + "srcCode": "000027", + "srcDetail": "E005", + "major": "기타선", + "mid": "통선", + "name": "통선", + "source": "GIC", + "tonnage": "5~30톤 미만", + "purpose": "항만 연락·통신", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-805", + "srcCode": "000027", + "srcDetail": "E006", + "major": "기타선", + "mid": "부선", + "name": "부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "화물 적재 전용", + "aisCode": "AIS:0", + "note": "자력항행 불가" + }, + { + "code": "VT-806", + "srcCode": "000027", + "srcDetail": "E007", + "major": "기타선", + "mid": "신조선", + "name": "신조선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "건조 중 선박", + "aisCode": "AIS:-", + "note": "임시 분류" + }, + { + "code": "VT-807", + "srcCode": "000027", + "srcDetail": "E008", + "major": "기타선", + "mid": "특수목적선", + "name": "특수목적선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "특수 목적", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-808", + "srcCode": "000027", + "srcDetail": "E009", + "major": "기타선", + "mid": "준설선", + "name": "준설선", + "source": "GIC", + "tonnage": "100톤 이상", + "purpose": "해저 준설", + "aisCode": "AIS:33", + "note": "" + }, + { + "code": "VT-809", + "srcCode": "000027", + "srcDetail": "E010", + "major": "기타선", + "mid": "청소선", + "name": "유창청소선", + "source": "GIC", + "tonnage": "50톤 이상", + "purpose": "유류 청소", + "aisCode": "AIS:54", + "note": "" + }, + { + "code": "VT-810", + "srcCode": "000027", + "srcDetail": "E011", + "major": "기타선", + "mid": "예선", + "name": "견인용예선", + "source": "GIC", + "tonnage": "50~500톤 미만", + "purpose": "선박 견인", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-811", + "srcCode": "000027", + "srcDetail": "E012", + "major": "기타선", + "mid": "예선", + "name": "이·접안용예선", + "source": "GIC", + "tonnage": "30~200톤 미만", + "purpose": "접·이안 지원", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-812", + "srcCode": "000027", + "srcDetail": "E013", + "major": "기타선", + "mid": "예선", + "name": "압항예선", + "source": "GIC", + "tonnage": "50~300톤 미만", + "purpose": "선박 압항", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-813", + "srcCode": "000027", + "srcDetail": "E014", + "major": "기타선", + "mid": "예선", + "name": "기타예선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "기타 예인", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-814", + "srcCode": "000027", + "srcDetail": "E015", + "major": "기타선", + "mid": "외국적선", + "name": "외국적선(풀컨테이너)", + "source": "GIC", + "tonnage": "5,000톤 이상", + "purpose": "컨테이너 운송", + "aisCode": "AIS:71", + "note": "" + }, + { + "code": "VT-815", + "srcCode": "000027", + "srcDetail": "E016", + "major": "기타선", + "mid": "외국적선", + "name": "외국적선(기타)", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "기타 외국선", + "aisCode": "AIS:-", + "note": "" + }, + { + "code": "VT-816", + "srcCode": "000027", + "srcDetail": "E020", + "major": "기타선", + "mid": "지원선", + "name": "급수선", + "source": "GIC", + "tonnage": "50~200톤 미만", + "purpose": "청수 공급", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-817", + "srcCode": "000027", + "srcDetail": "E021", + "major": "기타선", + "mid": "부선", + "name": "모래운반용-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "모래·골재 운반", + "aisCode": "AIS:0", + "note": "자력항행 불가" + }, + { + "code": "VT-818", + "srcCode": "000027", + "srcDetail": "E022", + "major": "기타선", + "mid": "부선", + "name": "철강재운반용-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "철강재 운반", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-819", + "srcCode": "000027", + "srcDetail": "E023", + "major": "기타선", + "mid": "부선", + "name": "원유운반용-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "원유 운반", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-820", + "srcCode": "000027", + "srcDetail": "E024", + "major": "기타선", + "mid": "부선", + "name": "석유제품운반용-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "석유제품 운반", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-821", + "srcCode": "000027", + "srcDetail": "E025", + "major": "기타선", + "mid": "부선", + "name": "화공약품운반용-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "화학약품 운반", + "aisCode": "AIS:0", + "note": "MARPOL 적용" + }, + { + "code": "VT-822", + "srcCode": "000027", + "srcDetail": "E026", + "major": "기타선", + "mid": "부선", + "name": "일반화물운반용-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "일반화물 운반", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-823", + "srcCode": "000027", + "srcDetail": "E027", + "major": "기타선", + "mid": "부선", + "name": "공사작업용-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "해상공사 작업", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-824", + "srcCode": "000027", + "srcDetail": "E028", + "major": "기타선", + "mid": "부선", + "name": "기타-부선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "기타 부선", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-825", + "srcCode": "000027", + "srcDetail": "E029", + "major": "기타선", + "mid": "특수선", + "name": "수면비행선", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "수면비행", + "aisCode": "AIS:49", + "note": "WIG선" + }, + { + "code": "VT-826", + "srcCode": "000027", + "srcDetail": "E030", + "major": "기타선", + "mid": "특수선", + "name": "이동식해양구조물", + "source": "GIC", + "tonnage": "제한없음", + "purpose": "해양플랜트", + "aisCode": "AIS:0", + "note": "MODU/FPSO 등" + }, + { + "code": "VT-827", + "srcCode": "000027", + "srcDetail": "E040", + "major": "기타선", + "mid": "특수", + "name": "난파물", + "source": "GIC", + "tonnage": "-", + "purpose": "-", + "aisCode": "AIS:-", + "note": "" + }, + { + "code": "VT-828", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "레저선박", + "name": "모터보트", + "source": "RRA", + "tonnage": "5톤 미만", + "purpose": "레저·스포츠", + "aisCode": "AIS:37", + "note": "수상레저안전법" + }, + { + "code": "VT-829", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "레저선박", + "name": "세일링요트", + "source": "RRA", + "tonnage": "5톤 미만", + "purpose": "레저 요트", + "aisCode": "AIS:36", + "note": "" + }, + { + "code": "VT-830", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "레저선박", + "name": "수상오토바이", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "레저", + "aisCode": "AIS:37", + "note": "" + }, + { + "code": "VT-831", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "레저선박", + "name": "고무보트", + "source": "RRA", + "tonnage": "1톤 미만", + "purpose": "레저", + "aisCode": "AIS:37", + "note": "" + }, + { + "code": "VT-832", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "레저선박", + "name": "요트", + "source": "RRA", + "tonnage": "5~100톤", + "purpose": "레저 범선", + "aisCode": "AIS:36", + "note": "" + }, + { + "code": "VT-833", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "레저선박", + "name": "플레저보트", + "source": "RRA", + "tonnage": "5톤 미만", + "purpose": "레저", + "aisCode": "AIS:37", + "note": "" + }, + { + "code": "VT-834", + "srcCode": "000027", + "srcDetail": "61", + "major": "기타선", + "mid": "예선", + "name": "견인용예선(PMS)", + "source": "PMS", + "tonnage": "50~500톤", + "purpose": "견인", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-835", + "srcCode": "000027", + "srcDetail": "62", + "major": "기타선", + "mid": "예선", + "name": "이접안용예선(PMS)", + "source": "PMS", + "tonnage": "30~200톤", + "purpose": "접이안", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-836", + "srcCode": "000027", + "srcDetail": "63", + "major": "기타선", + "mid": "예선", + "name": "압항예선(PMS)", + "source": "PMS", + "tonnage": "50~300톤", + "purpose": "압항", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-837", + "srcCode": "000027", + "srcDetail": "64", + "major": "기타선", + "mid": "예선", + "name": "예선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "예인", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-838", + "srcCode": "000027", + "srcDetail": "70", + "major": "기타선", + "mid": "부선", + "name": "부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "화물 적재", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-839", + "srcCode": "000027", + "srcDetail": "71", + "major": "기타선", + "mid": "부선", + "name": "모래운반용-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "모래", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-840", + "srcCode": "000027", + "srcDetail": "72", + "major": "기타선", + "mid": "부선", + "name": "철강재운반용-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "철강재", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-841", + "srcCode": "000027", + "srcDetail": "73", + "major": "기타선", + "mid": "부선", + "name": "원유운반용-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "원유", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-842", + "srcCode": "000027", + "srcDetail": "74", + "major": "기타선", + "mid": "부선", + "name": "석유제품운반용-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "석유", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-843", + "srcCode": "000027", + "srcDetail": "75", + "major": "기타선", + "mid": "부선", + "name": "화공약품운반용-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "화학약품", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-844", + "srcCode": "000027", + "srcDetail": "76", + "major": "기타선", + "mid": "부선", + "name": "일반화물운반용-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "일반화물", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-845", + "srcCode": "000027", + "srcDetail": "77", + "major": "기타선", + "mid": "부선", + "name": "공사작업용-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "공사작업", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-846", + "srcCode": "000027", + "srcDetail": "79", + "major": "기타선", + "mid": "부선", + "name": "기타-부선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "기타", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-847", + "srcCode": "000027", + "srcDetail": "94", + "major": "기타선", + "mid": "지원선", + "name": "급수선(PMS)", + "source": "PMS", + "tonnage": "50톤 이상", + "purpose": "청수", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-848", + "srcCode": "000027", + "srcDetail": "95", + "major": "기타선", + "mid": "통선", + "name": "용달선(통선)", + "source": "PMS", + "tonnage": "5~30톤", + "purpose": "통선·연락", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-849", + "srcCode": "000027", + "srcDetail": "96", + "major": "기타선", + "mid": "준설선", + "name": "준설선(PMS)", + "source": "PMS", + "tonnage": "100톤 이상", + "purpose": "해저준설", + "aisCode": "AIS:33", + "note": "" + }, + { + "code": "VT-850", + "srcCode": "000027", + "srcDetail": "98", + "major": "기타선", + "mid": "도선", + "name": "도선(PMS)", + "source": "PMS", + "tonnage": "5톤 미만", + "purpose": "단거리 도선", + "aisCode": "AIS:60", + "note": "" + }, + { + "code": "VT-851", + "srcCode": "000027", + "srcDetail": "99", + "major": "기타선", + "mid": "기타선", + "name": "기타선(PMS)", + "source": "PMS", + "tonnage": "제한없음", + "purpose": "기타", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-852", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "지원선", + "name": "방제선", + "source": "RRA", + "tonnage": "100톤 이상", + "purpose": "해양오염 방제", + "aisCode": "AIS:54", + "note": "" + }, + { + "code": "VT-853", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "지원선", + "name": "소방선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "해상 소방", + "aisCode": "AIS:54", + "note": "" + }, + { + "code": "VT-854", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "지원선", + "name": "청항선/청방선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "항만 청소", + "aisCode": "AIS:54", + "note": "" + }, + { + "code": "VT-855", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "지원선", + "name": "항로표지선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "항로표지 관리", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-856", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "조사선", + "name": "수로측량선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "수로 측량", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-857", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "조사선", + "name": "시험조사선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "시험·조사", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-858", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "조사선", + "name": "탐사선", + "source": "RRA", + "tonnage": "50톤 이상", + "purpose": "해저 탐사", + "aisCode": "AIS:52", + "note": "" + }, + { + "code": "VT-859", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "조사선", + "name": "해저케이블가설선", + "source": "RRA", + "tonnage": "500톤 이상", + "purpose": "해저케이블 설치", + "aisCode": "AIS:33", + "note": "" + }, + { + "code": "VT-860", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "특수선", + "name": "잠수정", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "수중 작전", + "aisCode": "AIS:29", + "note": "" + }, + { + "code": "VT-861", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "부선", + "name": "크레인부선", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "해상 크레인", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-862", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "부선", + "name": "모래채취운반부선", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "모래 채취·운반", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-863", + "srcCode": "000027", + "srcDetail": "", + "major": "기타선", + "mid": "기타", + "name": "기타선(일반)", + "source": "RRA", + "tonnage": "제한없음", + "purpose": "미분류 기타", + "aisCode": "AIS:0", + "note": "" + }, + { + "code": "VT-900", + "srcCode": "000027", + "srcDetail": "0", + "major": "기타선(AIS)", + "mid": "AIS국제", + "name": "Not available (default)", + "source": "AIS", + "tonnage": "-", + "purpose": "미분류", + "aisCode": "AIS:0", + "note": "ITU/IMO 표준" + }, + { + "code": "VT-901", + "srcCode": "000027", + "srcDetail": "29", + "major": "기타선(AIS)", + "mid": "AIS국제", + "name": "SAR Aircraft", + "source": "AIS", + "tonnage": "-", + "purpose": "수색구조항공기", + "aisCode": "AIS:29", + "note": "" + }, + { + "code": "VT-902", + "srcCode": "000027", + "srcDetail": "33", + "major": "기타선(AIS)", + "mid": "AIS국제", + "name": "Dredging or underwater ops", + "source": "AIS", + "tonnage": "-", + "purpose": "준설·수중작업", + "aisCode": "AIS:33", + "note": "" + }, + { + "code": "VT-903", + "srcCode": "000027", + "srcDetail": "34", + "major": "기타선(AIS)", + "mid": "AIS국제", + "name": "Diving ops", + "source": "AIS", + "tonnage": "-", + "purpose": "잠수작업", + "aisCode": "AIS:34", + "note": "" + }, + { + "code": "VT-904", + "srcCode": "000027", + "srcDetail": "36", + "major": "기타선(AIS)", + "mid": "AIS국제", + "name": "Sailing", + "source": "AIS", + "tonnage": "-", + "purpose": "범선", + "aisCode": "AIS:36", + "note": "" + }, + { + "code": "VT-905", + "srcCode": "000027", + "srcDetail": "37", + "major": "기타선(AIS)", + "mid": "AIS국제", + "name": "Pleasure Craft", + "source": "AIS", + "tonnage": "-", + "purpose": "레저선박", + "aisCode": "AIS:37", + "note": "" + } +] \ No newline at end of file diff --git a/src/features/admin/AccessControl.tsx b/src/features/admin/AccessControl.tsx new file mode 100644 index 0000000..41bcec8 --- /dev/null +++ b/src/features/admin/AccessControl.tsx @@ -0,0 +1,310 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; +import { Badge } from '@shared/components/ui/badge'; +import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; +import { + Shield, Users, UserCheck, Key, Clock, Search, Plus, Edit2, Trash2, + Eye, Lock, AlertTriangle, FileText, ChevronDown, ChevronRight +} from 'lucide-react'; + +/* + * SFR-01: 역할 기반 권한 관리(RBAC) + * - 조직·직급·직무에 따른 권한 관리 + * - 메뉴·기능·데이터 접근 권한 분리 + * - 감사 로그 기록 및 조회 + * - 비밀번호/계정 잠금 정책 설정 + */ + +interface UserAccount { + id: string; + name: string; + rank: string; + org: string; + role: string; + status: '활성' | '잠금' | '비활성'; + lastLogin: string; + loginCount: number; +} + +interface AuditLog { + time: string; + user: string; + action: string; + target: string; + ip: string; + result: '성공' | '실패' | '차단'; +} + +const ROLES = [ + { name: '시스템 관리자', level: 'ADMIN', count: 3, color: 'bg-red-500/20 text-red-400', menus: '전체 메뉴', data: '전체 데이터' }, + { name: '상황실 운영자', level: 'OPERATOR', count: 12, color: 'bg-blue-500/20 text-blue-400', menus: '상황판·통계·경보', data: '관할 해역' }, + { name: '분석 담당자', level: 'ANALYST', count: 8, color: 'bg-purple-500/20 text-purple-400', menus: 'AI모드·통계·항적', data: '분석 데이터' }, + { name: '현장 단속요원', level: 'FIELD', count: 45, color: 'bg-green-500/20 text-green-400', menus: '함정Agent·모바일', data: '할당 구역' }, + { name: '유관기관 열람자', level: 'VIEWER', count: 6, color: 'bg-yellow-500/20 text-yellow-400', menus: '공유 대시보드', data: '공개 정보' }, +]; + +const USERS: UserAccount[] = [ + { id: 'U001', name: '김영수', rank: '사무관', org: '본청 정보통신과', role: '시스템 관리자', status: '활성', lastLogin: '2026-04-03 09:15', loginCount: 342 }, + { id: 'U002', name: '이상호', rank: '경위', org: '서해지방해경청', role: '상황실 운영자', status: '활성', lastLogin: '2026-04-03 08:30', loginCount: 128 }, + { id: 'U003', name: '박민수', rank: '경사', org: '5001함 삼봉', role: '현장 단속요원', status: '활성', lastLogin: '2026-04-02 22:15', loginCount: 67 }, + { id: 'U004', name: '정해진', rank: '주무관', org: '남해지방해경청', role: '분석 담당자', status: '잠금', lastLogin: '2026-04-01 14:20', loginCount: 89 }, + { id: 'U005', name: '최원석', rank: '6급', org: '해수부 어업관리과', role: '유관기관 열람자', status: '활성', lastLogin: '2026-03-28 10:00', loginCount: 12 }, + { id: 'U006', name: '한지영', rank: '경장', org: '3009함', role: '현장 단속요원', status: '비활성', lastLogin: '2026-02-15 16:40', loginCount: 5 }, +]; + +const AUDIT_LOGS: AuditLog[] = [ + { time: '2026-04-03 09:15:23', user: '김영수', action: '로그인', target: '시스템', ip: '10.20.30.1', result: '성공' }, + { time: '2026-04-03 09:12:05', user: '미상', action: '로그인 시도', target: '시스템', ip: '192.168.5.99', result: '차단' }, + { time: '2026-04-03 08:55:11', user: '이상호', action: '위험도 지도 조회', target: 'SFR-05', ip: '10.20.31.5', result: '성공' }, + { time: '2026-04-03 08:30:44', user: '이상호', action: '로그인', target: '시스템', ip: '10.20.31.5', result: '성공' }, + { time: '2026-04-03 07:45:00', user: '정해진', action: '로그인 시도(5회 실패)', target: '시스템', ip: '10.20.40.12', result: '실패' }, + { time: '2026-04-03 07:44:30', user: '시스템', action: '계정 잠금 처리', target: '정해진(U004)', ip: '-', result: '성공' }, + { time: '2026-04-02 22:15:10', user: '박민수', action: '불법어선 탐지 결과 조회', target: 'SFR-09', ip: '10.50.1.33', result: '성공' }, + { time: '2026-04-02 21:00:00', user: '시스템', action: '일일 감사 로그 백업', target: 'DB', ip: '-', result: '성공' }, +]; + +type Tab = 'roles' | 'users' | 'audit' | 'policy'; + +// DataTable 컬럼: 사용자 관리 +const userColumns: DataColumn>[] = [ + { key: 'id', label: 'ID', width: '60px', render: (v) => {v as string} }, + { key: 'name', label: '이름', width: '70px', sortable: true, render: (v) => {v as string} }, + { key: 'rank', label: '직급', width: '60px' }, + { key: 'org', label: '소속', sortable: true }, + { key: 'role', label: '역할', width: '100px', sortable: true, + render: (v) => {v as string}, + }, + { key: 'status', label: '상태', width: '60px', sortable: true, + render: (v) => { + const s = v as string; + const c = s === '활성' ? 'bg-green-500/20 text-green-400' : s === '잠금' ? 'bg-red-500/20 text-red-400' : 'bg-muted text-muted-foreground'; + return {s}; + }, + }, + { key: 'lastLogin', label: '최종 로그인', width: '130px', sortable: true, + render: (v) => {v as string}, + }, + { key: 'id', label: '관리', width: '70px', align: 'center', sortable: false, + render: (_v, row) => ( +
+ + + {row.status === '잠금' && } +
+ ), + }, +]; + +// DataTable 컬럼: 감사 로그 +const auditColumns: DataColumn>[] = [ + { key: 'time', label: '일시', width: '160px', sortable: true, + render: (v) => {v as string}, + }, + { key: 'user', label: '사용자', width: '70px', sortable: true }, + { key: 'action', label: '행위', sortable: true, render: (v) => {v as string} }, + { key: 'target', label: '대상', width: '80px' }, + { key: 'ip', label: 'IP', width: '110px', render: (v) => {v as string} }, + { key: 'result', label: '결과', width: '60px', sortable: true, + render: (v) => { + const r = v as string; + const c = r === '성공' ? 'bg-green-500/20 text-green-400' : r === '실패' ? 'bg-red-500/20 text-red-400' : 'bg-orange-500/20 text-orange-400'; + return {r}; + }, + }, +]; + +export function AccessControl() { + const { t } = useTranslation('admin'); + const [tab, setTab] = useState('roles'); + + const tabs: { key: Tab; icon: React.ElementType; label: string }[] = [ + { key: 'roles', icon: Shield, label: '역할 관리' }, + { key: 'users', icon: Users, label: '사용자 관리' }, + { key: 'audit', icon: FileText, label: '감사 로그' }, + { key: 'policy', icon: Lock, label: '보안 정책' }, + ]; + + return ( +
+
+
+

+ + {t('accessControl.title')} +

+

{t('accessControl.desc')}

+
+
+ + 활성 사용자 {USERS.filter((u) => u.status === '활성').length}명 + | + 총 등록 {USERS.length}명 +
+
+ + {/* 탭 */} +
+ {tabs.map((t) => ( + + ))} +
+ + {/* ── 역할 관리 ── */} + {tab === 'roles' && ( +
+ {ROLES.map((r) => ( + + +
+
+ {r.level} +
+
{r.name}
+
할당 인원: {r.count}명
+
+
+
+
+ 메뉴 접근: + {r.menus} +
+
+ 데이터 범위: + {r.data} +
+ +
+
+
+
+ ))} + +
+ )} + + {/* ── 사용자 관리 — DataTable 적용 ── */} + {tab === 'users' && ( + )[]} + columns={userColumns} + pageSize={10} + searchPlaceholder="이름, 소속, 역할 검색..." + searchKeys={['name', 'org', 'role', 'rank']} + exportFilename="사용자목록" + showPagination + /> + )} + + {/* ── 감사 로그 — DataTable 적용 ── */} + {tab === 'audit' && ( + )[]} + columns={auditColumns} + pageSize={10} + searchPlaceholder="사용자, 행위, IP 검색..." + searchKeys={['user', 'action', 'ip', 'target']} + exportFilename="감사로그" + title="로그인/로그아웃·비정상 접속·중요 정보 접근 감사 로그" + showPagination + /> + )} + + {/* ── 보안 정책 ── */} + {tab === 'policy' && ( +
+ + + 비밀번호 정책 + + + {[ + ['최소 길이', '9자 이상'], + ['복잡도', '영문+숫자+특수문자 조합'], + ['변경 주기', '90일'], + ['재사용 제한', '최근 3회'], + ['만료 경고', '14일 전'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+ + + + 계정 잠금 정책 + + + {[ + ['잠금 임계', '5회 연속 실패'], + ['잠금 시간', '30분'], + ['자동 해제', '활성'], + ['관리자 해제', '즉시 가능'], + ['비정상 접속 알림', 'SMS + 시스템 알림'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+ + + + 세션 관리 + + + {[ + ['세션 타임아웃', '30분 (미사용 시)'], + ['동시 접속', '1계정 1세션'], + ['중복 로그인', '이전 세션 종료'], + ['세션 갱신', '활동 시 자동 연장'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+ + + + 감사 로그 정책 + + + {[ + ['로그 보존', '1년 이상'], + ['기록 대상', '로그인·권한변경·데이터접근'], + ['무결성 보장', 'Hash 검증'], + ['백업 주기', '일 1회 자동'], + ['조회 권한', 'ADMIN 전용'], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/src/features/admin/AdminPanel.tsx b/src/features/admin/AdminPanel.tsx new file mode 100644 index 0000000..f06bc5c --- /dev/null +++ b/src/features/admin/AdminPanel.tsx @@ -0,0 +1,91 @@ +import { useTranslation } from 'react-i18next'; +import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; +import { Settings, Server, HardDrive, Shield, Clock, Database } from 'lucide-react'; + +/* + * 시스템 관리 — 서버 상태, 디스크, 보안 설정 등 인프라 관리 + */ + +const SERVER_STATUS = [ + { name: 'WAS-01 (운영)', cpu: 42, mem: 65, disk: 38, status: '정상' }, + { name: 'WAS-02 (이중화)', cpu: 18, mem: 32, disk: 38, status: '정상' }, + { name: 'DB-01 (PostgreSQL)', cpu: 55, mem: 72, disk: 61, status: '정상' }, + { name: 'DB-02 (TimescaleDB)', cpu: 48, mem: 68, disk: 55, status: '정상' }, + { name: 'AI-Engine-01', cpu: 78, mem: 85, disk: 42, status: '주의' }, + { name: 'File-NAS', cpu: 5, mem: 12, disk: 82, status: '경고' }, +]; + +function UsageBar({ value }: { value: number }) { + const color = value > 80 ? 'bg-red-500' : value > 60 ? 'bg-yellow-500' : 'bg-green-500'; + return ( +
+
+
+
+ {value}% +
+ ); +} + +export function AdminPanel() { + const { t } = useTranslation('admin'); + return ( +
+
+

+ + {t('adminPanel.title')} +

+

{t('adminPanel.desc')}

+
+ + {/* 서버 상태 */} +
+ {SERVER_STATUS.map((s) => ( + + +
+
+ + {s.name} +
+ {s.status} +
+
+
CPU
+
MEM
+
DISK
+
+
+
+ ))} +
+ + {/* 시스템 정보 */} +
+ + + 데이터베이스 + + + {[['PostgreSQL', 'v15.4 운영중'], ['TimescaleDB', 'v2.12 운영중'], ['Redis 캐시', 'v7.2 운영중'], ['Kafka', 'v3.6 클러스터 3노드']].map(([k, v]) => ( +
{k}{v}
+ ))} +
+
+ + + 보안 현황 + + + {[['SSL 인증서', '2027-03-15 만료'], ['방화벽', '정상 동작'], ['IDS/IPS', '실시간 감시중'], ['백업', '금일 03:00 완료']].map(([k, v]) => ( +
{k}{v}
+ ))} +
+
+
+
+ ); +} diff --git a/src/features/admin/DataHub.tsx b/src/features/admin/DataHub.tsx new file mode 100644 index 0000000..daa3716 --- /dev/null +++ b/src/features/admin/DataHub.tsx @@ -0,0 +1,675 @@ +import { useState, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; +import { Badge } from '@shared/components/ui/badge'; +import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; +import { SaveButton } from '@shared/components/common/SaveButton'; +import { + Database, RefreshCw, Calendar, Wifi, WifiOff, Radio, + Activity, Server, ArrowDownToLine, Clock, AlertTriangle, + CheckCircle, XCircle, BarChart3, Layers, Plus, Play, Square, + Trash2, Edit2, Eye, FileText, HardDrive, Upload, FolderOpen, + Network, X, ChevronRight, Info, +} from 'lucide-react'; + +/* + * SFR-03: 통합데이터 허브 수집·연계 관리 + * ① 선박신호 수신 현황 — 24시간 타임라인 히트맵 + * ② 선박위치정보 모니터링 — 연계 채널 테이블 + */ + +// ─── ① 선박신호 수신 현황 데이터 ────────────── + +type SignalStatus = 'ok' | 'warn' | 'error'; + +interface SignalSource { + name: string; + rate: number; // 수신율 % + timeline: SignalStatus[]; // 24시간 × 6 (10분 단위 = 144 슬롯) +} + +function generateTimeline(): SignalStatus[] { + return Array.from({ length: 144 }, () => { + const r = Math.random(); + return r < 0.75 ? 'ok' : r < 0.90 ? 'warn' : 'error'; + }); +} + +const SIGNAL_SOURCES: SignalSource[] = [ + { name: 'VTS', rate: 88.9, timeline: generateTimeline() }, + { name: 'VTS-AIS', rate: 85.4, timeline: generateTimeline() }, + { name: 'V-PASS', rate: 84.0, timeline: generateTimeline() }, + { name: 'E-NAVI', rate: 88.9, timeline: generateTimeline() }, + { name: 'S&P AIS', rate: 85.4, timeline: generateTimeline() }, +]; + +const SIGNAL_COLORS: Record = { + ok: '#22c55e', + warn: '#eab308', + error: '#ef4444', +}; + +const HOURS = Array.from({ length: 25 }, (_, i) => `${String(i).padStart(2, '0')}시`); + +// ─── ② 선박위치정보 모니터링 데이터 ────────────── + +interface ChannelRecord { + no: number; + source: string; // 원천기관 + code: string; // 기관코드 + system: string; // 정보시스템명 + linkInfo: string; // 연계정보 + storage: string; // 저장장소 + linkMethod: string; // 연계방식 + cycle: string; // 수집주기 + vesselCount: string; // 선박건수/신호건수 + status: 'ON' | 'OFF'; // 연결상태 + lastUpdate: string; // 최종갱신 + [key: string]: unknown; +} + +const CHANNELS: ChannelRecord[] = [ + { no: 1, source: '부산항', code: 'BS', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '439 / 499', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 2, source: '부산항', code: 'BS', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '133 / 463', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 3, source: '부산신항', code: 'BSN', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '255 / 278', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 4, source: '부산신항', code: 'BSN', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '133 / 426', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 5, source: '동해안', code: 'DH', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, + { no: 6, source: '동해안', code: 'DH', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, + { no: 7, source: '대산항', code: 'DS', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '0', status: 'OFF', lastUpdate: '2026-03-15 15:38:57' }, + { no: 8, source: '대산항', code: 'DS', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '0', status: 'OFF', lastUpdate: '2026-03-15 15:38:56' }, + { no: 9, source: '경인항', code: 'GI', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '120 / 136', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 10, source: '경인항', code: 'GI', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '55 / 467', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 11, source: '경인연안', code: 'GIC', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '180 / 216', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 12, source: '경인연안', code: 'GIC', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, + { no: 13, source: '군산항', code: 'GS', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, + { no: 14, source: '군산항', code: 'GS', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, + { no: 15, source: '인천항', code: 'IC', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '149 / 176', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 16, source: '인천항', code: 'IC', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '55 / 503', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 17, source: '진도연안', code: 'JDC', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '433 / 524', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 18, source: '진도연안', code: 'JDC', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '256 / 1619', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 19, source: '제주항', code: 'JJ', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '429 / 508', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 20, source: '제주항', code: 'JJ', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '160 / 1592', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, + { no: 21, source: '목포항', code: 'MP', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, + { no: 22, source: '목포항', code: 'MP', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, +]; + +// ─── 채널 테이블 컬럼 정의 ─────────────────── + +const channelColumns: DataColumn[] = [ + { key: 'no', label: '번호', width: '50px', align: 'center', sortable: true, + render: (v) => {v as number}, + }, + { key: 'source', label: '원천기관', width: '80px', sortable: true, + render: (v) => {v as string}, + }, + { key: 'code', label: '기관코드', width: '65px', align: 'center', + render: (v) => {v as string}, + }, + { key: 'system', label: '정보시스템명', width: '100px', sortable: true, + render: (v) => {v as string}, + }, + { key: 'linkInfo', label: '연계정보', width: '65px' }, + { key: 'storage', label: '저장장소', render: (v) => {v as string} }, + { key: 'linkMethod', label: '연계방식', width: '70px', align: 'center', + render: (v) => {v as string}, + }, + { key: 'cycle', label: '수집주기', width: '80px', align: 'center', + render: (v) => { + const s = v as string; + return s === '수신대기중' + ? {s} + : {s}; + }, + }, + { key: 'vesselCount', label: '선박건수/신호건수', width: '120px', align: 'right', sortable: true, + render: (v) => {v as string}, + }, + { key: 'status', label: '연결상태', width: '80px', align: 'center', sortable: true, + render: (v, row) => { + const on = v === 'ON'; + return ( +
+ + {v as string} + + {row.lastUpdate && ( + {row.lastUpdate as string} + )} +
+ ); + }, + }, +]; + +// ─── 히트맵 컴포넌트 ────────────────────── + +function SignalTimeline({ source }: { source: SignalSource }) { + return ( +
+ {/* 라벨 */} +
+
{source.name}
+
{source.rate}%
+
+ {/* 타임라인 바 */} +
+ {source.timeline.map((status, i) => ( +
+ ))} +
+
+ ); +} + +// ─── ③ 수집 작업 관리 데이터 ────────────────── + +type JobStatus = '정지' | '대기중' | '수행중' | '장애발생'; +type ServerType = 'SQL' | 'FILE' | 'FTP'; + +interface CollectJob { + id: string; + name: string; + serverType: ServerType; + serverName: string; + serverIp: string; + status: JobStatus; + schedule: string; + lastRun: string; + successRate: number; + [key: string]: unknown; +} + +const COLLECT_JOBS: CollectJob[] = [ + { id: 'COL-001', name: '부산항 AIS 수집', serverType: 'SQL', serverName: 'vts-bs-db01', serverIp: '10.20.30.11', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', successRate: 98.5 }, + { id: 'COL-002', name: '인천항 VTS 수집', serverType: 'SQL', serverName: 'vts-ic-db01', serverIp: '10.20.30.12', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', successRate: 97.2 }, + { id: 'COL-003', name: 'V-PASS 파일 수집', serverType: 'FILE', serverName: 'vpass-ftp01', serverIp: '10.20.31.20', status: '수행중', schedule: '매 30분', lastRun: '2026-04-03 09:00:00', successRate: 94.1 }, + { id: 'COL-004', name: 'E-NAVI 로그 수집', serverType: 'FILE', serverName: 'enavi-nas01', serverIp: '10.20.31.30', status: '대기중', schedule: '매 1시간', lastRun: '2026-04-03 08:00:00', successRate: 91.8 }, + { id: 'COL-005', name: 'S&P AIS 해외 수집', serverType: 'FTP', serverName: 'sp-ais-ftp', serverIp: '203.45.67.89', status: '수행중', schedule: '매 15분', lastRun: '2026-04-03 09:15:00', successRate: 85.4 }, + { id: 'COL-006', name: '동해안 VTS 수집', serverType: 'SQL', serverName: 'vts-dh-db01', serverIp: '10.20.30.15', status: '장애발생', schedule: '매 10분', lastRun: '2026-04-02 22:10:00', successRate: 0 }, + { id: 'COL-007', name: '군산항 레이더 수집', serverType: 'SQL', serverName: 'vts-gs-db01', serverIp: '10.20.30.18', status: '장애발생', schedule: '매 10분', lastRun: '2026-04-01 14:30:00', successRate: 0 }, + { id: 'COL-008', name: '제주항 CCTV 수집', serverType: 'FTP', serverName: 'jj-cctv-ftp', serverIp: '10.20.32.50', status: '정지', schedule: '매 1시간', lastRun: '2026-03-28 12:00:00', successRate: 0 }, + { id: 'COL-009', name: '위성 SAR 수집', serverType: 'FTP', serverName: 'sat-sar-ftp', serverIp: '10.20.40.10', status: '수행중', schedule: '매 6시간', lastRun: '2026-04-03 06:00:00', successRate: 99.0 }, + { id: 'COL-010', name: '해수부 VMS 연동', serverType: 'SQL', serverName: 'mof-vms-db', serverIp: '10.20.50.11', status: '수행중', schedule: '매 5분', lastRun: '2026-04-03 09:20:00', successRate: 96.3 }, +]; + +const collectColumns: DataColumn[] = [ + { key: 'id', label: 'ID', width: '80px', render: (v) => {v as string} }, + { key: 'name', label: '작업명', sortable: true, render: (v) => {v as string} }, + { key: 'serverType', label: '타입', width: '60px', align: 'center', sortable: true, + render: (v) => { + const t = v as string; + const c = t === 'SQL' ? 'bg-blue-500/20 text-blue-400' : t === 'FILE' ? 'bg-green-500/20 text-green-400' : 'bg-purple-500/20 text-purple-400'; + return {t}; + }, + }, + { key: 'serverName', label: '서버명', width: '120px', render: (v) => {v as string} }, + { key: 'serverIp', label: 'IP', width: '120px', render: (v) => {v as string} }, + { key: 'status', label: '상태', width: '80px', align: 'center', sortable: true, + render: (v) => { + const s = v as JobStatus; + const c = s === '수행중' ? 'bg-green-500/20 text-green-400' : s === '대기중' ? 'bg-yellow-500/20 text-yellow-400' : s === '장애발생' ? 'bg-red-500/20 text-red-400' : 'bg-muted text-muted-foreground'; + return {s}; + }, + }, + { key: 'schedule', label: '스케줄', width: '80px' }, + { key: 'lastRun', label: '최종 수행', width: '140px', sortable: true, render: (v) => {v as string} }, + { key: 'successRate', label: '성공률', width: '70px', align: 'right', sortable: true, + render: (v) => { + const n = v as number; + const c = n >= 90 ? 'text-green-400' : n >= 70 ? 'text-yellow-400' : n > 0 ? 'text-red-400' : 'text-hint'; + return {n > 0 ? `${n}%` : '-'}; + }, + }, + { key: 'id', label: '', width: '70px', align: 'center', sortable: false, + render: (_v, row) => ( +
+ {row.status === '정지' ? ( + + ) : row.status !== '장애발생' ? ( + + ) : null} + + +
+ ), + }, +]; + +// ─── ④ 적재 작업 관리 데이터 ────────────────── + +interface LoadJob { + id: string; + name: string; + sourceJob: string; + targetTable: string; + targetDb: string; + status: JobStatus; + schedule: string; + lastRun: string; + recordCount: string; + [key: string]: unknown; +} + +const LOAD_JOBS: LoadJob[] = [ + { id: 'LOD-001', name: 'AIS 동적정보 적재', sourceJob: 'COL-001', targetTable: 'tb_ais_dynamic', targetDb: 'MDA_DB', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', recordCount: '12,450' }, + { id: 'LOD-002', name: 'VTS 레이더 적재', sourceJob: 'COL-002', targetTable: 'tb_vts_radar', targetDb: 'MDA_DB', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', recordCount: '8,320' }, + { id: 'LOD-003', name: 'V-PASS 위치 적재', sourceJob: 'COL-003', targetTable: 'tb_vpass_position', targetDb: 'MDA_DB', status: '수행중', schedule: '매 30분', lastRun: '2026-04-03 09:00:00', recordCount: '3,210' }, + { id: 'LOD-004', name: 'VMS 데이터 적재', sourceJob: 'COL-010', targetTable: 'tb_vms_track', targetDb: 'MDA_DB', status: '수행중', schedule: '매 5분', lastRun: '2026-04-03 09:20:00', recordCount: '5,677' }, + { id: 'LOD-005', name: 'SAR 위성 적재', sourceJob: 'COL-009', targetTable: 'tb_sat_imagery', targetDb: 'SAT_DB', status: '대기중', schedule: '매 6시간', lastRun: '2026-04-03 06:00:00', recordCount: '24' }, + { id: 'LOD-006', name: '해외 AIS 적재', sourceJob: 'COL-005', targetTable: 'tb_sais_global', targetDb: 'MDA_DB', status: '수행중', schedule: '매 15분', lastRun: '2026-04-03 09:15:00', recordCount: '45,200' }, + { id: 'LOD-007', name: '동해안 VTS 적재', sourceJob: 'COL-006', targetTable: 'tb_vts_dh', targetDb: 'MDA_DB', status: '장애발생', schedule: '매 10분', lastRun: '2026-04-02 22:10:00', recordCount: '0' }, +]; + +const loadColumns: DataColumn[] = [ + { key: 'id', label: 'ID', width: '80px', render: (v) => {v as string} }, + { key: 'name', label: '작업명', sortable: true, render: (v) => {v as string} }, + { key: 'sourceJob', label: '수집원', width: '80px', render: (v) => {v as string} }, + { key: 'targetTable', label: '대상 테이블', width: '140px', render: (v) => {v as string} }, + { key: 'targetDb', label: 'DB', width: '70px', align: 'center' }, + { key: 'status', label: '상태', width: '80px', align: 'center', sortable: true, + render: (v) => { + const s = v as JobStatus; + const c = s === '수행중' ? 'bg-green-500/20 text-green-400' : s === '대기중' ? 'bg-yellow-500/20 text-yellow-400' : s === '장애발생' ? 'bg-red-500/20 text-red-400' : 'bg-muted text-muted-foreground'; + return {s}; + }, + }, + { key: 'schedule', label: '스케줄', width: '80px' }, + { key: 'lastRun', label: '최종 적재', width: '140px', sortable: true, render: (v) => {v as string} }, + { key: 'recordCount', label: '적재건수', width: '80px', align: 'right', render: (v) => {v as string} }, + { key: 'id', label: '', width: '70px', align: 'center', sortable: false, + render: () => ( +
+ + + +
+ ), + }, +]; + +// ─── ⑤ 연계서버 모니터링 데이터 ──────────────── + +type AgentRole = '수집' | '적재'; + +interface AgentServer { + id: string; + name: string; + role: AgentRole; + hostname: string; + ip: string; + port: string; + mac: string; + status: JobStatus; + taskCount: number; + cpuUsage: number; + memUsage: number; + diskUsage: number; + lastHeartbeat: string; + [key: string]: unknown; +} + +const AGENTS: AgentServer[] = [ + { id: 'AGT-1001', name: '부산항 AIS 수집Agent', role: '수집', hostname: 'vts-bs-col01', ip: '10.20.30.11', port: '8081', mac: '00:1A:2B:3C:4D:01', status: '수행중', taskCount: 3, cpuUsage: 42, memUsage: 65, diskUsage: 38, lastHeartbeat: '2026-04-03 09:20:05' }, + { id: 'AGT-1002', name: '인천항 VTS 수집Agent', role: '수집', hostname: 'vts-ic-col01', ip: '10.20.30.12', port: '8081', mac: '00:1A:2B:3C:4D:02', status: '수행중', taskCount: 2, cpuUsage: 35, memUsage: 52, diskUsage: 41, lastHeartbeat: '2026-04-03 09:20:03' }, + { id: 'AGT-1003', name: '제주항 수집Agent', role: '수집', hostname: 'vts-jj-col01', ip: '10.20.30.19', port: '8081', mac: '00:1A:2B:3C:4D:03', status: '수행중', taskCount: 2, cpuUsage: 28, memUsage: 45, diskUsage: 55, lastHeartbeat: '2026-04-03 09:20:02' }, + { id: 'AGT-1004', name: '동해안 수집Agent', role: '수집', hostname: 'vts-dh-col01', ip: '10.20.30.15', port: '8081', mac: '00:1A:2B:3C:4D:04', status: '장애발생', taskCount: 0, cpuUsage: 0, memUsage: 0, diskUsage: 72, lastHeartbeat: '2026-04-02 22:10:15' }, + { id: 'AGT-1005', name: 'V-PASS 수집Agent', role: '수집', hostname: 'vpass-col01', ip: '10.20.31.20', port: '8082', mac: '00:1A:2B:3C:4D:05', status: '수행중', taskCount: 1, cpuUsage: 18, memUsage: 30, diskUsage: 25, lastHeartbeat: '2026-04-03 09:20:01' }, + { id: 'AGT-2001', name: 'MDA DB 적재Agent', role: '적재', hostname: 'mda-lod01', ip: '10.20.40.11', port: '9091', mac: '00:1A:2B:3C:4D:11', status: '수행중', taskCount: 5, cpuUsage: 55, memUsage: 72, diskUsage: 48, lastHeartbeat: '2026-04-03 09:20:04' }, + { id: 'AGT-2002', name: 'SAT DB 적재Agent', role: '적재', hostname: 'sat-lod01', ip: '10.20.40.12', port: '9091', mac: '00:1A:2B:3C:4D:12', status: '대기중', taskCount: 1, cpuUsage: 5, memUsage: 22, diskUsage: 33, lastHeartbeat: '2026-04-03 09:20:00' }, + { id: 'AGT-2003', name: '백업 적재Agent', role: '적재', hostname: 'bak-lod01', ip: '10.20.40.13', port: '9091', mac: '00:1A:2B:3C:4D:13', status: '정지', taskCount: 0, cpuUsage: 0, memUsage: 12, diskUsage: 15, lastHeartbeat: '2026-03-30 18:00:00' }, +]; + +function UsageBar({ value, color }: { value: number; color: string }) { + return ( +
+
+
+
+ {value}% +
+ ); +} + +// ─── 메인 컴포넌트 ────────────────────── + +type Tab = 'signal' | 'monitor' | 'collect' | 'load' | 'agents'; + +export function DataHub() { + const { t } = useTranslation('admin'); + const [tab, setTab] = useState('signal'); + const [selectedDate, setSelectedDate] = useState('2026-04-02'); + const [statusFilter, setStatusFilter] = useState<'' | 'ON' | 'OFF'>(''); + + // 수집 작업 필터 + const [collectTypeFilter, setCollectTypeFilter] = useState<'' | ServerType>(''); + const [collectStatusFilter, setCollectStatusFilter] = useState<'' | JobStatus>(''); + // 적재 작업 필터 + const [loadStatusFilter, setLoadStatusFilter] = useState<'' | JobStatus>(''); + // 연계서버 필터 + const [agentRoleFilter, setAgentRoleFilter] = useState<'' | AgentRole>(''); + const [agentStatusFilter, setAgentStatusFilter] = useState<'' | JobStatus>(''); + + const onCount = CHANNELS.filter((c) => c.status === 'ON').length; + const offCount = CHANNELS.filter((c) => c.status === 'OFF').length; + const hasPartialOff = offCount > 0; + + const filteredChannels = statusFilter + ? CHANNELS.filter((c) => c.status === statusFilter) + : CHANNELS; + + const filteredCollectJobs = COLLECT_JOBS.filter((j) => + (!collectTypeFilter || j.serverType === collectTypeFilter) && + (!collectStatusFilter || j.status === collectStatusFilter) + ); + + const filteredLoadJobs = LOAD_JOBS.filter((j) => + (!loadStatusFilter || j.status === loadStatusFilter) + ); + + const filteredAgents = AGENTS.filter((a) => + (!agentRoleFilter || a.role === agentRoleFilter) && + (!agentStatusFilter || a.status === agentStatusFilter) + ); + + return ( +
+ {/* 헤더 */} +
+
+

+ + {t('dataHub.title')} +

+

+ {t('dataHub.desc')} +

+
+
+ +
+
+ + {/* KPI */} +
+ {[ + { label: '전체 채널', value: CHANNELS.length, icon: Layers, color: 'text-label', bg: 'bg-muted' }, + { label: 'ON', value: onCount, icon: Wifi, color: 'text-blue-400', bg: 'bg-blue-500/10' }, + { label: 'OFF', value: offCount, icon: WifiOff, color: 'text-red-400', bg: 'bg-red-500/10' }, + { label: '평균 수신율', value: '86.5%', icon: BarChart3, color: 'text-green-400', bg: 'bg-green-500/10' }, + { label: '데이터 소스', value: '5종', icon: Radio, color: 'text-purple-400', bg: 'bg-purple-500/10' }, + { label: '연계 방식', value: 'KAFKA', icon: Server, color: 'text-orange-400', bg: 'bg-orange-500/10' }, + ].map((kpi) => ( +
+
+ +
+ {kpi.value} + {kpi.label} +
+ ))} +
+ + {/* 탭 */} +
+ {[ + { key: 'signal' as Tab, icon: Activity, label: '선박신호 수신 현황' }, + { key: 'monitor' as Tab, icon: Server, label: '선박위치정보 모니터링' }, + { key: 'collect' as Tab, icon: ArrowDownToLine, label: '수집 작업 관리' }, + { key: 'load' as Tab, icon: HardDrive, label: '적재 작업 관리' }, + { key: 'agents' as Tab, icon: Network, label: '연계서버 모니터링' }, + ].map((t) => ( + + ))} +
+ + {/* ── ① 선박신호 수신 현황 ── */} + {tab === 'signal' && ( + + + {/* 상단: 제목 + 날짜 */} +
+
선박신호 수신 현황
+
+
+ setSelectedDate(e.target.value)} + className="bg-surface-overlay border border-slate-700/50 rounded-lg px-3 py-1.5 text-[11px] text-heading focus:outline-none focus:border-cyan-500/50" + /> +
+ +
+
+ + {/* 시간축 헤더 */} +
+
+
+ {HOURS.map((h, i) => ( +
+ {i % 2 === 0 ? h : ''} +
+ ))} +
+
+ + {/* 타임라인 히트맵 */} +
+ {SIGNAL_SOURCES.map((src) => ( + + ))} +
+ + {/* 범례 */} +
+
범례:
+ {[ + { label: '정상 수신', color: '#22c55e' }, + { label: '지연/경고', color: '#eab308' }, + { label: '장애/미수신', color: '#ef4444' }, + ].map((item) => ( +
+
+ {item.label} +
+ ))} +
+ 10분 단위 집계 · 24시간 (144 슬롯) +
+
+ + + )} + + {/* ── ② 선박위치정보 모니터링 ── */} + {tab === 'monitor' && ( +
+ {/* 상태 요약 바 */} +
+
+ {hasPartialOff ? ( + + ) : ( + + )} + + {hasPartialOff ? `일부 OFF (${offCount}/${CHANNELS.length})` : '전체 정상'} + +
+ + 연계 채널 {CHANNELS.length}개 (ON: {onCount} / OFF: {offCount}) + + + 갱신: 오후 06:57:33 + + + {/* 상태 필터 */} +
+ {(['', 'ON', 'OFF'] as const).map((f) => ( + + ))} +
+
+ + {/* DataTable */} + +
+ )} + + {/* ── ③ 수집 작업 관리 ── */} + {tab === 'collect' && ( +
+
+ 서버 타입: + {(['', 'SQL', 'FILE', 'FTP'] as const).map((f) => ( + + ))} + 상태: + {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( + + ))} + +
+ +
+ )} + + {/* ── ④ 적재 작업 관리 ── */} + {tab === 'load' && ( +
+
+ 상태: + {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( + + ))} +
+ + +
+
+ +
+ )} + + {/* ── ⑤ 연계서버 모니터링 ── */} + {tab === 'agents' && ( +
+
+ 종류: + {(['', '수집', '적재'] as const).map((f) => ( + + ))} + 상태: + {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( + + ))} + +
+ + {/* 연계서버 카드 그리드 */} +
+ {filteredAgents.map((agent) => { + const stColor = agent.status === '수행중' ? 'text-green-400 bg-green-500/15' : agent.status === '대기중' ? 'text-yellow-400 bg-yellow-500/15' : agent.status === '장애발생' ? 'text-red-400 bg-red-500/15' : 'text-muted-foreground bg-muted'; + return ( + + +
+
+
{agent.name}
+
{agent.id} · {agent.role}에이전트
+
+ {agent.status} +
+
+
Hostname{agent.hostname}
+
IP{agent.ip}
+
Port{agent.port}
+
MAC{agent.mac}
+
+
+
CPU 80 ? 'bg-red-500' : agent.cpuUsage > 50 ? 'bg-yellow-500' : 'bg-green-500'} />
+
MEM 80 ? 'bg-red-500' : agent.memUsage > 50 ? 'bg-yellow-500' : 'bg-green-500'} />
+
DISK 80 ? 'bg-red-500' : agent.diskUsage > 50 ? 'bg-yellow-500' : 'bg-green-500'} />
+
+
+ 작업 {agent.taskCount}건 · heartbeat {agent.lastHeartbeat.slice(11)} +
+ + + +
+
+
+
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/src/features/admin/NoticeManagement.tsx b/src/features/admin/NoticeManagement.tsx new file mode 100644 index 0000000..e837dca --- /dev/null +++ b/src/features/admin/NoticeManagement.tsx @@ -0,0 +1,424 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; +import { Badge } from '@shared/components/ui/badge'; +import { + Bell, Plus, Edit2, Trash2, Eye, EyeOff, Calendar, + Users, Megaphone, AlertTriangle, Info, Search, Filter, + CheckCircle, Clock, Pin, Monitor, MessageSquare, X, +} from 'lucide-react'; +import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; +import { SaveButton } from '@shared/components/common/SaveButton'; +import type { SystemNotice, NoticeType, NoticeDisplay } from '@shared/components/common/NotificationBanner'; + +/* + * SFR-02: 공통알림(팝업, 배너, 시스템 공지 등) 관리 + * - 노출 기간 설정 (시작일 ~ 종료일) + * - 대상 설정 (역할 기반) + * - 알림 유형: 정보, 경고, 긴급, 점검 + * - 표시 방식: 배너, 팝업, 토스트 + */ + +// ─── 시뮬레이션 데이터 ────────────────── +const INITIAL_NOTICES: SystemNotice[] = [ + { + id: 'N-001', type: 'urgent', display: 'banner', title: '서해 NLL 인근 경보 강화', + message: '2026-04-03부터 서해 NLL 인근 해역에 대한 경계 경보가 강화되었습니다. 모든 상황실 운영자는 경계 태세를 유지하시기 바랍니다.', + startDate: '2026-04-03', endDate: '2026-04-10', targetRoles: ['ADMIN', 'OPERATOR'], dismissible: true, pinned: true, + }, + { + id: 'N-002', type: 'maintenance', display: 'popup', title: '정기 시스템 점검 안내', + message: '2026-04-05(토) 02:00~06:00 시스템 정기점검이 예정되어 있습니다. 점검 중 서비스 이용이 제한될 수 있습니다.', + startDate: '2026-04-03', endDate: '2026-04-05', targetRoles: [], dismissible: true, pinned: false, + }, + { + id: 'N-003', type: 'info', display: 'banner', title: 'AI 탐지 모델 v2.3 업데이트', + message: '다크베셀 탐지 정확도가 89% → 93%로 개선되었습니다. 환적 탐지 알고리즘이 업데이트되었습니다.', + startDate: '2026-04-01', endDate: '2026-04-15', targetRoles: ['ADMIN', 'ANALYST'], dismissible: true, pinned: false, + }, + { + id: 'N-004', type: 'warning', display: 'banner', title: '비밀번호 변경 권고', + message: '비밀번호 변경 주기(90일)가 도래한 사용자는 보안정책에 따라 비밀번호를 변경해 주세요.', + startDate: '2026-03-25', endDate: '2026-04-25', targetRoles: [], dismissible: true, pinned: false, + }, + { + id: 'N-005', type: 'info', display: 'toast', title: 'S-57 해도 데이터 갱신', + message: '해경GIS통합위치정보시스템 S-57 전자해도 데이터가 2026-04-01 기준으로 갱신되었습니다.', + startDate: '2026-04-01', endDate: '2026-04-07', targetRoles: ['ADMIN', 'OPERATOR', 'ANALYST'], dismissible: true, pinned: false, + }, + { + id: 'N-006', type: 'urgent', display: 'popup', title: '중국어선 대규모 출항 정보', + message: '중국 산동성 위해·영성 항에서 대규모 어선단(약 300척) 출항이 감지되었습니다. 서해 EEZ 진입 예상시간: 2026-04-04 06:00경.', + startDate: '2026-04-03', endDate: '2026-04-06', targetRoles: ['ADMIN', 'OPERATOR'], dismissible: false, pinned: true, + }, +]; + +const TYPE_OPTIONS: { key: NoticeType; label: string; icon: React.ElementType; color: string }[] = [ + { key: 'info', label: '정보', icon: Info, color: 'text-blue-400' }, + { key: 'warning', label: '경고', icon: AlertTriangle, color: 'text-yellow-400' }, + { key: 'urgent', label: '긴급', icon: Bell, color: 'text-red-400' }, + { key: 'maintenance', label: '점검', icon: Megaphone, color: 'text-orange-400' }, +]; + +const DISPLAY_OPTIONS: { key: NoticeDisplay; label: string; icon: React.ElementType }[] = [ + { key: 'banner', label: '배너', icon: Monitor }, + { key: 'popup', label: '팝업', icon: MessageSquare }, + { key: 'toast', label: '토스트', icon: Bell }, +]; + +const ROLE_OPTIONS = ['ADMIN', 'OPERATOR', 'ANALYST', 'FIELD', 'VIEWER']; + +export function NoticeManagement() { + const { t } = useTranslation('admin'); + const [notices, setNotices] = useState(INITIAL_NOTICES); + const [editingId, setEditingId] = useState(null); + const [showForm, setShowForm] = useState(false); + const [form, setForm] = useState({ + id: '', type: 'info', display: 'banner', title: '', message: '', + startDate: new Date().toISOString().slice(0, 10), + endDate: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10), + targetRoles: [], dismissible: true, pinned: false, + }); + + const now = new Date().toISOString().slice(0, 10); + + const openNew = () => { + setForm({ + id: `N-${String(notices.length + 1).padStart(3, '0')}`, + type: 'info', display: 'banner', title: '', message: '', + startDate: now, + endDate: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10), + targetRoles: [], dismissible: true, pinned: false, + }); + setEditingId(null); + setShowForm(true); + }; + + const openEdit = (notice: SystemNotice) => { + setForm({ ...notice }); + setEditingId(notice.id); + setShowForm(true); + }; + + const handleSave = () => { + if (editingId) { + setNotices((prev) => prev.map((n) => n.id === editingId ? form : n)); + } else { + setNotices((prev) => [form, ...prev]); + } + setShowForm(false); + }; + + const handleDelete = (id: string) => { + setNotices((prev) => prev.filter((n) => n.id !== id)); + }; + + const toggleRole = (role: string) => { + setForm((prev) => ({ + ...prev, + targetRoles: prev.targetRoles.includes(role) + ? prev.targetRoles.filter((r) => r !== role) + : [...prev.targetRoles, role], + })); + }; + + const getStatus = (n: SystemNotice) => { + if (n.endDate < now) return { label: '종료', color: 'bg-muted text-muted-foreground' }; + if (n.startDate > now) return { label: '예약', color: 'bg-blue-500/20 text-blue-400' }; + return { label: '노출 중', color: 'bg-green-500/20 text-green-400' }; + }; + + // KPI + const activeCount = notices.filter((n) => n.startDate <= now && n.endDate >= now).length; + const scheduledCount = notices.filter((n) => n.startDate > now).length; + const urgentCount = notices.filter((n) => n.type === 'urgent' && n.startDate <= now && n.endDate >= now).length; + + return ( +
+ {/* 헤더 */} +
+
+

+ + {t('notices.title')} +

+

+ {t('notices.desc')} +

+
+ +
+ + {/* KPI — 가로 한 줄 */} +
+ {[ + { label: '전체 알림', count: notices.length, icon: Bell, color: 'text-label', bg: 'bg-muted' }, + { label: '현재 노출 중', count: activeCount, icon: Eye, color: 'text-green-400', bg: 'bg-green-500/10' }, + { label: '예약됨', count: scheduledCount, icon: Clock, color: 'text-blue-400', bg: 'bg-blue-500/10' }, + { label: '긴급 알림', count: urgentCount, icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-500/10' }, + ].map((kpi) => ( +
+
+ +
+ {kpi.count} + {kpi.label} +
+ ))} +
+ + {/* 알림 목록 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + {notices.map((n) => { + const status = getStatus(n); + const typeOpt = TYPE_OPTIONS.find((t) => t.key === n.type)!; + const dispOpt = DISPLAY_OPTIONS.find((d) => d.key === n.display)!; + return ( + + + + + + + + + + + ); + })} + +
상태유형표시제목노출기간대상고정관리
+ {status.label} + + + + {typeOpt.label} + + + + + {dispOpt.label} + + + {n.title} + {n.message.slice(0, 50)}… + + {n.startDate.slice(5)} ~ {n.endDate.slice(5)} + + {n.targetRoles.length === 0 ? ( + 전체 + ) : ( + {n.targetRoles.join(' · ')} + )} + + {n.pinned && } + +
+ + +
+
+
+
+ + {/* ── 등록/수정 폼 모달 ── */} + {showForm && ( +
+
+
+ + {editingId ? '알림 수정' : '새 알림 등록'} + + +
+ +
+ {/* 제목 */} +
+ + setForm({ ...form, title: e.target.value })} + className="w-full bg-surface-overlay border border-slate-700/50 rounded-lg px-3 py-2 text-[11px] text-heading placeholder:text-hint focus:outline-none focus:border-blue-500/50" + placeholder="알림 제목 입력" + /> +
+ + {/* 내용 */} +
+ +