Merge remote-tracking branch 'origin/iipbs/common/main' into iipbs-sn-map
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { useEffect, useState } from "react";
|
||||
import Gis, { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { mapService } from "@service";
|
||||
import SearchModal from '@/map/SearchModal';
|
||||
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();
|
||||
@@ -23,9 +28,85 @@ const Map = () => {
|
||||
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} searchModal={SearchModal}/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client"
|
||||
import { Bar, BarChart, CartesianGrid, Legend, XAxis, YAxis } from "recharts";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
type ChartConfig
|
||||
} from "./lib/chart";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
const customStyles = {
|
||||
legendContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: "20px",
|
||||
fontSize: "9px",
|
||||
fontWeight : "bold",
|
||||
marginTop: "10px",
|
||||
} as CSSProperties,
|
||||
};
|
||||
|
||||
type BarChartProps = {
|
||||
chartConfig: ChartConfig | undefined;
|
||||
chartData: any[] | undefined;
|
||||
chartTitle: string;
|
||||
chartClassName?: string;
|
||||
graphSize?: number;
|
||||
yFieldText?: string;
|
||||
showLegend?: boolean;
|
||||
}
|
||||
|
||||
// 커스텀 툴팁
|
||||
const CustomTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
label
|
||||
}: {
|
||||
active?: boolean;
|
||||
payload?: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}>;
|
||||
label?: string;
|
||||
}) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
|
||||
<p className="text-[12px] font-bold m-3">{label}</p>
|
||||
{payload.map((item, index) => (
|
||||
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
{/* 색상 박스 추가 (범례 색상과 동일) */}
|
||||
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
|
||||
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가)
|
||||
const CustomLegend = (props: any) => {
|
||||
const { payload } = props;
|
||||
return (
|
||||
<div style={customStyles.legendContainer}>
|
||||
{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 as {color: string}).color }}></div>
|
||||
<span className="text-[12px]">{(entry as {value: string}).value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function DoubleBarChart({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());
|
||||
};
|
||||
console.log(chartData);
|
||||
const barMaxValue = getMaxValue(chartData, 'barVal');
|
||||
return (
|
||||
<>
|
||||
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
|
||||
<div className="text-left text-[13px] font-bold mt-[13px] mb-[10px]">
|
||||
{chartTitle}
|
||||
</div>): null}
|
||||
<div className="flex justify-center">
|
||||
<ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
tickLine={true}
|
||||
tickMargin={10}
|
||||
axisLine={true}
|
||||
fontSize={13}
|
||||
fontWeight={500}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={true}
|
||||
tickMargin={10}
|
||||
tickCount={6}
|
||||
axisLine={true}
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
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 && (
|
||||
Object.keys(chartData[0])
|
||||
.filter(key => key !== "label")
|
||||
.map((key, idx) => (
|
||||
<Bar
|
||||
dataKey={key}
|
||||
radius={5}
|
||||
key={key}
|
||||
stackId="a"
|
||||
barSize={graphSize}
|
||||
fill={colors[idx]}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{showLegend && <Legend content={CustomLegend}/>}
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import { Pie, PieChart } from "recharts";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent
|
||||
} from "./lib/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig
|
||||
} from "./lib/chart";
|
||||
import { ChartContainer, ChartTooltip } from "./lib/chart";
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default (props: { chartConfig: ChartConfig; chartData: never[] | undefined; chartTitle:string}) =>{
|
||||
type ChartDataItem = {
|
||||
label: string;
|
||||
val: number;
|
||||
};
|
||||
|
||||
type ChartConfigItem = {
|
||||
label: string;
|
||||
fill: string;
|
||||
};
|
||||
|
||||
type ChartConfig = {
|
||||
[key: string]: ChartConfigItem;
|
||||
};
|
||||
|
||||
type TooltipPayloadItem = {
|
||||
name: string;
|
||||
value: number;
|
||||
payload: {
|
||||
label: string;
|
||||
val: number;
|
||||
fill: string;
|
||||
};
|
||||
};
|
||||
|
||||
type LegendEntry = {
|
||||
value: string;
|
||||
fill: string;
|
||||
type?: string;
|
||||
payload: {
|
||||
label: string;
|
||||
val: number;
|
||||
fill: string;
|
||||
};
|
||||
};
|
||||
|
||||
type LegendProps = {
|
||||
payload?: LegendEntry[];
|
||||
};
|
||||
|
||||
/** 차트 기본 스타일 */
|
||||
const customStyles = {
|
||||
legendContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: "20px",
|
||||
fontSize: "9px",
|
||||
fontWeight: "bold",
|
||||
marginTop: "10px",
|
||||
} as CSSProperties,
|
||||
};
|
||||
|
||||
// 커스텀 툴팁
|
||||
const CustomTooltip = ({ active, payload, label }: {
|
||||
active?: boolean;
|
||||
payload?: TooltipPayloadItem[];
|
||||
label?: string
|
||||
}) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
|
||||
<p className="text-[12px] font-bold m-3">{label}</p>
|
||||
{payload.map((item, index) => (
|
||||
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<div style={{ width: "10px", height: "10px", backgroundColor: item.payload.fill, borderRadius: "50%" }}></div>
|
||||
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 커스텀 범례
|
||||
const CustomLegend = (props: LegendProps) => {
|
||||
const { payload } = props;
|
||||
if (!payload) return null;
|
||||
|
||||
return (
|
||||
<div style={customStyles.legendContainer}>
|
||||
{payload.map((entry: LegendEntry, index: number) => (
|
||||
<div key={`legend-${index}`} className="flex items-center gap-2">
|
||||
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.fill }}></div>
|
||||
<span className="text-[12px]">{entry.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function DoughnutChart(props: {
|
||||
chartConfig: ChartConfig;
|
||||
chartData: ChartDataItem[] | undefined;
|
||||
chartTitle: string
|
||||
}) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardContent className="flex-1 pb-0">
|
||||
{props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? (
|
||||
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
|
||||
{props.chartTitle}
|
||||
</div>): null}
|
||||
{props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? (
|
||||
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
|
||||
{props.chartTitle}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="h-[calc(100%-42px)] justify-center">
|
||||
<ChartContainer
|
||||
config={props.chartConfig}
|
||||
@@ -30,18 +119,22 @@ export default (props: { chartConfig: ChartConfig; chartData: never[] | undefine
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
content={<CustomTooltip />}
|
||||
/>
|
||||
<Pie
|
||||
data={props.chartData}
|
||||
data={props.chartData?.map(item => ({
|
||||
...item,
|
||||
fill: props.chartConfig[item.label]?.fill || 'blue'
|
||||
}))}
|
||||
dataKey="val"
|
||||
nameKey="label"
|
||||
innerRadius={60}
|
||||
/>
|
||||
<CustomLegend />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,9 +92,6 @@ export default function LineBarchart({
|
||||
return <div>데이터가 없습니다.</div>;
|
||||
}
|
||||
|
||||
const barConfig = chartConfig?.barVal;
|
||||
const lineConfig = chartConfig?.lineVal;
|
||||
|
||||
// 데이터의 최대값 계산
|
||||
const getMaxValue = (data: any[], keyPrefix: string) => {
|
||||
return Math.max(...data.map(item =>
|
||||
|
||||
@@ -5,6 +5,7 @@ import DoughnutChart from './DoughnutChart';
|
||||
import { type ChartConfig } from './lib/chart';
|
||||
import LineChart from './LineChart';
|
||||
import LineBarchart from './LineBarChart';
|
||||
import DoubleBarChart from './DoubleBar';
|
||||
|
||||
/**차트 FORM */
|
||||
const CHART_FORM = {
|
||||
@@ -15,7 +16,9 @@ const CHART_FORM = {
|
||||
/**도넛 차트 형태*/
|
||||
DN :'daughnut',
|
||||
/**LINE,BAR차트 */
|
||||
LINEBAR : 'linebar'
|
||||
LINEBAR : 'linebar',
|
||||
/**더블바 차트 형태*/
|
||||
DOUBLEBAR : 'doublebar'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,7 +37,7 @@ const CHART_FORM = {
|
||||
|
||||
type MainChartProps = {
|
||||
chartForm: string;
|
||||
chartConfig: any;
|
||||
chartConfig: ChartConfig;
|
||||
chartData: any;
|
||||
chartTitle: string;
|
||||
chartClassName?: string;
|
||||
@@ -47,23 +50,23 @@ type MainChartProps = {
|
||||
function MainChart(props: MainChartProps) {
|
||||
if( props.chartForm == CHART_FORM.BAR ){
|
||||
return (
|
||||
<BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}>
|
||||
</BarChart>
|
||||
<BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/>
|
||||
);
|
||||
} else if( props.chartForm == CHART_FORM.LINE ){
|
||||
return (
|
||||
<LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}>
|
||||
</LineChart>
|
||||
<LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/>
|
||||
);
|
||||
} else if( props.chartForm == CHART_FORM.DN ){
|
||||
return (
|
||||
<DoughnutChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}>
|
||||
</DoughnutChart>
|
||||
<DoughnutChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}/>
|
||||
);
|
||||
} else if (props.chartForm == CHART_FORM.LINEBAR){
|
||||
return (
|
||||
<LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}>
|
||||
</LineBarchart>
|
||||
<LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}/>
|
||||
);
|
||||
} else if (props.chartForm == CHART_FORM.DOUBLEBAR){
|
||||
return (
|
||||
<DoubleBarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,9 +303,7 @@ export const FileUploaderItem = forwardRef<
|
||||
|
||||
>
|
||||
<div className="font-medium leading-none tracking-tight flex items-center gap-1.5 h-full !w-4/5 overflow-hidden "
|
||||
onClick={() =>{
|
||||
|
||||
|
||||
onClick={() =>{onFileClick?.(index)
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { FormControl, FormItem, FormLabel, FormMessage } from "../form";
|
||||
|
||||
export const BZNM_TOT_NO = '종사업자등록번호';
|
||||
@@ -17,7 +17,11 @@ interface FormElementProps extends React.HTMLAttributes<HTMLElement> {
|
||||
|
||||
const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
|
||||
({ children, label, onCheckboxChange, isDisabled }, ref) => {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(isDisabled);
|
||||
|
||||
useEffect(() => {
|
||||
setIsChecked(isDisabled);
|
||||
}, [isDisabled]);
|
||||
|
||||
const handleCheckboxChange = useCallback((checked: boolean) => {
|
||||
setIsChecked(checked);
|
||||
@@ -50,7 +54,7 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
|
||||
<FormControl>
|
||||
{React.isValidElement(children) && isDisabled
|
||||
? React.cloneElement(children, {
|
||||
disabled: true,
|
||||
disabled: isDisabled,
|
||||
...(children.props as FormElementProps)
|
||||
})
|
||||
: children}
|
||||
|
||||
@@ -19,4 +19,4 @@ export * from "./tab";
|
||||
export * from "./table";
|
||||
export * from "./table_div";
|
||||
export * from "./title";
|
||||
|
||||
export * from "./sdmInsertForm";
|
||||
@@ -9,19 +9,23 @@ import proj4 from 'proj4';
|
||||
import { register } from 'ol/proj/proj4';
|
||||
import GisApp from './lib/gis.core';
|
||||
import GisContent from './GisContent';
|
||||
import FeatureClickEventPopup from '@/map/featureClickEventPopup';
|
||||
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({ treeData = [], isSearch = false, searchModal : SearchModal }) {
|
||||
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 [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([]);
|
||||
@@ -70,21 +74,6 @@ function Gis({ treeData = [], isSearch = false, searchModal : SearchModal }) {
|
||||
if (geometry) {
|
||||
let extent = geometry.getExtent();
|
||||
|
||||
// // EPSG:4326 좌표계로 추정되는 경우(값이 위도/경도 범위)
|
||||
// console.log('before extent', extent);
|
||||
// const is4326 =
|
||||
// extent[0] >= -90 && extent[0] <= 90 &&
|
||||
// extent[1] >= -180 && extent[1] <= 180 &&
|
||||
// extent[2] >= -90 && extent[2] <= 90 &&
|
||||
// extent[3] >= -180 && extent[3] <= 180;
|
||||
// if (is4326) {
|
||||
// // extent: [위도, 경도, 위도, 경도] → [경도, 위도, 경도, 위도]로 순서 변경
|
||||
// const reorderedExtent = [extent[1], extent[0], extent[3], extent[2]];
|
||||
// // EPSG:4326 extent를 EPSG:3857로 변환
|
||||
// extent = olProj.transformExtent(reorderedExtent, 'EPSG:4326', 'EPSG:3857');
|
||||
// console.log('after extent', extent);
|
||||
// }
|
||||
|
||||
gisMap.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
@@ -142,21 +131,24 @@ function Gis({ treeData = [], isSearch = false, searchModal : SearchModal }) {
|
||||
}),
|
||||
});
|
||||
|
||||
gisMaps.current = map
|
||||
|
||||
//미니맵
|
||||
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',
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
map.addControl(overviewMap);
|
||||
gisCore.__proto__.map = map;
|
||||
|
||||
gisCore.scaleControl();
|
||||
gisCore.changeBaseLayer('base');
|
||||
setMapObject({ map: map });
|
||||
@@ -206,25 +198,46 @@ function Gis({ treeData = [], isSearch = false, searchModal : SearchModal }) {
|
||||
<>
|
||||
<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} />
|
||||
|
||||
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
|
||||
|
||||
{/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
|
||||
{isOpen && featureData && !isSearchModalOpen && (
|
||||
<FeatureClickEventPopup
|
||||
<DetailView
|
||||
featureData={featureData}
|
||||
onClose={closePopup} // 닫기 콜백 전달
|
||||
/>
|
||||
)}
|
||||
<SearchModal
|
||||
<SearchView
|
||||
open={isSearchModalOpen}
|
||||
onOpenChange={setIsSearchModalOpen}
|
||||
onFeatureSelect={handleFeatureSelect}
|
||||
onFeatureSelect={onFeatureSelect}
|
||||
/>
|
||||
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
|
||||
{/* <GisLonLat/>
|
||||
<GisLonLat/>
|
||||
<GisToolBar/>
|
||||
<GisMapToggle/> */}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -51,10 +51,10 @@ export default function GisContent({ edit = false, onFeatureSelect, isSearch, se
|
||||
return (
|
||||
<>
|
||||
{/*<GisTree />*/}
|
||||
<GisMapToggle />
|
||||
{/* <GisMapToggle />
|
||||
<GisLonLat />
|
||||
<GisToolBar />
|
||||
<GisSelect onFeatureSelect={onFeatureSelect} />
|
||||
<GisToolBar /> */}
|
||||
{/* <GisSelect onFeatureSelect={onFeatureSelect} /> */}
|
||||
|
||||
{/* 검색 모달 */}
|
||||
{isSearch && (
|
||||
|
||||
@@ -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)}
|
||||
<span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoPk.substring(10,14)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoPk.substring(0,10)}
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type SearchBaseType = {
|
||||
initClick:() => void;
|
||||
btnClass:string;
|
||||
ref:React.RefObject<ReturnType>;
|
||||
isAdmin:boolean;
|
||||
}
|
||||
export type ReturnType = {
|
||||
partValue: string;
|
||||
@@ -29,9 +30,10 @@ export type ReturnType = {
|
||||
unitWaterShedValue: string;
|
||||
middleRegionValue: string;
|
||||
envPartValue: string;
|
||||
sidoText: string;
|
||||
sigunText: string;
|
||||
}
|
||||
|
||||
|
||||
function SearchBaseForm ({
|
||||
leftChildren,
|
||||
rightChildren,
|
||||
@@ -46,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");
|
||||
@@ -66,6 +71,8 @@ function SearchBaseForm ({
|
||||
unitWaterShedValue: untitWaterShedValue,
|
||||
middleRegionValue: middleRegionValue,
|
||||
envPartValue: envPartValue,
|
||||
sidoText: sidoText,
|
||||
sigunText: sigunText,
|
||||
});
|
||||
}
|
||||
const initVal = useCallback(() => {
|
||||
@@ -86,6 +93,8 @@ function SearchBaseForm ({
|
||||
unitWaterShedValue: untitWaterShedValue,
|
||||
middleRegionValue: middleRegionValue,
|
||||
envPartValue: envPartValue,
|
||||
sidoText: sidoText,
|
||||
sigunText: sigunText,
|
||||
})
|
||||
};
|
||||
useEffect(() => {
|
||||
@@ -98,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}
|
||||
@@ -118,7 +129,10 @@ function SearchBaseForm ({
|
||||
setRtnEnvPartValue={setEnvPartValue}
|
||||
setRtnEnvPartSidoValue={setSidoValue}
|
||||
setRtnEnvPartSigunValue={setSigunValue}
|
||||
setRtnSidoText={setSidoText}
|
||||
setRtnSigunText={setSigunText}
|
||||
resetTrigger={resetTrigger}
|
||||
isAdmin={isAdmin}
|
||||
>
|
||||
{leftChildren}
|
||||
</SearchFormLeft>
|
||||
|
||||
@@ -40,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 & {
|
||||
@@ -53,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');
|
||||
@@ -152,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);
|
||||
@@ -180,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;
|
||||
@@ -191,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;
|
||||
|
||||
// 유역 세팅
|
||||
@@ -228,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);
|
||||
}
|
||||
@@ -236,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);
|
||||
}
|
||||
@@ -285,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}/>
|
||||
|
||||
@@ -44,11 +44,11 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
|
||||
<TableRow key={rowIndex+ " headerRow"} className="bg-gray-50">
|
||||
{upHeadList
|
||||
.filter((item) => item.visible != false)
|
||||
.map((head: TableUpperProps) => {
|
||||
.map((head: TableUpperProps, idx) => {
|
||||
if (head.row === rowIndex) {
|
||||
return (
|
||||
<TableHead
|
||||
key={head.id}
|
||||
key={head.id?head.id:rowIndex+"_"+idx}
|
||||
colSpan={head.colSpan || 1}
|
||||
rowSpan={head.rowSpan || 1}
|
||||
style={{ whiteSpace: 'pre-line' }}
|
||||
|
||||
@@ -29,7 +29,6 @@ type UiHeadProps = {
|
||||
export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false, boldArray = [] }: UiTableProps) {
|
||||
const [cur, setCur] = useState<number>(1);
|
||||
const [idx, setIdx] = useState<number>(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetPageNo) {
|
||||
setCur(1);
|
||||
|
||||
Reference in New Issue
Block a user