diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md new file mode 100644 index 00000000..facaabf3 --- /dev/null +++ b/.claude/rules/code-style.md @@ -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 ( +
+

{name}

+

{email}

+ {onEdit && } +
+ ); +}; + +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/`에 관리 diff --git a/.claude/rules/git-workflow.md b/.claude/rules/git-workflow.md new file mode 100644 index 00000000..4fee6185 --- /dev/null +++ b/.claude/rules/git-workflow.md @@ -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 권장 (깔끔한 히스토리) +- 머지 후 소스 브랜치 삭제 diff --git a/.claude/rules/naming.md b/.claude/rules/naming.md new file mode 100644 index 00000000..4b3c0b18 --- /dev/null +++ b/.claude/rules/naming.md @@ -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 +``` diff --git a/.claude/rules/team-policy.md b/.claude/rules/team-policy.md new file mode 100644 index 00000000..16d7553b --- /dev/null +++ b/.claude/rules/team-policy.md @@ -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에 프로젝트 빌드/실행 방법 유지 diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 00000000..5b41b54c --- /dev/null +++ b/.claude/rules/testing.md @@ -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(); + expect(screen.getByText('홍길동')).toBeInTheDocument(); + expect(screen.getByText('hong@test.com')).toBeInTheDocument(); + }); + + it('편집 버튼 클릭 시 onEdit 콜백을 호출한다', async () => { + const onEdit = vi.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: '편집' })); + expect(onEdit).toHaveBeenCalledOnce(); + }); +}); +``` + +### 테스트 패턴 +- **Arrange-Act-Assert** 구조 +- 테스트 설명은 한국어로 작성 (`it('사용자 이름을 표시한다')`) +- 하나의 테스트에 하나의 검증 + +## 테스트 커버리지 +- 새로 작성하는 유틸리티 함수: 테스트 필수 +- 컴포넌트: 주요 상호작용 테스트 권장 +- API 호출: MSW로 모킹하여 에러/성공 시나리오 테스트 + +## 금지 사항 +- 구현 세부사항 테스트 금지 (state 값 직접 확인 등) +- `getByTestId` 남용 금지 (접근성 쿼리 우선) +- 스냅샷 테스트 남용 금지 (변경에 취약) +- `setTimeout`으로 비동기 대기 금지 → `waitFor`, `findBy` 사용 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..981e9494 --- /dev/null +++ b/.claude/settings.json @@ -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 + } + ] + } + ] + } +} diff --git a/.claude/skills/create-mr/SKILL.md b/.claude/skills/create-mr/SKILL.md new file mode 100644 index 00000000..741fbc25 --- /dev/null +++ b/.claude/skills/create-mr/SKILL.md @@ -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 접근 토큰 (없으면 안내) diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md new file mode 100644 index 00000000..70452ea9 --- /dev/null +++ b/.claude/skills/fix-issue/SKILL.md @@ -0,0 +1,49 @@ +--- +name: fix-issue +description: Gitea 이슈를 분석하고 수정 브랜치를 생성합니다 +allowed-tools: "Bash, Read, Write, Edit, Glob, Grep" +argument-hint: "" +--- + +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 접근 토큰 diff --git a/.claude/skills/init-project/SKILL.md b/.claude/skills/init-project/SKILL.md new file mode 100644 index 00000000..f0d4c308 --- /dev/null +++ b/.claude/skills/init-project/SKILL.md @@ -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` 확인 +- 빌드 명령 실행 가능 확인 +- 다음 단계 안내 (개발 시작, 첫 커밋 방법 등) diff --git a/.claude/skills/sync-team-workflow/SKILL.md b/.claude/skills/sync-team-workflow/SKILL.md new file mode 100644 index 00000000..ad0e5eea --- /dev/null +++ b/.claude/skills/sync-team-workflow/SKILL.md @@ -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) 표시 +- 필요한 추가 조치 안내 (빌드 확인, 의존성 업데이트 등) diff --git a/.claude/workflow-version.json b/.claude/workflow-version.json new file mode 100644 index 00000000..fb423eb2 --- /dev/null +++ b/.claude/workflow-version.json @@ -0,0 +1 @@ +{"applied_global_version": "1.2.0"} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6f831b50 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.env b/.env index ba05b9ba..8492e15d 100644 --- a/.env +++ b/.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 diff --git a/.env.dev b/.env.dev deleted file mode 100644 index 7d751e87..00000000 --- a/.env.dev +++ /dev/null @@ -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 diff --git a/.env.development b/.env.development index 6f09b3ff..f480fa97 100644 --- a/.env.development +++ b/.env.development @@ -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 diff --git a/.env.qa b/.env.qa deleted file mode 100644 index 1ec19406..00000000 --- a/.env.qa +++ /dev/null @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6171b0d5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# 오프라인 캐시 바이너리 보호 (Windows autocrlf 변환 방지) +*.tgz binary + diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 00000000..93bb350c --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,60 @@ +#!/bin/bash +#============================================================================== +# commit-msg hook +# Conventional Commits 형식 검증 (한/영 혼용 지원) +#============================================================================== + +COMMIT_MSG_FILE="$1" +COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") + +# Merge 커밋은 검증 건너뜀 +if echo "$COMMIT_MSG" | head -1 | grep -qE "^Merge "; then + exit 0 +fi + +# Revert 커밋은 검증 건너뜀 +if echo "$COMMIT_MSG" | head -1 | grep -qE "^Revert "; then + exit 0 +fi + +# Conventional Commits 정규식 +# type(scope): subject +# - type: feat|fix|docs|style|refactor|test|chore|ci|perf (필수) +# - scope: 영문, 숫자, 한글, 점, 밑줄, 하이픈 허용 (선택) +# - subject: 1~72자, 한/영 혼용 허용 (필수) +PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\([a-zA-Z0-9가-힣._-]+\))?: .{1,72}$' + +FIRST_LINE=$(head -1 "$COMMIT_MSG_FILE") + +if ! echo "$FIRST_LINE" | grep -qE "$PATTERN"; then + echo "" + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ 커밋 메시지가 Conventional Commits 형식에 맞지 않습니다 ║" + echo "╚══════════════════════════════════════════════════════════════╝" + echo "" + echo " 올바른 형식: type(scope): subject" + echo "" + echo " type (필수):" + echo " feat — 새로운 기능" + echo " fix — 버그 수정" + echo " docs — 문서 변경" + echo " style — 코드 포맷팅" + echo " refactor — 리팩토링" + echo " test — 테스트" + echo " chore — 빌드/설정 변경" + echo " ci — CI/CD 변경" + echo " perf — 성능 개선" + echo "" + echo " scope (선택): 한/영 모두 가능" + echo " subject (필수): 1~72자, 한/영 모두 가능" + echo "" + echo " 예시:" + echo " feat(auth): JWT 기반 로그인 구현" + echo " fix(배치): 야간 배치 타임아웃 수정" + echo " docs: README 업데이트" + echo " chore: Gradle 의존성 업데이트" + echo "" + echo " 현재 메시지: $FIRST_LINE" + echo "" + exit 1 +fi diff --git a/.githooks/post-checkout b/.githooks/post-checkout new file mode 100755 index 00000000..bae360fe --- /dev/null +++ b/.githooks/post-checkout @@ -0,0 +1,25 @@ +#!/bin/bash +#============================================================================== +# post-checkout hook +# 브랜치 체크아웃 시 core.hooksPath 자동 설정 +# clone/checkout 후 .githooks 디렉토리가 있으면 자동으로 hooksPath 설정 +#============================================================================== + +# post-checkout 파라미터: prev_HEAD, new_HEAD, branch_flag +# branch_flag=1: 브랜치 체크아웃, 0: 파일 체크아웃 +BRANCH_FLAG="$3" + +# 파일 체크아웃은 건너뜀 +if [ "$BRANCH_FLAG" = "0" ]; then + exit 0 +fi + +# .githooks 디렉토리 존재 확인 +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +if [ -d "${REPO_ROOT}/.githooks" ]; then + CURRENT_HOOKS_PATH=$(git config core.hooksPath 2>/dev/null || echo "") + if [ "$CURRENT_HOOKS_PATH" != ".githooks" ]; then + git config core.hooksPath .githooks + chmod +x "${REPO_ROOT}/.githooks/"* 2>/dev/null + fi +fi diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..681c3ced --- /dev/null +++ b/.githooks/pre-commit @@ -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 diff --git a/.gitignore b/.gitignore index 33c0f474..ba1152d6 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..42a1c98a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.22.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..3cd8426e --- /dev/null +++ b/CLAUDE.md @@ -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/* diff --git a/public/fgb/해경관할구역.fgb b/public/fgb/해경관할구역.fgb deleted file mode 100644 index 379f4a08..00000000 Binary files a/public/fgb/해경관할구역.fgb and /dev/null differ diff --git a/setup-windows.bat b/setup-windows.bat deleted file mode 100644 index af83981d..00000000 --- a/setup-windows.bat +++ /dev/null @@ -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 diff --git a/src/App.jsx b/src/App.jsx index d9600238..164b24c9 100644 --- a/src/App.jsx +++ b/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: () => ( -
- publish 폴더가 없습니다. 퍼블리시 파일을 추가하면 자동으로 활성화됩니다. -
- ), - })) - ) - : null; - export default function App() { return ( - {/* ===================== - 구현 영역 (메인) - - 모든 메뉴 경로를 MainLayout으로 처리 - ===================== */} } /> - - {/* ===================== - 퍼블리시 영역 (개발 환경 전용) - /publish/* 로 접근하여 퍼블리시 결과물 미리보기 - 프로덕션 빌드 시 이 라우트와 관련 모듈이 제외됨 - ===================== */} - {/*{import.meta.env.DEV && PublishRouter && (*/} - {/* Loading publish...}>*/} - {/* */} - {/* */} - {/* }*/} - {/* />*/} - {/*)}*/} ); diff --git a/src/api/aisTargetApi.js b/src/api/aisTargetApi.js new file mode 100644 index 00000000..3c92d68f --- /dev/null +++ b/src/api/aisTargetApi.js @@ -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} 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 ''; + } +} diff --git a/src/api/satelliteApi.js b/src/api/satelliteApi.js deleted file mode 100644 index 60beab89..00000000 --- a/src/api/satelliteApi.js +++ /dev/null @@ -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>} - */ -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} 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>} - */ -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>} - */ -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} 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} 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; - } -} diff --git a/src/api/weatherApi.js b/src/api/weatherApi.js deleted file mode 100644 index 0c034065..00000000 --- a/src/api/weatherApi.js +++ /dev/null @@ -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} - */ -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} 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} 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} ObservatorySelectDetailDto 또는 null - */ -const AIRPORT_ENDPOINT = '/api/gis/weather/airport/search'; -const AIRPORT_DETAIL_ENDPOINT = '/api/gis/weather/airport/select'; - -/** - * 공항 목록 조회 - * - * @returns {Promise} 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} 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; - } -} diff --git a/src/assets/img/shipDetail/detailKindIcon/kcgv.svg b/src/assets/img/shipDetail/detailKindIcon/kcgv.svg deleted file mode 100644 index 50d0f20d..00000000 --- a/src/assets/img/shipDetail/detailKindIcon/kcgv.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/kcgv.svg b/src/assets/img/shipKindIcons/kcgv.svg deleted file mode 100644 index 5afdfa0c..00000000 --- a/src/assets/img/shipKindIcons/kcgv.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/component/WrapComponent.jsx b/src/component/WrapComponent.jsx deleted file mode 100644 index e0bb67a2..00000000 --- a/src/component/WrapComponent.jsx +++ /dev/null @@ -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( -
- - - - -
- ) -} \ No newline at end of file diff --git a/src/component/common/FileUpload.jsx b/src/component/common/FileUpload.jsx deleted file mode 100644 index f55348c6..00000000 --- a/src/component/common/FileUpload.jsx +++ /dev/null @@ -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 ( -
- - - - {fileName ? truncateMiddle(fileName, maxLength) : placeholder} - -
- ); -} diff --git a/src/component/common/Slider.jsx b/src/component/common/Slider.jsx deleted file mode 100644 index a8d7e9b2..00000000 --- a/src/component/common/Slider.jsx +++ /dev/null @@ -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 ( - - ); -} - -export default Slider; diff --git a/src/component/wrap/HeaderComponent.jsx b/src/component/wrap/HeaderComponent.jsx deleted file mode 100644 index 1d9c98a1..00000000 --- a/src/component/wrap/HeaderComponent.jsx +++ /dev/null @@ -1,17 +0,0 @@ - -import { Link } from "react-router-dom"; - -export default function HeaderComponent() { - return( - - ) -} \ No newline at end of file diff --git a/src/component/wrap/MainComponent.jsx b/src/component/wrap/MainComponent.jsx deleted file mode 100644 index 688c707d..00000000 --- a/src/component/wrap/MainComponent.jsx +++ /dev/null @@ -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 ( -
- - - - {/* 기본 화면 */} - } /> - } /> - - } /> - } /> - } /> - } /> - - } /> - - } /> - } /> - } /> - {/* } /> */} - - } /> - - } /> - } /> - - } /> - -
- ); -} - diff --git a/src/component/wrap/SideComponent.jsx b/src/component/wrap/SideComponent.jsx deleted file mode 100644 index 52692c3c..00000000 --- a/src/component/wrap/SideComponent.jsx +++ /dev/null @@ -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 ( -
- - -
- - {/* 초기 진입 시 Panel1 */} - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
-
- ); -} diff --git a/src/component/wrap/ToolComponent.jsx b/src/component/wrap/ToolComponent.jsx deleted file mode 100644 index fb39b1c4..00000000 --- a/src/component/wrap/ToolComponent.jsx +++ /dev/null @@ -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( -
- {/* 툴바 */} -
-
    -
  • -
  • - -
  • -
  • -
-
    -
  • -
  • -
  • -
-
    -
  • -
  • -
-
- {/* 맵컨트롤 툴바 */} -
-
    -
  • -
  • 7
  • -
  • -
-
    -
  • -
  • -
  • -
-
- {/* 범례 */} - {isLegendOpen && ( -
-
    -
  • - 통합통합 - 0 -
  • -
  • - 중국어선중국어선 - 0 -
  • -
  • - 중국어선허가중국어선허가 - 0 -
  • -
  • - 일본어선일본어선 - 0 -
  • -
  • - 위험물위험물 - 0 -
  • -
  • - 여객선여객선 - 0 -
  • -
  • - 함정함정 - 0 -
  • -
  • - 함정-RADAR함정-RADAR - 0 -
  • -
  • - 일반일반 - 0 -
  • -
  • - VTS-일반VTS-일반 - 0 -
  • -
  • - VTS-RADARVTS-RADAR - 0 -
  • -
  • - VPASS일반VPASS일반 - 0 -
  • -
  • - ENAV어선ENAV어선 - 0 -
  • -
  • - ENAV위험물ENAV위험물 - 0 -
  • -
  • - ENAV화물선ENAV화물선 - 0 -
  • -
  • - ENAV관공선ENAV관공선 - 0 -
  • -
  • - ENAV일반ENAV일반 - 0 -
  • -
  • - D-MF/HFD-MF/HF - 0 -
  • -
  • - 항공기항공기 - 0 -
  • -
  • - NLLNLL - 0 -
  • -
-
- )} -
- ) -} \ No newline at end of file diff --git a/src/component/wrap/main/Analysis1Component.jsx b/src/component/wrap/main/Analysis1Component.jsx deleted file mode 100644 index 3a26ad26..00000000 --- a/src/component/wrap/main/Analysis1Component.jsx +++ /dev/null @@ -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 ( -
- - {/* 위성 영상 등록 팝업 */} - {isOpen && ( -
-
-
- 관심 해역 설정 -
- -
-
- - - -
-
- -
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/Analysis2Component.jsx b/src/component/wrap/main/Analysis2Component.jsx deleted file mode 100644 index 4a816e25..00000000 --- a/src/component/wrap/main/Analysis2Component.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState } from 'react'; - -export default function Analysis2Component() { - // 팝업 - const [isOpen, setIsOpen] = useState(true); // 처음 열림 - const closePopup = () => setIsOpen(false); - - return ( -
- - {/* 위성 영상 등록 팝업 */} - {isOpen && ( -
-
-
- 관심 해역 설정 -
- -
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
관심 해역 설정 - 해상영역명, 설정 옵션, 좌표,영역 옵션,해상영역명 크기, 해상영역명 색상,윤곽선 굵기,윤곽선 종류,윤곽선 색상,채우기 색상 에 대한 내용을 등록하는 표입니다.
해상영역명
설정 옵션 -
- - - -
-
좌표[124,96891368166156, 36.37855817450263]
- [125,25105622872591, 36.37855817450263]
- [125,25105622872591, 36.37855817450263]
- [125,25105622872591, 36.37855817450263]
- [125,25105622872591, 36.37855817450263] -
영역 옵션 -
- - -
-
해상영역명 크기 -
- -
- - -
-
-
해상영역명 색상
윤곽선 굵기 -
- -
- - -
-
-
윤곽선 종류 - -
윤곽선 색상 채우기 색상
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/Analysis3Component.jsx b/src/component/wrap/main/Analysis3Component.jsx deleted file mode 100644 index 054d96b0..00000000 --- a/src/component/wrap/main/Analysis3Component.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useState } from 'react'; - -export default function Analysis3Component() { - // 팝업 - const [isOpen, setIsOpen] = useState(true); // 처음 열림 - const closePopup = () => setIsOpen(false); - - return ( -
- - {/* 위성 영상 등록 팝업 */} - {isOpen && ( -
-
-
- 관심 해역 분석 등록 -
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
관심 해역 분석 등록 - 제목, 상세 내역, 공유 여부,공유 그룹 에 대한 내용을 등록하는 표입니다.
제목
상세 내역 - -
공유 여부 -
- - -
-
공유 그룹 - -
-
-
-
관심영역 목록
-
    -
  • - - -
  • -
  • - - -
  • -
-
-
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/LayerComponent.jsx b/src/component/wrap/main/LayerComponent.jsx deleted file mode 100644 index 20baa25b..00000000 --- a/src/component/wrap/main/LayerComponent.jsx +++ /dev/null @@ -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 ( -
- - {/* 레이어등록 팝업 */} - {isOpen && ( -
-
-
- 레이어 등록 -
- -
- - - - - - - - - - - - - - - - - - - - -
레이어등록 - 레이어명, 첨부파일, 공유설정 에 대한 내용을 나타내는 표입니다.
레이어명 *
첨부파일 * -
- - geojson 파일을 첨부해 주세요. -
-
공유설정 -
- - - -
-
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/MyPageComponent.jsx b/src/component/wrap/main/MyPageComponent.jsx deleted file mode 100644 index a2e8bc10..00000000 --- a/src/component/wrap/main/MyPageComponent.jsx +++ /dev/null @@ -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 ( -
- - {/* 내정보조회 팝업 */} - {isOpen && ( -
-
-
- 내 정보 조회 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
내 정보 조회 - 아이디, 비밀번호, 이름, 이메일, 직급, 상세소속, 공인인증서 삭제 에 대한 내용을 나타내는 표입니다.
아이디admin222
비밀번호
이름ADMIN
이메일123@korea.kr
직급경감
상세소속
공인인증서 삭제
-
- -
-
- - -
-
-
-
- )} - - {/* 비밀번호 변경 팝업 */} - {isPwPopupOpen && ( -
-
-
- 비밀번호 수정 -
-
- - - - - - - - - - - - - - - - - - - - -
비밀번호 수정 - 현재 비밀번호, 새 비밀번호, 새 비밀번호 확인 에 대한 내용을 나타내는 표입니다.
현재 비밀번호
새 비밀번호
새 비밀번호 확인
-
-
-
- - -
-
-
-
- )} - - {/* 공인인증서 삭제 팝업 */} - {isCertDeleteOpen && ( -
-
-
- 공인인증서 삭제 -
-
-
공인인증서를 삭제 하시겠습니까?
-
-
-
- - -
-
-
-
- )} -
- ); -} diff --git a/src/component/wrap/main/Satellite1Component.jsx b/src/component/wrap/main/Satellite1Component.jsx deleted file mode 100644 index 27ae709e..00000000 --- a/src/component/wrap/main/Satellite1Component.jsx +++ /dev/null @@ -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 ( -
- - {/* 위성 영상 등록 팝업 */} - {isOpen && ( -
-
-
- 위성 영상 등록 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위성 영상 등록 - 사업자명/위성명, 영상 촬영일, 위성영상파일,CSV 파일,위성영상명, 영상전송 주기,영상 종류,위성 궤도,영상 출처,촬영 목적,촬영 모드,취득방법,구매가격, 에 대한 내용을 등록하는 표입니다.
사업자명/위성명 * -
- - -
-
영상 촬영일 *
위성영상파일 * -
- -
-
CSV 파일 * -
- -
-
위성영상명 *
영상전송 주기 - -
영상 종류 -
- - - - - -
-
위성 궤도 - - 영상 출처 - -
촬영 목적 - - 촬영 모드 - -
취득방법 - - 구매가격 -
- -
- - -
-
-
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/Satellite2Component.jsx b/src/component/wrap/main/Satellite2Component.jsx deleted file mode 100644 index e5f48d5c..00000000 --- a/src/component/wrap/main/Satellite2Component.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useState } from 'react'; - -export default function Satellite2Component() { - // 팝업 - const [isOpen, setIsOpen] = useState(true); // 처음 열림 - const closePopup = () => setIsOpen(false); - - return ( -
- - {/* 위성 사업자 등록 팝업 */} - {isOpen && ( -
-
-
- 위성 사업자 등록 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위성 사업자 등록 - 사업자 분류, 사업자명, 국가, 소재지, 상세내역 에 대한 내용을 등록하는 표입니다.
사업자 분류 * - -
사업자명
국가 * - -
소재지
상세내역
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/Satellite3Component.jsx b/src/component/wrap/main/Satellite3Component.jsx deleted file mode 100644 index 17086346..00000000 --- a/src/component/wrap/main/Satellite3Component.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useState } from 'react'; - -export default function Satellite3Component() { - // 팝업 - const [isOpen, setIsOpen] = useState(true); // 처음 열림 - const closePopup = () => setIsOpen(false); - - return ( -
- - {/* 위성 관리 등록 팝업 */} - {isOpen && ( -
-
-
- 위성 관리 등록 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위성 관리 등록 - 사업자명, 위성명, 센서 타입, 촬영 해상도, 주파수, 상세내역 에 대한 내용을 등록하는 표입니다.
사업자명 * - -
위성명 *
센서 타입 - -
촬영 해상도
주파수
상세내역
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/Satellite4Component.jsx b/src/component/wrap/main/Satellite4Component.jsx deleted file mode 100644 index 145b3e92..00000000 --- a/src/component/wrap/main/Satellite4Component.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useState } from 'react'; - -export default function Satellite4Component() { - // 팝업 - const [isOpen, setIsOpen] = useState(true); // 처음 열림 - const closePopup = () => setIsOpen(false); - - return ( -
- - {/* 삭제 팝업 */} - {isOpen && ( -
-
-
- 삭제 -
- -
-
삭제 하시겠습니까?
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/ShipComponent.jsx b/src/component/wrap/main/ShipComponent.jsx deleted file mode 100644 index 5cd42583..00000000 --- a/src/component/wrap/main/ShipComponent.jsx +++ /dev/null @@ -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( -
- - {/* 배정보 팝업 */} -
- {/* header */} -
-
- - 대한민국 - 1511함A-05 - 13450135 -
- -
- -
- - - - {/* 이미지 영역 */} -
- {images[currentIndex].alt} -
-
- {/* body */} -
-
-
- -
    -
  • A
  • -
  • V
  • -
  • E
  • -
  • T
  • -
  • D
  • -
  • R
  • -
-
- -
- -
-
- {value}% - -
-
- -
    -
  • -
    - 출항지 - 서귀포해양경찰서 -
    -
    - 입항지 - 하태도 -
    -
  • -
  • -
    - 출항일시 - 2024-11-23 11:23:00 -
    -
    - 입항일시 - 2024-11-23 11:23:00 -
    -
  • -
  • -
    - 선박상태 - 정박 -
    -
    - 속도/항로 - 4.2 kn / 13.3˚ -
    -
    - 흘수 - 1.1m -
    -
  • -
- - {/*
    -
  • - AIS - 정상 -
  • -
  • - RF - 정상 -
  • -
  • - EO - 정상 -
  • -
  • - SAR - 비활성 -
  • -
*/} -
- - -
-
- {/* footer */} -
데이터 수신시간 : 2024-11-23 11:23:00
-
-
- ) -} \ No newline at end of file diff --git a/src/component/wrap/main/Signal1Component.jsx b/src/component/wrap/main/Signal1Component.jsx deleted file mode 100644 index 7bff3702..00000000 --- a/src/component/wrap/main/Signal1Component.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useState } from 'react'; - -export default function Signal1Component() { - // 팝업 - const [isOpen, setIsOpen] = useState(true); // 처음 열림 - const closePopup = () => setIsOpen(false); - - return ( -
- - {/* 신호설정 팝업 */} - {isOpen && ( -
-
-
- 신호설정 -
- -
- - - - - - - - - - - - - - - - -
신호설정 - 신호표출반경, 수신수기 설정 에 대한 내용을 나타내는 표입니다.
신호표출반경 - -
수신수기 설정
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/Signal2Component.jsx b/src/component/wrap/main/Signal2Component.jsx deleted file mode 100644 index 46c56d08..00000000 --- a/src/component/wrap/main/Signal2Component.jsx +++ /dev/null @@ -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 ( -
- - {/* 신호설정 팝업 */} - {isOpen && ( -
-
-
- 맞춤 설정 -
- -
- {/* 아코디언그룹 01 */} -
-
- NLL 고속 선박 탐지 - -
- {/* 여기서부터 아코디언 */} -
-
    -
  • - - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- {/* 여기까지 */} -
- - {/* 아코디언그룹 02 */} -
-
- 특정 어업수역 탐지 - -
- {/* 여기서부터 아코디언 */} -
-
    -
  • - - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- {/* 여기까지 */} -
- - {/* 아코디언그룹 03 */} -
-
- 위험화물 식별 - -
- {/* 여기서부터 아코디언 */} -
-
    -
  • - - -
  • -
-
- {/* 여기까지 */} -
-
- -
-
- - -
-
-
-
- )} - -
- ); -} diff --git a/src/component/wrap/main/ToastComponent.jsx b/src/component/wrap/main/ToastComponent.jsx deleted file mode 100644 index 1748fe4b..00000000 --- a/src/component/wrap/main/ToastComponent.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Link } from "react-router-dom"; - -export default function ToastComponent() { - return( -
- - {/* 지도상 배표식 */} -
-
- - 1511함A-05 - 12.5 kts | 45° - -
- -
- - 1511함A-05 - 12.5 kts | 45° - -
- -
- - 1511함A-05 - 12.5 kts | 45° - -
-
- - {/* 토스트팝업 */} -
-
- 104 어업구역 비인가 선박 - - - - -
- -
- 104 어업구역 비인가 선박 - - - - -
- -
- 저속 이동 의심 선박 - - - - -
-
-
- ) -} \ No newline at end of file diff --git a/src/component/wrap/main/TopComponent.jsx b/src/component/wrap/main/TopComponent.jsx deleted file mode 100644 index f1ffa98a..00000000 --- a/src/component/wrap/main/TopComponent.jsx +++ /dev/null @@ -1,21 +0,0 @@ -export default function TopComponent() { - return( -
-
-
    -
  • -
  • 경도129° 38’31.071”E
  • -
  • 위도35° 21’24.580”N
  • -
  • KST2024-07-01(화) 12:00:00
  • -
  • -
  • -
-
- -
- - -
-
- ) -} \ No newline at end of file diff --git a/src/component/wrap/main/WeatherComponent.jsx b/src/component/wrap/main/WeatherComponent.jsx deleted file mode 100644 index 1a31a2fd..00000000 --- a/src/component/wrap/main/WeatherComponent.jsx +++ /dev/null @@ -1,62 +0,0 @@ -export default function WeatherComponent() { - return( -
- - {/* 지도위 팝업 */} -
- {/* header */} -
-
- 해양관측소 -
- -
- {/* body */} -
- -
    -
  • - 2023.10.16 20:54 -
  • -
  • - 조위 - 251(cm) -
  • -
  • - 수온 - 19.6(°C) -
  • -
  • - 염분 - 31.8(PSU) -
  • -
  • - 기온 - 16.9(°C) -
  • -
  • - 기압 - 1016.6(hPa) -
  • -
  • - 풍향 - 315(deg) -
  • -
  • - 풍속 - 7.1(m/s) -
  • -
  • - 유속방향 - -(deg) -
  • -
  • - 유속 - -(m/s) -
  • -
-
-
-
- ) -} \ No newline at end of file diff --git a/src/component/wrap/side/DisplayComponent.jsx b/src/component/wrap/side/DisplayComponent.jsx deleted file mode 100644 index cd6d65d4..00000000 --- a/src/component/wrap/side/DisplayComponent.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/component/wrap/side/NavComponent.jsx b/src/component/wrap/side/NavComponent.jsx deleted file mode 100644 index 0beaa8e2..00000000 --- a/src/component/wrap/side/NavComponent.jsx +++ /dev/null @@ -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( - - ) -} diff --git a/src/component/wrap/side/Panel1Component.jsx b/src/component/wrap/side/Panel1Component.jsx deleted file mode 100644 index 905ce205..00000000 --- a/src/component/wrap/side/Panel1Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/component/wrap/side/Panel2Component.jsx b/src/component/wrap/side/Panel2Component.jsx deleted file mode 100644 index 179f0ee8..00000000 --- a/src/component/wrap/side/Panel2Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/component/wrap/side/Panel3Component.jsx b/src/component/wrap/side/Panel3Component.jsx deleted file mode 100644 index 8abc7bbd..00000000 --- a/src/component/wrap/side/Panel3Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/component/wrap/side/Panel4Component.jsx b/src/component/wrap/side/Panel4Component.jsx deleted file mode 100644 index 5d773db5..00000000 --- a/src/component/wrap/side/Panel4Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/component/wrap/side/Panel5Component.jsx b/src/component/wrap/side/Panel5Component.jsx deleted file mode 100644 index a469706d..00000000 --- a/src/component/wrap/side/Panel5Component.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; -export default function Panel5Component() { - - return ( -
- ); -} diff --git a/src/component/wrap/side/Panel6Component.jsx b/src/component/wrap/side/Panel6Component.jsx deleted file mode 100644 index 0abb6593..00000000 --- a/src/component/wrap/side/Panel6Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/component/wrap/side/Panel7Component.jsx b/src/component/wrap/side/Panel7Component.jsx deleted file mode 100644 index 3389c12a..00000000 --- a/src/component/wrap/side/Panel7Component.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; -export default function Panel7Component() { - - return ( -
- ); -} diff --git a/src/component/wrap/side/Panel8Component.jsx b/src/component/wrap/side/Panel8Component.jsx deleted file mode 100644 index 868a706e..00000000 --- a/src/component/wrap/side/Panel8Component.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; -export default function Panel8Component() { - - return ( -
- ); -} diff --git a/src/components/auth/SessionGuard.jsx b/src/components/auth/SessionGuard.jsx index 529923d5..40c83d30 100644 --- a/src/components/auth/SessionGuard.jsx +++ b/src/components/auth/SessionGuard.jsx @@ -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 (
diff --git a/src/components/layout/SideNav.jsx b/src/components/layout/SideNav.jsx index 2c46f1e7..e5953b4a 100644 --- a/src/components/layout/SideNav.jsx +++ b/src/components/layout/SideNav.jsx @@ -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 ( ); } @@ -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( diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx index d4b44506..c099caad 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.jsx @@ -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 ? : null, - gnb2: , - gnb3: , - gnb4: Panel4Component ? : null, - gnb5: Panel5Component ? : null, - gnb6: Panel6Component ? : null, + gnb1: null, // TODO: 필터/디스플레이 패널 재구현 + gnb4: null, // TODO: 분석 패널 + gnb5: null, // TODO: 타임라인 패널 gnb7: , gnb8: , - filter: DisplayComponent ? : null, - layer: DisplayComponent ? : null, }; return panelMap[activeKey] || null; }; diff --git a/src/components/map/PatrolShipSelector.jsx b/src/components/map/PatrolShipSelector.jsx deleted file mode 100644 index ec804bb5..00000000 --- a/src/components/map/PatrolShipSelector.jsx +++ /dev/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 ( -
- {/* 헤더 */} -
- 경비함정 선택 - -
- - {/* 검색 영역 */} -
-
- - {searchValue && ( - - )} -
-
- - {/* 반경 설정 */} -
- 반경 설정 -
- {RADIUS_OPTIONS.map((radius) => ( - - ))} - NM -
-
- - {/* 함정 목록 */} -
- {filteredShips.length === 0 ? ( -
- {searchValue ? '검색 결과가 없습니다' : '활성화된 경비함정이 없습니다'} -
- ) : ( -
    - {filteredShips.map((item) => ( -
  • handleSelectShip(item)} - > - {item.shipName} - {item.originalTargetId} -
  • - ))} -
- )} -
- - {/* 푸터 */} -
- - {searchValue ? `${filteredShips.length} / ${patrolShips.length}척` : `${patrolShips.length}척`} - -
-
- ); -} diff --git a/src/components/map/PatrolShipSelector.scss b/src/components/map/PatrolShipSelector.scss deleted file mode 100644 index b0e0ccee..00000000 --- a/src/components/map/PatrolShipSelector.scss +++ /dev/null @@ -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; - } - } -} diff --git a/src/components/map/TopBar.jsx b/src/components/map/TopBar.jsx index 81147b94..c0e1689b 100644 --- a/src/components/map/TopBar.jsx +++ b/src/components/map/TopBar.jsx @@ -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() {
)} - {/* 함정 선택 드롭다운 */} - diff --git a/src/components/ship/ShipContextMenu.jsx b/src/components/ship/ShipContextMenu.jsx index cbcfbaa9..7d159409 100644 --- a/src/components/ship/ShipContextMenu.jsx +++ b/src/components/ship/ShipContextMenu.jsx @@ -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; }); diff --git a/src/hooks/useCoastGuardLayer.js b/src/hooks/useCoastGuardLayer.js deleted file mode 100644 index 9510d5f5..00000000 --- a/src/hooks/useCoastGuardLayer.js +++ /dev/null @@ -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); - } -} diff --git a/src/hooks/useShipData.js b/src/hooks/useShipData.js index cadf4f8b..bf1720a2 100644 --- a/src/hooks/useShipData.js +++ b/src/hooks/useShipData.js @@ -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); - }, []); - - /** - * 버퍼 플러시 - 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; + const stopPolling = useCallback(() => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + console.log('[useShipData] Polling stopped'); } - // 남은 버퍼 플러시 - flushBuffer(); - }, [flushBuffer]); + }, []); /** - * 선박 삭제 메시지 수신 핸들러 - * @param {string} featureId - signalSourceCode + targetId + * 연결 (초기 로드 + 폴링 시작) */ - const handleShipDelete = useCallback((featureId) => { - deleteFeatureById(featureId); - }, [deleteFeatureById]); - - /** - * 토픽 구독 시작 - */ - const startSubscriptions = useCallback(() => { - // 기존 구독 해제 - subscriptionsRef.current.forEach((sub) => { - try { - sub.unsubscribe(); - } catch (e) { - // ignore - } - }); - subscriptionsRef.current = []; - - // 선박 토픽 구독 (Raw 모드 - Worker용) - const shipSub = subscribeShipsRaw(handleShipMessageRaw); - subscriptionsRef.current.push(shipSub); - - // 선박 삭제 토픽 구독 - const deleteSub = subscribeShipDelete(handleShipDelete); - subscriptionsRef.current.push(deleteSub); - - // 배치 처리 인터벌 시작 - startBatchInterval(); - }, [handleShipMessageRaw, handleShipDelete, startBatchInterval]); - - /** - * 연결 성공 시 토픽 구독 - */ - const handleConnect = useCallback(() => { - setConnected(true); - 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 - } - }); - subscriptionsRef.current = []; - - disconnectStomp(); - }, [stopBatchInterval]); - - /** - * 초기 선박 데이터 로드 (API 호출) - * Worker를 통해 파싱 - */ - const loadInitialData = useCallback(async () => { - if (initialLoadDoneRef.current) return; + const connect = useCallback(async () => { + if (initialLoadDoneRef.current) { + startPolling(); + setConnected(true); + 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(); - + connect(); return () => { - stopBatchInterval(); - disconnect(); + stopPolling(); }; - }, [autoConnect]); // loadInitialData, connect, disconnect, stopBatchInterval를 deps에서 제외 (의도적) + }, [autoConnect]); // connect, stopPolling를 deps에서 제외 (의도적) return { isConnected, diff --git a/src/map/MapContainer.jsx b/src/map/MapContainer.jsx index f17e5eb2..ec904926 100644 --- a/src/map/MapContainer.jsx +++ b/src/map/MapContainer.jsx @@ -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, ], diff --git a/src/map/layers/satelliteLayer.js b/src/map/layers/satelliteLayer.js deleted file mode 100644 index 0b31678f..00000000 --- a/src/map/layers/satelliteLayer.js +++ /dev/null @@ -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); -} diff --git a/src/pages/SatellitePage.jsx b/src/pages/SatellitePage.jsx deleted file mode 100644 index 478ae26f..00000000 --- a/src/pages/SatellitePage.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/pages/WeatherPage.jsx b/src/pages/WeatherPage.jsx deleted file mode 100644 index 0e98bc16..00000000 --- a/src/pages/WeatherPage.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/publish/PublishRoutes.jsx b/src/publish/PublishRoutes.jsx deleted file mode 100644 index f65a738c..00000000 --- a/src/publish/PublishRoutes.jsx +++ /dev/null @@ -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 = ( - <> - {/* 기본 페이지 - 전체 레이아웃 미리보기 */} - } /> - - {/* 개별 패널 미리보기 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - {/* 전체 레이아웃 (원본 구조 그대로) */} - } /> - -); - -// 퍼블리시 홈 -function PublishHome() { - return ( -
-

퍼블리시 미리보기

-

좌측 메뉴에서 확인할 페이지를 선택하세요.

-
-

폴더 구조

-
-{`src/publish/
-├── _incoming/     # 새 퍼블리시 파일 (원본)
-├── layouts/       # 레이아웃 컴포넌트
-├── pages/         # 페이지 컴포넌트
-└── components/    # 공통 컴포넌트`}
-        
-

병합 방법

-
    -
  1. 새 퍼블리시 파일을 _incoming/ 폴더에 복사
  2. -
  3. Claude에게 병합 요청
  4. -
  5. 변경사항 확인 후 적용
  6. -
-
-
- ); -} - -// 패널 래퍼 컴포넌트들 -function Panel1Wrapper() { - return ( -
- {}} /> -
- ); -} - -function Panel2Wrapper() { - return ( -
- {}} /> -
- ); -} - -function Panel3Wrapper() { - return ( -
- {}} /> -
- ); -} - -function Panel4Wrapper() { - return ( -
- {}} /> -
- ); -} - -function Panel5Wrapper() { - return ( -
- {}} /> -
- ); -} - -function Panel6Wrapper() { - return ( -
- {}} /> -
- ); -} - -function Panel7Wrapper() { - return ( -
- {}} /> -
- ); -} - -function Panel8Wrapper() { - return ( -
- {}} /> -
- ); -} - -export default PublishRoutes; diff --git a/src/publish/components/FileUpload.jsx b/src/publish/components/FileUpload.jsx deleted file mode 100644 index f55348c6..00000000 --- a/src/publish/components/FileUpload.jsx +++ /dev/null @@ -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 ( -
- - - - {fileName ? truncateMiddle(fileName, maxLength) : placeholder} - -
- ); -} diff --git a/src/publish/components/Slider.jsx b/src/publish/components/Slider.jsx deleted file mode 100644 index a8d7e9b2..00000000 --- a/src/publish/components/Slider.jsx +++ /dev/null @@ -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 ( - - ); -} - -export default Slider; diff --git a/src/publish/index.jsx b/src/publish/index.jsx deleted file mode 100644 index 0af72d8d..00000000 --- a/src/publish/index.jsx +++ /dev/null @@ -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 ( - - }> - {PublishRoutes} - - - ); -} diff --git a/src/publish/layouts/HeaderComponent.jsx b/src/publish/layouts/HeaderComponent.jsx deleted file mode 100644 index ad1626d0..00000000 --- a/src/publish/layouts/HeaderComponent.jsx +++ /dev/null @@ -1,36 +0,0 @@ - -import { Link } from "react-router-dom"; - -export default function HeaderComponent() { - return( - - ) -} \ No newline at end of file diff --git a/src/publish/layouts/MainComponent.jsx b/src/publish/layouts/MainComponent.jsx deleted file mode 100644 index 147476e0..00000000 --- a/src/publish/layouts/MainComponent.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Outlet } from "react-router-dom"; -import TopComponent from "../pages/TopComponent"; - -export default function MainComponent() { - return ( -
- - -
- ); -} diff --git a/src/publish/layouts/PublishLayout.jsx b/src/publish/layouts/PublishLayout.jsx deleted file mode 100644 index a7d5adca..00000000 --- a/src/publish/layouts/PublishLayout.jsx +++ /dev/null @@ -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 ( -
- {/* 퍼블리시 네비게이션 */} - - - {/* 퍼블리시 콘텐츠 */} -
- -
-
- ); -} diff --git a/src/publish/layouts/SideComponent.jsx b/src/publish/layouts/SideComponent.jsx deleted file mode 100644 index 40be2dba..00000000 --- a/src/publish/layouts/SideComponent.jsx +++ /dev/null @@ -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 ( -
- - -
- {activePanel === "gnb1" && } - {activePanel === "gnb2" && } - {activePanel === "gnb3" && } - {activePanel === "gnb4" && } - {activePanel === "gnb5" && } - {activePanel === "gnb6" && } - {activePanel === "gnb7" && } - {activePanel === "gnb8" && } - {(activePanel === "filter" || activePanel === "layer") && ( - - )} -
-
- ); -} diff --git a/src/publish/layouts/ToolComponent.jsx b/src/publish/layouts/ToolComponent.jsx deleted file mode 100644 index 99ac99cf..00000000 --- a/src/publish/layouts/ToolComponent.jsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useState } from "react" -export default function ToolComponent() { - const [isLegendOpen, setIsLegendOpen] = useState(false); - - return( -
- {/* 툴바 */} -
-
    -
  • -
  • -
  • -
-
    -
  • -
  • -
  • -
-
    -
  • -
  • -
-
- {/* 맵컨트롤 툴바 */} -
-
    -
  • -
  • 7
  • -
  • -
-
    -
  • -
  • -
  • -
-
- {/* 범례 */} - {isLegendOpen && ( -
-
    -
  • - 통합통합 - 0 -
  • -
  • - 중국어선중국어선 - 0 -
  • -
  • - 중국어선허가중국어선허가 - 0 -
  • -
  • - 일본어선일본어선 - 0 -
  • -
  • - 위험물위험물 - 0 -
  • -
  • - 여객선여객선 - 0 -
  • -
  • - 함정함정 - 0 -
  • -
  • - 함정-RADAR함정-RADAR - 0 -
  • -
  • - 일반일반 - 0 -
  • -
  • - VTS-일반VTS-일반 - 0 -
  • -
  • - VTS-RADARVTS-RADAR - 0 -
  • -
  • - VPASS일반VPASS일반 - 0 -
  • -
  • - ENAV어선ENAV어선 - 0 -
  • -
  • - ENAV위험물ENAV위험물 - 0 -
  • -
  • - ENAV화물선ENAV화물선 - 0 -
  • -
  • - ENAV관공선ENAV관공선 - 0 -
  • -
  • - ENAV일반ENAV일반 - 0 -
  • -
  • - D-MF/HFD-MF/HF - 0 -
  • -
  • - 항공기항공기 - 0 -
  • -
  • - NLLNLL - 0 -
  • -
-
- )} -
- ) -} \ No newline at end of file diff --git a/src/publish/layouts/WrapComponent.jsx b/src/publish/layouts/WrapComponent.jsx deleted file mode 100644 index f4030866..00000000 --- a/src/publish/layouts/WrapComponent.jsx +++ /dev/null @@ -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 ( -
- - - {/* Main 영역 */} - -
- ); -} diff --git a/src/publish/pages/Analysis1Component.jsx b/src/publish/pages/Analysis1Component.jsx deleted file mode 100644 index 9c09260b..00000000 --- a/src/publish/pages/Analysis1Component.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Analysis1Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 위성 영상 등록 팝업 */} -
-
-
- 관심 해역 설정 -
- -
-
- - - -
-
- -
-
- -
- ); -} diff --git a/src/publish/pages/Analysis2Component.jsx b/src/publish/pages/Analysis2Component.jsx deleted file mode 100644 index 74210a34..00000000 --- a/src/publish/pages/Analysis2Component.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Analysis2Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 위성 영상 등록 팝업 */} -
-
-
- 관심 해역 설정 -
- -
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
관심 해역 설정 - 해상영역명, 설정 옵션, 좌표,영역 옵션,해상영역명 크기, 해상영역명 색상,윤곽선 굵기,윤곽선 종류,윤곽선 색상,채우기 색상 에 대한 내용을 등록하는 표입니다.
해상영역명
설정 옵션 -
- - - -
-
좌표[124,96891368166156, 36.37855817450263]
- [125,25105622872591, 36.37855817450263]
- [125,25105622872591, 36.37855817450263]
- [125,25105622872591, 36.37855817450263]
- [125,25105622872591, 36.37855817450263] -
영역 옵션 -
- - -
-
해상영역명 크기 -
- -
- - -
-
-
해상영역명 색상
윤곽선 굵기 -
- -
- - -
-
-
윤곽선 종류 - -
윤곽선 색상 채우기 색상
-
- -
-
- - -
-
-
-
-
- ); -} diff --git a/src/publish/pages/Analysis3Component.jsx b/src/publish/pages/Analysis3Component.jsx deleted file mode 100644 index bc53847f..00000000 --- a/src/publish/pages/Analysis3Component.jsx +++ /dev/null @@ -1,198 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Analysis3Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 위성 영상 등록 팝업 */} -
-
-
- 관심 해역 분석 등록 -
- -
- -
- {/* 지도캡쳐/테이블 영역 */} -
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
관심 해역 분석 등록 - 제목, 상세 내역, 공유 여부,공유 그룹 에 대한 내용을 등록하는 표입니다.
제목
상세 내역 - -
공유 여부 -
- - -
-
공유 그룹 - -
-
- {/* 관심영역 체크박스 목록 -스크롤됨 */} -
-
관심영역 목록
-
    -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
  • - - -
  • -
-
-
-
- -
-
- - -
-
-
-
-
- ); -} diff --git a/src/publish/pages/Analysis4Component.jsx b/src/publish/pages/Analysis4Component.jsx deleted file mode 100644 index efdc3c4d..00000000 --- a/src/publish/pages/Analysis4Component.jsx +++ /dev/null @@ -1,235 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Analysis4Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 위성 영상 등록 팝업 */} -
-
-
-
- 350 대해구도 - 조회시간: 2026-07-00 17:15:13 -
- -
- -
- -
- {/* 지도캡쳐/테이블 영역 */} -
-
통항 선박
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
통항 선박 - 선박 종류, 승선원, 위험물 운반, 공유 여부 및 그룹 에 대한 표입니다
카고(척)0
카고 승성원(명)-
탱커수(척)
탱커 승선원(명)
위험물 운반석(척)
위험물 운반선 승선원(명)
위험물 양(톤)
어선(척)
어선 승선원(명)
기타 어선(척)
기타 어선 승선원(명)
여객선(척)
유도선(척)
유도선 승선원(명)
기타 선박(척)
기타 선박 승선원(명)
함정수(척)
-
- {/* 관심영역 체크박스 목록 -스크롤됨 */} -
-
신호별
- - - - - - - - - - - - - - - - - - - - - - - - -
신호별 - 제목, 상세 내역, 공유 여부,공유 그룹 에 대한 내용을 등록하는 표입니다.
AIS0
V-PASS-
VHF
MFHF
- - -
E-NAV
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
E-NAV - 여객선, 어선, 카고, 관공선, 기타 선박과 공유 정보 에 대한 표입니다.
E-NAV 여객선(척)0
E-NAV 어선(척)-
E-NAV 카고(척)
E-NAV 관공선(척)
E-NAV 기타(척)
- - -
기상정보
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
기상정보 - 유향, 유속, 유의 파고, 파향, 파주기, 풍속, 풍향 을 나타내는 표입니다
유향0
유속-
유의 파고0.5(m)
파향 -
- 파향 - 350(°) -
-
파주기3.7(s)
풍속9.2(m/s)
풍향 -
- 풍향 - 45(°) -
-
-
-
-
- -
-
-
-
-
- ); -} diff --git a/src/publish/pages/DisplayComponent.jsx b/src/publish/pages/DisplayComponent.jsx deleted file mode 100644 index bf57d9e0..00000000 --- a/src/publish/pages/DisplayComponent.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/publish/pages/EmptyMain.jsx b/src/publish/pages/EmptyMain.jsx deleted file mode 100644 index 2550b09a..00000000 --- a/src/publish/pages/EmptyMain.jsx +++ /dev/null @@ -1,4 +0,0 @@ - -export default function EmptyMain() { - return null; // 또는 지도만 보여주는 영역 -} diff --git a/src/publish/pages/LayerComponent.jsx b/src/publish/pages/LayerComponent.jsx deleted file mode 100644 index 1117ff3f..00000000 --- a/src/publish/pages/LayerComponent.jsx +++ /dev/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 ( -
- - {/* 레이어등록 팝업 */} -
-
-
- 레이어 등록 -
- -
- - - - - - - - - - - - - - - - - - - - -
레이어등록 - 레이어명, 첨부파일, 공유설정 에 대한 내용을 나타내는 표입니다.
레이어명 *
첨부파일 * -
- - geojson 파일을 첨부해 주세요. -
-
공유설정 -
- - - -
-
-
- -
-
- - -
-
-
-
-
- ); -} diff --git a/src/publish/pages/MyPageComponent.jsx b/src/publish/pages/MyPageComponent.jsx deleted file mode 100644 index c8824c42..00000000 --- a/src/publish/pages/MyPageComponent.jsx +++ /dev/null @@ -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 ( -
- - {/* 내 정보 조회 */} -
-
-
- 내 정보 조회 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- 내 정보 조회 - 아이디, 비밀번호, 이름, 이메일, 직급, 상세소속, 공인인증서 삭제 -
아이디admin222
비밀번호 - -
이름ADMIN
이메일123@korea.kr
직급경감
상세소속
공인인증서 삭제 - -
-
- -
-
- - -
-
-
-
- - {/* 딤 + 서브 팝업 */} - {subPopup && ( -
- - {/* 비밀번호 변경 */} - {subPopup === "password" && ( -
-
- 비밀번호 수정 -
- -
- - - - - - - - - - - - - - - - - - - - -
- 비밀번호 수정 - 현재 비밀번호, 새 비밀번호, 새 비밀번호 확인 -
현재 비밀번호 - -
새 비밀번호 - -
새 비밀번호 확인 - -
-
- -
-
- - -
-
-
- )} - - {/* 공인인증서 삭제 */} - {subPopup === "cert" && ( -
-
- 공인인증서 삭제 -
- -
-
- 공인인증서를 삭제 하시겠습니까? -
-
- -
-
- - -
-
-
- )} - -
- )} -
- ); -} diff --git a/src/publish/pages/NavComponent.jsx b/src/publish/pages/NavComponent.jsx deleted file mode 100644 index bf44b8d7..00000000 --- a/src/publish/pages/NavComponent.jsx +++ /dev/null @@ -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( - - ) -} diff --git a/src/publish/pages/Panel1Component.jsx b/src/publish/pages/Panel1Component.jsx deleted file mode 100644 index 8912f09c..00000000 --- a/src/publish/pages/Panel1Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/publish/pages/Panel1DetailComponent.jsx b/src/publish/pages/Panel1DetailComponent.jsx deleted file mode 100644 index f1c74977..00000000 --- a/src/publish/pages/Panel1DetailComponent.jsx +++ /dev/null @@ -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 ( - <> - {/* */} - - ); -} diff --git a/src/publish/pages/Panel2Component.jsx b/src/publish/pages/Panel2Component.jsx deleted file mode 100644 index 4540fb5f..00000000 --- a/src/publish/pages/Panel2Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/publish/pages/Panel3Component.jsx b/src/publish/pages/Panel3Component.jsx deleted file mode 100644 index 359d8240..00000000 --- a/src/publish/pages/Panel3Component.jsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/publish/pages/Panel4Component.jsx b/src/publish/pages/Panel4Component.jsx deleted file mode 100644 index 66433394..00000000 --- a/src/publish/pages/Panel4Component.jsx +++ /dev/null @@ -1,521 +0,0 @@ -import { useState } from 'react'; -import { 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 ( - - ); -} diff --git a/src/publish/pages/Panel5Component.jsx b/src/publish/pages/Panel5Component.jsx deleted file mode 100644 index a469706d..00000000 --- a/src/publish/pages/Panel5Component.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; -export default function Panel5Component() { - - return ( -
- ); -} diff --git a/src/publish/pages/Panel6Component.jsx b/src/publish/pages/Panel6Component.jsx deleted file mode 100644 index 513549d1..00000000 --- a/src/publish/pages/Panel6Component.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useState } from "react"; -import { Link } from "react-router-dom"; -export default function Panel6Component({ isOpen, onToggle }) { - - return ( - - ); -} diff --git a/src/publish/pages/Panel7Component.jsx b/src/publish/pages/Panel7Component.jsx deleted file mode 100644 index 3389c12a..00000000 --- a/src/publish/pages/Panel7Component.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; -export default function Panel7Component() { - - return ( -
- ); -} diff --git a/src/publish/pages/Panel8Component.jsx b/src/publish/pages/Panel8Component.jsx deleted file mode 100644 index 868a706e..00000000 --- a/src/publish/pages/Panel8Component.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; -export default function Panel8Component() { - - return ( -
- ); -} diff --git a/src/publish/pages/ReplayComponent.jsx b/src/publish/pages/ReplayComponent.jsx deleted file mode 100644 index 7f734dcd..00000000 --- a/src/publish/pages/ReplayComponent.jsx +++ /dev/null @@ -1,100 +0,0 @@ - -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; - -export default function ReplayComponent() { - const navigate = useNavigate(); - - const max = 100; - const [value, setValue] = useState(30); - - const percent = (value / max) * 100; - - return( -
- -
- - -
- {/* 재생상태 컨트롤 */} -
-
- - -
- -
- setValue(Number(e.target.value))} - style={{ - background: `linear-gradient( - to right, - #FF0000 0%, - #FF0000 ${percent}%, - #D7DBEC ${percent}%, - #D7DBEC 100% - )` - }} - /> -
- 2023-08-20  10:15:30 -
-
- -
- - -
- -
- {/* 재생옵션 영역 */} -
- - -
- -
-
- -
- ) -} \ No newline at end of file diff --git a/src/publish/pages/Satellite1Component.jsx b/src/publish/pages/Satellite1Component.jsx deleted file mode 100644 index b2d4ee23..00000000 --- a/src/publish/pages/Satellite1Component.jsx +++ /dev/null @@ -1,189 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; -import FileUpload from '../components/FileUpload'; - -export default function Satellite1Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 위성 영상 등록 팝업 */} -
-
-
- 위성 영상 등록 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위성 영상 등록 - 사업자명/위성명, 영상 촬영일, 위성영상파일,CSV 파일,위성영상명, 영상전송 주기,영상 종류,위성 궤도,영상 출처,촬영 목적,촬영 모드,취득방법,구매가격, 에 대한 내용을 등록하는 표입니다.
사업자명/위성명 * -
- - -
-
영상 촬영일 *
위성영상파일 * -
- -
-
CSV 파일 * -
- -
-
위성영상명 *
영상전송 주기 - -
영상 종류 -
- - - - - -
-
위성 궤도 - - 영상 출처 - -
촬영 목적 - - 촬영 모드 - -
취득방법 - - 구매가격 -
- -
- - -
-
-
-
- -
-
- - -
-
-
-
- -
- ); -} diff --git a/src/publish/pages/Satellite2Component.jsx b/src/publish/pages/Satellite2Component.jsx deleted file mode 100644 index 5d604e62..00000000 --- a/src/publish/pages/Satellite2Component.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Satellite2Component() { - const navigate = useNavigate(); - return ( -
- - {/* 위성 사업자 등록 팝업 */} -
-
-
- 위성 사업자 등록 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위성 사업자 등록 - 사업자 분류, 사업자명, 국가, 소재지, 상세내역 에 대한 내용을 등록하는 표입니다.
사업자 분류 * - -
사업자명
국가 * - -
소재지
상세내역
-
- -
-
- - -
-
-
-
- -
- ); -} diff --git a/src/publish/pages/Satellite3Component.jsx b/src/publish/pages/Satellite3Component.jsx deleted file mode 100644 index f24eec73..00000000 --- a/src/publish/pages/Satellite3Component.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Satellite3Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 위성 관리 등록 팝업 */} -
-
-
- 위성 관리 등록 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위성 관리 등록 - 사업자명, 위성명, 센서 타입, 촬영 해상도, 주파수, 상세내역 에 대한 내용을 등록하는 표입니다.
사업자명 * - -
위성명 *
센서 타입 - -
촬영 해상도
주파수
상세내역
-
- -
-
- - -
-
-
-
- -
- ); -} diff --git a/src/publish/pages/Satellite4Component.jsx b/src/publish/pages/Satellite4Component.jsx deleted file mode 100644 index 05c705a7..00000000 --- a/src/publish/pages/Satellite4Component.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Satellite4Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 삭제 팝업 */} -
-
-
- 삭제 -
- -
-
삭제 하시겠습니까?
-
- -
-
- - -
-
-
-
- -
- ); -} diff --git a/src/publish/pages/ShipComponent.jsx b/src/publish/pages/ShipComponent.jsx deleted file mode 100644 index bc54d6fa..00000000 --- a/src/publish/pages/ShipComponent.jsx +++ /dev/null @@ -1,169 +0,0 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; - -export default function ShipComponent() { - const navigate = useNavigate(); - - //progress bar value 선언 - const [value, setValue] = useState(60); - - // 갤러리 이미지 - const images = [ - { src: "/images/photo_ship_001.png", alt: "1511함A-05" }, - { src: "/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( -
- - {/* 배정보 팝업 */} -
- {/* header */} -
-
- - 대한민국 - 1511함A-05 - 13450135 -
-
- -
- - - - {/* 이미지 영역 */} -
- {images[currentIndex].alt} -
-
- {/* body */} -
-
-
- -
    -
  • A
  • -
  • V
  • -
  • E
  • -
  • T
  • -
  • D
  • -
  • R
  • -
-
- -
- -
-
- {value}% - -
-
- -
    -
  • -
    - 출항지 - 서귀포해양경찰서 -
    -
    - 입항지 - 하태도 -
    -
  • -
  • -
    - 출항일시 - 2024-11-23 11:23:00 -
    -
    - 입항일시 - 2024-11-23 11:23:00 -
    -
  • -
  • -
    - 선박상태 - 정박 -
    -
    - 속도/항로 - 4.2 kn / 13.3˚ -
    -
    - 흘수 - 1.1m -
    -
  • -
- - {/*
    -
  • - AIS - 정상 -
  • -
  • - RF - 정상 -
  • -
  • - EO - 정상 -
  • -
  • - SAR - 비활성 -
  • -
*/} -
- - -
-
- {/* footer */} -
데이터 수신시간 : 2024-11-23 11:23:00
-
- -
- ) -} \ No newline at end of file diff --git a/src/publish/pages/Signal1Component.jsx b/src/publish/pages/Signal1Component.jsx deleted file mode 100644 index f64b90fa..00000000 --- a/src/publish/pages/Signal1Component.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Signal1Component() { - const navigate = useNavigate(); - - return ( -
- - {/* 신호설정 팝업 */} -
-
-
- 신호설정 -
- -
- - - - - - - - - - - - - - - - -
신호설정 - 신호표출반경, 수신수기 설정 에 대한 내용을 나타내는 표입니다.
신호표출반경 - -
수신수기 설정
-
- -
-
- - -
-
-
-
- -
- ); -} diff --git a/src/publish/pages/Signal2Component.jsx b/src/publish/pages/Signal2Component.jsx deleted file mode 100644 index 997ae2ac..00000000 --- a/src/publish/pages/Signal2Component.jsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from "react-router-dom"; - -export default function Signal2Component() { - const navigate = useNavigate(); - - // 아코디언 상태 (3개, 초기 모두 열림) - const [accordionOpen, setAccordionOpen] = useState({ - signal1: true, - signal2: true, - signal3: true, - }); - - const toggleAccordion = (key) => { - setAccordionOpen((prev) => ({ ...prev, [key]: !prev[key] })); - }; - - return ( -
- - {/* 신호설정 팝업 */} -
-
-
- 맞춤 설정 -
- -
- {/* 아코디언그룹 01 */} -
-
- NLL 고속 선박 탐지 - -
- {/* 여기서부터 아코디언 */} -
-
    -
  • - - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- {/* 여기까지 */} -
- - {/* 아코디언그룹 02 */} -
-
- 특정 어업수역 탐지 - -
- {/* 여기서부터 아코디언 */} -
-
    -
  • - - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- {/* 여기까지 */} -
- - {/* 아코디언그룹 03 */} -
-
- 위험화물 식별 - -
- {/* 여기서부터 아코디언 */} -
-
    -
  • - - -
  • -
-
- {/* 여기까지 */} -
-
- -
-
- - -
-
-
-
- -
- ); -} diff --git a/src/publish/pages/ToastComponent.jsx b/src/publish/pages/ToastComponent.jsx deleted file mode 100644 index 1748fe4b..00000000 --- a/src/publish/pages/ToastComponent.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Link } from "react-router-dom"; - -export default function ToastComponent() { - return( -
- - {/* 지도상 배표식 */} -
-
- - 1511함A-05 - 12.5 kts | 45° - -
- -
- - 1511함A-05 - 12.5 kts | 45° - -
- -
- - 1511함A-05 - 12.5 kts | 45° - -
-
- - {/* 토스트팝업 */} -
-
- 104 어업구역 비인가 선박 - - - - -
- -
- 104 어업구역 비인가 선박 - - - - -
- -
- 저속 이동 의심 선박 - - - - -
-
-
- ) -} \ No newline at end of file diff --git a/src/publish/pages/TopComponent.jsx b/src/publish/pages/TopComponent.jsx deleted file mode 100644 index f1ffa98a..00000000 --- a/src/publish/pages/TopComponent.jsx +++ /dev/null @@ -1,21 +0,0 @@ -export default function TopComponent() { - return( -
-
-
    -
  • -
  • 경도129° 38’31.071”E
  • -
  • 위도35° 21’24.580”N
  • -
  • KST2024-07-01(화) 12:00:00
  • -
  • -
  • -
-
- -
- - -
-
- ) -} \ No newline at end of file diff --git a/src/publish/pages/TrackComponent.jsx b/src/publish/pages/TrackComponent.jsx deleted file mode 100644 index 9216aa70..00000000 --- a/src/publish/pages/TrackComponent.jsx +++ /dev/null @@ -1,41 +0,0 @@ - -import { useNavigate } from "react-router-dom"; - -export default function TrackComponent() { - const navigate = useNavigate(); - - return( -
- -
- - - {/* 항적조회 검색바 */} -
- - - -
- -
- -
- ) -} \ No newline at end of file diff --git a/src/publish/pages/WeatherComponent.jsx b/src/publish/pages/WeatherComponent.jsx deleted file mode 100644 index eb664225..00000000 --- a/src/publish/pages/WeatherComponent.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useNavigate } from "react-router-dom"; - -export default function WeatherComponent() { - const navigate = useNavigate(); - - return( -
- -
- {/* header */} -
-
- 해양관측소 -
- -
- {/* body */} -
- -
    -
  • - 2023.10.16 20:54 -
  • -
  • - 조위 - 251(cm) -
  • -
  • - 수온 - 19.6(°C) -
  • -
  • - 염분 - 31.8(PSU) -
  • -
  • - 기온 - 16.9(°C) -
  • -
  • - 기압 - 1016.6(hPa) -
  • -
  • - 풍향 - 315(deg) -
  • -
  • - 풍속 - 7.1(m/s) -
  • -
  • - 유속방향 - -(deg) -
  • -
  • - 유속 - -(m/s) -
  • -
-
-
-
- ) -} \ No newline at end of file diff --git a/src/publish/scss/HeaderComponent.scss b/src/publish/scss/HeaderComponent.scss deleted file mode 100644 index d033824c..00000000 --- a/src/publish/scss/HeaderComponent.scss +++ /dev/null @@ -1,109 +0,0 @@ - -@charset "utf-8"; - -#wrap { - - /* header */ - #header { - width: 100%; - height: 4.4rem; - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - background: var(--secondary5); - - .logoArea { - display: flex; - align-items: center; - - .logo { - width: 4.3rem; - height: 4.4rem; - display: flex; - align-items: center; - justify-content: center; - background: url(../assets/images/logo.svg) no-repeat center / contain; - } - - .logoTxt { - color: var(--white); - font-weight: var(--fw-bold); - } - } - - aside { - ul { - display: flex; - - li { - position: relative; - width: 3rem; - height: 3rem; - margin-right: .9rem; - - &.setWrap:hover .setMenu { - display: block; - } - - a { - position: relative; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-repeat: no-repeat; - background-position: center; - background-size: 3rem; - - &.alram { - background-image: url(../assets/images/ico_alarm.svg); - } - - &.set { - background-image: url(../assets/images/ico_set.svg); - } - - &.user { - background-image: url(../assets/images/ico_user.svg); - } - } - } - } - } - .setMenu { - display: none; - position: absolute; - top: 2.8rem; - right: -1rem; - min-width: 22rem; - background-color:var(--secondary2) ; - padding: .6rem 0; - z-index: 100; - pointer-events: auto; - - a { - display: block; - padding: .8rem 1.5rem; - font-size: var(--fs-m); - font-weight: var(--fw-bold); - - &:hover { - background-color:var(--primary1); - } - } - } - - .badge { - position: absolute; - top: .6rem; - right: .8rem; - display: block; - width: .5rem; - height: .6rem; - border-radius: 50%; - background-color: var(--alert); - } - } -} \ No newline at end of file diff --git a/src/publish/scss/Layout.scss b/src/publish/scss/Layout.scss deleted file mode 100644 index 81b9c753..00000000 --- a/src/publish/scss/Layout.scss +++ /dev/null @@ -1,557 +0,0 @@ - -@charset "utf-8"; - -#wrap { - width: 100%; - height: 100%; - - /* header */ - #header { - width: 100%; - height: 4.4rem; - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - background: var(--secondary5); - - .logoArea { - display: flex; - align-items: center; - - .logo { - width: 4.3rem; - height: 4.4rem; - display: flex; - align-items: center; - justify-content: center; - background: url(../assets/images/logo.svg) no-repeat center / contain; - } - - .logoTxt { - color: var(--white); - font-weight: var(--fw-bold); - } - } - - aside { - ul { - display: flex; - - li { - width: 3rem; - height: 3rem; - margin-right: .9rem; - - a { - position: relative; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-repeat: no-repeat; - background-position: center; - background-size: 3rem; - - &.alram { - background-image: url(../assets/images/ico_alarm.svg); - } - - &.set { - background-image: url(../assets/images/ico_set.svg); - } - - &.user { - background-image: url(../assets/images/ico_user.svg); - } - } - } - } - } - - .badge { - position: absolute; - top: .6rem; - right: .8rem; - display: block; - width: .5rem; - height: .6rem; - border-radius: 50%; - background-color: var(--alert); - } - } - - #sidePanel { - /* gnb */ - #nav { - position: fixed; - top: 4.4rem; - left: 0; - z-index: 100; - width: 4.3rem; - height: calc(100% - 4.4rem); - display: flex; - flex-direction: column; - justify-content: space-between; - background: var(--secondary1); - border-right: 1px solid var(--tertiary2); - - .gnb { - width: 4.2rem; - display: flex; - flex-direction: column; - - li { - width: 100%; - height: 4.4rem; - display: flex; - align-items: center; - justify-content: center; - - a { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-repeat: no-repeat; - background-position: center; - background-size: 3rem; - - /* 공통 상태 */ - &:hover, - &:active, - &.active { - background-color: var(--primary1); - } - - /* gnb icons */ - &.gnb1 { - background-image: url(../assets/images/ico_gnb01.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb01_on.svg); - } - } - - &.gnb2 { - background-image: url(../assets/images/ico_gnb02.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb02_on.svg); - } - } - - &.gnb3 { - background-image: url(../assets/images/ico_gnb03.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb03_on.svg); - } - } - - &.gnb4 { - background-image: url(../assets/images/ico_gnb04.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb04_on.svg); - } - } - - &.gnb5 { - background-image: url(../assets/images/ico_gnb05.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb05_on.svg); - } - } - - &.gnb6 { - background-image: url(../assets/images/ico_gnb06.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb06_on.svg); - } - } - } - } - } - } - - - /* side-pannel */ - .slidePanel { - position: fixed; - top: 4.4rem; - left: 4.3rem; - width: 54rem; - height: calc(100% - 4.4rem); - display: flex; - flex-direction: column; - background-color: var(--secondary2); - z-index: 99; - transform: translateX(0); - transition: transform .3s ease; - - /* 닫힘 상태 */ - &.is-closed { - transform: translateX(-100%); - - .toogle::after { - transform: translate(-50%, -50%) rotate(180deg); - } - } - - .tabBox { - flex-shrink: 0; - width: 100%; - height: 4.8rem; - padding: 0 1.9rem; - margin-top: 1.5rem; - } - - .tabWrap { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - - &.is-active { - display: flex; - } - - .tabTop { - flex-shrink: 0; - width: 100%; - padding: 0 2rem; - border-bottom: 1px solid var(--secondary1); - - .title { - height: 6.4rem; - font-size: var(--fs-ml); - color: var(--white); - padding: 2rem 0; - } - } - - .tabBtm { - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: hidden; - padding: 3rem 1.9rem; - } - } - - .toogle { - position: absolute; - overflow: hidden; - z-index: 10; - top: 50%; - left: 100%; - width: 2.4rem; - height: 5rem; - border-radius: 0 1rem 1rem 0; - transform: translateY(-50%); - background-color: var(--secondary2); - border: solid 1px var(--secondary3); - - &::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: 2.4rem; - height: 2.4rem; - transform: translate(-50%, -50%); - background: url(../assets/images/ico_toggle.svg) no-repeat center / 2.4rem; - transition: transform .3s ease; - } - } - } - - } - /* main */ - #main { - width: 100%; - height: calc(100% - 4.4rem); - position: relative; - - /* top-bar */ - .topBar { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - padding: 1.5rem 5.4rem; - } - - /* top-location */ - .locationInfo { - grid-column: 2; - width: 71rem; - max-width: 100%; - height: 3.8rem; - background-color: rgba(var(--secondary6-rgb), .9); - - ul { - display: flex; - align-items: center; - width: 100%; - height: 100%; - - li { - display: flex; - align-items: center; - height: 100%; - padding: 0 1rem; - color: var(--white); - font-weight: var(--fw-bold); - font-size: 1.3rem; - - &:first-child, - &:last-child { - padding: 0; - } - - span + span { - padding-right: .5rem; - } - - &.divider { - position: relative; - - &::after { - content: ""; - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - width: 1px; - height: 1.8rem; - background-color: rgba(255, 255, 255, .3); - } - } - - .wgs { - display: flex; - align-items: center; - padding-right: .5rem; - - &::before { - content: ""; - display: block; - width: 2rem; - height: 2rem; - margin-right: .5rem; - background: url(../assets/images/ico_globe.svg) no-repeat center / 2rem; - } - } - - .kst { - display: flex; - align-items: center; - justify-content: center; - width: 3.1rem; - height: 1.8rem; - margin-right: .5rem; - border-radius: .3rem; - background-color: var(--tertiary3); - color: var(--white); - font-size: var(--fs-s); - } - } - } - - button { - width: 4rem; - height: 3.8rem; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - box-sizing: border-box; - background-color: var(--secondary3); - background-repeat: no-repeat; - background-position: center; - background-size: 3rem; - - &:hover, - &.active { - background-color: rgba(var(--primary1-rgb), .8); - } - - &.map { - background-image: url(../assets/images/ico_map.svg); - } - - &.ship { - background-image: url(../assets/images/ico_ship.svg); - } - - &.set { - width: 2rem; - height: 2rem; - background-color: transparent; - background-image: url(../assets/images/ico_set_s.svg); - background-size: 2rem; - - &:hover, - &.active { - background-color: transparent; - } - } - } - } - - /* top-search */ - .schBox { - position: relative; - grid-column: 3; - justify-self: end; - width: 20rem; - height: 3.8rem; - - .sch { - width: 100%; - height: 100%; - padding-right: 4.4rem; - border-radius: 2rem; - background-color: rgba(var(--secondary6-rgb), .9); - - &::placeholder { - color: var(--white); - } - } - - .mainSchBtn { - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); - width: 4rem; - height: 3.8rem; - font-size: 0; - text-indent: -999999em; - border: none; - cursor: pointer; - background: url(../assets/images/ico_search_main.svg) no-repeat center left / 2.4rem; - } - } - } - - /* tool-bar */ - #tool { - .toolBar { - position: absolute; - top: 5.9rem; - right: .9rem; - width: 3rem; - height: 41rem; - } - - .control { - position: absolute; - bottom: 1.8rem; - right: .9rem; - width: 3rem; - height: 20rem; - } - - .toolItem { - width: 100%; - display: flex; - flex-direction: column; - - &.space { - li { - margin-bottom: .5rem; - } - } - - &.zoom { - li { - height: 3rem; - - &.num { - background-color: var(--white); - color: var(--gray-scale2); - border-left: 1px solid rgba(var(--secondary6-rgb), .8); - border-right: 1px solid rgba(var(--secondary6-rgb), .8); - font-size: var(--fs-m); - } - - button { - padding-bottom: 0; - } - } - } - - li { - display: flex; - align-items: center; - justify-content: center; - width: 3rem; - height: 4.2rem; - background-color: rgba(var(--secondary6-rgb), .8); - - &:hover, - &.active { - background-color: rgba(var(--primary1-rgb), .8); - } - - button { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - padding-bottom: .2rem; - cursor: pointer; - text-align: center; - font-size: var(--fs-xxs); - color: var(--white); - background-repeat: no-repeat; - background-position: top left; - background-size: 3rem; - - &::before { - content: ""; - flex-shrink: 0; - width: 3rem; - height: 3rem; - } - - &.tool01 { background-image: url(../assets/images/ico_tool01.svg); } - &.tool02 { background-image: url(../assets/images/ico_tool02.svg); } - &.tool03 { background-image: url(../assets/images/ico_tool03.svg); } - &.tool04 { background-image: url(../assets/images/ico_tool04.svg); } - &.tool05 { background-image: url(../assets/images/ico_tool05.svg); } - &.tool06 { background-image: url(../assets/images/ico_tool06.svg); } - &.tool07 { background-image: url(../assets/images/ico_tool07.svg); } - &.tool08 { background-image: url(../assets/images/ico_tool08.svg); } - - &.zoomin { background-image: url(../assets/images/ico_plus.svg); } - &.zoomout { background-image: url(../assets/images/ico_minus.svg); } - &.legend { background-image: url(../assets/images/ico_legend.svg); } - &.minimap { background-image: url(../assets/images/ico_minimap.svg); } - } - } - } - } - -} \ No newline at end of file diff --git a/src/publish/scss/MainComponent.scss b/src/publish/scss/MainComponent.scss deleted file mode 100644 index f9a1e878..00000000 --- a/src/publish/scss/MainComponent.scss +++ /dev/null @@ -1,188 +0,0 @@ - -@charset "utf-8"; - -#wrap { - //* main */ - #main { - width: 100%; - min-height: calc(100vh - 4.4rem); - position: relative; - overscroll-behavior: none; - overflow: hidden; - - //* top-bar */ - .topBar { - position: absolute; - top: 1.5rem; - left: 0; - right: 0; - height: 4.4rem; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 5.4rem; - box-sizing: border-box; - z-index: 95; - } - - //* top-location */ - .locationInfo { - flex: 0 0 auto; - min-width: 71rem; - height: 3.8rem; - margin: 0 auto; - display: flex; - align-items: center; - background-color: rgba(var(--secondary6-rgb), .9); - overflow: hidden; - - ul { - display: flex; - align-items: center; - width: 100%; - height: 100%; - - li { - display: flex; - align-items: center; - height: 100%; - padding: 0 1rem; - color: var(--white); - font-weight: var(--fw-bold); - font-size: 1.3rem; - - &:first-child, - &:last-child { - padding: 0; - } - - span + span { - padding-right: .5rem; - } - - &.divider { - position: relative; - - &::after { - content: ""; - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - width: 1px; - height: 1.8rem; - background-color: rgba(255, 255, 255, .3); - } - } - - .wgs { - display: flex; - align-items: center; - padding-right: .5rem; - - &::before { - content: ""; - display: block; - width: 2rem; - height: 2rem; - margin-right: .5rem; - background: url(../assets/images/ico_globe.svg) no-repeat center / 2rem; - } - } - - .kst { - display: flex; - align-items: center; - justify-content: center; - width: 3.1rem; - height: 1.8rem; - margin-right: .5rem; - border-radius: .3rem; - background-color: var(--tertiary3); - color: var(--white); - font-size: var(--fs-s); - } - } - } - - button { - width: 4rem; - height: 3.8rem; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - box-sizing: border-box; - background-color: var(--secondary3); - background-repeat: no-repeat; - background-position: center; - background-size: 3rem; - - &:hover, - &.active { - background-color: rgba(var(--primary1-rgb), .8); - } - - &.map { - background-image: url(../assets/images/ico_map.svg); - } - - &.ship { - background-image: url(../assets/images/ico_ship.svg); - } - - &.set { - width: 2rem; - height: 2rem; - background-color: transparent; - background-image: url(../assets/images/ico_set_s.svg); - background-size: 2rem; - - &:hover, - &.active { - background-color: transparent; - } - } - } - } - - //* top-search */ - .topSchBox { - flex: 0 0 auto; - width: 20rem; - height: 3.8rem; - display: flex; - justify-content: flex-end; - position: relative; - - .tschInput { - width: 100%; - height: 100%; - padding-right: 4.4rem; - border-radius: 2rem; - background-color: rgba(var(--secondary6-rgb), .9); - border: 0; - - &::placeholder { - color: var(--white); - } - } - - .mainSchBtn { - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); - width: 4rem; - height: 3.8rem; - font-size: 0; - text-indent: -999999em; - border: none; - cursor: pointer; - background: url(../assets/images/ico_search_main.svg) no-repeat center left / 2.4rem; - } - } - - } -} \ No newline at end of file diff --git a/src/publish/scss/SideComponent.scss b/src/publish/scss/SideComponent.scss deleted file mode 100644 index 7dc48fe2..00000000 --- a/src/publish/scss/SideComponent.scss +++ /dev/null @@ -1,539 +0,0 @@ - -@charset "utf-8"; - -#wrap { - - #sidePanel { - /* gnb */ - #nav { - position: fixed; - top: 4.4rem; - left: 0; - z-index: 100; - width: 4.3rem; - height: calc(100% - 4.4rem); - display: flex; - flex-direction: column; - justify-content: space-between; - background: var(--secondary1); - border-right: 1px solid var(--tertiary2); - - .gnb { - width: 4.2rem; - display: flex; - flex-direction: column; - - li { - width: 100%; - height: 4.4rem; - display: flex; - align-items: center; - justify-content: center; - - button { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-repeat: no-repeat; - background-position: center; - background-size: 3rem; - - /* 공통 상태 */ - &:hover, - &:active, - &.active { - background-color: var(--primary1); - } - - /* gnb icons */ - &.gnb1 { - background-image: url(../assets/images/ico_gnb01.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb01_on.svg); - } - } - - &.gnb2 { - background-image: url(../assets/images/ico_gnb02.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb02_on.svg); - } - } - - &.gnb3 { - background-image: url(../assets/images/ico_gnb03.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb03_on.svg); - } - } - - &.gnb4 { - background-image: url(../assets/images/ico_gnb04.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb04_on.svg); - } - } - - &.gnb5 { - background-image: url(../assets/images/ico_gnb05.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb05_on.svg); - } - } - - &.gnb6 { - background-image: url(../assets/images/ico_gnb06.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb06_on.svg); - } - } - - &.gnb7 { - background-image: url(../assets/images/ico_gnb07.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb07_on.svg); - } - } - - &.gnb8 { - background-image: url(../assets/images/ico_gnb08.svg); - - &:hover, - &:active, - &.active { - background-image: url(../assets/images/ico_gnb08_on.svg); - } - } - } - } - } - .side { - width: 4.2rem; - display: flex; - flex-direction: column; - - li { - width: 100%; - height: 4.4rem; - display: flex; - align-items: center; - justify-content: center; - - button { - width: 3rem; - height: 3rem; - display: flex; - align-items: center; - justify-content: center; - background-repeat: no-repeat; - background-position: center; - background-size: 3rem; - background-color: var(--primary3); - - &.filter { - background-image: url('../assets/images/ico_side01.svg'); - } - - &.layer { - background-image: url('../assets/images/ico_side02.svg'); - } - } - } - } - - } - - - /* side-pannel */ - .slidePanel { - position: fixed; - top: 4.4rem; - left: 4.3rem; - width: 54rem; - height: calc(100% - 4.4rem); - min-height: 0; - display: flex; - flex-direction: column; - background-color: var(--secondary2); - border-right: .1rem solid var(--secondary3); - z-index: 99; - transform: translateX(0); - transition: transform .3s ease; - - /* 닫힘 상태 */ - &.is-closed { - transform: translateX(-100%); - - .toogle::after { - transform: translate(-50%, -50%) rotate(180deg); - } - } - - .tabBox { - flex-shrink: 0; - width: 100%; - padding: 1.5rem 2rem 0 2rem; - } - - .tabWrap { - flex: 1; - min-height: 0; - display: none; - flex-direction: column; - - &.is-active { - display: flex; - } - - .tabWrapInner { - display: flex; - flex-direction: column; - flex: 1; - min-height: 100%; - - .tabWrapCnt { - flex: 1 1 auto; - min-height: 0; - overflow-y: auto; - padding-bottom: 3rem; - - // 필터 스위치그룹 - .switchGroup { - display: flex; - flex-direction: column; - width: 100%; - - .sgHeader { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1.2rem 1rem; - background-color: var(--secondary1); - border-bottom: .1rem solid var(--secondary3); - - .colL { - display: flex; - align-items: center; - gap: 1rem; - font-weight: var(--fw-bold); - - .favship { - width: 2rem; - height: 2rem; - background: url(../assets/images/ico_favship.svg) no-repeat center / contain; - } - } - - .toggleBtn { - display: flex; - align-items: center; - width: 2.4rem; - height: 2.4rem; - background: url(../assets/images/ico_arrow_toggle_down.svg) no-repeat center / contain; - transition: transform 0.3s ease; - - &.is-open { - transform: rotate(180deg); - } - } - } - - .switchBox { - overflow: hidden; - max-height: 0; - transition: max-height 0.3s ease; - - &.is-open { - max-height: 500rem; - } - - .switchList { - display: flex; - flex-direction: column; - flex: 1 1 auto; - width: 100%; - background-color: var(--tertiary1); - padding: 1rem; - - li { - display: flex; - justify-content: space-between; - padding: .1rem 0; - - span { - flex: 1; - min-width: 0; - word-break: break-word; - overflow-wrap: break-word; - white-space: normal; - } - } - } - } - } - } - - .btnBox { - flex: 0 0 auto; - margin-top: auto; - display: flex; - justify-content: flex-end; - padding: 2rem; - } - } - - .tabTop { - flex-shrink: 0; - width: 100%; - padding: 0 2rem; - - .title { - font-size: var(--fs-ml); - color: var(--white); - font-weight: var(--fw-bold); - padding: 1.7rem 0; - - .prevBtn { - width: 2rem; - height: 2rem; - background: url(../assets/images/ico_tit_prev.svg) no-repeat center /contain; - margin-right: .5rem; - } - } - // 기상패널 범례 - .legend { - display: flex; - align-items: flex-start; - flex-direction: column; - gap: .4rem; - margin-bottom: 2.5rem; - - .legendTitle { - font-size: var(--fs-m); - } - - .legendList { - width: 100%; - border-radius: .6rem; - padding: 1.5rem; - background-color: rgba(var(--secondary4-rgb), .2); - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1rem; - - li { - display: flex; - align-items: center; - justify-content: flex-start; - font-size: var(--fs-ml); - - img { - width: 2.6rem; - height: 2.6rem; - margin-right: .5rem; - } - } - } - } - } - - .tabBtm { - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: hidden; - border-top: 1px solid var(--secondary1); - padding: 3rem 2rem; - - &.noLine { - overflow-y: visible; - border-top: 0; - padding: 0 2rem; - } - - &.noSc { - overflow-y: visible; - padding-bottom: 0; - } - .tabBtmInner { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - - .tabBtmCnt { - flex: 1 1 auto; - min-height: 0 ; - overflow-y: auto; - padding-bottom: 2rem; - } - - .btnBox { - flex: 0 0 auto; - margin-top: auto; - display: flex; - justify-content: flex-end; - gap: 1rem; - padding: 2rem 0; - - &.rowSB { - justify-content: space-between; - } - - button { - flex: initial; - min-width: 12rem; - } - } - } - } - - } - - .toogle { - position: absolute; - overflow: hidden; - z-index: 10; - top: 50%; - left: 100%; - width: 2.4rem; - height: 5rem; - border-radius: 0 1rem 1rem 0; - transform: translateY(-50%); - background-color: var(--secondary2); - border: solid 1px var(--secondary3); - - &::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: 2.4rem; - height: 2.4rem; - transform: translate(-50%, -50%); - background: url(../assets/images/ico_toggle.svg) no-repeat center / 2.4rem; - transition: transform .3s ease; - } - } - // ai모드 - .panelHeader { - display: flex; - align-items: center; - padding: 0 2rem; - - .panelTitle { - padding: 1.7rem 0; - font-size: var(--fs-ml); - font-weight: var(--fw-bold); - } - } - - .panelBody { - display: flex; - flex: 1; - min-height: 0; - padding: 0 2rem; - - .ai { - display: grid; - grid-template-columns: repeat(2, 1fr); - align-items: start; - gap: 1rem; - row-gap: 1rem; - grid-auto-rows: min-content; - width: 100%; - - li { - display: flex; - align-items: center; - justify-content: flex-start; - - a { - position: relative; - display: flex; - flex-direction: column; - gap: .6rem; - width: 100%; - height: auto; - padding: 2rem 1.2rem; - background-color: var(--secondary1); - border: 2px solid transparent; - border-radius: .6rem; - - &.on { - background-color: var(--secondary5); - border-color: var(--primary1); - } - - &:not(.on) > * { - opacity: .5; - } - - .title { - display: flex; - align-items: center; - font-weight: var(--fw-bold); - - img { - width: 2.2rem; - height: 2.2rem; - margin-right: .5rem; - } - } - - .keyword { - font-weight: var(--fw-bold); - } - - .control { - position: absolute; - top: 2rem; - right: 1.5rem; - display: flex; - align-items: center; - - i { - width: .8rem; - height: .8rem; - border-radius: 50%; - background-color: var(--white); - margin-right: .5rem; - } - } - } - } - } - } - - .panelFooter { - border-top: 1px solid var(--secondary1); - padding: 1rem 2rem; - } - - } - - } -} \ No newline at end of file diff --git a/src/publish/scss/ToolComponent.scss b/src/publish/scss/ToolComponent.scss deleted file mode 100644 index 626898a9..00000000 --- a/src/publish/scss/ToolComponent.scss +++ /dev/null @@ -1,153 +0,0 @@ - -@charset "utf-8"; - -#wrap { - //* tool-bar */ - #tool { - .toolBar { - position: absolute; - top: 5.9rem; - right: .9rem; - width: 3rem; - height: 41rem; - z-index: 95; - } - - .control { - position: absolute; - bottom: 1.8rem; - right: .9rem; - width: 3rem; - height: 20rem; - } - - .toolItem { - width: 100%; - display: flex; - flex-direction: column; - - &.space { - li { - margin-bottom: .5rem; - } - } - - &.zoom { - li { - height: 3rem; - - &.num { - background-color: var(--white); - color: var(--gray-scale1); - border-left: 1px solid rgba(var(--secondary6-rgb), .8); - border-right: 1px solid rgba(var(--secondary6-rgb), .8); - font-size: var(--fs-m); - } - - button { - padding-bottom: 0; - } - } - } - - li { - display: flex; - align-items: center; - justify-content: center; - width: 3rem; - height: 4.2rem; - background-color: rgba(var(--secondary6-rgb), .8); - - button { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - padding-bottom: .2rem; - cursor: pointer; - text-align: center; - font-size: var(--fs-xxs); - color: var(--white); - background-repeat: no-repeat; - background-position: top left; - background-size: 3rem; - - &:hover, - &.active { - background-color: rgba(var(--primary1-rgb), .8); - } - - &::before { - content: ""; - flex-shrink: 0; - width: 3rem; - height: 3rem; - } - - &.tool01 { background-image: url(../assets/images/ico_tool01.svg); } - &.tool02 { background-image: url(../assets/images/ico_tool02.svg); } - &.tool03 { background-image: url(../assets/images/ico_tool03.svg); } - &.tool04 { background-image: url(../assets/images/ico_tool04.svg); } - &.tool05 { background-image: url(../assets/images/ico_tool05.svg); } - &.tool06 { background-image: url(../assets/images/ico_tool06.svg); } - &.tool07 { background-image: url(../assets/images/ico_tool07.svg); } - &.tool08 { background-image: url(../assets/images/ico_tool08.svg); } - - &.zoomin { background-image: url(../assets/images/ico_plus.svg); } - &.zoomout { background-image: url(../assets/images/ico_minus.svg); } - &.legend { background-image: url(../assets/images/ico_legend.svg); } - &.minimap { background-image: url(../assets/images/ico_minimap.svg); } - } - } - } - // 범례 - .legendWrap { - position: fixed; - right: 5rem; - bottom: 5rem; - - .legendList { - display: flex; - flex-direction: column; - width: 14rem; - height: auto; - border-radius: .5rem; - background-color: var(--gray-scale3); - padding: .7rem; - gap: .4rem; - - li { - display: flex; - justify-content: space-between; - align-items: center; - - &.legendItem { - padding: .5rem 0; - border-bottom: 1px solid var(--gray-scale7); - } - - .legendLabel { - display: flex; - align-items: center; - gap: .2rem; - font-size: var(--fs-s); - font-weight: var(--fw-bold); - - img { - width: 1.6rem; - height: 1.6rem; - } - } - - .legendValue { - font-size: var(--fs-s); - font-weight: var(--fw-heavy); - } - } - } - } - - } -} \ No newline at end of file diff --git a/src/publish/scss/WrapComponent.scss b/src/publish/scss/WrapComponent.scss deleted file mode 100644 index fa86d43f..00000000 --- a/src/publish/scss/WrapComponent.scss +++ /dev/null @@ -1,7 +0,0 @@ - -@charset "utf-8"; - -#wrap { - width: 100%; - min-height: 100vh; -} \ No newline at end of file diff --git a/src/satellite/components/SatelliteImageManage.jsx b/src/satellite/components/SatelliteImageManage.jsx deleted file mode 100644 index 4dc146db..00000000 --- a/src/satellite/components/SatelliteImageManage.jsx +++ /dev/null @@ -1,387 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { fetchCommonCodeList } from '@/api/commonApi'; -import { fetchSatelliteVideoList, fetchSatelliteCsvFeatures } from '@/api/satelliteApi'; -import { useSatelliteStore } from '@/stores/satelliteStore'; -import { useMapStore } from '@/stores/mapStore'; -import { satelliteLayer, showSatelliteImage, showCsvFeatures, hideSatelliteImage } from '@/map/layers/satelliteLayer'; -import Slider from './Slider'; -import SatelliteRegisterPopup from './SatelliteRegisterPopup'; - -const LIMIT = 10; - -export default function SatelliteImageManage() { - const [isAccordionOpen, setIsAccordionOpen] = useState(false); - - // 공통코드 옵션 - const [videoKindOptions, setVideoKindOptions] = useState([]); - const [videoOriginOptions, setVideoOriginOptions] = useState([]); - const [videoOrbitOptions, setVideoOrbitOptions] = useState([]); - const [videoCycleOptions, setVideoCycleOptions] = useState([]); - - // 폼 필터 state - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - const [satelliteVideoName, setSatelliteVideoName] = useState(''); - const [satelliteVideoKind, setSatelliteVideoKind] = useState(''); - const [satelliteVideoOrigin, setSatelliteVideoOrigin] = useState(''); - const [satelliteVideoOrbit, setSatelliteVideoOrbit] = useState(''); - const [satelliteVideoTransmissionCycle, setSatelliteVideoTransmissionCycle] = useState(''); - - // 결과 state - const [list, setList] = useState([]); - const [page, setPage] = useState(1); - const [totalPage, setTotalPage] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - // 지도 표출 state - const [activeImageId, setActiveImageId] = useState(null); - - // 상세 팝업 state - const [detailPopupId, setDetailPopupId] = useState(null); - - // 등록 팝업 state - const [isRegisterOpen, setIsRegisterOpen] = useState(false); - - // 위성 투명도/밝기 스토어 - const opacity = useSatelliteStore((s) => s.opacity); - const brightness = useSatelliteStore((s) => s.brightness); - const setOpacity = useSatelliteStore((s) => s.setOpacity); - const setBrightness = useSatelliteStore((s) => s.setBrightness); - - // 마운트 시 공통코드 로드 - useEffect(() => { - fetchCommonCodeList('000109').then(setVideoKindOptions).catch(() => setVideoKindOptions([])); - fetchCommonCodeList('000111').then(setVideoOriginOptions).catch(() => setVideoOriginOptions([])); - fetchCommonCodeList('000110').then(setVideoOrbitOptions).catch(() => setVideoOrbitOptions([])); - fetchCommonCodeList('000108').then(setVideoCycleOptions).catch(() => setVideoCycleOptions([])); - }, []); - - const toggleAccordion = () => setIsAccordionOpen((prev) => !prev); - - const search = useCallback(async (targetPage) => { - setIsLoading(true); - setError(null); - - try { - const result = await fetchSatelliteVideoList({ - page: targetPage, - startDate, - endDate, - satelliteVideoName, - satelliteVideoTransmissionCycle, - satelliteVideoKind, - satelliteVideoOrbit, - satelliteVideoOrigin, - }); - setList(result.list); - setTotalPage(result.totalPage); - setPage(targetPage); - } catch (err) { - setError('위성영상 조회 중 오류가 발생했습니다.'); - setList([]); - setTotalPage(0); - } finally { - setIsLoading(false); - } - }, [startDate, endDate, satelliteVideoName, satelliteVideoTransmissionCycle, satelliteVideoKind, satelliteVideoOrbit, satelliteVideoOrigin]); - - const handleSearch = () => { - search(1); - }; - - const handlePageChange = (newPage) => { - search(newPage); - }; - - // 투명도 변경 (Slider → 스토어 + 레이어 실시간 반영) - const handleOpacityChange = (v) => { - const val = v / 100; - setOpacity(val); - satelliteLayer.setOpacity(val); - }; - - // 밝기 변경 (Slider → 스토어 + CSS filter 실시간 반영) - const handleBrightnessChange = (v) => { - setBrightness(v); - const el = document.querySelector('.satellite-map'); - if (el) { - el.style.filter = `brightness(${v}%)`; - } - }; - - // btnMap: 위성영상 지도 표출 토글 - const handleShowOnMap = async (item) => { - const map = useMapStore.getState().map; - if (!map) return; - - // 같은 영상이면 제거 (토글) - if (activeImageId === item.satelliteManageId) { - hideSatelliteImage(); - setActiveImageId(null); - return; - } - - const { opacity: curOpacity, brightness: curBrightness } = useSatelliteStore.getState(); - const extent = [item.tifMinX, item.tifMinY, item.tifMaxX, item.tifMaxY]; - showSatelliteImage(map, item.tifGeoName, extent, curOpacity, curBrightness); - - // CSV 선박 점 표시 - if (item.csvFileName) { - try { - const features = await fetchSatelliteCsvFeatures(item.csvFileName); - showCsvFeatures(features); - } catch { - // CSV 없으면 무시 - } - } - - setActiveImageId(item.satelliteManageId); - }; - - return ( -
-
-
위성영상 관리
- -
-
    -
  • - -
  • - - {/* 아코디언 — 상세검색 */} -
    -
  • - - -
  • -
  • - - -
  • -
    - -
  • - -
  • -
  • - -
  • -
  • - <> -
    - 투명도 -
    - -
    -
    -
    - 밝기 -
    - -
    -
    - - -
  • -
-
-
- -
-
- {/* 스크롤영역 */} -
- {isLoading &&
조회 중...
} - - {error &&
{error}
} - - {!isLoading && !error && list.length === 0 && ( -
검색 결과가 없습니다.
- )} - - {!isLoading && list.length > 0 && ( -
- {list.map((item) => ( -
    -
  • -
    - {item.satelliteVideoName} - {item.photographDate} -
    -
  • -
  • -
      -
    • - 위성명 - {item.satelliteName} -
    • -
    • - 위성영상파일 - {item.tifFileName} -
    • -
    • - 영상 종류 - {item.satelliteVideoKind} -
    • -
    • - 영상 출처 - {item.satelliteVideoOrigin} -
    • -
    -
    - - - -
    -
  • -
- ))} -
- )} - - {!isLoading && totalPage > 1 && ( -
- - {page > 3 && } - {page > 4 && ...} - {Array.from({ length: 5 }, (_, i) => page - 2 + i) - .filter((p) => p >= 1 && p <= totalPage) - .map((p) => ( - - ))} - {page < totalPage - 3 && ...} - {page < totalPage - 2 && } - -
- )} -
- {/* 하단버튼 영역 */} -
- {/**/} - -
-
-
- - {detailPopupId && ( - setDetailPopupId(null)} - onSaved={() => search(page)} - /> - )} - - {isRegisterOpen && ( - setIsRegisterOpen(false)} - onSaved={() => search(page)} - /> - )} -
- ); -} diff --git a/src/satellite/components/SatelliteManage.jsx b/src/satellite/components/SatelliteManage.jsx deleted file mode 100644 index c86b2989..00000000 --- a/src/satellite/components/SatelliteManage.jsx +++ /dev/null @@ -1,204 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { fetchCommonCodeList } from '@/api/commonApi'; -import { fetchSatelliteCompanyList, searchSatelliteManage } from '@/api/satelliteApi'; -import SatelliteManageRegisterPopup from './SatelliteManageRegisterPopup'; - -export default function SatelliteManage() { - // 드롭다운 옵션 - const [companyOptions, setCompanyOptions] = useState([]); - const [sensorTypeOptions, setSensorTypeOptions] = useState([]); - - // 검색 폼 - const [companyNo, setCompanyNo] = useState(''); - const [satelliteName, setSatelliteName] = useState(''); - const [sensorType, setSensorType] = useState(''); - - // 결과 - const [list, setList] = useState([]); - const [page, setPage] = useState(1); - const [totalPage, setTotalPage] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - // 등록 팝업 state - const [isRegisterOpen, setIsRegisterOpen] = useState(false); - - // 상세/수정 팝업 state - const [detailManageId, setDetailManageId] = useState(null); - - // 마운트 시 사업자명 목록 + 센서타입 공통코드 병렬 로드 - useEffect(() => { - fetchSatelliteCompanyList() - .then(setCompanyOptions) - .catch(() => setCompanyOptions([])); - - fetchCommonCodeList('000092') - .then(setSensorTypeOptions) - .catch(() => setSensorTypeOptions([])); - }, []); - - const search = useCallback(async (targetPage) => { - setIsLoading(true); - setError(null); - - try { - const limit = 10; - const page = targetPage != null ? targetPage : 1; - const result = await searchSatelliteManage({ - companyNo: companyNo ? Number(companyNo) : undefined, - satelliteName, - sensorType, - page, - limit, - }); - setList(result.list); - setTotalPage(result.totalPage); - setPage(targetPage); - } catch { - setError('위성 관리 조회 중 오류가 발생했습니다.'); - setList([]); - setTotalPage(0); - } finally { - setIsLoading(false); - } - }, [companyNo, satelliteName, sensorType]); - - const handleSearch = () => { - search(1); - }; - - const handlePageChange = (newPage) => { - search(newPage); - }; - - return ( -
-
-
위성 관리
-
-
    -
  • - - -
  • -
  • - -
  • -
  • - -
  • -
-
-
- -
-
- {/* 스크롤영역 */} -
- {isLoading &&
조회 중...
} - - {error &&
{error}
} - - {!isLoading && !error && list.length === 0 && ( -
검색 결과가 없습니다.
- )} - - {!isLoading && list.length > 0 && ( -
- {list.map((item) => ( -
    setDetailManageId(item.satelliteManageId)}> -
  • - 사업자명 - {item.companyName} -
  • -
  • - 위성명 - {item.satelliteName} -
  • -
  • - 센서 타입 - {item.sensorType} -
  • -
  • - 촬영 해상도 - {item.photoResolution} -
  • -
- ))} -
- )} - - {!isLoading && totalPage > 1 && ( -
- - {page > 3 && } - {page > 4 && ...} - {Array.from({ length: 5 }, (_, i) => page - 2 + i) - .filter((p) => p >= 1 && p <= totalPage) - .map((p) => ( - - ))} - {page < totalPage - 3 && ...} - {page < totalPage - 2 && } - -
- )} -
- {/* 하단버튼 영역 */} -
- -
-
-
- - {isRegisterOpen && ( - setIsRegisterOpen(false)} - onSaved={() => search(page)} - /> - )} - - {detailManageId && ( - setDetailManageId(null)} - onSaved={() => search(page)} - /> - )} -
- ); -} diff --git a/src/satellite/components/SatelliteManageRegisterPopup.jsx b/src/satellite/components/SatelliteManageRegisterPopup.jsx deleted file mode 100644 index 8f787f1c..00000000 --- a/src/satellite/components/SatelliteManageRegisterPopup.jsx +++ /dev/null @@ -1,254 +0,0 @@ -import { useState, useEffect } from 'react'; -import {createPortal} from "react-dom"; -import { showToast } from '@/components/common/Toast'; -import { fetchCommonCodeList } from '@/api/commonApi'; -import { - fetchSatelliteCompanyList, - saveSatelliteManage, - fetchSatelliteManageDetail, - updateSatelliteManage, -} from '@/api/satelliteApi'; -import useDraggable from '../hooks/useDraggable'; - -/** - * 위성 관리 등록/수정 팝업 - * @param {{ satelliteManageId?: number, onClose: () => void, onSaved: () => void }} props - * satelliteManageId가 있으면 수정 모드, 없으면 등록 모드 - */ -export default function SatelliteManageRegisterPopup({ satelliteManageId, onClose, onSaved }) { - const isEditMode = !!satelliteManageId; - - const [sensorTypeOptions, setSensorTypeOptions] = useState([]); - const [companyOptions, setCompanyOptions] = useState([]); - - const [companyNo, setCompanyNo] = useState(''); - const [satelliteName, setSatelliteName] = useState(''); - const [sensorType, setSensorType] = useState(''); - const [photoResolution, setPhotoResolution] = useState(''); - const [frequency, setFrequency] = useState(''); - const [photoDetail, setPhotoDetail] = useState(''); - - const [isLoading, setIsLoading] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - const { position, handleMouseDown } = useDraggable(); - - // 마운트 시 센서타입 공통코드 로드 + 수정 모드면 상세조회 - useEffect(() => { - let cancelled = false; - - async function load() { - setIsLoading(true); - setError(null); - - try { - const [codeList, companyList] = await Promise.all([ - fetchCommonCodeList('000092'), - fetchSatelliteCompanyList(), - ]); - if (cancelled) return; - setSensorTypeOptions(codeList); - setCompanyOptions(companyList); - - if (satelliteManageId) { - const data = await fetchSatelliteManageDetail(satelliteManageId); - if (cancelled || !data) return; - - setCompanyNo(data.companyNo ?? ''); - setSatelliteName(data.satelliteName || ''); - setSensorType(data.sensorType || ''); - setPhotoResolution(data.photoResolution || ''); - setFrequency(data.frequency || ''); - setPhotoDetail(data.photoDetail || ''); - } - } catch { - setError('데이터 조회 중 오류가 발생했습니다.'); - } finally { - if (!cancelled) setIsLoading(false); - } - } - - load(); - return () => { cancelled = true; }; - }, [satelliteManageId]); - - const handleSave = async () => { - if (!companyNo) { - showToast('사업자명을 선택해주세요.'); - return; - } - if (!satelliteName.trim()) { - showToast('위성명을 입력해주세요.'); - return; - } - - setIsSaving(true); - setError(null); - - try { - if (isEditMode) { - await updateSatelliteManage({ - satelliteManageId, - companyNo: Number(companyNo), - satelliteName, - sensorType, - photoResolution, - frequency, - photoDetail, - }); - } else { - await saveSatelliteManage({ - companyNo: Number(companyNo), - satelliteName, - sensorType, - photoResolution, - frequency, - photoDetail, - }); - } - onSaved?.(); - onClose(); - } catch { - setError('저장 중 오류가 발생했습니다.'); - } finally { - setIsSaving(false); - } - }; - - return createPortal( -
-
-
- - {isEditMode ? '위성 관리 상세' : '위성 관리 등록'} - -
- -
- {isLoading &&
조회 중...
} - {error &&
{error}
} - - {!isLoading && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위성 관리 등록 - 사업자명, 위성명, 센서 타입, 촬영 해상도, 주파수, 상세내역에 대한 내용을 등록하는 표입니다.
사업자명 * - -
위성명 * - setSatelliteName(e.target.value)} - /> -
센서 타입 - -
촬영 해상도 - setPhotoResolution(e.target.value)} - /> -
주파수 - setFrequency(e.target.value)} - /> -
상세내역 -