Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev

This commit is contained in:
2026-09-03 13:52:34 +09:00
6 changed files with 248 additions and 4 deletions
@@ -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>>;
}
@@ -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>
);
}
+8
View File
@@ -1217,6 +1217,9 @@ importers:
dayjs:
specifier: ^1.11.13
version: 1.11.20
decimal.js:
specifier: ^10.6.0
version: 10.6.0
js-cookie:
specifier: ^3.0.5
version: 3.0.5
@@ -4886,6 +4889,9 @@ packages:
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@@ -10599,6 +10605,8 @@ snapshots:
decimal.js-light@2.5.1: {}
decimal.js@10.6.0: {}
deep-eql@5.0.2: {}
deep-is@0.1.4: {}