refactor: 민간화 + 팀 프로젝트 구조 전환

- 해경 관련 코드/에셋 정리 (KCGV, 해경관할구역 FGB, PatrolShipSelector)
- 위성/기상/퍼블리시/레거시 모듈 전체 삭제
- STOMP WebSocket → AIS Target API HTTP 폴링 방식 전환
- 세션 인증 임시 비활성화 (VITE_DEV_SKIP_AUTH)
- 환경변수 민간 데모용으로 재구성
- 팀 워크플로우 v1.2.0 구조 적용 (.claude/rules, skills, settings)
- .githooks, .editorconfig, .node-version 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-02-15 06:13:08 +09:00
부모 086599bb6d
커밋 ac3c204843
141개의 변경된 파일1133개의 추가작업 그리고 16583개의 파일을 삭제

파일 보기

@ -0,0 +1,69 @@
# TypeScript/React 코드 스타일 규칙
## TypeScript 일반
- strict 모드 필수 (`tsconfig.json`)
- `any` 사용 금지 (불가피한 경우 주석으로 사유 명시)
- 타입 정의: `interface` 우선 (type은 유니온/인터섹션에만)
- 들여쓰기: 2 spaces
- 세미콜론: 사용
- 따옴표: single quote
- trailing comma: 사용
## React 규칙
### 컴포넌트
- 함수형 컴포넌트 + hooks 패턴만 사용
- 클래스 컴포넌트 사용 금지
- 컴포넌트 파일 당 하나의 export default 컴포넌트
- Props 타입은 interface로 정의 (ComponentNameProps)
```tsx
interface UserCardProps {
name: string;
email: string;
onEdit?: () => void;
}
const UserCard = ({ name, email, onEdit }: UserCardProps) => {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
{onEdit && <button onClick={onEdit}>편집</button>}
</div>
);
};
export default UserCard;
```
### Hooks
- 커스텀 훅은 `use` 접두사 (예: `useAuth`, `useFetch`)
- 훅은 `src/hooks/` 디렉토리에 분리
- 복잡한 상태 로직은 커스텀 훅으로 추출
### 상태 관리
- 컴포넌트 로컬 상태: `useState`
- 공유 상태: Context API 또는 Zustand
- 서버 상태: React Query (TanStack Query) 권장
### 이벤트 핸들러
- `handle` 접두사: `handleClick`, `handleSubmit`
- Props로 전달 시 `on` 접두사: `onClick`, `onSubmit`
## 스타일링
- CSS Modules 또는 Tailwind CSS (프로젝트 설정에 따름)
- 인라인 스타일 지양
- !important 사용 금지
## API 호출
- API 호출 로직은 `src/services/`에 분리
- Axios 또는 fetch wrapper 사용
- 에러 처리: try-catch + 사용자 친화적 에러 메시지
- 환경별 API URL은 `.env`에서 관리
## 기타
- console.log 커밋 금지 (디버깅 후 제거)
- 매직 넘버/문자열 → 상수 파일로 추출
- 사용하지 않는 import, 변수 제거 (ESLint로 검증)
- 이미지/아이콘은 `src/assets/`에 관리

파일 보기

@ -0,0 +1,84 @@
# Git 워크플로우 규칙
## 브랜치 전략
### 브랜치 구조
```
main ← 배포 가능한 안정 브랜치 (보호됨)
└── develop ← 개발 통합 브랜치
├── feature/ISSUE-123-기능설명
├── bugfix/ISSUE-456-버그설명
└── hotfix/ISSUE-789-긴급수정
```
### 브랜치 네이밍
- feature 브랜치: `feature/ISSUE-번호-간단설명` (예: `feature/ISSUE-42-user-login`)
- bugfix 브랜치: `bugfix/ISSUE-번호-간단설명`
- hotfix 브랜치: `hotfix/ISSUE-번호-간단설명`
- 이슈 번호가 없는 경우: `feature/간단설명` (예: `feature/add-swagger-docs`)
### 브랜치 규칙
- main, develop 브랜치에 직접 커밋/푸시 금지
- feature 브랜치는 develop에서 분기
- hotfix 브랜치는 main에서 분기
- 머지는 반드시 MR(Merge Request)을 통해 수행
## 커밋 메시지 규칙
### Conventional Commits 형식
```
type(scope): subject
body (선택)
footer (선택)
```
### type (필수)
| type | 설명 |
|------|------|
| feat | 새로운 기능 추가 |
| fix | 버그 수정 |
| docs | 문서 변경 |
| style | 코드 포맷팅 (기능 변경 없음) |
| refactor | 리팩토링 (기능 변경 없음) |
| test | 테스트 추가/수정 |
| chore | 빌드, 설정 변경 |
| ci | CI/CD 설정 변경 |
| perf | 성능 개선 |
### scope (선택)
- 변경 범위를 나타내는 짧은 단어
- 한국어, 영어 모두 허용 (예: `feat(인증): 로그인 기능`, `fix(auth): token refresh`)
### subject (필수)
- 변경 내용을 간결하게 설명
- 한국어, 영어 모두 허용
- 72자 이내
- 마침표(.) 없이 끝냄
### 예시
```
feat(auth): JWT 기반 로그인 구현
fix(배치): 야간 배치 타임아웃 수정
docs: README에 빌드 방법 추가
refactor(user-service): 중복 로직 추출
test(결제): 환불 로직 단위 테스트 추가
chore: Gradle 의존성 버전 업데이트
```
## MR(Merge Request) 규칙
### MR 생성
- 제목: 커밋 메시지와 동일한 Conventional Commits 형식
- 본문: 변경 내용 요약, 테스트 방법, 관련 이슈 번호
- 라벨: 적절한 라벨 부착 (feature, bugfix, hotfix 등)
### MR 리뷰
- 최소 1명의 리뷰어 승인 필수
- CI 검증 통과 필수 (설정된 경우)
- 리뷰 코멘트 모두 해결 후 머지
### MR 머지
- Squash Merge 권장 (깔끔한 히스토리)
- 머지 후 소스 브랜치 삭제

53
.claude/rules/naming.md Normal file
파일 보기

@ -0,0 +1,53 @@
# TypeScript/React 네이밍 규칙
## 파일명
| 항목 | 규칙 | 예시 |
|------|------|------|
| 컴포넌트 | PascalCase | `UserCard.tsx`, `LoginForm.tsx` |
| 페이지 | PascalCase | `Dashboard.tsx`, `UserList.tsx` |
| 훅 | camelCase + use 접두사 | `useAuth.ts`, `useFetch.ts` |
| 서비스 | camelCase | `userService.ts`, `authApi.ts` |
| 유틸리티 | camelCase | `formatDate.ts`, `validation.ts` |
| 타입 정의 | camelCase | `user.types.ts`, `api.types.ts` |
| 상수 | camelCase | `routes.ts`, `constants.ts` |
| 스타일 | 컴포넌트명 + .module | `UserCard.module.css` |
| 테스트 | 대상 + .test | `UserCard.test.tsx` |
## 변수/함수
| 항목 | 규칙 | 예시 |
|------|------|------|
| 변수 | camelCase | `userName`, `isLoading` |
| 함수 | camelCase | `getUserList`, `formatDate` |
| 상수 | UPPER_SNAKE_CASE | `MAX_RETRY`, `API_BASE_URL` |
| boolean 변수 | is/has/can/should 접두사 | `isActive`, `hasPermission` |
| 이벤트 핸들러 | handle 접두사 | `handleClick`, `handleSubmit` |
| 이벤트 Props | on 접두사 | `onClick`, `onSubmit` |
## 타입/인터페이스
| 항목 | 규칙 | 예시 |
|------|------|------|
| interface | PascalCase | `UserProfile`, `ApiResponse` |
| Props | 컴포넌트명 + Props | `UserCardProps`, `ButtonProps` |
| 응답 타입 | 도메인 + Response | `UserResponse`, `LoginResponse` |
| 요청 타입 | 동작 + Request | `CreateUserRequest` |
| Enum | PascalCase | `UserStatus`, `HttpMethod` |
| Enum 값 | UPPER_SNAKE_CASE | `ACTIVE`, `PENDING` |
| Generic | 단일 대문자 | `T`, `K`, `V` |
## 디렉토리
- 모두 kebab-case 또는 camelCase (프로젝트 통일)
- 예: `src/components/common/`, `src/hooks/`, `src/services/`
## 컴포넌트 구조 예시
```
src/components/user-card/
├── UserCard.tsx # 컴포넌트
├── UserCard.module.css # 스타일
├── UserCard.test.tsx # 테스트
└── index.ts # re-export
```

파일 보기

@ -0,0 +1,34 @@
# 팀 정책 (Team Policy)
이 규칙은 조직 전체에 적용되는 필수 정책입니다.
프로젝트별 `.claude/rules/`에 추가 규칙을 정의할 수 있으나, 이 정책을 위반할 수 없습니다.
## 보안 정책
### 금지 행위
- `.env`, `.env.*`, `secrets/` 파일 읽기 및 내용 출력 금지
- 비밀번호, API 키, 토큰 등 민감 정보를 코드에 하드코딩 금지
- `git push --force`, `git reset --hard`, `git clean -fd` 실행 금지
- `rm -rf /`, `rm -rf ~`, `rm -rf .git` 등 파괴적 명령 실행 금지
- main/develop 브랜치에 직접 push 금지 (MR을 통해서만 머지)
### 인증 정보 관리
- 환경변수 또는 외부 설정 파일(`.env`, `application-local.yml`)로 관리
- 설정 파일은 `.gitignore`에 반드시 포함
- 예시 파일(`.env.example`, `application.yml.example`)만 커밋
## 코드 품질 정책
### 필수 검증
- 커밋 전 빌드(컴파일) 성공 확인
- 린트 경고 0개 유지 (CI에서도 검증)
- 테스트 코드가 있는 프로젝트는 테스트 통과 필수
### 코드 리뷰
- main 브랜치 머지 시 최소 1명 리뷰 필수
- 리뷰어 승인 없이 머지 불가
## 문서화 정책
- 공개 API(controller endpoint)에는 반드시 설명 주석 작성
- 복잡한 비즈니스 로직에는 의도를 설명하는 주석 작성
- README.md에 프로젝트 빌드/실행 방법 유지

64
.claude/rules/testing.md Normal file
파일 보기

@ -0,0 +1,64 @@
# TypeScript/React 테스트 규칙
## 테스트 프레임워크
- Vitest (Vite 프로젝트) 또는 Jest
- React Testing Library (컴포넌트 테스트)
- MSW (Mock Service Worker, API 모킹)
## 테스트 구조
### 단위 테스트
- 유틸리티 함수, 커스텀 훅 테스트
- 외부 의존성 없이 순수 로직 검증
```typescript
describe('formatDate', () => {
it('날짜를 YYYY-MM-DD 형식으로 변환한다', () => {
const result = formatDate(new Date('2026-02-14'));
expect(result).toBe('2026-02-14');
});
it('유효하지 않은 날짜는 빈 문자열을 반환한다', () => {
const result = formatDate(new Date('invalid'));
expect(result).toBe('');
});
});
```
### 컴포넌트 테스트
- React Testing Library 사용
- 사용자 관점에서 테스트 (구현 세부사항이 아닌 동작 테스트)
- `getByRole`, `getByText` 등 접근성 기반 쿼리 우선
```tsx
describe('UserCard', () => {
it('사용자 이름과 이메일을 표시한다', () => {
render(<UserCard name="홍길동" email="hong@test.com" />);
expect(screen.getByText('홍길동')).toBeInTheDocument();
expect(screen.getByText('hong@test.com')).toBeInTheDocument();
});
it('편집 버튼 클릭 시 onEdit 콜백을 호출한다', async () => {
const onEdit = vi.fn();
render(<UserCard name="홍길동" email="hong@test.com" onEdit={onEdit} />);
await userEvent.click(screen.getByRole('button', { name: '편집' }));
expect(onEdit).toHaveBeenCalledOnce();
});
});
```
### 테스트 패턴
- **Arrange-Act-Assert** 구조
- 테스트 설명은 한국어로 작성 (`it('사용자 이름을 표시한다')`)
- 하나의 테스트에 하나의 검증
## 테스트 커버리지
- 새로 작성하는 유틸리티 함수: 테스트 필수
- 컴포넌트: 주요 상호작용 테스트 권장
- API 호출: MSW로 모킹하여 에러/성공 시나리오 테스트
## 금지 사항
- 구현 세부사항 테스트 금지 (state 값 직접 확인 등)
- `getByTestId` 남용 금지 (접근성 쿼리 우선)
- 스냅샷 테스트 남용 금지 (변경에 취약)
- `setTimeout`으로 비동기 대기 금지 → `waitFor`, `findBy` 사용

78
.claude/settings.json Normal file
파일 보기

@ -0,0 +1,78 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(yarn *)",
"Bash(npm *)",
"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 reset --hard*)",
"Bash(git clean -fd*)",
"Bash(git checkout -- .)",
"Bash(rm -rf /)",
"Bash(rm -rf ~)",
"Bash(rm -rf .git*)",
"Bash(rm -rf /*)",
"Read(./**/.env*)",
"Read(./**/secrets/**)"
]
},
"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
}
]
}
]
}
}

파일 보기

@ -0,0 +1,65 @@
---
name: create-mr
description: 현재 브랜치에서 Gitea MR(Merge Request)을 생성합니다
allowed-tools: "Bash, Read, Grep"
argument-hint: "[target-branch: develop|main] (기본: develop)"
---
현재 브랜치의 변경 사항을 기반으로 Gitea에 MR을 생성합니다.
타겟 브랜치: $ARGUMENTS (기본: develop)
## 수행 단계
### 1. 사전 검증
- 현재 브랜치가 main/develop이 아닌지 확인
- 커밋되지 않은 변경 사항 확인 (있으면 경고)
- 리모트에 현재 브랜치가 push되어 있는지 확인 (안 되어 있으면 push)
### 2. 변경 내역 분석
```bash
git log develop..HEAD --oneline
git diff develop..HEAD --stat
```
- 커밋 목록과 변경된 파일 목록 수집
- 주요 변경 사항 요약 작성
### 3. MR 정보 구성
- **제목**: 브랜치의 첫 커밋 메시지 또는 브랜치명에서 추출
- `feature/ISSUE-42-user-login``feat: ISSUE-42 user-login`
- **본문**:
```markdown
## 변경 사항
- (커밋 기반 자동 생성)
## 관련 이슈
- closes #이슈번호 (브랜치명에서 추출)
## 테스트
- [ ] 빌드 성공 확인
- [ ] 기존 테스트 통과
```
### 4. Gitea API로 MR 생성
```bash
# Gitea remote URL에서 owner/repo 추출
REMOTE_URL=$(git remote get-url origin)
# Gitea API 호출
curl -X POST "GITEA_URL/api/v1/repos/{owner}/{repo}/pulls" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"title": "MR 제목",
"body": "MR 본문",
"head": "현재브랜치",
"base": "타겟브랜치"
}'
```
### 5. 결과 출력
- MR URL 출력
- 리뷰어 지정 안내
- 다음 단계: 리뷰 대기 → 승인 → 머지
## 필요 환경변수
- `GITEA_TOKEN`: Gitea API 접근 토큰 (없으면 안내)

파일 보기

@ -0,0 +1,49 @@
---
name: fix-issue
description: Gitea 이슈를 분석하고 수정 브랜치를 생성합니다
allowed-tools: "Bash, Read, Write, Edit, Glob, Grep"
argument-hint: "<issue-number>"
---
Gitea 이슈 #$ARGUMENTS 를 분석하고 수정 작업을 시작합니다.
## 수행 단계
### 1. 이슈 조회
```bash
curl -s "GITEA_URL/api/v1/repos/{owner}/{repo}/issues/$ARGUMENTS" \
-H "Authorization: token ${GITEA_TOKEN}"
```
- 이슈 제목, 본문, 라벨, 담당자 정보 확인
- 이슈 내용을 사용자에게 요약하여 보여줌
### 2. 브랜치 생성
이슈 라벨에 따라 브랜치 타입 결정:
- `bug` 라벨 → `bugfix/ISSUE-번호-설명`
- 그 외 → `feature/ISSUE-번호-설명`
- 긴급 → `hotfix/ISSUE-번호-설명`
```bash
git checkout develop
git pull origin develop
git checkout -b {type}/ISSUE-{number}-{slug}
```
### 3. 이슈 분석
이슈 내용을 바탕으로:
- 관련 파일 탐색 (Grep, Glob 활용)
- 영향 범위 파악
- 수정 방향 제안
### 4. 수정 계획 제시
사용자에게 수정 계획을 보여주고 승인을 받은 후 작업 진행:
- 수정할 파일 목록
- 변경 내용 요약
- 예상 영향
### 5. 작업 완료 후
- 변경 사항 요약
- `/create-mr` 실행 안내
## 필요 환경변수
- `GITEA_TOKEN`: Gitea API 접근 토큰

파일 보기

@ -0,0 +1,90 @@
---
name: init-project
description: 팀 표준 워크플로우로 프로젝트를 초기화합니다
allowed-tools: "Bash, Read, Write, Edit, Glob, Grep"
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/` 디렉토리 존재 여부 확인
### 2. CLAUDE.md 생성
프로젝트 루트에 CLAUDE.md를 생성하고 다음 내용 포함:
- 프로젝트 개요 (이름, 타입, 주요 기술 스택)
- 빌드/실행 명령어 (감지된 빌드 도구 기반)
- 테스트 실행 명령어
- 프로젝트 디렉토리 구조 요약
- 팀 컨벤션 참조 (`.claude/rules/` 안내)
### 3. .claude/ 디렉토리 구성
이미 팀 표준 파일이 존재하면 건너뜀. 없는 경우:
- `.claude/settings.json` — 프로젝트 타입별 표준 권한 설정
- `.claude/rules/` — 팀 규칙 파일 (team-policy, git-workflow, code-style, naming, testing)
- `.claude/skills/` — 팀 스킬 (create-mr, fix-issue, sync-team-workflow)
### 4. Git Hooks 설정
```bash
git config core.hooksPath .githooks
```
`.githooks/` 디렉토리에 실행 권한 부여:
```bash
chmod +x .githooks/*
```
### 5. 프로젝트 타입별 추가 설정
#### 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` 성공 확인
### 6. .gitignore 확인
다음 항목이 .gitignore에 포함되어 있는지 확인하고, 없으면 추가:
```
.claude/settings.local.json
.claude/CLAUDE.local.md
.env
.env.*
*.local
```
### 7. workflow-version.json 생성
`.claude/workflow-version.json` 파일을 생성하여 현재 글로벌 워크플로우 버전 기록:
```json
{
"applied_global_version": "1.0.0",
"applied_date": "현재날짜",
"project_type": "감지된타입"
}
```
### 8. 검증 및 요약
- 생성/수정된 파일 목록 출력
- `git config core.hooksPath` 확인
- 빌드 명령 실행 가능 확인
- 다음 단계 안내 (개발 시작, 첫 커밋 방법 등)

파일 보기

@ -0,0 +1,73 @@
---
name: sync-team-workflow
description: 팀 글로벌 워크플로우를 현재 프로젝트에 동기화합니다
allowed-tools: "Bash, Read, Write, Edit, Glob, Grep"
---
팀 글로벌 워크플로우의 최신 버전을 현재 프로젝트에 적용합니다.
## 수행 절차
### 1. 글로벌 버전 조회
Gitea API로 template-common 리포의 workflow-version.json 조회:
```bash
GITEA_URL=$(python3 -c "import json; print(json.load(open('.claude/workflow-version.json')).get('gitea_url', 'http://211.208.115.83:3000'))" 2>/dev/null || echo "http://211.208.115.83:3000")
curl -sf "${GITEA_URL}/api/v1/repos/gcsc/template-common/raw/workflow-version.json"
```
### 2. 버전 비교
로컬 `.claude/workflow-version.json`과 비교:
- 버전 일치 → "최신 버전입니다" 안내 후 종료
- 버전 불일치 → 미적용 변경 항목 추출하여 표시
### 3. 프로젝트 타입 감지
자동 감지 순서:
1. `.claude/workflow-version.json``project_type` 필드 확인
2. 없으면: `pom.xml` → java-maven, `build.gradle` → java-gradle, `package.json` → react-ts
### 4. 파일 다운로드 및 적용
Gitea API로 해당 타입 + common 템플릿 파일 다운로드:
#### 4-1. 규칙 파일 (덮어쓰기)
팀 규칙은 로컬 수정 불가 — 항상 글로벌 최신으로 교체:
```
.claude/rules/team-policy.md
.claude/rules/git-workflow.md
.claude/rules/code-style.md (타입별)
.claude/rules/naming.md (타입별)
.claude/rules/testing.md (타입별)
```
#### 4-2. settings.json (부분 갱신)
- `deny` 목록: 글로벌 최신으로 교체
- `allow` 목록: 기존 사용자 커스텀 유지 + 글로벌 기본값 병합
- `hooks`: 글로벌 최신으로 교체
#### 4-3. 스킬 파일 (덮어쓰기)
```
.claude/skills/create-mr/SKILL.md
.claude/skills/fix-issue/SKILL.md
.claude/skills/sync-team-workflow/SKILL.md
```
#### 4-4. Git Hooks (덮어쓰기 + 실행 권한)
```bash
chmod +x .githooks/*
```
### 5. 로컬 버전 업데이트
`.claude/workflow-version.json` 갱신:
```json
{
"applied_global_version": "새버전",
"applied_date": "오늘날짜",
"project_type": "감지된타입"
}
```
### 6. 변경 보고
- `git diff`로 변경 내역 확인
- 업데이트된 파일 목록 출력
- 변경 로그(글로벌 workflow-version.json의 changes) 표시
- 필요한 추가 조치 안내 (빌드 확인, 의존성 업데이트 등)

파일 보기

@ -0,0 +1 @@
{"applied_global_version": "1.2.0"}

33
.editorconfig Normal file
파일 보기

@ -0,0 +1,33 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{java,kt}]
indent_style = space
indent_size = 4
[*.{js,jsx,ts,tsx,json,yml,yaml,css,scss,html}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.{sh,bash}]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
[*.{gradle,groovy}]
indent_style = space
indent_size = 4
[*.xml]
indent_style = space
indent_size = 4

35
.env
파일 보기

@ -1,33 +1,16 @@
# ============================================ # ============================================
# 프로덕션 환경 (Production) # 프로덕션 환경 (Production)
# - 빌드: npm run build:prod (또는 npm run build) # - 빌드: yarn build:prod (또는 yarn build)
# - 실제 운영 서버 배포용
# ============================================ # ============================================
# 배포 경로 (서브 경로 배포 시 설정) # 배포 경로
# 반드시 '/'로 시작하고 '/'로 끝나야 함 VITE_BASE_URL=/
VITE_BASE_URL=/kcgnv/
# API 서버 (프록시 타겟) # API 서버 (SNP-Batch API)
VITE_API_URL=https://mda.kcg.go.kr VITE_API_URL=http://211.208.115.83:8041/snp-api
# 지도 타일 서버 # 선박 데이터 쓰로틀링 (ms, 0=무제한)
VITE_MAP_TILE_URL=https://mda.kcg.go.kr VITE_SHIP_THROTTLE=0
# 선박 신호 WebSocket # 인증 우회 (민간 데모)
VITE_SIGNAL_WS=wss://mda.kcg.go.kr/v3/connect VITE_DEV_SKIP_AUTH=true
# 선박 신호 API
VITE_SIGNAL_API=https://mda.kcg.go.kr/signal-api
# 항적 조회 API
VITE_TRACK_API=https://mda.kcg.go.kr
# 항적 조회 WebSocket (STOMP)
VITE_TRACKING_WS=wss://mda.kcg.go.kr/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms, 위성망 대역폭 절약)
VITE_SHIP_THROTTLE=30
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
VITE_MAIN_APP_URL=https://mda.kcg.go.kr

파일 보기

@ -1,32 +0,0 @@
# ============================================
# 개발 서버 배포 환경 (Development Server)
# - 빌드: yarn build:dev (또는 npm run build:dev)
# - 개발 서버 /kcgv 경로 배포용
# ============================================
# 배포 경로 (개발서버 서브 경로)
VITE_BASE_URL=/kcgnv/
# API 서버 (개발서버)
VITE_API_URL=http://10.26.252.39:9090
# 지도 타일 서버
VITE_MAP_TILE_URL=http://10.26.252.39:9090
# 선박 신호 WebSocket
VITE_SIGNAL_WS=ws://10.26.252.39:9090/connect
# 선박 신호 API
VITE_SIGNAL_API=http://10.26.252.39:9090/signal-api
# 항적 조회 API (별도 서버)
VITE_TRACK_API=http://10.26.252.51:8090
# 항적 조회 WebSocket (STOMP)
VITE_TRACKING_WS=ws://10.26.252.51:8090/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms)
VITE_SHIP_THROTTLE=30
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
VITE_MAIN_APP_URL=http://10.26.252.39:9090

파일 보기

@ -1,36 +1,16 @@
# ============================================ # ============================================
# 로컬 개발 환경 (Local Development) # 로컬 개발 환경 (Local Development)
# - 서버: yarn dev # - 서버: yarn dev
# - 로컬 개발 전용
# ============================================ # ============================================
# 배포 경로 (프록시 모드: localhost:9090/kcgnv/ → localhost:3000/kcgnv/) # 배포 경로
VITE_BASE_URL=/kcgnv/ VITE_BASE_URL=/
# API 서버 (프록시 타겟) # API 서버 (SNP-Batch API)
VITE_API_URL=http://10.26.252.39:9090 VITE_API_URL=http://211.208.115.83:8041/snp-api
# 지도 타일 서버
VITE_MAP_TILE_URL=http://10.26.252.39:9090
# 선박 신호 WebSocket
VITE_SIGNAL_WS=ws://10.26.252.39:9090/connect
# 선박 신호 API
VITE_SIGNAL_API=http://10.26.252.39:9090/signal-api
# 항적 조회 API (별도 서버)
VITE_TRACK_API=http://10.26.252.51:8090
# 항적 조회 WebSocket (STOMP)
VITE_TRACKING_WS=ws://10.26.252.51:8090/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms, 0=무제한) # 선박 데이터 쓰로틀링 (ms, 0=무제한)
VITE_SHIP_THROTTLE=0 VITE_SHIP_THROTTLE=0
# 메인 프로젝트 URL (세션 만료 시 리다이렉트) # 인증 우회 (민간 데모)
VITE_MAIN_APP_URL=http://localhost:9090 VITE_DEV_SKIP_AUTH=true
# 로컬 개발 인증 우회 (포트가 달라 localStorage 공유 불가)
# true면 세션 없을 때 모의 사용자로 자동 로그인
VITE_DEV_SKIP_AUTH=false

32
.env.qa
파일 보기

@ -1,32 +0,0 @@
# ============================================
# QA 환경 (Quality Assurance)
# - 빌드: npm run build:qa
# - QA/스테이징 서버 배포용
# ============================================
# 배포 경로 (QA 환경 서브 경로)
VITE_BASE_URL=/kcgv/
# API 서버 (QA 서버)
VITE_API_URL=http://10.188.141.123:9090
# 지도 타일 서버
VITE_MAP_TILE_URL=http://10.188.141.123:9090
# 선박 신호 WebSocket (프로덕션 서버 사용)
VITE_SIGNAL_WS=wss://mda.kcg.go.kr/v3/connect
# 선박 신호 API (프로덕션 서버 사용)
VITE_SIGNAL_API=https://mda.kcg.go.kr/signal-api
# 항적 조회 API (QA 서버)
VITE_TRACK_API=http://10.188.141.123:9090
# 항적 조회 WebSocket (QA 서버)
VITE_TRACKING_WS=ws://10.188.141.123:9090/ws-tracks/websocket
# 선박 데이터 쓰로틀링 (ms)
VITE_SHIP_THROTTLE=30
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
VITE_MAIN_APP_URL=http://10.188.141.123:9090

3
.gitattributes vendored Normal file
파일 보기

@ -0,0 +1,3 @@
# 오프라인 캐시 바이너리 보호 (Windows autocrlf 변환 방지)
*.tgz binary

60
.githooks/commit-msg Executable file
파일 보기

@ -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

25
.githooks/post-checkout Executable file
파일 보기

@ -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

36
.githooks/pre-commit Executable file
파일 보기

@ -0,0 +1,36 @@
#!/bin/bash
#==============================================================================
# pre-commit hook (React JavaScript)
# ESLint 검증 — 실패 시 커밋 차단
#==============================================================================
# npm 확인
if ! command -v npx &>/dev/null; then
echo "경고: npx가 설치되지 않았습니다. 검증을 건너뜁니다."
exit 0
fi
# node_modules 확인
if [ ! -d "node_modules" ]; then
echo "경고: node_modules가 없습니다. 'yarn install' 실행 후 다시 시도하세요."
exit 0
fi
# ESLint 검증 (설정 파일이 있는 경우만)
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then
echo "pre-commit: ESLint 검증 중..."
npx eslint src/ --ext .js,.jsx --quiet 2>&1
LINT_RESULT=$?
if [ $LINT_RESULT -ne 0 ]; then
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ ESLint 에러! 커밋이 차단되었습니다. ║"
echo "║ 'npx eslint src/ --fix'로 자동 수정을 시도해보세요. ║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
exit 1
fi
echo "pre-commit: ESLint 통과"
fi

36
.gitignore vendored
파일 보기

@ -2,18 +2,13 @@
node_modules node_modules
package-lock.json package-lock.json
# Offline cache - git에 포함 (폐쇄망 배포용)
!.yarn-offline-cache/
docs
# Build # Build
dist/ dist/
build/ build/
# Environment # Environment (production secrets)
.env .env.local
.env.* .env.*.local
# IDE # IDE
.idea/ .idea/
@ -24,25 +19,20 @@ build/
# Logs # Logs
npm-debug.log* npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Claude # OS
.claude/ Thumbs.db
ehthumbs.db
Desktop.ini
*.md # Claude Code (개인 파일만 무시, 팀 파일은 추적)
!README.md .claude/settings.local.json
.claude/scripts/
# TypeScript files (메인 프로젝트 참조용, 빌드/커밋 제외) # TypeScript files (메인 프로젝트 참조용, 빌드/커밋 제외)
**/*.ts **/*.ts
**/*.tsx **/*.tsx
# tracking VesselListManager (참조용 전체 제외 - replay 패키지에서 별도 구현) # tracking VesselListManager (참조용)
src/tracking/components/VesselListManager/ src/tracking/components/VesselListManager/
# 단, d.ts 타입 선언 파일은 필요시 포함 가능
# !**/*.d.ts
# Publish 폴더 (퍼블리시 원본 포함, 없어도 빌드 가능)
nul
확인요청.txt
# Build artifacts
*.zip
httpd.conf

1
.node-version Normal file
파일 보기

@ -0,0 +1 @@
24

1
.nvmrc Normal file
파일 보기

@ -0,0 +1 @@
v22.22.0

62
CLAUDE.md Normal file
파일 보기

@ -0,0 +1,62 @@
# Ship GIS - 해양 선박위치정보 GIS 프론트엔드
## 프로젝트 개요
해양 선박위치정보를 지도 위에 실시간으로 시각화하는 GIS 프론트엔드 애플리케이션.
민간용 데모 버전으로, OSM + OpenSeaMap 기반 지도와 AIS API 폴링 방식의 선박 데이터를 사용.
## 기술 스택
- **프레임워크**: React 18 + Vite 5
- **지도 엔진**: OpenLayers 9 + Deck.gl 9 (MapLibre 전환 예정)
- **상태관리**: Zustand 4
- **HTTP**: Axios
- **스타일**: SASS
- **라우팅**: React Router DOM 6
## 빌드 / 실행
```bash
yarn install # 의존성 설치
yarn dev # 로컬 개발 서버 (port 3000)
yarn build # 프로덕션 빌드
```
## 데이터 소스
- **선박 위치**: SNP-Batch API (`/api/ais-target/search`) HTTP 폴링
- 초기: 최근 60분 전체 로드
- 이후: 1분마다 최근 2분 증분 조회
- **인증**: 임시 비활성화 (`VITE_DEV_SKIP_AUTH=true`)
## 환경변수
```env
VITE_BASE_URL=/ # 배포 경로
VITE_API_URL=http://211.208.115.83:8041/snp-api # API 서버
VITE_DEV_SKIP_AUTH=true # 인증 우회
```
## 핵심 디렉토리 구조
```
src/
├── api/ # API 클라이언트 (aisTargetApi, signalApi, trackApi)
├── areaSearch/ # 항적분석 모듈
├── assets/ # 이미지, 아이콘 에셋
├── common/ # STOMP 클라이언트 (비활성화)
├── components/ # 공통 컴포넌트 (layout, ship, map, auth, common)
├── hooks/ # 커스텀 훅 (useShipData, useShipLayer 등)
├── map/ # 지도 컨테이너, 레이어, 측정 도구
├── pages/ # 페이지 (Home, Replay)
├── replay/ # 리플레이 모듈
├── scss/ # 글로벌 SCSS
├── stores/ # Zustand 스토어 (ship, map, auth, tracking 등)
├── tracking/ # 항적조회 모듈
├── types/ # 상수 정의
├── utils/ # 유틸리티
└── workers/ # Web Worker (signalWorker)
```
## Git 저장소
- **Remote**: https://gitea.gc-si.dev/gc/ship-gis.git
- **브랜치**: main (보호), develop (작업 브랜치)
## 팀 워크플로우
- 버전: v1.2.0
- 커밋 형식: Conventional Commits (한/영 혼용)
- 브랜치 전략: main ← develop ← feature/*

파일 보기

@ -1,78 +0,0 @@
@echo off
chcp 65001 >nul
echo ============================================
echo dark 프로젝트 - 폐쇄망 Windows 초기 세팅
echo ============================================
echo.
:: 1. 사전 조건 확인
echo [1/4] 사전 조건 확인 중...
where node >nul 2>&1
if %errorlevel% neq 0 (
echo [오류] Node.js가 설치되어 있지 않습니다.
echo Node.js 18 이상을 설치해주세요.
pause
exit /b 1
)
where yarn >nul 2>&1
if %errorlevel% neq 0 (
echo [오류] Yarn이 설치되어 있지 않습니다.
echo npm install -g yarn 으로 설치해주세요.
pause
exit /b 1
)
for /f "tokens=*" %%i in ('node -v') do set NODE_VER=%%i
for /f "tokens=*" %%i in ('yarn -v') do set YARN_VER=%%i
echo Node.js: %NODE_VER%
echo Yarn: %YARN_VER%
echo [확인 완료]
echo.
:: 2. 기존 node_modules 정리
echo [2/4] 기존 node_modules 정리 중...
if exist node_modules (
rmdir /s /q node_modules
echo 기존 node_modules 삭제 완료
) else (
echo node_modules 없음 (정상)
)
echo.
:: 3. 오프라인 캐시에서 의존성 설치
echo [3/4] 오프라인 캐시에서 의존성 설치 중...
echo (.yarn-offline-cache 폴더 사용)
yarn install --offline
if %errorlevel% neq 0 (
echo.
echo [오류] yarn install 실패.
echo .yarn-offline-cache 폴더가 존재하는지 확인해주세요.
echo 폴더가 없으면 인터넷 가능 환경에서 프로젝트를 다시 받아주세요.
pause
exit /b 1
)
echo [설치 완료]
echo.
:: 4. 설치 검증
echo [4/4] 설치 검증 중...
if not exist node_modules\.bin\vite.cmd (
echo [경고] vite.cmd가 생성되지 않았습니다.
echo yarn install이 정상 완료되었는지 확인해주세요.
pause
exit /b 1
)
echo vite.cmd 확인 완료
echo.
echo ============================================
echo 세팅 완료!
echo ============================================
echo.
echo 사용 가능한 명령어:
echo yarn dev - 로컬 개발 서버 (localhost:3000)
echo yarn build:dev - 개발서버 배포 빌드 (BASE_URL=/kcgv/)
echo yarn build:qa - QA서버 빌드
echo yarn build - 프로덕션 빌드
echo.
pause

파일 보기

@ -1,53 +1,17 @@
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// -
import MainLayout from './components/layout/MainLayout'; import MainLayout from './components/layout/MainLayout';
import SessionGuard from './components/auth/SessionGuard'; import SessionGuard from './components/auth/SessionGuard';
import { ToastContainer } from './components/common/Toast'; import { ToastContainer } from './components/common/Toast';
import { AlertModalContainer } from './components/common/AlertModal'; import { AlertModalContainer } from './components/common/AlertModal';
// ( )
// tree-shaking
const PublishRouter = import.meta.env.DEV
? lazy(() =>
import('./publish').catch(() => ({
default: () => (
<div style={{ color: '#fff', padding: '2rem' }}>
publish 폴더가 없습니다. 퍼블리시 파일을 추가하면 자동으로 활성화됩니다.
</div>
),
}))
)
: null;
export default function App() { export default function App() {
return ( return (
<SessionGuard> <SessionGuard>
<ToastContainer /> <ToastContainer />
<AlertModalContainer /> <AlertModalContainer />
<Routes> <Routes>
{/* =====================
구현 영역 (메인)
- 모든 메뉴 경로를 MainLayout으로 처리
===================== */}
<Route path="/*" element={<MainLayout />} /> <Route path="/*" element={<MainLayout />} />
{/* =====================
퍼블리시 영역 (개발 환경 전용)
/publish/* 접근하여 퍼블리시 결과물 미리보기
프로덕션 빌드 라우트와 관련 모듈이 제외됨
===================== */}
{/*{import.meta.env.DEV && PublishRouter && (*/}
{/* <Route*/}
{/* path="/publish/*"*/}
{/* element={*/}
{/* <Suspense fallback={<div style={{ color: '#fff', padding: '2rem' }}>Loading publish...</div>}>*/}
{/* <PublishRouter />*/}
{/* </Suspense>*/}
{/* }*/}
{/* />*/}
{/*)}*/}
</Routes> </Routes>
</SessionGuard> </SessionGuard>
); );

134
src/api/aisTargetApi.js Normal file
파일 보기

@ -0,0 +1,134 @@
/**
* AIS Target API 클라이언트
* SNP-Batch 서버의 AIS 데이터를 HTTP 폴링으로 조회
*/
import axios from 'axios';
const BASE_URL = import.meta.env.VITE_API_URL || '';
/**
* AIS 타겟 검색 (최근 N분 데이터)
* @param {number} minutes - 조회 기간 ()
* @returns {Promise<Array>} AIS 타겟 데이터 배열
*/
export async function searchAisTargets(minutes = 60) {
const res = await axios.get(`${BASE_URL}/api/ais-target/search`, {
params: { minutes },
timeout: 30000,
});
return res.data.data || [];
}
/**
* AIS API 응답 shipStore feature 객체로 변환
*
* API 응답 필드:
* mmsi, imo, name, callsign, vesselType, lat, lon,
* heading, sog, cog, rot, length, width, draught,
* destination, eta, status, messageTimestamp, receivedDate,
* source, classType
*
* @param {Object} aisTarget - API 응답 단건
* @returns {Object} shipStore 호환 feature 객체
*/
export function aisTargetToFeature(aisTarget) {
const mmsi = String(aisTarget.mmsi || '');
const signalKindCode = mapVesselTypeToKindCode(aisTarget.vesselType);
return {
// 고유 식별자 (AIS 신호원 코드 + MMSI)
featureId: `000001${mmsi}`,
// 기본 식별 정보
targetId: mmsi,
originalTargetId: mmsi,
signalSourceCode: '000001', // AIS
shipName: aisTarget.name || '',
shipType: aisTarget.vesselType || '',
// 위치 정보
longitude: aisTarget.lon || 0,
latitude: aisTarget.lat || 0,
// 항해 정보
sog: aisTarget.sog || 0,
cog: aisTarget.cog || 0,
// 시간 정보
receivedTime: formatTimestamp(aisTarget.messageTimestamp),
// 선종 코드
signalKindCode,
// 상태 플래그
lost: false,
integrate: false,
isPriority: true,
// 위험물 카테고리
hazardousCategory: '',
// 국적 코드
nationalCode: '',
// IMO 번호
imo: String(aisTarget.imo || ''),
// 흘수
draught: String(aisTarget.draught || ''),
// 선박 크기
dimA: '',
dimB: '',
dimC: '',
dimD: '',
// AVETDR 신호장비 플래그
ais: '1',
vpass: '',
enav: '',
vtsAis: '',
dMfHf: '',
vtsRadar: '',
// 추가 메타데이터
callsign: aisTarget.callsign || '',
heading: aisTarget.heading || 0,
destination: aisTarget.destination || '',
status: aisTarget.status || '',
length: aisTarget.length || 0,
width: aisTarget.width || 0,
_raw: null,
};
}
/**
* vesselType 문자열 선종 코드 매핑
*/
function mapVesselTypeToKindCode(vesselType) {
if (!vesselType) return '000027'; // 일반
const vt = vesselType.toLowerCase();
if (vt.includes('fishing')) return '000020'; // 어선
if (vt.includes('passenger')) return '000022'; // 여객선
if (vt.includes('cargo')) return '000023'; // 화물선
if (vt.includes('tanker')) return '000024'; // 유조선
if (vt.includes('military') || vt.includes('law enforcement')) return '000025'; // 관공선
if (vt.includes('tug') || vt.includes('pilot') || vt.includes('search')) return '000025'; // 관공선
return '000027'; // 일반
}
/**
* ISO 타임스탬프 "YYYYMMDDHHmmss" 형식 변환
*/
function formatTimestamp(isoString) {
if (!isoString) return '';
try {
const d = new Date(isoString);
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
} catch {
return '';
}
}

파일 보기

@ -1,482 +0,0 @@
/**
* 위성 API
*/
import { fetchWithAuth } from './fetchWithAuth';
const SATELLITE_VIDEO_SEARCH_ENDPOINT = '/api/gis/satlit/search';
const SATELLITE_CSV_ENDPOINT = '/api/gis/satlit/excelToJson';
const SATELLITE_DETAIL_ENDPOINT = '/api/gis/satlit/id/search';
const SATELLITE_UPDATE_ENDPOINT = '/api/gis/satlit/update';
const SATELLITE_COMPANY_LIST_ENDPOINT = '/api/gis/satlit/sat-bz/all/search';
const SATELLITE_MANAGE_LIST_ENDPOINT = '/api/gis/satlit/sat-mng/bz/search';
const SATELLITE_SAVE_ENDPOINT = '/api/gis/satlit/save';
const SATELLITE_COMPANY_SEARCH_ENDPOINT = '/api/gis/satlit/sat-bz/search';
const SATELLITE_COMPANY_SAVE_ENDPOINT = '/api/gis/satlit/sat-bz/save';
const SATELLITE_COMPANY_DETAIL_ENDPOINT = '/api/gis/satlit/sat-bz/id/search';
const SATELLITE_COMPANY_UPDATE_ENDPOINT = '/api/gis/satlit/sat-bz/update';
const SATELLITE_MANAGE_SEARCH_ENDPOINT = '/api/gis/satlit/sat-mng/search';
const SATELLITE_MANAGE_SAVE_ENDPOINT = '/api/gis/satlit/sat-mng/save';
const SATELLITE_MANAGE_DETAIL_ENDPOINT = '/api/gis/satlit/sat-mng/id/search';
const SATELLITE_MANAGE_UPDATE_ENDPOINT = '/api/gis/satlit/sat-mng/update';
/**
* 위성영상 목록 조회
*
* @param {Object} params
* @param {number} params.page - 페이지 번호
* @param {string} [params.startDate] - 촬영 시작일
* @param {string} [params.endDate] - 촬영 종료일
* @param {string} [params.satelliteVideoName] - 위성영상명
* @param {string} [params.satelliteVideoTransmissionCycle] - 전송주기
* @param {string} [params.satelliteVideoKind] - 영상 종류
* @param {string} [params.satelliteVideoOrbit] - 위성 궤도
* @param {string} [params.satelliteVideoOrigin] - 영상 출처
* @returns {Promise<{ list: Array, totalPage: number }>}
*/
export async function fetchSatelliteVideoList({
page,
startDate,
endDate,
satelliteVideoName,
satelliteVideoTransmissionCycle,
satelliteVideoKind,
satelliteVideoOrbit,
satelliteVideoOrigin,
}) {
try {
const response = await fetchWithAuth(SATELLITE_VIDEO_SEARCH_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
page,
startDate,
endDate,
satelliteVideoName,
satelliteVideoTransmissionCycle,
satelliteVideoKind,
satelliteVideoOrbit,
satelliteVideoOrigin,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return {
list: result?.satelliteVideoInfoList || [],
totalPage: result?.totalPage || 0,
};
} catch (error) {
console.error('[fetchSatelliteVideoList] Error:', error);
throw error;
}
}
/**
* 위성영상 CSV JSON 변환 (선박 좌표 추출)
*
* @param {string} csvFileName - CSV 파일명
* @returns {Promise<Array<{ coordinates: [number, number] }>>}
*/
export async function fetchSatelliteCsvFeatures(csvFileName) {
try {
const response = await fetchWithAuth(SATELLITE_CSV_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ csvFileName }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
const data = result?.jsonData;
if (!data) return [];
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
return parsed.map(({ lon, lat }) => ({
coordinates: [parseFloat(lon), parseFloat(lat)],
}));
} catch (error) {
console.error('[fetchSatelliteCsvFeatures] Error:', error);
throw error;
}
}
/**
* 위성영상 상세조회
*
* @param {number} satelliteId - 위성 ID
* @returns {Promise<Object>} SatelliteVideoInfoOneDto
*/
export async function fetchSatelliteVideoDetail(satelliteId) {
try {
const response = await fetchWithAuth(SATELLITE_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ satelliteId }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.satelliteVideoInfoById || null;
} catch (error) {
console.error('[fetchSatelliteVideoDetail] Error:', error);
throw error;
}
}
/**
* 위성영상 수정
*
* @param {Object} params
* @param {number} params.satelliteId
* @param {number} params.satelliteManageId
* @param {string} [params.photographDate]
* @param {string} [params.satelliteVideoName]
* @param {string} [params.satelliteVideoTransmissionCycle]
* @param {string} [params.satelliteVideoKind]
* @param {string} [params.satelliteVideoOrbit]
* @param {string} [params.satelliteVideoOrigin]
* @param {string} [params.photographPurpose]
* @param {string} [params.photographMode]
* @param {string} [params.purchaseCode]
* @param {number} [params.purchasePrice]
*/
export async function updateSatelliteVideo(params) {
try {
const response = await fetchWithAuth(SATELLITE_UPDATE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('[updateSatelliteVideo] Error:', error);
throw error;
}
}
/**
* 사업자 목록 조회
*
* @returns {Promise<Array<{ companyNo: number, companyName: string }>>}
*/
export async function fetchSatelliteCompanyList() {
try {
const response = await fetchWithAuth(SATELLITE_COMPANY_LIST_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.satelliteCompanyNameList || [];
} catch (error) {
console.error('[fetchSatelliteCompanyList] Error:', error);
throw error;
}
}
/**
* 사업자별 위성명 목록 조회
*
* @param {number} companyNo - 사업자 번호
* @returns {Promise<Array<{ satelliteManageId: number, satelliteName: string }>>}
*/
export async function fetchSatelliteManageList(companyNo) {
try {
const response = await fetchWithAuth(SATELLITE_MANAGE_LIST_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ companyNo }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.satelliteManageInfoList || [];
} catch (error) {
console.error('[fetchSatelliteManageList] Error:', error);
throw error;
}
}
/**
* 위성영상 등록 (multipart/form-data)
*
* @param {FormData} formData - 파일(tifFile, csvFile, cloudMaskFile) + 필드
*/
export async function saveSatelliteVideo(formData) {
try {
const response = await fetchWithAuth(SATELLITE_SAVE_ENDPOINT, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('[saveSatelliteVideo] Error:', error);
throw error;
}
}
/**
* 위성 사업자 목록 검색
*
* @param {Object} params
* @param {string} [params.companyTypeCode] - 사업자 분류 코드
* @param {string} [params.companyName] - 사업자명
* @returns {Promise<{ list: Array, totalPage: number }>}
*/
export async function searchSatelliteCompany({ companyTypeCode, companyName, page, limit }) {
try {
const response = await fetchWithAuth(SATELLITE_COMPANY_SEARCH_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ companyTypeCode, companyName, page, limit }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return {
list: result?.satelliteCompanySearchList || [],
totalPage: result?.totalPage || 0,
};
} catch (error) {
console.error('[searchSatelliteCompany] Error:', error);
throw error;
}
}
/**
* 위성 사업자 등록
*
* @param {Object} params
* @param {string} params.companyTypeCode - 사업자 분류 코드
* @param {string} params.companyName - 사업자명
* @param {string} params.nationalCode - 국가코드
* @param {string} [params.location] - 소재지
* @param {string} [params.companyDetail] - 상세내역
*/
export async function saveSatelliteCompany(params) {
try {
const response = await fetchWithAuth(SATELLITE_COMPANY_SAVE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('[saveSatelliteCompany] Error:', error);
throw error;
}
}
/**
* 위성 사업자 상세조회
*
* @param {number} companyNo - 사업자 번호
* @returns {Promise<Object>} SatelliteCompanySearchDto
*/
export async function fetchSatelliteCompanyDetail(companyNo) {
try {
const response = await fetchWithAuth(SATELLITE_COMPANY_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ companyNo }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.satelliteCompany || null;
} catch (error) {
console.error('[fetchSatelliteCompanyDetail] Error:', error);
throw error;
}
}
/**
* 위성 사업자 수정
*
* @param {Object} params
* @param {number} params.companyNo - 사업자 번호
* @param {string} params.companyTypeCode - 사업자 분류 코드
* @param {string} params.companyName - 사업자명
* @param {string} params.nationalCode - 국가코드
* @param {string} [params.location] - 소재지
* @param {string} [params.companyDetail] - 상세내역
*/
export async function updateSatelliteCompany(params) {
try {
const response = await fetchWithAuth(SATELLITE_COMPANY_UPDATE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('[updateSatelliteCompany] Error:', error);
throw error;
}
}
/**
* 위성관리 목록 검색
*
* @param {Object} params
* @param {number} [params.companyNo] - 사업자 번호
* @param {string} [params.satelliteName] - 위성명
* @param {string} [params.sensorType] - 센서 타입
* @returns {Promise<{ list: Array, totalPage: number }>}
*/
export async function searchSatelliteManage({ companyNo, satelliteName, sensorType, page, limit }) {
try {
const response = await fetchWithAuth(SATELLITE_MANAGE_SEARCH_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ companyNo, satelliteName, sensorType, page, limit }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return {
list: result?.satelliteManageInfoSearchList || [],
totalPage: result?.totalPage || 0,
};
} catch (error) {
console.error('[searchSatelliteManage] Error:', error);
throw error;
}
}
/**
* 위성 관리 등록
*
* @param {Object} params
* @param {number} params.companyNo - 사업자 번호
* @param {string} params.satelliteName - 위성명
* @param {string} [params.sensorType] - 센서 타입
* @param {string} [params.photoResolution] - 촬영 해상도
* @param {string} [params.frequency] - 주파수
* @param {string} [params.photoDetail] - 상세내역
*/
export async function saveSatelliteManage(params) {
try {
const response = await fetchWithAuth(SATELLITE_MANAGE_SAVE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('[saveSatelliteManage] Error:', error);
throw error;
}
}
/**
* 위성 관리 상세조회
*
* @param {number} satelliteManageId - 위성 관리 ID
* @returns {Promise<Object>} SatelliteManageInfoDto
*/
export async function fetchSatelliteManageDetail(satelliteManageId) {
try {
const response = await fetchWithAuth(SATELLITE_MANAGE_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ satelliteManageId }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.satelliteManageInfo || null;
} catch (error) {
console.error('[fetchSatelliteManageDetail] Error:', error);
throw error;
}
}
/**
* 위성 관리 수정
*
* @param {Object} params
* @param {number} params.satelliteManageId - 위성 관리 ID
* @param {number} params.companyNo - 사업자 번호
* @param {string} params.satelliteName - 위성명
* @param {string} [params.sensorType] - 센서 타입
* @param {string} [params.photoResolution] - 촬영 해상도
* @param {string} [params.frequency] - 주파수
* @param {string} [params.photoDetail] - 상세내역
*/
export async function updateSatelliteManage(params) {
try {
const response = await fetchWithAuth(SATELLITE_MANAGE_UPDATE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('[updateSatelliteManage] Error:', error);
throw error;
}
}

파일 보기

@ -1,310 +0,0 @@
/**
* 기상해양 API
*/
import { fetchWithAuth } from './fetchWithAuth';
const SPECIAL_NEWS_ENDPOINT = '/api/gis/weather/special-news/search';
/**
* 기상특보 목록 조회
*
* @param {Object} params
* @param {string} params.startPresentationDate - 조회 시작일 (e.g. '2026-01-01')
* @param {string} params.endPresentationDate - 조회 종료일 (e.g. '2026-01-31')
* @param {number} params.page - 페이지 번호
* @param {number} params.limit - 페이지당 항목
* @returns {Promise<{ list: Array, totalPage: number }>}
*/
export async function fetchWeatherAlerts({ startPresentationDate, endPresentationDate, page, limit }) {
try {
const response = await fetchWithAuth(SPECIAL_NEWS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
startPresentationDate,
endPresentationDate,
page,
limit,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return {
list: result?.specialNewsDetailList || [],
totalPage: result?.totalPage || 0,
};
} catch (error) {
console.error('[fetchWeatherAlerts] Error:', error);
throw error;
}
}
const TYPHOON_LIST_ENDPOINT = '/api/gis/weather/typhoon/list/search';
const TYPHOON_DETAIL_ENDPOINT = '/api/gis/weather/typhoon/search';
/**
* 태풍 목록 조회
*
* @param {Object} params
* @param {string} params.typhoonBeginningYear - 조회 연도
* @param {string} params.typhoonBeginningMonth - 조회 ( 문자열이면 전체)
* @param {number} params.page - 페이지 번호
* @param {number} params.limit - 페이지당 항목
* @returns {Promise<{ list: Array, totalPage: number }>}
*/
export async function fetchTyphoonList({ typhoonBeginningYear, typhoonBeginningMonth, page, limit }) {
try {
const response = await fetchWithAuth(TYPHOON_LIST_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
typhoonBeginningYear,
typhoonBeginningMonth,
page,
limit,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
const grouped = result?.typhoonList || [];
const list = grouped.flatMap((group) => group.typhoonList || []);
return {
list,
totalPage: result?.totalPage || 0,
};
} catch (error) {
console.error('[fetchTyphoonList] Error:', error);
throw error;
}
}
/**
* 태풍 상세(진행정보) 조회
*
* @param {Object} params
* @param {string} params.typhoonSequence - 태풍 순번
* @param {string} params.year - 연도
* @returns {Promise<Array>}
*/
export async function fetchTyphoonDetail({ typhoonSequence, year }) {
try {
const response = await fetchWithAuth(TYPHOON_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
typhoonSequence,
year,
page: 1,
limit: 10000,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.typhoonSelectDto || [];
} catch (error) {
console.error('[fetchTyphoonDetail] Error:', error);
throw error;
}
}
const TIDE_INFORMATION_ENDPOINT = '/api/gis/weather/tide-information/search';
const SUNRISE_SUNSET_DETAIL_ENDPOINT = '/api/gis/weather/tide-information/observatory/detail/search';
/**
* 조석정보 통합 조회 (조위관측소 + 일출몰관측지역)
*
* @returns {Promise<{ observatories: Array, sunriseSunsets: Array }>}
*/
export async function fetchTideInformation() {
try {
const response = await fetchWithAuth(TIDE_INFORMATION_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return {
observatories: result?.observatorySearchDto || [],
sunriseSunsets: result?.sunriseSunsetSearchDto || [],
};
} catch (error) {
console.error('[fetchTideInformation] Error:', error);
throw error;
}
}
/**
* 일출일몰 상세 조회
*
* @param {Object} params - SunriseSunsetSearchDto
* @param {string} params.locationName - 지역명
* @param {string} params.locationType - 지역 유형
* @param {Object} params.coordinate - 좌표
* @param {boolean} params.isChecked - 체크 여부
* @param {Array} params.locationCoordinates - 좌표 배열
* @returns {Promise<Object|null>} SunriseSunsetSelectDetailDto 또는 null
*/
export async function fetchSunriseSunsetDetail(params) {
try {
const response = await fetchWithAuth(SUNRISE_SUNSET_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.sunriseSunsetSelectDetailDto?.[0] || null;
} catch (error) {
console.error('[fetchSunriseSunsetDetail] Error:', error);
throw error;
}
}
const OBSERVATORY_ENDPOINT = '/api/gis/weather/observatory/search';
const OBSERVATORY_DETAIL_ENDPOINT = '/api/gis/weather/observatory/select/detail/search';
/**
* 관측소 목록 조회
*
* @returns {Promise<Array>} ObservatorySearchDto 배열
*/
export async function fetchObservatoryList() {
try {
const response = await fetchWithAuth(OBSERVATORY_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.dtoList || [];
} catch (error) {
console.error('[fetchObservatoryList] Error:', error);
throw error;
}
}
/**
* 관측소 상세정보 조회
*
* @param {Object} params
* @param {string} params.observatoryId - 관측소 ID
* @param {string} params.toDate - 조회 기준일 (e.g. '2026-02-10')
* @returns {Promise<Object|null>} ObservatorySelectDetailDto 또는 null
*/
const AIRPORT_ENDPOINT = '/api/gis/weather/airport/search';
const AIRPORT_DETAIL_ENDPOINT = '/api/gis/weather/airport/select';
/**
* 공항 목록 조회
*
* @returns {Promise<Array>} AirportSearchDto 배열
*/
export async function fetchAirportList() {
try {
const response = await fetchWithAuth(AIRPORT_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.airportSearchDto || [];
} catch (error) {
console.error('[fetchAirportList] Error:', error);
throw error;
}
}
/**
* 공항 상세정보 조회
*
* @param {Object} params
* @param {string} params.airportId - 공항 ID
* @returns {Promise<Object|null>} AirportSelectDto 또는 null
*/
export async function fetchAirportDetail({ airportId }) {
try {
const response = await fetchWithAuth(AIRPORT_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ airportId }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.airportSelectDto || null;
} catch (error) {
console.error('[fetchAirportDetail] Error:', error);
throw error;
}
}
export async function fetchObservatoryDetail({ observatoryId, toDate }) {
try {
const response = await fetchWithAuth(OBSERVATORY_DETAIL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ observatoryId, toDate }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result?.observatorySelectDetail?.[0] || null;
} catch (error) {
console.error('[fetchObservatoryDetail] Error:', error);
throw error;
}
}

파일 보기

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#8B98C5" d="M0 0h24v24H0z"/><g stroke="#fff"><path d="M9.904 6.718 10.997 2h2.899l1.073 4.718M6.667 11.875v-5.05h11.541V12"/><path d="M6.8 21.538 4 13.303l8.343-3.865 8.531 3.73-3.146 8.37M12.437 9.704v11.488M4.507 21.698h15.86M8.14 3.92h8.596"/></g></svg>

Before

Width:  |  Height:  |  크기: 343 B

파일 보기

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" fill="none"><path fill="#0029FF" stroke="#000A62" stroke-linejoin="round" d="m16.985 2.84-6.717 25.482 7.299-7.327 7.753 6.853z"/></svg>

Before

Width:  |  Height:  |  크기: 199 B

파일 보기

@ -1,16 +0,0 @@
import HeaderComponent from "./wrap/HeaderComponent";
import MainComponent from "./wrap/MainComponent";
import SideComponent from "./wrap/SideComponent";
import ToolComponent from "./wrap/ToolComponent";
import { Routes, Route} from 'react-router-dom';
export default function WrapComponent(){
return(
<div id="wrap" className="wrap">
<HeaderComponent />
<SideComponent />
<MainComponent />
<ToolComponent />
</div>
)
}

파일 보기

@ -1,35 +0,0 @@
import React, { useState } from 'react';
export default function FileUpload({ label = "파일 선택", inputId, maxLength = 25, placeholder = "선택된 파일 없음" }) {
const [fileName, setFileName] = useState('');
//
const truncateMiddle = (str, maxLen) => {
if (!str) return '';
if (str.length <= maxLen) return str;
const keep = Math.floor((maxLen - 3) / 2);
return str.slice(0, keep) + '...' + str.slice(str.length - keep);
};
const handleChange = (e) => {
const name = e.target.files[0]?.name || '';
setFileName(name);
};
return (
<div className="fileWrap">
<input
type="file"
id={inputId}
className="fileInput"
onChange={handleChange}
/>
<label htmlFor={inputId} className="fileLabel">
{label}
</label>
<span className="fileName">
{fileName ? truncateMiddle(fileName, maxLength) : placeholder}
</span>
</div>
);
}

파일 보기

@ -1,24 +0,0 @@
import { useState } from "react";
function Slider({ label = "", min = 0, max = 100, defaultValue = 50 }) {
const [value, setValue] = useState(defaultValue);
const percent = ((value - min) / (max - min)) * 100;
return (
<label className="rangeWrap">
<span className="rangeLabel">{label}</span>
<input
type="range"
min={min}
max={max}
value={value}
onChange={(e) => setValue(Number(e.target.value))}
style={{ "--percent": `${percent}%` }}
aria-label={label}
/>
</label>
);
}
export default Slider;

파일 보기

@ -1,17 +0,0 @@
import { Link } from "react-router-dom";
export default function HeaderComponent() {
return(
<header id="header">
<div className="logoArea"><Link to="/" className="logo"><span className="blind">GIS 함정용</span></Link> <span className="logoTxt">GIS 함정용</span></div>
<aside>
<ul>
<li><Link to="/" className="alram" title="알람"><i className="badge"></i><span className="blind">알람</span></Link></li>
<li><Link to="/signal/custom" className="set" title="설정"><span className="blind">설정</span></Link></li>
<li><Link to="/mypage" className="user" title="마이페이지"><span className="blind">마이페이지</span></Link></li>
</ul>
</aside>
</header>
)
}

파일 보기

@ -1,51 +0,0 @@
import { Routes, Route } from "react-router-dom";
import TopComponent from "./main/TopComponent"; //
import ShipComponent from "./main/ShipComponent"; //
import Satellite1Component from "./main/Satellite1Component"; //
import Satellite2Component from "./main/Satellite2Component"; //
import Satellite3Component from "./main/Satellite3Component"; //
import Satellite4Component from "./main/Satellite4Component"; //
import WeatherComponent from "./main/WeatherComponent"; //
import Analysis1Component from "./main/Analysis1Component"; // -
import Analysis2Component from "./main/Analysis2Component"; // -
import Analysis3Component from "./main/Analysis3Component"; // -
// import Analysis4Component from "./main/Analysis4Component"; // -
import LayerComponent from "./main/LayerComponent"; //
import SignalComponent from "./main/Signal1Component"; //
import Signal2Component from "./main/Signal2Component"; //
import MyPageComponent from "./main/MyPageComponent"; //
export default function MainComponent() {
return (
<main id="main">
<TopComponent />
<Routes>
{/* 기본 화면 */}
<Route path="*" element={<ShipComponent />} />
<Route path="panel1/ship" element={<ShipComponent />} />
<Route path="panel2/satellite/add" element={<Satellite1Component />} />
<Route path="panel2/satellite/provider" element={<Satellite2Component />} />
<Route path="panel2/satellite/manage" element={<Satellite3Component />} />
<Route path="panel2/satellite/delete" element={<Satellite4Component />} />
<Route path="panel3/weather" element={<WeatherComponent />} />
<Route path="panel4/analysis/area" element={<Analysis1Component />} />
<Route path="panel4/analysis/result" element={<Analysis2Component />} />
<Route path="panel4/analysis/register" element={<Analysis3Component />} />
{/* <Route path="panel4/analysis/trench" element={<Analysis4Component />} /> */}
<Route path="display/layer/register" element={<LayerComponent />} />
<Route path="signal" element={<SignalComponent />} />
<Route path="signal/custom" element={<Signal2Component />} />
<Route path="mypage" element={<MyPageComponent />} />
</Routes>
</main>
);
}

파일 보기

@ -1,106 +0,0 @@
import { useState } from 'react';
import { useNavigate, useLocation, Routes, Route, Navigate } from 'react-router-dom';
import NavComponent from "./side/NavComponent";
import Panel1Component from "./side/Panel1Component"; //
import Panel2Component from "./side/Panel2Component"; //
import Panel3Component from "./side/Panel3Component"; //
import Panel4Component from "./side/Panel4Component"; //
import Panel5Component from "./side/Panel5Component"; //
import Panel6Component from "./side/Panel6Component"; // AI
import Panel7Component from "./side/Panel7Component"; //
import Panel8Component from "./side/Panel8Component"; //
import DisplayComponent from "./side/DisplayComponent"; //
export default function SideComponent() {
const navigate = useNavigate();
const location = useLocation();
/* =========================
패널 열림 상태 (단일 관리)
========================= */
const [isPanelOpen, setIsPanelOpen] = useState(true);
const handleTogglePanel = () => {
setIsPanelOpen(prev => !prev);
};
/* =========================
URL activeKey 매핑
========================= */
const getActiveKey = () => {
const path = location.pathname.split('/')[1];
switch (path) {
case 'panel1': return 'gnb1';
case 'panel2': return 'gnb2';
case 'panel3': return 'gnb3';
case 'panel4': return 'gnb4';
case 'panel5': return 'gnb5';
case 'panel6': return 'gnb6';
case 'panel7': return 'gnb7';
case 'panel8': return 'gnb8';
case 'filter': return 'filter';
case 'layer': return 'layer';
default: return 'gnb1';
}
};
const activeKey = getActiveKey();
/* =========================
네비 클릭 라우트 이동
패널은 닫지 않음
========================= */
const handleChangePanel = (key) => {
//
setIsPanelOpen(true);
switch (key) {
case 'gnb1': navigate('/panel1'); break;
case 'gnb2': navigate('/panel2'); break;
case 'gnb3': navigate('/panel3'); break;
case 'gnb4': navigate('/panel4'); break;
case 'gnb5': navigate('/panel5'); break;
case 'gnb6': navigate('/panel6'); break;
case 'gnb7': navigate('/panel7'); break;
case 'gnb8': navigate('/panel8'); break;
case 'filter': navigate('/filter'); break;
case 'layer': navigate('/layer'); break;
default: navigate('/panel1'); break;
}
};
/* =========================
공통 패널 props
========================= */
const panelProps = {
isOpen: isPanelOpen,
onToggle: handleTogglePanel,
};
return (
<section id="sidePanel">
<NavComponent
activeKey={activeKey}
onChange={handleChangePanel}
/>
<div className="sidePanelContent">
<Routes>
{/* 초기 진입 시 Panel1 */}
<Route index element={<Navigate to="/panel1" replace />} />
<Route path="panel1/*" element={<Panel1Component {...panelProps} />} />
<Route path="panel2/*" element={<Panel2Component {...panelProps} />} />
<Route path="panel3/*" element={<Panel3Component {...panelProps} />} />
<Route path="panel4/*" element={<Panel4Component {...panelProps} />} />
<Route path="panel5/*" element={<Panel5Component {...panelProps} />} />
<Route path="panel6/*" element={<Panel6Component {...panelProps} />} />
<Route path="panel7/*" element={<Panel7Component {...panelProps} />} />
<Route path="panel8/*" element={<Panel8Component {...panelProps} />} />
<Route path="filter/*" element={<DisplayComponent {...panelProps} />} />
<Route path="layer/*" element={<DisplayComponent {...panelProps} />} />
</Routes>
</div>
</section>
);
}

파일 보기

@ -1,146 +0,0 @@
import { useState } from "react"
import useShipStore from '../../stores/shipStore';
const BASE_URL = import.meta.env.BASE_URL;
export default function ToolComponent() {
const [isLegendOpen, setIsLegendOpen] = useState(false);
const { isIntegrate, toggleIntegrate } = useShipStore();
return(
<section id="tool">
{/* 툴바 */}
<div className="toolBar">
<ul className="toolItem space">
<li><button type="button" className="tool01">초기화</button></li>
<li>
<button
type="button"
className={`tool02 ${isIntegrate ? 'active' : ''}`}
onClick={() => {
console.log('[ToolComponent] 선박통합 버튼 클릭, current isIntegrate:', isIntegrate);
toggleIntegrate();
}}
title={isIntegrate ? '선박통합 ON' : '선박통합 OFF'}
>선박통합</button>
</li>
<li><button type="button" className="tool03">구역설정</button></li>
</ul>
<ul className="toolItem mt30">
<li><button type="button" className="tool04">거리</button></li>
<li><button type="button" className="tool05">면적</button></li>
<li><button type="button" className="tool06">거리환</button></li>
</ul>
<ul className="toolItem space mt30">
<li><button type="button" className="tool07">인쇄</button></li>
<li><button type="button" className="tool08">다운로드</button></li>
</ul>
</div>
{/* 맵컨트롤 툴바 */}
<div className="control">
<ul className="toolItem zoom">
<li><button type="button" className="zoomin" title="확대"><span className="blind">확대</span></button></li>
<li className="num">7</li>
<li><button type="button" className="zoomout" title="축소"><span className="blind">축소</span></button></li>
</ul>
<ul className="toolItem space mt30">
<li><button
type="button"
className={`legend ${isLegendOpen ? "active" : ""}`}
onClick={() => setIsLegendOpen(prev => !prev)}
>
범례</button>
</li>
<li><button type="button" className="minimap">미니맵</button></li>
</ul>
</div>
{/* 범례 */}
{isLegendOpen && (
<div className="legendWrap">
<ul className="legendList">
<li className="legendItem">
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_all.svg`} alt="통합" />통합</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_china.svg`} alt="중국어선" />중국어선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_china_permit.svg`} alt="중국어선허가" />중국어선허가</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_japan.svg`} alt="일본어선" />일본어선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_danger.svg`} alt="위험물" />위험물</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_passenger.svg`} alt="여객선" />여객선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_vessel.svg`} alt="함정" />함정</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_vessel_radar.svg`} alt="함정-RADAR" />함정-RADAR</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_general.svg`} alt="일반" />일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_vts_general.svg`} alt="VTS-일반" />VTS-일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_vts_radar.svg`} alt="VTS-RADAR" />VTS-RADAR</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_vpass.svg`} alt="VPASS일반" />VPASS일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_enav_fishing.svg`} alt="ENAV어선" />ENAV어선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_enav_danger.svg`} alt="ENAV위험물" />ENAV위험물</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_enav_cargo.svg`} alt="ENAV화물선" />ENAV화물선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_enav_government.svg`} alt="ENAV관공선" />ENAV관공선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_enav_general.svg`} alt="ENAV일반" />ENAV일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_dmfhf.svg`} alt="D-MF/HF" />D-MF/HF</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_aircraft.svg`} alt="항공기" />항공기</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src={`${BASE_URL}images/ico_legend_nll.svg`} alt="NLL" />NLL</span>
<span className="legendValue">0</span>
</li>
</ul>
</div>
)}
</section>
)
}

파일 보기

@ -1,54 +0,0 @@
import { useState } from 'react';
import { useNavigate } from "react-router-dom";
export default function Analysis1Component() {
const navigate = useNavigate();
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="Analysis2Component">
{/* 위성 영상 등록 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill w46r">
<div className="puHeader">
<span className="title">관심 해역 설정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody p0">
<div className="rowSB gap10">
<button type="button"
className="drawBtn"
onClick={() => navigate("/panel4/analysis/result")}
>
<i className="rect"></i>
사각형 그리기
</button>
<button type="button"
className="drawBtn"
onClick={() => navigate("/panel4/analysis/result")}
>
<i className="polygon"></i>
다각형 그리기
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,132 +0,0 @@
import { useState } from 'react';
export default function Analysis2Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="Analysis2Component">
{/* 위성 영상 등록 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill w61r">
<div className="puHeader">
<span className="title">관심 해역 설정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<div className="rowSB gap10 pb10">
<button type="button" className="drawBtn sm">사각형 그리기<i className="rect"></i></button>
<button type="button" className="drawBtn sm">다각형 그리기<i className="polygon"></i></button>
</div>
<table className="table">
<caption>관심 해역 설정 - 해상영역명, 설정 옵션, 좌표,영역 옵션,해상영역명 크기, 해상영역명 색상,윤곽선 굵기,윤곽선 종류,윤곽선 색상,채우기 색상 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">해상영역명</th>
<td colSpan={3}><input type="text" placeholder="해상영역명" aria-label="해상영역명" /></td>
</tr>
<tr>
<th scope="row">설정 옵션</th>
<td colSpan={3}>
<div className="row">
<label className="checkbox checkL"><input type="checkbox" /><span>사용 여부</span></label>
<label className="checkbox checkL"><input type="checkbox" /><span>알림 여부</span></label>
<label className="checkbox checkL"><input type="checkbox" /><span>공유 여부</span></label>
</div>
</td>
</tr>
<tr>
<th scope="row">좌표</th>
<td colSpan={3}>[124,96891368166156, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]
</td>
</tr>
<tr>
<th scope="row">영역 옵션</th>
<td colSpan={3}>
<div className="row">
<label className="checkbox checkL"><input type="checkbox" /><span>해상영역 표시</span></label>
<label className="checkbox checkL"><input type="checkbox" /><span>해상영역명 표시</span></label>
</div>
</td>
</tr>
<tr>
<th scope="row">해상영역명 크기</th>
<td>
<div className="numInput">
<input type="number" placeholder="0" min="" max="" aria-label="해상영역명 크기" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</td>
<th scope="row">해상영역명 색상</th>
<td><i className="colorBox" style={{ backgroundColor: "#000" }}></i></td>
</tr>
<tr>
<th scope="row">윤곽선 굵기 </th>
<td>
<div className="numInput">
<input type="number" placeholder="0" min="" max="" aria-label="윤곽선 굵기 " />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</td>
<th scope="row">윤곽선 종류 </th>
<td>
<select aria-label="윤곽선 종류 ">
<option value="">선택</option>
<option value="">실선</option>
<option value="">점선</option>
</select>
</td>
</tr>
<tr>
<th scope="row">윤곽선 색상 </th>
<td><i className="colorBox" style={{ backgroundColor: "#FF0000" }}></i></td>
<th scope="row">채우기 색상 </th>
<td><i className="colorBox" style={{ backgroundColor: "#7BEBB1" }}></i></td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button
className="btn dark"
onClick={closePopup}
>
취소
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,106 +0,0 @@
import { useState } from 'react';
export default function Analysis3Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="Analysis2Component">
{/* 위성 영상 등록 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill w61r">
<div className="puHeader">
<span className="title">관심 해역 분석 등록</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<div className="analyRow">
<div className="reg">
<div className="mapCapture"></div>
<table className="table">
<caption>관심 해역 분석 등록 - 제목, 상세 내역, 공유 여부,공유 그룹 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">제목</th>
<td><input type="text" placeholder="제목" aria-label="제목" /></td>
</tr>
<tr>
<th scope="row">상세 내역</th>
<td>
<textarea placeholder="내용을 입력하세요" aria-label="상세내역"></textarea>
</td>
</tr>
<tr>
<th scope="row">공유 여부</th>
<td >
<div className="row">
<label className="radio radioL"> <input type="radio" name="share" /> <span>공유</span></label>
<label className="radio radioL"> <input type="radio" name="share" /> <span>공유 안함</span></label>
</div>
</td>
</tr>
<tr>
<th scope="row">공유 그룹 </th>
<td>
<select aria-label="윤곽선 종류 ">
<option value="">전체</option>
<option value="">부서</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div className="list" >
<div className="tit14">관심영역 목록</div>
<ul className="lineList rowSB">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>진입진출 테스트</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
</ul>
</div>
</div>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button
className="btn dark"
onClick={closePopup}
>
취소
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,94 +0,0 @@
import { useState } from 'react';
import FileUpload from '../../common/FileUpload';
export default function LayerComponent() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="LayerComponent">
{/* 레이어등록 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title">레이어 등록</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<table className="table">
<caption>레이어등록 - 레이어명, 첨부파일, 공유설정 대한 내용을 나타내는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">레이어명 <span className="required">*</span></th>
<td><input type="text" placeholder="" aria-label="레이어명" /></td>
</tr>
<tr>
<th scope="row">첨부파일 <span className="required">*</span></th>
<td>
<div className="rowC">
<FileUpload
label="파일 선택"
inputId="layerFile"
maxLength={35}
placeholder="선택된 파일 없음"
/>
<span className="helpTxt">geojson 파일을 첨부해 주세요. </span>
</div>
</td>
</tr>
<tr>
<th scope="row">공유설정</th>
<td>
<div className="row">
<label className="checkbox checkL w10r">
<input type="checkbox" />
<span>공유 여부</span>
</label>
<label className="flx1">
<span className="blind">공유설정</span>
<select>
<option value="">전체</option>
<option value="">부서</option>
<option value="">개인</option>
<option value="">개인 & 부서</option>
</select>
</label>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button
className="btn dark"
onClick={closePopup}
>
취소
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,158 +0,0 @@
import { useState } from 'react';
export default function MyPageComponent() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
//
const [isPwPopupOpen, setIsPwPopupOpen] = useState(false);
const openPwPopup = () => setIsPwPopupOpen(true);
const closePwPopup = () => setIsPwPopupOpen(false);
//
const [isCertDeleteOpen, setIsCertDeleteOpen] = useState(false);
const openCertDeletePopup = () => setIsCertDeleteOpen(true);
const closeCertDeletePopup = () => setIsCertDeleteOpen(false);
return (
<section id="MyPageComponent">
{/* 내정보조회 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title"> 정보 조회</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<table className="table">
<caption> 정보 조회 - 아이디, 비밀번호, 이름, 이메일, 직급, 상세소속, 공인인증서 삭제 대한 내용을 나타내는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">아이디</th>
<td>admin222</td>
</tr>
<tr scope="row">
<th>비밀번호</th>
<td><button className="btn btnM deep flx0" onClick={openPwPopup}>비밀번호 변경</button></td>
</tr>
<tr scope="row">
<th>이름</th>
<td>ADMIN</td>
</tr>
<tr scope="row">
<th>이메일</th>
<td>123@korea.kr</td>
</tr>
<tr scope="row">
<th>직급</th>
<td>경감</td>
</tr>
<tr scope="row">
<th>상세소속</th>
<td></td>
</tr>
<tr scope="row">
<th>공인인증서 삭제</th>
<td><button className="btn btnM deep flx0" onClick={openCertDeletePopup}>공인인증서 삭제</button></td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button className="btn dark">초기화</button>
</div>
</div>
</div>
</div>
)}
{/* 비밀번호 변경 팝업 */}
{isPwPopupOpen && (
<div className="popupDim">
<div className="popupUtill">
<div className="puHeader">
<span className="title">비밀번호 수정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePwPopup}
/>
</div>
<div className="puBody">
<table className="table">
<caption>비밀번호 수정 - 현재 비밀번호, 비밀번호, 비밀번호 확인 대한 내용을 나타내는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">현재 비밀번호</th>
<td><input type="text" placeholder="" aria-label="현재 비밀번호" /></td>
</tr>
<tr>
<th scope="row"> 비밀번호</th>
<td><input type="password" placeholder="" aria-label="새 비밀번호" /></td>
</tr>
<tr>
<th scope="row"> 비밀번호 확인</th>
<td><input type="password" placeholder="" aria-label="새 비밀번호 확인" /></td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic" onClick={closePwPopup}>수정</button>
<button className="btn dark" onClick={closePwPopup}>취소</button>
</div>
</div>
</div>
</div>
)}
{/* 공인인증서 삭제 팝업 */}
{isCertDeleteOpen && (
<div className="popupDim">
<div className="popupUtill cert">
<div className="puHeader">
<span className="title">공인인증서 삭제</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closeCertDeletePopup}
/>
</div>
<div className="puBody">
<div className="puTxtBox">공인인증서를 삭제 하시겠습니까?</div>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic" onClick={closeCertDeletePopup}>삭제</button>
<button className="btn dark" onClick={closeCertDeletePopup}>취소</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,191 +0,0 @@
import { useState } from 'react';
import FileUpload from '../../common/FileUpload';
export default function Satellite1Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="Satellite1Component">
{/* 위성 영상 등록 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill w61r">
<div className="puHeader">
<span className="title">위성 영상 등록</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<table className="table">
<caption>위성 영상 등록 - 사업자명/위성명, 영상 촬영일, 위성영상파일,CSV 파일,위성영상명, 영상전송 주기,영상 종류,위성 궤도,영상 출처,촬영 목적,촬영 모드,취득방법,구매가격, 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">사업자명/위성명 <span className="required">*</span></th>
<td colSpan={3}>
<div className="row flex1">
<select aria-label="사업자명">
<option value="">BlackSky</option>
<option value="">ICEYE</option>
<option value="">VIIRS</option>
<option value="">hawkeye360</option>
<option value="">test1</option>
<option value="">국토지리정보원</option>
</select>
<input type="text" placeholder="" aria-label="위성명" />
</div>
</td>
</tr>
<tr>
<th scope="row">영상 촬영일 <span className="required">*</span></th>
<td colSpan={3}><input className="dateInput" placeholder="연도-월-일" type="text" aria-label="영상 촬영일" /></td>
</tr>
<tr>
<th scope="row">위성영상파일 <span className="required">*</span></th>
<td colSpan={3}>
<div className="rowC">
<FileUpload
label="파일 선택"
inputId="layerFile"
maxLength={35}
placeholder="선택된 파일 없음"
/>
</div>
</td>
</tr>
<tr>
<th scope="row">CSV 파일 <span className="required">*</span></th>
<td colSpan={3}>
<div className="rowC">
<FileUpload
label="파일 선택"
inputId="layerFile"
maxLength={35}
placeholder="선택된 파일 없음"
/>
</div>
</td>
</tr>
<tr>
<th scope="row">위성영상명 <span className="required">*</span></th>
<td colSpan={3}><input type="text" placeholder="" aria-label="위성영상명" /></td>
</tr>
<tr>
<th scope="row">영상전송 주기 </th>
<td colSpan={3}>
<select aria-label=">영상전송 주기">
<option value="">선택</option>
<option value="">0</option>
<option value="">10</option>
<option value="">30</option>
<option value="">60</option>
</select>
</td>
</tr>
<tr>
<th scope="row">영상 종류 </th>
<td colSpan={3}>
<div className="row">
<label className="radio radioL"> <input type="radio" name="type" /> <span>VIRS</span></label>
<label className="radio radioL"> <input type="radio" name="type" /> <span>ICEYE_SAR</span></label>
<label className="radio radioL"> <input type="radio" name="type" /> <span>광학</span></label>
<label className="radio radioL"> <input type="radio" name="type" /> <span>예약</span></label>
<label className="radio radioL"> <input type="radio" name="type" /> <span>RF</span></label>
</div>
</td>
</tr>
<tr>
<th scope="row">위성 궤도 </th>
<td>
<select aria-label="위성 궤도">
<option value="">선택</option>
<option value="">저궤도</option>
<option value="">중궤도</option>
<option value="">정지궤도</option>
<option value="">기타</option>
</select>
</td>
<th scope="row">영상 출처</th>
<td>
<select aria-label="영상 출처">
<option value="">선택</option>
<option value="">국내/자동</option>
<option value="">국내/수동</option>
<option value="">국외/수동</option>
<option value="">기타</option>
</select>
</td>
</tr>
<tr>
<th scope="row">촬영 목적 </th>
<td>
<input type="text" placeholder="촬영 목적" aria-label="촬영 목적"/>
</td>
<th scope="row">촬영 모드 </th>
<td>
<select aria-label="촬영 모드">
<option value="">선택</option>
<option value="">스핏모드</option>
<option value="">스트랩모드</option>
<option value="">기타</option>
</select>
</td>
</tr>
<tr>
<th scope="row">취득방법 </th>
<td>
<select aria-label="취득방법">
<option value="">선택</option>
<option value="">무료</option>
<option value="">개별구매</option>
<option value="">단가계약</option>
<option value="">연간계약</option>
<option value="">기타</option>
</select>
</td>
<th scope="row">구매가격 </th>
<td>
<div className="numInput">
<input type="number" placeholder="0" min="" max="" aria-label="구매가격" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button
className="btn dark"
onClick={closePopup}
>
취소
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,90 +0,0 @@
import { useState } from 'react';
export default function Satellite2Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="Satellite2Component">
{/* 위성 사업자 등록 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title">위성 사업자 등록</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<table className="table">
<caption>위성 사업자 등록 - 사업자 분류, 사업자명, 국가, 소재지, 상세내역 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">사업자 분류 <span className="required">*</span></th>
<td>
<select aria-label="사업자 분류">
<option value="">전체</option>
<option value="">국가</option>
<option value="">연구기관</option>
<option value="">민간사업자</option>
<option value="">기타</option>
</select>
</td>
</tr>
<tr>
<th scope="row">사업자명 </th>
<td><input type="text" placeholder="사업자명" aria-label="사업자명" /></td>
</tr>
<tr>
<th scope="row">국가 <span className="required">*</span></th>
<td>
<select aria-label="국가">
<option value="">선택</option>
<option value="">대한민국</option>
<option value="">미국</option>
<option value="">일본</option>
<option value="">중국</option>
</select>
</td>
</tr>
<tr>
<th scope="row">소재지 </th>
<td><input type="text" placeholder="소재지" aria-label="소재지" /></td>
</tr>
<tr>
<th scope="row">상세내역 </th>
<td><textarea placeholder="내용을 입력하세요" aria-label="상세내역"></textarea></td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button
className="btn dark"
onClick={closePopup}
>
취소
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,96 +0,0 @@
import { useState } from 'react';
export default function Satellite3Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="Satellite3Component">
{/* 위성 관리 등록 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title">위성 관리 등록</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<table className="table">
<caption>위성 관리 등록 - 사업자명, 위성명, 센서 타입, 촬영 해상도, 주파수, 상세내역 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">사업자명 <span className="required">*</span></th>
<td>
<select aria-label="사업자명">
<option value="">전체</option>
<option value="">BlackSky</option>
<option value="">ICEYE</option>
<option value="">VIIRS</option>
<option value="">hawkeye360</option>
<option value="">test1</option>
<option value="">국토지리정보원</option>
</select>
</td>
</tr>
<tr>
<th scope="row">위성명 <span className="required">*</span></th>
<td><input type="text" placeholder="위성명" aria-label="위성명" /></td>
</tr>
<tr>
<th scope="row">센서 타입 </th>
<td>
<select aria-label="센서 타입">
<option value="">전체</option>
<option value="">광학</option>
<option value="">SAR</option>
<option value="">RF</option>
<option value="">기타</option>
</select>
</td>
</tr>
<tr>
<th scope="row">촬영 해상도 </th>
<td><input type="text" placeholder="촬영 해상도" aria-label="촬영 해상도" /></td>
</tr>
<tr>
<th scope="row">주파수 </th>
<td><input type="text" placeholder="주파수" aria-label="주파수" /></td>
</tr>
<tr>
<th scope="row">상세내역 </th>
<td><textarea placeholder="내용을 입력하세요" aria-label="상세내역"></textarea></td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button
className="btn dark"
onClick={closePopup}
>
취소
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,48 +0,0 @@
import { useState } from 'react';
export default function Satellite4Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="Satellite4Component">
{/* 삭제 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill w46r">
<div className="puHeader">
<span className="title">삭제</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<div className="puTxtBox">삭제 하시겠습니까?</div>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic"
onClick={closePopup}
>삭제</button>
<button
className="btn dark"
onClick={closePopup}
>
취소
</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,162 +0,0 @@
import { useState } from "react";
const BASE_URL = import.meta.env.BASE_URL;
export default function ShipComponent() {
//progress bar value
const [value, setValue] = useState(60);
//
const images = [
{ src: `${BASE_URL}images/photo_ship_001.png`, alt: "1511함A-05" },
{ src: `${BASE_URL}images/photo_ship_002.png`, alt: "1511함A-05" },
];
const [currentIndex, setCurrentIndex] = useState(0);
const handlePrev = () => {
if (currentIndex === 0) return;
setCurrentIndex(prev => prev - 1);
};
const handleNext = () => {
if (currentIndex === images.length - 1) return;
setCurrentIndex(prev => prev + 1);
};
return(
<section id="shipComponent">
{/* 배정보 팝업 */}
<div className="popupMap shipInfo">
{/* header */}
<div className="pmHeader">
<div className="rowL">
<i className="shipType"></i>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
<span className="shipName">1511함A-05</span>
<span className="shipNum">13450135</span>
</div>
<button type="button" className="pmClose" aria-label="닫기"></button>
</div>
<div className="pmGallery">
<button
type="button"
className="navBtn prev"
onClick={handlePrev}
disabled={currentIndex === 0}
>
<span className="blind">이전</span>
</button>
<button
type="button"
className="navBtn next"
onClick={handleNext}
disabled={currentIndex === images.length - 1}
>
<span className="blind">다음</span>
</button>
{/* 이미지 영역 */}
<div className="galleryView">
<img
className="galleryImg"
src={images[currentIndex].src}
alt={images[currentIndex].alt}
/>
</div>
</div>
{/* body */}
<div className="pmBody">
<div className="shipAction">
<div className="rowL">
<button type="button" className="detailBtn">상세정보</button>
<ul className="shipTypeIco">
<li>A</li>
<li>V</li>
<li>E</li>
<li>T</li>
<li>D</li>
<li>R</li>
</ul>
</div>
<button type="button" className="favBtn" aria-label="즐겨찾기"></button>
</div>
<div className="shipRoute">
<div
className="routeProgress"
style={{ "--progress": value }}
>
<progress max="100" value={value}>{value}%</progress>
<span className="routeShip"></span>
</div>
</div>
<ul className="shipStatus">
<li className="port">
<div className="rowL">
<span className="portLabel">출항지</span>
<span className="portName">서귀포해양경찰서</span>
</div>
<div className="rowR">
<span className="portLabel">입항지</span>
<span className="portName">하태도</span>
</div>
</li>
<li className="schedule">
<div className="rowL">
<span className="depart">출항일시</span>
<span className="scheduleDate">2024-11-23 11:23:00</span>
</div>
<div className="rowR">
<span className="arrive">입항일시</span>
<span className="scheduleDate">2024-11-23 11:23:00</span>
</div>
</li>
<li className="status">
<div className="statusItem">
<span className="statusLabel">선박상태</span>
<span className="statusValue">정박</span>
</div>
<div className="statusItem w13r">
<span className="statusLabel">속도/항로</span>
<span className="statusValue">4.2 kn / 13.3˚</span>
</div>
<div className="statusItem">
<span className="statusLabel">흘수</span>
<span className="statusValue">1.1m</span>
</div>
</li>
</ul>
{/* <ul className="shipSensor">
<li>
<span className="sensorLabel">AIS</span>
<span className="sensorValue"><i className="isNomal"></i>정상</span>
</li>
<li>
<span className="sensorLabel">RF</span>
<span className="sensorValue"><i className="isNomal"></i>정상</span>
</li>
<li>
<span className="sensorLabel">EO</span>
<span className="sensorValue"><i className="isNomal"></i>정상</span>
</li>
<li>
<span className="sensorLabel">SAR</span>
<span className="sensorValue"><i className="isOff"></i>비활성</span>
</li>
</ul> */}
<div className="btnWrap">
<button type="button" className="trackBtn">항적조회</button>
<button type="button" className="trackBtn">항로예측</button>
</div>
</div>
{/* footer */}
<div className="pmFooter">데이터 수신시간 : 2024-11-23 11:23:00</div>
</div>
</section>
)
}

파일 보기

@ -1,61 +0,0 @@
import { useState } from 'react';
export default function Signal1Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
return (
<section id="SignalComponent">
{/* 신호설정 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title">신호설정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
<table className="table">
<caption>신호설정 - 신호표출반경, 수신수기 설정 대한 내용을 나타내는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">신호표출반경</th>
<td>
<select aria-label="신호표출반경">
<option value="">25NM</option>
</select>
</td>
</tr>
<tr scope="row">
<th>수신수기 설정</th>
<td><input type="text" placeholder="" aria-label="수신수기 설정" /></td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button className="btn dark">초기화</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,152 +0,0 @@
import { useState } from 'react';
export default function Signal2Component() {
//
const [isOpen, setIsOpen] = useState(true); //
const closePopup = () => setIsOpen(false);
// (3, )
const [accordionOpen, setAccordionOpen] = useState({
signal1: true,
signal2: true,
signal3: true,
});
const toggleAccordion = (key) => {
setAccordionOpen((prev) => ({ ...prev, [key]: !prev[key] }));
};
return (
<section id="SignalComponent">
{/* 신호설정 팝업 */}
{isOpen && (
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title">맞춤 설정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={closePopup}
/>
</div>
<div className="puBody">
{/* 아코디언그룹 01 */}
<div className="accordionWrap">
<div className="acdHeader">
<span className="title">NLL 고속 선박 탐지</span>
<button
type="button"
className={`toggleListBtn ${accordionOpen.signal1 ? 'open' : ''}`}
onClick={() => toggleAccordion('signal1')}
aria-expanded={accordionOpen.signal1}
aria-label={accordionOpen.signal1 ? '접기' : '펼치기'}
></button>
</div>
{/* 여기서부터 아코디언 */}
<div className={`acdListBox ${accordionOpen.signal1 ? 'open' : ''}`}>
<ul className="acdList input">
<li className="state">
<label className="radio radioL"> <input type="radio" name="state1" /> <span>사용</span></label>
<label className="radio radioL"> <input type="radio" name="state1" /> <span>미사용</span></label>
</li>
<li className="input">
<label>
<span>SOG 기준</span>
<input type="text" placeholder="" />
</label>
</li>
<li className="input">
<label>
<span>COG 기준</span>
<input type="text" placeholder="" />
</label>
</li>
<li className="input">
<label>
<span>유지시간()</span>
<input type="text" placeholder="" />
</label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 아코디언그룹 02 */}
<div className="accordionWrap">
<div className="acdHeader">
<span className="title">특정 어업수역 탐지</span>
<button
type="button"
className={`toggleListBtn ${accordionOpen.signal2 ? 'open' : ''}`}
onClick={() => toggleAccordion('signal2')}
aria-expanded={accordionOpen.signal2}
aria-label={accordionOpen.signal2 ? '접기' : '펼치기'}
></button>
</div>
{/* 여기서부터 아코디언 */}
<div className={`acdListBox ${accordionOpen.signal2 ? 'open' : ''}`}>
<ul className="acdList check">
<li className="state">
<label className="radio radioL"> <input type="radio" name="state2" /> <span>사용</span></label>
<label className="radio radioL"> <input type="radio" name="state2" /> <span>미사용</span></label>
</li>
<li className="check">
<label className="checkbox checkL"><input type="checkbox" /><span>특정 어업수역 I</span></label>
</li>
<li className="check">
<label className="checkbox checkL"><input type="checkbox" /><span>특정 어업수역 II</span></label>
</li>
<li className="check">
<label className="checkbox checkL"><input type="checkbox" /><span>특정 어업수역 III</span></label>
</li>
<li className="check">
<label className="checkbox checkL"><input type="checkbox" /><span>특정 어업수역 IV</span></label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 아코디언그룹 03 */}
<div className="accordionWrap">
<div className="acdHeader">
<span className="title">위험화물 식별</span>
<button
type="button"
className={`toggleListBtn ${accordionOpen.signal3 ? 'open' : ''}`}
onClick={() => toggleAccordion('signal3')}
aria-expanded={accordionOpen.signal3}
aria-label={accordionOpen.signal1 ? '접기' : '펼치기'}
></button>
</div>
{/* 여기서부터 아코디언 */}
<div className={`acdListBox ${accordionOpen.signal3 ? 'open' : ''}`}>
<ul className="acdList">
<li className="state">
<label className="radio radioL"> <input type="radio" name="state3" /> <span>사용</span></label>
<label className="radio radioL"> <input type="radio" name="state3" /> <span>미사용</span></label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button className="btn basic">저장</button>
<button className="btn dark">초기화</button>
</div>
</div>
</div>
</div>
)}
</section>
);
}

파일 보기

@ -1,59 +0,0 @@
import { Link } from "react-router-dom";
export default function ToastComponent() {
return(
<section id="toastComponent">
{/* 지도상 배표식 */}
<div className="shipMapContainer">
<div className="shipMap shipCaution">
<Link to="/">
1511함A-05
<span className="status">12.5 kts | 45°</span>
</Link>
</div>
<div className="shipMap shipWarning">
<Link to="/">
1511함A-05
<span className="status">12.5 kts | 45°</span>
</Link>
</div>
<div className="shipMap shipDefault">
<Link to="/">
1511함A-05
<span className="status">12.5 kts | 45°</span>
</Link>
</div>
</div>
{/* 토스트팝업 */}
<div className="toastContainer">
<div className="toast toastCaution">
<span className="toastMsg">104 어업구역 비인가 선박</span>
<span className="toastR">
<button type="button" className="toastAction">위치보기</button>
<button type="button" className="toastClose" aria-label="닫기"></button>
</span>
</div>
<div className="toast toastCaution">
<span className="toastMsg">104 어업구역 비인가 선박</span>
<span className="toastR">
<button type="button" className="toastAction">위치보기</button>
<button type="button" className="toastClose" aria-label="닫기"></button>
</span>
</div>
<div className="toast toastWarining">
<span className="toastMsg">저속 이동 의심 선박</span>
<span className="toastR">
<button type="button" className="toastAction">위치보기</button>
<button type="button" className="toastClose" aria-label="닫기"></button>
</span>
</div>
</div>
</section>
)
}

파일 보기

@ -1,21 +0,0 @@
export default function TopComponent() {
return(
<section className="topBar">
<div className="locationInfo">
<ul>
<li><button type="button" className="map active"><span className="blind">지도</span></button></li>
<li className="divider"><span className="wgs">경도</span><span>129°</span> <span>38</span><span>31.071</span><span>E</span></li>
<li className="divider"><span className="wgs">위도</span><span>35° </span> <span>21</span><span>24.580</span><span>N</span></li>
<li><span className="kst">KST</span><span>2024-07-01()</span> <span>12:00:00</span></li>
<li><button type="button" className="set"><span className="blind">설정</span></button></li>
<li><button type="button" className="ship"><span className="blind">선박</span></button></li>
</ul>
</div>
<div className="topSchBox">
<input type="text" className="tschInput" placeholder="선박 위치 검색" />
<button type="button" className="mainSchBtn">검색</button>
</div>
</section>
)
}

파일 보기

@ -1,62 +0,0 @@
export default function WeatherComponent() {
return(
<section id="WeatherComponent">
{/* 지도위 팝업 */}
<div className="popupMap osbInfo">
{/* header */}
<div className="pmHeader">
<div className="rowL">
<span className="title">해양관측소</span>
</div>
<button type="button" className="pmClose" aria-label="닫기"></button>
</div>
{/* body */}
<div className="pmBody">
<ul className="osbStatus">
<li className="date">
2023.10.16 20:54
</li>
<li>
<span className="label">조위</span>
<span className="value">251(cm)</span>
</li>
<li>
<span className="label">수온</span>
<span className="value">19.6(°C)</span>
</li>
<li>
<span className="label">염분</span>
<span className="value">31.8(PSU)</span>
</li>
<li>
<span className="label">기온</span>
<span className="value">16.9(°C)</span>
</li>
<li>
<span className="label">기압</span>
<span className="value">1016.6(hPa)</span>
</li>
<li>
<span className="label">풍향</span>
<span className="value">315(deg)</span>
</li>
<li>
<span className="label">풍속</span>
<span className="value">7.1(m/s)</span>
</li>
<li>
<span className="label">유속방향</span>
<span className="value">-(deg)</span>
</li>
<li>
<span className="label">유속</span>
<span className="value">-(m/s)</span>
</li>
</ul>
</div>
</div>
</section>
)
}

파일 보기

@ -1,567 +0,0 @@
import { useState, useCallback, useEffect } from 'react';
import { Link, useNavigate } from "react-router-dom";
import Slider from '../../common/Slider';
import useShipStore from '../../../stores/shipStore';
import { useMapStore, BASE_MAP_TYPES } from '../../../stores/mapStore';
import { saveUserFilter } from '../../../api/userSettingApi';
import { showToast } from '../../../components/common/Toast';
import useFavoriteStore from '../../../stores/favoriteStore';
import {
SIGNAL_SOURCE_CODE_AIS,
SIGNAL_SOURCE_CODE_ENAV,
SIGNAL_SOURCE_CODE_VPASS,
SIGNAL_SOURCE_CODE_VTS_AIS,
SIGNAL_SOURCE_CODE_D_MF_HF,
SIGNAL_SOURCE_CODE_RADAR,
SIGNAL_KIND_CODE_FISHING,
SIGNAL_KIND_CODE_PASSENGER,
SIGNAL_KIND_CODE_CARGO,
SIGNAL_KIND_CODE_TANKER,
SIGNAL_KIND_CODE_GOV,
SIGNAL_KIND_CODE_KCGV,
SIGNAL_KIND_CODE_NORMAL,
SIGNAL_KIND_CODE_BUOY,
NATIONAL_CODE_KR,
NATIONAL_CODE_CN,
NATIONAL_CODE_JP,
NATIONAL_CODE_KP,
NATIONAL_CODE_OTHER,
} from '../../../types/constants';
// ( )
const SIGNAL_FILTERS = [
{ code: SIGNAL_SOURCE_CODE_AIS, label: 'AIS' },
{ code: SIGNAL_SOURCE_CODE_VPASS, label: 'V-PASS' },
{ code: SIGNAL_SOURCE_CODE_ENAV, label: 'E-NAV' },
{ code: SIGNAL_SOURCE_CODE_VTS_AIS, label: 'VTS_AIS' },
{ code: SIGNAL_SOURCE_CODE_D_MF_HF, label: 'D_MF_HF' },
{ code: SIGNAL_SOURCE_CODE_RADAR, label: 'VTS_RADAR' },
];
// ( )
const KIND_FILTERS = [
{ code: SIGNAL_KIND_CODE_FISHING, label: '어선' },
{ code: SIGNAL_KIND_CODE_PASSENGER, label: '여객선' },
{ code: SIGNAL_KIND_CODE_CARGO, label: '화물선' },
{ code: SIGNAL_KIND_CODE_TANKER, label: '유조선' },
{ code: SIGNAL_KIND_CODE_GOV, label: '관공선' },
{ code: SIGNAL_KIND_CODE_KCGV, label: '함정' },
{ code: SIGNAL_KIND_CODE_BUOY, label: '어망/부이' },
{ code: SIGNAL_KIND_CODE_NORMAL, label: '기타' },
];
//
const NATIONAL_FILTERS = [
{ code: NATIONAL_CODE_KR, label: '한국' },
{ code: NATIONAL_CODE_CN, label: '중국' },
{ code: NATIONAL_CODE_JP, label: '일본' },
{ code: NATIONAL_CODE_KP, label: '북한' },
{ code: NATIONAL_CODE_OTHER, label: '기타' },
];
export default function DisplayComponent({ isOpen, onToggle, initialTab = 'filter' }) {
const navigate = useNavigate();
//
const baseMapType = useMapStore((s) => s.baseMapType);
const setBaseMapType = useMapStore((s) => s.setBaseMapType);
//
const {
sourceVisibility,
kindVisibility,
nationalVisibility,
darkSignalVisible,
darkSignalCount,
aiModeVisibility,
hazardVisible,
toggleSourceVisibility,
toggleKindVisibility,
toggleNationalVisibility,
toggleDarkSignalVisible,
toggleAiModeEnabled,
toggleAiModeVisibility,
toggleHazardVisible,
clearDarkSignals,
} = useShipStore();
// /
const isFavoriteEnabled = useFavoriteStore((s) => s.isFavoriteEnabled);
const toggleFavoriteEnabled = useFavoriteStore((s) => s.toggleFavoriteEnabled);
const isRealmVisible = useFavoriteStore((s) => s.isRealmVisible);
const toggleRealmVisible = useFavoriteStore((s) => s.toggleRealmVisible);
//
const isCoastGuardVisible = useMapStore((s) => s.isCoastGuardVisible);
const toggleCoastGuard = useMapStore((s) => s.toggleCoastGuard);
//
const [opacity, setOpacity] = useState(70);
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(true); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(true); //
const [isAccordionOpen3, setIsAccordionOpen3] = useState(true); //
const [isAccordionOpen4, setIsAccordionOpen4] = useState(false); // AI
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev);
const toggleAccordion3 = () => setIsAccordionOpen3(prev => !prev);
const toggleAccordion4 = () => setIsAccordionOpen4(prev => !prev);
// On/Off
const isAllSignalsOn = SIGNAL_FILTERS.every(f => sourceVisibility[f.code]);
const toggleAllSignals = useCallback(() => {
SIGNAL_FILTERS.forEach(f => {
if (isAllSignalsOn) {
//
if (sourceVisibility[f.code]) toggleSourceVisibility(f.code);
} else {
//
if (!sourceVisibility[f.code]) toggleSourceVisibility(f.code);
}
});
}, [isAllSignalsOn, sourceVisibility, toggleSourceVisibility]);
// On/Off
const isAllKindsOn = KIND_FILTERS.every(f => kindVisibility[f.code]);
const toggleAllKinds = useCallback(() => {
KIND_FILTERS.forEach(f => {
if (isAllKindsOn) {
if (kindVisibility[f.code]) toggleKindVisibility(f.code);
} else {
if (!kindVisibility[f.code]) toggleKindVisibility(f.code);
}
});
}, [isAllKindsOn, kindVisibility, toggleKindVisibility]);
// On/Off
const isAllNationalsOn = NATIONAL_FILTERS.every(f => nationalVisibility[f.code]);
const toggleAllNationals = useCallback(() => {
NATIONAL_FILTERS.forEach(f => {
if (isAllNationalsOn) {
if (nationalVisibility[f.code]) toggleNationalVisibility(f.code);
} else {
if (!nationalVisibility[f.code]) toggleNationalVisibility(f.code);
}
});
}, [isAllNationalsOn, nationalVisibility, toggleNationalVisibility]);
// AI On/Off (// )
const isAllAiModeOn = Object.values(aiModeVisibility).every(v => v);
//
const handleSaveFilter = useCallback(async () => {
try {
const settings = useShipStore.getState().buildFilterSettings();
await saveUserFilter(settings);
showToast('필터 설정이 저장되었습니다.');
} catch (err) {
console.error('[Filter] 저장 실패:', err);
showToast('필터 저장에 실패했습니다.');
}
}, []);
// ( )
const [activeTab, setActiveTab] = useState(initialTab);
//
useEffect(() => {
setActiveTab(initialTab);
}, [initialTab]);
const tabs = [
{ id: 'filter', label: '필터' },
{ id: 'layer', label: '레이어' },
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox p0">
<div className="tabDefault borderLess">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap scrollY ${activeTab === 'filter' ? 'is-active' : ''}`}>
<div className="tabWrapInner">
<div className="tabWrapCnt">
{/* 스위치그룹 01 - 선종 (메인 프로젝트와 동일 순서) */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>선종/기종</span>
<label className="switch">
<input
type="checkbox"
aria-label="선종/기종"
checked={isAllKindsOn}
onChange={toggleAllKinds}
/>
<span></span>
</label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen1 ? 'is-open' : ''}`}>
<ul className="switchList">
{KIND_FILTERS.map(({ code, label }) => (
<li key={code}>
<span>{label}</span>
<label className="switch sm">
<input
type="checkbox"
aria-label={label}
checked={kindVisibility[code] || false}
onChange={() => toggleKindVisibility(code)}
/>
<span></span>
</label>
</li>
))}
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 02 - 국적 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>국적</span>
<label className="switch">
<input
type="checkbox"
aria-label="국적"
checked={isAllNationalsOn}
onChange={toggleAllNationals}
/>
<span></span>
</label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen2 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen2}
onClick={toggleAccordion2}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen2 ? 'is-open' : ''}`}>
<ul className="switchList">
{NATIONAL_FILTERS.map(({ code, label }) => (
<li key={code}>
<span>{label}</span>
<label className="switch sm">
<input
type="checkbox"
aria-label={label}
checked={nationalVisibility[code] || false}
onChange={() => toggleNationalVisibility(code)}
/>
<span></span>
</label>
</li>
))}
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 03 - 신호종류 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>신호</span>
<label className="switch">
<input
type="checkbox"
aria-label="신호"
checked={isAllSignalsOn}
onChange={toggleAllSignals}
/>
<span></span>
</label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen3 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen3}
onClick={toggleAccordion3}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen3 ? 'is-open' : ''}`}>
<ul className="switchList">
{SIGNAL_FILTERS.map(({ code, label }) => (
<li key={code}>
<span>{label}</span>
<label className="switch sm">
<input
type="checkbox"
aria-label={label}
checked={sourceVisibility[code] || false}
onChange={() => toggleSourceVisibility(code)}
/>
<span></span>
</label>
</li>
))}
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 04 - AI 모드 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>AI 모드</span>
<label className="switch">
<input type="checkbox" aria-label="AI 모드" checked={isAllAiModeOn} onChange={toggleAiModeEnabled} />
<span></span>
</label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen4 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen4}
onClick={toggleAccordion4}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen4 ? 'is-open' : ''}`}>
<ul className="switchList">
<li>
<span>MMSI 변조</span>
<label className="switch sm">
<input type="checkbox" aria-label="MMSI 변조" checked={aiModeVisibility.mmsiChange} onChange={() => toggleAiModeVisibility('mmsiChange')} />
<span></span>
</label>
</li>
<li>
<span>중국 허가선박</span>
<label className="switch sm">
<input type="checkbox" aria-label="중국 허가선박" checked={aiModeVisibility.chinaPermission} onChange={() => toggleAiModeVisibility('chinaPermission')} />
<span></span>
</label>
</li>
<li>
<span>관공선</span>
<label className="switch sm">
<input type="checkbox" aria-label="관공선" checked={aiModeVisibility.govShip} onChange={() => toggleAiModeVisibility('govShip')} />
<span></span>
</label>
</li>
<li>
<span>비정상 접촉</span>
<label className="switch sm">
<input type="checkbox" aria-label="비정상 접촉" checked={aiModeVisibility.sseZoneContact} onChange={() => toggleAiModeVisibility('sseZoneContact')} />
<span></span>
</label>
</li>
<li>
<span>비정상 선박</span>
<label className="switch sm">
<input type="checkbox" aria-label="비정상 선박" checked={aiModeVisibility.nonPermission} onChange={() => toggleAiModeVisibility('nonPermission')} />
<span></span>
</label>
</li>
<li>
<span>북한선박</span>
<label className="switch sm">
<input type="checkbox" aria-label="북한선박" checked={aiModeVisibility.northKoreaAi} onChange={() => toggleAiModeVisibility('northKoreaAi')} />
<span></span>
</label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 05 - 다크시그널 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>다크시그널</span>
{darkSignalCount > 0 && <span className="count">({darkSignalCount})</span>}
{darkSignalCount > 0 && (
<button
type="button"
className="btnDelDark"
onClick={clearDarkSignals}
title="다크시그널 삭제"
>
삭제
</button>
)}
</div>
<label className="switch">
<input
type="checkbox"
aria-label="다크시그널"
checked={darkSignalVisible}
onChange={toggleDarkSignalVisible}
/>
<span></span>
</label>
</div>
</div>
{/* 스위치그룹 06 - 위험물 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>위험물</span>
</div>
<label className="switch">
<input type="checkbox" aria-label="위험물" checked={hazardVisible} onChange={toggleHazardVisible} />
<span></span>
</label>
</div>
</div>
{/* 스위치그룹 07 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<i className="favship"></i>
<span>관심선박</span>
</div>
<label className="switch"> <input type="checkbox" aria-label="관심선박" checked={isFavoriteEnabled} onChange={toggleFavoriteEnabled} /> <span></span></label>
</div>
</div>
</div>
{/* 버튼영역 */}
<div className="btnBox">
<button type="button" className="btn btnLine" onClick={handleSaveFilter}>저장</button>
</div>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'layer' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">레이어</div>
</div>
<div className="tabBtm noLine">
<div className="tabBtmInner">
<ul className="lineList tabBtmCnt">
<li className="rowSB">
<label className="checkbox checkL">
<input type="checkbox" />
<span>배경지도</span>
</label>
<div className="row">
<span>투명도 조절</span>
<div>
<Slider label="투명도 조절" />
</div>
</div>
</li>
<li className="p0">
<ul className="optionList">
<li>
<span>전자해도</span>
<label className="radio">
<input
type="radio"
name="baseMap"
aria-label="전자해도"
checked={baseMapType === BASE_MAP_TYPES.ENC}
onChange={() => setBaseMapType(BASE_MAP_TYPES.ENC)}
/>
<span></span>
</label>
</li>
<li>
<span>일반지도</span>
<label className="radio">
<input
type="radio"
name="baseMap"
aria-label="일반지도"
checked={baseMapType === BASE_MAP_TYPES.NORMAL}
onChange={() => setBaseMapType(BASE_MAP_TYPES.NORMAL)}
/>
<span></span>
</label>
</li>
<li>
<span>야간지도</span>
<label className="radio">
<input
type="radio"
name="baseMap"
aria-label="야간지도"
checked={baseMapType === BASE_MAP_TYPES.DARK}
onChange={() => setBaseMapType(BASE_MAP_TYPES.DARK)}
/>
<span></span>
</label>
</li>
</ul>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" checked={isRealmVisible} onChange={toggleRealmVisible} />
<span>관심구역</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" checked={isCoastGuardVisible} onChange={toggleCoastGuard} />
<span>해경관할구역</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>검문검색위치</span>
</label>
</li>
</ul>
<div className='btnBox'>
<button
type="button"
className="btn btnLine w15r"
onClick={() => navigate("/display/layer/register")}
>레이어 등록</button>
</div>
</div>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,53 +0,0 @@
export default function NavComponent({ activeKey, onChange }) {
const gnbList = [
{ key: 'gnb1', class: 'gnb1', label: '선박' },
{ key: 'gnb2', class: 'gnb2', label: '위성' },
{ key: 'gnb3', class: 'gnb3', label: '기상' },
{ key: 'gnb4', class: 'gnb4', label: '분석' },
{ key: 'gnb5', class: 'gnb5', label: '타임라인' },
{ key: 'gnb6', class: 'gnb6', label: 'AI모드' },
{ key: 'gnb7', class: 'gnb7', label: '리플레이' },
{ key: 'gnb8', class: 'gnb8', label: '항적조회' },
];
const sideList = [
{ key: 'filter', class: 'filter', label: '필터' },
{ key: 'layer', class: 'layer', label: '레이어' },
];
return(
<nav id="nav">
<ul className="gnb">
{gnbList.map(item => (
<li key={item.key}>
<button
type="button"
className={`${item.class} ${activeKey === item.key ? 'active' : ''}`}
onClick={() => onChange(item.key)}
aria-label={item.label}
>
<span className="blind">{item.label}</span>
</button>
</li>
))}
</ul>
<ul className="side">
{sideList.map(item => (
<li key={item.key}>
<button
type="button"
className={`${item.class} ${activeKey === item.key ? 'active' : ''}`}
onClick={() => onChange(item.key)}
aria-label={item.label}
>
<span className="blind">{item.label}</span>
</button>
</li>
))}
</ul>
</nav>
)
}

파일 보기

@ -1,704 +0,0 @@
import { useState } from 'react';
import { Link } from "react-router-dom";
const BASE_URL = import.meta.env.BASE_URL;
export default function Panel1Component({ isOpen, onToggle }) {
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(false); //
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev);
//
const [activeTab, setActiveTab] = useState('ship01');
const tabs = [
{ id: 'ship01', label: '선박검색' },
{ id: 'ship02', label: '허가선박' },
{ id: 'ship03', label: '제재단속' },
{ id: 'ship04', label: '침몰선박' },
{ id: 'ship05', label: '선박입출항' },
{ id: 'ship06', label: '관심선박' }
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'ship01' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">선박 검색</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>선종</span>
<select>
<option value="">전체</option>
<option value="">어선</option>
<option value="">함정</option>
<option value="">여객선</option>
<option value="">카고</option>
<option value="">탱커</option>
<option value="">관공선</option>
<option value="">기타</option>
<option value="">낚시어선</option>
</select>
</label>
<label>
<span>국적</span>
<select>
<option value="">전체</option>
<option value="">한국</option>
<option value="">미국</option>
<option value="">중국</option>
<option value="">일본</option>
<option value="">북한</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>타겟ID</span>
<input type="text" placeholder="타겟ID" />
</label>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
{/* 아코디언 1 */}
<div className={`accordion ${isAccordionOpen1 ? 'is-open' : ''}`}>
<li>
<label>
<span>위험물</span>
<input type="text" placeholder="타겟ID" />
</label>
<label className="checkbox">
<input type="checkbox" />
<span className="w70">MMSI / 호출부호 변경이력</span>
</label>
</li>
<li>
<label>
<span>승선원수</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>-</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li>
<label>
<span>너비(m)</span>
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</label>
</li>
</div>
{/* 여기까지 아코디언1 */}
<li>
<button
type="button"
className={`btn btnS semi btnToggle ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
>
상세검색
{isAccordionOpen1 ? ' 닫기' : ' 열기'}
</button>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
{/* <div className="schbox mtb24">
<ul>
<li>
<input type="text" className="schInput" placeholder="대표검도" />
</li>
<li>
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div> */}
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/" className="">
<i className="cicle red"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/" className="">
<i className="cicle orng"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'ship02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">허가선박</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>타겟 ID</span>
<input type="text" placeholder="타겟 ID" />
</label>
</li>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<div className="detailWrap">
<ul className="detailBox">
<li className="dbHeader">
<div className="headerL">
<span className="name">ZHELINGYU29801</span>
<span className="type">Fishing</span>
</div>
<div className="headerR">
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
<span className="num">412</span>
<button className="icoArrow"></button>
</div>
</li>
<li>
<span className="label">타겟 ID</span>
<span className="value">412417712</span>
</li>
<li>
<span className="label">주정박항</span>
<span className="value">zhelingyu29801</span>
</li>
<li>
<span className="label">어획할당량</span>
<span className="value">100(ton)</span>
</li>
<li>
<span className="label">조업수역구역</span>
<span className="value">, </span>
</li>
</ul>
</div>
</div>
</div>
{/* 탭 콘텐츠 03 */}
<div className={`tabWrap ${activeTab === 'ship03' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">제재단속</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>제재 유형</span>
<select>
<option value="">전체</option>
<option value="">고래포획 의심</option>
<option value="">UN 제재</option>
<option value="">위반행위 규제 정보</option>
<option value="">불법 선박</option>
<option value="">음주 운항 이력</option>
<option value="">다잡아 처분 선박</option>
<option value="">어획량 위반</option>
<option value="">조업 일지 위반</option>
<option value="">망목 내경 미준수</option>
<option value="">입출역 미통보</option>
<option value="">선박서류 미비치</option>
<option value="">어구위반</option>
<option value="">허가 /표지판 위반</option>
<option value="">어획물 전재 위반</option>
<option value="">선원수첩 신분증명서 위반</option>
<option value="">정선 명령 위반</option>
<option value="">어구 설치 조업수역 이탈</option>
<option value="">어획물 운반선 체크포인트 제도 위반</option>
<option value="">포획 채취 금지 체장 위반 어획물 포획</option>
<option value="">조업수역 위반</option>
<option value="">조업 기간 위반</option>
<option value="">어창 용적 위반</option>
<option value="">어창 용적 위반</option>
<option value="">메모</option>
</select>
</label>
</li>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 04 */}
<div className={`tabWrap ${activeTab === 'ship04' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">침몰선박</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li>
<label>
<span>사고기간</span>
<div className='labelRow'>
<input type="text" className="dateInput" placeholder="연도-월-일" />
<span>-</span>
<input type="text"className="dateInput" placeholder="연도-월-일" /></div>
</label>
</li>
<li>
<label>
<span>사고내용</span>
<input type="text" placeholder="사고내용" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 05 */}
<div className={`tabWrap ${activeTab === 'ship05' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">선박입출항</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>출항일시</span>
<input type="text" className="dateInput" placeholder="연도-월-일 - -:-" />
</label>
<label>
<span>~ 입항일시</span>
<input type="text" className="dateInput" placeholder="연도-월-일 - -:-" />
</label>
</li>
<li>
<label>
<span>PMS<br/>출항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
<label>
<span>PMS<br/>입항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
</li>
<li>
<label>
<span>SIE<br/>출항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
<label>
<span>SIE<br/>입항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
</li>
<li>
<label>
<span>타겟ID</span>
<input type="text" placeholder="타겟ID" />
</label>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
{/* 여기부터 아코디언 */}
<div className={`accordion ${isAccordionOpen2 ? 'is-open' : ''}`}>
<li>
<label>
<span>낚시여부</span>
<select>
<option value="">전체</option>
<option value="">미선택</option>
<option value="">선택</option>
</select>
</label>
</li>
<li>
<label>
<span>최대<br/>적재톤수</span>
<input type="text" placeholder="0" />
</label>
<label>
<span>최소<br/>적재톤수</span>
<input type="text" placeholder="0" />
</label>
</li>
<li>
<label>
<span>최대<br/>승선원</span>
<input type="text" placeholder="0" />
</label>
<label>
<span>최소<br/>승선원</span>
<input type="text" placeholder="0" />
</label>
</li>
<li>
<label>
<span>최대<br/>승객수</span>
<input type="text" placeholder="0" />
</label>
<label>
<span>최소<br/>승객수</span>
<input type="text" placeholder="0" />
</label>
</li>
<li>
<label>
<span>선종</span>
<select>
<option value="">전체</option>
<option value="">어선</option>
<option value="">함정</option>
<option value="">여객선</option>
<option value="">카고</option>
<option value="">탱커</option>
<option value="">관공선</option>
<option value="">기타</option>
<option value="">낚시어선</option>
</select>
</label>
<label>
<span>국적</span>
<select>
<option value="">전체</option>
<option value="">한국</option>
<option value="">미국</option>
<option value="">중국</option>
<option value="">일본</option>
<option value="">북한</option>
<option value="">기타</option>
</select>
</label>
</li>
</div>
{/* 여기까지 아코디언 */}
<li>
<button
type="button"
className={`btn btnS semi btnToggle ${isAccordionOpen2 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen2}
onClick={toggleAccordion2}
>
상세검색
{isAccordionOpen2 ? ' 닫기' : ' 열기'}
</button>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 06 */}
<div className={`tabWrap ${activeTab === 'ship06' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">관심선박</div>
<div className="formGroup">
<ul className="lagelW12">
<li>
<label>
<span>관심사유 지정사유</span>
<select>
<option value="">전체</option>
<option value="">불법조업의심</option>
<option value="">불법포경의심</option>
<option value="">MMSI 신호 임의 변경</option>
<option value="">제재 선박 의심</option>
<option value="">북한 선박 의심</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>타겟 ID</span>
<input type="text" placeholder="타겟 ID" />
</label>
</li>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src={`${BASE_URL}images/flag_kor.svg`} alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src={`${BASE_URL}images/legend_ship_pink.svg`} alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,420 +0,0 @@
import { useState } from 'react';
import { Link, useNavigate } from "react-router-dom";
import Slider from '../../common/Slider';
export default function Panel2Component({ isOpen, onToggle }) {
const navigate = useNavigate();
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(false); //
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev);
//
const [activeTab, setActiveTab] = useState('ship01');
const tabs = [
{ id: 'ship01', label: '위성영상 관리' },
{ id: 'ship02', label: '위성사업자 관리' },
{ id: 'ship03', label: '위성 관리' },
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'ship01' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">위성영상 관리</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>영상 촬영일</span>
<div className="labelRow">
<input className="dateInput" placeholder="연도-월-일" type="text" />
<span>-</span>
<input className="dateInput" placeholder="연도-월-일" type="text" />
</div>
</label>
</li>
{/* 아코디언 1 */}
<div className={`accordion pt8 ${isAccordionOpen1 ? 'is-open' : ''}`}>
<li>
<label>
<span>영상 종류</span>
<select>
<option value="">전체</option>
<option value="">VIRS</option>
<option value="">ICEYE_SAR</option>
<option value="">광학</option>
<option value="">예약</option>
<option value="">RF</option>
</select>
</label>
<label>
<span>영상 출처</span>
<select>
<option value="">전체</option>
<option value="">국내/자동</option>
<option value="">국내/수동</option>
<option value="">국외/수동</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>위성 궤도</span>
<select>
<option value="">전체</option>
<option value="">저궤도</option>
<option value="">중궤도</option>
<option value="">정지궤도</option>
<option value="">기타</option>
</select>
</label>
<label>
<span>주기</span>
<select>
<option value="">전체</option>
<option value="">0</option>
<option value="">10</option>
<option value="">30</option>
<option value="">60</option>
</select>
</label>
</li>
</div>
{/* 여기까지 아코디언1 */}
<li>
<label>
<span>위성영상명</span>
<input type="text" placeholder="위성영상명" />
</label>
</li>
<li>
<button
type="button"
className={`btn btnS semi btnToggle ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
>
상세검색
{isAccordionOpen1 ? ' 닫기' : ' 열기'}
</button>
</li>
<li className="fgBtn rowSB">
<>
<div className="row gap10">
<span>투명도</span>
<div>
<Slider label="투명도 조절" />
</div>
</div>
<div className="row gap10">
<span>밝기</span>
<div>
<Slider label="밝기 조절" />
</div>
</div>
</>
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤영역 */}
<div className="tabBtmCnt">
<div className="detailWrap">
{/* 위성정보 박스 */}
<ul className="detailBox stretch">
<li className="dbHeader">
<div className="headerL">
<span className="name">업로드 테스트</span>
<span className="type">2025-09-25 16:09:00</span>
</div>
</li>
<li>
<ul className="dbList">
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">위성영상파일</span>
<span className="value">mda:fae19b9b-e99e-4794- a305-bce6d537ac36_remark_ resized_2022_0102_1709_npp_DNB_750</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">영상 출처</span>
<span className="value">VIRS</span>
</li>
</ul>
<div className="btnArea">
<button type="button" className="btnEdit"></button>
<button type="button" className="btnDel" onClick={() => navigate("/panel2/satellite/delete")}></button>
<button type="button" className="btnMap"></button>
</div>
</li>
</ul>
{/* 위성정보 박스 */}
<ul className="detailBox stretch">
<li className="dbHeader">
<div className="headerL">
<span className="name">업로드 테스트</span>
<span className="type">2025-09-25 16:09:00</span>
</div>
</li>
<li>
<ul className="dbList">
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">위성영상파일</span>
<span className="value">mda:fae19b9b-e99e-4794- a305-bce6d537ac36_remark_ resized_2022_0102_1709_npp_DNB_750</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">영상 출처</span>
<span className="value">VIRS</span>
</li>
</ul>
<div className="btnArea">
<button type="button" className="btnEdit"></button>
<button type="button" className="btnDel"></button>
<button type="button" className="btnMap"></button>
</div>
</li>
</ul>
</div>
</div>
{/* 하단버튼 영역 */}
<div className="btnBox rowSB">
<button type="button" className="btn btnLine">위성영상 폴더 업로드</button>
<button type="button" className="btn btnLine" onClick={() => navigate("/panel2/satellite/add")}>위성영상 등록</button>
</div>
</div>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'ship02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">위성사업자 관리</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>사업자 분류</span>
<select>
<option value="">전체</option>
<option value="">국가</option>
<option value="">연구기관</option>
<option value="">민간사업자</option>
<option value="">기타</option>
</select>
</label>
<label>
<span>사업자명</span>
<input type="text" placeholder="사업자명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤영역 */}
<div className="tabBtmCnt">
<div className="detailWrap">
{/* 위성정보 박스 */}
<ul className="detailBox">
<li className="dbHeader">
<div className="headerL">
<span className="name">Test 01</span>
</div>
</li>
<li>
<span className="label">사업자 분류</span>
<span className="value">국가</span>
</li>
<li>
<span className="label">국가</span>
<span className="value">대한민국</span>
</li>
<li>
<span className="label">소재지</span>
<span className="value">test</span>
</li>
</ul>
</div>
</div>
{/* 하단버튼 영역 */}
<div className="btnBox">
<button
type="button"
className="btn btnLine"
onClick={() => navigate("/panel2/satellite/provider")}
>
등록
</button>
</div>
</div>
</div>
</div>
{/* 탭 콘텐츠 03 */}
<div className={`tabWrap ${activeTab === 'ship03' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">위성 관리</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>사업자 분류</span>
<select>
<option value="">전체</option>
<option value="">국가</option>
<option value="">연구기관</option>
<option value="">민간사업자</option>
<option value="">기타</option>
</select>
</label>
<label>
<span>센서 타입</span>
<select>
<option value="">전체</option>
<option value="">광학</option>
<option value="">SAR</option>
<option value="">RF</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>위성명</span>
<input type="text" placeholder="위성명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤영역 */}
<div className="tabBtmCnt">
<div className="detailWrap">
{/* 위성정보 박스 */}
<ul className="detailBox">
<li>
<span className="label">사업자명</span>
<span className="value">국토지리정보원</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">국가</span>
</li>
<li>
<span className="label">센서 타입</span>
<span className="value">test</span>
</li>
<li>
<span className="label">촬영 해상도</span>
<span className="value"></span>
</li>
</ul>
{/* 위성정보 박스 */}
<ul className="detailBox">
<li>
<span className="label">사업자명</span>
<span className="value">국토지리정보원</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">국가</span>
</li>
<li>
<span className="label">센서 타입</span>
<span className="value">test</span>
</li>
<li>
<span className="label">촬영 해상도</span>
<span className="value"></span>
</li>
</ul>
</div>
</div>
{/* 하단버튼 영역 */}
<div className="btnBox">
<button
type="button"
className="btn btnLine"
onClick={() => navigate("/panel2/satellite/manage")}
>
등록
</button>
</div>
</div>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,325 +0,0 @@
import { useState } from 'react';
import { Link } from "react-router-dom";
const BASE_URL = import.meta.env.BASE_URL;
export default function Panel3Component({ isOpen, onToggle }) {
//
const [activeTab, setActiveTab] = useState('weather01');
const tabs = [
{ id: 'weather01', label: '기상특보' },
{ id: 'weather02', label: '태풍정보' },
{ id: 'weather03', label: '조위관측' },
{ id: 'weather04', label: '조석정보' },
{ id: 'weather05', label: '항공기상' },
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'weather01' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">기상특보</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>일자</span>
<div className='labelRow'>
<input type="text" className="dateInput" placeholder="연도-월-일" />
<span>-</span>
<input type="text"className="dateInput" placeholder="연도-월-일" /></div>
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList lineSB">
<li>
<Link to="/" className="">
<span className="title">1. 폭풍주의: 남해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/" className="">
<span className="title">2. 폭풍주의: 서해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/" className="">
<span className="title">3. 폭풍주의: 동해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'weather02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">태풍정보</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>연도</span>
<select>
<option value="">선택</option>
</select>
</label>
<label>
<span></span>
<select>
<option value="">선택</option>
</select>
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList lineSB">
<li>
<Link to="/" className="">
<span className="title">1. 폭풍주의: 남해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/" className="">
<span className="title">2. 폭풍주의: 서해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/" className="">
<span className="title">3. 폭풍주의: 동해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 03 */}
<div className={`tabWrap ${activeTab === 'weather03' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">조위관측</div>
<div className="legend">
<span className="legendTitle">조위관측 범례</span>
<ul className="legendList">
<li><img src={`${BASE_URL}images/ico_obsTide.svg`} alt="조위관측소" />조위관측소</li>
<li><img src={`${BASE_URL}images/ico_obsOcean.svg`} alt="해양관측소" />해양관측소</li>
<li><img src={`${BASE_URL}images/ico_obsBuoy.svg`} alt="해양관측부이" />해양관측부이</li>
<li><img src={`${BASE_URL}images/ico_obsCurrent.svg`} alt="해수유동관측소" />해수유동관측소</li>
<li><img src={`${BASE_URL}images/ico_obsScience.svg`} alt="해양과학기지" />해양과학기지</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="lineList">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>조위관측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해양관측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해양관측부이</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해수유동관측측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해양과학기지</span>
</label>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 04 */}
<div className={`tabWrap ${activeTab === 'weather04' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">조석정보</div>
<div className="legend">
<span className="legendTitle">조위관측 범례</span>
<ul className="legendList">
<li><img src={`${BASE_URL}images/ico_obsTide.svg`} alt="조위관측소" />조위관측소</li>
<li><img src={`${BASE_URL}images/ico_obsSunrise.svg`} alt="일출몰관측지역" />일출몰관측지역</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="lineList">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>조위관측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>일출몰관측지역</span>
</label>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 05 */}
<div className={`tabWrap ${activeTab === 'weather05' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">항공기상</div>
</div>
<div className="tabBtm noLine">
<ul className="lineList">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>전체</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>양양공항(RKNY)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>김포공항(RKSS)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>인천공항(RKSI)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>청주공항(RKTU)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>포항공항(RKTH)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>대구공항(RKTN)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>울산공항(RKPU)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>김해공항(RKPK)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>광주공항(RKJJ)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>사천공항(RKPS)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>무안공항(RKJB)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>여수공항(RKYJ)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>제주공항(RKPC)</span>
</label>
</li>
</ul>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,418 +0,0 @@
import { useState } from 'react';
import { Link, useNavigate } from "react-router-dom";
export default function Panel4Component({ isOpen, onToggle }) {
const navigate = useNavigate();
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); //
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
//
const [activeTab, setActiveTab] = useState('analysis01');
const tabs = [
{ id: 'analysis01', label: '관심 해역' },
{ id: 'analysis02', label: '해역 분석' },
{ id: 'analysis03', label: '해역 진입 선박' },
{ id: 'analysis04', label: '해구 분석' },
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'analysis01' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">관심 해역</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>영역명</span>
<input type="text" placeholder="" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤 영역 */}
<div className="tabBtmCnt">
<span>데이터가 없습니다.</span>
{/* <ul className="colList lineSB">
<li>
<Link to="/" className="">
<span className="title">1. 폭풍주의: 남해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
</ul> */}
</div>
{/* 하단고정버튼 */}
<div className="btnBox">
<button
type="button"
className="btn btnLine"
onClick={() => navigate("/panel4/analysis/area")}
>등록</button>
</div>
</div>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'analysis02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">해역 분석</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>조회기간</span>
<div className="labelRow">
<input className="dateInput" placeholder="연도-월-일" type="text" />
<span>-</span>
<input className="dateInput" placeholder="연도-월-일" type="text" />
</div>
</label>
</li>
<li>
<label>
<span>제목</span>
<input type="text" placeholder="" />
</label>
</li>
<li className="fgBtn">
<button
type="button"
className="schBtn"
>검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤 영역 */}
<div className="tabBtmCnt">
<span>데이터가 없습니다.</span>
</div>
{/* 하단고정버튼 */}
<div className="btnBox">
<button
type="button"
className="btn btnLine"
onClick={() => navigate("/panel4/analysis/register")}
>등록</button>
</div>
</div>
</div>
</div>
{/* 탭 콘텐츠 03 */}
<div className={`tabWrap ${activeTab === 'analysis03' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">해역 진입 선박</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>진입 일시</span>
<div className="labelRow">
<input className="dateInput" placeholder="연도-월-일" type="text" />
<span>-</span>
<input className="dateInput" placeholder="연도-월-일" type="text" />
</div>
</label>
</li>
{/* 아코디언 1 */}
<div className={`accordion pt8 ${isAccordionOpen1 ? 'is-open' : ''}`}>
<li>
<label>
<span>국적</span>
<select>
<option value="">전체</option>
<option value="">한국</option>
<option value="">미국</option>
<option value="">중국</option>
<option value="">일본</option>
<option value="">북한</option>
<option value="">기타</option>
</select>
</label>
<label>
<span>선종</span>
<select>
<option value="">전체</option>
<option value="">어선</option>
<option value="">함정</option>
<option value="">여객선</option>
<option value="">카고</option>
<option value="">탱커</option>
<option value="">관공선</option>
<option value="">기타</option>
<option value="">낚시어선</option>
</select>
</label>
</li>
<li>
<label>
{/* 사용자가 등록한 관심해역리스트 */}
<span>관심 해역</span>
<select>
<option value="">전체</option>
</select>
</label>
<label>
<span>위험물</span>
<select>
<option value="">전체</option>
<option value="">고압</option>
<option value="">가연성/인화성</option>
<option value="">산화성</option>
<option value="">독성</option>
<option value="">방사성</option>
<option value="">기타</option>
</select>
</label>
</li>
</div>
{/* 여기까지 아코디언1 */}
<li>
<label>
<span>타겟ID</span>
<input type="text" placeholder="타겟ID" />
</label>
<label className="checkbox">
<input type="checkbox" />
<span className="w70">허가 선박 여부</span>
</label>
</li>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li>
<button
type="button"
className={`btn btnS semi btnToggle ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
>
상세검색
{isAccordionOpen1 ? ' 닫기' : ' 열기'}
</button>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
</div>
</div>
{/* 탭 콘텐츠 04 */}
<div className={`tabWrap ${activeTab === 'analysis04' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">해구 분석</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>전체 통화량</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>~</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li>
<label>
<span>유의파고(m)</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>~</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li>
<label>
<span>파향(deg)</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>~</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li>
<label>
<span>파주기()</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>~</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li>
<label>
<span>풍속(m/s)</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>~</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li>
<label>
<span>풍향(deg)</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>~</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li className="fgBtn rowSB">
<span className="infoTxt">통화량 조회에 최대 30 소요될 있습니다.</span>
<button
type="button"
className="schBtn"
>
검색
</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,7 +0,0 @@
import { useState } from "react";
export default function Panel5Component() {
return (
<section></section>
);
}

파일 보기

@ -1,80 +0,0 @@
import { useState } from "react";
import { Link } from "react-router-dom";
const BASE_URL = import.meta.env.BASE_URL;
export default function Panel6Component({ isOpen, onToggle }) {
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
<div className="panelHeader">
<h2 className="panelTitle">AI 분석 모드</h2>
</div>
<div className="panelBody">
<ul className="ai">
<li>
<Link to="/" className="on">
<div className="control"><i></i> ON</div>
<span className="title"><img src={`${BASE_URL}images/ico_ai_trackgap.svg`} alt="소실항적" />소실항적</span>
<span className="desc">AIS 신호가 소실된 선박</span>
<span className="keywords">Signal Gap</span>
</Link>
</li>
<li>
<Link to="/" className="">
<div className="control"><i></i> OFF</div>
<span className="title"><img src={`${BASE_URL}images/ico_ai_route.svg`} alt="항로예측" />항로예측</span>
<span className="desc">AI 기반 선박 항로 예측</span>
<span className="keywords">ML Pattern</span>
</Link>
</li>
<li>
<Link to="/" className="">
<div className="control"><i></i> OFF</div>
<span className="title"><img src={`${BASE_URL}images/ico_ai_shiptype.svg`} alt="선종분석" />선종분석</span>
<span className="desc">선박 유형 자동 분류</span>
<span className="keywords">Auto Class</span>
</Link>
</li>
<li>
<Link to="/" className="on">
<div className="control"><i></i> ON</div>
<span className="title"><img src={`${BASE_URL}images/ico_ai_fishing.svg`} alt="조업분석" />조업분석</span>
<span className="desc">구역별 위험도 평가</span>
<span className="keywords">Risk Score</span>
</Link>
</li>
<li>
<Link to="/" className="on">
<div className="control"><i></i> ON</div>
<span className="title"><img src={`${BASE_URL}images/ico_ai_risk.svg`} alt="해역별 위험지수" />해역별 위험지수</span>
<span className="desc">구역별 위험도 평가</span>
<span className="keywords">Risk Score</span>
</Link>
</li>
</ul>
</div>
<div className="panelFooter">
<div className="btnWrap">
<button className="btn deep">전체 해제</button>
<button className="btn basic">설정 저장</button>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,7 +0,0 @@
import { useState } from "react";
export default function Panel7Component() {
return (
<section></section>
);
}

파일 보기

@ -1,7 +0,0 @@
import { useState } from "react";
export default function Panel8Component() {
return (
<section></section>
);
}

파일 보기

@ -1,29 +1,31 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useAuthStore } from '../../stores/authStore'; import { useAuthStore } from '../../stores/authStore';
import useShipStore from '../../stores/shipStore';
import { fetchUserFilter } from '../../api/userSettingApi';
import './SessionGuard.scss'; import './SessionGuard.scss';
const SKIP_AUTH = import.meta.env.VITE_DEV_SKIP_AUTH === 'true';
export default function SessionGuard({ children }) { export default function SessionGuard({ children }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const isChecking = useAuthStore((s) => s.isChecking); const isChecking = useAuthStore((s) => s.isChecking);
useEffect(() => { useEffect(() => {
if (SKIP_AUTH) {
// :
useAuthStore.getState().checkSession();
return;
}
const result = useAuthStore.getState().checkSession(); const result = useAuthStore.getState().checkSession();
if (!result.valid) { if (!result.valid) {
window.location.href = import.meta.env.VITE_MAIN_APP_URL || '/'; window.location.href = import.meta.env.VITE_MAIN_APP_URL || '/';
return;
} }
//
fetchUserFilter()
.then((filterArray) => {
if (filterArray) {
useShipStore.getState().applyFilterSettings(filterArray);
}
})
.catch((err) => console.warn('[SessionGuard] 필터 로드 실패, 기본값 사용:', err));
}, []); }, []);
// children
if (SKIP_AUTH) {
return children;
}
if (isChecking || !isAuthenticated) { if (isChecking || !isAuthenticated) {
return ( return (
<div className="session-guard-loading"> <div className="session-guard-loading">

파일 보기

@ -1,25 +1,15 @@
/** /**
* 사이드 네비게이션 메뉴 * 사이드 네비게이션 메뉴
* - 퍼블리시 NavComponent 구조와 동일하게 맞춤
*/ */
const gnbList = [ const gnbList = [
{ key: 'gnb1', className: 'gnb1', label: '선박', path: 'ship' }, { key: 'gnb1', className: 'gnb1', label: '선박', path: 'ship' },
{ key: 'gnb2', className: 'gnb2', label: '위성', path: 'satellite' },
{ key: 'gnb3', className: 'gnb3', label: '기상', path: 'weather' },
{ key: 'gnb4', className: 'gnb4', label: '분석', path: 'analysis' }, { key: 'gnb4', className: 'gnb4', label: '분석', path: 'analysis' },
{ key: 'gnb5', className: 'gnb5', label: '타임라인', path: 'timeline' }, { key: 'gnb5', className: 'gnb5', label: '타임라인', path: 'timeline' },
// { key: 'gnb6', className: 'gnb6', label: 'AI', path: 'ai' },
{ key: 'gnb7', className: 'gnb7', label: '리플레이', path: 'replay' }, { key: 'gnb7', className: 'gnb7', label: '리플레이', path: 'replay' },
{ key: 'gnb8', className: 'gnb8', label: '항적분석', path: 'area-search' }, { key: 'gnb8', className: 'gnb8', label: '항적분석', path: 'area-search' },
]; ];
// / (gnb1) DisplayComponent
const sideList = [
// { key: 'filter', className: 'filter', label: '', path: 'filter' },
// { key: 'layer', className: 'layer', label: '', path: 'layer' },
];
export default function SideNav({ activeKey, onChange }) { export default function SideNav({ activeKey, onChange }) {
return ( return (
<nav id="nav"> <nav id="nav">
@ -38,22 +28,6 @@ export default function SideNav({ activeKey, onChange }) {
</li> </li>
))} ))}
</ul> </ul>
<ul className="side">
{sideList.map((item) => (
<li key={item.key}>
<button
type="button"
className={`${item.className} ${activeKey === item.key ? 'active' : ''}`}
onClick={() => onChange(item.key)}
aria-label={item.label}
title={item.label}
>
<span className="blind">{item.label}</span>
</button>
</li>
))}
</ul>
</nav> </nav>
); );
} }
@ -61,15 +35,10 @@ export default function SideNav({ activeKey, onChange }) {
// - export (Sidebar ) // - export (Sidebar )
export const keyToPath = { export const keyToPath = {
gnb1: 'ship', gnb1: 'ship',
gnb2: 'satellite',
gnb3: 'weather',
gnb4: 'analysis', gnb4: 'analysis',
gnb5: 'timeline', gnb5: 'timeline',
gnb6: 'ai',
gnb7: 'replay', gnb7: 'replay',
gnb8: 'area-search', gnb8: 'area-search',
filter: 'filter',
layer: 'layer',
}; };
export const pathToKey = Object.fromEntries( export const pathToKey = Object.fromEntries(

파일 보기

@ -2,25 +2,9 @@ import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import SideNav, { keyToPath, pathToKey } from './SideNav'; import SideNav, { keyToPath, pathToKey } from './SideNav';
// ( import.meta.glob )
const publishPanels = import.meta.glob('../../publish/pages/Panel*Component.jsx', { eager: true });
const getPanel = (name) => publishPanels[`../../publish/pages/${name}.jsx`]?.default || null;
const Panel1Component = getPanel('Panel1Component');
const Panel2Component = getPanel('Panel2Component');
const Panel3Component = getPanel('Panel3Component');
const Panel4Component = getPanel('Panel4Component');
const Panel5Component = getPanel('Panel5Component');
const Panel6Component = getPanel('Panel6Component');
const Panel8Component = getPanel('Panel8Component');
// DisplayComponent
import DisplayComponent from '../../component/wrap/side/DisplayComponent';
// //
import ReplayPage from '../../pages/ReplayPage'; import ReplayPage from '../../pages/ReplayPage';
import AreaSearchPage from '../../areaSearch/components/AreaSearchPage'; import AreaSearchPage from '../../areaSearch/components/AreaSearchPage';
import WeatherPage from '../../pages/WeatherPage';
import SatellitePage from '../../pages/SatellitePage';
/** /**
* 사이드바 컴포넌트 * 사이드바 컴포넌트
@ -62,19 +46,14 @@ export default function Sidebar() {
onToggle: handleTogglePanel, onToggle: handleTogglePanel,
}; };
// ( null ) //
const renderPanel = () => { const renderPanel = () => {
const panelMap = { const panelMap = {
gnb1: DisplayComponent ? <DisplayComponent {...panelProps} initialTab="filter" /> : null, gnb1: null, // TODO: /
gnb2: <SatellitePage {...panelProps} />, gnb4: null, // TODO:
gnb3: <WeatherPage {...panelProps} />, gnb5: null, // TODO:
gnb4: Panel4Component ? <Panel4Component {...panelProps} /> : null,
gnb5: Panel5Component ? <Panel5Component {...panelProps} /> : null,
gnb6: Panel6Component ? <Panel6Component {...panelProps} /> : null,
gnb7: <ReplayPage {...panelProps} />, gnb7: <ReplayPage {...panelProps} />,
gnb8: <AreaSearchPage {...panelProps} />, gnb8: <AreaSearchPage {...panelProps} />,
filter: DisplayComponent ? <DisplayComponent {...panelProps} initialTab="filter" /> : null,
layer: DisplayComponent ? <DisplayComponent {...panelProps} initialTab="layer" /> : null,
}; };
return panelMap[activeKey] || null; return panelMap[activeKey] || null;
}; };

파일 보기

@ -1,203 +0,0 @@
/**
* 경비함정 선택 드롭다운
* 선박 모드에서 추적할 함정을 선택
* - 검색 기능 (like 검색)
* - 반경 설정
*/
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import useShipStore from '../../stores/shipStore';
import useTrackingModeStore, { isPatrolShip, RADIUS_OPTIONS } from '../../stores/trackingModeStore';
import './PatrolShipSelector.scss';
/**
* 검색어 정규화 (공백/특수문자 제거, 소문자 변환)
*/
function normalizeText(text) {
if (!text) return '';
return text.toLowerCase().replace(/[\s\-_.,:;!@#$%^&*()+=\[\]{}|\\/<>?'"]/g, '');
}
export default function PatrolShipSelector() {
const features = useShipStore((s) => s.features);
const showShipSelector = useTrackingModeStore((s) => s.showShipSelector);
const closeShipSelector = useTrackingModeStore((s) => s.closeShipSelector);
const selectTrackedShip = useTrackingModeStore((s) => s.selectTrackedShip);
const setMapMode = useTrackingModeStore((s) => s.setMapMode);
const setRadius = useTrackingModeStore((s) => s.setRadius);
const currentRadius = useTrackingModeStore((s) => s.radiusNM);
const [searchValue, setSearchValue] = useState('');
const [selectedRadius, setSelectedRadius] = useState(currentRadius);
const containerRef = useRef(null);
const searchInputRef = useRef(null);
//
useEffect(() => {
if (showShipSelector && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [showShipSelector]);
//
useEffect(() => {
if (!showShipSelector) return;
const handleClickOutside = (e) => {
if (containerRef.current && !containerRef.current.contains(e.target)) {
// (TopBar )
if (e.target.closest('.ship')) return;
closeShipSelector();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showShipSelector, closeShipSelector]);
//
useEffect(() => {
if (!showShipSelector) {
setSearchValue('');
}
}, [showShipSelector]);
//
const patrolShips = useMemo(() => {
const ships = [];
features.forEach((ship, featureId) => {
if (isPatrolShip(ship.originalTargetId)) {
ships.push({
featureId,
ship,
shipName: ship.shipName || ship.originalTargetId || '-',
originalTargetId: ship.originalTargetId,
});
}
});
//
ships.sort((a, b) => a.shipName.localeCompare(b.shipName, 'ko'));
return ships;
}, [features]);
//
const filteredShips = useMemo(() => {
const normalizedSearch = normalizeText(searchValue);
if (!normalizedSearch) return patrolShips;
return patrolShips.filter((item) => {
const normalizedName = normalizeText(item.shipName);
const normalizedId = normalizeText(item.originalTargetId);
return normalizedName.includes(normalizedSearch) || normalizedId.includes(normalizedSearch);
});
}, [patrolShips, searchValue]);
//
const handleSelectShip = useCallback((item) => {
setRadius(selectedRadius);
selectTrackedShip(item.featureId, item.ship);
setSearchValue('');
}, [selectTrackedShip, setRadius, selectedRadius]);
// ( )
const handleCancel = useCallback(() => {
setMapMode();
setSearchValue('');
}, [setMapMode]);
//
const handleSearchChange = useCallback((e) => {
setSearchValue(e.target.value);
}, []);
//
const handleClearSearch = useCallback(() => {
setSearchValue('');
searchInputRef.current?.focus();
}, []);
//
const handleRadiusChange = useCallback((radius) => {
setSelectedRadius(radius);
}, []);
if (!showShipSelector) return null;
return (
<div className="patrol-ship-selector" ref={containerRef}>
{/* 헤더 */}
<div className="selector-header">
<span className="selector-title">경비함정 선택</span>
<button type="button" className="close-btn" onClick={handleCancel}>
×
</button>
</div>
{/* 검색 영역 */}
<div className="selector-search">
<div className="search-input-wrapper">
<input
ref={searchInputRef}
type="text"
className="search-input"
placeholder="함정명 또는 ID 검색"
value={searchValue}
onChange={handleSearchChange}
/>
{searchValue && (
<button type="button" className="search-clear-btn" onClick={handleClearSearch}>
×
</button>
)}
</div>
</div>
{/* 반경 설정 */}
<div className="selector-radius">
<span className="radius-label">반경 설정</span>
<div className="radius-options">
{RADIUS_OPTIONS.map((radius) => (
<button
key={radius}
type="button"
className={`radius-btn ${selectedRadius === radius ? 'active' : ''}`}
onClick={() => handleRadiusChange(radius)}
>
{radius}
</button>
))}
<span className="radius-unit">NM</span>
</div>
</div>
{/* 함정 목록 */}
<div className="selector-content">
{filteredShips.length === 0 ? (
<div className="no-ships">
{searchValue ? '검색 결과가 없습니다' : '활성화된 경비함정이 없습니다'}
</div>
) : (
<ul className="ship-list">
{filteredShips.map((item) => (
<li
key={item.featureId}
className="ship-item"
onClick={() => handleSelectShip(item)}
>
<span className="ship-name">{item.shipName}</span>
<span className="ship-id">{item.originalTargetId}</span>
</li>
))}
</ul>
)}
</div>
{/* 푸터 */}
<div className="selector-footer">
<span className="ship-count">
{searchValue ? `${filteredShips.length} / ${patrolShips.length}` : `${patrolShips.length}`}
</span>
</div>
</div>
);
}

파일 보기

@ -1,258 +0,0 @@
/**
* 경비함정 선택 드롭다운 스타일
*/
.patrol-ship-selector {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
width: 32rem;
max-height: 50rem;
background-color: rgba(var(--secondary6-rgb), 0.95);
border-radius: 0.8rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
z-index: 200;
display: flex;
flex-direction: column;
// 헤더
.selector-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.2rem;
border-bottom: 1px solid rgba(var(--white-rgb), 0.1);
flex-shrink: 0;
.selector-title {
color: var(--white);
font-size: 1.4rem;
font-weight: var(--fw-bold);
}
.close-btn {
width: 2.4rem;
height: 2.4rem;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: rgba(var(--white-rgb), 0.6);
font-size: 2rem;
cursor: pointer;
border-radius: 0.4rem;
transition: all 0.15s ease;
&:hover {
color: var(--white);
background-color: rgba(var(--white-rgb), 0.1);
}
}
}
// 검색 영역
.selector-search {
padding: 0.8rem 1.2rem;
border-bottom: 1px solid rgba(var(--white-rgb), 0.1);
flex-shrink: 0;
.search-input-wrapper {
position: relative;
width: 100%;
}
.search-input {
width: 100%;
height: 3.2rem;
padding: 0 3rem 0 1rem;
border: 1px solid rgba(var(--white-rgb), 0.2);
border-radius: 0.4rem;
background-color: rgba(var(--white-rgb), 0.05);
color: var(--white);
font-size: 1.3rem;
outline: none;
transition: all 0.15s ease;
&::placeholder {
color: rgba(var(--white-rgb), 0.4);
}
&:focus {
border-color: var(--primary1);
background-color: rgba(var(--white-rgb), 0.1);
}
}
.search-clear-btn {
position: absolute;
top: 50%;
right: 0.6rem;
transform: translateY(-50%);
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: rgba(var(--white-rgb), 0.5);
font-size: 1.6rem;
cursor: pointer;
border-radius: 0.3rem;
&:hover {
color: var(--white);
background-color: rgba(var(--white-rgb), 0.1);
}
}
}
// 반경 설정
.selector-radius {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.8rem 1.2rem;
border-bottom: 1px solid rgba(var(--white-rgb), 0.1);
flex-shrink: 0;
.radius-label {
color: rgba(var(--white-rgb), 0.7);
font-size: 1.2rem;
flex-shrink: 0;
}
.radius-options {
display: flex;
align-items: center;
gap: 0.4rem;
}
.radius-btn {
min-width: 3.6rem;
height: 2.6rem;
padding: 0 0.6rem;
border: 1px solid rgba(var(--white-rgb), 0.2);
border-radius: 0.4rem;
background-color: transparent;
color: rgba(var(--white-rgb), 0.7);
font-size: 1.2rem;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
border-color: rgba(var(--white-rgb), 0.4);
color: var(--white);
}
&.active {
border-color: var(--primary1);
background-color: var(--primary1);
color: var(--white);
}
}
.radius-unit {
color: rgba(var(--white-rgb), 0.5);
font-size: 1.1rem;
margin-left: 0.3rem;
}
}
// 함정 목록 영역
.selector-content {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
min-height: 10rem;
max-height: 28rem;
// 스크롤바 스타일
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(var(--white-rgb), 0.3);
border-radius: 3px;
&:hover {
background: rgba(var(--white-rgb), 0.5);
}
}
}
.no-ships {
padding: 3rem 1.2rem;
text-align: center;
color: rgba(var(--white-rgb), 0.5);
font-size: 1.3rem;
}
.ship-list {
list-style: none;
padding: 0.5rem 0;
margin: 0;
display: flex;
flex-direction: column;
// TopBar li 스타일 상속 초기화
li {
height: auto;
padding: 0;
}
}
.ship-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.9rem 1.2rem !important; // TopBar li:first-child, li:last-child 오버라이드
cursor: pointer;
transition: background-color 0.15s ease;
width: 100%;
box-sizing: border-box;
height: auto !important; // TopBar li height: 100% 오버라이드
&:hover {
background-color: rgba(var(--primary1-rgb), 0.3);
}
.ship-name {
color: var(--white);
font-size: 1.3rem;
font-weight: var(--fw-bold);
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ship-id {
color: rgba(var(--white-rgb), 0.5);
font-size: 1.1rem;
margin-left: 1rem;
flex-shrink: 0;
}
}
// 푸터
.selector-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0.8rem 1.2rem;
border-top: 1px solid rgba(var(--white-rgb), 0.1);
flex-shrink: 0;
.ship-count {
color: rgba(var(--white-rgb), 0.6);
font-size: 1.2rem;
}
}
}

파일 보기

@ -10,7 +10,6 @@ import { toLonLat } from 'ol/proj';
import { useMapStore } from '../../stores/mapStore'; import { useMapStore } from '../../stores/mapStore';
import useTrackingModeStore from '../../stores/trackingModeStore'; import useTrackingModeStore from '../../stores/trackingModeStore';
import useShipSearch from '../../hooks/useShipSearch'; import useShipSearch from '../../hooks/useShipSearch';
import PatrolShipSelector from './PatrolShipSelector';
import './TopBar.scss'; import './TopBar.scss';
/** /**
@ -353,8 +352,6 @@ export default function TopBar() {
</div> </div>
)} )}
{/* 함정 선택 드롭다운 */}
<PatrolShipSelector />
</li> </li>
</ul> </ul>
</div> </div>

파일 보기

@ -6,7 +6,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import useShipStore from '../../stores/shipStore'; import useShipStore from '../../stores/shipStore';
import { useTrackQueryStore } from '../../tracking/stores/trackQueryStore'; import { useTrackQueryStore } from '../../tracking/stores/trackQueryStore';
import useTrackingModeStore, { RADIUS_OPTIONS, isPatrolShip } from '../../stores/trackingModeStore'; import useTrackingModeStore, { RADIUS_OPTIONS } from '../../stores/trackingModeStore';
import { import {
fetchVesselTracksV2, fetchVesselTracksV2,
convertToProcessedTracks, convertToProcessedTracks,
@ -184,12 +184,12 @@ export default function ShipContextMenu() {
const { x, y, ships } = contextMenu; const { x, y, ships } = contextMenu;
// ( ) //
const isSinglePatrolShip = ships.length === 1 && isPatrolShip(ships[0].originalTargetId); const isSingleShip = ships.length === 1;
// //
const visibleMenuItems = MENU_ITEMS.filter((item) => { const visibleMenuItems = MENU_ITEMS.filter((item) => {
if (item.key === 'radius') return isSinglePatrolShip; if (item.key === 'radius') return isSingleShip;
return true; return true;
}); });

파일 보기

@ -1,160 +0,0 @@
import { useEffect, useRef } from 'react';
import VectorImageLayer from 'ol/layer/VectorImage';
import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON';
import { Style, Fill, Stroke, Text } from 'ol/style';
import { deserialize } from 'flatgeobuf/lib/mjs/geojson.js';
import { useMapStore, THEME_TYPES } from '../stores/mapStore';
const BASE_URL = import.meta.env.BASE_URL || '/';
const FGB_URL = `${BASE_URL}fgb/해경관할구역.fgb`;
/** 테마별 색상 정의 */
const THEME_STYLE = {
[THEME_TYPES.DARK]: {
lineColor: 'rgba(100, 200, 255, 0.8)',
textColor: 'rgba(100, 200, 255, 0.9)',
textStrokeColor: 'rgba(0, 0, 0, 0.6)',
textStrokeWidth: 1,
font: 'bold 1.1rem "NanumSquare", sans-serif',
},
[THEME_TYPES.LIGHT]: {
lineColor: 'rgba(20, 60, 100, 0.7)',
textColor: 'rgba(20, 60, 100, 0.8)',
textStrokeColor: 'rgba(255, 255, 255, 0.7)',
textStrokeWidth: 1,
font: 'bold 1.1rem "NanumSquare", sans-serif',
},
};
/**
* 해경관할구역 스타일 팩토리
* 테마에 따라 스타일 함수를 생성
*/
function createKcgAreaStyle(theme) {
const ts = THEME_STYLE[theme] || THEME_STYLE[THEME_TYPES.DARK];
return (feature) => {
const areaName = feature.get('해역명');
const isSpecial = areaName != null && areaName.includes('특별');
if (isSpecial) {
return [
new Style({
stroke: new Stroke({
color: 'rgba(255, 80, 80, 0.8)',
lineDash: [5, 5],
width: 2,
}),
fill: new Fill({ color: 'rgba(255,255,255,0)' }),
text: new Text({
offsetY: -15,
text: areaName || '',
font: ts.font,
fill: new Fill({ color: 'rgba(255, 80, 80, 0.9)' }),
stroke: new Stroke({ color: ts.textStrokeColor, width: ts.textStrokeWidth }),
}),
zIndex: 999,
}),
];
}
return [
new Style({
stroke: new Stroke({ color: ts.lineColor, width: 2 }),
fill: new Fill({ color: 'rgba(255,255,255,0)' }),
text: new Text({
offsetY: -15,
text: areaName || '',
font: ts.font,
fill: new Fill({ color: ts.textColor }),
stroke: new Stroke({ color: ts.textStrokeColor, width: ts.textStrokeWidth }),
}),
}),
];
};
}
/**
* 해경관할구역 FGB 레이어 관리
* 참조: mda-react-front/src/common/targetLayer.ts - kcgWatchZoneLayer, setFGBFeatures
*/
export default function useCoastGuardLayer() {
const map = useMapStore((s) => s.map);
const layerRef = useRef(null);
const loadedRef = useRef(false);
useEffect(() => {
if (!map) return;
const currentTheme = useMapStore.getState().getTheme();
const source = new VectorSource();
const layer = new VectorImageLayer({
source,
zIndex: 45,
style: createKcgAreaStyle(currentTheme),
declutter: true,
visible: useMapStore.getState().isCoastGuardVisible,
});
map.addLayer(layer);
layerRef.current = layer;
// FGB 파일 로드 (1회)
if (!loadedRef.current) {
loadedRef.current = true;
loadFgb(source);
}
// visible 토글 구독
const unsubVisible = useMapStore.subscribe(
(state) => state.isCoastGuardVisible,
(isVisible) => {
if (layerRef.current) {
layerRef.current.setVisible(isVisible);
}
},
);
// 배경지도(테마) 변경 구독 → 스타일 재적용
const unsubTheme = useMapStore.subscribe(
(state) => state.baseMapType,
() => {
if (layerRef.current) {
const theme = useMapStore.getState().getTheme();
layerRef.current.setStyle(createKcgAreaStyle(theme));
}
},
);
return () => {
unsubVisible();
unsubTheme();
if (map && layerRef.current) {
map.removeLayer(layerRef.current);
}
layerRef.current = null;
};
}, [map]);
}
/**
* FlatGeobuf 파일 로드 VectorSource에 Feature 추가
*/
async function loadFgb(source) {
try {
const response = await fetch(FGB_URL);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const format = new GeoJSON();
for await (const geojsonFeature of deserialize(response.body)) {
const feature = format.readFeature(geojsonFeature);
source.addFeature(feature);
}
console.log(`[useCoastGuardLayer] 해경관할구역 ${source.getFeatures().length}건 로드 완료`);
} catch (err) {
console.warn('[useCoastGuardLayer] FGB 로드 실패:', err);
}
}

파일 보기

@ -1,259 +1,120 @@
/** /**
* 선박 데이터 관리 * 선박 데이터 관리 (HTTP 폴링 방식)
* - 초기 선박 데이터 API 로드 (/all/12) * - 초기 로드: AIS Target API에서 최근 60 데이터 전체 조회
* - STOMP WebSocket 연결 구독 * - 이후 1분마다 최근 2 데이터를 증분 조회하여 스토어에 병합
* - Web Worker를 통한 데이터 파싱 (메인 스레드 부담 감소)
* - 배치 머지 최적화 (500ms 인터벌)
*
* 참조: mda-react-front/src/map/MapUpdater.tsx
* 위성통신망 환경 최적화: 최소 트래픽, 최소 스펙
*/ */
import { useEffect, useRef, useCallback, useState } from 'react'; import { useEffect, useRef, useCallback, useState } from 'react';
import {
signalStompClient,
connectStomp,
disconnectStomp,
subscribeShipsRaw,
subscribeShipDelete,
} from '../common/stompClient';
import useShipStore from '../stores/shipStore'; import useShipStore from '../stores/shipStore';
import { fetchAllSignalsRaw } from '../api/signalApi'; import { searchAisTargets, aisTargetToFeature } from '../api/aisTargetApi';
// ===================== /** 폴링 간격 (ms) */
// Web Worker 인스턴스 생성 const POLLING_INTERVAL_MS = 60 * 1000; // 1분
// =====================
const SignalWorker = new Worker(
new URL('../workers/signalWorker.js', import.meta.url),
{ type: 'module' }
);
// ===================== /** 초기 로드 기간 (분) */
// 배치 처리 설정 const INITIAL_LOAD_MINUTES = 60;
// 참조: mda-react-front/src/map/MapUpdater.tsx
// ===================== /** 증분 로드 기간 (분) */
const WEBSOCKET_CHUNK_INTERVAL = 500; // WebSocket 데이터 청크 처리 주기 (ms) const INCREMENT_MINUTES = 2; // 약간의 중복 허용으로 누락 방지
/** /**
* 선박 데이터 관리 * 선박 데이터 관리
* @param {Object} options - 옵션 * @param {Object} options - 옵션
* @param {boolean} options.autoConnect - 자동 연결 여부 (기본값: true) * @param {boolean} options.autoConnect - 자동 시작 여부 (기본값: true)
* @returns {Object} { isConnected, isLoading, connect, disconnect } * @returns {Object} { isConnected, isLoading, connect, disconnect }
*/ */
export default function useShipData(options = {}) { export default function useShipData(options = {}) {
const { autoConnect = true } = options; const { autoConnect = true } = options;
const subscriptionsRef = useRef([]); const pollingRef = useRef(null);
const shipBufferRef = useRef([]); // Raw 문자열 버퍼
const batchIntervalRef = useRef(null); // 배치 처리 인터벌
const initialLoadDoneRef = useRef(false); const initialLoadDoneRef = useRef(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const mergeFeatures = useShipStore((s) => s.mergeFeatures); const mergeFeatures = useShipStore((s) => s.mergeFeatures);
const deleteFeatureById = useShipStore((s) => s.deleteFeatureById);
const setConnected = useShipStore((s) => s.setConnected); const setConnected = useShipStore((s) => s.setConnected);
const isConnected = useShipStore((s) => s.isConnected); const isConnected = useShipStore((s) => s.isConnected);
/** /**
* Worker 메시지 핸들러 (파싱된 선박 데이터 수신) * AIS 데이터를 shipStore feature 형식으로 변환하여 머지
*/ */
const handleWorkerMessage = useCallback((e) => { const loadAndMerge = useCallback(async (minutes) => {
const ships = e.data; try {
if (ships.length > 0) { const aisTargets = await searchAisTargets(minutes);
mergeFeatures(ships); if (aisTargets.length > 0) {
const features = aisTargets.map(aisTargetToFeature);
mergeFeatures(features);
console.log(`[useShipData] Merged ${features.length} ships (${minutes}min)`);
}
return aisTargets.length;
} catch (error) {
console.error('[useShipData] Polling error:', error);
return 0;
} }
}, [mergeFeatures]); }, [mergeFeatures]);
/** /**
* Worker 설정 * 폴링 시작
*/ */
useEffect(() => { const startPolling = useCallback(() => {
SignalWorker.onmessage = handleWorkerMessage; if (pollingRef.current) return;
SignalWorker.onerror = (err) => {
console.error('[SignalWorker] Error:', err);
};
return () => { pollingRef.current = setInterval(() => {
SignalWorker.onmessage = null; loadAndMerge(INCREMENT_MINUTES);
SignalWorker.onerror = null; }, POLLING_INTERVAL_MS);
};
}, [handleWorkerMessage]); console.log(`[useShipData] Polling started: ${POLLING_INTERVAL_MS}ms interval`);
}, [loadAndMerge]);
/** /**
* Raw 선박 메시지 수신 핸들러 (버퍼에 누적) * 폴링 중지
* @param {string[]} lines - 파이프 구분 문자열 배열
*/ */
const handleShipMessageRaw = useCallback((lines) => { const stopPolling = useCallback(() => {
// 버퍼에 추가 if (pollingRef.current) {
shipBufferRef.current.push(...lines); clearInterval(pollingRef.current);
pollingRef.current = null;
console.log('[useShipData] Polling stopped');
}
}, []); }, []);
/** /**
* 버퍼 플러시 - Worker로 전송 * 연결 (초기 로드 + 폴링 시작)
*/ */
const flushBuffer = useCallback(() => { const connect = useCallback(async () => {
if (shipBufferRef.current.length === 0) return; if (initialLoadDoneRef.current) {
startPolling();
// 버퍼 복사 후 초기화
const rawMessages = shipBufferRef.current;
shipBufferRef.current = [];
// Worker로 전송 (파싱은 Worker에서 수행)
SignalWorker.postMessage(rawMessages);
}, []);
/**
* 배치 처리 인터벌 시작
*/
const startBatchInterval = useCallback(() => {
if (batchIntervalRef.current) return;
batchIntervalRef.current = setInterval(() => {
flushBuffer();
}, WEBSOCKET_CHUNK_INTERVAL);
console.log(`[useShipData] Batch interval started: ${WEBSOCKET_CHUNK_INTERVAL}ms`);
}, [flushBuffer]);
/**
* 배치 처리 인터벌 중지
*/
const stopBatchInterval = useCallback(() => {
if (batchIntervalRef.current) {
clearInterval(batchIntervalRef.current);
batchIntervalRef.current = null;
}
// 남은 버퍼 플러시
flushBuffer();
}, [flushBuffer]);
/**
* 선박 삭제 메시지 수신 핸들러
* @param {string} featureId - signalSourceCode + targetId
*/
const handleShipDelete = useCallback((featureId) => {
deleteFeatureById(featureId);
}, [deleteFeatureById]);
/**
* 토픽 구독 시작
*/
const startSubscriptions = useCallback(() => {
// 기존 구독 해제
subscriptionsRef.current.forEach((sub) => {
try {
sub.unsubscribe();
} catch (e) {
// ignore
}
});
subscriptionsRef.current = [];
// 선박 토픽 구독 (Raw 모드 - Worker용)
const shipSub = subscribeShipsRaw(handleShipMessageRaw);
subscriptionsRef.current.push(shipSub);
// 선박 삭제 토픽 구독
const deleteSub = subscribeShipDelete(handleShipDelete);
subscriptionsRef.current.push(deleteSub);
// 배치 처리 인터벌 시작
startBatchInterval();
}, [handleShipMessageRaw, handleShipDelete, startBatchInterval]);
/**
* 연결 성공 토픽 구독
*/
const handleConnect = useCallback(() => {
setConnected(true); setConnected(true);
startSubscriptions(); return;
}, [setConnected, startSubscriptions]);
/**
* 연결 해제
*/
const handleDisconnect = useCallback(() => {
setConnected(false);
stopBatchInterval();
}, [setConnected, stopBatchInterval]);
/**
* 에러 발생
*/
const handleError = useCallback(() => {
setConnected(false);
}, [setConnected]);
/**
* STOMP 연결 시작
*/
const connect = useCallback(() => {
connectStomp({
onConnect: handleConnect,
onDisconnect: handleDisconnect,
onError: handleError,
});
}, [handleConnect, handleDisconnect, handleError]);
/**
* STOMP 연결 해제
*/
const disconnect = useCallback(() => {
// 배치 처리 인터벌 중지
stopBatchInterval();
// 구독 해제
subscriptionsRef.current.forEach((sub) => {
try {
sub.unsubscribe();
} catch (e) {
// ignore
} }
});
subscriptionsRef.current = [];
disconnectStomp();
}, [stopBatchInterval]);
/**
* 초기 선박 데이터 로드 (API 호출)
* Worker를 통해 파싱
*/
const loadInitialData = useCallback(async () => {
if (initialLoadDoneRef.current) return;
setIsLoading(true); setIsLoading(true);
try { try {
console.log('[useShipData] Loading initial ship data...'); console.log('[useShipData] Loading initial ship data...');
const rawLines = await fetchAllSignalsRaw(); const count = await loadAndMerge(INITIAL_LOAD_MINUTES);
console.log(`[useShipData] Initial load complete: ${count} ships`);
if (rawLines.length > 0) { initialLoadDoneRef.current = true;
// Worker로 전송하여 파싱 setConnected(true);
SignalWorker.postMessage(rawLines); startPolling();
console.log(`[useShipData] Initial data sent to Worker: ${rawLines.length} ships`);
}
} catch (error) { } catch (error) {
console.error('[useShipData] Initial load error:', error); console.error('[useShipData] Initial load error:', error);
} finally { } finally {
initialLoadDoneRef.current = true;
setIsLoading(false); setIsLoading(false);
} }
}, []); }, [loadAndMerge, startPolling, setConnected]);
// 초기화: API로 선박 데이터 로드 후 STOMP 연결 /**
* 연결 해제
*/
const disconnect = useCallback(() => {
stopPolling();
setConnected(false);
}, [stopPolling, setConnected]);
// 자동 시작
useEffect(() => { useEffect(() => {
if (!autoConnect) return; if (!autoConnect) return;
const initialize = async () => {
await loadInitialData();
connect(); connect();
};
initialize();
return () => { return () => {
stopBatchInterval(); stopPolling();
disconnect();
}; };
}, [autoConnect]); // loadInitialData, connect, disconnect, stopBatchInterval를 deps에서 제외 (의도적) }, [autoConnect]); // connect, stopPolling를 deps에서 제외 (의도적)
return { return {
isConnected, isConnected,

파일 보기

@ -7,7 +7,6 @@ import { defaults as defaultInteractions, DragBox } from 'ol/interaction';
import { platformModifierKeyOnly } from 'ol/events/condition'; import { platformModifierKeyOnly } from 'ol/events/condition';
import { createBaseLayers } from './layers/baseLayer'; import { createBaseLayers } from './layers/baseLayer';
import { satelliteLayer, csvDeckLayer } from './layers/satelliteLayer';
import { useMapStore, BASE_MAP_TYPES } from '../stores/mapStore'; import { useMapStore, BASE_MAP_TYPES } from '../stores/mapStore';
import useShipStore from '../stores/shipStore'; import useShipStore from '../stores/shipStore';
import useShipData from '../hooks/useShipData'; import useShipData from '../hooks/useShipData';
@ -46,7 +45,6 @@ import useMeasure from './measure/useMeasure';
import useTrackingMode from '../hooks/useTrackingMode'; import useTrackingMode from '../hooks/useTrackingMode';
import useFavoriteData from '../hooks/useFavoriteData'; import useFavoriteData from '../hooks/useFavoriteData';
import useRealmLayer from '../hooks/useRealmLayer'; import useRealmLayer from '../hooks/useRealmLayer';
import useCoastGuardLayer from '../hooks/useCoastGuardLayer';
import './measure/measure.scss'; import './measure/measure.scss';
import './MapContainer.scss'; import './MapContainer.scss';
@ -80,9 +78,6 @@ export default function MapContainer() {
// OL // OL
useRealmLayer(); useRealmLayer();
// FGB
useCoastGuardLayer();
// Deck.gl // Deck.gl
const { deckRef } = useShipLayer(map); const { deckRef } = useShipLayer(map);
@ -455,8 +450,6 @@ export default function MapContainer() {
worldMap, worldMap,
encMap, encMap,
darkMap, darkMap,
satelliteLayer,
csvDeckLayer,
eastAsiaMap, eastAsiaMap,
korMap, korMap,
], ],

파일 보기

@ -1,131 +0,0 @@
/**
* 위성영상 레이어
* - TIF 영상: OL TileLayer + GeoServer WMS
* - CSV 선박 : Deck.gl ScatterplotLayer + 별도 Deck 인스턴스 OL WebGLTileLayer 래핑
*
* 참조: mda-react-front/src/common/targetLayer.ts (satelliteLayer, deckSatellite )
* 참조: mda-react-front/src/util/satellite.ts (createSatellitePictureLayer, removeSatelliteLayer)
*/
import TileLayer from 'ol/layer/Tile';
import TileWMS from 'ol/source/TileWMS';
import WebGLTileLayer from 'ol/layer/WebGLTile';
import { transformExtent, toLonLat } from 'ol/proj';
import { Deck } from '@deck.gl/core';
import { ScatterplotLayer } from '@deck.gl/layers';
// =====================
// TIF 영상 레이어 (GeoServer WMS)
// =====================
export const satelliteLayer = new TileLayer({
source: new TileWMS({
url: '/geo/geoserver/mda/wms',
params: { tiled: true, LAYERS: '' },
}),
className: 'satellite-map',
zIndex: 10,
visible: false,
});
// =====================
// CSV 선박 점 레이어 (Deck.gl ScatterplotLayer)
// =====================
export const csvScatterLayer = new ScatterplotLayer({
id: 'satellite-csv-layer',
data: [],
getPosition: (d) => d.coordinates,
getFillColor: [232, 232, 21],
getRadius: 3,
radiusUnits: 'pixels',
pickable: false,
});
export const csvDeck = new Deck({
initialViewState: {
longitude: 127.1388684,
latitude: 37.4449168,
zoom: 6,
transitionDuration: 0,
},
controller: false,
layers: [csvScatterLayer],
});
export const csvDeckLayer = new WebGLTileLayer({
source: undefined,
zIndex: 200,
visible: false,
render: (frameState) => {
const { center, zoom } = frameState.viewState;
csvDeck.setProps({
viewState: {
longitude: toLonLat(center)[0],
latitude: toLonLat(center)[1],
zoom: zoom - 1,
},
});
csvDeck.redraw();
return csvDeck.canvas;
},
});
// =====================
// 표출/제거 함수
// =====================
/**
* 위성영상 TIF를 지도에 표출
* @param {import('ol/Map').default} map - OL 인스턴스
* @param {string} tifGeoName - GeoServer 레이어명
* @param {[number,number,number,number]} extent - [minX, minY, maxX, maxY] EPSG:4326
* @param {number} opacity - 0~1
* @param {number} brightness - 0~200 (%)
*/
export function showSatelliteImage(map, tifGeoName, extent, opacity, brightness) {
const extent3857 = transformExtent(extent, 'EPSG:4326', 'EPSG:3857');
const source = new TileWMS({
url: '/geo/geoserver/mda/wms',
params: { tiled: true, LAYERS: tifGeoName },
hidpi: false,
transition: 0,
});
satelliteLayer.setExtent(extent3857);
satelliteLayer.setSource(source);
satelliteLayer.setOpacity(Number(opacity));
satelliteLayer.setVisible(true);
// CSS brightness 적용
const el = document.querySelector('.satellite-map');
if (el) {
el.style.filter = `brightness(${brightness}%)`;
}
// 해당 영상 범위로 지도 이동
map.getView().fit(extent3857);
// 타일 로딩 강제 트리거
source.refresh();
}
/**
* CSV 선박 좌표를 Deck.gl ScatterplotLayer로 표시
* @param {Array<{ coordinates: [number, number] }>} features
*/
export function showCsvFeatures(features) {
const layer = csvScatterLayer.clone({ data: features });
csvDeck.setProps({ layers: [layer] });
csvDeckLayer.setVisible(true);
}
/**
* 위성영상 + CSV 레이어 제거
*/
export function hideSatelliteImage() {
satelliteLayer.setVisible(false);
satelliteLayer.setSource(null);
const emptyLayer = csvScatterLayer.clone({ data: [] });
csvDeck.setProps({ layers: [emptyLayer] });
csvDeckLayer.setVisible(false);
}

파일 보기

@ -1,57 +0,0 @@
import { useState } from 'react';
import SatelliteImageManage from '@/satellite/components/SatelliteImageManage';
import SatelliteProviderManage from '@/satellite/components/SatelliteProviderManage';
import SatelliteManage from '@/satellite/components/SatelliteManage';
const tabs = [
{ id: 'satellite01', label: '위성영상 관리' },
{ id: 'satellite02', label: '위성사업자 관리' },
{ id: 'satellite03', label: '위성 관리' },
];
const tabComponents = {
satellite01: SatelliteImageManage,
satellite02: SatelliteProviderManage,
satellite03: SatelliteManage,
};
export default function SatellitePage({ isOpen, onToggle }) {
const [activeTab, setActiveTab] = useState('satellite01');
const ActiveComponent = tabComponents[activeTab];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 */}
{ActiveComponent && <ActiveComponent />}
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,63 +0,0 @@
import { useState } from 'react';
import WeatherAlert from '@/weather/components/WeatherAlert';
import TyphoonInfo from '@/weather/components/TyphoonInfo';
import TidalObservation from '@/weather/components/TidalObservation';
import TidalInfo from '@/weather/components/TidalInfo';
import AviationWeather from '@/weather/components/AviationWeather';
const tabs = [
{ id: 'weather01', label: '기상특보' },
{ id: 'weather02', label: '태풍정보' },
{ id: 'weather03', label: '조위관측' },
{ id: 'weather04', label: '조석정보' },
{ id: 'weather05', label: '항공기상' },
];
const tabComponents = {
weather01: WeatherAlert,
weather02: TyphoonInfo,
weather03: TidalObservation,
weather04: TidalInfo,
weather05: AviationWeather,
};
export default function WeatherPage({ isOpen, onToggle }) {
const [activeTab, setActiveTab] = useState('weather01');
const ActiveComponent = tabComponents[activeTab];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 */}
{ActiveComponent && <ActiveComponent />}
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,134 +0,0 @@
import { Route } from 'react-router-dom';
//
import WrapComponent from './layouts/WrapComponent';
import HeaderComponent from './layouts/HeaderComponent';
import SideComponent from './layouts/SideComponent';
import MainComponent from './layouts/MainComponent';
//
import Panel1Component from './pages/Panel1Component';
import Panel2Component from './pages/Panel2Component';
import Panel3Component from './pages/Panel3Component';
import Panel4Component from './pages/Panel4Component';
import Panel5Component from './pages/Panel5Component';
import Panel6Component from './pages/Panel6Component';
import Panel7Component from './pages/Panel7Component';
import Panel8Component from './pages/Panel8Component';
/**
* 퍼블리시 라우트 정의
* - /publish/* 하위에서 퍼블리시 파일들을 미리볼 있음
*/
const PublishRoutes = (
<>
{/* 기본 페이지 - 전체 레이아웃 미리보기 */}
<Route index element={<PublishHome />} />
{/* 개별 패널 미리보기 */}
<Route path="panel1/*" element={<Panel1Wrapper />} />
<Route path="panel2/*" element={<Panel2Wrapper />} />
<Route path="panel3/*" element={<Panel3Wrapper />} />
<Route path="panel4/*" element={<Panel4Wrapper />} />
<Route path="panel5/*" element={<Panel5Wrapper />} />
<Route path="panel6/*" element={<Panel6Wrapper />} />
<Route path="panel7/*" element={<Panel7Wrapper />} />
<Route path="panel8/*" element={<Panel8Wrapper />} />
{/* 전체 레이아웃 (원본 구조 그대로) */}
<Route path="full/*" element={<WrapComponent />} />
</>
);
//
function PublishHome() {
return (
<div className="publish-home">
<h1>퍼블리시 미리보기</h1>
<p>좌측 메뉴에서 확인할 페이지를 선택하세요.</p>
<div className="publish-info">
<h2>폴더 구조</h2>
<pre>
{`src/publish/
_incoming/ # 퍼블리시 파일 (원본)
layouts/ # 레이아웃 컴포넌트
pages/ # 페이지 컴포넌트
components/ # 공통 컴포넌트`}
</pre>
<h2>병합 방법</h2>
<ol>
<li> 퍼블리시 파일을 <code>_incoming/</code> 폴더에 복사</li>
<li>Claude에게 병합 요청</li>
<li>변경사항 확인 적용</li>
</ol>
</div>
</div>
);
}
//
function Panel1Wrapper() {
return (
<div className="panel-wrapper">
<Panel1Component isOpen={true} onToggle={() => {}} />
</div>
);
}
function Panel2Wrapper() {
return (
<div className="panel-wrapper">
<Panel2Component isOpen={true} onToggle={() => {}} />
</div>
);
}
function Panel3Wrapper() {
return (
<div className="panel-wrapper">
<Panel3Component isOpen={true} onToggle={() => {}} />
</div>
);
}
function Panel4Wrapper() {
return (
<div className="panel-wrapper">
<Panel4Component isOpen={true} onToggle={() => {}} />
</div>
);
}
function Panel5Wrapper() {
return (
<div className="panel-wrapper">
<Panel5Component isOpen={true} onToggle={() => {}} />
</div>
);
}
function Panel6Wrapper() {
return (
<div className="panel-wrapper">
<Panel6Component isOpen={true} onToggle={() => {}} />
</div>
);
}
function Panel7Wrapper() {
return (
<div className="panel-wrapper">
<Panel7Component isOpen={true} onToggle={() => {}} />
</div>
);
}
function Panel8Wrapper() {
return (
<div className="panel-wrapper">
<Panel8Component isOpen={true} onToggle={() => {}} />
</div>
);
}
export default PublishRoutes;

파일 보기

@ -1,35 +0,0 @@
import React, { useState } from 'react';
export default function FileUpload({ label = "파일 선택", inputId, maxLength = 25, placeholder = "선택된 파일 없음" }) {
const [fileName, setFileName] = useState('');
//
const truncateMiddle = (str, maxLen) => {
if (!str) return '';
if (str.length <= maxLen) return str;
const keep = Math.floor((maxLen - 3) / 2);
return str.slice(0, keep) + '...' + str.slice(str.length - keep);
};
const handleChange = (e) => {
const name = e.target.files[0]?.name || '';
setFileName(name);
};
return (
<div className="fileWrap">
<input
type="file"
id={inputId}
className="fileInput"
onChange={handleChange}
/>
<label htmlFor={inputId} className="fileLabel">
{label}
</label>
<span className="fileName">
{fileName ? truncateMiddle(fileName, maxLength) : placeholder}
</span>
</div>
);
}

파일 보기

@ -1,24 +0,0 @@
import { useState } from "react";
function Slider({ label = "", min = 0, max = 100, defaultValue = 50 }) {
const [value, setValue] = useState(defaultValue);
const percent = ((value - min) / (max - min)) * 100;
return (
<label className="rangeWrap">
<span className="rangeLabel">{label}</span>
<input
type="range"
min={min}
max={max}
value={value}
onChange={(e) => setValue(Number(e.target.value))}
style={{ "--percent": `${percent}%` }}
aria-label={label}
/>
</label>
);
}
export default Slider;

파일 보기

@ -1,21 +0,0 @@
/**
* 퍼블리시 모듈 엔트리 포인트
* 개발 환경에서만 사용되며, 프로덕션 빌드 제외됨
*/
import { Routes, Route } from 'react-router-dom';
import PublishLayout from './layouts/PublishLayout';
import PublishRoutes from './PublishRoutes';
/**
* 퍼블리시 라우터 컴포넌트
* App.jsx에서 lazy loading으로 로드됨
*/
export default function PublishRouter() {
return (
<Routes>
<Route path="*" element={<PublishLayout />}>
{PublishRoutes}
</Route>
</Routes>
);
}

파일 보기

@ -1,36 +0,0 @@
import { Link } from "react-router-dom";
export default function HeaderComponent() {
return(
<header id="header">
<div className="logoArea"><Link to="/main" className="logo"><span className="blind">GIS 함정용</span></Link> <span className="logoTxt">GIS 함정용</span></div>
<aside>
<ul>
<li><Link to="/main" className="alram" title="알람"><i className="badge"></i><span className="blind">알람</span></Link></li>
<li className="setWrap">
<Link
to="/signal"
className="set"
title="설정"
><span className="blind">설정</span>
</Link>
<div className="setMenu">
<Link to="/signal">신호설정</Link>
<Link to="/signal/custom">맞춤설정</Link>
</div>
</li>
<li>
<Link
to="/mypage"
className="user"
title="마이페이지"
><span className="blind">마이페이지</span>
</Link>
</li>
</ul>
</aside>
</header>
)
}

파일 보기

@ -1,11 +0,0 @@
import { Outlet } from "react-router-dom";
import TopComponent from "../pages/TopComponent";
export default function MainComponent() {
return (
<main id="main">
<TopComponent />
<Outlet />
</main>
);
}

파일 보기

@ -1,57 +0,0 @@
import { Outlet, Link, useLocation } from 'react-router-dom';
/**
* 퍼블리시 레이아웃
* - 퍼블리시 파일들을 미리보기 위한 레이아웃
* - 상단에 네비게이션 제공
*/
export default function PublishLayout() {
const location = useLocation();
const currentPath = location.pathname;
const menuItems = [
{ path: '/publish', label: '메인', exact: true },
{ path: '/publish/panel1', label: 'Panel1 (선박)' },
{ path: '/publish/panel2', label: 'Panel2 (위성)' },
{ path: '/publish/panel3', label: 'Panel3 (기상)' },
{ path: '/publish/panel4', label: 'Panel4 (분석)' },
{ path: '/publish/panel5', label: 'Panel5 (타임라인)' },
{ path: '/publish/panel6', label: 'Panel6 (AI모드)' },
{ path: '/publish/panel7', label: 'Panel7 (리플레이)' },
{ path: '/publish/panel8', label: 'Panel8 (항적조회)' },
];
const isActive = (path, exact) => {
if (exact) return currentPath === path;
return currentPath.startsWith(path);
};
return (
<div className="publish-wrapper">
{/* 퍼블리시 네비게이션 */}
<nav className="publish-nav">
<div className="publish-nav-header">
<Link to="/"> 메인으로</Link>
<span className="publish-title">퍼블리시 미리보기</span>
</div>
<ul className="publish-menu">
{menuItems.map((item) => (
<li key={item.path}>
<Link
to={item.path}
className={isActive(item.path, item.exact) ? 'active' : ''}
>
{item.label}
</Link>
</li>
))}
</ul>
</nav>
{/* 퍼블리시 콘텐츠 */}
<div className="publish-content">
<Outlet />
</div>
</div>
);
}

파일 보기

@ -1,94 +0,0 @@
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import NavComponent from "../pages/NavComponent";
import Panel1Component from "../pages/Panel1Component";
import Panel2Component from "../pages/Panel2Component";
import Panel3Component from "../pages/Panel3Component";
import Panel4Component from "../pages/Panel4Component";
import Panel5Component from "../pages/Panel5Component";
import Panel6Component from "../pages/Panel6Component";
import Panel7Component from "../pages/Panel7Component";
import Panel8Component from "../pages/Panel8Component";
import DisplayComponent from "../pages/DisplayComponent";
export default function SideComponent() {
const navigate = useNavigate();
//const location = useLocation();
//
const [activePanel, setActivePanel] = useState("gnb1");
//
const [isPanelOpen, setIsPanelOpen] = useState(true);
const handleTogglePanel = () => setIsPanelOpen(prev => !prev);
// Display
const [displayTab, setDisplayTab] = useState("filter");
/* =========================
Nav 클릭 패널 + 라우팅
========================= */
const handleChangePanel = (key) => {
setIsPanelOpen(true);
//setActivePanel(key); // navigate
switch (key) {
case "gnb8": //
setActivePanel("gnb8");
navigate("/track");
break;
case "gnb7": //
setActivePanel("gnb7");
navigate("/replay");
break;
case "filter": //
case "layer": //
setActivePanel(key);
setDisplayTab(key);
// /
navigate("/main");
break;
default:
setActivePanel(key);
navigate("/main");
break;
}
};
/* =========================
공통 props
========================= */
const panelProps = {
isOpen: isPanelOpen,
onToggle: handleTogglePanel,
};
return (
<section id="sidePanel">
<NavComponent
activeKey={activePanel}
onChange={handleChangePanel}
/>
<div className="sidePanelContent">
{activePanel === "gnb1" && <Panel1Component {...panelProps} />}
{activePanel === "gnb2" && <Panel2Component {...panelProps} />}
{activePanel === "gnb3" && <Panel3Component {...panelProps} />}
{activePanel === "gnb4" && <Panel4Component {...panelProps} />}
{activePanel === "gnb5" && <Panel5Component {...panelProps} />}
{activePanel === "gnb6" && <Panel6Component {...panelProps} />}
{activePanel === "gnb7" && <Panel7Component {...panelProps} />}
{activePanel === "gnb8" && <Panel8Component {...panelProps} />}
{(activePanel === "filter" || activePanel === "layer") && (
<DisplayComponent {...panelProps}
activeTab={displayTab}/>
)}
</div>
</section>
);
}

파일 보기

@ -1,131 +0,0 @@
import { useState } from "react"
export default function ToolComponent() {
const [isLegendOpen, setIsLegendOpen] = useState(false);
return(
<section id="tool">
{/* 툴바 */}
<div className="toolBar">
<ul className="toolItem space">
<li><button type="button" className="tool01">초기화</button></li>
<li><button type="button" className="tool02">선박통합</button></li>
<li><button type="button" className="tool03">구역설정</button></li>
</ul>
<ul className="toolItem mt30">
<li><button type="button" className="tool04">거리</button></li>
<li><button type="button" className="tool05">면적</button></li>
<li><button type="button" className="tool06">거리환</button></li>
</ul>
<ul className="toolItem space mt30">
<li><button type="button" className="tool07">인쇄</button></li>
<li><button type="button" className="tool08">다운로드</button></li>
</ul>
</div>
{/* 맵컨트롤 툴바 */}
<div className="control">
<ul className="toolItem zoom">
<li><button type="button" className="zoomin" title="확대"><span className="blind">확대</span></button></li>
<li className="num">7</li>
<li><button type="button" className="zoomout" title="축소"><span className="blind">축소</span></button></li>
</ul>
<ul className="toolItem space mt30">
<li><button
type="button"
className={`legend ${isLegendOpen ? "active" : ""}`}
onClick={() => setIsLegendOpen(prev => !prev)}
>
범례</button>
</li>
<li><button type="button" className="minimap">미니맵</button></li>
</ul>
</div>
{/* 범례 */}
{isLegendOpen && (
<div className="legendWrap">
<ul className="legendList">
<li className="legendItem">
<span className="legendLabel"><img src="/images/ico_legend_all.svg" alt="통합" />통합</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_china.svg" alt="중국어선" />중국어선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_china_permit.svg" alt="중국어선허가" />중국어선허가</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_japan.svg" alt="일본어선" />일본어선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_danger.svg" alt="위험물" />위험물</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_passenger.svg" alt="여객선" />여객선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_vessel.svg" alt="함정" />함정</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_vessel_radar.svg" alt="함정-RADAR" />함정-RADAR</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_general.svg" alt="일반" />일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_vts_general.svg" alt="VTS-일반" />VTS-일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_vts_radar.svg" alt="VTS-RADAR" />VTS-RADAR</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_vpass.svg" alt="VPASS일반" />VPASS일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_enav_fishing.svg" alt="ENAV어선" />ENAV어선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_enav_danger.svg" alt="ENAV위험물" />ENAV위험물</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_enav_cargo.svg" alt="ENAV화물선" />ENAV화물선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_enav_government.svg" alt="ENAV관공선" />ENAV관공선</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_enav_general.svg" alt="ENAV일반" />ENAV일반</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_dmfhf.svg" alt="D-MF/HF" />D-MF/HF</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_aircraft.svg" alt="항공기" />항공기</span>
<span className="legendValue">0</span>
</li>
<li>
<span className="legendLabel"><img src="/images/ico_legend_nll.svg" alt="NLL" />NLL</span>
<span className="legendValue">0</span>
</li>
</ul>
</div>
)}
</section>
)
}

파일 보기

@ -1,15 +0,0 @@
import { Outlet } from "react-router-dom";
import HeaderComponent from "./HeaderComponent";
import SideComponent from "./SideComponent";
import ToolComponent from "./ToolComponent";
export default function WrapComponent() {
return (
<div id="wrap" className="wrap">
<HeaderComponent />
<SideComponent />
<Outlet /> {/* Main 영역 */}
<ToolComponent />
</div>
);
}

파일 보기

@ -1,48 +0,0 @@
import { useState } from 'react';
import { useNavigate } from "react-router-dom";
export default function Analysis1Component() {
const navigate = useNavigate();
return (
<section id="Analysis2Component">
{/* 위성 영상 등록 팝업 */}
<div className="popupUtillWrap">
<div className="popupUtill w46r">
<div className="puHeader">
<span className="title">관심 해역 설정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => navigate("/main")}
/>
</div>
<div className="puBody p0">
<div className="rowSB gap10">
<button type="button"
className="drawBtn"
onClick={() => navigate("/analysis/result")}
>
<i className="rect"></i>
사각형 그리기
</button>
<button type="button"
className="drawBtn"
onClick={() => navigate("/analysis/result")}
>
<i className="polygon"></i>
다각형 그리기
</button>
</div>
</div>
</div>
</div>
</section>
);
}

파일 보기

@ -1,129 +0,0 @@
import { useState } from 'react';
import { useNavigate } from "react-router-dom";
export default function Analysis2Component() {
const navigate = useNavigate();
return (
<section id="Analysis2Component">
{/* 위성 영상 등록 팝업 */}
<div className="popupUtillWrap">
<div className="popupUtill w61r">
<div className="puHeader">
<span className="title">관심 해역 설정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => navigate("/main")}
/>
</div>
<div className="puBody">
<div className="rowSB gap10 pb10">
<button type="button" className="drawBtn sm">사각형 그리기<i className="rect"></i></button>
<button type="button" className="drawBtn sm">다각형 그리기<i className="polygon"></i></button>
</div>
<table className="table">
<caption>관심 해역 설정 - 해상영역명, 설정 옵션, 좌표,영역 옵션,해상영역명 크기, 해상영역명 색상,윤곽선 굵기,윤곽선 종류,윤곽선 색상,채우기 색상 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">해상영역명</th>
<td colSpan={3}><input type="text" placeholder="해상영역명" aria-label="해상영역명" /></td>
</tr>
<tr>
<th scope="row">설정 옵션</th>
<td colSpan={3}>
<div className="row">
<label className="checkbox checkL"><input type="checkbox" /><span>사용 여부</span></label>
<label className="checkbox checkL"><input type="checkbox" /><span>알림 여부</span></label>
<label className="checkbox checkL"><input type="checkbox" /><span>공유 여부</span></label>
</div>
</td>
</tr>
<tr>
<th scope="row">좌표</th>
<td colSpan={3}>[124,96891368166156, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]<br />
[125,25105622872591, 36.37855817450263]
</td>
</tr>
<tr>
<th scope="row">영역 옵션</th>
<td colSpan={3}>
<div className="row">
<label className="checkbox checkL"><input type="checkbox" /><span>해상영역 표시</span></label>
<label className="checkbox checkL"><input type="checkbox" /><span>해상영역명 표시</span></label>
</div>
</td>
</tr>
<tr>
<th scope="row">해상영역명 크기</th>
<td>
<div className="numInput">
<input type="number" placeholder="0" min="" max="" aria-label="해상영역명 크기" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</td>
<th scope="row">해상영역명 색상</th>
<td><i className="colorBox" style={{ backgroundColor: "#000" }}></i></td>
</tr>
<tr>
<th scope="row">윤곽선 굵기 </th>
<td>
<div className="numInput">
<input type="number" placeholder="0" min="" max="" aria-label="윤곽선 굵기 " />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</td>
<th scope="row">윤곽선 종류 </th>
<td>
<select aria-label="윤곽선 종류 ">
<option value="">선택</option>
<option value="">실선</option>
<option value="">점선</option>
</select>
</td>
</tr>
<tr>
<th scope="row">윤곽선 색상 </th>
<td><i className="colorBox" style={{ backgroundColor: "#FF0000" }}></i></td>
<th scope="row">채우기 색상 </th>
<td><i className="colorBox" style={{ backgroundColor: "#7BEBB1" }}></i></td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button type="button" className="btn basic">저장</button>
<button
type="button"
className="btn dark"
onClick={() => navigate("/main")}
>
취소
</button>
</div>
</div>
</div>
</div>
</section>
);
}

파일 보기

@ -1,198 +0,0 @@
import { useState } from 'react';
import { useNavigate } from "react-router-dom";
export default function Analysis3Component() {
const navigate = useNavigate();
return (
<section id="Analysis2Component">
{/* 위성 영상 등록 팝업 */}
<div className="popupUtillWrap">
<div className="popupUtill w61r">
<div className="puHeader">
<span className="title">관심 해역 분석 등록</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => navigate("/main")}
/>
</div>
<div className="puBody noSc">
<div className="analyRow">
{/* 지도캡쳐/테이블 영역 */}
<div className="reg">
<div className="mapCapture"></div>
<button type="button" className="btn btnMS basic icoCapture">지도캡쳐</button>
<table className="table">
<caption>관심 해역 분석 등록 - 제목, 상세 내역, 공유 여부,공유 그룹 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">제목</th>
<td><input type="text" placeholder="제목" aria-label="제목" /></td>
</tr>
<tr>
<th scope="row">상세 내역</th>
<td>
<textarea placeholder="내용을 입력하세요" aria-label="상세내역"></textarea>
</td>
</tr>
<tr>
<th scope="row">공유 여부</th>
<td >
<div className="row">
<label className="radio radioL"> <input type="radio" name="share" /> <span>공유</span></label>
<label className="radio radioL"> <input type="radio" name="share" /> <span>공유 안함</span></label>
</div>
</td>
</tr>
<tr>
<th scope="row">공유 그룹 </th>
<td>
<select aria-label="윤곽선 종류 ">
<option value="">전체</option>
<option value="">부서</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
{/* 관심영역 체크박스 목록 -스크롤됨 */}
<div className="list" >
<div className="tit14">관심영역 목록</div>
<ul className="lineList rowSB">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>진입진출 테스트</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>테스트 01</span>
</label>
<button type="button" className="btnMap"></button>
</li>
</ul>
</div>
</div>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button type="button" className="btn basic">저장</button>
<button
type="button"
className="btn dark"
onClick={() => navigate("/main")}
>
취소
</button>
</div>
</div>
</div>
</div>
</section>
);
}

파일 보기

@ -1,235 +0,0 @@
import { useState } from 'react';
import { useNavigate } from "react-router-dom";
export default function Analysis4Component() {
const navigate = useNavigate();
return (
<section id="Analysis2Component">
{/* 위성 영상 등록 팝업 */}
<div className="popupUtillWrap">
<div className="popupUtill w61r">
<div className="puHeader">
<div className="headerL">
<span className="title">350 대해구도</span>
<span className="subTxt">조회시간: 2026-07-00 17:15:13</span>
</div>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => navigate("/main")}
/>
</div>
<div className="puBody noSc">
<div className="trenchRow">
{/* 지도캡쳐/테이블 영역 */}
<div className="list">
<div className="tit14">통항 선박</div>
<table className="table dataView">
<caption>통항 선박 - 선박 종류, 승선원, 위험물 운반, 공유 여부 그룹 대한 표입니다</caption>
<colgroup>
<col style={{ width: '135px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">카고()</th>
<td>0</td>
</tr>
<tr>
<th scope="row">카고 승성원()</th>
<td>-</td>
</tr>
<tr>
<th scope="row">탱커수()</th>
<td></td>
</tr>
<tr>
<th scope="row">탱커 승선원()</th>
<td></td>
</tr>
<tr>
<th scope="row">위험물 운반석()</th>
<td></td>
</tr>
<tr>
<th scope="row">위험물 운반선 승선원()</th>
<td></td>
</tr>
<tr>
<th scope="row">위험물 ()</th>
<td></td>
</tr>
<tr>
<th scope="row">어선()</th>
<td></td>
</tr>
<tr>
<th scope="row">어선 승선원()</th>
<td></td>
</tr>
<tr>
<th scope="row">기타 어선()</th>
<td></td>
</tr>
<tr>
<th scope="row">기타 어선 승선원()</th>
<td></td>
</tr>
<tr>
<th scope="row">여객선()</th>
<td></td>
</tr>
<tr>
<th scope="row">유도선()</th>
<td></td>
</tr>
<tr>
<th scope="row">유도선 승선원()</th>
<td></td>
</tr>
<tr>
<th scope="row">기타 선박()</th>
<td></td>
</tr>
<tr>
<th scope="row">기타 선박 승선원()</th>
<td></td>
</tr>
<tr>
<th scope="row">함정수()</th>
<td></td>
</tr>
</tbody>
</table>
</div>
{/* 관심영역 체크박스 목록 -스크롤됨 */}
<div className="list" >
<div className="tit14">신호별</div>
<table className="table dataView">
<caption>신호별 - 제목, 상세 내역, 공유 여부,공유 그룹 대한 내용을 등록하는 표입니다.</caption>
<colgroup>
<col style={{ width: '135px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">AIS</th>
<td>0</td>
</tr>
<tr>
<th scope="row">V-PASS</th>
<td>-</td>
</tr>
<tr>
<th scope="row">VHF</th>
<td></td>
</tr>
<tr>
<th scope="row">MFHF</th>
<td></td>
</tr>
</tbody>
</table>
<div className="tit14">E-NAV</div>
<table className="table dataView">
<caption>E-NAV - 여객선, 어선, 카고, 관공선, 기타 선박과 공유 정보 대한 표입니다.</caption>
<colgroup>
<col style={{ width: '135px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">E-NAV 여객선()</th>
<td>0</td>
</tr>
<tr>
<th scope="row">E-NAV 어선()</th>
<td>-</td>
</tr>
<tr>
<th scope="row">E-NAV 카고()</th>
<td></td>
</tr>
<tr>
<th scope="row">E-NAV 관공선()</th>
<td></td>
</tr>
<tr>
<th scope="row">E-NAV 기타()</th>
<td></td>
</tr>
</tbody>
</table>
<div className="tit14">기상정보</div>
<table className="table dataView">
<caption>기상정보 - 유향, 유속, 유의 파고, 파향, 파주기, 풍속, 풍향 나타내는 표입니다</caption>
<colgroup>
<col style={{ width: '135px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">유향</th>
<td>0</td>
</tr>
<tr>
<th scope="row">유속</th>
<td>-</td>
</tr>
<tr>
<th scope="row">유의 파고</th>
<td>0.5(m)</td>
</tr>
<tr>
<th scope="row">파향</th>
<td>
<div className="rowR gap5">
<img src="/images/ico_dir_arrow.svg" alt="파향" className="arrowDirect"
style={{ transform: 'rotate(350deg)' }}
/>
350(°)
</div>
</td>
</tr>
<tr>
<th scope="row">파주기</th>
<td>3.7(s)</td>
</tr>
<tr>
<th scope="row">풍속</th>
<td>9.2(m/s)</td>
</tr>
<tr>
<th scope="row">풍향</th>
<td>
<div className="rowR gap5">
<img src="/images/ico_dir_arrow.svg" alt="풍향" className="arrowDirect"
style={{ transform: 'rotate(45deg)' }}
/>
45(°)
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div className="puFooter">
</div>
</div>
</div>
</section>
);
}

파일 보기

@ -1,355 +0,0 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import Slider from '../components/Slider';
export default function DisplayComponent({ isOpen, onToggle, activeTab: externalTab }) {
const navigate = useNavigate();
//
const [opacity, setOpacity] = useState(70);
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(true); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(true); //
const [isAccordionOpen3, setIsAccordionOpen3] = useState(true); //
const [isAccordionOpen4, setIsAccordionOpen4] = useState(false); //
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev);
const toggleAccordion3 = () => setIsAccordionOpen3(prev => !prev);
const toggleAccordion4 = () => setIsAccordionOpen4(prev => !prev);
//
const [activeTab, setActiveTab] = useState('filter');
useEffect(() => {
if (externalTab) {
setActiveTab(externalTab);
}
}, [externalTab]);
const tabs = [
{ id: 'filter', label: '필터' },
{ id: 'layer', label: '레이어' },
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox p0">
<div className="tabDefault borderLess">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap scrollY ${activeTab === 'filter' ? 'is-active' : ''}`}>
<div className="tabWrapInner">
<div className="tabWrapCnt">
{/* 스위치그룹 01 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>신호</span>
<label className="switch"> <input type="checkbox" aria-label="신호"/> <span></span></label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen1 ? 'is-open' : ''}`}>
<ul className="switchList">
<li>
<span>AIS</span>
<label className="switch sm"> <input type="checkbox" aria-label="AIS" /> <span></span></label>
</li>
<li>
<span>V-PASS</span>
<label className="switch sm"> <input type="checkbox" aria-label="V-PASS" /> <span></span></label>
</li>
<li>
<span>VTS_AIS</span>
<label className="switch sm"> <input type="checkbox" aria-label="VTS_AIS" /> <span></span></label>
</li>
<li>
<span>D_MF_HF</span>
<label className="switch sm"> <input type="checkbox" aria-label="D_MF_HF" /> <span></span></label>
</li>
<li>
<span>VTS_RADAR</span>
<label className="switch sm"> <input type="checkbox" aria-label="VTS_RADAR" /> <span></span></label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 02 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>선종/기종</span>
<label className="switch"> <input type="checkbox" aria-label="선종/기종" /> <span></span></label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen2 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen2}
onClick={toggleAccordion2}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen2 ? 'is-open' : ''}`}>
<ul className="switchList">
<li>
<span>어선</span>
<label className="switch sm"> <input type="checkbox" aria-label="어선" /> <span></span></label>
</li>
<li>
<span>여객선</span>
<label className="switch sm"> <input type="checkbox" aria-label="여객선" /> <span></span></label>
</li>
<li>
<span>화물선</span>
<label className="switch sm"> <input type="checkbox" aria-label="화물선" /> <span></span></label>
</li>
<li>
<span>유조선</span>
<label className="switch sm"> <input type="checkbox" aria-label="유조선" /> <span></span></label>
</li>
<li>
<span>관공선</span>
<label className="switch sm"> <input type="checkbox" aria-label="관공선" /> <span></span></label>
</li>
<li>
<span>함정</span>
<label className="switch sm"> <input type="checkbox" aria-label="함정" /> <span></span></label>
</li>
<li>
<span>항공기</span>
<label className="switch sm"> <input type="checkbox" aria-label="항공기" /> <span></span></label>
</li>
<li>
<span>기타</span>
<label className="switch sm"> <input type="checkbox" aria-label="기타" /> <span></span></label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 03 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>국적</span>
<label className="switch"> <input type="checkbox" aria-label="국적" /> <span></span></label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen3 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen3}
onClick={toggleAccordion3}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen3 ? 'is-open' : ''}`}>
<ul className="switchList">
<li>
<span>한국</span>
<label className="switch sm"> <input type="checkbox" aria-label="한국" /> <span></span></label>
</li>
<li>
<span>중국</span>
<label className="switch sm"> <input type="checkbox" aria-label="중국" /> <span></span></label>
</li>
<li>
<span>일본</span>
<label className="switch sm"> <input type="checkbox" aria-label="일본" /> <span></span></label>
</li>
<li>
<span>북한</span>
<label className="switch sm"> <input type="checkbox" aria-label="북한" /> <span></span></label>
</li>
<li>
<span>기타</span>
<label className="switch sm"> <input type="checkbox" aria-label="기타" /> <span></span></label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 04 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>AI 모드</span>
<label className="switch"> <input type="checkbox" aria-label="AI 모드" /> <span></span></label>
</div>
<button
type="button"
className={`toggleBtn ${isAccordionOpen4 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen4}
onClick={toggleAccordion4}
></button>
</div>
{/* 여기서부터 토글 */}
<div className={`switchBox ${isAccordionOpen4 ? 'is-open' : ''}`}>
<ul className="switchList">
<li>
<span>MMSI 변조</span>
<label className="switch sm"> <input type="checkbox" aria-label="MMSI 변조" /> <span></span></label>
</li>
<li>
<span>중국 허가선박</span>
<label className="switch sm"> <input type="checkbox" aria-label="중국 허가선박" /> <span></span></label>
</li>
<li>
<span>관공선</span>
<label className="switch sm"> <input type="checkbox" aria-label="관공선" /> <span></span></label>
</li>
<li>
<span>비정상 접촉</span>
<label className="switch sm"> <input type="checkbox" aria-label="비정상 접촉" /> <span></span></label>
</li>
<li>
<span>비정상 선박</span>
<label className="switch sm"> <input type="checkbox" aria-label="비정상 선박" /> <span></span></label>
</li>
<li>
<span>북한선박</span>
<label className="switch sm"> <input type="checkbox" aria-label="북한선박" /> <span></span></label>
</li>
</ul>
</div>
{/* 여기까지 */}
</div>
{/* 스위치그룹 05 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>다크시그널</span>
</div>
<label className="switch"> <input type="checkbox" aria-label="다크시그널" /> <span></span></label>
</div>
</div>
{/* 스위치그룹 06 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<span>위험물</span>
</div>
<label className="switch"> <input type="checkbox" aria-label="위험물" /> <span></span></label>
</div>
</div>
{/* 스위치그룹 07 */}
<div className="switchGroup">
<div className="sgHeader">
<div className="colL">
<i className="favship"></i>
<span>관심선박</span>
</div>
<label className="switch"> <input type="checkbox" aria-label="관심선박" /> <span></span></label>
</div>
</div>
</div>
{/* 버튼영역 */}
<div className="btnBox">
<button type="button" className="btn btnLine">저장</button>
</div>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'layer' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">레이어</div>
</div>
<div className="tabBtm noLine">
<div className="tabBtmInner">
<ul className="lineList tabBtmCnt">
<li className="rowSB">
<label className="checkbox checkL">
<input type="checkbox" />
<span>배경지도</span>
</label>
<div className="row">
<span>투명도 조절</span>
<div>
<Slider label="투명도 조절" />
</div>
</div>
</li>
<li className="p0">
<ul className="optionList">
<li>
<span>전자해도</span>
<label className="radio"> <input type="radio" name="map" aria-label="전자해도" /> <span></span></label>
</li>
<li>
<span>일반지도</span>
<label className="radio"> <input type="radio" name="map" aria-label="일반지도" /> <span></span></label>
</li>
<li>
<span>영상지도</span>
<label className="radio"> <input type="radio" name="map" aria-label="영상지도" /> <span></span></label>
</li>
</ul>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해경관할구역</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>검문검색위치</span>
</label>
</li>
</ul>
<div className='btnBox'>
<button
type="button"
className="btn btnLine w15r"
onClick={() => navigate("/layer/register")}
>레이어 등록</button>
</div>
</div>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,4 +0,0 @@
export default function EmptyMain() {
return null; //
}

파일 보기

@ -1,91 +0,0 @@
import { useState } from 'react';
import { useNavigate } from "react-router-dom";
import FileUpload from '../components/FileUpload';
export default function LayerComponent() {
const navigate = useNavigate();
return (
<section id="LayerComponent">
{/* 레이어등록 팝업 */}
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title">레이어 등록</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => navigate("/main")}
/>
</div>
<div className="puBody">
<table className="table">
<caption>레이어등록 - 레이어명, 첨부파일, 공유설정 대한 내용을 나타내는 표입니다.</caption>
<colgroup>
<col style={{ width: '30%' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">레이어명 <span className="required">*</span></th>
<td><input type="text" placeholder="" aria-label="레이어명" /></td>
</tr>
<tr>
<th scope="row">첨부파일 <span className="required">*</span></th>
<td>
<div className="rowC">
<FileUpload
label="파일 선택"
inputId="layerFile"
maxLength={35}
placeholder="선택된 파일 없음"
/>
<span className="helpTxt">geojson 파일을 첨부해 주세요. </span>
</div>
</td>
</tr>
<tr>
<th scope="row">공유설정</th>
<td>
<div className="row flx1">
<label className="checkbox checkL w10r">
<input type="checkbox" />
<span>공유 여부</span>
</label>
<label className="">
<span className="blind">공유설정</span>
<select>
<option value="">전체</option>
<option value="">부서</option>
<option value="">개인</option>
<option value="">개인 & 부서</option>
</select>
</label>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button type="button" className="btn basic">저장</button>
<button
type="button"
className="btn dark"
onClick={() => navigate("/main")}
>
취소
</button>
</div>
</div>
</div>
</div>
</section>
);
}

파일 보기

@ -1,208 +0,0 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
export default function MyPageComponent() {
const navigate = useNavigate();
//
// null | "password" | "cert"
const [subPopup, setSubPopup] = useState(null);
return (
<section id="MyPageComponent">
{/* 내 정보 조회 */}
<div className="popupUtillWrap">
<div className="popupUtill">
<div className="puHeader">
<span className="title"> 정보 조회</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => navigate("/main")}
/>
</div>
<div className="puBody">
<table className="table">
<caption>
정보 조회 - 아이디, 비밀번호, 이름, 이메일, 직급, 상세소속, 공인인증서 삭제
</caption>
<colgroup>
<col style={{ width: "30%" }} />
<col />
</colgroup>
<tbody>
<tr>
<th scope="row">아이디</th>
<td>admin222</td>
</tr>
<tr>
<th scope="row">비밀번호</th>
<td>
<button
type="button"
className="btn btnM deep flx0"
onClick={() => setSubPopup("password")}
>
비밀번호 변경
</button>
</td>
</tr>
<tr>
<th scope="row">이름</th>
<td>ADMIN</td>
</tr>
<tr>
<th scope="row">이메일</th>
<td>123@korea.kr</td>
</tr>
<tr>
<th scope="row">직급</th>
<td>경감</td>
</tr>
<tr>
<th scope="row">상세소속</th>
<td></td>
</tr>
<tr>
<th scope="row">공인인증서 삭제</th>
<td>
<button
type="button"
className="btn btnM deep flx0"
onClick={() => setSubPopup("cert")}
>
공인인증서 삭제
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button type="button" className="btn basic">저장</button>
<button type="button" className="btn dark">초기화</button>
</div>
</div>
</div>
</div>
{/* 딤 + 서브 팝업 */}
{subPopup && (
<div className="popupDim">
{/* 비밀번호 변경 */}
{subPopup === "password" && (
<div className="popupUtill">
<div className="puHeader">
<span className="title">비밀번호 수정</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => setSubPopup(null)}
/>
</div>
<div className="puBody">
<table className="table">
<caption>
비밀번호 수정 - 현재 비밀번호, 비밀번호, 비밀번호 확인
</caption>
<colgroup>
<col style={{ width: "30%" }} />
<col />
</colgroup>
<tbody>
<tr>
<th scope="row">현재 비밀번호</th>
<td>
<input type="password" aria-label="현재 비밀번호" />
</td>
</tr>
<tr>
<th scope="row"> 비밀번호</th>
<td>
<input type="password" aria-label="새 비밀번호" />
</td>
</tr>
<tr>
<th scope="row"> 비밀번호 확인</th>
<td>
<input type="password" aria-label="새 비밀번호 확인" />
</td>
</tr>
</tbody>
</table>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button
type="button"
className="btn basic"
onClick={() => setSubPopup(null)}
>
수정
</button>
<button
type="button"
className="btn dark"
onClick={() => setSubPopup(null)}
>
취소
</button>
</div>
</div>
</div>
)}
{/* 공인인증서 삭제 */}
{subPopup === "cert" && (
<div className="popupUtill cert">
<div className="puHeader">
<span className="title">공인인증서 삭제</span>
<button
type="button"
className="puClose"
aria-label="닫기"
onClick={() => setSubPopup(null)}
/>
</div>
<div className="puBody">
<div className="puTxtBox">
공인인증서를 삭제 하시겠습니까?
</div>
</div>
<div className="puFooter">
<div className="popBtnWrap">
<button
type="button"
className="btn basic"
onClick={() => setSubPopup(null)}
>
삭제
</button>
<button
type="button"
className="btn dark"
onClick={() => setSubPopup(null)}
>
취소
</button>
</div>
</div>
</div>
)}
</div>
)}
</section>
);
}

파일 보기

@ -1,71 +0,0 @@
export default function NavComponent({ activeKey, onChange }) {
const gnbList = [
{ key: 'gnb1', class: 'gnb1', label: '선박' },
{ key: 'gnb2', class: 'gnb2', label: '위성' },
{ key: 'gnb3', class: 'gnb3', label: '기상' },
{ key: 'gnb4', class: 'gnb4', label: '분석' },
{ key: 'gnb5', class: 'gnb5', label: '타임라인' },
{ key: 'gnb6', class: 'gnb6', label: 'AI모드' },
{ key: 'gnb7', class: 'gnb7', label: '리플레이' },
{ key: 'gnb8', class: 'gnb8', label: '항적조회' },
];
const sideList = [
{ key: 'filter', class: 'filter', label: '필터' },
{ key: 'layer', class: 'layer', label: '레이어' },
];
return(
<nav id="nav">
{/* <ul className="gnb">
<li><button type="button" className="gnb1 active" title="선박" aria-label="선박"></button></li>
<li><button type="button" className="gnb2" title="위성" aria-label="위성"></button></li>
<li><button type="button" className="gnb3" title="기상" aria-label="기상"></button></li>
<li><button type="button" className="gnb4" title="분석" aria-label="분석"></button></li>
<li><button type="button" className="gnb5" title="타임라인" aria-label="타임라인"></button></li>
<li><button type="button" className="gnb6" title="AI모드" aria-label="AI모드"></button></li>
<li><button type="button" className="gnb7" title="리플레이" aria-label="리플레이"></button></li>
<li><button type="button" className="gnb8" title="항적조회" aria-label="항적조회"><</button></li>
</ul>
<ul className="side">
<li><button type="button" className="filter" title="필터" aria-label="필터"></button></li>
<li><button type="button" className="layer" title="레이어" aria-label="레이어"></button></li>
</ul> */}
<ul className="gnb">
{gnbList.map(item => (
<li key={item.key}>
<button
type="button"
className={`${item.class} ${activeKey === item.key ? 'active' : ''}`}
onClick={() => onChange(item.key)}
aria-label={item.label}
title={item.label}
>
<span className="blind">{item.label}</span>
</button>
</li>
))}
</ul>
<ul className="side">
{sideList.map(item => (
<li key={item.key}>
<button
type="button"
className={`${item.class} ${activeKey === item.key ? 'active' : ''}`}
onClick={() => onChange(item.key)}
aria-label={item.label}
title={item.label}
>
<span className="blind">{item.label}</span>
</button>
</li>
))}
</ul>
</nav>
)
}

파일 보기

@ -1,727 +0,0 @@
import { useState, useEffect } from 'react';
import { Link } from "react-router-dom";
import Panel1DetailComponent from './Panel1DetailComponent';
export default function Panel1Component({ isOpen, onToggle }) {
//
const [view, setView] = useState('list'); // list | detail
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(false); //
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev);
//
const [activeTab, setActiveTab] = useState('ship01');
const tabs = [
{ id: 'ship01', label: '선박검색' },
{ id: 'ship02', label: '허가선박' },
{ id: 'ship03', label: '제재단속' },
{ id: 'ship04', label: '침몰선박' },
{ id: 'ship05', label: '선박입출항' },
{ id: 'ship06', label: '관심선박' }
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 👉 상세 화면일 때 */}
{view === 'detail' ? (
<Panel1DetailComponent
isOpen={isOpen}
onToggle={onToggle}
onBack={() => setView('list')}
/>
) : (
<>
{/* ===== 목록 화면 ===== */}
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'ship01' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">선박 검색</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>선종</span>
<select>
<option value="">전체</option>
<option value="">어선</option>
<option value="">함정</option>
<option value="">여객선</option>
<option value="">카고</option>
<option value="">탱커</option>
<option value="">관공선</option>
<option value="">기타</option>
<option value="">낚시어선</option>
</select>
</label>
<label>
<span>국적</span>
<select>
<option value="">전체</option>
<option value="">한국</option>
<option value="">미국</option>
<option value="">중국</option>
<option value="">일본</option>
<option value="">북한</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>타겟ID</span>
<input type="text" placeholder="타겟ID" />
</label>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
{/* 아코디언 1 */}
<div className={`accordion ${isAccordionOpen1 ? 'is-open' : ''}`}>
<li>
<label>
<span>위험물</span>
<input type="text" placeholder="타겟ID" />
</label>
<label className="checkbox">
<input type="checkbox" />
<span className="w70">MMSI / 호출부호 변경이력</span>
</label>
</li>
<li>
<label>
<span>승선원수</span>
<div className="labelRow">
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
<span>-</span>
<div className="numInput">
<input type="number" placeholder="최대" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</div>
</label>
</li>
<li>
<label>
<span>너비(m)</span>
<div className="numInput">
<input type="number" placeholder="최소" min="" max="" />
<div className="spin">
<button type="button" className="spinUp"><span className="blind">증가</span></button>
<button type="button" className="spinDown"><span className="blind">감소</span></button>
</div>
</div>
</label>
</li>
</div>
{/* 여기까지 아코디언1 */}
<li>
<button
type="button"
className={`btn btnS semi btnToggle ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
>
상세검색
{isAccordionOpen1 ? ' 닫기' : ' 열기'}
</button>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
{/* <div className="schbox mtb24">
<ul>
<li>
<input type="text" className="schInput" placeholder="대표검도" />
</li>
<li>
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div> */}
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/ship" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/ship" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/ship" className="">
<i className="cicle red"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/ship" className="">
<i className="cicle orng"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'ship02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">허가선박</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>타겟 ID</span>
<input type="text" placeholder="타겟 ID" />
</label>
</li>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<div className="detailWrap">
{/* 선박정보 박스 */}
<ul className="detailBox">
<li className="dbHeader">
<div className="headerL">
<span className="name">ZHELINGYU29801</span>
<span className="type">Fishing</span>
</div>
<div className="headerR">
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
<span className="num">412</span>
<button
type="button"
className="icoArrow"
aria-label="상세보기"
onClick={() => setView('detail')}
></button>
</div>
</li>
<li>
<span className="label">타겟 ID</span>
<span className="value">412417712</span>
</li>
<li>
<span className="label">주정박항</span>
<span className="value">zhelingyu29801</span>
</li>
<li>
<span className="label">어획할당량</span>
<span className="value">100(ton)</span>
</li>
<li>
<span className="label">조업수역구역</span>
<span className="value">, </span>
</li>
</ul>
</div>
</div>
</div>
{/* 탭 콘텐츠 03 */}
<div className={`tabWrap ${activeTab === 'ship03' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">제재단속</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>제재 유형</span>
<select>
<option value="">전체</option>
<option value="">고래포획 의심</option>
<option value="">UN 제재</option>
<option value="">위반행위 규제 정보</option>
<option value="">불법 선박</option>
<option value="">음주 운항 이력</option>
<option value="">다잡아 처분 선박</option>
<option value="">어획량 위반</option>
<option value="">조업 일지 위반</option>
<option value="">망목 내경 미준수</option>
<option value="">입출역 미통보</option>
<option value="">선박서류 미비치</option>
<option value="">어구위반</option>
<option value="">허가 /표지판 위반</option>
<option value="">어획물 전재 위반</option>
<option value="">선원수첩 신분증명서 위반</option>
<option value="">정선 명령 위반</option>
<option value="">어구 설치 조업수역 이탈</option>
<option value="">어획물 운반선 체크포인트 제도 위반</option>
<option value="">포획 채취 금지 체장 위반 어획물 포획</option>
<option value="">조업수역 위반</option>
<option value="">조업 기간 위반</option>
<option value="">어창 용적 위반</option>
<option value="">어창 용적 위반</option>
<option value="">메모</option>
</select>
</label>
</li>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/ship" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/ship" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 04 */}
<div className={`tabWrap ${activeTab === 'ship04' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">침몰선박</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li>
<label>
<span>사고기간</span>
<div className='labelRow'>
<input type="text" className="dateInput" placeholder="연도-월-일" />
<span>-</span>
<input type="text"className="dateInput" placeholder="연도-월-일" /></div>
</label>
</li>
<li>
<label>
<span>사고내용</span>
<input type="text" placeholder="사고내용" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/ship" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/ship" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 05 */}
<div className={`tabWrap ${activeTab === 'ship05' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">선박입출항</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>출항일시</span>
<input type="text" className="dateInput" placeholder="연도-월-일 - -:-" />
</label>
<label>
<span>~ 입항일시</span>
<input type="text" className="dateInput" placeholder="연도-월-일 - -:-" />
</label>
</li>
<li>
<label>
<span>PMS<br/>출항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
<label>
<span>PMS<br/>입항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
</li>
<li>
<label>
<span>SIE<br/>출항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
<label>
<span>SIE<br/>입항항구</span>
<select>
<option value="">전체</option>
</select>
</label>
</li>
<li>
<label>
<span>타겟ID</span>
<input type="text" placeholder="타겟ID" />
</label>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
{/* 여기부터 아코디언 */}
<div className={`accordion ${isAccordionOpen2 ? 'is-open' : ''}`}>
<li>
<label>
<span>낚시여부</span>
<select>
<option value="">전체</option>
<option value="">미선택</option>
<option value="">선택</option>
</select>
</label>
</li>
<li>
<label>
<span>최대<br/>적재톤수</span>
<input type="text" placeholder="0" />
</label>
<label>
<span>최소<br/>적재톤수</span>
<input type="text" placeholder="0" />
</label>
</li>
<li>
<label>
<span>최대<br/>승선원</span>
<input type="text" placeholder="0" />
</label>
<label>
<span>최소<br/>승선원</span>
<input type="text" placeholder="0" />
</label>
</li>
<li>
<label>
<span>최대<br/>승객수</span>
<input type="text" placeholder="0" />
</label>
<label>
<span>최소<br/>승객수</span>
<input type="text" placeholder="0" />
</label>
</li>
<li>
<label>
<span>선종</span>
<select>
<option value="">전체</option>
<option value="">어선</option>
<option value="">함정</option>
<option value="">여객선</option>
<option value="">카고</option>
<option value="">탱커</option>
<option value="">관공선</option>
<option value="">기타</option>
<option value="">낚시어선</option>
</select>
</label>
<label>
<span>국적</span>
<select>
<option value="">전체</option>
<option value="">한국</option>
<option value="">미국</option>
<option value="">중국</option>
<option value="">일본</option>
<option value="">북한</option>
<option value="">기타</option>
</select>
</label>
</li>
</div>
{/* 여기까지 아코디언 */}
<li>
<button
type="button"
className={`btn btnS semi btnToggle ${isAccordionOpen2 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen2}
onClick={toggleAccordion2}
>
상세검색
{isAccordionOpen2 ? ' 닫기' : ' 열기'}
</button>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/ship" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/ship" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 06 */}
<div className={`tabWrap ${activeTab === 'ship06' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">관심선박</div>
<div className="formGroup">
<ul className="lagelW12">
<li>
<label>
<span>관심사유 지정사유</span>
<select>
<option value="">전체</option>
<option value="">불법조업의심</option>
<option value="">불법포경의심</option>
<option value="">MMSI 신호 임의 변경</option>
<option value="">제재 선박 의심</option>
<option value="">북한 선박 의심</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>타겟 ID</span>
<input type="text" placeholder="타겟 ID" />
</label>
</li>
<li>
<label>
<span>선박명</span>
<input type="text" placeholder="선박명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList line">
<li>
<Link to="/ship" className="active">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
<li>
<Link to="/ship" className="">
<i className="cicle default"></i>
<span>0001</span>
<span>1511함A-05</span>
<span>
<img src="/images/flag_kor.svg" alt="대한민국" className="flagIcon" />
</span>
<span>(AIS)</span>
<span className="legend">
<img src="/images/legend_ship_pink.svg" alt="선박" className="legendShip" />
</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
{/* 여기까지 전체목록 페이지 */}
</>
)}
</aside>
);
}

파일 보기

@ -1,112 +0,0 @@
import { useState, useEffect } from 'react';
import { Link } from "react-router-dom";
export default function Panel1DetailComponent({ isOpen, onToggle, onBack }) {
//
const [activeTab, setActiveTab] = useState('ship02');
const tabs = [
{ id: 'ship01', label: '선박검색' },
{ id: 'ship02', label: '허가선박' },
{ id: 'ship03', label: '제재단속' },
{ id: 'ship04', label: '침몰선박' },
{ id: 'ship05', label: '선박입출항' },
{ id: 'ship06', label: '관심선박' }
];
return (
<>
{/* <aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}> */}
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'ship02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">
<button
type="button"
className="prevBtn"
aria-label="이전"
onClick={onBack}
/>
ZHELINGYU29801
</div>
</div>
<div className="tabBtm noLine">
<table className="table">
<caption>선박상세설명 - 타겟 ID, 국가, 주정박항,선종,조업수역 구역,어획 할당량(ton),조업 기간,신호 출처 대한 내용을 나타내는 표입니다.</caption>
<colgroup>
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
<col style={{ width: '125px' }} />
<col style={{ width: '' }} />
</colgroup>
<tbody>
<tr>
<th scope="row">타겟 ID</th>
<td>412417712</td>
<th scope="row">국가</th>
<td>412</td>
</tr>
<tr>
<th scope="row">주정박항</th>
<td>zhelingyu29801</td>
<th scope="row">선종</th>
<td>Fishing</td>
</tr>
<tr>
<th scope="row">조업수역 구역</th>
<td></td>
<th scope="row">어획 할당량(ton)</th>
<td></td>
</tr>
<tr>
<th scope="row">조업 기간 1</th>
<td colSpan={3}>2024/01/01 - 2024/04/15</td>
</tr>
<tr>
<th scope="row">조업 기간 2</th>
<td colSpan={3}>2024/10/16 - 2024/12/31</td>
</tr>
<tr>
<th scope="row">신호 출처</th>
<td colSpan={3}>VTS_AIS</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
{/* </aside> */}
</>
);
}

파일 보기

@ -1,420 +0,0 @@
import { useState } from 'react';
import { Link, useNavigate } from "react-router-dom";
import Slider from '../components/Slider';
export default function Panel2Component({ isOpen, onToggle }) {
const navigate = useNavigate();
//
const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); //
const [isAccordionOpen2, setIsAccordionOpen2] = useState(false); //
const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev);
const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev);
//
const [activeTab, setActiveTab] = useState('ship01');
const tabs = [
{ id: 'ship01', label: '위성영상 관리' },
{ id: 'ship02', label: '위성사업자 관리' },
{ id: 'ship03', label: '위성 관리' },
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'ship01' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">위성영상 관리</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>영상 촬영일</span>
<div className="labelRow">
<input className="dateInput" placeholder="연도-월-일" type="text" />
<span>-</span>
<input className="dateInput" placeholder="연도-월-일" type="text" />
</div>
</label>
</li>
{/* 아코디언 1 */}
<div className={`accordion pt8 ${isAccordionOpen1 ? 'is-open' : ''}`}>
<li>
<label>
<span>영상 종류</span>
<select>
<option value="">전체</option>
<option value="">VIRS</option>
<option value="">ICEYE_SAR</option>
<option value="">광학</option>
<option value="">예약</option>
<option value="">RF</option>
</select>
</label>
<label>
<span>영상 출처</span>
<select>
<option value="">전체</option>
<option value="">국내/자동</option>
<option value="">국내/수동</option>
<option value="">국외/수동</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>위성 궤도</span>
<select>
<option value="">전체</option>
<option value="">저궤도</option>
<option value="">중궤도</option>
<option value="">정지궤도</option>
<option value="">기타</option>
</select>
</label>
<label>
<span>주기</span>
<select>
<option value="">전체</option>
<option value="">0</option>
<option value="">10</option>
<option value="">30</option>
<option value="">60</option>
</select>
</label>
</li>
</div>
{/* 여기까지 아코디언1 */}
<li>
<label>
<span>위성영상명</span>
<input type="text" placeholder="위성영상명" />
</label>
</li>
<li>
<button
type="button"
className={`btn btnS semi btnToggle ${isAccordionOpen1 ? 'is-open' : ''}`}
aria-expanded={isAccordionOpen1}
onClick={toggleAccordion1}
>
상세검색
{isAccordionOpen1 ? ' 닫기' : ' 열기'}
</button>
</li>
<li className="fgBtn rowSB">
<>
<div className="row gap10">
<span>투명도</span>
<div>
<Slider label="투명도 조절" />
</div>
</div>
<div className="row gap10">
<span>밝기</span>
<div>
<Slider label="밝기 조절" />
</div>
</div>
</>
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤영역 */}
<div className="tabBtmCnt">
<div className="detailWrap">
{/* 위성정보 박스 */}
<ul className="detailBox stretch">
<li className="dbHeader">
<div className="headerL item2">
<span className="name">업로드 테스트</span>
<span className="type">2025-09-25 16:09:00</span>
</div>
</li>
<li>
<ul className="dbList">
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">위성영상파일</span>
<span className="value">mda:fae19b9b-e99e-4794- a305-bce6d537ac36_remark_ resized_2022_0102_1709_npp_DNB_750</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">영상 출처</span>
<span className="value">VIRS</span>
</li>
</ul>
<div className="btnArea">
<button type="button" className="btnEdit"></button>
<button type="button" className="btnDel" onClick={() => navigate("/satellite/delete")}></button>
<button type="button" className="btnMap"></button>
</div>
</li>
</ul>
{/* 위성정보 박스 */}
<ul className="detailBox stretch">
<li className="dbHeader">
<div className="headerL item2">
<span className="name">업로드 테스트</span>
<span className="type">2025-09-25 16:09:00</span>
</div>
</li>
<li>
<ul className="dbList">
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">위성영상파일</span>
<span className="value">mda:fae19b9b-e99e-4794- a305-bce6d537ac36_remark_ resized_2022_0102_1709_npp_DNB_750</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">VIRS</span>
</li>
<li>
<span className="label">영상 출처</span>
<span className="value">VIRS</span>
</li>
</ul>
<div className="btnArea">
<button type="button" className="btnEdit"></button>
<button type="button" className="btnDel"></button>
<button type="button" className="btnMap"></button>
</div>
</li>
</ul>
</div>
</div>
{/* 하단버튼 영역 */}
<div className="btnBox rowSB">
<button type="button" className="btn btnLine">위성영상 폴더 업로드</button>
<button type="button" className="btn btnLine" onClick={() => navigate("/satellite/add")}>위성영상 등록</button>
</div>
</div>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'ship02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">위성사업자 관리</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>사업자 분류</span>
<select>
<option value="">전체</option>
<option value="">국가</option>
<option value="">연구기관</option>
<option value="">민간사업자</option>
<option value="">기타</option>
</select>
</label>
<label>
<span>사업자명</span>
<input type="text" placeholder="사업자명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤영역 */}
<div className="tabBtmCnt">
<div className="detailWrap">
{/* 위성정보 박스 */}
<ul className="detailBox">
<li className="dbHeader">
<div className="headerL item1">
<span className="name">Test 01</span>
</div>
</li>
<li>
<span className="label">사업자 분류</span>
<span className="value">국가</span>
</li>
<li>
<span className="label">국가</span>
<span className="value">대한민국</span>
</li>
<li>
<span className="label">소재지</span>
<span className="value">test</span>
</li>
</ul>
</div>
</div>
{/* 하단버튼 영역 */}
<div className="btnBox">
<button
type="button"
className="btn btnLine"
onClick={() => navigate("/satellite/provider")}
>
등록
</button>
</div>
</div>
</div>
</div>
{/* 탭 콘텐츠 03 */}
<div className={`tabWrap ${activeTab === 'ship03' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">위성 관리</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>사업자 분류</span>
<select>
<option value="">전체</option>
<option value="">국가</option>
<option value="">연구기관</option>
<option value="">민간사업자</option>
<option value="">기타</option>
</select>
</label>
<label>
<span>센서 타입</span>
<select>
<option value="">전체</option>
<option value="">광학</option>
<option value="">SAR</option>
<option value="">RF</option>
<option value="">기타</option>
</select>
</label>
</li>
<li>
<label>
<span>위성명</span>
<input type="text" placeholder="위성명" />
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm noSc">
<div className="tabBtmInner">
{/* 스크롤영역 */}
<div className="tabBtmCnt">
<div className="detailWrap">
{/* 위성정보 박스 */}
<ul className="detailBox">
<li>
<span className="label">사업자명</span>
<span className="value">국토지리정보원</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">국가</span>
</li>
<li>
<span className="label">센서 타입</span>
<span className="value">test</span>
</li>
<li>
<span className="label">촬영 해상도</span>
<span className="value"></span>
</li>
</ul>
{/* 위성정보 박스 */}
<ul className="detailBox">
<li>
<span className="label">사업자명</span>
<span className="value">국토지리정보원</span>
</li>
<li>
<span className="label">위성명</span>
<span className="value">국가</span>
</li>
<li>
<span className="label">센서 타입</span>
<span className="value">test</span>
</li>
<li>
<span className="label">촬영 해상도</span>
<span className="value"></span>
</li>
</ul>
</div>
</div>
{/* 하단버튼 영역 */}
<div className="btnBox">
<button
type="button"
className="btn btnLine"
onClick={() => navigate("/satellite/manage")}
>
등록
</button>
</div>
</div>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

파일 보기

@ -1,323 +0,0 @@
import { useState } from 'react';
import { Link } from "react-router-dom";
export default function Panel3Component({ isOpen, onToggle }) {
//
const [activeTab, setActiveTab] = useState('weather01');
const tabs = [
{ id: 'weather01', label: '기상특보' },
{ id: 'weather02', label: '태풍정보' },
{ id: 'weather03', label: '조위관측' },
{ id: 'weather04', label: '조석정보' },
{ id: 'weather05', label: '항공기상' },
];
return (
<aside className={`slidePanel ${!isOpen ? 'is-closed' : ''}`}>
{/* 탭 버튼 */}
<div className="tabBox">
<div className="tabDefault">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
className={activeTab === tab.id ? 'on' : ''}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 탭 콘텐츠 01 */}
<div className={`tabWrap ${activeTab === 'weather01' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">기상특보</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>일자</span>
<div className='labelRow'>
<input type="date" className="dateInput" placeholder="연도-월-일" />
<span>-</span>
<input type="date" className="dateInput" placeholder="연도-월-일" />
</div>
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList lineSB">
<li>
<Link to="/weather" className="">
<span className="title">1. 폭풍주의: 남해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/weather" className="">
<span className="title">2. 폭풍주의: 서해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/weather" className="">
<span className="title">3. 폭풍주의: 동해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 02 */}
<div className={`tabWrap ${activeTab === 'weather02' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">태풍정보</div>
<div className="formGroup">
<ul>
<li>
<label>
<span>연도</span>
<select>
<option value="">선택</option>
</select>
</label>
<label>
<span></span>
<select>
<option value="">선택</option>
</select>
</label>
</li>
<li className="fgBtn">
<button type="button" className="schBtn">검색</button>
</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="colList lineSB">
<li>
<Link to="/weather" className="">
<span className="title">1. 폭풍주의: 남해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/weather" className="">
<span className="title">2. 폭풍주의: 서해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
<li>
<Link to="/weather" className="">
<span className="title">3. 폭풍주의: 동해</span>
<span className="meta">시작일시 00:00:00 / 발효일시 00:00:00</span>
</Link>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 03 */}
<div className={`tabWrap ${activeTab === 'weather03' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">조위관측</div>
<div className="legend">
<span className="legendTitle">조위관측 범례</span>
<ul className="legendList">
<li><img src="/images/ico_obsTide.svg" alt="조위관측소" />조위관측소</li>
<li><img src="/images/ico_obsOcean.svg" alt="해양관측소" />해양관측소</li>
<li><img src="/images/ico_obsBuoy.svg" alt="해양관측부이" />해양관측부이</li>
<li><img src="/images/ico_obsCurrent.svg" alt="해수유동관측소" />해수유동관측소</li>
<li><img src="/images/ico_obsScience.svg" alt="해양과학기지" />해양과학기지</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="lineList">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>조위관측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해양관측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해양관측부이</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해수유동관측측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>해양과학기지</span>
</label>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 04 */}
<div className={`tabWrap ${activeTab === 'weather04' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">조석정보</div>
<div className="legend">
<span className="legendTitle">조위관측 범례</span>
<ul className="legendList">
<li><img src="/images/ico_obsTide.svg" alt="조위관측소" />조위관측소</li>
<li><img src="/images/ico_obsSunrise.svg" alt="일출몰관측지역" />일출몰관측지역</li>
</ul>
</div>
</div>
<div className="tabBtm">
<ul className="lineList">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>조위관측소</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>일출몰관측지역</span>
</label>
</li>
</ul>
</div>
</div>
{/* 탭 콘텐츠 05 */}
<div className={`tabWrap ${activeTab === 'weather05' ? 'is-active' : ''}`}>
<div className="tabTop">
<div className="title">항공기상</div>
</div>
<div className="tabBtm noLine">
<ul className="lineList">
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>전체</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>양양공항(RKNY) </span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>김포공항(RKSS)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>인천공항(RKSI)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>청주공항(RKTU)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>포항공항(RKTH)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>대구공항(RKTN)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>울산공항(RKPU)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>김해공항(RKPK)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>광주공항(RKJJ)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>사천공항(RKPS)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>무안공항(RKJB)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>여수공항(RKYJ)</span>
</label>
</li>
<li>
<label className="checkbox checkL">
<input type="checkbox" />
<span>제주공항(RKPC)</span>
</label>
</li>
</ul>
</div>
</div>
{/* 사이드패널 토글버튼 */}
<button
type="button"
className="toogle"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className="blind">
{isOpen ? '패널 접기' : '패널 열기'}
</span>
</button>
</aside>
);
}

Some files were not shown because too many files have changed in this diff Show More