From 76dab1c6a98cdeb18d8004b3ad02641b00b3fd05 Mon Sep 17 00:00:00 2001 From: "JoohyunKim(Lucy)" Date: Thu, 9 Apr 2026 16:05:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=95=AD=EA=B3=B5=EC=98=81=EC=83=81?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20api=20=ED=95=A8=EC=88=98=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-app/app/features/imagery/api/aerial.ts | 148 ++++++++++++++++++ .../imagery/components/AerialList.tsx | 119 ++++++++++++++ web-app/app/features/imagery/types/aerial.ts | 76 +++++++++ .../change-model/classification/page.tsx | 3 + web-app/app/routes/change-model/g1/page.tsx | 3 + web-app/app/routes/change-model/g2/page.tsx | 3 + web-app/app/routes/change-model/g3/page.tsx | 3 + .../routes/change-model/parameter/page.tsx | 3 + .../detection-model/classification/page.tsx | 3 + .../app/routes/detection-model/g1/page.tsx | 3 + .../app/routes/detection-model/g2/page.tsx | 3 + .../app/routes/detection-model/g3/page.tsx | 3 + .../routes/detection-model/parameter/page.tsx | 3 + .../labeling-management/change/page.tsx | 3 + .../labeling-management/object/page.tsx | 3 + web-app/app/routes/log/error/page.tsx | 3 + web-app/app/routes/object/page.tsx | 3 + web-app/app/routes/terrain/page.tsx | 3 + web-app/app/shared/types/api.ts | 17 ++ 19 files changed, 405 insertions(+) create mode 100644 web-app/app/features/imagery/api/aerial.ts create mode 100644 web-app/app/features/imagery/components/AerialList.tsx create mode 100644 web-app/app/features/imagery/types/aerial.ts create mode 100644 web-app/app/routes/change-model/classification/page.tsx create mode 100644 web-app/app/routes/change-model/g1/page.tsx create mode 100644 web-app/app/routes/change-model/g2/page.tsx create mode 100644 web-app/app/routes/change-model/g3/page.tsx create mode 100644 web-app/app/routes/change-model/parameter/page.tsx create mode 100644 web-app/app/routes/detection-model/classification/page.tsx create mode 100644 web-app/app/routes/detection-model/g1/page.tsx create mode 100644 web-app/app/routes/detection-model/g2/page.tsx create mode 100644 web-app/app/routes/detection-model/g3/page.tsx create mode 100644 web-app/app/routes/detection-model/parameter/page.tsx create mode 100644 web-app/app/routes/labeling-management/change/page.tsx create mode 100644 web-app/app/routes/labeling-management/object/page.tsx create mode 100644 web-app/app/routes/log/error/page.tsx create mode 100644 web-app/app/routes/object/page.tsx create mode 100644 web-app/app/routes/terrain/page.tsx create mode 100644 web-app/app/shared/types/api.ts diff --git a/web-app/app/features/imagery/api/aerial.ts b/web-app/app/features/imagery/api/aerial.ts new file mode 100644 index 0000000..a3121f9 --- /dev/null +++ b/web-app/app/features/imagery/api/aerial.ts @@ -0,0 +1,148 @@ +import axios from 'axios'; +import type { ApiResponse, PagedResponse } from '~/shared/types/api'; +import type { + AerialDetail, + AerialItem, + AerialListParams, + ChunkUploadParams, + ChunkUploadResponse, + FolderListResponse, + Region, +} from '../types/aerial'; + +/** + * 항공영상 목록 조회 + */ +export const fetchAerialList = async (params: AerialListParams) => { + try { + const response = await axios.get>>('/api/imagery/aerial/list', { + params: { + dateRangeType: params.dateRangeType, + strtDttm: params.strtDttm, + endDttm: params.endDttm, + page: params.page ?? 0, + size: params.size ?? 20, + }, + }); + + return { + list: response.data.data.content, + pagination: { + currentPage: response.data.data.number, + pageSize: response.data.data.size, + totalPages: response.data.data.totalPages, + totalItems: response.data.data.totalElements, + }, + }; + } catch (error) { + console.error('fetchAerialList error:', error); + throw error; + } +}; + +/** + * 항공영상 상세 요약정보 조회 + */ +export const fetchAerialDetail = async (uuid: string) => { + try { + const response = await axios.get>(`/api/imagery/aerial/detail/${uuid}`); + return response.data.data; + } catch (error) { + console.error('fetchAerialDetail error:', error); + throw error; + } +}; + +/** + * 항공영상 상세 영상 조회 (이미지 다운로드 URL 반환) + */ +export const getAerialImageUrl = (uuid: string, imageType: 'before' | 'after' = 'before') => { + return `/api/imagery/detail/image?uuid=${uuid}&imageType=${imageType}`; +}; + +/** + * 항공영상 상세 영상 조회 (Blob으로 다운로드) + */ +export const fetchAerialImage = async (uuid: string, imageType: 'before' | 'after' = 'before') => { + try { + const response = await axios.get('/api/imagery/detail/image', { + params: { uuid, imageType }, + responseType: 'blob', + }); + return response.data; + } catch (error) { + console.error('fetchAerialImage error:', error); + throw error; + } +}; + +/** + * 업로드 지역 조회 (시/도) + */ +export const fetchRegions = async () => { + try { + const response = await axios.get>('/api/imagery/regions/provinces'); + return response.data.data; + } catch (error) { + console.error('fetchRegions error:', error); + throw error; + } +}; + +/** + * 업로드 폴더 조회 + */ +export const fetchFolderList = async (dirPath: string) => { + try { + const response = await axios.post>('/api/imagery/folder-list', { + dirPath, + }); + return response.data.data; + } catch (error) { + console.error('fetchFolderList error:', error); + throw error; + } +}; + +/** + * 대용량 파일 분할 전송 (청크 업로드) + */ +export const uploadFileChunk = async (params: ChunkUploadParams) => { + try { + const formData = new FormData(); + formData.append('fileName', params.fileName); + formData.append('fileSize', String(params.fileSize)); + formData.append('chunkIndex', String(params.chunkIndex)); + formData.append('chunkTotalIndex', String(params.chunkTotalIndex)); + formData.append('chunkFile', params.chunkFile); + + const response = await axios.post>( + '/api/imagery/upload/file-chunk-upload', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + return response.data.data; + } catch (error) { + console.error('uploadFileChunk error:', error); + throw error; + } +}; + +/** + * 업로드 완료된 파일 병합 + */ +export const completeChunkUpload = async (uuid: string) => { + try { + const response = await axios.put>( + `/api/imagery/upload/chunk-upload-complete/${uuid}`, + ); + return response.data.data; + } catch (error) { + console.error('completeChunkUpload error:', error); + throw error; + } +}; diff --git a/web-app/app/features/imagery/components/AerialList.tsx b/web-app/app/features/imagery/components/AerialList.tsx new file mode 100644 index 0000000..1a6bdfe --- /dev/null +++ b/web-app/app/features/imagery/components/AerialList.tsx @@ -0,0 +1,119 @@ +import { useEffect, useState } from 'react'; +import { Button } from '~/shared/components/button/Button'; +import { Section } from '~/shared/components/section/Section'; +import { Table } from '~/shared/components/table'; +import type { AerialItem } from '../types/aerial'; + +const statusColors: Record = { + 대기: 'text-dabeeo-gray-99', + 처리중: 'text-primary', + 완료: 'text-dabeeo-navy-tertiary', + 실패: 'text-red-500', +}; + +export function AerialList() { + const [selectedId, setSelectedId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState([]); + const [totalCount, setTotalCount] = useState(0); + + useEffect(() => { + const load = async () => { + setIsLoading(true); + try { + // const result = await fetchAerialList('YEAR', strtDttm, endDttm); + const result = { + list: [], + pagination: { + currentPage: 0, + pagiSize: 0, + totalPages: 0, + totalItems: 0, + }, + }; + if (result) { + setData(result.list); + setTotalCount(result.pagination.totalItems); + } + } finally { + setIsLoading(false); + } + }; + + load(); + }, []); + + return ( +
+
+

항공영상 데이터 관리

+ + + + + + + +
+ + +
+
+
+ + + + + + + + + + + + + + + No + 영상명 + 촬영 지역 + 촬영일 + 해상도 + 파일크기(MB) + 상태 + + + + + {isLoading ? ( + + ) : data.length === 0 ? ( + 등록된 항공영상이 없습니다. + ) : ( + data.map((item, index) => ( + setSelectedId(item.mapId)} + > + {index + 1} + {item.fileName} + {item.region} + {item.capturedDttm} + {item.scale} + {item.fileSize} + + + {item.status} + + + + )) + )} + + +
+
+
+ ); +} diff --git a/web-app/app/features/imagery/types/aerial.ts b/web-app/app/features/imagery/types/aerial.ts new file mode 100644 index 0000000..f6b6bb2 --- /dev/null +++ b/web-app/app/features/imagery/types/aerial.ts @@ -0,0 +1,76 @@ +// 목록 아이템 +export type AerialItem = { + mapId: string; + fileName: string; + region: string; + scale: string; + capturedDttm: string; + fileSize: string; + createdDttm: string; + createdBy: string; + status: string; +}; + +// 상세 정보 +export type AerialDetail = { + uuid: string; + fileName: string; + fileSize: number; + region: string; + category: string; + createdName: string; + createdDttm: string; + capturedDttm: string; + latitude: number; + longitude: number; +}; + +// 지역 (시/도) +export type Region = { + code: string; + name: string; +}; + +// 폴더 +export type Folder = { + folderNm: string; + parentFolderNm: string; + parentPath: string; + fullPath: string; + depth: number; + childCnt: number; + lastModified: string; + isValid: boolean; +}; + +export type FolderListResponse = { + dirPath: string; + folders: Folder[]; +}; + +// 청크 업로드 +export type ChunkUploadParams = { + fileName: string; + fileSize: number; + chunkIndex: number; + chunkTotalIndex: number; + chunkFile: Blob; +}; + +export type ChunkUploadResponse = { + uuid: string; + filePath: string; + fileName: string; + chunkIndex: number; + chunkTotalIndex: number; + uploadId?: string; +}; + +// 목록 조회 파라미터 +export type AerialListParams = { + dateRangeType: 'capturedDttm' | 'createdDttm'; + strtDttm: string; + endDttm: string; + page?: number; + size?: number; +}; diff --git a/web-app/app/routes/change-model/classification/page.tsx b/web-app/app/routes/change-model/classification/page.tsx new file mode 100644 index 0000000..73155a3 --- /dev/null +++ b/web-app/app/routes/change-model/classification/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
변화모델 분류모델관리
; +} diff --git a/web-app/app/routes/change-model/g1/page.tsx b/web-app/app/routes/change-model/g1/page.tsx new file mode 100644 index 0000000..ac222f5 --- /dev/null +++ b/web-app/app/routes/change-model/g1/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
변화모델 G1 모델관리
; +} diff --git a/web-app/app/routes/change-model/g2/page.tsx b/web-app/app/routes/change-model/g2/page.tsx new file mode 100644 index 0000000..cdc3de3 --- /dev/null +++ b/web-app/app/routes/change-model/g2/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
변화모델 G2 모델관리
; +} diff --git a/web-app/app/routes/change-model/g3/page.tsx b/web-app/app/routes/change-model/g3/page.tsx new file mode 100644 index 0000000..127fe68 --- /dev/null +++ b/web-app/app/routes/change-model/g3/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
변화모델 G3 모델관리
; +} diff --git a/web-app/app/routes/change-model/parameter/page.tsx b/web-app/app/routes/change-model/parameter/page.tsx new file mode 100644 index 0000000..d70c698 --- /dev/null +++ b/web-app/app/routes/change-model/parameter/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
변화모델 파라미터관리
; +} diff --git a/web-app/app/routes/detection-model/classification/page.tsx b/web-app/app/routes/detection-model/classification/page.tsx new file mode 100644 index 0000000..9fee21e --- /dev/null +++ b/web-app/app/routes/detection-model/classification/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
탐지모델 분류모델관리
; +} diff --git a/web-app/app/routes/detection-model/g1/page.tsx b/web-app/app/routes/detection-model/g1/page.tsx new file mode 100644 index 0000000..3964305 --- /dev/null +++ b/web-app/app/routes/detection-model/g1/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
탐지모델 G1 모델관리
; +} diff --git a/web-app/app/routes/detection-model/g2/page.tsx b/web-app/app/routes/detection-model/g2/page.tsx new file mode 100644 index 0000000..37d2415 --- /dev/null +++ b/web-app/app/routes/detection-model/g2/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
탐지모델 G2 모델관리
; +} diff --git a/web-app/app/routes/detection-model/g3/page.tsx b/web-app/app/routes/detection-model/g3/page.tsx new file mode 100644 index 0000000..06337d0 --- /dev/null +++ b/web-app/app/routes/detection-model/g3/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
탐지모델 G3 모델관리
; +} diff --git a/web-app/app/routes/detection-model/parameter/page.tsx b/web-app/app/routes/detection-model/parameter/page.tsx new file mode 100644 index 0000000..41240c2 --- /dev/null +++ b/web-app/app/routes/detection-model/parameter/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
탐지모델 파라미터관리
; +} diff --git a/web-app/app/routes/labeling-management/change/page.tsx b/web-app/app/routes/labeling-management/change/page.tsx new file mode 100644 index 0000000..38e9510 --- /dev/null +++ b/web-app/app/routes/labeling-management/change/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
변화탐지 라벨링관리
; +} diff --git a/web-app/app/routes/labeling-management/object/page.tsx b/web-app/app/routes/labeling-management/object/page.tsx new file mode 100644 index 0000000..220b5d1 --- /dev/null +++ b/web-app/app/routes/labeling-management/object/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
객체탐지 라벨링관리
; +} diff --git a/web-app/app/routes/log/error/page.tsx b/web-app/app/routes/log/error/page.tsx new file mode 100644 index 0000000..1e1ffb6 --- /dev/null +++ b/web-app/app/routes/log/error/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
에러 로그
; +} diff --git a/web-app/app/routes/object/page.tsx b/web-app/app/routes/object/page.tsx new file mode 100644 index 0000000..ef9a747 --- /dev/null +++ b/web-app/app/routes/object/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
객체탐지
; +} diff --git a/web-app/app/routes/terrain/page.tsx b/web-app/app/routes/terrain/page.tsx new file mode 100644 index 0000000..adaf3e1 --- /dev/null +++ b/web-app/app/routes/terrain/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
지형변화탐지
; +} diff --git a/web-app/app/shared/types/api.ts b/web-app/app/shared/types/api.ts new file mode 100644 index 0000000..0de4ae6 --- /dev/null +++ b/web-app/app/shared/types/api.ts @@ -0,0 +1,17 @@ +export type ApiResponse = { + data: T; + error?: never; +}; + +export type PagedResponse = { + content: T[]; + number: number; + totalElements: number; + totalPages: number; + size: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; +};