Merge remote-tracking branch 'origin/iipbs/sn/map' into iipbs/sn/dev

This commit is contained in:
yschoi
2025-05-22 11:13:11 +09:00
10 changed files with 315 additions and 106 deletions
@@ -0,0 +1,74 @@
import type { AxiosResponse } from "axios";
interface SortConfig {
empty: boolean;
sorted: boolean;
unsorted: boolean;
}
interface PageableConfig {
sort: SortConfig;
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
}
export interface PageableList<T> {
content: T[];
pageable: PageableConfig;
sort: SortConfig;
totalPages: number;
totalElements: number;
number: number;
numberOfElements: number;
size: number;
first: boolean;
last: boolean;
empty: boolean;
}
export interface PageableParams {
page?: number;
size?: number;
sort?: string[];
}
export interface AttachmentParams extends PageableParams {
keywordType?: string;
keyword?: string;
atflCd?: string;
atflSn?: number;
atflId?: string;
orgnlFileNm?: string;
atflFlieSz?: number;
atflType?: string;
dwnldNocs?: number;
delYn?: string;
lnkgDmnId?: string;
lnkgDmnNm?: string;
}
export interface Attachment {
atflCd: string;
atflSn: number;
atflId: string;
physFileNm: string;
orgnlFileNm: string;
atflFlieSz: number;
atflType: string;
dwnldNocs: number;
delYn: string;
lnkgDmnId: string;
lnkgDmnNm: string;
regDt: string;
}
export type AttachmentList = PageableList<Attachment>;
export interface AttachmentService {
getAttachmentsList: (params: AttachmentParams) => Promise<AttachmentList>;
getAttachmentsInfo: (atflCd: string) => Promise<Attachment[]>;
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
}
@@ -0,0 +1,42 @@
import type { AxiosResponse } from "axios";
import type {
Attachment,
AttachmentList,
AttachmentParams,
AttachmentService,
} from "../attachment/attachment-interface";
import { getData, deleteData } from "../index";
export const ENDPOINT = {
ATTACH_SERVICE: {
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor",
IMAGES: "/attach-service/api/v1/images",
MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail",
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
DOWNLOAD: "/attach-service/api/v1/atflcd/download",
STREAM_VIDEO: "/attach-service/api/v1/stream/video",
},
};
export class AttachmentApiService implements AttachmentService {
public async getAttachmentsList(
params: AttachmentParams,
): Promise<AttachmentList> {
const response = await getData(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}`);
return response.data;
}
public async getAttachmentsInfo(atflCd: string): Promise<Attachment[]> {
const response = await getData(
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`,
);
return response.data;
}
public async deleteAttachmentByAtflCd(atflCd: string): Promise<void> {
await deleteData(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/atflCd/${atflCd}`);
}
}
export const AttachmentApi = new AttachmentApiService();
@@ -1,10 +1,9 @@
import { Upload, type HttpResponse } from "tus-js-client";
import { getCookie, setCookie } from "@utils";
import { ENDPOINT } from "../api/attachment/attachment-service";
const AUTH_USER_ID = process.env.AUTH_USER_ID || '';
const CLAIM_NAME = process.env.CLAIM_NAME || '';
const BASE_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || '';
const END_URL = `${BASE_URL}/attach-service/api/v1/tus/upload`;
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
type UploadOptions = {
handleUploadStart?: (upload: Upload) => void;
@@ -24,7 +23,7 @@ export const handleUpload = (files: FileList, options?: UploadOptions) => {
};
const upload = new Upload(file, {
endpoint: END_URL,
endpoint: ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD,
chunkSize: 10 * 1024 * 1024,
headers: {
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
+47 -19
View File
@@ -1,11 +1,14 @@
"use client"
import { FileUpload, SelectBox } from '@common/business_components/ui';
import { Button, Input } from '@common/components/ui';
import React, { useState } from 'react'
import type { HttpResponse, Upload } from "tus-js-client";
import { FileUploader } from "@common/components";
import { handleUpload } from "@utils";
const FileUploadForm = ({ title, onSubmit }: { title: string, onSubmit:(files:FileList, year:string, quarter:string) => void }) => {
// 현재 년도부터 10년 전까지의 년도 옵션 생성
// 현재 년도부터 10년 전까지의 년도 옵션 생성
const yearOptions = Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i;
return { text: `${year}년`, val: year.toString() };
@@ -35,25 +38,50 @@ const FileUploadForm = ({ title, onSubmit }: { title: string, onSubmit:(files:Fi
selectArray={quater}
onSelectValue={(value: string | number) => setSelectedQuarter(String(value))}
/>
</div>
<div className='flex mt-5 items-center'>
<div className="w-[200px] mr-4">
<Input id="file" type="file" onChange={(e)=>{if(e.target.files)setFiles(e.target.files)}}/>
</div>
<Button
className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
size="sm"
onClick={() => {
console.log(files, selectedYear, selectedQuarter);
if (!files || !selectedYear || !selectedQuarter) {
alert('파일과 등록년도를 선택해주세요.');
return;
<div className='flex ml-5 items-center'>
<div className="w-[80px] mr-4">
<FileUploader
id="atflCd"
className="!h-auto !max-w-none flex-col !items-start"
accept=".jpeg,.jpg,.png"
onUpload={(e) => {
const selectedFiles = e.target.files;
if (!selectedFiles) return;
const fileArray = Array.from(selectedFiles);
setFiles(fileArray);
if (fileArray[0]) {
handleUpload(selectedFiles, {
handleUploadStart: setUpload,
handleSuccess: setUpload,
handleResponse: handleUploadResponse(fileArray[0]),
});
}
onSubmit(files, selectedYear, selectedQuarter); // 부모 컴포넌트로 데이터 전달
}}
>
등록
</Button>
customPreview={<></>}
/>
</div>
<Button
className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
size="sm"
onClick={() => {
if (!files?.[0]) {
alert('파일을 선택해주세요.');
return;
}
if (!selectedYear) {
alert('등록년도를 선택해주세요.');
return;
}
if (!selectedQuarter) {
alert('분기기를 선택해주세요.');
return;
}
}}
>
등록
</Button>
</div>
</div>
</div>
</div>
+73 -65
View File
@@ -18,74 +18,82 @@ const Page = () => {
const [manageYear, setManageYear] = useState('')
const [mngQu, setMngQu] = useState('')
const [usr, setUsr] = useState('')
const [pjNm, setpjNm] = useState('')
const [fileNm, setFileNm] = useState('')
// TODO: 초기값 세팅필요
const isChckFile = () => {
if (fileNm === '' || fileNm === undefined) {
alert('파일을 선택해주세요')
const [pjNm, setPjNm] = useState('')
const handleDownload = async () => {
try {
const response = await fetch('/attach-service/api/v1/download/6066b08c-2a55-438c-b8ff-2aed9739a0c6', {
method: 'GET',
});
if (!response.ok) {
throw new Error('파일 다운로드 실패');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
// 다운로드를 트리거할 가짜 <a> 태그 생성
const link = document.createElement('a');
link.href = url;
link.download = 'test.jpg'; // 원하는 파일명 지정
document.body.appendChild(link);
link.click();
// 메모리 해제
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('다운로드 중 오류 발생:', error);
}
// TODO: 추후 파일코드 동적으로 세팅
handleDownload('/sn/attach-service/api/v1/download/6066b08c-2a55-438c-b8ff-2aed9739a0c6', fileNm)
}
//TODO: 파일 업로드 처리
return (
<ContentLayout title='도시침수 예방사업'>
<SearchDiv>
<SearchFormLeft>
<div className='flex items-center gap-4'>
<div className='flex items-center gap-2'>
<Label className="w-20 text-xl text-gray-600">등록기간</Label>
<DatePicker mode={'single'}
selected={fromDate}
onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} />
<span>~</span>
<DatePicker mode={'single'}
selected={toDate}
onSelect={(date: Date | undefined) => { if (date) setToDate(date) }} />
};
return (
<ContentLayout title='도시침수 예방사업'>
<SearchDiv>
<SearchFormLeft>
<div className='flex items-center gap-4'>
<div className='flex items-center gap-2'>
<Label className="w-20 text-xl text-gray-600">등록기간</Label>
<DatePicker mode={'single'}
selected={selectedDate}
onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} />
<span>~</span>
<DatePicker mode={'single'}
selected={selectedDate}
onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} />
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">등록자</Label>
<Input
className='w-[100px]'
value={company}
onChange={(e) => setCompany(e.target.value)}
/>
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">파일명</Label>
<Input
className='w-[100px]'
value={pjNm}
onChange={(e) => setPjNm(e.target.value)}
/>
</div>
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">관리년도</Label>
<Input
className='w-[100px]'
value={manageYear}
onChange={(e) => setManageYear(e.target.value)}
/>
<span>~</span>
<Input
className='w-[100px]'
value={mngQu}
onChange={(e) => setMngQu(e.target.value)}
/>
</SearchFormLeft>
<SearchFormRight>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm">조회</Button>
</SearchFormRight>
</SearchDiv>
<ListDiv className="mt-4">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-medium">사업정보 등록현황</h3>
<div>
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" >파일 다운로드</Button>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={handleDownload} >삭제</Button>
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">등록자</Label>
<Input
className='w-[100px]'
value={usr}
onChange={(e) => setUsr(e.target.value)}
/>
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">파일명</Label>
<Input
className='w-[100px]'
value={pjNm}
onChange={(e) => setpjNm(e.target.value)}
/>
</div>
</div>
</SearchFormLeft>
<SearchFormRight>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={() => { searchData('v1/sbmrgPrevnt', { fromRegDt: formatDate(fromDate), fromMngYr: manageYear, toMngYr: mngQu, toRegDt: formatDate(toDate), atflNm: pjNm, rgtrNm: usr, page: '0', size: '10' }) }} >조회</Button>
</SearchFormRight>
</SearchDiv>
<ListDiv className="mt-4">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-medium">사업정보 등록현황</h3>
<div>
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={isChckFile} >파일 다운로드</Button>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={() => { }} >삭제</Button>
</div>
</div>
<UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={10} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.fileNm ?? ""; setFileNm(fileName) } }} onPageChange={function (page: number): void {
+4
View File
@@ -22,4 +22,8 @@
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
+54
View File
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3002";
const geoServerUrl = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
export function middleware(req: NextRequest) {
console.log("Middleware 진입:", req.nextUrl.pathname);
const pathname = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams;
if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/geoserver")) {
const queryParams = searchParams.toString();
const newUrl = `${geoServerUrl}${pathname.replace("/geoserver", "")}${
queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
console.log(
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`,
);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
return NextResponse.next();
}
export const config = {
matcher: ["/server/:path*","/geoserver/:path*", "/attach-service/:path*"],
};
+13 -13
View File
@@ -6,7 +6,7 @@ const cnServerUrl = process.env.NEXT_PUBLIC_SERVER_API_URL?.replace(/\/$/, '') ?
const nextConfig: NextConfig = {
basePath: "/sn",
// basePath: "/sn",
typescript: {
ignoreBuildErrors: true, // 이 줄 추가
},
@@ -44,18 +44,18 @@ const nextConfig: NextConfig = {
return config;
},
async rewrites() {
return [
{
source: '/geoserver/:hasudo*',
destination: `${geoServerUrl}/:hasudo*`,
},
{
source: '/attach-service/:path*',
destination: `${cnServerUrl}/attach-service/:path*`,
},
];
},
// async rewrites() {
// return [
// {
// source: '/geoserver/:hasudo*',
// destination: `${geoServerUrl}/:hasudo*`,
// },
// {
// source: '/attach-service/:path*',
// destination: `${cnServerUrl}/attach-service/:path*`,
// },
// ];
// },
};
export default nextConfig;
@@ -2,7 +2,7 @@
export default function MainDiv(props: {children: React.ReactNode}) {
return (
<div className="h-auto bg-container">
<div className="min-h-[89vh] bg-container">
{props.children}
</div>
)
@@ -28,8 +28,8 @@ GisApp.layerInfos = [];
//기본정보
//GisApp.GEOSERVER_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/ows';
//GisApp.GEOSERVER_WMS_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/wms';
GisApp.GEOSERVER_URL = '/sn/geoserver/hasudo/ows';
GisApp.GEOSERVER_WMS_URL = '/sn/geoserver/hasudo/wms';
GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows';
GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms';
//맵 정보
GisApp.EMAP_URL = '/emap/';
@@ -47,7 +47,7 @@ GisApp.BASE_MAP_TYPES = [];
GisApp.FROM_PROJ = 'EPSG:4326';
GisApp.TO_PROJ = 'EPSG:3857';
GisApp.TO_DATA_PROJ = 'EPSG:4326';
GisApp.CONTEXT_LAYER_PATH = '/sn/images/map/layericon/';
GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/';
GisApp.layerCode = {};
GisApp.hlLayerId = 'LightFeatureList';
GisApp.hLayerId = 'highLightFeature';