st: feat: 하수도보급률 입력 저장, 체출 로직 작업, 도-시 조회 시 버튼 조건 추가

This commit is contained in:
2026-02-25 14:39:40 +09:00
parent f51f6e58d8
commit cc3c5c5d68
9 changed files with 628 additions and 288 deletions
@@ -9,6 +9,7 @@ import LastYearDataButton from "@/components/button/LastYearDataButton";
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
import { LoadingWithAnimation } from "@common/components";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import SubmitDailog from "./SubmitDailog";
import { useAlertStore } from "@/shared/lib";
import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service";
@@ -21,11 +22,13 @@ import type {
SewerageSupplyRateSaveRequest,
SewerageSupplyRateStatusInfo,
} from "@/entities/statistics/model/statistics-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { getFooterSummary } from "@/components/util/grid/footerSummary";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { doList } from "@/components/util/column/input/DoList";
type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageSupplyRateVerificationResult;
@@ -36,10 +39,10 @@ function getSewerageSupplyRateParams(
overrides?: { yr?: string; demoYr?: string; mode?: string }
): SewerageSupplyRateRequestParams {
return {
sido: values.sido,
gugun: values.gugun === 'ALL' ? '' : values.gugun,
bsnmCd: values.bsnmCd,
yr: overrides?.yr ?? values.year,
ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: overrides?.yr ?? values.year,
...(overrides?.mode && { mode: overrides.mode }),
};
}
@@ -52,13 +55,15 @@ const SeweragePenetrationRateInp = () => {
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [isEnterable, setIsEnterable] = useState<boolean>(true);
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [statusInfo, setStatusInfo] = useState<SewerageSupplyRateStatusInfo | null>(null);
const [showDailog, setShowDailog] = useState<boolean>(false);
const { confirm } = useConfirm();
const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number } | null>(null);
const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number; refArea?: number } | null>(null);
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm'] as const;
const footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm', 'refArea'] as const;
const footerWithApiDefault = (fieldName: (typeof footerFieldNames)[number], numberFormat: string) => ({
expression: 'sum' as const,
numberFormat,
@@ -76,6 +81,7 @@ const SeweragePenetrationRateInp = () => {
if (col.fieldName === 'totalPopl') return { ...col, footer: footerWithApiDefault('totalPopl', '#,##0') };
if (col.fieldName === 'poplSm') return { ...col, footer: footerWithApiDefault('poplSm', '#,##0') };
if (col.fieldName === 'frgnrPoplSm') return { ...col, footer: footerWithApiDefault('frgnrPoplSm', '#,##0') };
if (col.fieldName === 'refArea') return { ...col, footer: footerWithApiDefault('refArea', '#,##0.000') };
return col;
}),
};
@@ -85,20 +91,22 @@ const SeweragePenetrationRateInp = () => {
setRowData([]);
setStatusInfo(null);
setLastSearchedYear(null);
setLastSearchedValues(null);
setFooterFromApi(null);
};
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams) => {
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => {
try {
setIsLoading(true);
const response = await statisticsInputAPi.getSewerageSupplyRateList(params);
if (response.status === 'success' && response.data) {
const commonInfo = response.data.commonInfo;
const statusInfo = response.data.statusInfo;
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({
...d,
sido: commonInfo.sido,
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
piplSupprt: commonInfo.piplSupprt,
ctpvNm: commonInfo.ctpvNm,
spcCtpnRgnCn: d.spcCtpnRgnCn === '' || d.spcCtpnRgnCn === null ? '해당없음' : d.spcCtpnRgnCn,
swagePplPsplyRt: commonInfo.swagePplPsplyRt,
}));
setRowData(
convertedData.map((d) => {
@@ -109,16 +117,27 @@ const SeweragePenetrationRateInp = () => {
);
setStatusInfo(response.data.statusInfo);
setFooterFromApi(
commonInfo
? {
totalPopl: commonInfo.bsnmTotalPopl,
poplSm: commonInfo.bsnmPoplSm,
frgnrPoplSm: commonInfo.bsnmFrgnrPoplSm,
totalPopl: commonInfo.sewerBzmnTotalPopl,
poplSm: commonInfo.sewerBzmnPoplSm,
frgnrPoplSm: commonInfo.sewerBzmnFrgnrPoplSm,
refArea: commonInfo.sewerBzmnArea,
}
: null
);
if (statusInfo && statusInfo.locgStatus) {
if (['2', '3'].includes(statusInfo.locgStatus)) {
setIsEnterable(false);
} else {
setIsEnterable(isEnterable);
}
} else {
setIsEnterable(isEnterable);
}
} else {
alert(response.message);
}
@@ -142,10 +161,11 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsInputAPi.checkInputPeriod(values.year || '');
if (response.status === 'success') {
setIsEnterable(response.data);
const checkEnterable = response.data;
// setIsEnterable(response.data);
setLastSearchedYear(values.year || null);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values));
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable);
setLastSearchedValues(values);
} else {
alert(response.message);
}
@@ -170,16 +190,101 @@ const SeweragePenetrationRateInp = () => {
return;
}
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }));
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }), true);
};
const submitFunc = () => {
const submitFunc = async () => {
const isError = rowData.filter((d) => d.verificationResult);
if (isError.length > 0) {
alert('검증 오류 데이터가 있습니다.');
return;
}
if (await confirm('제출 하시겠습니까?')) {
setShowDailog(true);
}
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const currentData = getGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert('제출할 데이터가 없습니다.');
return;
}
const columnFormatMap = new Map<string, string>();
SeweragePenetrationRateInpColumn.columns.forEach((col) => {
if (col.fieldName && col.dataFormat) {
columnFormatMap.set(col.fieldName, col.dataFormat);
}
});
const formatNumber = (value: string | number | null | undefined, fieldName: string): number => {
if (value === null || value === undefined || value === '') return 0;
const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : value;
if (isNaN(num)) return 0;
const format = columnFormatMap.get(fieldName);
if (!format) return num;
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
};
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
sggNm: (d.sggNm as string) || null,
admdstCd: d.admdstCd as string,
admdstNm: d.admdstNm as string,
trbtrNm: d.trbtrNm as string,
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null),
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'),
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'),
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'),
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'),
staaArea: formatNumber(d.staaArea, 'staaArea'),
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'),
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'),
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'),
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'),
rmrk: (d.rmrk as string) ?? '',
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'),
wholArea: formatNumber(d.wholArea, 'wholArea'),
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'),
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'),
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'),
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'),
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'),
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'),
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'),
}));
const requestData: SewerageSupplyRateSaveRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '',
ctpvNm: searchValues?.sido ?? '',
statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId,
submitList: convertedData,
sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm,
locgCharger: submitInfo.locgCharger,
locgJob: submitInfo.locgJob,
locgDept: submitInfo.locgDept,
locgCttpc: submitInfo.locgCttpc,
locgCnfirmNm: submitInfo.locgCnfirmNm,
locgCnfirmJob: submitInfo.locgCnfirmJob,
locgCnfirmDept: submitInfo.locgCnfirmDept,
locgCnfirmPh: submitInfo.locgCnfirmPh,
};
const response = await statisticsInputAPi.submitSewerageSupplyRate(requestData);
if (response.status === 'success') {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
alert('제출되었습니다.');
} else {
alert('데이터 제출 중 오류가 발생했습니다.');
}
} catch {
alert('데이터 제출 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};
const saveFunc = async () => {
@@ -218,47 +323,54 @@ const SeweragePenetrationRateInp = () => {
};
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
gugun: (d.gugun as string) || null,
adstCd: d.adstCd as string,
adstDong: d.adstDong as string,
bstrm: d.bstrm as string,
speclCntrplnAr: d.speclCntrplnAr === '해당없음' ? null : (d.speclCntrplnAr as string | null),
pblswrgPrmTrt: formatNumber(d.pblswrgPrmTrt, 'pblswrgPrmTrt'),
pblswrgScdTrt: formatNumber(d.pblswrgScdTrt, 'pblswrgScdTrt'),
pblswrgTertTrt: formatNumber(d.pblswrgTertTrt, 'pblswrgTertTrt'),
wstrPrmTrt: formatNumber(d.wstrPrmTrt, 'wstrPrmTrt'),
wstrScdTrt: formatNumber(d.wstrScdTrt, 'wstrScdTrt'),
wstrTertTrt: formatNumber(d.wstrTertTrt, 'wstrTertTrt'),
notConectPopltn: formatNumber(d.notConectPopltn, 'notConectPopltn'),
swgeProcZoneInnerAr: formatNumber(d.swgeProcZoneInnerAr, 'swgeProcZoneInnerAr'),
townUnprocPopltn: formatNumber(d.townUnprocPopltn, 'townUnprocPopltn'),
townSwttPopltn: formatNumber(d.townSwttPopltn, 'townSwttPopltn'),
townWrrtnPopltn: formatNumber(d.townWrrtnPopltn, 'townWrrtnPopltn'),
swgeProcZoneOuterAr: formatNumber(d.swgeProcZoneOuterAr, 'swgeProcZoneOuterAr'),
rmk: (d.rmk as string) ?? '',
popltnSm: formatNumber(d.popltnSm, 'popltnSm'),
arSm: formatNumber(d.arSm, 'arSm'),
swgeProcZoneInnerSm: formatNumber(d.swgeProcZoneInnerSm, 'swgeProcZoneInnerSm'),
pblswrgPopltnSm: formatNumber(d.pblswrgPopltnSm, 'pblswrgPopltnSm'),
wstrPopltnSm: formatNumber(d.wstrPopltnSm, 'wstrPopltnSm'),
swgeProcZoneOuterSm: formatNumber(d.swgeProcZoneOuterSm, 'swgeProcZoneOuterSm'),
pblswrgPopltnSuprt: formatNumber(d.pblswrgPopltnSuprt, 'pblswrgPopltnSuprt'),
adtrtPopltnSuprt: formatNumber(d.adtrtPopltnSuprt, 'adtrtPopltnSuprt'),
swrgInstlrt: formatNumber(d.swrgInstlrt, 'swrgInstlrt'),
// 행정구역
sggNm: (d.sggNm as string) || null, // 시군구
admdstCd: d.admdstCd as string, // 행정코드
admdstNm: d.admdstNm as string, // 행정구역명
trbtrNm: d.trbtrNm as string, // 지류
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null), // 특별대책지역
// 공공하수처리인구(명) - 물리적/생물학적/고도처리
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), // 고도처리
// 폐수처리인구(명) - 물리적/생물학적/고도처리
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'), // 미접속인구
staaArea: formatNumber(d.staaArea, 'staaArea'), // 하수처리구역 내 면적(㎢)
// 하수처리구역 외
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'), // 미처리인구
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢)
rmrk: (d.rmrk as string) ?? '', // 비고
// 합계·소계(계산필드 저장용)
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명)
wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢)
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), // 하수처리구역 내 합계
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'), // 공공하수처리인구 소계
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'), // 폐수처리인구 소계
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'), // 하수처리구역 외 합계
// 보급률(%)
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'), // 공공하수처리구역 인구보급률
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'), // 고도처리 인구보급률
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'), // 하수도 설치율
}));
const requestData: SewerageSupplyRateSaveRequest = {
bsnmCd: searchValues?.bsnmCd ?? '',
sido: searchValues?.sido ?? '',
yr: searchValues?.year ?? '',
userId: userInfo.userName,
saveList: convertedData,
}
sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드
ctpvNm: searchValues?.sido ?? '', // 시도
statsYr: searchValues?.year ?? '', // 통계 연도
userId: userInfo.userUnqId, // 사용자
saveList: convertedData, // 저장 데이터
sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm // 행정참고자료 외국인 합계
};
const response = await statisticsInputAPi.saveSewerageSupplyRate(requestData);
if (response.status === 'success') {
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}));
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
alert('저장되었습니다.');
} else {
alert('데이터 저장 중 오류가 발생했습니다.');
@@ -277,12 +389,12 @@ const SeweragePenetrationRateInp = () => {
<>
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
{rowData.length > 0 &&
{rowData.length > 0 && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) &&
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
{isEnterable && <SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />}
{isEnterable && <LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />}
{isEnterable && <SubmitButton submitFunc={submitFunc} />}
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
<SubmitButton submitFunc={submitFunc} />
{
// 최고 관리자일땐 저장 가능하게
<SaveButton saveFunc={saveFunc} />
@@ -298,7 +410,7 @@ const SeweragePenetrationRateInp = () => {
<DataGrid
key={isEnterable ? 'enterable' : 'readonly'}
columnInfo={columnInfo}
isExcelImport={(rowData.length > 0) && isEnterable}
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)}
rowStartNum={3}
fixedColumns={1}
rowData={rowData}
@@ -313,9 +425,18 @@ const SeweragePenetrationRateInp = () => {
return 'rg-cell-verify-error';
}
}}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
return errors.map((e) => e.rsnExpln).filter(Boolean).join('\n');
}
}}
/>
</div>
<ConfirmDialog isOpen={false} />
<SubmitDailog open={showDailog} onOpenChange={setShowDailog} statusInfo={statusInfo} onConfirm={executeSubmit} />
</>
);
};
@@ -0,0 +1,167 @@
"use client";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import { useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { statisticsInputSubmitInfoSchema, type StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-input-interface";
/** 숫자만 추출 후 하이픈 포맷 (010-1234-5678, 02-123-4567 등) */
function formatPhoneWithHyphen(value: string): string {
const digits = value.replace(/\D/g, "");
if (digits.length <= 3) return digits;
if (digits.startsWith("010")) {
if (digits.length <= 7) return `010-${digits.slice(3)}`;
return `010-${digits.slice(3, 7)}-${digits.slice(7, 11)}`;
}
if (digits.startsWith("02")) {
if (digits.length <= 5) return `02-${digits.slice(2)}`;
if (digits.length <= 9) return `02-${digits.slice(2, 5)}-${digits.slice(5, 9)}`;
return `02-${digits.slice(2, 6)}-${digits.slice(6, 10)}`;
}
if (digits.length <= 6) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
}
const PHONE_FIELDS: (keyof StatisticsInputSubmitInfoFormData)[] = ["locgCttpc", "locgCnfirmPh"];
interface SubmitDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
statusInfo: SewerageSupplyRateStatusInfo | null;
/** 다이얼로그에서 저장 클릭 시 호출. 제출 정보를 넘기면 부모에서 requestData에 포함해 제출 API 호출 */
onConfirm?: (data: StatisticsInputSubmitInfoFormData) => void | Promise<void>;
}
const defaultValues: StatisticsInputSubmitInfoFormData = {
locgCharger: "",
locgJob: "",
locgDept: "",
locgCttpc: "",
locgCnfirmNm: "",
locgCnfirmJob: "",
locgCnfirmDept: "",
locgCnfirmPh: "",
};
function statusInfoToFormData(info: SewerageSupplyRateStatusInfo): StatisticsInputSubmitInfoFormData {
return {
locgCharger: info.locgCharger ?? "",
locgJob: info.locgJob ?? "",
locgDept: info.locgDept ?? "",
locgCttpc: info.locgCttpc ?? "",
locgCnfirmNm: info.locgCnfirmNm ?? "",
locgCnfirmJob: info.locgCnfirmJob ?? "",
locgCnfirmDept: info.locgCnfirmDept ?? "",
locgCnfirmPh: info.locgCnfirmPh ?? "",
};
}
const SubmitDailog = ({ open, onOpenChange, statusInfo, onConfirm }: SubmitDialogProps) => {
const { control, handleSubmit, reset, formState: { errors } } = useForm<StatisticsInputSubmitInfoFormData>({
resolver: zodResolver(statisticsInputSubmitInfoSchema),
defaultValues,
});
useEffect(() => {
if (open) {
reset(statusInfo ? statusInfoToFormData(statusInfo) : defaultValues);
}
}, [open, statusInfo, reset]);
async function onSubmit(data: StatisticsInputSubmitInfoFormData) {
await onConfirm?.(data);
onOpenChange(false);
reset(defaultValues);
}
function handleCancel() {
onOpenChange(false);
reset(defaultValues);
}
const rows: { name: keyof StatisticsInputSubmitInfoFormData; label: string; placeholder: string }[] = [
{ name: "locgCharger", label: "작성자 명", placeholder: "작성자 명 입력" },
{ name: "locgJob", label: "작성자 직제", placeholder: "작성자 직제 입력" },
{ name: "locgDept", label: "작성자 부서", placeholder: "작성자 부서 입력" },
{ name: "locgCttpc", label: "작성자 연락처", placeholder: "작성자 연락처 입력" },
{ name: "locgCnfirmNm", label: "승인자명", placeholder: "승인자명 입력" },
{ name: "locgCnfirmJob", label: "승인자 직제", placeholder: "승인자 직제 입력" },
{ name: "locgCnfirmDept", label: "승인자 부서", placeholder: "승인자 부서 입력" },
{ name: "locgCnfirmPh", label: "승인자 연락처", placeholder: "승인자 연락처" },
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>제출 정보</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<Table variant="secondary">
<TableBody>
{rows.map(({ name, label, placeholder }) => (
<TableRow key={name}>
<TableHead className="w-[150px] bg-muted font-medium" data-required>
{label}
</TableHead>
<TableCell>
<Controller
name={name}
control={control}
render={({ field }) => {
const isPhone = PHONE_FIELDS.includes(name);
return (
<div className="space-y-1">
<Input
{...field}
value={field.value ?? ""}
placeholder={placeholder}
onChange={(e) => {
const v = e.target.value;
field.onChange(isPhone ? formatPhoneWithHyphen(v) : v);
}}
/>
{errors[name]?.message && (
<p className="text-sm text-destructive ml-1">
{errors[name]?.message}
</p>
)}
</div>
);
}}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<DialogFooter className="mt-6">
<Button type="button" color="gray" onClick={handleCancel}>
취소
</Button>
<Button type="submit" color="green">
저장
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export default SubmitDailog;