Merge branch 'iipbs/dm/dev'
Deploy Frontend / deploy (push) Successful in 4m44s

This commit is contained in:
2026-05-12 16:26:00 +09:00
31 changed files with 513 additions and 291 deletions
@@ -33,8 +33,8 @@ type CaseItem = {
};
const TABS: { key: TabKey; label: string }[] = [
{ key: "rain", label: "빗물 재이용 사례" },
{ key: "reuse", label: "중수 재이용 사례" },
{ key: "rain", label: "빗물이용시설 사례" },
{ key: "reuse", label: "중수도 사례" },
{ key: "sewage", label: "하수 재이용 사례" },
{ key: "global", label: "해외 재이용 사례" },
];
@@ -59,7 +59,7 @@ const MOCK: Record<TabKey, CaseItem[]> = {
],
reuse: Array.from({ length: 6 }).map((_, i) => ({
id: `reuse-${i}`,
title: `중수 재이용 사례 ${i + 1}`,
title: `중수도 사례 ${i + 1}`,
desc: "설명 문구가 들어갑니다. (최대 2~3줄) …",
imageSrc: "/images/landing/sample_case.png",
href: "#",
@@ -23,7 +23,7 @@ export const LNB_CASES = [
// 물 재이용 사례처럼 2depth가 있는 메뉴
export const LNB_BUSINESS: LnbItem[] = [
{
label: "빗물 재이용 사례",
label: "빗물이용시설 사례",
children: [
{ label: "경기도 수원시", href: "/reuse/business/rain/suwon" },
// { label: "충청남도 보령시", href: "/reuse/business/rain/bolyeong" },
@@ -31,7 +31,7 @@ export const LNB_BUSINESS: LnbItem[] = [
],
},
{
label: "중수 재이용 사례",
label: "중수도 사례",
children: [{ label: "사례 목록", href: "/reuse/business/graywater" }],
},
{
@@ -109,7 +109,7 @@ function InfoList({
}
export default function GreywaterExampleSection() {
const title = "중수 재이용 사례";
const title = "중수도 사례";
return (
<div className="bg-white">
{/* 상단 타이틀 */}
@@ -17,7 +17,7 @@ function Hr({ className = "" }: { className?: string }) {
}
export default function RainExampleSection() {
const title = "빗물 재이용 사례";
const title = "빗물이용시설 사례";
return (
<div className="bg-white">
{/* 상단 타이틀 */}
@@ -9,7 +9,7 @@ import GlobalCaseSection from "./_components/GlobalCaseSection";
const menus: BusinessSubNavMenu[] = [
{
key: "example-rain",
label: "빗물 재이용 사례",
label: "빗물이용시설 사례",
sections: [
{ id: "example-rain-suwon", label: "경기도 수원시" },
{ id: "example-rain-jeju", label: "제주특별자치도" },
@@ -17,7 +17,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
key: "example-greywater",
label: "중수 재이용 사례",
label: "중수도 사례",
defaultOpen: false,
sections: [
{ id: "example-greywater-ansan", label: "경기도 안산시" },
@@ -46,7 +46,7 @@ export default function GreywaterPage() {
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 사례" },
{ label: "빗물 재이용 사례" },
{ label: "빗물이용시설 사례" },
{ label: "수원시", href: "/reuse/business/rain/suwon" },
]}
hideFooterLine
@@ -650,8 +650,7 @@ const globalCases: GlobalCase[] = [
depth: 1,
},
{ text: "안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능" },
{ text: "공급단가는 수질과 운영조건에 따라 다르게 책정" },
],
},
{
@@ -109,7 +109,7 @@ function InfoList({
}
export default function GreywaterExampleSection() {
const title = "중수 재이용 사례";
const title = "중수도 사례";
return (
<div className="bg-white">
{/* 상단 타이틀 */}
@@ -123,7 +123,7 @@ function RainTable({ rows }: { rows: RainTableRow[] }) {
}
export default function RainExampleSection() {
const title = "빗물 재이용 사례";
const title = "빗물이용시설 사례";
return (
<div className="bg-white">
@@ -334,7 +334,7 @@ const sewageCases: SewageCase[] = [
];
export default function SewageExampleSection() {
const title = "하수처리 재이용 사례";
const title = "하수처리수 재이용 사례";
return (
<div className="bg-white">
@@ -115,11 +115,11 @@ export default function GreywaterPage() {
return (
<div className="bg-white">
<SubPageHeader
title="중수 재이용 사례"
title="중수도 사례"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 사례" },
{ label: "중수 재이용 사례" },
{ label: "중수도 사례" },
]}
hideFooterLine
hideGuideText
+3 -3
View File
@@ -9,7 +9,7 @@ import GlobalCaseSection from "./_components/GlobalCaseSection";
const menus: BusinessSubNavMenu[] = [
{
key: "example-rain",
label: "빗물 재이용 사례",
label: "빗물이용시설 사례",
sections: [
{ id: "example-rain-suwon", label: "경기도 수원시" },
{ id: "example-rain-jeju", label: "제주특별자치도" },
@@ -17,7 +17,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
key: "example-greywater",
label: "중수 재이용 사례",
label: "중수도 사례",
defaultOpen: false,
sections: [
{ id: "example-greywater-ansan", label: "경기도 안산시" },
@@ -27,7 +27,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
key: "example-sewage",
label: "하수처리 재이용 사례",
label: "하수처리수 재이용 사례",
defaultOpen: false,
sections: [
{ id: "example-sewage-guri", label: "경기도 구리시" },
@@ -24,11 +24,11 @@ export default function RainPage() {
<BusinessSubNav />
<SubPageHeader
title="빗물 재이용 사례"
title="빗물이용시설 사례"
crumbs={[
{ label: "Home", href: "/main" },
{ label: "물 재이용 사례", href: "/example/rain" },
{ label: "빗물 재이용 사례" },
{ label: "빗물이용시설 사례" },
]}
hideFooterLine
hideGuideText
@@ -56,7 +56,7 @@ function SectionShell({
}
export default function LawIntroSection() {
const title = "물 재이용 법";
const title = "물재이용법";
return (
<div className="bg-white">
@@ -54,7 +54,7 @@ function SectionShell({
}
export default function RainIntroSection() {
const title = "빗물 이용시설";
const title = "빗물이용시설";
return (
<div className="mt-[120px] bg-white">
@@ -52,7 +52,7 @@ function SectionShell({
}
export default function TreatedIntroSection() {
const title = "하수처리수";
const title = "하수처리수 재이용";
return (
<div className="mt-[120px] bg-white">
+3 -3
View File
@@ -9,7 +9,7 @@ import TreatedIntroSection from "./_components/TreatedIntroSection";
const menus: BusinessSubNavMenu[] = [
{
key: "intro-law",
label: "물 재이용 법",
label: "물재이용법",
sections: [
{ id: "law-plan", label: "법률 제·개정 및 기본계획 수립" },
{ id: "plan-performance", label: "제2차 계획 대비 실적 현황" },
@@ -21,7 +21,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
key: "intro-rain",
label: "빗물 이용시설",
label: "빗물이용시설",
defaultOpen: false,
sections: [
{ id: "rain-intro", label: "빗물 재이용 소개" },
@@ -48,7 +48,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
key: "intro-treated",
label: "하수처리수",
label: "하수처리수 재이용",
defaultOpen: false,
sections: [
{ id: "treatedwater-intro", label: "하수처리수 소개" },
+2 -2
View File
@@ -33,8 +33,8 @@ type CaseItem = {
};
const TABS: { key: TabKey; label: string }[] = [
{ key: "rain", label: "빗물 재이용 사례" },
{ key: "greywater", label: "중수 재이용 사례" },
{ key: "rain", label: "빗물이용시설 사례" },
{ key: "greywater", label: "중수도 사례" },
{ key: "sewage", label: "하수 재이용 사례" },
{ key: "global", label: "해외 재이용 사례" },
];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 284 KiB

+6
View File
@@ -0,0 +1,6 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: "./tsconfig.eslint.json",
},
};
@@ -0,0 +1,28 @@
"use client";
/**
* 표준공사비 시뮬레이션 Popup
*/
import { useForm } from "react-hook-form";
import ConCostForm from "@/components/ui/form/coMat/conCostForm/conCostForm";
import type { DmdCoReqType } from "@/app/store/matDmd/coMatRvw/type";
import ListDiv from "@common/business_components/ui/list_div";
export default function ConstCostSimulationPage() {
const form = useForm<DmdCoReqType>({
defaultValues: {
dmndQty: 0,
demandCst: { cstrnCst: 0, usgPrcsProcs: "", prcsFcltCpctCstrnCst: 0, fcltSbsdCstrnCst: 0 },
totalConstCost: 0
} as any
});
return (
<ListDiv>
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-medium text-label">표준공사비 산정</h3>
</div>
<ConCostForm form={form} type="simulation" readonly={false} />
</ListDiv>
)
}
@@ -52,6 +52,7 @@ export default function CoMatRvw() {
const { auth, code, coRvw, coMatRvwInit, coMatRvwSearch, coMatRvwInqua } =
useWrStore((state) => state);
const { coMatRvwRes, coMatRvwCount } = coRvw;
// const [mounted, setMounted] = useState(false);
const { regAuthrtYn } = auth;
const alert = useAlertStore((state) => state.setMessage);
@@ -74,9 +75,7 @@ export default function CoMatRvw() {
const [toDate, setToDate] = useState<string>(
String(new Date().getFullYear())
);
const [selectData, setSelectData] = useState<DmdCoReqType>(
{} as DmdCoReqType
);
const [selectData, setSelectData] = useState<DmdCoReqType | null>(null); // selectData를 null로 초기화
const [selectionVersion, setSelectionVersion] = useState(0);
const bottomRef = useRef<HTMLDivElement | null>(null);
@@ -122,7 +121,7 @@ export default function CoMatRvw() {
if (coMatRvwRes && coMatRvwRes[index]) {
setSelectData(coMatRvwRes[index]);
setSelectionVersion((v) => v + 1); // 행을 클릭할 때마다 버전을 1씩 증가
bottomRef.current?.scrollIntoView({ behavior: "smooth" }); // 화면 스크로 처리
bottomRef.current?.scrollIntoView({ behavior: "smooth" }); // 화면 스크롤 처리
}
};
const useUsgCdWatch = useWatch({ control: form.control, name: "useUsgCd" });
@@ -131,7 +130,6 @@ export default function CoMatRvw() {
String(useUsgCdWatch ?? "");
useEffect(() => {
if (useUsgCdWatch) {
console.log("useUsgCdWatch:" , useUsgCdWatch);
const normalizedUseUsgCd =
code.sewageOutFclttUse?.find((item) => item.val === useUsgCdWatch)?.val ??
code.sewageOutFclttUse?.find((item) => item.text === useUsgCdWatch)?.val ??
@@ -188,6 +186,7 @@ export default function CoMatRvw() {
useEffect(() => {
if (!initRef.current) {
coMatRvwInit(1, 13, fromDate, toDate);
//setMounted(true);
initRef.current = true;
}
}, [coMatRvwInit, initRef, fromDate, toDate]);
@@ -205,6 +204,20 @@ export default function CoMatRvw() {
setSelectionVersion((v) => v + 1); // 강제로 바뀌었다고 알리기
}
}, [selectData, form]);
/**
* 하수 표준공사비 시뮬레이션(모의산정)
*/
const constCostSimulation = () => {
const url = `/constCostSimulation`;
const width = Math.floor(window.screen.availWidth * 0.6);
const height = Math.floor(window.screen.availHeight * 0.6);
const left = Math.floor(window.screenX + (window.outerWidth - width) / 2);
const top = Math.floor(window.screenY + (window.outerHeight - height) / 2);
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
window.open(url, "candidatePopup", popupOptions);
};
return (
<>
<ContentsDiv>
@@ -290,6 +303,9 @@ export default function CoMatRvw() {
>
조회
</Button>
<Button className={btnClass} onClick={()=>constCostSimulation()} >
표준공사비 모의산정
</Button>
</div>
</div>
</SearchFormRight>
@@ -318,10 +334,9 @@ export default function CoMatRvw() {
getSearch(val, Number(fromDate), Number(toDate), con, dmdCoNm);
}}
></UiTable>
<div ref={bottomRef} />
<div ref={bottomRef} className="scroll-mt-[45px]"/>
</ListDiv>
<br /><hr />
<br />
<hr className="my-4"/>
{/* <ScrollArea className="h-[49vh]"> */}
<ListDiv>
<div className="card-header flex items-center justify-between">
@@ -343,10 +358,11 @@ export default function CoMatRvw() {
onChange={(e) => {
field.onChange(String(e.target.value));
}}
value={
moment(field.value).format("YYYY-MM-DD") ?? ""
value={ // 값이 있을 때만 moment 실행, 없으면 빈 문자열
field.value ? moment(field.value).format("YYYY-MM-DD") : ""
}
readOnly
suppressHydrationWarning // 시간 차이로 인한 경고 무시
className={readOnlyInputClass}
/>
</FormUi>
@@ -773,5 +789,3 @@ export default function CoMatRvw() {
</>
);
}
+31 -4
View File
@@ -53,17 +53,19 @@ export type comFcltState = {
searchFclts: pubHasuType[];
demandCompList: DemandCompType[];
checkDemand: DemandCompType;
constCostRes:conCostFeeType[], // 표준사업비
constCostRes:conCostFeeType[], // 표준사업비 (공사비 산정표)
rateFeeRes:rateFeeType[],
};
type comFcltActions = {
type comFcltActions = {
getSwtrmtFcltNm: (searchNm: string) => void;
getDemandCompList: (searchNm: string, cnt: number) => void;
regDemandCompany: (data: DemandCompType) => Promise<string>;
updateDemandCompany: (data: DemandCompType) => Promise<string>;
getConstCostFee: (year: string) => void;
getRateFee: (year: string) => void;
getRateFeeRtn: (year: string) => Promise<rateFeeType[]>;
getConstCostFeeRtn: (year: string) => Promise<conCostFeeType[]>;
};
export const comFcltSearchAction: (set: any) => comFcltActions = (set: any) => {
@@ -168,7 +170,20 @@ export const comFcltSearchAction: (set: any) => comFcltActions = (set: any) => {
console.log(" error ", e);
}
},
// 공사금액 조회 : ONSEMIRO_NEW.EM_STD_COST_REUSE 설치관리표준사업비 재이용시설 공사비
// 요율정보 조회 : ONSEMIRO_NEW.EM_STD_COST_INC_EXP_RT 설치관리표준사업비 부대경비요율
// 팝업에서 연도 변경하여 볼 수 있도록 데이터를 return한다.
getRateFeeRtn: async (year: string) => {
try {
const url = `/prcsFclt/CstRt/${year}`;
const rateFeeRes = await getFetch(fetchType.COMMON, url);
return rateFeeRes;
} catch (e) {
console.log(" error ", e);
return [];
}
},
// 공사금액 (공사비 산정표) 조회 : ONSEMIRO_NEW.EM_STD_COST_REUSE 설치관리표준사업비 재이용시설 공사비
getConstCostFee: async (year: string) => {
try {
const url = `/prcsFclt/CstRuse/${year}`;
@@ -182,9 +197,21 @@ export const comFcltSearchAction: (set: any) => comFcltActions = (set: any) => {
});
} catch (e) {
console.log('getConstCostFee ', e)
console.log('getConstCostFee ', e);
}
},
// 공사금액 (공사비 산정표) 조회 : ONSEMIRO_NEW.EM_STD_COST_REUSE 설치관리표준사업비 재이용시설 공사비
getConstCostFeeRtn: async (year: string): Promise<conCostFeeType[]> => {
try {
const url = `/prcsFclt/CstRuse/${year}`;
const constCost = await getFetch(fetchType.COMMON, url);
return constCost;
} catch (e) {
console.log('getConstCostFee ', e);
return [];
}
}
};
};
+12 -11
View File
@@ -24,17 +24,18 @@ export const coRvwNodeListExport: CommonTableProps[] = [
];
export const constCostHeadListExport: CommonTableProps[] = [
{ id: "gubun", title: "구분", visible: true, },
{ id: "500", title: "500", visible: true, },
{ id: "1000", title: "1,000", visible: true, },
{ id: "2000", title: "2,000", visible: true, },
{ id: "3000", title: "3,000", visible: true, }, //바꾸기
{ id: "5000", title: "5,000", visible: true, }, //바꾸기
{ id: "10000", title: "10,000", visible: true, }, //
{ id: "20000", title: "20,000", visible: true, }, //
{ id: "30000", title: "30,000", visible: true, }, //
{ id: "50000", title: "50,000", visible: true, }, //
{ id: "100000", title: "100,000", visible: true, }, //바꾸기
{ id: "gubun", title: "구분", visible: true, row: 0, rowSpan: 2, colSpan: 1 },
{ id: "A", title: "처리시설용량(m³)", visible: true, row: 0, rowSpan: 1, colSpan: 10 },
{ id: "500", title: "500", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "1000", title: "1,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "2000", title: "2,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "3000", title: "3,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "5000", title: "5,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "10000", title: "10,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "20000", title: "20,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "30000", title: "30,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "50000", title: "50,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
{ id: "100000", title: "100,000", visible: true, row: 1, rowSpan: 1, colSpan: 1 },
];
export const rateFeeHeadListExport: CommonTableProps[] = [
+5 -3
View File
@@ -66,7 +66,7 @@ type coRvwActions = {
sggCd: string
) => void;
};
// 수요매칭검토현황 목록
const coMatInit = async (
pageNo: number,
pageCnt: number,
@@ -99,9 +99,10 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => {
},
}));
},
// 수요처 매칭 검토 저장
coMatRvwUpdate: async (
splyCd: string,
dmdCd: string,
splyCd: string, // 공공기관재사용시설아이디 - 하수재이용시설id : pbnst_ruse_fclt_id
dmdCd: string, // 수요정보id dmd_info_id
data: DedMatUpdateType
): Promise<string> => {
try {
@@ -148,6 +149,7 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => {
sggCd: string,
dmdCoNm: string
) => {
// 수요처 년도별 정보 검색
let url = `/reuseFc/request/infoList/Sub/${pageNo}/${pageCnt}/${startYear}/${endYear}`;
if (sggCd.length > 0) {
+36 -36
View File
@@ -60,47 +60,47 @@ const DemandCoSchema = z.object({
});
const DemandCstSchema = z.object({
dmdInfoSn: z.number().nullish(),
usgPrcsProcs: z.string().nullish(),
cstrnCst: z.number().default(0),
fcltSbsdCstrnCst: z.number().default(0),
prcsFcltCpctCstrnCst: z.number().default(0),
dmdInfoSn: z.number().nullish(), // 수요정보 일련번호
usgPrcsProcs: z.string().nullish(), // 용도에 따른 처리공정
cstrnCst: z.number().default(0), // 공급관로 표준공사비(백만원)
fcltSbsdCstrnCst: z.number().default(0), // 시설부대경비
prcsFcltCpctCstrnCst: z.number().default(0), // 처리시설용량에 따른 공사비(백만원) - 처리시설용량공사비용
});
export const DmdCoReqSchema = z.object({
cityText: z.string().nullish(),
sggText: z.string().nullish(),
dmdInfoSn: z.number().default(0),
dmdInfoId: z.string().default(''),
dmndTmng: z.coerce.date().nullish(),
dmndQty: z.number().nullish(), // null 또는 undefined 허용
blgcOxygDmndQnt: z.number().nullish(),
wntUntprc: z.number().nullish(),
grssClfg: z.number().nullish(),
combResdch: z.number().nullish(),
trbt: z.number().nullish(),
smll: z.string().nullish(),
crmty: z.number().nullish(),
tn: z.number().nullish(),
tph: z.number().nullish(),
ph: z.string().nullish(),
clIonQnt: z.number().nullish(),
cdtnv: z.number().nullish(),
rmrk: z.string().nullish(),
useUsgCd: z.string().nullish(),
mtchnCmptnYmd: z.string().nullish(),
rvwCmptnYmd: z.string().nullish(),
demandCo: DemandCoSchema.nullish(),
demandMatching: DemandMatchingSchema.nullish(),
demandCst: DemandCstSchema.nullish(),
ruseUtztnFcltNm: z.string().nullish(),
cityText: z.string().nullish(), // 시도
sggText: z.string().nullish(), // 시군구
dmdInfoSn: z.number().default(0), // 수요정보 일련번호
dmdInfoId: z.string().default(''), // 수요정보 ID
dmndTmng: z.coerce.date().nullish(), // 요구시기
dmndQty: z.number().nullish(), // 요구수량(m³/일)
blgcOxygDmndQnt: z.number().nullish(), // 생물학적 산소요구량(BOD)(mg/L)
wntUntprc: z.number().nullish(), // 희망단가(원/m3)
grssClfg: z.number().nullish(), // 총대장균군수(개/mg)
combResdch: z.number().nullish(), // 결합잔류염소(mg/L)
trbt: z.number().nullish(), // 탁도(NTU)
smll: z.string().nullish(), // 냄새
crmty: z.number().nullish(), // 색도(도)
tn: z.number().nullish(), // 총질소(T-N)(mg/L)
tph: z.number().nullish(), // 총인(T-P)(mg/L)
ph: z.string().nullish(), // 수소이온농도(pH)
clIonQnt: z.number().nullish(), // 염화물(mgCL/L)
cdtnv: z.number().nullish(), // 전기전도도(㎲/cm)
rmrk: z.string().nullish(), // 비고
useUsgCd: z.string().nullish(), // 사용용도
mtchnCmptnYmd: z.string().nullish(), // 매칭완료일
rvwCmptnYmd: z.string().nullish(), // 검토완료일
demandCo: DemandCoSchema.nullish(), // 수요처 정보
demandMatching: DemandMatchingSchema.nullish(), // 매칭 정보
demandCst: DemandCstSchema.nullish(), // 공사비 정보
ruseUtztnFcltNm: z.string().nullish(), // 공급처명
splyCndtList: z.array(
z.object({
ruseUtztnFcltNm: z.string(),
ruseUtztnFcltCd: z.string(),
ruseUtztnFcltNm: z.string(), // 공급처명
ruseUtztnFcltCd: z.string(), // 공급처 코드
}).nullish()
).nullish(),
totalConstCost: z.number().nullish(),
).nullish(), // 공급처 후보지 목록
totalConstCost: z.number().nullish(), // 처리시설 총사업비(백만원)
});
export type DmdCoReqType = z.infer<typeof DmdCoReqSchema>;
@@ -159,4 +159,4 @@ export type rateFeeType = {
export type SupplyCandidate = {
ruseUtztnFcltNm: string;
ruseUtztnFcltCd: string;
};
};
@@ -20,20 +20,21 @@ import {
Form,
FormField,
FormUi,
SelectBox,
UiTable,
} from "@common/business_components/ui";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import PopoverUi from "@common/business_components/ui/popover_ui";
import { Button, Checkbox, Input, Label } from "@common/components/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import { type UseFormReturn } from "react-hook-form";
import { useForm, type UseFormReturn } from "react-hook-form";
/**
* ConCostForm
* 호출: (with-layout)/matMng/coMat/coMatRvw 에서 호출
* ConCostForm
* 호출: (with-layout)/matMng/coMat/coMatRvw
*
* 공사비를 산정하고
*
* */
* @param form - 외부에서 전달된 폼 객체 (없을 경우 시뮬레이션용 로컬 폼 사용)
* @param type - "mat": 매칭, "hist": 이력, "simulation": 모의산정
*/
export default function ConCostForm({
form,
@@ -41,11 +42,27 @@ export default function ConCostForm({
selectionVersion,
readonly = false,
}: {
form: UseFormReturn<DmdCoReqType>;
form?: UseFormReturn<DmdCoReqType>;
type: string;
selectionVersion: number;
readonly: boolean;
selectionVersion?: number;
readonly?: boolean;
}) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// 시뮬레이션 모드를 위한 로컬 폼 생성
const localForm = useForm<DmdCoReqType>({
defaultValues: {
dmndQty: 0,
demandCst: { cstrnCst: 0, usgPrcsProcs: "" }
} as any
});
const activeForm = form || localForm;
const isSim = type === "simulation";
const readOnlyInputClass = "bg-slate-50 text-gray-700";
const {
constCostRes, // 표준사업비
@@ -105,23 +122,54 @@ export default function ConCostForm({
link.click();
});
};
const usgPrcsProcsWatch = form.watch("demandCst.usgPrcsProcs");
const dmndQtyWatch = form.watch("dmndQty");
const totalConstCost = form.watch("totalConstCost");
const cstrnCst = form.watch("demandCst.cstrnCst");
const splyList = form.watch("splyCndtList");
const useUsgCdWatch = form.watch("useUsgCd");
const usgPrcsProcsWatch = activeForm.watch("demandCst.usgPrcsProcs"); // 용도에 따른 처리공정
const dmndQtyWatch = activeForm.watch("dmndQty"); // 요구수량(m³/일)
const totalConstCost = activeForm.watch("totalConstCost"); // 처리시설 총사업비(백만원)
const cstrnCst = activeForm.watch("demandCst.cstrnCst") || 0; // 공급관로 표준공사비(백만원)
const prcsFcltCpctCstrnCst = activeForm.watch("demandCst.prcsFcltCpctCstrnCst") || 0; // 처리시설용량에 따른 공사비(백만원)
const fcltSbsdCstrnCst = activeForm.watch("demandCst.fcltSbsdCstrnCst") || 0; // 시설부대경비
const splyList = activeForm.watch("splyCndtList"); // 공급처 후보지 목록
const useUsgCdWatch = activeForm.watch("useUsgCd"); // 사용 용도 코드
const ruseUtztnFcltNmWatch = activeForm.watch("ruseUtztnFcltNm"); // 선정된 공급처 명
const useUsgCdLabel =
code.sewageOutFclttUse?.find((item) => item.val === useUsgCdWatch)?.text ??
code.sewageOutFclttUse?.find((item) => item.val === useUsgCdWatch)?.text ?? // 사용 용도 명칭(라벨)
String(useUsgCdWatch ?? "");
useEffect(() => {
const calculatedTotalConstCost = Number(prcsFcltCpctCstrnCst) + Number(fcltSbsdCstrnCst);
if ((totalConstCost ?? 0) !== calculatedTotalConstCost) {
activeForm.setValue("totalConstCost", calculatedTotalConstCost);
}
}, [
activeForm,
prcsFcltCpctCstrnCst,
fcltSbsdCstrnCst,
totalConstCost,
selectionVersion,
]);
// 용도 변경 시 처리공정 자동 매핑 (시뮬레이션 대응)
useEffect(() => {
if (useUsgCdWatch) {
const matched = usageProcData.find((item) => item.useUsgCd === useUsgCdWatch);
if (matched && matched.process !== usgPrcsProcsWatch) {
activeForm.setValue("demandCst.usgPrcsProcs", matched.process);
}
}
}, [useUsgCdWatch, activeForm, usgPrcsProcsWatch]);
/**
* 표준공산비 산정 : "계산"버튼 click시
*/
const calculateConstCost = useCallback(() => {
const usgPrcsProcs = usgPrcsProcsWatch;
if (selectionVersion == 0) {
alert("먼저 목록에서 수요신청 건을 선택하세요.");
if (!isSim && selectionVersion == 0) {
alert("목록에서 수요신청 건을 선택하세요.");
return;
}
if (!useUsgCdWatch) {
alert("용도 값이 없습니다.");
return;
}
if (!dmndQtyWatch) {
@@ -148,7 +196,7 @@ export default function ConCostForm({
const firstProc = getConstFctCode(usgPrcsProcs?.split("+")[0] || "") || ""; // 첫번째 공정
const secondProc = getConstFctCode(usgPrcsProcs?.split("+")[1] || "") || ""; // 두번째 공정
// 첫번째 처리공정 코드에 해당하는 표준공사비 테이블이서 요구수량을 감싸는 하한/상한 구간을 찾는 부분이다.
// 첫번째 처리공정 코드에 해당하는 표준공사비 테이블에서 요구수량을 감싸는 하한/상한 구간을 찾는 부분이다.
const { lower: firstLower, upper: firstUpper } = findBounds(
constCostRes, // 표준공사비
firstProc || "",
@@ -194,12 +242,12 @@ export default function ConCostForm({
100;
const realResult = result + fcltSbsdCstrnCst;
form.setValue("demandCst.prcsFcltCpctCstrnCst", Math.round(result));
form.setValue("demandCst.fcltSbsdCstrnCst", Math.round(fcltSbsdCstrnCst));
form.setValue("totalConstCost", Math.round(realResult));
activeForm.setValue("demandCst.prcsFcltCpctCstrnCst", Math.round(result));
activeForm.setValue("demandCst.fcltSbsdCstrnCst", Math.round(fcltSbsdCstrnCst));
activeForm.setValue("totalConstCost", Math.round(realResult));
alert("표준공산비 산정이 완료되었습니다.");
}
}, [constCostRes, rateFeeRes, usgPrcsProcsWatch, dmndQtyWatch]);
}, [constCostRes, rateFeeRes, usgPrcsProcsWatch, dmndQtyWatch, isSim, selectionVersion, activeForm]);
/**
* 저장 버튼 click 시 실행
@@ -212,22 +260,22 @@ export default function ConCostForm({
return;
}
// const pk =
// splycd && splycd[0]?.ruseUtztnFcltCd
// ? splycd[0]?.ruseUtztnFcltCd
// : "11000SW001R1";
// 1순위: 팝업에서 새로 선택한 코드 (lastSub)
// 2순위: 기존 DB에서 불러와 폼에 저장된 코드 (demandMatching.pbnstRuseFcltId)
const pk =
lastSub?.ruseUtztnFcltCd ??
activeForm.getValues("demandMatching.pbnstRuseFcltId") ??
null;
const pk =lastSub?.ruseUtztnFcltCd??null; // 후보지
if (type === "mat") {
if (value.demandCst) {
const data: DedMatUpdateType = {
dmdInfoSn: value.dmdInfoSn,
usgPrcsProcs: value.demandCst.usgPrcsProcs || "",
cstrnCst: value.demandCst.cstrnCst || 0,
fcltSbsdCstrnCst: value.demandCst.fcltSbsdCstrnCst || 0,
prcsFcltCpctCstrnCst: value.demandCst.prcsFcltCpctCstrnCst || 0,
flg: checkFinish ? "Checked" : "Sub",
dmdInfoSn: value.dmdInfoSn, // 수요정보 일련번호
usgPrcsProcs: value.demandCst.usgPrcsProcs || "", // 용도에 따른 처리공정
cstrnCst: value.demandCst.cstrnCst || 0, // 공급관로 표준공사비(백만원)
fcltSbsdCstrnCst: value.demandCst.fcltSbsdCstrnCst || 0, // 시설부대경비
prcsFcltCpctCstrnCst: value.demandCst.prcsFcltCpctCstrnCst || 0, // 처리시설용량에 따른 공사비(백만원)
flg: checkFinish ? "Checked" : "Sub", // "Sub": 검토중, "Checked": 검토완료(재이용검토이력 화면에 나타남)
rgtr: TEST_UUID,
};
@@ -235,11 +283,11 @@ export default function ConCostForm({
if (checkFinish) {
if (await confirm("검토완료 변경시 공사비는 변경할수 없습니다.\n진행하시겠습니까?")) {
const message = await coMatRvwUpdate( pk, form.getValues("dmdInfoId"), data );
const message = await coMatRvwUpdate( pk, activeForm.getValues("dmdInfoId"), data );
alert(message);
}
} else {
const message = await coMatRvwUpdate( pk, form.getValues("dmdInfoId"), data );
const message = await coMatRvwUpdate( pk, activeForm.getValues("dmdInfoId"), data );
alert(message);
}
}else{
@@ -292,10 +340,11 @@ export default function ConCostForm({
useEffect(() => {
form.register("dmdInfoSn");
form.register("dmdInfoId");
form.register("ruseUtztnFcltNm");
}, []);
if (!activeForm) return;
activeForm.register("dmdInfoSn");
activeForm.register("dmdInfoId");
activeForm.register("ruseUtztnFcltNm");
}, [activeForm]);
/**
@@ -309,7 +358,7 @@ export default function ConCostForm({
alert("먼저 목록에서 수요신청 건을 선택하세요.");
return;
}
const dmdInfoId = form.getValues("dmdInfoId");
const dmdInfoId = activeForm.getValues("dmdInfoId");
console.log(dmdInfoId)
const url = `/splyPopup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`;
@@ -321,10 +370,12 @@ export default function ConCostForm({
window.open(url, "candidatePopup", popupOptions);
};
if (!mounted) return null; // 서버 사이드 렌더링 시에는 아무것도 그리지 않음 (선택 사항)
return (
<>
<Form {...form}>
<Form {...activeForm}>
{/* <h3 className="mt-4 ml-2 text-2xl font-medium text-label">
표준공사비 산정
</h3> */}
@@ -334,62 +385,72 @@ export default function ConCostForm({
<Button className="mx-3" size={"sm"} onClick={calculateConstCost}>
계산
</Button>
<Button
className="mr-3"
size={"sm"}
onClick={form.handleSubmit(onSubmit)}
disabled={readonly}
>
저장
</Button>
<Label>검토완료</Label>
<Checkbox
className="ml-3 mr-6"
checked={checkFinish}
onCheckedChange={
(checked) => {
if (readonly) return;
setCheckFinish(checked === true);
}
// 'indeterminate' 상태를 무시하고 순수 boolean 값으로만 상태를 업데이트합니다.
}
></Checkbox>
<Label>관로 표준공사비 산정 프로그램</Label>
<Button
color="green"
size={"sm"}
onClick={zipDowonLoad}
className="bg-none ml-6"
>
다운로드
</Button>
</div>
<div className="col-span-12 ml-4 mr-4">
<h1 className={formH1Class}>기본사항</h1>
{!isSim && (
<>
<Button
className="mr-3"
size={"sm"}
onClick={activeForm.handleSubmit(onSubmit)}
disabled={readonly}
>
저장
</Button>
<Label>검토완료</Label>
<Checkbox
className="ml-3 mr-6"
checked={checkFinish}
onCheckedChange={(checked) => {
if (readonly) return;
setCheckFinish(checked === true);
}}
/>
</>
)}
{!isSim && (
<>
<Label>관로 표준공사비 산정 프로그램</Label>
<Button color="green" size={"sm"} onClick={zipDowonLoad} className="bg-none ml-6">
다운로드
</Button>
</>
)}
</div>
<div className="grid grid-cols-12 gap-4 ml-4 mr-4">
<div className="col-span-12 w-full">
<h1 className={formH1Class}>기본사항</h1>
</div>
<div className="col-span-3 mt-4">
<FormField
control={form.control}
control={activeForm.control}
name="useUsgCd"
render={({ field }) => (
<FormUi label="용도">
<Input
placeholder=""
type="text"
onChange={field.onChange}
value={useUsgCdLabel}
readOnly
className={readOnlyInputClass}
/>
{isSim ? (
<SelectBox
label=""
selectClass="rounded-md"
selectArray={code.sewageOutFclttUse || []}
onSelectValue={field.onChange}
value={field.value ?? ""}
className=""
/>
) : (
<Input
placeholder=""
type="text"
value={useUsgCdLabel}
readOnly
className={readOnlyInputClass}
/>
)}
</FormUi>
)}
/>
</div>
<div className="col-span-3 mt-4">
<FormField
control={form.control}
control={activeForm.control}
name="dmndQty"
render={({ field }) => (
<FormUi label="요구수량(m³/일)">
@@ -401,8 +462,8 @@ export default function ConCostForm({
field.onChange(Number(e.target.value));
}}
value={field.value ?? 0}
readOnly={true}
className={readOnlyInputClass}
readOnly={!isSim}
className={!isSim ? readOnlyInputClass : ""}
/>
</FormUi>
)}
@@ -439,7 +500,7 @@ export default function ConCostForm({
bodyClassName={"w-[25vw] max-w-none"}
></PopoverUi>
<FormField
control={form.control}
control={activeForm.control}
name="demandCst.usgPrcsProcs"
render={({ field }) => (
<FormUi label="">
@@ -457,7 +518,7 @@ export default function ConCostForm({
</div>
<div className="col-span-3 mt-4">
<FormField
control={form.control}
control={activeForm.control}
name="dmndQty"
render={({ field }) => (
<FormUi label="처리시설용량(m³/일)">
@@ -482,7 +543,7 @@ export default function ConCostForm({
<h1 className={formH1Class}>공사비 입력사항</h1>
</div>
<div className="col-span-2 mt-4">
<div className="col-span-3 mt-4">
<div className="flex items-center justify-between">
<PopoverUi
childrenTriger={
@@ -498,11 +559,11 @@ export default function ConCostForm({
<ConstCostFeeTable />
</>
}
bodyClassName={"w-fit max-w-[40vw]"}
bodyClassName={"w-fit max-w-[400] overflow-x-auto"}
></PopoverUi>
</div>
<FormField
control={form.control}
control={activeForm.control}
name="demandCst.prcsFcltCpctCstrnCst"
render={({ field }) => (
<FormUi label="">
@@ -515,7 +576,7 @@ export default function ConCostForm({
}}
value={field.value ?? 0}
readOnly
className={readOnlyInputClass}
className={`mt-2 ${readOnlyInputClass}`}
/>
</FormUi>
)}
@@ -536,11 +597,11 @@ export default function ConCostForm({
<RateFeeTableModal />
</>
}
bodyClassName={"w-[22vw]"}
bodyClassName={"w-fit max-w-[100] overflow-x-auto"}
></PopoverUi>
</div>
<FormField
control={form.control}
control={activeForm.control}
name="demandCst.fcltSbsdCstrnCst"
render={({ field }) => (
<FormUi label="">
@@ -553,7 +614,7 @@ export default function ConCostForm({
}}
value={field.value ?? 0}
readOnly
className={readOnlyInputClass}
className={`mt-2 ${readOnlyInputClass}`}
/>
</FormUi>
)}
@@ -585,7 +646,7 @@ export default function ConCostForm({
bodyClassName={"w-[22vw]"}
></PopoverUi>
<FormField
control={form.control}
control={activeForm.control}
name="totalConstCost"
render={({ field }) => (
<FormUi label="">
@@ -604,35 +665,30 @@ export default function ConCostForm({
)}
/>
</div>
<div className="col-span-3 mt-4">
<div className="flex items-center justify-between">
<Label>공급처 선정</Label>
<Button
size={"sm"}
onClick={(e) => {
searchCandidate(e);
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
}}
className="bg-none"
>
후보지 조회
</Button>
{!isSim && (
<div className="col-span-3 mt-4">
<div className="flex items-center justify-between">
<Label>공급처 선정</Label>
<Button size={"xs"} onClick={searchCandidate} className="bg-none">
후보지 조회
</Button>
</div>
<FormUi label="">
<Input
placeholder="공급처를 선정해주세요"
type="text"
// 1순위: 팝업에서 새로 선택한 값(lastSub)
// 2순위: 기존 DB에서 불러온 값(ruseUtztnFcltNmWatch)
value={lastSub?.ruseUtztnFcltNm || ruseUtztnFcltNmWatch || ""}
readOnly
className={`mt-2 ${readOnlyInputClass}`}
/>
</FormUi>
</div>
<FormUi label="">
<Input
placeholder=""
type="text"
value={lastSub?.ruseUtztnFcltNm || ""}
readOnly
className={readOnlyInputClass}
/>
</FormUi>
</div>
<div className="col-span-3 mt-4">
)}
<div className="col-span-2 mt-4">
<FormField
control={form.control}
control={activeForm.control}
name="demandCst.cstrnCst"
render={({ field }) => (
<FormUi label="공급관로 표준공사비(백만원)">
@@ -641,7 +697,7 @@ export default function ConCostForm({
type="number"
step="0.01"
onChange={(e) => {
if (readonly) return;
if (readonly && !isSim) return;
field.onChange(Number(e.target.value));
}}
@@ -652,17 +708,16 @@ export default function ConCostForm({
/>
</div>
<div className="col-span-3 mt-4 mb-3">
<Label className="text-xl">
처리시설 + 공급관로 총사업비(백만원)
</Label>
<Input
placeholder=""
type="number"
step="0.001"
readOnly={true}
value={(totalConstCost ?? 0) + (cstrnCst ?? 0)}
className={readOnlyInputClass}
/>
<FormUi label="처리시설 + 공급관로 총사업비(백만원)">
<Input
placeholder=""
type="number"
step="0.001"
readOnly={true}
value={(totalConstCost ?? 0) + (cstrnCst ?? 0)}
className={readOnlyInputClass}
/>
</FormUi>
</div>
</div>
</Form>
@@ -1,3 +1,6 @@
/**
* 공사비 산정표
*/
"use client";
import { useWrStore } from "@/app/store";
@@ -5,29 +8,24 @@ import type {
conCostFeeType,
ContCostMap,
} from "@/app/store/common/searchCode/type";
import { UiTable } from "@common/business_components/ui";
import { SelectBox, UiTable } from "@common/business_components/ui";
import ListDiv from "@common/business_components/ui/list_div";
import { Label, } from "@common/components/ui";
import { useEffect, useState } from "react";
export function ConstCostFeeTable() {
const { coRvw, constCostRes } = useWrStore((state) => state);
const { coRvw, constCostRes, getConstCostFeeRtn } = useWrStore((state) => state);
const [constCostData, setConstCostData] = useState<ContCostMap[]>([]);
const 기준년도 = constCostRes?.[0]?.yr ?? "";
const [selectedYear, setSelectedYear] = useState<string>(String(new Date().getFullYear()));
// 연도 선택을 위한 옵션 생성 (예: 현재 연도부터 10년 전까지)
const yearOptions = Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i;
return { val: String(year), text: String(year) };
});
const getFctDescription = (fctCd: string) => {
const fctMap: { [key: string]: string } = {
"001": "총인처리시설",
"002": "염소소독",
"003": "UV소독",
"004": "전처리시설",
"005": "RO",
};
return fctMap[fctCd];
}
useEffect(() => {
const fetchData = async (constCostRes: conCostFeeType[]) => {
const tableData = constCostRes?.reduce(
(acc: ContCostMap[], cur: conCostFeeType) => {
const key = cur.fctCd;
@@ -48,7 +46,7 @@ export function ConstCostFeeTable() {
};
acc?.push(target);
}
target[cur.fctCpc] = cur.stdCost;
target[cur.fctCpc] = Number(cur.stdCost);
return acc;
},
@@ -60,19 +58,66 @@ export function ConstCostFeeTable() {
gubun: getFctDescription(item.gubun) || item.gubun,
};
});
setConstCostData(formattedTableData);
return formattedTableData;
};
const getFctDescription = (fctCd: string) => {
const fctMap: { [key: string]: string } = {
"001": "총인처리시설",
"002": "염소소독",
"003": "UV소독",
"004": "전처리시설",
"005": "RO",
};
return fctMap[fctCd];
}
useEffect(() => {
const fetchConstCost = async () => {
const data = await fetchData(constCostRes);
setConstCostData(data);
};
fetchConstCost();
}, [constCostRes]);
useEffect(() => {
const fetchConstCost = async () => {
if (selectedYear) {
const result = await getConstCostFeeRtn(selectedYear);
if (result && result.length > 0) {
const data = await fetchData(result);
setConstCostData(data);
} else {
alert(selectedYear+'년 데이터가 없습니다.');
}
}
}
fetchConstCost();
}, [selectedYear]);
return (
<>
<ListDiv className="h-[calc(100vh-60vh)] overflow-auto">
<h3 className="mt-4 ml-2 text-2xl font-medium text-label">
<ListDiv className="min-w-[600px] h-[48vh] overflow-auto">
<div className="p-4">
<h3 className="text-2xl font-medium text-label">
공사비 산정표 보기
</h3>
<hr />
<div className="col-span-5 flex items-center">
<Label className="ml-2 mr-1 text-xl text-gray-600">기준년도</Label>
<span>{기준년도}</span>
<hr className="mt-4 mb-4"/>
<div className="col-span-5 flex items-center justify-between">
<div className="flex items-center">
<Label className="mr-1 text-xl text-gray-600">기준년도</Label>
<SelectBox
className="h-full w-32 text-2xl"
label=""
selectClass={""}
selectArray={yearOptions}
value={selectedYear}
onSelectValue={(val) => setSelectedYear(String(val))
}
/>
</div>
<span className="mr-2 text-xl text-gray-600">단위: 백만원</span>
</div>
<UiTable
headName={""}
@@ -83,7 +128,10 @@ export function ConstCostFeeTable() {
cellClick={undefined}
onPageChange={() => { }}
/>
</ListDiv>
</>
<div className="px-3 text-lg text-gray-700 leading-relaxed">
시설용량 구간내 공사비는 직선보간법에 의하여 산출
</div>
</div>
</ListDiv>
);
}
@@ -2,15 +2,24 @@
import { useWrStore } from "@/app/store";
import type { rateFeeType } from "@/app/store/common/searchCode/type";
import { UiTable } from "@common/business_components/ui";
import { SelectBox, UiTable } from "@common/business_components/ui";
import ListDiv from "@common/business_components/ui/list_div";
import { Label } from "@common/components/ui";
import { useEffect, useState } from "react";
export function RateFeeTableModal() {
const { coRvw, rateFeeRes } = useWrStore((state) => state);
const [reateFeeTable, setRateFeeTable] = useState<rateFeeType[]>([]);
const 기준년도 = rateFeeRes?.[0]?.yr ?? "";
const { coRvw, rateFeeRes, getRateFeeRtn } = useWrStore((state) => state);
const [rateFeeTable, setRateFeeTable] = useState<rateFeeType[]>([]);
const [selectedYear, setSelectedYear] = useState<string>(String(new Date().getFullYear()));
// 연도 선택을 위한 옵션 생성 (예: 현재 연도부터 10년 전까지)
const yearOptions = Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i;
return { val: String(year), text: String(year) };
});
// const 기준년도 = rateFeeRes?.[0]?.yr ?? selectedYear; // 실제 데이터의 기준년도 또는 선택된 연도
// setSelectedYear(rateFeeRes?.[0]?.yr ?? selectedYear);
const gubunLabelMap: Record<number, string> = {
50: "5천만원까지",
@@ -32,6 +41,11 @@ export function RateFeeTableModal() {
500000: "5000억원까지",
};
/**
* rateFeeRes 데이터를 가공하여 테이블에 표시하기 적합한 형태로 변환합니다.
* - 'SS' 코드를 제외하고, stdCost 기준으로 그룹화하여 BD, SE, CE 요율을 컬럼으로 피벗합니다.
* - gubunLabelMap을 사용하여 stdCost에 해당하는 한글 라벨을 추가합니다.
*/
const fetchData = async (rateFeeRes: rateFeeType[]) => {
const data = await rateFeeRes
?.filter((item: rateFeeType) => {
@@ -65,40 +79,68 @@ export function RateFeeTableModal() {
useEffect(() => {
if (rateFeeRes?.length >= 0) {
setSelectedYear(rateFeeRes?.[0]?.yr ?? selectedYear);
fetchData(rateFeeRes).then((data) => {
setRateFeeTable(data);
});
}
}, [rateFeeRes]);
return (
<>
<ListDiv className="min-w-[600px] h-[calc(100vh-60vh)] overflow-auto">
<div className="sticky top-0 z-10 grid grid-cols-6 gap-4 p-4 bg-white">
<h3 className="col-span-6 text-2xl font-medium text-label">
요율표 보기
</h3>
<hr className="col-span-6 border-gray-300" />
// 컴포넌트 마운트 시 또는 선택된 연도가 변경될 때 데이터 조회
useEffect(() => {
const fetchRateFee = async () => {
if (selectedYear) {
const result = await getRateFeeRtn(selectedYear);
if (result && result.length > 0) {
const data = await fetchData(result);
setRateFeeTable(data);
} else {
alert(selectedYear+'년 데이터가 없습니다.');
}
}
}
fetchRateFee();
}, [selectedYear]);
<div className="col-span-5 flex items-center">
<Label className="mr-1 text-xl text-gray-600">기준년도</Label>
<span>{기준년도}</span>
</div>
<div className="col-span-6 pr-6">
<UiTable
className="min-w-full pr-12"
key={"rateTable" + reateFeeTable?.length}
headName={""}
tableData={reateFeeTable || []}
headlist={coRvw.rateFeeHeadList}
onPageChange={() => {}}
pageSize={0}
total={0}
cellClick={undefined}
/>
</div>
return (
<ListDiv className="min-w-[460px] h-[50vh] overflow-auto">
<div className="sticky top-0 z-10 grid grid-cols-6 gap-4 p-4 bg-white">
<h3 className="col-span-6 text-2xl font-medium text-label">
시설부대경비 요율(%)
</h3>
<hr className="col-span-6 border-gray-300" />
<div className="col-span-5 flex items-center">
<Label className="mr-1 text-xl text-gray-600">기준년도</Label>
<SelectBox
className="h-full w-32 text-2xl"
label=""
selectClass={""}
selectArray={yearOptions}
value={selectedYear}
onSelectValue={(val) => setSelectedYear(String(val))
}
/>
</div>
</ListDiv>
</>
<div className="col-span-6 pr-6">
<UiTable
className="min-w-full pr-12"
key={"rateTable" + rateFeeTable?.length}
headName={""}
tableData={rateFeeTable || []}
headlist={coRvw.rateFeeHeadList}
onPageChange={() => {}}
pageSize={0}
total={0}
cellClick={undefined}
/>
</div>
<div className="col-span-6 px-3 text-lg text-gray-700 leading-relaxed">
출처 : 하수도분야 보조금 편성 및 실무요령
<br />
시설부대경비 및 감리비 요율적용시 공사비가 요율표의 중간에 있을 때는 직선보간법에 의하여 산출
</div>
</div>
</ListDiv>
);
}
@@ -13,7 +13,7 @@ export default function PopoverUi({
<>
<Popover>
<PopoverTrigger>{childrenTriger}</PopoverTrigger>
<PopoverContent className={bodyClassName}>
<PopoverContent className={bodyClassName} onOpenAutoFocus={(e) => e.preventDefault()}>
{childrenBody}
</PopoverContent>
</Popover>