feat: 항공영상관리 api 함수 임시 구현 #7

Merged
lucy merged 3 commits from feature/lucy-aerial into develop 2026-04-10 08:57:20 +09:00
20 changed files with 399 additions and 1 deletions

View 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;
}
};

View 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>
);
}

View 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;
};

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> G1 </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> G2 </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> G3 </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> G1 </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> G2 </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> G3 </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> </div>;
}

View File

@@ -1,5 +1,7 @@
import { AerialList } from '~/features/imagery/components/AerialList';
export default function Page() {
return (
<div> </div>
<AerialList></AerialList>
);
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div> </div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div></div>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div></div>;
}

View 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;
};
};