Merge branch 'iipbs/common/main' into iipbs/sn/ii

This commit is contained in:
swcho
2025-07-10 10:04:54 +09:00
9 changed files with 114 additions and 38 deletions
@@ -74,17 +74,25 @@ const CustomLegend = (props: any) => {
);
};
// chartData의 모든 값 중 최대값 구하기
const getGlobalMax = (data: Record<string, number>[]) => {
if (!data || data.length === 0) return 0;
return Math.max(
...data.flatMap(item =>
Object.entries(item)
.filter(([key]) => key !== 'label')
.map(([, value]) => value as number)
)
);
};
export default function Chart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) {
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#FED16A", "#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');
const globalMax = getGlobalMax(chartData);
const tickCount = 5;
// interval을 globalMax / tickCount로 계산
const interval = globalMax / tickCount;
const ticks = Array.from({ length: tickCount + 1 }, (_, i) => Math.round(interval * i));
return (
<>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
@@ -93,8 +101,8 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle='
</div>): null}
<div className="flex justify-center">
<ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<BarChart accessibilityLayer data={chartData} className="!h-[88%]">
<CartesianGrid vertical={false} className="!h-[85%]" />
<XAxis
dataKey="label"
tickLine={true}
@@ -106,12 +114,12 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle='
<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}
domain={[0, globalMax]}
ticks={ticks}
label={yFieldText ? { value: yFieldText, className:"text-[15px] font-bold", position: 'insideLeft', angle: -90, offset: -30 } : undefined}
/>
<ChartTooltip cursor={false} content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />}/>
{chartData && chartData.length > 0 && (
@@ -0,0 +1,46 @@
import "../date_picker/date-picker.css";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import type { DayPickerSingleProps } from "react-day-picker";
import { cn } from "../../lib/utils";
import { Button } from "@common/components/ui";
import { Calendar } from "@common/components/ui";
import { Popover, PopoverContent, PopoverTrigger } from "@common/components/ui";
type DatePickerRangeProps = {
placeholder?: string;
disabled?: boolean | ((date: Date) => boolean);
} & DayPickerSingleProps;
export function DatePickerRange({
className,
placeholder = "날짜 선택",
disabled,
...props
}: DatePickerRangeProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("date-picker-trigger", className)}
disabled={typeof disabled === "boolean" ? disabled : false}
>
<CalendarIcon className="!size-[1.6rem]" />
{props.selected ? (
format(props.selected, "yyyy-MM-dd")
) : (
<span>{placeholder}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar initialFocus classNames={{ cell: "rounded-md" }} {...props}
disabled={typeof disabled === "function" ? disabled : undefined}
/>
</PopoverContent>
</Popover>
);
}
@@ -21,4 +21,5 @@ export * from "./table_div";
export * from "./title";
export * from "./sdmInsertForm";
export * from "./popover_ui";
export * from "./date_month_picker";
export * from "./date_month_picker";
export * from "./date_picker_range"
@@ -30,6 +30,7 @@ function Gis({
setIsOpen,
setFeatureData,
featureData,
imgVisble = false,
searchView: SearchView,
detailView: DetailView,
onFeatureSelect,
@@ -46,7 +47,7 @@ function Gis({
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
const mapInstanceRef = useRef(null);
const checkedLayerIds = useRef([]);
const layerNmRef = useRef('')
// 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가
// const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]);
@@ -84,12 +85,32 @@ function Gis({
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
let featureId
const layerName = layerNmRef.current
if (layerName === '하수재이용시설') {
featureId = data.searchPk;
} else if (layerName === '빗물이용시설'|| layerName == '중수도시설') {
featureId = data.bdstInfoPk;
} else if (layerName === '하수처리시설') {
featureId = data.bscFctCd;
} else if (layerName === '온배수시설' ) {
featureId = data.bdstRuseFcltPk;
} else {
console.warn('해당 레이어에 대한 featureId 매핑 없음', layerNm);
featureId = data.fid || data.id || data.ftrIdn;
}
gisMap.getLayers().getArray().forEach(layer => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
if (source?.getFeatures) {
const allFeatures = source.getFeatures();
for (const f of allFeatures) {
const id = String(f.getId());
// console.log('id', id, featureId)
if (id && id.split('.')[1] === String(featureId)) {
foundFeature = f;
break;
}
}
}
});
}
@@ -360,11 +381,13 @@ function Gis({
setVisible={setTabVisible}
onCheckedChange={handleCheckedLayersChange}
onLayerIconClick={(nodeNm) => {
layerNmRef.current = nodeNm
setLayerNm(nodeNm); setIsOpen(true); const dummyFeature = {
getProperties: () => ({}), // 최소 구조
};
onFeatureSelect(dummyFeature);
}}
imageVisible={imgVisble}
/>
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
@@ -375,6 +398,7 @@ function Gis({
featureData={featureData || { getProperties: () => ({}) }}
seletectedLayerNm={layerNm}
onClose={closePopup} // 닫기 콜백 전달
onFeatureSelect={handleFeatureSelect}
/>
)}
{/* SearchView가 존재할 때만 렌더링 */}
@@ -10,7 +10,7 @@ import { DndProvider } from "react-dnd";
import { useGisContext } from './Gis';
import GisApp from './lib/gis.core';
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick }) {
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick,imageVisible=false }) {
const { gisCore, treeData } = useGisContext();
const [checked, setChecked] = useState([]);
const [expanded, setExpanded] = useState([]);
@@ -188,9 +188,9 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
image.src = canvas.toDataURL('image/png');
return image;
};
const handleLayerImgClick = (nodeNm) => {
console.log('click!', nodeNm)
onLayerIconClick(nodeNm)
const handleLayerImgClick = (nodeNm, nodeId) => {
if(!checked.includes(nodeId)) alert('시설을 선택해주세요')
else onLayerIconClick(nodeNm)
}
if (!treeData || treeData.length === 0) return null;
@@ -224,7 +224,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
<input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer" />
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">{node.text}</span>
{depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={()=>{handleLayerImgClick(node.text)}} alt={node.text}></img></span>}
{imageVisible ? depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={()=>{handleLayerImgClick(node.text, node.id)}} alt={node.text}></img></span> : null}
</div>
)}
sort={(a, b) => {
@@ -234,6 +234,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun
}, [updateList,form]);
useEffect(() => {
console.log( 'form.formState.errors ', form.formState.errors)
},[form.formState.errors])
const formFields: Array<{
@@ -42,12 +42,14 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up
.min(8, "전화번호는 8자 이상 입력해주세요")
.max(11, "전화번호는 11자 이내로 입력해주세요")
.regex(/^[0-9]+$/, "전화번호는 숫자만 입력 가능합니다"),
rgtr: z.string().optional(),
rgtr: z.string().nullish(),
dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우
? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음
: z.string().optional()
.refine((val) => !val || (val.length >= 8 && val.length <= 16), "패스워드는 8자 이상 16자 이내로 입력해주세요"),
dmdAplyPswdConfirm: z.string()
dmdAplyPswdConfirm: updateList?.dmdInfoPk // 업데이트일경우
? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음
:z.string()
.min(1, "패스워드확인을 입력해주세요"),
}).refine((data) => {
// 패스워드 확인 검증
@@ -301,7 +301,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
resetAllValues();
// 리셋 후 트리거를 false로 설정
if (setRtnPartValue) {
setRtnPartValue('0');
setRtnPartValue('A');
}
}
}, [resetTrigger]);
@@ -2,7 +2,7 @@ import "./date-picker.css";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import type { DayPicker } from "react-day-picker";
import type { DayPickerSingleProps } from "react-day-picker";
import { cn } from "../../lib/utils";
import { Button } from "../button";
@@ -12,15 +12,12 @@ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
type DatePickerProps = {
placeholder?: string;
disabled?: boolean;
selected?: Date;
zIndex?: number; // z-index prop 추가
} & React.ComponentProps<typeof DayPicker>;
} & DayPickerSingleProps;
export function DatePicker({
className,
placeholder = "날짜 선택",
disabled,
zIndex, // zIndex prop 받기
...props
}: DatePickerProps) {
return (
@@ -39,11 +36,8 @@ export function DatePicker({
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
style={{ zIndex: zIndex || 'auto' }}
>
<Calendar classNames={{ cell: "rounded-md" }} {...props} />
<PopoverContent className="w-auto p-0">
<Calendar initialFocus classNames={{ cell: "rounded-md" }} {...props} />
</PopoverContent>
</Popover>
);