Merge branch 'iipbs/common/main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/common/main

This commit is contained in:
jyha00
2025-06-09 19:15:20 +09:00
23 changed files with 1293 additions and 117 deletions
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+12
View File
@@ -0,0 +1,12 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
+114
View File
@@ -0,0 +1,114 @@
"use client";
import Gis, { useGisContext } from "@common/business_components/ui/map/Gis";
import { mapService } from "@service";
import { useCallback, useEffect, useRef, useState } from "react";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import SearchModal from "./SearchModal";
import FeatureClickEventPopup from "./featureClickEventPopup";
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태
const gisMaps = useRef(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
setTreeData(response.data); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: any) => {
let map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature = null;
if (
data &&
typeof data.getGeometry === "function" &&
typeof data.setStyle === "function"
) {
// 이미 Feature 인스턴스라면 그대로 사용
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
map
.getLayers()
.getArray()
.forEach((layer: any) => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
}
});
}
// 3. 피처 선택
if (foundFeature) {
let geometry = foundFeature.getGeometry();
if (geometry) {
let extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
select.selectFeature(foundFeature, true);
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
}
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
};
return (
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={true}
gisMaps={gisMaps}
isMiniMap={true}
searchView={SearchModal}
detailView={FeatureClickEventPopup}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
/>
</div>
);
};
export default Map;
+1 -1
View File
@@ -9,7 +9,7 @@
"packages/*"
],
"scripts": {
"build": "turbo run build --filter=!wr",
"build": "turbo run build",
"dev": "turbo run dev",
"dev:cs": "turbo run dev --filter=cs",
"dev:wr": "turbo run dev --filter=wr",
@@ -64,10 +64,10 @@ const CustomLegend = (props: any) => {
const { payload } = props;
return (
<div style={customStyles.legendContainer}>
{payload.map((entry, index: number) => (
{payload.map((entry: unknown, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div>
<span className="text-[12px]">{entry.value}</span>
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: (entry as {color: string}).color }}></div>
<span className="text-[12px]">{(entry as {value: string}).value}</span>
</div>
))}
</div>
@@ -76,7 +76,15 @@ const CustomLegend = (props: any) => {
export default function Chart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) {
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEEAD", "#D4A5A5"];
const getMaxValue = (data: any[], keyPrefix: string) => {
return Math.max(...data.map(item =>
Object.keys(item)
.filter(key => key.includes(keyPrefix))
.map(key => item[key])
).flat());
};
const barMaxValue = getMaxValue(chartData, 'barVal');
return (
<>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
@@ -102,7 +110,8 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle='
axisLine={true}
fontSize={12}
fontWeight={500}
label={yFieldText ? { value: yFieldText, className:"text-[13px] foont-bold", position: 'top', textAnchor: 'middle', offset: 20 } : undefined}
domain={[0, Math.ceil(barMaxValue * 1)]}
label={yFieldText ? { value: yFieldText, className:"text-[13px] font-bold", position: 'insideLeft', angle: -90, offset: 20 } : undefined}
/>
<ChartTooltip cursor={false} content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />}/>
{chartData && chartData.length > 0 && (
@@ -113,20 +113,21 @@ export default function Linechart({
}, []);
// 데이터의 최대값 계산
const getMaxValue = (data: ChartDataType, keyPrefix: string) => {
return Math.max(...Object.values(data).map(item =>
Object.keys(item)
.filter(key => key.includes(keyPrefix))
.map(key => item[key] as unknown as number)
).flat());
const getMaxValue = (data: ChartDataType): number => {
return Math.max(...Object.values(data).flatMap(dataset =>
dataset.map(entry => {
const val = parseFloat(entry.val);
return isNaN(val) ? 0 : val;
})
));
};
const lineMaxValue = getMaxValue(chartData, 'lineVal');
const lineMaxValue = getMaxValue(chartData);
return (
<div className={chartClassName}>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
<div className="text-left text-[13px] font-bold mt-[13px] mb-[10px]">
{chartTitle}
</div>
) : null}
@@ -1,5 +1,6 @@
import type { sheetParameter } from '@/index';
// import type { sheetParameter } from '@/index';
import * as XlsxPopulate from '@xlsx/xlsx-populate';
import type { SheetParameter } from './index';
@@ -38,13 +39,13 @@ export const excelStyle = async (data: ArrayBuffer, range: string = "A1:V3", fil
}
export const excelArrayStyle = async (data: ArrayBuffer, range: [typeof sheetParameter], fileNm:string) => {
export const excelArrayStyle = async (data: ArrayBuffer, range: SheetParameter[], fileNm: string) => {
const workbook = await XlsxPopulate.Workbook.fromData(data);
if(range){
range.forEach((item , index)=>{
if (range) {
range.forEach((item, index) => {
const rangeCells = workbook.activeSheet(index)?.sheet(index)?.range(item.headerRange);
console.log(rangeCells);
rangeCells?.style((styleQ)=>{
rangeCells?.style((styleQ) => {
makeCellStyle(styleQ)
});
@@ -5,7 +5,7 @@ type ExcelDownloadProps = {
data: JSON | JSON[];
fileName: string;
sheetNameArray: string[];
headerRange: string;
headerRange?: string;
}
export const downloadExcel = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"], headerRange='A1:V3'}: ExcelDownloadProps) => {
@@ -15,17 +15,21 @@ export const downloadExcel = ({ data, fileName = new Date().getTime().toString()
excelStyle(XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }), headerRange, fileName);
};
export const sheetParameter ={
data:[[]], headerNum:0, headerRange:'A1:V3', sheetName:'Sheet1', startCol:0
}
export type SheetParameter = {
data: any[][];
headerNum: number;
headerRange: string;
sheetName: string;
startCol?: string;
};
export const downloadMultipleExcel = (fileName = new Date().getTime().toString() + '.xlsx', sheetArray: Array<typeof sheetParameter>): Promise<JSON | undefined> => {
export const downloadMultipleExcel = (fileName = new Date().getTime().toString() + '.xlsx', sheetArray: SheetParameter[]): Promise<JSON | undefined> => {
const workbook = XLSX.utils.book_new();
for (let i = 0; i < sheetArray.length; i++) {
// Create a new worksheet for each sheet
const worksheet = XLSX.utils.aoa_to_sheet(sheetArray[i]?.data || [[]]);
// Add data to the worksheet starting from the specified header number
if (sheetArray&&sheetArray[i]?.headerNum) {
if (sheetArray && sheetArray[i]?.headerNum) {
XLSX.utils.sheet_add_aoa(worksheet, sheetArray[i]?.data || [[]], { origin: `A${sheetArray[i]?.headerNum}` });
}
// Append the worksheet to the workbook with the specified sheet name
@@ -37,7 +41,6 @@ export const downloadMultipleExcel = (fileName = new Date().getTime().toString()
return Promise.resolve(undefined);
};
export const filePicked=(oFile: File, headerNum:number=0, dateType="yyyymmdd"):Promise<JSON | undefined> =>{
// Create A File Reader HTML5
const reader = new FileReader();
@@ -80,8 +83,6 @@ export const filePicked=(oFile: File, headerNum:number=0, dateType="yyyymmdd"):P
});
}
export const filePickedArray=(oFile: File, headerNum:number=0):Promise<JSON[] | undefined> =>{
// Create A File Reader HTML5
const reader = new FileReader();
@@ -123,8 +124,6 @@ export const filePickedArray=(oFile: File, headerNum:number=0):Promise<JSON[] |
});
}
export const filePickedBlob=(oFile: ArrayBuffer, data:[][],fileNm:string, headerNum:number=0, headerRange='A1:V3', startCol?:number):Promise<JSON | undefined> =>{ // Create A File Reader HTML5
return new Promise<JSON | undefined>(() => {
// Ready The Event For When A File Gets Selected
@@ -149,28 +148,20 @@ export const filePickedBlob=(oFile: ArrayBuffer, data:[][],fileNm:string, header
});
}
export const filePickedBlobArray= (oFile: ArrayBuffer,fileNm:string, sheetArray: [typeof sheetParameter]):Promise<JSON | undefined> =>{ // Create A File Reader HTML5
return new Promise<JSON | undefined> (async () => {
// Ready The Event For When A File Gets Selected
const wb = XLSX.read(oFile, { type: 'binary' });
// Loop Over Each Sheet
wb.SheetNames.forEach( function (sheetName, index) {
// Obtain The Current Row As CSV
if(sheetArray[index]){
const sheet = wb.Sheets[sheetName];
if (sheet) {
//오리진 확인 test 필요
// const sheet = XLSX.utils.json_to_sheet(data);
const startCol = sheetArray[index].startCol?sheetArray[index].startCol.toString():'A';
XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: startCol+sheetArray[index].headerNum });
// XLSX.utils.book_append_sheet(wb, sheet, 'test');
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
}
});
await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm)
});
}
export const filePickedBlobArray = (oFile: ArrayBuffer, fileNm: string, sheetArray: SheetParameter[]): Promise<JSON | undefined> => {
return new Promise<JSON | undefined>(async () => {
const wb = XLSX.read(oFile, { type: 'binary' });
wb.SheetNames.forEach(function (sheetName, index) {
if (sheetArray[index]) {
const sheet = wb.Sheets[sheetName];
if (sheet) {
const startCol = sheetArray[index].startCol ? sheetArray[index].startCol : 'A';
XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: startCol + sheetArray[index].headerNum });
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
}
});
await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm)
});
}
@@ -12,10 +12,12 @@ const dropZoneConfig = {
files: File[]; // 파일 배열
setFiles: (files: File[] | null) => void; // 파일 배열 세터
fileTypes?: string; // 옵션 (파일 확장자 문자열)
onFileClick?:(index:number)=> void; // 파일 클릭 핸들러
}
export const FileUpload =
({ files, setFiles, fileTypes }: FileUploadProps) => {
({ files, setFiles, fileTypes ,onFileClick}: FileUploadProps) => {
return (
<FileUploader
value={files}
@@ -43,9 +45,9 @@ export const FileUpload =
{files&&
files.length > 0 &&
files.map((file:File, i:number) => (
<FileUploaderItem key={i} index={i}>
<FileUploaderItem key={i} index={i} onFileClick={onFileClick }>
<Paperclip className="h-4 w-4 stroke-current" />
<span>{file.name}</span>
<span className="w-4/5 overflow-hidden">{file.name}</span>
</FileUploaderItem>
))}
</FileUploaderContent>
@@ -286,9 +286,9 @@ FileUploaderContent.displayName = "FileUploaderContent";
export const FileUploaderItem = forwardRef<
HTMLDivElement,
{ index: number } & React.HTMLAttributes<HTMLDivElement>
>(({ className, index, children, ...props }, ref) => {
const { removeFileFromSet, activeIndex, direction } = useFileUpload();
{ index: number } & React.HTMLAttributes<HTMLDivElement> &{onFileClick?:(index:number)=>void}
>(({ className, index, children, onFileClick, ...props }, ref) => {
const { removeFileFromSet , activeIndex, direction } = useFileUpload();
const isSelected = index === activeIndex;
return (
<div
@@ -300,8 +300,11 @@ export const FileUploaderItem = forwardRef<
isSelected ? "bg-muted" : "",
)}
{...props}
>
<div className="font-medium leading-none tracking-tight flex items-center gap-1.5 h-full w-full">
<div className="font-medium leading-none tracking-tight flex items-center gap-1.5 h-full !w-4/5 overflow-hidden "
onClick={() =>{onFileClick?.(index)
}}>
{children}
</div>
<button
@@ -309,8 +312,10 @@ export const FileUploaderItem = forwardRef<
className={cn(
"absolute",
direction === "rtl" ? "top-1 left-1" : "top-1 right-1",
)}
onClick={() => removeFileFromSet(index)}
onClick={() =>{
removeFileFromSet(index)}}
>
<span className="sr-only">remove item {index}</span>
<RemoveIcon className="w-4 h-4 hover:stroke-destructive duration-200 ease-in-out" />
@@ -1,19 +1,70 @@
import React from "react";
import { Checkbox } from "@common/components/ui";
import React, { useCallback, useEffect, useState } from "react";
import { FormControl, FormItem, FormLabel, FormMessage } from "../form";
const FormUi = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {label:string|undefined}
>(({children, label},ref)=>{
export const BZNM_TOT_NO = '종사업자등록번호';
export const CANDIDATE = '공급처 후보지 선택(필수아님)';
interface FormUiProps extends React.HTMLAttributes<HTMLDivElement> {
label?: string;
onCheckboxChange?: (checked: boolean) => void;
isDisabled?: boolean;
}
interface FormElementProps extends React.HTMLAttributes<HTMLElement> {
disabled?: boolean;
}
const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
({ children, label, onCheckboxChange, isDisabled }, ref) => {
const [isChecked, setIsChecked] = useState(isDisabled);
useEffect(() => {
setIsChecked(isDisabled);
}, [isDisabled]);
const handleCheckboxChange = useCallback((checked: boolean) => {
setIsChecked(checked);
onCheckboxChange?.(checked);
}, [onCheckboxChange]);
return (
<FormItem ref={ref}>
{label?<FormLabel className="text-xl">{label}</FormLabel>:<></>}
<FormControl>
{children}
</FormControl>
<FormMessage />
</FormItem>
)
})
<FormItem ref={ref}>
{label && <FormLabel className="text-xl">{label}</FormLabel>}
{label === BZNM_TOT_NO && (
<>
<div className="float-right">
<div className="flex items-center gap-2">
<Checkbox
id="check"
checked={isChecked}
onCheckedChange={handleCheckboxChange}
/>
<label className="w-20 text-xl text-gray-600" htmlFor="check">해당없음</label>
</div>
</div>
{children}
</>
)}
{label === CANDIDATE && (
<FormControl>
{children}
</FormControl>
)}
{label !== BZNM_TOT_NO && label !== CANDIDATE && (
<FormControl>
{React.isValidElement(children) && isDisabled
? React.cloneElement(children, {
disabled: isDisabled,
...(children.props as FormElementProps)
})
: children}
</FormControl>
)}
<FormMessage />
</FormItem>
);
}
);
FormUi.displayName = 'FormUi';
export { FormUi };
@@ -19,4 +19,4 @@ export * from "./tab";
export * from "./table";
export * from "./table_div";
export * from "./title";
export * from "./sdmInsertForm";
@@ -0,0 +1,246 @@
import './lib/css/gis.common.css';
import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react';
import OlMap from 'ol/Map';
import View from 'ol/View';
import { defaults as defaultControls, OverviewMap } from 'ol/control';
import { defaults } from 'ol/interaction';
import * as olProj from 'ol/proj';
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
import GisApp from './lib/gis.core';
import GisContent from './GisContent';
import GisTree from './GisTree';
import GisLonLat from './GisLonLat';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import GisMapToggle from './GisMapToggle';
import { Search } from 'lucide-react';
export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null });
export const useGisContext = () => useContext(GisContext);
function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView : SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
// const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
// const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터
// const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
const mapInstanceRef = useRef(null);
const checkedLayerIds = useRef([]);
let gisMap = null;
const closePopup = () => {
setIsOpen(false); // 팝업 닫기
setFeatureData(null)
};
// SearchModal, GisContent 등에서 호출
const handleFeatureSelect = useCallback((data) => {
gisMap = mapInstanceRef.current;
if (!gisMap) {
console.warn('지도가 초기화되지 않았습니다.');
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature = null;
if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') {
// 이미 Feature 인스턴스라면 그대로 사용
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
gisMap.getLayers().getArray().forEach(layer => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
}
});
}
// 3. 피처 선택
if (foundFeature) {
let geometry = foundFeature.getGeometry();
if (geometry) {
let extent = geometry.getExtent();
gisMap.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
select.selectFeature(foundFeature, true);
} else {
console.warn('OpenLayers Feature를 찾을 수 없습니다:', data);
}
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
}, [mapInstanceRef]);
useEffect(() => {
if (!mapRef.current) return;
if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단
// EPSG:5179 등록
proj4.defs([
[
'EPSG:5179',
'+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',
],
]);
register(proj4);
//맵 생성
const map = new OlMap({
layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']],
renderer: 'webgl',
target: mapRef.current,
interactions: defaults({ doubleClickZoom: false }),
controls: defaultControls({
zoom: false,
rotate: false,
attributionOptions: {
collapsible: false,
},
}).extend([]),
view: new View({
projection: GisApp.TO_PROJ,
zoom: gisCore.defaults.zoom,
center: olProj.transform(
[126.6757, 37.5585],
GisApp.FROM_PROJ,
GisApp.TO_PROJ
),
minZoom: gisCore.defaults.minZoom,
maxZoom: gisCore.defaults.maxZoom,
}),
});
gisMaps.current = map
//미니맵
if(isMiniMap){
const overviewMap = new OverviewMap({
layers: [GisApp.Module.core.baseMiniMapLayers['base']],
view: new View({
projection: GisApp.TO_PROJ,
minZoom: 9,
}),
collapsed: false,
collapseLabel: '\u00BB',
label: '\u00AB',
});
map.addControl(overviewMap);
}
gisCore.__proto__.map = map;
gisCore.scaleControl();
gisCore.changeBaseLayer('base');
setMapObject({ map: map });
mapInstanceRef.current = map; // 생성 완료 플래그 설정
}, []);
useEffect(() => {
if (!treeData || treeData.length === 0) return;
gisCore.deleteAllOverlay();
gisCore.deleteAllLayer();
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn');
GisApp.layerCode = layerCode;
//setTreeData(data);
const layerItems = getLayerItems(layerCode);
//setEditLayerItems(prev => [...layerItems])
}, [treeData]);
const getLayerItems = (layerCode) => {
const layerItems = Object.entries(layerCode)
.filter(([key, obj], idx) => obj?.updtPsblYn === 'Y')
.map(([key, obj]) => ({
...obj,
name: obj.lyrNm,
label: obj.lyrNm,
value: obj.lyrSn + '',
}))
.sort((a, b) => {
if (a.sortOrdr > b.sortOrdr) return 1;
if (a.sortOrdr < b.sortOrdr) return -1;
return 0;
});
return layerItems;
};
if (mapObject.map) {
try {
gisMap = mapObject.map;
} catch (e) {
console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
}
}
return (
<>
<GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}>
<div className="h-[calc(100vh-10rem)] flex relative">
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
<GisTree visible={tabVisible} setVisible={setTabVisible} />
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
{/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && featureData && !isSearchModalOpen && (
<DetailView
featureData={featureData}
onClose={closePopup} // 닫기 콜백 전달
/>
)}
<SearchView
open={isSearchModalOpen}
onOpenChange={setIsSearchModalOpen}
onFeatureSelect={onFeatureSelect}
/>
<GisLonLat/>
<GisToolBar/>
<GisMapToggle/>
<GisSelect onFeatureSelect={onFeatureSelect} />
</div>
{isSearch && (
<div className="absolute right-8 top-[305px] z-20">
<button
type="button"
onClick={() => setIsSearchModalOpen(true)}
className="overflow-hidden w-[40px] h-[40px] bg-white
border border-[#9aa7b8]
rounded
shadow-[0_0_5px_#999]
flex items-center justify-center
p-0
"
>
<Search size={24} className="text-gray-700" />
</button>
</div>
)}
</GisContext.Provider>
</>
);
}
export default React.memo(Gis);
@@ -0,0 +1,79 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import GisApp from './lib/gis.core';
import { useGisContext } from './Gis';
import GisLonLat from './GisLonLat';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import './lib/css/gis.edit.css';
import GisMapToggle from './GisMapToggle';
import { Search } from 'lucide-react';
export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) {
const { gisMap, gisCore } = useGisContext();
const [visibleComponents, setVisibleComponents] = useState({});
const addressResultLayer = useRef(null);
const [playShow, setPlayShow] = useState({ show: false });
//셀렉트 버튼 이벤트
// const clickFeature = useCallback(feature => {
// if (feature) {
// const layer = feature.getLayer();
// setVisibleComponents(prevState => ({
// ...prevState,
// }));
// }
// }, []);
const selectRslt = useCallback(feature => {
GisApp.Module.layer.highLightFeature(
[feature],
{
id: GisApp.hlLayerId,
zIndex: 10,
opacity: 1,
selectable: true,
},
{ width: 30 }
);
clickFeature(feature);
}, []);
useEffect(() => {
if (gisMap) {
setVisibleComponents(prevState => ({
...prevState,
}));
}
}, [gisMap]);
if (!gisMap) return null;
return (
<>
{/*<GisTree />*/}
{/* <GisMapToggle />
<GisLonLat />
<GisToolBar /> */}
{/* <GisSelect onFeatureSelect={onFeatureSelect} /> */}
{/* 검색 모달 */}
{isSearch && (
<div className="absolute right-8 top-[254px] z-20">
<button
type="button"
onClick={() => setIsSearchModalOpen(true)}
className="overflow-hidden w-[40px] h-[40px] bg-white
border border-[#9aa7b8]
rounded
shadow-[0_0_5px_#999]
flex items-center justify-center
p-0
"
>
<Search size={24} className="text-gray-700" />
</button>
</div>
)}
</>
);
}
@@ -0,0 +1,285 @@
import { FormField } from "../form";
import { SelectBox } from "../select";
import { Input } from "@common/components";
import { FormUi } from "../form_ui";
import { useCallback, useState } from "react";
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
import { SdmSearchForm } from "./sdmNameBox";
import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng";
type SelectDataType = {
text: string;
val: string;
};
interface FormSchema {
ctpvCd: string;
sggCd: string;
dmdCoNm: string;
dmdInfoPk: string;
daddr: string;
telno: string;
bznmTotNo?: string;
rgtr?: string;
dmdAplyPswd?: string;
}
interface SdmRegFormProps {
form: UseFormReturn<FormSchema>;
onCheckboxChange?: (checked: boolean) => void;
selectSidoData: SelectDataType[];
selectSigunData: SelectDataType[];
dataList: DemandCompType[];
rtnValue: (searchText: string, pageCnt: number) => void;
setIsDuplicate: (value: boolean) => void;
}
type FieldComponent = (field: ControllerRenderProps<FormSchema, keyof FormSchema>) => JSX.Element;
export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigunData, dataList, rtnValue, setIsDuplicate }: SdmRegFormProps) {
const [sidoValue, setSidoValue] = useState<string>('');
const [sigunValue, setSigunValue] = useState<string>('');
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([{ text: "전체", val: '0' }]);
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true);
const [orgBznmNo, setOrgBznmNo] = useState<string>('');
const initializeSigunData = useCallback((sidoCd: string) => {
if (sidoCd === '0') {
setSigunValue('0');
setFilteredSigunData([{ text: "전체", val: '0' }]);
} else {
const newFilteredData = [
{ text: "전체", val: '0' },
...selectSigunData.filter((item: SelectDataType) => item.val.substring(0, 2) === sidoCd)
];
setSigunValue('0');
setFilteredSigunData(newFilteredData);
}
}, [selectSigunData]);
const onSelectValue = useCallback((
val: string,
type: string,
field: ControllerRenderProps<FormSchema, keyof FormSchema> | undefined
) => {
switch (type) {
case "ctpvCd":
initializeSigunData(val);
setSidoValue(val);
field?.onChange(val);
form.setValue('dmdInfoPk', '');
form.setValue('bznmTotNo', '');
break;
case "sggCd":
setSigunValue(val);
field?.onChange(val);
form.setValue('dmdInfoPk', '');
form.setValue('bznmTotNo', '');
break;
}
}, [initializeSigunData, form]);
const handleCheckboxChange = useCallback((checked: boolean) => {
setIsBznmTotNoDisabled(checked);
onCheckboxChange?.(checked);
}, [onCheckboxChange]);
const setRtnValue = useCallback((searchText: string, pageCnt: number) => {
rtnValue(searchText, pageCnt);
}, [rtnValue]);
const setRtnData = useCallback((item: DemandCompType) => {
const rtnCompData = item;
form.setValue('dmdCoNm', rtnCompData?.dmdCoNm || '');
form.setValue('dmdInfoPk', rtnCompData?.dmdInfoPk.substring(0,10) || '');
form.setValue('bznmTotNo', rtnCompData?.dmdInfoPk.substring(10,14)==='0000' ? '' : rtnCompData?.dmdInfoPk.substring(10,14));
// 수요처 불러온 후 사업자번호를 바꿀경우를 대비함
setOrgBznmNo(rtnCompData?.dmdInfoPk);
const sidoCd = rtnCompData?.ctpvCd.trim() || '';
setSidoValue(sidoCd);
form.setValue('ctpvCd', sidoCd);
initializeSigunData(sidoCd);
setTimeout(() => {
const sigunCd = rtnCompData?.sggCd.trim() || '';
setSigunValue(sigunCd);
form.setValue('sggCd', sigunCd);
}, 100);
if(rtnCompData?.dmdInfoPk.substring(10,14) === '0000') {
handleCheckboxChange(true);
} else {
handleCheckboxChange(false);
}
form.setValue('daddr', rtnCompData?.daddr || '');
form.setValue('telno', rtnCompData?.telno || '');
// 선택을 했을 때 중복 상태를 true로 설정
setIsDuplicate(true);
}, [form, handleCheckboxChange, initializeSigunData, setIsDuplicate]);
const setRtnText = useCallback((text: string) => {
form.setValue('dmdCoNm', text);
}, [form]);
const bznNoChange = useCallback((e: React.ChangeEvent<HTMLInputElement>, noType: string) => {
const value = e.target.value;
if(noType === 'dmdInfoPk') {
if(orgBznmNo.substring(0,10) === value) {
setIsDuplicate(true);
} else {
setIsDuplicate(false);
}
form.setValue('dmdInfoPk', value);
} else {
if(orgBznmNo.substring(10,14) === value) {
setIsDuplicate(true);
} else {
setIsDuplicate(false);
}
form.setValue('bznmTotNo', value);
}
}, [form, orgBznmNo, setIsDuplicate]);
const formFields: Array<{
name: keyof FormSchema;
label: string;
component: FieldComponent;
}> = [
{
name: "ctpvCd",
label: "시도",
component: (field) => (
<SelectBox
selectClass={undefined}
label=""
selectArray={selectSidoData}
onSelectValue={(val) => onSelectValue(val.toString(), 'ctpvCd', field)}
className=""
value={sidoValue}
/>
)
},
{
name: "sggCd",
label: "시군구",
component: (field) => (
<SelectBox
selectClass={undefined}
label=""
selectArray={filteredSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), 'sggCd', field)}
className=""
value={sigunValue}
/>
)
},
{
name: "dmdCoNm",
label: "회사명",
component: (field) => (
<SdmSearchForm
rtnData={setRtnData}
label="수요처명"
initValue={''}
dataList={dataList}
rtnValue={setRtnValue}
rtnText={setRtnText}
{...field}
/>
)
},
{
name: "dmdInfoPk",
label: "사업자등록번호",
component: (field) => (
<Input
placeholder="사업자등록번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
{...field}
onChange={(e) => bznNoChange(e, 'dmdInfoPk')}
/>
)
},
{
name: "bznmTotNo",
label: "종사업자등록번호",
component: (field) => (
<FormUi
label="종사업자등록번호"
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
<Input
placeholder="여러사업장이 있을 경우 입력해주세요"
type="text"
className="!mt-5 !text-xl text-gray-600"
disabled={isBznmTotNoDisabled}
{...field}
onChange={(e) => bznNoChange(e, 'bznmTotNo')}
/>
</FormUi>
)
},
{
name: "daddr",
label: "사업자주소",
component: (field) => (
<Input
placeholder="주소를 입력하세요"
type="text"
className="!text-xl text-gray-600"
{...field}
/>
)
},
{
name: "telno",
label: "전화번호",
component: (field) => (
<Input
placeholder="전화번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
{...field}
/>
)
},
{
name: "dmdAplyPswd",
label: "패스워드",
component: (field) => (
<Input
placeholder="패스워드를 입력하세요"
type="password"
className="!text-xl text-gray-600"
{...field}
/>
)
}
];
return (
<>
{formFields.map(({ name, label, component }) => (
<FormField
key={String(name)}
control={form.control}
name={name}
render={({ field }) => (
name === 'bznmTotNo' ? component(field) : (
<FormUi label={label}>
{component(field)}
</FormUi>
)
)}
/>
))}
</>
);
}
@@ -0,0 +1,221 @@
"use client";
/* import { useDmStore } from "@/app/store"; */
import { SdmRegForm } from "./form";
import { Button } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
/* import { useRouter } from "next/navigation"; */
import { useCallback, useEffect, useState } from "react";
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
import { toast, Toaster } from "sonner";
/* import { Toaster, toast } from "react-hot-toast"; */
import * as z from "zod";
import type { SelectDataType } from "../search_input_select";
export type DemandCompType = {
dmdInfoPk : string | '';
bzmnNo : string | '';
dmdCoNm : string | '';
daddr : string | '';
telno : string | '';
rgtr : string | '';
useUsgCd : string | '';
ctpvCd : string | '';
sggCd : string | '';
dmdAplyPswd: string | '';
}
type thisProps = {
selectSidoData: SelectDataType[];
selectSigunData: SelectDataType[];
dataList: DemandCompType[];
rtnValue: (searchText: string, pageCnt: number) => void;
rtnInputData: (data: DemandCompType) => void;
bodyClassNm?: string;
}
const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => z.object({
ctpvCd: z.string().min(1, "시/도를 선택해주세요"),
sggCd: z.string().min(1, "시/군/구를 선택해주세요"),
dmdCoNm: z.string()
.min(1, "회사명을 입력해주세요")
.max(20, "회사명은 20자 이내로 입력해주세요"),
dmdInfoPk: z.string()
.min(1, "사업자등록번호를 입력해주세요")
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다")
.refine(() => isBznmTotNoDisabled ? !isDuplicate : true, "이미 등록된 사업자등록번호입니다"),
bznmTotNo: isBznmTotNoDisabled
? z.string().optional()
: z.string()
.length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다")
.refine(() => !isDuplicate, "이미 등록된 사업자등록번호입니다"),
daddr: z.string()
.min(1, "주소를 입력해주세요")
.min(6, "주소는 6자 이상 입력해주세요")
.max(40, "주소는 40자 이내로 입력해주세요"),
telno: z.string()
.min(8, "전화번호는 8자 이상 입력해주세요")
.max(12, "전화번호는 12자 이내로 입력해주세요")
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"),
rgtr: z.string().optional(),
dmdAplyPswd : z.string()
.min(1, "패스워드를 입력해주세요")
.min(8, "패스워드는 8자 이상 입력해주세요")
.max(16, "패스워드는 16자 이내로 입력해주세요"),
});
type FormSchema = ReturnType<typeof createFormSchema>;
export type FormData = z.infer<FormSchema>;
const defaultValues: FormData = {
ctpvCd: '',
sggCd: '',
dmdCoNm: '',
dmdInfoPk: '',
bznmTotNo: '',
daddr: '',
telno: '',
rgtr: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
dmdAplyPswd : '',
};
/**
* 수요처 등록 폼
* @param selectSidoData 시/도 데이터
* @param selectSigunData 시/군/구 데이터
* @param dataList 수요처 목록
* @param rtnValue 검색 결과(텍스트) 콜백
* @param rtnInputData 수요처 리스트 콜백
* @returns
*/
/**
* 페이지에서 작업
const {Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm} = useDmStore((state) => state);
const { selectSidoData, selectSigunData } = Common;
const { demandCompList } = Sdm;
// 공통데이터 (시도, 시군구 데이터 호출)
useEffect(() => {
initializeCommonData();
}, [initializeCommonData]);
// 컴포넌트에서 텍스트 검색 호출
const callback = useCallback((searchText: string, pageCnt: number) => {
searchText = searchText.trim();
if(searchText.length > 0) {
getDemandCompList(searchText, pageCnt);
}
}, [getDemandCompList]);
// 컴포넌트에서 수요처 등록 호출
const handleInsertSdmData = useCallback((data: DemandCompType) => {
insertSdmData(data);
}, [insertSdmData]);
*/
export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm}: thisProps) {
/* const { insertSdmData } = useDmStore((state) => state);
const router = useRouter(); */
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true);
const [isDuplicate, setIsDuplicate] = useState(false);
const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate);
const form = useForm<z.infer<FormSchema>>({
resolver: zodResolver(formSchema),
defaultValues,
mode: "onChange",
criteriaMode: "all",
shouldFocusError: false,
shouldUnregister: false
});
// 체크박스 상태가 변경될 때마다 bznmTotNo 필드의 에러를 초기화
useEffect(() => {
if (isBznmTotNoDisabled) {
form.clearErrors('bznmTotNo');
form.setValue('bznmTotNo', '');
}
}, [isBznmTotNoDisabled, form]);
const setRtnValue = useCallback((searchText: string, pageCnt: number) => {
rtnValue(searchText, pageCnt);
}, [rtnValue]);
const onSubmit: SubmitHandler<z.infer<FormSchema>> = async (data) => {
try {
// form 에러 체크
const formErrors = form.formState.errors;
if (Object.keys(formErrors).length > 0) {
const errorMessage = Object.values(formErrors)[0]?.message || '입력값을 확인해주세요.';
toast.error(errorMessage);
return;
}
const dmdInfoPk = data.dmdInfoPk + (data.bznmTotNo || '0000');
const formData = {
...data,
dmdInfoPk,
};
console.log(formData);
/* await insertSdmData(formData); */
rtnInputData(formData as DemandCompType);
const queryParams = new URLSearchParams({
dmdCoNm: data.dmdCoNm,
ctpvCd: data.ctpvCd,
sggCd: data.sggCd,
dmdInfoPk,
bznmTotNo: data.bznmTotNo || '',
daddr: data.daddr,
telno: data.telno,
});
//router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
toast.success('수요처가 성공적으로 등록되었습니다.');
} catch (error) {
console.error('Error submitting form:', error);
toast.error('수요처 등록에 실패했습니다.');
}
};
return (
<>
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 underline">
수요처등록
</h1>
</div>
<div className= {bodyClassNm?bodyClassNm:"!max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"}>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<SdmRegForm
form={form}
onCheckboxChange={setIsBznmTotNoDisabled}
selectSidoData={selectSidoData}
selectSigunData={selectSigunData}
dataList={dataList}
rtnValue={setRtnValue}
setIsDuplicate={setIsDuplicate}
/>
<div className="flex justify-center pt-4">
<Button
type="submit"
className="w-32 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
>
수요처등록
</Button>
</div>
</form>
</FormProvider>
</div>
<Toaster position="top-center" />
</>
);
}
@@ -0,0 +1,105 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng";
export type thisProps = {
label: string | undefined;
initValue: string | undefined;
dataList : DemandCompType[];
rtnValue : (searchText: string, pageCnt: number) => void;
rtnData : (item: DemandCompType) => void;
rtnText : (text: string) => void;
}
/**
* 수요처 검색 컴포넌트
* @param label 라벨
* @param initValue 초기값
* @param rtnValue 검색 결과 반환 함수
* @param dataList 검색할 때 넘기는 데이터 리스트
* @param rtnData 검색 결과 반환 함수
*/
export const SdmSearchForm = ({label=undefined, initValue=undefined, rtnValue=()=>{}, dataList=[], rtnData=()=>{}, rtnText=()=>{} } :thisProps) => {
const [isOpen, setIsOpen] = useState(false);
const [textValue, setTextValue] = useState(initValue || '');
const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setTextValue(value);
rtnValue(value, 10);
rtnText(value);
if (value) {
setIsOpen(true);
} else {
setIsOpen(false);
}
}, [rtnValue, rtnText]);
const handleSuggestionClick = useCallback((item: DemandCompType) => {
setTextValue(item.dmdCoNm);
rtnData(item);
setIsOpen(false);
}, [rtnData]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div ref={wrapperRef}>
<input
type="text"
value={textValue}
onChange={handleInputChange}
/* onFocus={() => setIsOpen(true)} */
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`${label}을(를) 입력하세요`}
/>
{isOpen && (
<div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto">
<div className="max-h-72">
{dataList && dataList.length > 0 ? dataList.map((item : DemandCompType, index : number) => (
<div
key={index}
className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"
onClick={() => handleSuggestionClick(item)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleSuggestionClick(item);
}
}}
>
{item.dmdInfoPk.substring(10,14) !== '0000' ? (
<>
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoPk.substring(0,10)}&nbsp;
<span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoPk.substring(10,14)}&nbsp;
</>
) : (
<>
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoPk.substring(0,10)}&nbsp;
</>
)}
<span className="float-right">{item.dmdCoNm}</span>
</div>
)) : (
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
검색 결과가 없습니다.
</div>
)}
</div>
</div>
)}
</div>
)
}
@@ -1,3 +1,5 @@
"use client"
import { Button } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
import { SearchFormLeft, type EnvPartSelectDataType } from "../search_form_left";
@@ -18,6 +20,7 @@ type SearchBaseType = {
initClick:() => void;
btnClass:string;
ref:React.RefObject<ReturnType>;
isAdmin:boolean;
}
export type ReturnType = {
partValue: string;
@@ -27,9 +30,10 @@ export type ReturnType = {
unitWaterShedValue: string;
middleRegionValue: string;
envPartValue: string;
sidoText: string;
sigunText: string;
}
function SearchBaseForm ({
leftChildren,
rightChildren,
@@ -44,11 +48,14 @@ function SearchBaseForm ({
queryClick,
excelClick,
initClick,
ref
ref,
isAdmin=false
}: SearchBaseType) {
const [partValue, setPartValue] = useState<string>("A");
const [sidoValue, setSidoValue] = useState<string>("0");
const [sigunValue, setSigunValue] = useState<string>("0");
const [sidoText, setSidoText] = useState<string>("");
const [sigunText, setSigunText] = useState<string>("");
const [regionValue, setRegionValue] = useState<string>("0");
const [untitWaterShedValue, setUntitWaterShedValue] = useState<string>("0");
const [middleRegionValue, setMiddleRegionValue] = useState<string>("0");
@@ -64,6 +71,8 @@ function SearchBaseForm ({
unitWaterShedValue: untitWaterShedValue,
middleRegionValue: middleRegionValue,
envPartValue: envPartValue,
sidoText: sidoText,
sigunText: sigunText,
});
}
const initVal = useCallback(() => {
@@ -84,6 +93,8 @@ function SearchBaseForm ({
unitWaterShedValue: untitWaterShedValue,
middleRegionValue: middleRegionValue,
envPartValue: envPartValue,
sidoText: sidoText,
sigunText: sigunText,
})
};
useEffect(() => {
@@ -96,8 +107,10 @@ function SearchBaseForm ({
customRef.unitWaterShedValue = untitWaterShedValue;
customRef.middleRegionValue = middleRegionValue;
customRef.envPartValue = envPartValue;
customRef.sidoText = sidoText;
customRef.sigunText = sigunText;
}
}, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]);
}, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, sidoText, sigunText, ref]);
return (
<div className="flex card card-border !mt-6">
<SearchFormLeft isShowPart={true}
@@ -116,7 +129,10 @@ function SearchBaseForm ({
setRtnEnvPartValue={setEnvPartValue}
setRtnEnvPartSidoValue={setSidoValue}
setRtnEnvPartSigunValue={setSigunValue}
setRtnSidoText={setSidoText}
setRtnSigunText={setSigunText}
resetTrigger={resetTrigger}
isAdmin={isAdmin}
>
{leftChildren}
</SearchFormLeft>
@@ -1,4 +1,6 @@
import React, { useEffect, useState } from 'react';
"use client"
import React, { useEffect, useState } from 'react';
import type { SelectDataType } from '../search_input_select';
import { SelectBox } from '../select';
@@ -38,7 +40,10 @@ type SearchFormLeftProps = {
setRtnEnvPartValue?: (value: string) => void;
setRtnEnvPartSidoValue?: (value: string) => void;
setRtnEnvPartSigunValue?: (value: string) => void;
setRtnSidoText?: (value: string) => void;
setRtnSigunText?: (value: string) => void;
resetTrigger?: boolean;
isAdmin?: boolean;
}
export type EnvPartSelectDataType = SelectDataType & {
@@ -51,12 +56,14 @@ const selectPartData:SelectDataType[] = [
{ text: "환경청별", val: 'G' },
];
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) {
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) {
/******************선택된 데이터******************/
const [partValue, setPartValue] = useState<string>('A');
//행정별 시도 시군구
const [sidoValue, setSidoValue] = useState<string>('0');
const [sigunValue, setSigunValue] = useState<string>('0');
const [sidoText, setSidoText] = useState<string>('');
const [sigunText, setSigunText] = useState<string>('');
//유역별
const [regionValue, setRegionValue] = useState<string>('0');
const [unitWaterShedValue, setUnitWaterShedValue] = useState<string>('0');
@@ -150,6 +157,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
}
const onSelectValue = (val: string, type: string) => {
let selectedSido;
let selectedSigun;
let selectedEnvPartSido;
let selectedEnvPartSigun;
switch(type) {
case 'part':
setPartValue(val);
@@ -178,9 +190,14 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
break;
case 'sido':
setSidoValue(val);
selectedSido = selectSidoData.find(item => item.val === val);
setSidoText(selectedSido?.text || '');
if (setRtnSidoValue) {
setRtnSidoValue(val);
}
if (setRtnSidoText) {
setRtnSidoText(selectedSido?.text || '');
}
initializeSigunData(val);
onSelectValue(selectSigunData[0]?.val || '0', 'sigun');
break;
@@ -189,6 +206,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
setRtnSigunValue(val);
}
setSigunValue(val);
selectedSigun = filteredSigunData.find(item => item.val === val);
if (setRtnSigunText) {
setRtnSigunText(selectedSigun?.text || '');
}
setSigunText(selectedSigun?.text || '');
break;
// 유역 세팅
@@ -226,6 +248,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
break;
case 'envPartSido' :
setSidoValue(val);
selectedEnvPartSido = filteredEnvPartSidoData.find(item => item.val === val);
setSidoText(selectedEnvPartSido?.text || '');
if (setRtnEnvPartSidoValue) {
setRtnEnvPartSidoValue(val);
}
@@ -234,6 +258,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
break;
case 'envPartSigun' :
setSigunValue(val);
selectedEnvPartSigun = filteredEnvPartSigunData.find(item => item.val === val);
setSigunText(selectedEnvPartSigun?.text || '');
if (setRtnEnvPartSigunValue) {
setRtnEnvPartSigunValue(val);
}
@@ -283,7 +309,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
return (
isShowPart ? (
<div className="grid grid-cols-12 gap-5 w-11/12 m-4">
<SelectBox className="col-span-3" label={'구분'} selectArray={selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/>
<SelectBox className="col-span-3" label={'구분'} selectArray={isAdmin ? selectPartData.slice(0, 1) : selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/>
{partValue === 'R' ? (
<>
<SelectBox className="col-span-3" label={'유역'} selectArray={selectRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/>
@@ -6,37 +6,41 @@ interface TabItem {
label: string;
}
interface UITabProps extends React.HTMLAttributes<HTMLDivElement> {
interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'dir'> {
defaultValue?: string;
tabList: TabItem[];
children: React.ReactNode;
onValueChange?: (value: string) => void;
}
const UITab = React.forwardRef<
const UITab = React.forwardRef<
HTMLDivElement,
UITabProps
>(({defaultValue, tabList, children, onValueChange, ...props}, ref)=>{
return (
<Tabs
defaultValue={defaultValue || tabList[0]?.value}
className={"w-full space-x-1 border-b border-gray-200"}
onValueChange={onValueChange}
{...props}
>
<TabsList className="flex space-x-1">
{tabList.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="py-2 px-4 text-sm font-medium border-b-2 focus:outline-none data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 data-[state=inactive]:border-transparent data-[state=inactive]:text-gray-500 hover:text-gray-700 hover:border-gray-300"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{children}
</Tabs>
)
})
>(({defaultValue, tabList, children, onValueChange, ...props}, ref) => {
return (
<Tabs
defaultValue={defaultValue || tabList[0]?.value}
className={"w-full space-x-1 border-b border-gray-200"}
onValueChange={onValueChange}
ref={ref}
{...props}
>
<TabsList className="flex space-x-1">
{tabList.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="py-2 px-4 text-sm font-medium border-b-2 focus:outline-none data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 data-[state=inactive]:border-transparent data-[state=inactive]:text-gray-500 hover:text-gray-700 hover:border-gray-300"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{children}
</Tabs>
);
});
UITab.displayName = 'UITab';
export { UITab };
@@ -1,3 +1,5 @@
"use client"
import { Table, TableBody, TableCell, TableHeader, TableRow } from "@common/components";
import { useEffect, useState } from "react";
import { HeadMakeColSpan } from "./HeadMakeColSpan";
@@ -8,7 +8,7 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
className={cn("mx-auto flex w-full justify-center text-lg mt-2", className)}
{...props}
/>
)
@@ -42,7 +42,7 @@ type PaginationLinkProps = {
const PaginationLink = ({
className,
isActive,
size = "icon",
size = "default",
...props
}: PaginationLinkProps) => (
<a
@@ -52,6 +52,7 @@ const PaginationLink = ({
variant: isActive ? "outline" : "ghost",
size,
}),
"px-4 py-2 text-lg",
className
)}
{...props}
@@ -66,10 +67,10 @@ const PaginationPrevious = ({
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
className={cn("gap-2 pl-4 text-lg", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<ChevronLeft className="h-6 w-6" />
<span>Previous</span>
</PaginationLink>
)
@@ -82,11 +83,11 @@ const PaginationNext = ({
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
className={cn("gap-2 pr-4 text-lg", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
<ChevronRight className="h-6 w-6" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
@@ -97,10 +98,10 @@ const PaginationEllipsis = ({
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
className={cn("flex h-12 w-12 items-center justify-center text-lg", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<MoreHorizontal className="h-6 w-6" />
<span className="sr-only">More pages</span>
</span>
)
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
return (
<input
className={cn(
"flex h-[3.2rem] w-full rounded-md border border-input bg-form px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-placeholder focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex h-[3.2rem] w-full rounded-md border border-input bg-form px-3 py-2 text-xl ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-placeholder focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-xl",
className,
)}
ref={ref}