Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Generated
+8
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user