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:
부모
086599bb6d
커밋
ac3c204843
69
.claude/rules/code-style.md
Normal file
69
.claude/rules/code-style.md
Normal file
@ -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/`에 관리
|
||||
84
.claude/rules/git-workflow.md
Normal file
84
.claude/rules/git-workflow.md
Normal file
@ -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
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
|
||||
```
|
||||
34
.claude/rules/team-policy.md
Normal file
34
.claude/rules/team-policy.md
Normal file
@ -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
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
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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
65
.claude/skills/create-mr/SKILL.md
Normal file
65
.claude/skills/create-mr/SKILL.md
Normal file
@ -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 접근 토큰 (없으면 안내)
|
||||
49
.claude/skills/fix-issue/SKILL.md
Normal file
49
.claude/skills/fix-issue/SKILL.md
Normal file
@ -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 접근 토큰
|
||||
90
.claude/skills/init-project/SKILL.md
Normal file
90
.claude/skills/init-project/SKILL.md
Normal file
@ -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` 확인
|
||||
- 빌드 명령 실행 가능 확인
|
||||
- 다음 단계 안내 (개발 시작, 첫 커밋 방법 등)
|
||||
73
.claude/skills/sync-team-workflow/SKILL.md
Normal file
73
.claude/skills/sync-team-workflow/SKILL.md
Normal file
@ -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) 표시
|
||||
- 필요한 추가 조치 안내 (빌드 확인, 의존성 업데이트 등)
|
||||
1
.claude/workflow-version.json
Normal file
1
.claude/workflow-version.json
Normal file
@ -0,0 +1 @@
|
||||
{"applied_global_version": "1.2.0"}
|
||||
33
.editorconfig
Normal file
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
35
.env
@ -1,33 +1,16 @@
|
||||
# ============================================
|
||||
# 프로덕션 환경 (Production)
|
||||
# - 빌드: npm run build:prod (또는 npm run build)
|
||||
# - 실제 운영 서버 배포용
|
||||
# - 빌드: yarn build:prod (또는 yarn build)
|
||||
# ============================================
|
||||
|
||||
# 배포 경로 (서브 경로 배포 시 설정)
|
||||
# 반드시 '/'로 시작하고 '/'로 끝나야 함
|
||||
VITE_BASE_URL=/kcgnv/
|
||||
# 배포 경로
|
||||
VITE_BASE_URL=/
|
||||
|
||||
# API 서버 (프록시 타겟)
|
||||
VITE_API_URL=https://mda.kcg.go.kr
|
||||
# API 서버 (SNP-Batch API)
|
||||
VITE_API_URL=http://211.208.115.83:8041/snp-api
|
||||
|
||||
# 지도 타일 서버
|
||||
VITE_MAP_TILE_URL=https://mda.kcg.go.kr
|
||||
# 선박 데이터 쓰로틀링 (ms, 0=무제한)
|
||||
VITE_SHIP_THROTTLE=0
|
||||
|
||||
# 선박 신호 WebSocket
|
||||
VITE_SIGNAL_WS=wss://mda.kcg.go.kr/v3/connect
|
||||
|
||||
# 선박 신호 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
|
||||
# 인증 우회 (민간 데모)
|
||||
VITE_DEV_SKIP_AUTH=true
|
||||
|
||||
32
.env.dev
32
.env.dev
@ -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)
|
||||
# - 서버: yarn dev
|
||||
# - 로컬 개발 전용
|
||||
# ============================================
|
||||
|
||||
# 배포 경로 (프록시 모드: localhost:9090/kcgnv/ → localhost:3000/kcgnv/)
|
||||
VITE_BASE_URL=/kcgnv/
|
||||
# 배포 경로
|
||||
VITE_BASE_URL=/
|
||||
|
||||
# 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
|
||||
# API 서버 (SNP-Batch API)
|
||||
VITE_API_URL=http://211.208.115.83:8041/snp-api
|
||||
|
||||
# 선박 데이터 쓰로틀링 (ms, 0=무제한)
|
||||
VITE_SHIP_THROTTLE=0
|
||||
|
||||
# 메인 프로젝트 URL (세션 만료 시 리다이렉트)
|
||||
VITE_MAIN_APP_URL=http://localhost:9090
|
||||
|
||||
# 로컬 개발 인증 우회 (포트가 달라 localStorage 공유 불가)
|
||||
# true면 세션 없을 때 모의 사용자로 자동 로그인
|
||||
VITE_DEV_SKIP_AUTH=false
|
||||
# 인증 우회 (민간 데모)
|
||||
VITE_DEV_SKIP_AUTH=true
|
||||
|
||||
32
.env.qa
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
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# 오프라인 캐시 바이너리 보호 (Windows autocrlf 변환 방지)
|
||||
*.tgz binary
|
||||
|
||||
60
.githooks/commit-msg
Executable file
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
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
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
36
.gitignore
vendored
@ -2,18 +2,13 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
# Offline cache - git에 포함 (폐쇄망 배포용)
|
||||
!.yarn-offline-cache/
|
||||
|
||||
docs
|
||||
|
||||
# Build
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
# Environment (production secrets)
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
@ -24,25 +19,20 @@ build/
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
# OS
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
*.md
|
||||
!README.md
|
||||
# Claude Code (개인 파일만 무시, 팀 파일은 추적)
|
||||
.claude/settings.local.json
|
||||
.claude/scripts/
|
||||
|
||||
# TypeScript files (메인 프로젝트 참조용, 빌드/커밋 제외)
|
||||
**/*.ts
|
||||
**/*.tsx
|
||||
# tracking VesselListManager (참조용 전체 제외 - replay 패키지에서 별도 구현)
|
||||
# tracking VesselListManager (참조용)
|
||||
src/tracking/components/VesselListManager/
|
||||
# 단, d.ts 타입 선언 파일은 필요시 포함 가능
|
||||
# !**/*.d.ts
|
||||
|
||||
# Publish 폴더 (퍼블리시 원본 포함, 없어도 빌드 가능)
|
||||
nul
|
||||
확인요청.txt
|
||||
|
||||
# Build artifacts
|
||||
*.zip
|
||||
httpd.conf
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
||||
24
|
||||
62
CLAUDE.md
Normal file
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/*
|
||||
Binary file not shown.
@ -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
|
||||
36
src/App.jsx
36
src/App.jsx
@ -1,53 +1,17 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
// 구현 영역 - 레이아웃
|
||||
import MainLayout from './components/layout/MainLayout';
|
||||
import SessionGuard from './components/auth/SessionGuard';
|
||||
import { ToastContainer } from './components/common/Toast';
|
||||
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() {
|
||||
return (
|
||||
<SessionGuard>
|
||||
<ToastContainer />
|
||||
<AlertModalContainer />
|
||||
<Routes>
|
||||
{/* =====================
|
||||
구현 영역 (메인)
|
||||
- 모든 메뉴 경로를 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>
|
||||
</SessionGuard>
|
||||
);
|
||||
|
||||
134
src/api/aisTargetApi.js
Normal file
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 { useAuthStore } from '../../stores/authStore';
|
||||
import useShipStore from '../../stores/shipStore';
|
||||
import { fetchUserFilter } from '../../api/userSettingApi';
|
||||
import './SessionGuard.scss';
|
||||
|
||||
const SKIP_AUTH = import.meta.env.VITE_DEV_SKIP_AUTH === 'true';
|
||||
|
||||
export default function SessionGuard({ children }) {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const isChecking = useAuthStore((s) => s.isChecking);
|
||||
|
||||
useEffect(() => {
|
||||
if (SKIP_AUTH) {
|
||||
// 인증 우회: 모의 사용자로 자동 인증
|
||||
useAuthStore.getState().checkSession();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = useAuthStore.getState().checkSession();
|
||||
if (!result.valid) {
|
||||
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) {
|
||||
return (
|
||||
<div className="session-guard-loading">
|
||||
|
||||
@ -1,25 +1,15 @@
|
||||
/**
|
||||
* 사이드 네비게이션 메뉴
|
||||
* - 퍼블리시 NavComponent 구조와 동일하게 맞춤
|
||||
*/
|
||||
|
||||
const gnbList = [
|
||||
{ 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: 'gnb5', className: 'gnb5', label: '타임라인', path: 'timeline' },
|
||||
// { key: 'gnb6', className: 'gnb6', label: 'AI모드', path: 'ai' },
|
||||
{ key: 'gnb7', className: 'gnb7', label: '리플레이', path: 'replay' },
|
||||
{ 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 }) {
|
||||
return (
|
||||
<nav id="nav">
|
||||
@ -38,22 +28,6 @@ export default function SideNav({ activeKey, onChange }) {
|
||||
</li>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -61,15 +35,10 @@ export default function SideNav({ activeKey, onChange }) {
|
||||
// 키-경로 매핑 export (Sidebar에서 사용)
|
||||
export const keyToPath = {
|
||||
gnb1: 'ship',
|
||||
gnb2: 'satellite',
|
||||
gnb3: 'weather',
|
||||
gnb4: 'analysis',
|
||||
gnb5: 'timeline',
|
||||
gnb6: 'ai',
|
||||
gnb7: 'replay',
|
||||
gnb8: 'area-search',
|
||||
filter: 'filter',
|
||||
layer: 'layer',
|
||||
};
|
||||
|
||||
export const pathToKey = Object.fromEntries(
|
||||
|
||||
@ -2,25 +2,9 @@ import { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
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 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,
|
||||
};
|
||||
|
||||
// 활성 키에 따른 패널 컴포넌트 렌더링 (퍼블리시 패널이 없으면 null 반환)
|
||||
// 활성 키에 따른 패널 컴포넌트 렌더링
|
||||
const renderPanel = () => {
|
||||
const panelMap = {
|
||||
gnb1: DisplayComponent ? <DisplayComponent {...panelProps} initialTab="filter" /> : null,
|
||||
gnb2: <SatellitePage {...panelProps} />,
|
||||
gnb3: <WeatherPage {...panelProps} />,
|
||||
gnb4: Panel4Component ? <Panel4Component {...panelProps} /> : null,
|
||||
gnb5: Panel5Component ? <Panel5Component {...panelProps} /> : null,
|
||||
gnb6: Panel6Component ? <Panel6Component {...panelProps} /> : null,
|
||||
gnb1: null, // TODO: 필터/디스플레이 패널 재구현
|
||||
gnb4: null, // TODO: 분석 패널
|
||||
gnb5: null, // TODO: 타임라인 패널
|
||||
gnb7: <ReplayPage {...panelProps} />,
|
||||
gnb8: <AreaSearchPage {...panelProps} />,
|
||||
filter: DisplayComponent ? <DisplayComponent {...panelProps} initialTab="filter" /> : null,
|
||||
layer: DisplayComponent ? <DisplayComponent {...panelProps} initialTab="layer" /> : 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 useTrackingModeStore from '../../stores/trackingModeStore';
|
||||
import useShipSearch from '../../hooks/useShipSearch';
|
||||
import PatrolShipSelector from './PatrolShipSelector';
|
||||
import './TopBar.scss';
|
||||
|
||||
/**
|
||||
@ -353,8 +352,6 @@ export default function TopBar() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 함정 선택 드롭다운 */}
|
||||
<PatrolShipSelector />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import useShipStore from '../../stores/shipStore';
|
||||
import { useTrackQueryStore } from '../../tracking/stores/trackQueryStore';
|
||||
import useTrackingModeStore, { RADIUS_OPTIONS, isPatrolShip } from '../../stores/trackingModeStore';
|
||||
import useTrackingModeStore, { RADIUS_OPTIONS } from '../../stores/trackingModeStore';
|
||||
import {
|
||||
fetchVesselTracksV2,
|
||||
convertToProcessedTracks,
|
||||
@ -184,12 +184,12 @@ export default function ShipContextMenu() {
|
||||
|
||||
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) => {
|
||||
if (item.key === 'radius') return isSinglePatrolShip;
|
||||
if (item.key === 'radius') return isSingleShip;
|
||||
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 @@
|
||||
/**
|
||||
* 선박 데이터 관리 훅
|
||||
* - 초기 선박 데이터 API 로드 (/all/12)
|
||||
* - STOMP WebSocket 연결 및 구독
|
||||
* - Web Worker를 통한 데이터 파싱 (메인 스레드 부담 감소)
|
||||
* - 배치 머지 최적화 (500ms 인터벌)
|
||||
*
|
||||
* 참조: mda-react-front/src/map/MapUpdater.tsx
|
||||
* 위성통신망 환경 최적화: 최소 트래픽, 최소 스펙
|
||||
* 선박 데이터 관리 훅 (HTTP 폴링 방식)
|
||||
* - 초기 로드: AIS Target API에서 최근 60분 데이터 전체 조회
|
||||
* - 이후 1분마다 최근 2분 데이터를 증분 조회하여 스토어에 병합
|
||||
*/
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import {
|
||||
signalStompClient,
|
||||
connectStomp,
|
||||
disconnectStomp,
|
||||
subscribeShipsRaw,
|
||||
subscribeShipDelete,
|
||||
} from '../common/stompClient';
|
||||
import useShipStore from '../stores/shipStore';
|
||||
import { fetchAllSignalsRaw } from '../api/signalApi';
|
||||
import { searchAisTargets, aisTargetToFeature } from '../api/aisTargetApi';
|
||||
|
||||
// =====================
|
||||
// Web Worker 인스턴스 생성
|
||||
// =====================
|
||||
const SignalWorker = new Worker(
|
||||
new URL('../workers/signalWorker.js', import.meta.url),
|
||||
{ type: 'module' }
|
||||
);
|
||||
/** 폴링 간격 (ms) */
|
||||
const POLLING_INTERVAL_MS = 60 * 1000; // 1분
|
||||
|
||||
// =====================
|
||||
// 배치 처리 설정
|
||||
// 참조: mda-react-front/src/map/MapUpdater.tsx
|
||||
// =====================
|
||||
const WEBSOCKET_CHUNK_INTERVAL = 500; // WebSocket 데이터 청크 처리 주기 (ms)
|
||||
/** 초기 로드 기간 (분) */
|
||||
const INITIAL_LOAD_MINUTES = 60;
|
||||
|
||||
/** 증분 로드 기간 (분) */
|
||||
const INCREMENT_MINUTES = 2; // 약간의 중복 허용으로 누락 방지
|
||||
|
||||
/**
|
||||
* 선박 데이터 관리 훅
|
||||
* @param {Object} options - 옵션
|
||||
* @param {boolean} options.autoConnect - 자동 연결 여부 (기본값: true)
|
||||
* @param {boolean} options.autoConnect - 자동 시작 여부 (기본값: true)
|
||||
* @returns {Object} { isConnected, isLoading, connect, disconnect }
|
||||
*/
|
||||
export default function useShipData(options = {}) {
|
||||
const { autoConnect = true } = options;
|
||||
|
||||
const subscriptionsRef = useRef([]);
|
||||
const shipBufferRef = useRef([]); // Raw 문자열 버퍼
|
||||
const batchIntervalRef = useRef(null); // 배치 처리 인터벌
|
||||
const pollingRef = useRef(null);
|
||||
const initialLoadDoneRef = useRef(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const mergeFeatures = useShipStore((s) => s.mergeFeatures);
|
||||
const deleteFeatureById = useShipStore((s) => s.deleteFeatureById);
|
||||
const setConnected = useShipStore((s) => s.setConnected);
|
||||
const isConnected = useShipStore((s) => s.isConnected);
|
||||
|
||||
/**
|
||||
* Worker 메시지 핸들러 (파싱된 선박 데이터 수신)
|
||||
* AIS 데이터를 shipStore feature 형식으로 변환하여 머지
|
||||
*/
|
||||
const handleWorkerMessage = useCallback((e) => {
|
||||
const ships = e.data;
|
||||
if (ships.length > 0) {
|
||||
mergeFeatures(ships);
|
||||
const loadAndMerge = useCallback(async (minutes) => {
|
||||
try {
|
||||
const aisTargets = await searchAisTargets(minutes);
|
||||
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]);
|
||||
|
||||
/**
|
||||
* Worker 설정
|
||||
* 폴링 시작
|
||||
*/
|
||||
useEffect(() => {
|
||||
SignalWorker.onmessage = handleWorkerMessage;
|
||||
SignalWorker.onerror = (err) => {
|
||||
console.error('[SignalWorker] Error:', err);
|
||||
};
|
||||
const startPolling = useCallback(() => {
|
||||
if (pollingRef.current) return;
|
||||
|
||||
return () => {
|
||||
SignalWorker.onmessage = null;
|
||||
SignalWorker.onerror = null;
|
||||
};
|
||||
}, [handleWorkerMessage]);
|
||||
pollingRef.current = setInterval(() => {
|
||||
loadAndMerge(INCREMENT_MINUTES);
|
||||
}, POLLING_INTERVAL_MS);
|
||||
|
||||
console.log(`[useShipData] Polling started: ${POLLING_INTERVAL_MS}ms interval`);
|
||||
}, [loadAndMerge]);
|
||||
|
||||
/**
|
||||
* Raw 선박 메시지 수신 핸들러 (버퍼에 누적)
|
||||
* @param {string[]} lines - 파이프 구분 문자열 배열
|
||||
* 폴링 중지
|
||||
*/
|
||||
const handleShipMessageRaw = useCallback((lines) => {
|
||||
// 버퍼에 추가
|
||||
shipBufferRef.current.push(...lines);
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
pollingRef.current = null;
|
||||
console.log('[useShipData] Polling stopped');
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 버퍼 플러시 - Worker로 전송
|
||||
* 연결 (초기 로드 + 폴링 시작)
|
||||
*/
|
||||
const flushBuffer = useCallback(() => {
|
||||
if (shipBufferRef.current.length === 0) return;
|
||||
|
||||
// 버퍼 복사 후 초기화
|
||||
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(() => {
|
||||
const connect = useCallback(async () => {
|
||||
if (initialLoadDoneRef.current) {
|
||||
startPolling();
|
||||
setConnected(true);
|
||||
startSubscriptions();
|
||||
}, [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
|
||||
return;
|
||||
}
|
||||
});
|
||||
subscriptionsRef.current = [];
|
||||
|
||||
disconnectStomp();
|
||||
}, [stopBatchInterval]);
|
||||
|
||||
/**
|
||||
* 초기 선박 데이터 로드 (API 호출)
|
||||
* Worker를 통해 파싱
|
||||
*/
|
||||
const loadInitialData = useCallback(async () => {
|
||||
if (initialLoadDoneRef.current) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('[useShipData] Loading initial ship data...');
|
||||
const rawLines = await fetchAllSignalsRaw();
|
||||
|
||||
if (rawLines.length > 0) {
|
||||
// Worker로 전송하여 파싱
|
||||
SignalWorker.postMessage(rawLines);
|
||||
console.log(`[useShipData] Initial data sent to Worker: ${rawLines.length} ships`);
|
||||
}
|
||||
const count = await loadAndMerge(INITIAL_LOAD_MINUTES);
|
||||
console.log(`[useShipData] Initial load complete: ${count} ships`);
|
||||
initialLoadDoneRef.current = true;
|
||||
setConnected(true);
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
console.error('[useShipData] Initial load error:', error);
|
||||
} finally {
|
||||
initialLoadDoneRef.current = true;
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [loadAndMerge, startPolling, setConnected]);
|
||||
|
||||
// 초기화: API로 선박 데이터 로드 후 STOMP 연결
|
||||
/**
|
||||
* 연결 해제
|
||||
*/
|
||||
const disconnect = useCallback(() => {
|
||||
stopPolling();
|
||||
setConnected(false);
|
||||
}, [stopPolling, setConnected]);
|
||||
|
||||
// 자동 시작
|
||||
useEffect(() => {
|
||||
if (!autoConnect) return;
|
||||
|
||||
const initialize = async () => {
|
||||
await loadInitialData();
|
||||
connect();
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
return () => {
|
||||
stopBatchInterval();
|
||||
disconnect();
|
||||
stopPolling();
|
||||
};
|
||||
}, [autoConnect]); // loadInitialData, connect, disconnect, stopBatchInterval를 deps에서 제외 (의도적)
|
||||
}, [autoConnect]); // connect, stopPolling를 deps에서 제외 (의도적)
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
|
||||
@ -7,7 +7,6 @@ import { defaults as defaultInteractions, DragBox } from 'ol/interaction';
|
||||
import { platformModifierKeyOnly } from 'ol/events/condition';
|
||||
|
||||
import { createBaseLayers } from './layers/baseLayer';
|
||||
import { satelliteLayer, csvDeckLayer } from './layers/satelliteLayer';
|
||||
import { useMapStore, BASE_MAP_TYPES } from '../stores/mapStore';
|
||||
import useShipStore from '../stores/shipStore';
|
||||
import useShipData from '../hooks/useShipData';
|
||||
@ -46,7 +45,6 @@ import useMeasure from './measure/useMeasure';
|
||||
import useTrackingMode from '../hooks/useTrackingMode';
|
||||
import useFavoriteData from '../hooks/useFavoriteData';
|
||||
import useRealmLayer from '../hooks/useRealmLayer';
|
||||
import useCoastGuardLayer from '../hooks/useCoastGuardLayer';
|
||||
import './measure/measure.scss';
|
||||
import './MapContainer.scss';
|
||||
|
||||
@ -80,9 +78,6 @@ export default function MapContainer() {
|
||||
// 관심구역 OL 레이어
|
||||
useRealmLayer();
|
||||
|
||||
// 해경관할구역 FGB 레이어
|
||||
useCoastGuardLayer();
|
||||
|
||||
// Deck.gl 선박 레이어
|
||||
const { deckRef } = useShipLayer(map);
|
||||
|
||||
@ -455,8 +450,6 @@ export default function MapContainer() {
|
||||
worldMap,
|
||||
encMap,
|
||||
darkMap,
|
||||
satelliteLayer,
|
||||
csvDeckLayer,
|
||||
eastAsiaMap,
|
||||
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
불러오는 중...
Reference in New Issue
Block a user