Compare commits
4 Commits
ec281bf354
...
57a6e4b51e
| Author | SHA1 | Date | |
|---|---|---|---|
| 57a6e4b51e | |||
| 5c4df5a19a | |||
| 839901d663 | |||
| 76dab1c6a9 |
148
web-app/app/features/imagery/api/aerial.ts
Normal file
148
web-app/app/features/imagery/api/aerial.ts
Normal file
@@ -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<ApiResponse<PagedResponse<AerialItem>>>('/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<ApiResponse<AerialDetail>>(`/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<ApiResponse<Region[]>>('/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<ApiResponse<FolderListResponse>>('/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<ApiResponse<ChunkUploadResponse>>(
|
||||||
|
'/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<ApiResponse<ChunkUploadResponse>>(
|
||||||
|
`/api/imagery/upload/chunk-upload-complete/${uuid}`,
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('completeChunkUpload error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
110
web-app/app/features/imagery/components/AerialList.tsx
Normal file
110
web-app/app/features/imagery/components/AerialList.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export function AerialList() {
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<AerialItem[]>([]);
|
||||||
|
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 (
|
||||||
|
<Section className="w-full pb-4" variant="list">
|
||||||
|
<div className="flex flex-col h-full p-4 gap-4">
|
||||||
|
<h1 className="text-xl font-bold">항공영상관리</h1>
|
||||||
|
|
||||||
|
<Table isLayoutFixed isFullHeight>
|
||||||
|
<Table.Caption>
|
||||||
|
<Table.CaptionLeft>
|
||||||
|
<Table.Total count={totalCount} />
|
||||||
|
</Table.CaptionLeft>
|
||||||
|
<Table.CaptionRight>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button color="light" size="small">엑셀 다운로드</Button>
|
||||||
|
<Button color="primary" size="small">영상 등록</Button>
|
||||||
|
</div>
|
||||||
|
</Table.CaptionRight>
|
||||||
|
</Table.Caption>
|
||||||
|
|
||||||
|
<Table.Container>
|
||||||
|
<Table.Colgroup>
|
||||||
|
<Table.Col width={60} />
|
||||||
|
<Table.Col width={180} />
|
||||||
|
<Table.Col width={200} />
|
||||||
|
<Table.Col width={120} />
|
||||||
|
<Table.Col width={100} />
|
||||||
|
<Table.Col width={120} />
|
||||||
|
<Table.Col width={80} />
|
||||||
|
</Table.Colgroup>
|
||||||
|
|
||||||
|
<Table.Header>
|
||||||
|
<Table.HeaderRow>
|
||||||
|
<Table.HeaderCell align="center">No</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>파일명</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>지역</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell align="center">촬영일시</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell align="center">축적</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell align="right">용량</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell align="center">전처리</Table.HeaderCell>
|
||||||
|
</Table.HeaderRow>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{isLoading ? (
|
||||||
|
<Table.Loading colSpan={7} />
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<Table.Empty colSpan={7}>등록된 항공영상이 없습니다.</Table.Empty>
|
||||||
|
) : (
|
||||||
|
data.map((item, index) => (
|
||||||
|
<Table.Row
|
||||||
|
key={item.mapId}
|
||||||
|
isSelected={selectedId === item.mapId}
|
||||||
|
onClick={() => setSelectedId(item.mapId)}
|
||||||
|
>
|
||||||
|
<Table.Cell align="center">{index + 1}</Table.Cell>
|
||||||
|
<Table.Cell>{item.fileName}</Table.Cell>
|
||||||
|
<Table.Cell>{item.region}</Table.Cell>
|
||||||
|
<Table.Cell align="center">{item.capturedDttm}</Table.Cell>
|
||||||
|
<Table.Cell align="center">{item.scale}</Table.Cell>
|
||||||
|
<Table.Cell align="right">{item.fileSize}</Table.Cell>
|
||||||
|
<Table.Cell align="center">
|
||||||
|
{item.status}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Container>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
web-app/app/features/imagery/types/aerial.ts
Normal file
76
web-app/app/features/imagery/types/aerial.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
3
web-app/app/routes/change-model/classification/page.tsx
Normal file
3
web-app/app/routes/change-model/classification/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>변화모델 분류모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/change-model/g1/page.tsx
Normal file
3
web-app/app/routes/change-model/g1/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>변화모델 G1 모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/change-model/g2/page.tsx
Normal file
3
web-app/app/routes/change-model/g2/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>변화모델 G2 모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/change-model/g3/page.tsx
Normal file
3
web-app/app/routes/change-model/g3/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>변화모델 G3 모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/change-model/parameter/page.tsx
Normal file
3
web-app/app/routes/change-model/parameter/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>변화모델 파라미터관리</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>탐지모델 분류모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/detection-model/g1/page.tsx
Normal file
3
web-app/app/routes/detection-model/g1/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>탐지모델 G1 모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/detection-model/g2/page.tsx
Normal file
3
web-app/app/routes/detection-model/g2/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>탐지모델 G2 모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/detection-model/g3/page.tsx
Normal file
3
web-app/app/routes/detection-model/g3/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>탐지모델 G3 모델관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/detection-model/parameter/page.tsx
Normal file
3
web-app/app/routes/detection-model/parameter/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>탐지모델 파라미터관리</div>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { AerialList } from '~/features/imagery/components/AerialList';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div>항공영상관리 목록</div>
|
<AerialList></AerialList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3
web-app/app/routes/labeling-management/change/page.tsx
Normal file
3
web-app/app/routes/labeling-management/change/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>변화탐지 라벨링관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/labeling-management/object/page.tsx
Normal file
3
web-app/app/routes/labeling-management/object/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>객체탐지 라벨링관리</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/log/error/page.tsx
Normal file
3
web-app/app/routes/log/error/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>에러 로그</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/object/page.tsx
Normal file
3
web-app/app/routes/object/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>객체탐지</div>;
|
||||||
|
}
|
||||||
3
web-app/app/routes/terrain/page.tsx
Normal file
3
web-app/app/routes/terrain/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <div>지형변화탐지</div>;
|
||||||
|
}
|
||||||
17
web-app/app/shared/types/api.ts
Normal file
17
web-app/app/shared/types/api.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export type ApiResponse<T = unknown> = {
|
||||||
|
data: T;
|
||||||
|
error?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PagedResponse<T> = {
|
||||||
|
content: T[];
|
||||||
|
number: number;
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
size: number;
|
||||||
|
sort: {
|
||||||
|
empty: boolean;
|
||||||
|
sorted: boolean;
|
||||||
|
unsorted: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user