Compare commits

...
Author SHA1 Message Date
leedj312 ca9f2da83c 수요정보 등록 수질 입력 부분 validation check 2025-08-01 14:38:38 +09:00
leedj312 e54a0d0ef8 수요신청현황 이상/이하 작업 완료 2025-08-01 11:37:23 +09:00
leedj312 82e29ec4e2 Merge commit '6808430c9ae74890d0f0e84f20a0ffe5409de157' into iipbs/dm/dev 2025-08-01 09:43:41 +09:00
jyha00 96a50e4f5a 수요처 로그인 비밀번호 초기화 요청 cursor-pointer 추가 2025-07-31 20:01:25 +09:00
jyha00 96448b6427 수요처 로그인 시 비밀번호 초기화 요청 추가 2025-07-31 17:50:05 +09:00
swcho 6808430c9a feat: UITable 컴포넌트의 index props를 통해 외부에서 내부 select idx를 제어할 수 있도록 변경 2025-07-31 16:09:36 +09:00
leedj312 0ef6aea502 Merge commit '33ad044ed34fe78bfe832c09c1128385a3fc285e' into iipbs/dm/dev 2025-07-31 15:42:36 +09:00
jyha00 33ad044ed3 대민 수정사항 반영 2025-07-31 15:01:36 +09:00
leedj312 c94b33b1b8 대민 신청현황 매칭된 공급처 보이도록 수정 2025-07-31 11:05:05 +09:00
leedj312 886561535e 대민 수요신청 정보 변경 2025-07-30 17:57:03 +09:00
Your Name dad8b9925e monthpicker enableMonth string[] 파라미터 추가
(선택가능한 월 배열 추가하면됨 ex.['4월', '5월'])
2025-07-28 11:12:27 +09:00
swcho 07c7947da1 fix: gis.layer.js 하수펌프장 CQL 필터 적용 안되는 에러 수정 2025-07-28 10:51:37 +09:00
swcho 35e1203168 fix: monthPicker.tsx 현재 월에 hover 효과가 지속되는 코드 주석 처리 2025-07-28 10:34:35 +09:00
leedj312 ebde6d433a 대민 수요처 등록후 매인으로 이동 2025-07-28 10:26:40 +09:00
yschoi 556d3f33b6 Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2025-07-28 10:19:05 +09:00
yschoi f826f0fb3f env 2025-07-28 10:18:49 +09:00
leedj312 11010099b4 저장시 원래 화면으로 이동 2025-07-28 10:11:55 +09:00
yschoi 17886b0f9d Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev 2025-07-28 09:46:53 +09:00
yschoi 46c166a87c 개발 서버 2025-07-28 09:46:00 +09:00
yschoi 5b96245e7d 빌드오류 수정 2025-07-28 09:25:54 +09:00
yschoi 6ba78d0923 빌드 오류 수정 2025-07-28 08:56:16 +09:00
yschoi 15b6520ee1 if (typeof window !== "undefined") {
tokenManager.setToken("site-id-key", 'DM');
      tokenManager.setToken("site-theme-key", 'green');
    }
2025-07-28 08:04:58 +09:00
yschoi ed53ad249c 후보지 선택 버튼 삭제 2025-07-28 07:52:40 +09:00
yschoi b67d1261ff 로컬 환경변수 2025-07-28 07:50:19 +09:00
yschoi cfde5d695f 환경변수 2025-07-28 07:35:02 +09:00
yschoi 13ce985fe5 1 2025-07-28 06:38:50 +09:00
yschoi 3e634fa575 개발 환경변수 수정 2025-07-28 06:29:44 +09:00
mind-one005 bec3132417 SelectBox labelClass props 추가 2025-07-27 17:13:43 +09:00
Your Name e7a46ac3a2 년월 캘린더 년 readonly 옵션추가 (not necessary) 2025-07-27 14:43:42 +09:00
23 changed files with 1197 additions and 588 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
NEXT_PUBLIC_SITE_ID = DM
NEXT_PUBLIC_PORT = 3000
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_PORT = 11035
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
+2 -2
View File
@@ -3,9 +3,9 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_PORT = 11035
NEXT_PUBLIC_PORT = 3005
#NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000/
NEXT_PUBLIC_PROXY_HOST = http://localhost:3005/
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
@@ -7,7 +7,7 @@ import ListDiv from '@common/business_components/ui/list_div';
import { useEffect, useState } from 'react';
import { Toaster } from 'react-hot-toast';
import SdmLogin from '../sdmLogin/page';
import type { DemandCompType } from '@/app/store/sdmMng';
import type { DemandCompBizType } from '@/app/store/sdmMng';
import TransValueFormat from '@/app/common/transValueFormat';
import { useRouter } from 'next/navigation';
@@ -31,8 +31,8 @@ export default function SdmApplyList({}: SdmApplyProps) {
const {selectSidoData, selectSigunData} = Common;
const [isLogin, setIsLogin] = useState(false);
const [isLoginStorageInfo, setIsLoginStorageInfo] = useState(false);
const [sdmApplyDetail, setSdmApplyDetail] = useState<DemandCompType>({} as DemandCompType);
const [transformedList, setTransformedList] = useState<DemandCompType[]>([]);
const [sdmApplyDetail, setSdmApplyDetail] = useState<DemandCompBizType>({} as DemandCompBizType);
const [transformedList, setTransformedList] = useState<DemandCompBizType[]>([]);
useEffect(() => {
console.log('sessionStorage : ',sessionStorage.getItem('user'));
@@ -60,10 +60,10 @@ export default function SdmApplyList({}: SdmApplyProps) {
// 데이터 변환을 위한 별도의 useEffect
useEffect(() => {
if (sdmInfoStatusList.length > 0) {
const newTransformedList = sdmInfoStatusList.map((item: DemandCompType) => ({
const newTransformedList = sdmInfoStatusList.map((item: DemandCompBizType) => ({
...item,
ctpvCd: selectSidoData.find(sido => sido.val === item.ctpvCd?.trim())?.text || '',
sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || ''
sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || '',
}));
setTransformedList(newTransformedList);
}
@@ -73,6 +73,8 @@ export default function SdmApplyList({}: SdmApplyProps) {
if (index !== undefined) {
const selectedStatus = transformedList[index];
if (selectedStatus) {
setSdmApplyDetail(selectedStatus);
}
}
@@ -121,33 +123,33 @@ export default function SdmApplyList({}: SdmApplyProps) {
</TableRow>
<TableRow>
<TableCell colSpan={1} className={TABLE_STYLES.header}>총대장균군수(개/100ml)</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.grssClfg} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{ sdmApplyDetail?.grssClfg!=-1?<TransValueFormat value={sdmApplyDetail?.grssClfg ?? ''} format="number"/>:''} {sdmApplyDetail?.grssClfg?sdmApplyDetail.grssClfg!=-1? '이하':'불검출':'-'} </TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>결합잔류염소(mg/L)​</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch ?? ''} format="number"/>} {sdmApplyDetail?.combResdch?'이하':'-'}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>탁도(NTU)</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt ?? ''} format="number"/>} {sdmApplyDetail?.trbt?'이하':'-'}</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={1} className={TABLE_STYLES.header}>생물학적 산소요구량(BOD)(mg/L)</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt ?? ''} format="number"/>} {sdmApplyDetail?.blgcOxygDmndQnt?'이하':'-'}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>냄새</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>} {sdmApplyDetail?.smll?'이하':'-'}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>색도(도)</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty ?? ''} format="number"/>} {sdmApplyDetail?.crmty?'이하':'-'}</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={1} className={TABLE_STYLES.header}>총질소(T-N)(mg/L)</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn ?? ''} format="number"/>} {sdmApplyDetail?.tn?'이하':'-'}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>총인(T-P)(mg/L)​</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph ?? ''} format="number"/>} {sdmApplyDetail?.tph?'이하':'-'}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>수소이온농도(pH)​</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.ph} format="number"/>}</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={1} className={TABLE_STYLES.header}>염화물(mgCL/L)​</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt ?? ''} format="number"/>} {sdmApplyDetail?.clIonQnt?'이하':'-'}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>전기전도도(㎲/cm)​</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv} format="number"/>}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv ?? ''} format="number"/>} {sdmApplyDetail?.cdtnv?'이하':'-'}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>비고</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.rmrk}</TableCell>
</TableRow>
@@ -156,11 +158,11 @@ export default function SdmApplyList({}: SdmApplyProps) {
</TableRow>
<TableRow>
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지1</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{ sdmApplyDetail?.splyCndtList?.[0]?.ruseUtztnFcltNm ?? ""}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지2</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.splyCndtList?.[1]?.ruseUtztnFcltNm ?? ""}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지3</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.splyCndtList?.[2]?.ruseUtztnFcltNm ?? ""}</TableCell>
</TableRow>
</TableBody>
</Table>
@@ -177,7 +179,7 @@ export default function SdmApplyList({}: SdmApplyProps) {
<TableCell colSpan={1} className={TABLE_STYLES.header}>진행상태</TableCell>
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.status?.toString()}</TableCell>
<TableCell colSpan={1} className={TABLE_STYLES.header}>공급처매칭결과</TableCell>
<TableCell colSpan={2} className={TABLE_STYLES.cell}></TableCell>
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.ruseUtztnFcltNm??''}</TableCell>
</TableRow>
</TableBody>
</Table>
@@ -1,7 +1,7 @@
"use client";
import { useDmStore } from "@/app/store";
import type { DemandCompType } from "@/app/store/sdmMng";
import { sdmInfoFormSchema, type DemandCompBizType, type SdmInfoFormData } from "@/app/store/sdmMng";
import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component";
import { UiModal } from "@common/business_components/ui/dialog_ui";
import { Button } from "@common/components/ui";
@@ -10,227 +10,206 @@ import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import * as z from "zod";
import SdmLogin from "../sdmLogin/page";
const sdmInfoFormSchema = z.object({
sdmNm: z.string()
.min(1, "수요처를 선택해주세요"),
demandYmd: z.date().optional(),
demandQnty: z.string()
.min(1, "요구수량을 입력해주세요")
.regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"),
usePurpose: z.string().optional(),
money: z.string()
.regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
/**요구수질 */
reqWaterQlty: z.string().optional(),
grssClfg: z.string()
.min(1, "총대장균군수를 입력해주세요")
.regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"),
tn: z.string()
.min(1, "총질소를 입력해주세요")
.regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"),
combResdch: z.string()
.min(1, "결합잔류염소를 입력해주세요")
.regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"),
tph: z.string()
.min(1, "총인산물을 입력해주세요")
.regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"),
trbt: z.string()
.min(1, "탁도를 입력해주세요")
.regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"),
ph: z.string()
.min(1, "수소이온농도를 입력해주세요")
.regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"),
bod: z.string()
.min(1, "생물학적 산소요구량을 입력해주세요")
.regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"),
/**컬럼확인필요 */
clIonQnt: z.string()
.min(1, "염화물을 입력해주세요")
.regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"),
smll: z.string()
.min(1, "냄새를 입력해주세요")
.regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"),
cdtnv: z.string()
.min(1, "전기전도도를 입력해주세요")
.regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"),
crmty: z.string()
.min(1, "색도를 입력해주세요")
.regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"),
rmrk: z.string().optional(),
/* telno: z.string()
.min(8, "전화번호는 8자 이상 입력해주세요")
.max(12, "전화번호는 12자 이내로 입력해주세요")
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
candidateTitle: z.array(z.object({
ruseUtztnFcltNm: z.string(),
ruseUtztnFcltCd: z.string(),
})).optional(),
});
export type FormData = z.infer<typeof sdmInfoFormSchema>;
function SdmInfoInsertContent() {
const searchParams = useSearchParams();
const {insertSdmInfoData, Sdm ,setSubsInfo} = useDmStore((state) => state);
const { insertSdmInfoData, Sdm, setSubsInfo } = useDmStore((state) => state);
const { successLogin } = Sdm;
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
const formSchema = sdmInfoFormSchema;
const [isLogin, setIsLogin] = useState(false);
const [facilityName, setFacilityName] = useState<string>('');
const [defaultValues, setDefaultValues] = useState<FormData>({
sdmNm: '',
const [facilityName, setFacilityName] = useState<string>("");
const [defaultValues, setDefaultValues] = useState<SdmInfoFormData>({
sdmNm: "",
demandYmd: new Date(),
demandQnty: '',
usePurpose: '',
money: '',
demandQnty: "",
usePurpose: "",
money: "",
/**요구수질 */
reqWaterQlty: '',
grssClfg: '',
tn: '',
combResdch: '',
tph: '',
trbt: '',
ph: '',
bod: '',
reqWaterQlty: "",
grssClfg: null,
tn: null,
combResdch: null,
tph: null,
trbt: null,
ph: null,
bod: null,
/**컬럼확인필요 */
clIonQnt: '',
smll: '',
cdtnv: '',
crmty: '',
rmrk: '',
clIonQnt: null,
smll: "",
cdtnv: null,
crmty: null,
rmrk: "",
/** 후보지 제목 */
candidateTitle: [
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
],
});
useEffect(() => {
const user = sessionStorage.getItem('user');
setDefaultValues(prev => ({
const user = sessionStorage.getItem("user");
setDefaultValues((prev) => ({
...prev,
sdmNm: searchParams.get('dmdCoNm') || '',
sdmNm: searchParams.get("dmdCoNm") || "",
}));
sessionStorage.setItem('user', searchParams.get('dmdInfoId') || '');
setIsLogin(user || searchParams.get('dmdInfoId') ? true : false);
sessionStorage.setItem("user", searchParams.get("dmdInfoId") || "");
setIsLogin(user || searchParams.get("dmdInfoId") ? true : false);
}, [searchParams]);
useEffect(() => {
const local = localStorage.getItem('user');
const local = localStorage.getItem("user");
if (local) {
setIsLogin(true);
}
setSubsInfo([]); // subsInfo 초기화 후보지 처음 화면 들어올때 초기화(스토어내 객체만)
}, [setSubsInfo]);
const form = useForm<FormData>({
const form = useForm<SdmInfoFormData>({
resolver: zodResolver(formSchema),
defaultValues,
mode: "onChange",
mode: "onSubmit",
});
const subsInfo = useDmStore((state) => state.map.subsInfo);
useEffect(()=>{
useEffect(() => {
//map.subsInfo 스토어 객체 값이 변경되면 해당 값이 폼에 자동으로 들어가도록 수정 대신 값이 있어야함
if(subsInfo.length>0){
form.setValue('candidateTitle', subsInfo)
if (subsInfo.length > 0) {
form.setValue("candidateTitle", subsInfo);
}
}, [subsInfo, form]);
},[subsInfo, form])
//후보지 팝업 값 셋팅
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === "SET_SUBS_INFO") {
setSubsInfo(event.data.payload); // zustand로 값 설정
}
};
//후보지 팝업 값 셋팅
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'SET_SUBS_INFO') {
setSubsInfo(event.data.payload); // zustand로 값 설정
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [setSubsInfo]);
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [setSubsInfo]);
// 로그인 성공 시 모달 닫기 및 폼 업데이트
useEffect(() => {
if (successLogin) {
setIsLogin(true);
const user = sessionStorage.getItem('user');
const user = sessionStorage.getItem("user");
if (user) {
try {
const userData = JSON.parse(user);
if (userData.dmdCoNm) {
setFacilityName(userData.dmdCoNm);
form.setValue('sdmNm', userData.dmdCoNm);
form.setValue("sdmNm", userData.dmdCoNm);
}
} catch (error) {
console.error('Error parsing user data:', error);
console.error("Error parsing user data:", error);
}
}
}
}, [successLogin, form]);
useEffect(() => {
console.log("Form state changed:", form.formState.errors);
}, [form.formState.errors]);
// 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화
useEffect(() => {
if (ismoneyDisabled) {
form.clearErrors('money');
form.setValue('money', '');
form.clearErrors("money");
form.setValue("money", "");
}
}, [ismoneyDisabled, form]);
const router = useRouter();
const onSubmit: SubmitHandler<FormData> = async (data) => {
const router = useRouter();
const onSubmit: SubmitHandler<SdmInfoFormData> = async (data) => {
try {
// Transform form data to match API schema
const apiData: DemandCompType = {
dmdInfoId: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
dmdInfoFk: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm,
ctpvCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.ctpvCd?.toString() || searchParams.get('ctpvCd') || '',
sggCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.sggCd?.toString() || searchParams.get('sggCd') || '',
daddr: JSON.parse(sessionStorage.getItem('user') || '{}')?.daddr?.toString() || searchParams.get('daddr') || '',
telno: JSON.parse(sessionStorage.getItem('user') || '{}')?.telno?.toString() || searchParams.get('telno') || '',
rgtr: JSON.parse(sessionStorage.getItem('user') || '{}')?.rgtr?.toString() || searchParams.get('rgtr') || '',
useUsgCd: data.usePurpose || '',
wntUntprc: Number(data.money) || '',
const dmSubsInfo = data.candidateTitle?.filter(
(item) => item.ruseUtztnFcltNm && item.ruseUtztnFcltCd
);
// Transform form data to madata.candidateTitletch API schema
const apiData: DemandCompBizType = {
dmdInfoId:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.dmdInfoId?.toString() ||
searchParams.get("dmdInfoId") + (searchParams.get("bznmTotNo") || ""),
dmdInfoFk:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.dmdInfoId?.toString() ||
searchParams.get("dmdInfoId") + (searchParams.get("bznmTotNo") || ""),
dmdCoNm:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.dmdCoNm?.toString() || data.sdmNm,
ctpvCd:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.ctpvCd?.toString() ||
searchParams.get("ctpvCd") ||
"",
sggCd:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.sggCd?.toString() ||
searchParams.get("sggCd") ||
"",
daddr:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.daddr?.toString() ||
searchParams.get("daddr") ||
"",
telno:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.telno?.toString() ||
searchParams.get("telno") ||
"",
rgtr:
JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.rgtr?.toString() ||
searchParams.get("rgtr") ||
"",
useUsgCd: data.usePurpose || "",
wntUntprc: Number(data.money) || "",
dmndTmng: data.demandYmd || new Date(),
dmndQty: Number(data.demandQnty) || '',
grssClfg: Number(data.grssClfg) || '',
tn: Number(data.tn) || '',
combResdch: Number(data.combResdch) || '',
tph: Number(data.tph) || '',
trbt: Number(data.trbt) || '',
ph: Number(data.ph) || '',
blgcOxygDmndQnt: Number(data.bod) || '',
clIonQnt: Number(data.clIonQnt) || '',
smll: Number(data.smll) || '',
cdtnv: Number(data.cdtnv) || '',
crmty: Number(data.crmty) || '',
rmrk: data.rmrk || '',
dmndQty: Number(data.demandQnty) || "",
grssClfg: data.grssClfg === null ? -1 : Number(data.grssClfg),
tn: Number(data.tn) || null,
combResdch: Number(data.combResdch) || null,
tph: Number(data.tph) || null,
trbt: Number(data.trbt) || null,
ph: data.ph || "",
blgcOxygDmndQnt: Number(data.bod) ||null,
clIonQnt: Number(data.clIonQnt) || null,
smll: data.smll || "",
cdtnv: Number(data.cdtnv) ||null,
crmty: Number(data.crmty) || null,
rmrk: data.rmrk || "",
bzmnNo: "",
dmdAplyPswd: "",
index: 0,
reqDt: "",
dmdInfoSn: 0,
status: '',
splyCndtList: data.candidateTitle || [],
status: "",
splyCndtList: dmSubsInfo || [],
};
await insertSdmInfoData(apiData);
setTimeout(() => {
router.push("/");
}, 1000); // 0.8초 후 이동
} catch (error) {
console.error('Error submitting form:', error);
console.error("Error submitting form:", error);
}
};
@@ -253,8 +232,10 @@ const router = useRouter();
/>
<div className="flex justify-center pt-4">
<Button
color="sky"
size={"sm"}
type="submit"
className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
className="w-40 font-semibold py-2 px-4 rounded-md "
>
수요정보 등록
</Button>
@@ -264,7 +245,7 @@ const router = useRouter();
</div>
</div>
{!isLogin ?
{!isLogin ? (
<UiModal
body={<SdmLogin />}
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
@@ -277,7 +258,7 @@ const router = useRouter();
noHeaderYn="Y"
noFooterYn="Y"
/>
: null}
) : null}
<Toaster position="top-center" />
</div>
);
@@ -290,4 +271,3 @@ export default function MyForm() {
</Suspense>
);
}
@@ -1,8 +1,8 @@
"use client";
import { useDmStore } from "@/app/store";
import type { DemandCompType } from "@/app/store/sdmMng";
import SdmInsertForm from '@common/business_components/ui/sdmInsertForm';
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef } from "react";
import { Toaster } from "react-hot-toast";
@@ -44,15 +44,9 @@ export default function MyForm() {
const handleInsertSdmData = useCallback((data: import('@common/business_components/ui/sdmInsertForm/form').DemandCompType) => {
const convertedData: DemandCompType = {
...data,
dmdInfoSn: '',
dmdInfoFk: '',
status: '',
splyCndtList: []
};
formDataRef.current = convertedData;
insertSdmData(convertedData).catch(error => {
console.error('Error inserting SDM data:', error);
});
insertSdmData(convertedData)
}, [insertSdmData]);
return (
@@ -70,4 +64,3 @@ export default function MyForm() {
</div>
);
}
@@ -131,7 +131,7 @@ export default function WrAboutPage() {
물재이용 필요성
</h2>
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card2-desc" className="font-NotoSansKR text-[#ED5534] text-5xl font-semibold mb-8">물 재이용, 왜 필요한가요?</h1>
<h1 id="card2-desc" className="font-NotoSansKR text-[#D63F1F] text-5xl font-semibold mb-8">물 재이용, 왜 필요한가요?</h1>
<p className="font-NotoSansKR text-2xl font-normal mb-10 leading-relaxed">
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
@@ -237,7 +237,7 @@ export default function WrAboutPage() {
하수 재이용
</h2>
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card5-desc" className="font-NotoSansKR leading-snug text-[#0099A4] text-5xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
<h1 id="card5-desc" className="font-NotoSansKR leading-snug text-[#00818A] text-5xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
<Image
src="/img/wrAbout/img_05_1.png"
@@ -1,7 +1,7 @@
'use client';
import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
interface RegionData {
name: string;
@@ -213,7 +213,7 @@ const WrStatusPage: React.FC = () => {
{ region: '제주특별자치도', sewage: '8(1,666)', rainwater: '47(5,746)', reclaimed: '64(660)', thermal: '0' },
];
const regionData: RegionData[] = [
const regionData: RegionData[] = useMemo(() => [
{ name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] },
@@ -231,7 +231,7 @@ const WrStatusPage: React.FC = () => {
{ name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] },
];
], []);
const handleMarkerClick = (regionId: string) => {
if (selectedRegion === regionId) {
@@ -242,7 +242,7 @@ const WrStatusPage: React.FC = () => {
};
const createDonutMarker = (svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
const createDonutMarker = useCallback((svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
const radius = 26;
const innerRadius = 18;
@@ -334,7 +334,7 @@ const WrStatusPage: React.FC = () => {
});
svg.appendChild(g);
};
}, [handleMarkerClick, regionIdMap]);
useEffect(() => {
const loadSVG = async () => {
@@ -374,7 +374,7 @@ const WrStatusPage: React.FC = () => {
};
loadSVG();
}, []);
}, [createDonutMarker, regionData]);
useEffect(() => {
const svg = svgContainerRef.current?.querySelector('svg');
+3
View File
@@ -8,6 +8,9 @@ export default function RootLayout({
}>) {
return (
<html lang="ko">
<head>
<title>물재이용 대민</title>
</head>
<body>
{children}
</body>
+262 -23
View File
@@ -2,12 +2,108 @@
import type { SelectDataType } from "@common/business_components/ui";
import type { TableUpperProps } from "../common";
import toast from "react-hot-toast";
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
import { z } from "zod";
/**====================================
* Store - 타입
=====================================*/
export const sdmInfoFormSchema = z.object({
sdmNm: z.string().min(1, "수요처를 선택해주세요"),
demandYmd: z.date().optional(),
demandQnty: z
.string()
.min(1, "요구수량을 입력해주세요")
.regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"),
usePurpose: z.string().optional(),
money: z.string().regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
/**요구수질 */
reqWaterQlty: z.string().optional(),
grssClfg: z.number().nullish(),
tn: z.number().nullish(),
combResdch: z.number().nullish(),
tph: z.number().nullish(),
trbt: z.number().nullish(),
ph: z.string().nullish(),
bod: z.number().nullish(),
/**컬럼확인필요 */
clIonQnt: z.number().nullish(),
smll: z.string(),
cdtnv: z.number().nullish(),
crmty: z.number().nullish(),
rmrk: z.string().optional(),
/* telno: z.string()
.min(8, "전화번호는 8자 이상 입력해주세요")
.max(12, "전화번호는 12자 이내로 입력해주세요")
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
candidateTitle: z
.array(
z.object({
ruseUtztnFcltNm: z.string(),
ruseUtztnFcltCd: z.string(),
})
)
.optional(),
}).superRefine((data, ctx) => {
const disabledFields = disableRules[data.usePurpose ?? ""] || [];
// 모든 수질 관련 필드
// 총대장균군수는 null 값을 불검출로 표시하기때문에 해당 검사에서 제외함.
const allFields: (keyof typeof data)[] = [
"tn", "combResdch", "tph",
"trbt", "ph", "bod", "clIonQnt",
"smll", "cdtnv", "crmty",
];
const allFieldsName: Record<string, string> = {
tn: "총질소(T-N)(mg/L)",
combResdch: "결합잔류염소(mg/L)",
tph: "총인(T-P)(mg/L)",
trbt: "탁도(NTU)",
ph: "수소이온농도(pH)",
bod: "생물학적 산소요구량(BOD)(mg/L)",
clIonQnt: "염화물(mgCL/L)",
smll: "냄새",
cdtnv: "전기전도도(㎲/cm)",
crmty: "색도(도)",
};
allFields.forEach((field) => {
const isDisabled = disabledFields.includes(field);
if (!isDisabled) {
// disabled가 아닌 필드는 반드시 값이 있어야 함
const value = data[field];
if (typeof value === "string") {
if (value.trim().length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [field],
message: `${allFieldsName[field]} 을 입력해주세요.`,
});
}
} else if (typeof value === "number") {
if (value <= 0 || Number.isNaN(value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [field],
message: `${allFieldsName[field]} 을 입력해주세요.`,
});
}
} else if (value == null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [field],
message: `${allFieldsName[field]} 을 입력해주세요.`,
});
}
}
});
});
export type SdmInfoFormData = z.infer<typeof sdmInfoFormSchema>;
// 수요처 정보
export type DemandCompType = {
export type DemandCompBizType = {
index : number | '';
dmdInfoId : string | '';
dmdInfoFk : string | '';
@@ -23,22 +119,23 @@ export type DemandCompType = {
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 | '';
grssClfg : number | null //총대장균군수
tn : number | null //총질소
combResdch : number | null //결합잔류염소
tph : number | null //총인
trbt : number | null //탁도
ph : string | ''; //수소이온농
blgcOxygDmndQnt : number | null //생물학적산소요구량
clIonQnt : number | null //염화물
smll : string | ''; //냄새
cdtnv : number | null; //전기전도도
crmty : number | null //색도
rmrk : string | '';
reqDt : string | '';
dmdInfoSn : number | '';
status : string | '';
splyCndtList:SupplyCandidate[]|[],
ruseUtztnFcltNm?:string|''
}
@@ -152,7 +249,8 @@ export type MoneyDetailType = {
clclnBrdnCst : string | '';
clclnEcqrRt : string | '';
}
// 규칙 타입 선언
export type DisableRulesType = Record<string, string[]>;
/**====================================
* Store - 데이터
====================================*/
@@ -160,6 +258,139 @@ export type MoneyDetailType = {
const demandCompList : DemandCompType[] = [
]
const disableRules: DisableRulesType = {
"청소": ["tn", 'tph',"clIonQnt" ,'cdtnv'],
"화장실용수": ["tn", 'tph',"clIonQnt" ,'cdtnv'],
"세척": ["combResdch", "crmty",'tph','tn',"clIonQnt",'cdtnv'],
"살수": ["combResdch", "crmty",'tph','tn',"clIonQnt",'cdtnv'],
"조경": ["combResdch", "crmty",'tph','cdtnv'],
"농업_간접용수": ["combResdch", "crmty",'tn','tph','clIonQnt'],
"농업_직접용수": ["combResdch", "crmty",'tn','tph','clIonQnt'],
"하천": ["combResdch", "trbt", "crmty" ,'clIonQnt','cdtnv'],
'공업':[]
};
const defaultRules: Record<string, object> = {
"청소": {
grssClfg: null,
combResdch:0.2,
trbt: 2,
bod: 5,
crmty: 20,
ph: "5.8~8.5",
smll:'불쾌하지않을것',
tn:null,
tph: null,
clIonQnt: null,
cdtnv: null,
},
"화장실용수": {
grssClfg: null,
combResdch:0.2,
trbt: 2,
bod: 5,
crmty: 20,
smll:'불쾌하지않을것',
ph: "5.8~8.5",
tn:null,
tph: null,
clIonQnt: null,
cdtnv: null,
},
"세척": {
grssClfg: null,
trbt: 2,
bod: 5,
ph: "5.8~8.5",
combResdch:null,
smll:'불쾌하지않을것',
crmty:null,
tn:null,
tph: null,
clIonQnt: null,
cdtnv: null,
},
"살수": {
grssClfg: null,
trbt: 2,
bod: 5,
ph: "5.8~8.5",
smll:'불쾌하지않을것',
combResdch:null,
crmty:null,
tn:null,
tph: null,
clIonQnt: null,
cdtnv: null,
},
"조경": {
grssClfg: null,
trbt: 2,
bod: 5,
ph: "5.8~8.5",
smll:'불쾌하지않을것',
clIonQnt:250,
combResdch:null,
crmty:null,
tn:null,
tph: null,
cdtnv: null,
},
"농업_간접용수": {
grssClfg: null,
trbt: 5,
bod: 8,
ph: "5.8~8.5",
cdtnv:2000,
combResdch:null,
crmty:null,
tn:null,
tph: null,
clIonQnt: null,
},
"농업_직접용수": {
grssClfg: null,
trbt: 2,
bod: 8,
smll:'불쾌하지않을것',
ph: "5.8~8.5",
cdtnv:700,
combResdch:null,
crmty:null,
tn:null,
tph: null,
clIonQnt: null,
},
"하천": {
grssClfg: 1000,
bod: 5,
tn: 10,
tph: 0.5,
smll:'불쾌하지않을것',
ph: "5.8~8.5",
combResdch:null,
trbt:null,
crmty:null,
clIonQnt: null,
cdtnv:null
},
'공업':{
grssClfg: null,
bod: null,
tn: null,
tph: null,
ph: "~",
combResdch:null,
smll:'불쾌하지않을것',
trbt:null,
crmty:null,
clIonQnt: null,
cdtnv:null
}
};
// 사용용도
const useTypesData : SelectDataType[] = [
{ text: '청소', val: "청소" },
@@ -167,9 +398,9 @@ const useTypesData : SelectDataType[] = [
{ text: "세척", val: "세척" },
{ text: "살수", val: "살수" },
{ text: "조경", val: "조경" },
{ text: "농업", val: "농업" },
{ text: "농업_간접용수", val: "농업_간접용수" },
{ text: "농업_직접용수", val: "농업_직접용수" },
{ text: "하천", val: "하천" },
{ text: "지하수", val: "지하수" },
{ text: "공업", val: "공업" },
];
@@ -280,7 +511,7 @@ const postSdmData = async (data: DemandCompType) => {
}
// 수요정보 등록
const postSdmInfoData = async (data: DemandCompType) => {
const postSdmInfoData = async (data: DemandCompBizType) => {
try {
await postFetch("/reuseFc/request/demandInfo", data);
toast.success('수요 정보 등록에 성공하였습니다.');
@@ -360,7 +591,7 @@ const fetchGetSimilarCaseList = async (part: string, value: number|string) => {
export type SdmActions = {
initializeSdmData: () => void;
insertSdmData: (data: DemandCompType) => Promise<void>;
insertSdmInfoData: (data: DemandCompType) => Promise<void>;
insertSdmInfoData: (data: DemandCompBizType) => Promise<void>;
getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>;
actionLogin: (data: DemandCompType) => Promise<void>;
getSdmInfoStatusList: (dmdInfoId: string) => Promise<void>;
@@ -378,9 +609,10 @@ export type SdmType ={
moneyDetailList: MoneyDetailType[];
// 수요정보 신청현황
sdmInfoStatusList: DemandCompType[];
sdmInfoStatusList: DemandCompBizType[];
upHeadSdmInfoStatus: TableUpperProps[];
defaultRules: Record<string, object>
disableRules: DisableRulesType;
// 로그인관련
successNextPage : boolean;
successLogin: boolean;
@@ -398,7 +630,10 @@ export const SdmState: SdmType = {
moneyDetailList: moneyDetailList,
// 수요정보 신청현황
sdmInfoStatusList: [],
defaultRules:defaultRules,
disableRules:disableRules,
upHeadSdmInfoStatus: upHeadSdmInfoStatus,
// 로그인관련
successNextPage : false,
successLogin: false,
@@ -428,7 +663,7 @@ export const SdmReducer = (set: any): SdmActions => {
}
},
insertSdmInfoData: async (data: DemandCompType) => {
insertSdmInfoData: async (data: DemandCompBizType) => {
postSdmInfoData(data);
},
@@ -465,11 +700,13 @@ export const SdmReducer = (set: any): SdmActions => {
}))
return;
}
const sdmInfoStatusList : DemandCompType[] = [];
const sdmInfoStatusList : DemandCompBizType[] = [];
res.demandInfo.map((el: DemandCompType, index: number) => {
res.demandInfo.map((el: DemandCompBizType, index: number) => {
const matchingInfo = res.demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn);
const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청';
const ruseUtztnFcltNm = matchingInfo?.ruseUtztnFcltNm ? matchingInfo.ruseUtztnFcltNm : '';
const tmpData = {
...el,
@@ -485,7 +722,9 @@ export const SdmReducer = (set: any): SdmActions => {
useUsgCd: el.useUsgCd,
dmndTmng: el.dmndTmng,
dmdInfoSn : el.dmdInfoSn,
status : status
status : status,
splyCndtList: el.splyCndtList,
ruseUtztnFcltNm: ruseUtztnFcltNm || ''
}
sdmInfoStatusList.push(tmpData);
})
+4 -2
View File
@@ -13,8 +13,10 @@ export const StoreZus = (initState: DmState= {
...dmMapState,
}) => {
return createStore<DmStore>()((set) => {
tokenManager.setToken("site-id-key", 'DM');
tokenManager.setToken("site-theme-key", 'green');
if (typeof window !== "undefined") {
tokenManager.setToken("site-id-key", 'DM');
tokenManager.setToken("site-theme-key", 'green');
}
return {
@@ -1,5 +1,5 @@
import { useDmStore } from '@/app/store';
import type { DemandCompType } from '@/app/store/sdmMng';
import type { DemandCompType } from '@common/business_components/ui/sdmInsertForm/form';
import React, { useCallback, useEffect, useRef, useState } from 'react';
type thisProps = {
@@ -0,0 +1,18 @@
export const sanitizeInput = (val: string, allowDecimal = false) => {
if (val === "") return ""; // 빈 값 허용
// 소숫점 허용
if (allowDecimal) {
// 숫자와 점만 허용
let cleaned = val.replace(/[^0-9.]/g, "");
// 소숫점 여러 개일 경우 첫 번째만 유지
const parts = cleaned.split(".");
if (parts.length > 2) {
cleaned = parts[0] + "." + parts.slice(1).join("");
}
return cleaned;
}
// 정수만 허용
return val.replace(/[^0-9]/g, "");
};
@@ -118,14 +118,6 @@ export function SearchBar({
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="red"
className="mx-2 mt-2"
size={"sm"}
onClick={() => {}}
>
후보지선택
</Button>
<Button
color="sky"
className="mt-2"
@@ -1,28 +1,29 @@
import { FormUi } from "@common/business_components/ui";
import type { ReactNode } from "react";
import type { ControllerRenderProps, FieldError, FieldValues, Path } from "react-hook-form";
import { cloneElement, isValidElement } from "react";
interface AccessibleFormFieldProps<T extends FieldValues> {
field: ControllerRenderProps<T, Path<T>>;
label: string;
required?: boolean;
error?: FieldError;
inputComponent: ReactNode;
isDisabled?: boolean;
inputComponent: React.ReactElement<{ disabled?: boolean }>;
}
export function AccessibleFormField<T extends FieldValues>({
label,
required = false,
error,
inputComponent,
isDisabled = false,
}: AccessibleFormFieldProps<T>) {
const labelText = required ? `${label} *` : label;
return (
<FormUi label={labelText}>
<FormUi label={labelText} isDisabled={isDisabled}>
<div>
{inputComponent}
{isValidElement(inputComponent)
? cloneElement(inputComponent, { disabled: isDisabled } as Partial<typeof inputComponent.props>)
: inputComponent}
{/* {error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
<p key={idx} className="text-red-500 text-sm mt-1" role="alert">
{msg}
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,12 @@
import { useDmStore } from "@/app/store";
import type { DemandCompType } from "@/app/store/sdmMng";
import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components";
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
import { Button, Input } from "@common/components";
import { zodResolver } from "@hookform/resolvers/zod";
import { sha512 } from 'js-sha512';
import { useEffect, useState, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, type ControllerRenderProps } from "react-hook-form";
import toast from "react-hot-toast";
import * as z from "zod";
export function SdmLoginForm() {
@@ -15,10 +16,10 @@ export function SdmLoginForm() {
const formatBusinessNumber = (value: string) => {
// 숫자만 추출
const digitsOnly = value.replace(/\D/g, "");
// 10자리까지만 처리
const limitedDigits = digitsOnly.slice(0, 10);
// 3-2-5 형식으로 포맷팅
if (limitedDigits.length <= 3) {
return limitedDigits;
@@ -28,33 +29,33 @@ export function SdmLoginForm() {
return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3, 5)}-${limitedDigits.slice(5)}`;
}
};
// 스키마를 useMemo로 최적화
const sdmLoginFormSchema = useMemo(() => z.object({
dmdInfoId: z.string()
.min(1, "사업자등록번호를 입력해주세요")
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다"),
bznmTotNo: isBznmTotNoDisabled
bznmTotNo: isBznmTotNoDisabled
? z.string().optional()
: z.string().length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다"),
password: z.string()
.min(1, "비밀번호를 입력해주세요")
.min(8, "비밀번호는 8자 이상 입력해주세요")
.min(8, "비밀번호는 8자 이상 입력해주세요")
.max(16, "비밀번호는 16자 이내로 입력해주세요"),
rgtr: z.string().optional(),
dmdAplyPswd: z.string().optional(),
}), [isBznmTotNoDisabled]);
type FormData = z.infer<typeof sdmLoginFormSchema>;
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
type SdmLoginForm = z.infer<typeof sdmLoginFormSchema>;
type FieldComponent = (field: ControllerRenderProps<SdmLoginForm, keyof SdmLoginForm>) => JSX.Element;
const formSchema = sdmLoginFormSchema;
useEffect(() => {
initializeCommonData();
}, [initializeCommonData]);
const defaultValues: FormData = {
const defaultValues: SdmLoginForm = {
dmdInfoId: '',
bznmTotNo: '',
password: '',
@@ -62,7 +63,7 @@ export function SdmLoginForm() {
dmdAplyPswd: '',
};
const form = useForm<FormData>({
const form = useForm<SdmLoginForm>({
resolver: zodResolver(formSchema),
defaultValues,
mode: "onChange",
@@ -72,6 +73,23 @@ export function SdmLoginForm() {
setIsBznmTotNoDisabled(checked);
};
const initPassword = () => {
toast(
() => (
<div className="flex flex-col items-center">
<div className="text-2xl mb-2">
관리자(031-0000-0000)에게 비밀번호 초기화 요청을 유선으로 하시기 바랍니다.
</div>
</div>
),
{
id: "pwd-reset",
duration: 5000,
position: "top-center",
}
);
}
const handleLogin = () => {
const data = form.getValues();
const passwordStr = sha512.hex(data.password)
@@ -82,61 +100,61 @@ export function SdmLoginForm() {
}
actionLogin(param as unknown as DemandCompType);
};
const formFields: Array<{
name: keyof FormData;
name: keyof SdmLoginForm;
label: string;
component: FieldComponent | undefined;
}> = [
{
name: "dmdInfoId",
label: "사업자등록번호",
component: (field) => (
<Input
placeholder="사업자등록번호를 입력하세요"
type="text"
className="!mt-5 !mb-5 !text-xl text-gray-600"
{...field}
value={formatBusinessNumber(field.value as string)}
onChange={(e) => {
const dmdId = e.target.value.replace(/\D/g, "").slice(0, 10);
field.onChange(dmdId);
}}
/>
)
},
{
name: "bznmTotNo",
label: BZNM_TOT_NO || "종사업자등록번호",
component: (field) => (
<FormUi
label="종사업자등록번호"
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
<Input
placeholder="여러사업장이 있을 경우 입력해주세요"
{
name: "dmdInfoId",
label: "사업자등록번호",
component: (field) => (
<Input
placeholder="사업자등록번호를 입력하세요"
type="text"
className="!mt-5 !text-xl text-gray-600"
disabled={isBznmTotNoDisabled}
className="!mt-5 !mb-5 !text-xl text-gray-600"
{...field}
value={formatBusinessNumber(field.value as string)}
onChange={(e) => {
const dmdId = e.target.value.replace(/\D/g, "").slice(0, 10);
field.onChange(dmdId);
}}
/>
)
},
{
name: "bznmTotNo",
label: BZNM_TOT_NO || "종사업자등록번호",
component: (field) => (
<FormUi
label="종사업자등록번호"
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
<Input
placeholder="여러사업장이 있을 경우 입력해주세요"
type="text"
className="!mt-5 !text-xl text-gray-600"
disabled={isBznmTotNoDisabled}
{...field}
/>
</FormUi>
)
},
{
name: "password",
label: "비밀번호",
component: (field) => (
<Input
placeholder="비밀번호를 입력하세요"
type="password"
className="!mt-5 !mb-5 !text-xl text-gray-600"
{...field}
/>
</FormUi>
)
},
{
name: "password",
label: "비밀번호",
component: (field) => (
<Input
placeholder="비밀번호를 입력하세요"
type="password"
className="!mt-5 !mb-5 !text-xl text-gray-600"
{...field}
/>
)
}
];
)
}
];
return (
<>
@@ -148,11 +166,11 @@ export function SdmLoginForm() {
control={form.control}
name={name}
render={({ field }) => (
label==='종사업자등록번호' ?
<>
label === '종사업자등록번호' ?
<>
{component && component(field)}
</>
: <FormUi label={label}>
: <FormUi label={label}>
{component && component(field)}
</FormUi>
)}
@@ -160,12 +178,15 @@ export function SdmLoginForm() {
))}
</FormProvider>
<div className="flex justify-center pt-4">
<Button
onClick={handleLogin}
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 className="flex flex-col items-center">
<Button
onClick={handleLogin}
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 className="text-xl mt-2 text-gray-400 cursor-pointer" onClick={initPassword}>비밀번호 초기화 요청</div>
</div>
</div>
</div>
</>
+2 -2
View File
@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev -p 3005",
"build": "next build",
"start": "next start",
"start:dm": "next start -p 3005",
"lint": "next lint"
},
"dependencies": {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 35 KiB

@@ -18,6 +18,8 @@ type MonthPickerProps = {
selectClass?: string;
readOnly?: boolean;
startYear?: number; // 시작 연도 props 추가
yearReadOnly?: boolean;
enableMonth?: string[];
};
// yyyyMM → Date 객체
@@ -39,6 +41,8 @@ export const DateMonthPicker = ({
selectClass,
readOnly = false,
startYear,
yearReadOnly = false,
enableMonth,
}: MonthPickerProps) => {
const [open, setOpen] = useState(false);
@@ -78,6 +82,8 @@ export const DateMonthPicker = ({
currentMonth={parsedDate}
onMonthChange={handleSelect}
startYear={startYear}
yearReadOnly={yearReadOnly}
enableMonth={enableMonth}
/>
</PopoverContent>
</Popover>
@@ -29,9 +29,11 @@ export type MonthPickerProps = {
shouldClosePopover?: boolean
) => void;
startYear?: number;
yearReadOnly?: boolean;
enableMonth?: string[];
};
export function MonthPicker({ currentMonth, onMonthChange, startYear }: MonthPickerProps) {
export function MonthPicker({ currentMonth, onMonthChange, startYear, yearReadOnly, enableMonth }: MonthPickerProps) {
const today = startOfToday();
const currentYear = today.getFullYear();
@@ -60,9 +62,9 @@ const selectedYear = parseInt(yearFromProp);
"inline-flex h-9 w-16 items-center justify-center rounded-md p-0 text-sm font-normal ring-offset-white transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-selected:opacity-100 dark:ring-offset-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus-visible:ring-slate-800",
isEqual(month, currentMonth) &&
"bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900",
!isEqual(month, currentMonth) &&
isEqual(month, getStartOfCurrentMonth()) &&
"bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50"
// !isEqual(month, currentMonth) &&
// isEqual(month, getStartOfCurrentMonth()) &&
// "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50"
);
return (
@@ -73,6 +75,7 @@ const selectedYear = parseInt(yearFromProp);
<div className="flex justify-center pt-1">
<Select
value={yearFromProp}
disabled={yearReadOnly}
onValueChange={(selectedYearStr) => {
const selectedYearNum = parseInt(selectedYearStr);
const newDate = new Date(
@@ -111,7 +114,7 @@ const selectedYear = parseInt(yearFromProp);
<button
name="month"
className={getMonthButtonClass(month)}
disabled={isFuture(month)}
disabled={isFuture(month) || (enableMonth && !enableMonth?.includes(format(month, 'MMM', { locale: ko })))}
role="gridcell"
tabIndex={-1}
type="button"
@@ -149,6 +149,7 @@ GisOlLayer.prototype = {
|| lyrTyNm == 'tb_ms_sbmrg_dngr_map'
|| lyrTyNm == 'tb_ms_swage_mhl'
|| lyrTyNm == 'tb_ms_swage_ppl'
|| lyrTyNm == 'tb_ms_swage_pmstn'
) {
source = this.getVectorSourceBbox(option);
}else {
@@ -28,6 +28,7 @@ const SelectBox = React.forwardRef<
value?: string | number;
readOnly?: boolean; // readOnly 속성 추가
selectContentClass?: string;
labelClass?: string;
}
>(
(
@@ -40,6 +41,7 @@ const SelectBox = React.forwardRef<
value,
readOnly = false,
selectContentClass,
labelClass = "w-20 whitespace-nowrap text-xl text-gray-600",
},
ref,
) => {
@@ -66,7 +68,7 @@ const SelectBox = React.forwardRef<
return (
<div className={className + " flex items-center"} ref={ref}>
{label && (
<Label className="w-20 whitespace-nowrap text-xl text-gray-600">
<Label className={labelClass}>
{label}
</Label>
)}
@@ -78,6 +78,10 @@ export function UiTable({
}
}, [tableData, headlist]);
useEffect(() => {
setIdx(index);
}, [index])
const handlePageChange = (page: number) => {
setCur(page);
onPageChange(page);
@@ -120,10 +124,10 @@ export function UiTable({
(boldArray.includes(index) ? (
<TableCell
className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${cellClick && idx === index
? "cursor-pointer !font-[1000]"
: cellClick
? "cursor-pointer"
: ""
? "cursor-pointer !font-[1000]"
: cellClick
? "cursor-pointer"
: ""
} sticky top-20 z-40 bg-secondary !font-bold`}
style={{ top: `${headerHeight + index * 32 - 4}px` }}
onClick={() => handleCellClick(index, head.id)}
@@ -151,10 +155,10 @@ export function UiTable({
) : (
<TableCell
className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${cellClick && idx === index
? "cursor-pointer !font-[1000]"
: cellClick
? "cursor-pointer"
: ""
? "cursor-pointer !font-[1000]"
: cellClick
? "cursor-pointer"
: ""
} mt-[${boldArray.length * 40}px]`}
onClick={() => handleCellClick(index, head.id)}
key={`cell-${head.id}-${index}`}