diff --git a/packages/common/business_components/src/ui/chart/DoubleBar.tsx b/packages/common/business_components/src/ui/chart/DoubleBar.tsx new file mode 100644 index 000000000..9627485aa --- /dev/null +++ b/packages/common/business_components/src/ui/chart/DoubleBar.tsx @@ -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 ( +
+

{label}

+ {payload.map((item, index) => ( +
+ {/* 색상 박스 추가 (범례 색상과 동일) */} +
+ {` ${item.name} : ${item.value}`} +
+ ))} +
+ ); + } + return null; +}; + +// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가) +const CustomLegend = (props: any) => { + const { payload } = props; + return ( +
+ {payload.map((entry: unknown, index: number) => ( +
+
+ {(entry as {value: string}).value} +
+ ))} +
+ ); +}; + +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" ? ( +
+ {chartTitle} +
): null} +
+ + + + + + }/> + {chartData && chartData.length > 0 && ( + Object.keys(chartData[0]) + .filter(key => key !== "label") + .map((key, idx) => ( + + )) + )} + {showLegend && } + + +
+ + ) +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx index f88b35476..d666f1d14 100644 --- a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx +++ b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx @@ -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 ( +
+

{label}

+ {payload.map((item, index) => ( +
+
+ {` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`} +
+ ))} +
+ ); + } + return null; +}; + +// 커스텀 범례 +const CustomLegend = (props: LegendProps) => { + const { payload } = props; + if (!payload) return null; + + return ( +
+ {payload.map((entry: LegendEntry, index: number) => ( +
+
+ {entry.value} +
+ ))} +
+ ); +}; + +export default function DoughnutChart(props: { + chartConfig: ChartConfig; + chartData: ChartDataItem[] | undefined; + chartTitle: string +}) { return ( - {props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? ( -
- {props.chartTitle} -
): null} + {props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? ( +
+ {props.chartTitle} +
+ ) : null}
} + content={} /> ({ + ...item, + fill: props.chartConfig[item.label]?.fill || 'blue' + }))} dataKey="val" nameKey="label" innerRadius={60} /> +
- ) + ); } diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index 2e83458d8..8c7202104 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -92,9 +92,6 @@ export default function LineBarchart({ return
데이터가 없습니다.
; } - const barConfig = chartConfig?.barVal; - const lineConfig = chartConfig?.lineVal; - // 데이터의 최대값 계산 const getMaxValue = (data: any[], keyPrefix: string) => { return Math.max(...data.map(item => diff --git a/packages/common/business_components/src/ui/chart/index.tsx b/packages/common/business_components/src/ui/chart/index.tsx index 0d17a99b1..4c58549cc 100644 --- a/packages/common/business_components/src/ui/chart/index.tsx +++ b/packages/common/business_components/src/ui/chart/index.tsx @@ -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' }; /** @@ -61,6 +64,10 @@ function MainChart(props: MainChartProps) { return ( ); + } else if (props.chartForm == CHART_FORM.DOUBLEBAR){ + return ( + + ); } } diff --git a/packages/common/business_components/src/ui/form_ui/index.tsx b/packages/common/business_components/src/ui/form_ui/index.tsx index 96214b6cf..27b74bd97 100644 --- a/packages/common/business_components/src/ui/form_ui/index.tsx +++ b/packages/common/business_components/src/ui/form_ui/index.tsx @@ -39,7 +39,7 @@ const FormUi = React.forwardRef( checked={isChecked} onCheckedChange={handleCheckboxChange} /> - + {children} diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index c5c0ee0db..6ccaac946 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -2,10 +2,43 @@ import { SelectBox } from "../select"; import { Input } from "@common/components"; import { FormUi } from "../form_ui"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; import { SdmSearchForm } from "./sdmNameBox"; -import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; +import { set } from "date-fns"; + +// 수요처 정보 +export type DemandCompType = { + index : number | ''; + dmdInfoPk : string | ''; + dmdInfoFk : string | ''; + bzmnNo : string | ''; + dmdCoNm : string | ''; + daddr : string | ''; + telno : string | ''; + rgtr : string | ''; + useUsgCd : string | ''; + ctpvCd : string | ''; + sggCd : string | ''; + dmdAplyPswd : string | ''; + wntUntprc : number | ''; + dmndTmng : Date | ''; + dmndQty : number | ''; + grssClfg : number | ''; + tn : number | ''; + combResdch : number | ''; + tph : number | ''; + trbt : number | ''; + ph : number | ''; + blgcOxygDmndQnt : number | ''; + clIonQnt : number | ''; + smll : number | ''; + cdtnv : number | ''; + crmty : number | ''; + rmrk : string | ''; + reqDt : string | ''; +} + type SelectDataType = { text: string; @@ -32,16 +65,19 @@ interface SdmRegFormProps { dataList: DemandCompType[]; rtnValue: (searchText: string, pageCnt: number) => void; setIsDuplicate: (value: boolean) => void; + updateList?:DemandCompType } type FieldComponent = (field: ControllerRenderProps) => JSX.Element; -export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigunData, dataList, rtnValue, setIsDuplicate }: SdmRegFormProps) { +export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigunData, dataList, rtnValue, setIsDuplicate ,updateList}: SdmRegFormProps) { const [sidoValue, setSidoValue] = useState(''); const [sigunValue, setSigunValue] = useState(''); const [filteredSigunData, setFilteredSigunData] = useState([{ text: "전체", val: '0' }]); const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); const [orgBznmNo, setOrgBznmNo] = useState(''); + const [updateCoNm, setUpdateNm] = useState('') + const initializeSigunData = useCallback((sidoCd: string) => { if (sidoCd === '0') { @@ -62,6 +98,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun type: string, field: ControllerRenderProps | undefined ) => { + if(!updateList?.dmdInfoPk) switch (type) { case "ctpvCd": initializeSigunData(val); @@ -77,7 +114,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun form.setValue('bznmTotNo', ''); break; } - }, [initializeSigunData, form]); + }, [initializeSigunData, form,updateList]); const handleCheckboxChange = useCallback((checked: boolean) => { setIsBznmTotNoDisabled(checked); @@ -90,6 +127,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun 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)); @@ -124,26 +162,46 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun form.setValue('dmdCoNm', text); }, [form]); - const bznNoChange = useCallback((e: React.ChangeEvent, noType: string) => { + const bznNoChange = useCallback((e: React.ChangeEvent, noType: string, field?: ControllerRenderProps) => { const value = e.target.value; - + if(noType === 'dmdInfoPk') { if(orgBznmNo.substring(0,10) === value) { setIsDuplicate(true); } else { setIsDuplicate(false); } - form.setValue('dmdInfoPk', value); + field?.onChange(value); } else { if(orgBznmNo.substring(10,14) === value) { setIsDuplicate(true); } else { setIsDuplicate(false); } - form.setValue('bznmTotNo', value); + field?.onChange(value); } + }, [orgBznmNo, setIsDuplicate]); - }, [form, orgBznmNo, setIsDuplicate]); + + useEffect(() => { + if(updateList?.dmdInfoPk){ + form.reset(updateList) + if(updateList?.dmdInfoPk.slice(10,14) !== '0000') { + handleCheckboxChange( false); + form.setValue('bznmTotNo', updateList?.dmdInfoPk.slice(10,14)); + } else { + handleCheckboxChange( true); + } + form.setValue('dmdInfoPk', updateList?.dmdInfoPk.slice(0,10)); + setUpdateNm(updateList?.dmdCoNm) + // setSigunValue(updateList?.sggCd) + }else{ + handleCheckboxChange( true); + } + }, [updateList,form]); + + useEffect(() => { + },[form.formState.errors]) const formFields: Array<{ name: keyof FormSchema; @@ -154,28 +212,51 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun name: "ctpvCd", label: "시도", component: (field) => ( - {field.onChange(e.target.value)} } + value={ selectSidoData.find(item => item.val === field.value?.trim() )?.text??''} + readOnly={!!updateList?.dmdInfoPk} + /> + ) : ( + onSelectValue(val.toString(), 'ctpvCd', field)} + onSelectValue={(val) => onSelectValue(val.toString(), 'ctpvCd', field??'')} className="" value={sidoValue} /> + ) ) }, { name: "sggCd", label: "시군구", component: (field) => ( + updateList?.dmdInfoPk ? ( + {field.onChange(e.target.value)} } + value={selectSigunData.find(item => item.val === field.value?.trim() )?.text??''} + readOnly={!!updateList?.dmdInfoPk} + /> + ) : ( onSelectValue(val.toString(), 'sggCd', field)} className="" value={sigunValue} /> + + ) ) }, { @@ -185,7 +266,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun bznNoChange(e, 'dmdInfoPk')} + onChange={(e) => bznNoChange(e, 'dmdInfoPk', field)} + value={field.value} + readOnly={!!updateList?.dmdInfoPk} /> ) }, @@ -220,8 +302,10 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun type="text" className="!mt-5 !text-xl text-gray-600" disabled={isBznmTotNoDisabled} - {...field} - onChange={(e) => bznNoChange(e, 'bznmTotNo')} + onChange={(e) => bznNoChange(e, 'bznmTotNo', field)} + // {...field} + value={ field.value} + readOnly={!!updateList?.dmdInfoPk} /> ) @@ -250,6 +334,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun /> ) }, + { name: "dmdAplyPswd", label: "패스워드", @@ -262,11 +347,18 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun /> ) } - ]; + ] return ( <> - {formFields.map(({ name, label, component }) => ( + {formFields.filter((item)=> + {if(updateList?.dmdInfoPk){ + return item.name !== 'dmdAplyPswd' + }else{ + return item + } + }) + .map(({ name, label, component }) => ( void; rtnInputData: (data: DemandCompType) => void; bodyClassNm?: string; + updateList?:DemandCompType } -const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => z.object({ +const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,updateList?:DemandCompType) => z.object({ ctpvCd: z.string().min(1, "시/도를 선택해주세요"), sggCd: z.string().min(1, "시/군/구를 선택해주세요"), dmdCoNm: z.string() @@ -57,19 +40,19 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => .max(40, "주소는 40자 이내로 입력해주세요"), telno: z.string() .min(8, "전화번호는 8자 이상 입력해주세요") - .max(12, "전화번호는 12자 이내로 입력해주세요") + .max(11, "전화번호는 11자 이내로 입력해주세요") .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), rgtr: z.string().optional(), - dmdAplyPswd : z.string() - .min(1, "패스워드를 입력해주세요") - .min(8, "패스워드는 8자 이상 입력해주세요") - .max(16, "패스워드는 16자 이내로 입력해주세요"), + dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우 + ? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음 + : z.string().optional() + .refine((val) => !val || (val.length >= 8 && val.length <= 16), "패스워드는 8자 이상 16자 이내로 입력해주세요"), + }); -type FormSchema = ReturnType; -export type FormData = z.infer; +type FormSchema = z.infer>; -const defaultValues: FormData = { +const defaultValues: FormSchema = { ctpvCd: '', sggCd: '', dmdCoNm: '', @@ -78,7 +61,7 @@ const defaultValues: FormData = { daddr: '', telno: '', rgtr: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - dmdAplyPswd : '', + dmdAplyPswd: '', }; /** @@ -117,15 +100,15 @@ const defaultValues: FormData = { }, [insertSdmData]); */ -export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm}: thisProps) { +export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm, updateList}: thisProps) { /* const { insertSdmData } = useDmStore((state) => state); const router = useRouter(); */ - const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false); const [isDuplicate, setIsDuplicate] = useState(false); - const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate); + const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate, updateList ); - const form = useForm>({ + const form = useForm({ resolver: zodResolver(formSchema), defaultValues, mode: "onChange", @@ -136,7 +119,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList // 체크박스 상태가 변경될 때마다 bznmTotNo 필드의 에러를 초기화 useEffect(() => { - if (isBznmTotNoDisabled) { + if (isBznmTotNoDisabled===true) { form.clearErrors('bznmTotNo'); form.setValue('bznmTotNo', ''); } @@ -146,7 +129,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList rtnValue(searchText, pageCnt); }, [rtnValue]); - const onSubmit: SubmitHandler> = async (data) => { + const onSubmit: SubmitHandler = async (data) => { try { // form 에러 체크 const formErrors = form.formState.errors; @@ -160,27 +143,18 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList const formData = { ...data, dmdInfoPk, + bzmnNo: data.dmdInfoPk, }; + + console.log(' formData ', formData) - 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('수요처 등록에 실패했습니다.'); } }; @@ -203,6 +177,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList dataList={dataList} rtnValue={setRtnValue} setIsDuplicate={setIsDuplicate} + updateList={updateList} />