Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 5m9s

This commit is contained in:
2026-09-03 18:23:16 +09:00
35 changed files with 340 additions and 40 deletions
+47 -28
View File
@@ -28,7 +28,7 @@ import {
SelectValue,
} from "@common/components";
import { doList } from "@/components/util/column/input/DoList";
import { doList, doNameList } from "@/components/util/column/input/DoList";
import {
defaultSearchFormProps,
@@ -41,12 +41,27 @@ function isDoListBsnm(bsnmCd: string | undefined): boolean {
return Boolean(bsnmCd && doList.includes(bsnmCd));
}
function isDoSido(sido?: { bsnmCd?: string; sido?: string }): boolean {
if (!sido) return false;
if (isDoListBsnm(sido.bsnmCd)) return true;
return Boolean(sido.sido && doNameList.includes(sido.sido));
}
function isAllGugunValue(gugun: string | undefined): boolean {
const value = (gugun ?? "").trim();
return !value || value === "ALL" || value === "전체";
}
function firstConcreteSgg(list: Sgg[] | undefined): Sgg | undefined {
return (list ?? []).find((sgg) => !isAllGugunValue(sgg.gugun));
}
const getSggListFetcher = async (
key: [string, string, string, "Y" | "N"],
): Promise<ApiResponse<Sgg[]>> => {
const [, sidoName, instSeCd, gugunSkipYn] = key;
const [kind, sidoName, instSeCd, gugunSkipYn] = key;
const params = { instSeCd, gugunSkipYn };
if (gugunSkipYn === "N") {
if (kind === "sgg-all") {
return statisticsApi.getSggAllList(sidoName, params);
}
return statisticsApi.getSggList(sidoName, params);
@@ -69,7 +84,7 @@ function buildSearchFormValues(
);
const gugun =
d.gugun ||
(sidoItem && !isDoListBsnm(sidoItem.bsnmCd) ? "ALL" : "");
(sidoItem && !isDoSido(sidoItem) ? "ALL" : "");
return {
sido: d.sido,
gugun,
@@ -105,7 +120,7 @@ function buildSearchFormValues(
return {
sido: useFirstSido ? first.sido : "",
gugun:
useFirstSido && first && !isDoListBsnm(first.bsnmCd) ? "ALL" : "",
useFirstSido && first && !isDoSido(first) ? "ALL" : "",
year: d?.year || defaultYear.toString(),
order: d?.order || "",
fctName: d?.fctName || "",
@@ -171,7 +186,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
: {
...prev,
sido: first.sido,
gugun: isDoListBsnm(first.bsnmCd) ? "" : "ALL",
gugun: isDoSido(first) ? "" : "ALL",
bsnmCd: first.bsnmCd,
},
);
@@ -186,10 +201,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
(item) => item.sido === values.sido,
);
const gugunSkipYn = selectedSido?.gugunSkipYn ?? "Y";
const useSggAllList =
gugunSkipYn === "N" && !isDoListBsnm(selectedSido?.bsnmCd);
const sggKey: [string, string, string, "Y" | "N"] | null =
values.sido && values.sido !== "ALL" && sidoList?.data
? [
gugunSkipYn === "N" ? "sgg-all" : "sgg-list",
useSggAllList ? "sgg-all" : "sgg-list",
values.sido,
instSeCd,
gugunSkipYn,
@@ -217,45 +234,49 @@ const SearchForm = (props: SearchFormProps = {}) => {
},
);
const sggOptions = sggList?.data ?? [];
const isDoSelected = isDoSido(selectedSido);
const firstConcreteSggOption = firstConcreteSgg(sggOptions);
const gugunSelectValue =
isDoSelected && isAllGugunValue(values.gugun)
? (firstConcreteSggOption?.gugun ?? undefined)
: values.gugun || undefined;
// 도(doList): 시군구 목록 첫 항목. 광역시·세종·제주: 전체
// 도: 전체(ALL/"")가 아니라 그 다음 시군구. 그 외: 전체
useEffect(() => {
if (!mergedProps.gugun) return;
if (props.defaultRegionAll) return;
if (props.defaultValues?.gugun) return;
if (!values.sido || values.sido === "ALL") return;
if (!selectedSido) return;
if (!isDoListBsnm(selectedSido.bsnmCd)) {
if (!isDoSelected) {
setValues((prev) => {
if (prev.gugun) return prev;
return {
...prev,
gugun: "ALL",
bsnmCd: selectedSido?.bsnmCd || prev.bsnmCd,
bsnmCd: selectedSido.bsnmCd || prev.bsnmCd,
};
});
return;
}
const firstSgg = sggList?.data?.[0];
if (!firstSgg) return;
if (!firstConcreteSggOption) return;
setValues((prev) => {
if (prev.gugun) return prev;
if (!isAllGugunValue(prev.gugun)) return prev;
return {
...prev,
gugun: firstSgg.gugun,
bsnmCd: firstSgg.bsnmCd,
gugun: firstConcreteSggOption.gugun,
bsnmCd: firstConcreteSggOption.bsnmCd,
};
});
}, [
sggList,
values.sido,
selectedSido?.bsnmCd,
firstConcreteSggOption,
isDoSelected,
mergedProps.gugun,
props.defaultValues?.gugun,
props.defaultRegionAll,
selectedSido,
values.sido,
]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
@@ -269,15 +290,13 @@ const SearchForm = (props: SearchFormProps = {}) => {
};
const handleSidoChange = (value: string) => {
const selectedBsnm =
const selected =
value === "ALL"
? ""
: sidoList?.data?.sidoList?.find((v) => v.sido === value)
?.bsnmCd || "";
? undefined
: sidoList?.data?.sidoList?.find((v) => v.sido === value);
const selectedBsnm = selected?.bsnmCd || "";
const gugunAll =
props.defaultRegionAll ||
value === "ALL" ||
!isDoListBsnm(selectedBsnm);
props.defaultRegionAll || value === "ALL" || !isDoSido(selected);
setValues((prev) => ({
...prev,
sido: value,
@@ -341,7 +360,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
<div className="flex items-center gap-2">
<Label className="text-label">시군구</Label>
<Select
value={values.gugun}
value={gugunSelectValue}
onValueChange={handleSggChange}
>
<SelectTrigger className="w-[12rem]">
@@ -98,6 +98,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
@@ -116,6 +117,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
"vrfcMthdNm",
"ctpvNm",
"admdstNm",
"fcltNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
@@ -206,6 +208,7 @@ export function VerificationResultDialog({
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
ctpvNm: item.ctpvNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
fcltNm: item.fcltNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
@@ -810,6 +810,7 @@ const ExcretaTreatmentFacilityInp = () => {
lae_sbmsn_stts_cd:
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -835,7 +835,7 @@ const PrivateSewageFacilityInp = () => {
const userInfo = await getUserInfo();
setGlobalProgressLabel("일반 검증 진행 중...");
let response: { status: string; message?: string; data?: { finalStatus?: string; nvpassed?: boolean } };
let response: { status: string; message?: string; data?: { finalStatus?: string; nvpassed?: boolean; isHbPassed?: string } };
if (isSewageTreatTab) {
const param: SewageTreatFactStatusSubmitRequest = {
@@ -888,6 +888,7 @@ const PrivateSewageFacilityInp = () => {
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -434,6 +434,7 @@ const PublicSewageFacilityInp = () => {
stats_yr: lastSearchedValues.year ?? '',
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2',
gen_verify_stts_cd: nvpassed ? '1' : '2',
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -793,6 +793,7 @@ const PumpingStationInp = () => {
lae_sbmsn_stts_cd:
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -1486,6 +1486,7 @@ const SewageExcretaRelatedInp = () => {
lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -1489,6 +1489,7 @@ const SewageExcretaSlurgeInp = () => {
lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -175,12 +175,13 @@ const applyPipeTypeStatusColumnLayout = (
return walk(layout ?? []);
};
/** ST00020003: 입력기간 통과 시 조회 연도의 매설연장(y{year}LyngPrlg)만 편집 가능, 해당 연도 철거연장 컬럼 숨김 */
/** ST00020003: 일반 사용자는 조회 연도 매설연장만, 관리자는 매설연장 컬럼 전체 편집 가능. 해당 연도 철거연장 컬럼 숨김 */
const applyPipeTypeStatusEditableColumns = (
cols: ColumnConfig[] | undefined,
canEdit: boolean,
periodEnterable: boolean,
statsYr: string | null,
isAdmin: boolean,
): ColumnConfig[] => {
const hideDmltnYear = shouldHidePipeTypeEditableYearDmltn(canEdit, periodEnterable, statsYr);
@@ -205,11 +206,12 @@ const applyPipeTypeStatusEditableColumns = (
if (fieldName === "lyngPrlg" || isYearLyngField) {
const editableLyngField = periodEnterable ? resolvePipeTypeEditableLyngField(statsYr) : null;
const isTargetYear = editableLyngField !== null && fieldName === editableLyngField;
if (isTargetYear) {
const { styleName: _readonlyStyle, ...rest } = col;
if (isAdmin || isTargetYear) {
const { styleName, ...rest } = col;
return {
...rest,
editable: true as const,
...(styleName && styleName !== "rg-cell-readonly" ? { styleName } : {}),
};
}
return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" };
@@ -640,6 +642,7 @@ const SewagePipeInp = () => {
canEdit,
inputPeriodEnterable,
lastSearchedYear,
isAdminInfo,
)
: (cols ?? []).map((col: ColumnConfig) =>
canEdit
@@ -1289,7 +1292,6 @@ const SewagePipeInp = () => {
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
@@ -1493,6 +1495,7 @@ const SewagePipeInp = () => {
stats_yr: values.year ?? "",
lae_sbmsn_stts_cd: statusForTab?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -1772,7 +1775,6 @@ const SewagePipeInp = () => {
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
@@ -675,6 +675,7 @@ const SewagePipeMaintainInp = () => {
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -860,6 +860,7 @@ const SewageReservoirPumpInp = () => {
lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -945,6 +945,7 @@ const SewerageFinanceInp = () => {
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -559,6 +559,7 @@ const SeweragePenetrationRateInp = () => {
stats_yr: searchValues?.year ?? '',
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
gen_verify_stts_cd: response.data.nvpassed ? '1' : '2',
hb_pass_yn: response.data.isHbPassed,
};
try {
@@ -837,6 +837,7 @@ const SewerageServiceLevelInp = () => {
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -1195,7 +1195,7 @@ const WaterReuseInp = () => {
let response: {
status: string;
message?: string;
data?: { finalStatus: string; nvpassed: boolean };
data?: { finalStatus: string; nvpassed: boolean; isHbPassed?: string };
};
if (isPublicSewageTab) {
@@ -1352,6 +1352,7 @@ const WaterReuseInp = () => {
lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
try {
@@ -14,7 +14,7 @@ export const FactInstallStatusInpColumn = {
// 하수처리장 정보
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리장명' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리시설용량(m³/일)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리시설용량(m³/일)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
// 처리구역면적(km²)
{
@@ -88,6 +88,23 @@ export function useDashboardEcptReqCount(params: DashboardSearchParams | null) {
});
}
// 지자체/광역시도/공단 3단계 처리상태 조회
export function useDashboardThreeStageStatus(
params: DashboardSearchParams | null,
) {
return useQuery({
queryKey: createDashboardQueryKey("three-stage-status", params),
queryFn: async () => {
if (!params)
throw new Error("Dashboard search params are required.");
const response = await dashboardApi.getThreeStageStatus(params);
return response.data;
},
enabled: hasRequiredDashboardParams(params),
refetchOnWindowFocus: false,
});
}
function createDashboardQueryKey(
name: string,
params: DashboardSearchParams | null,
@@ -6,6 +6,7 @@ import type {
DashboardSearchParams,
DashboardService,
EcptReqCountResponse,
ThreeStageStatusResponse,
StatsSubmissionResponse,
UnSubmitCountResponse,
VrfcErrCountResponse,
@@ -57,10 +58,22 @@ async function getEcptReqCount(
return response.data;
}
async function getThreeStageStatus(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<ThreeStageStatusResponse>> {
const response = await stClient.get<DashboardApiResponse<ThreeStageStatusResponse>>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/three-stage-status`,
{ params },
);
return response.data;
}
export const dashboardApi: DashboardService = {
getVrfcReport,
getVrfcErrCount,
getUnSubmitCount,
getStatsSubmissionRate,
getEcptReqCount,
getThreeStageStatus,
};
@@ -65,6 +65,16 @@ export interface EcptReqCountResponse {
ecptReqCount: number;
}
export interface ThreeStageStatusResponse {
statsYr: string;
laeStatusCd: string;
laeStatusNm: string;
ctpvStatusCd: string;
ctpvStatusNm: string;
pbcpStatusCd: string;
pbcpStatusNm: string;
}
export interface DashboardService {
getVrfcReport(
params: DashboardSearchParams,
@@ -81,4 +91,7 @@ export interface DashboardService {
getEcptReqCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
getThreeStageStatus(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<ThreeStageStatusResponse>>;
}
@@ -182,6 +182,7 @@ export interface ExcretaTreatmentFacilitySubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -270,6 +270,7 @@ export interface SewageTreatFactStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -356,6 +357,7 @@ export interface SepticTankStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -419,6 +421,7 @@ export interface PrivateSewageDesigStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -175,6 +175,7 @@ export interface PumpingStationSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -303,6 +303,7 @@ export interface SewageExcretaRelatedSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -203,6 +203,7 @@ export interface SlurgeProcessingFacStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -312,6 +313,7 @@ export interface DigestorOperatingStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -417,6 +419,7 @@ export interface SewageSlurgeProcessingFacStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -523,6 +526,7 @@ export interface ExcretaSlurgeProcessingStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -157,6 +157,7 @@ export interface SewagePipeMaintainRepairSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
message?: string;
status: string;
@@ -228,6 +229,7 @@ export interface SewagePipeMaintainDredgingSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
message?: string;
status: string;
@@ -252,6 +252,7 @@ export interface SewageReservoirSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -168,6 +168,7 @@ export interface SewerageFinanceSettleSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -195,6 +196,7 @@ export interface SewerageFinanceSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -249,6 +249,7 @@ export interface SewerageServiceLevelSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -167,6 +167,7 @@ export interface PublicSewageTreatWaterReuseSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -280,6 +281,7 @@ export interface RecycledWaterStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -395,6 +397,7 @@ export interface RainFallUtilFactStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -495,6 +498,7 @@ export interface TreatWaterReuseDesignConstStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -595,6 +599,7 @@ export interface TreatWaterReuseBusinessApprStatusSubmitResponse {
data?: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
};
}
@@ -277,6 +277,7 @@ export interface PublicSewageFacilitySubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
}
message?: string;
status: string;
@@ -114,6 +114,7 @@ export type SeweragePipeInputFactInstallStatusSaveItem = Omit<
| "fcltSn"
| "swtrmtFcltCd"
| "swtrmtFcltNm"
| "prcsFcltCpct"
| "rmrk"
> & {
admdstCd: string | null;
@@ -168,6 +169,7 @@ export interface SeweragePipeInputRegionalFactStatusSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
}
message?: string;
status: string;
@@ -198,6 +200,7 @@ export interface SeweragePipeInputFactInstallStatusSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
}
message?: string;
status: string;
@@ -208,6 +211,7 @@ export interface SeweragePipeInputSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
}
message?: string;
status: string;
@@ -147,6 +147,7 @@ export interface SewerageSupplyRateSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
isHbPassed?: string;
}
message?: string;
status: string;
@@ -138,6 +138,7 @@ export interface ImrRequest {
stats_yr: string;
lae_sbmsn_stts_cd: string;
gen_verify_stts_cd?: string;
hb_pass_yn?: string;
}
export interface ImrResponse {
@@ -2,9 +2,10 @@ import type { DashboardSearchParams } from "@/entities/statistics/model/dashboar
import type { ScheduleInfoSearchParams } from "@/entities/statistics/model/dashboard/schedule-info-interface";
import { NoticePanel } from "../ui/panels/notice-panel";
import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel";
// import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel";
import { ScheduleInfoPanel } from "../ui/panels/schedule-info-panel";
import { StatsSubmissionPanel } from "../ui/panels/stats-submission-panel";
import { ThreeStageStatusPanel } from "../ui/panels/three-stage-status-panel";
import { UnSubmitPanel } from "../ui/panels/unsubmit-panel";
// import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel";
import { VrfcReportPanel } from "../ui/panels/vrfc-report-panel";
@@ -33,7 +34,7 @@ export function getInitialPanels({
},
{
id: "panel-c",
component: <NoticePanel />,
component: <ThreeStageStatusPanel params={dashboardParams} />,
className: "",
},
// {
@@ -51,10 +52,15 @@ export function getInitialPanels({
component: <VrfcReportPanel params={dashboardParams} />,
className: "",
},
// {
// id: "panel-f",
// component: <ScheduleCalendarPanel />,
// className: "",
// },
{
id: "panel-f",
component: <ScheduleCalendarPanel />,
component: <NoticePanel />,
className: "",
},
];
}
}
@@ -0,0 +1,187 @@
"use client";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import { useDashboardThreeStageStatus } from "@/entities/statistics/api/dashboard/dashboard-queries";
import { LoadingWithAnimation } from "@common/components";
import { useDragHandle } from "../sortable-panel";
type StepKey = "input" | "submit" | "approve" | "review" | "verify";
interface StepDef {
key: StepKey;
label: string;
subLabel: string;
description: string;
}
const STEPS: StepDef[] = [
{ key: "input", label: "입력", subLabel: "작성자", description: "담당자가 통계 자료를 입력합니다." },
{ key: "submit", label: "제출", subLabel: "작성자", description: "입력을 마친 자료를 제출합니다." },
{ key: "approve", label: "승인", subLabel: "승인자", description: "지자체 승인자가 제출 내용을 승인합니다." },
{ key: "review", label: "검토", subLabel: "시도", description: "광역시도 담당자가 자료를 검토합니다." },
{ key: "verify", label: "검증", subLabel: "공단", description: "환경공단이 최종 검증을 진행합니다." },
];
/**
* lae_sbmsn_stts_cd(지자체) : 0=미작성 1=작성중 2=제출완료 3=승인
* stat_pic_sbmsn_stts_cd(광역시도), pbcp_sbmsn_stts_cd(공단) 도 동일 코드 도메인 가정
* - 실제 코드값 의미는 STINPUTSTP 코드 그룹 기준 재확인 필요
*/
function resolveCheckedSteps(data?: {
laeStatusCd: string;
ctpvStatusCd: string;
pbcpStatusCd: string;
}): boolean[] {
if (!data) return STEPS.map(() => false);
const lae = Number(data.laeStatusCd);
const ctpv = Number(data.ctpvStatusCd);
const pbcp = Number(data.pbcpStatusCd);
return [
lae >= 1,
lae >= 2,
lae >= 3,
lae >= 3 && ctpv >= 3,
lae >= 3 && ctpv >= 3 && pbcp >= 3,
];
}
interface ThreeStageStatusPanelProps {
params: DashboardSearchParams | null;
}
export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, isFetching } = useDashboardThreeStageStatus(params);
const checkedList = resolveCheckedSteps(data);
const currentIndex = checkedList.findIndex((v) => !v);
const isAllDone = currentIndex === -1;
const currentStep = STEPS[isAllDone ? STEPS.length - 1 : currentIndex]!;
const currentLabel = isAllDone ? "전 단계 완료" : `${currentStep.label} 대기중`;
return (
<div className="card card-border !m-0 flex h-full flex-col border-border bg-background text-foreground">
<div
className={`card-header flex cursor-grab items-center justify-between border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">통계 처리 현황</h3>
<span
className={`rounded-full px-4 py-1.5 text-base font-medium ${
isAllDone
? "bg-blue-50 text-blue-600"
: "bg-amber-50 text-amber-600"
}`}
>
{currentLabel}
</span>
</div>
<div className="card-body relative flex flex-1 flex-col items-center justify-center px-8">
<div className="flex w-full max-w-3xl flex-col items-center gap-10">
<div className="flex w-full items-start">
{STEPS.map((step, index) => {
const isChecked = checkedList[index];
const isCurrent = index === currentIndex;
const isLast = index === STEPS.length - 1;
const lineFilled = checkedList[index];
return (
<div
key={step.key}
className="flex flex-1 flex-col items-center last:flex-none"
>
<div className="flex w-full items-center">
<div className="relative flex flex-col items-center">
{isCurrent && (
<span className="absolute inset-0 -m-1.5 animate-ping rounded-full bg-blue-400 opacity-40" />
)}
<div
className={`relative flex size-20 items-center justify-center rounded-full border-4 transition-colors ${
isChecked
? "border-blue-600 bg-blue-600 shadow-lg shadow-blue-600/20"
: isCurrent
? "border-blue-500 bg-white"
: "border-gray-200 bg-white"
}`}
>
{isChecked ? (
<svg
className="size-9 text-white"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
) : (
<span
className={`size-4 rounded-full ${
isCurrent ? "bg-blue-500" : "bg-gray-200"
}`}
/>
)}
</div>
</div>
{!isLast && (
<div className="mx-3 h-1.5 flex-1 rounded-full bg-gray-200 self-center">
<div
className="h-full rounded-full bg-blue-600 transition-all duration-500"
style={{ width: lineFilled ? "100%" : "0%" }}
/>
</div>
)}
</div>
<div className="mt-4 text-center">
<p
className={`text-xl font-bold ${
isChecked
? "text-blue-700"
: isCurrent
? "text-blue-600"
: "text-gray-400"
}`}
>
{step.label}
</p>
<p className="mt-1 text-base text-muted-foreground">
({step.subLabel})
</p>
</div>
</div>
);
})}
</div>
<div className="w-full rounded-2xl border border-border bg-blue-50/40 px-7 py-6 text-center">
<p className="text-base font-medium text-blue-600">
{isAllDone ? "현재 단계" : "다음 진행 단계"} · {currentStep.label}
</p>
<p className="mt-2 text-lg text-foreground">
{isAllDone
? "모든 절차가 완료되었습니다."
: currentStep.description}
</p>
</div>
</div>
{(isLoading || isFetching) && (
<div className="bg-background/50 absolute inset-0 flex items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
</div>
</div>
);
}