Merge remote-tracking branch 'origin/iipbs/st/dev'

This commit is contained in:
2026-10-02 13:07:41 +09:00
3 changed files with 121 additions and 28 deletions
@@ -69,10 +69,13 @@ export interface ThreeStageStatusResponse {
statsYr: string;
laeStatusCd: string;
laeStatusNm: string;
laeRejected: boolean;
ctpvStatusCd: string;
ctpvStatusNm: string;
ctpvRejected: boolean;
pbcpStatusCd: string;
pbcpStatusNm: string;
pbcpRejected: boolean;
}
export interface DashboardService {
@@ -15,26 +15,62 @@ interface StepDef {
label: string;
subLabel: string;
description: string;
rejectedDescription: 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: "환경공단이 최종 검증을 진행합니다." },
{
key: "input",
label: "입력",
subLabel: "작성자",
description: "담당자가 통계 자료를 입력합니다.",
rejectedDescription: "반려된 자료에 대한 수정보완 후 제출합니다.",
},
{
key: "submit",
label: "제출",
subLabel: "작성자",
description: "입력을 마친 자료를 제출합니다.",
rejectedDescription: "반려된 자료에 대한 수정보완 후 제출합니다.",
},
{
key: "approve",
label: "승인",
subLabel: "승인자",
description: "지자체 승인자가 제출 내용을 승인합니다.",
rejectedDescription: "반려된 자료에 대한 수정보완 후 제출합니다.",
},
{
key: "review",
label: "검토",
subLabel: "시도",
description: "광역시도 담당자가 자료를 검토합니다.",
rejectedDescription: "반려된 자료에 대한 수정보완 후 재검토를 요청합니다.",
},
{
key: "verify",
label: "검증",
subLabel: "공단",
description: "환경공단이 최종 검증을 진행합니다.",
rejectedDescription: "반려된 자료에 대한 수정보완 후 재검증을 요청합니다.",
},
];
interface ThreeStageData {
laeStatusCd: string;
laeRejected: boolean;
ctpvStatusCd: string;
ctpvRejected: boolean;
pbcpStatusCd: string;
pbcpRejected: boolean;
}
/**
* 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[] {
function resolveCheckedSteps(data?: ThreeStageData): boolean[] {
if (!data) return STEPS.map(() => false);
const lae = Number(data.laeStatusCd);
@@ -50,6 +86,18 @@ function resolveCheckedSteps(data?: {
];
}
// input/submit/approve → lae 반려여부, review → ctpv, verify → pbcp
function resolveRejectedFlags(data?: ThreeStageData): boolean[] {
if (!data) return STEPS.map(() => false);
return [
data.laeRejected,
data.laeRejected,
data.laeRejected,
data.ctpvRejected,
data.pbcpRejected,
];
}
interface ThreeStageStatusPanelProps {
params: DashboardSearchParams | null;
}
@@ -59,10 +107,23 @@ export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
const { data, isLoading, isFetching } = useDashboardThreeStageStatus(params);
const checkedList = resolveCheckedSteps(data);
const rejectedFlags = resolveRejectedFlags(data);
const currentIndex = checkedList.findIndex((v) => !v);
const isAllDone = currentIndex === -1;
const currentStep = STEPS[isAllDone ? STEPS.length - 1 : currentIndex]!;
const currentLabel = isAllDone ? "전 단계 완료" : `${currentStep.label} 대기중`;
const isCurrentRejected = !isAllDone && rejectedFlags[currentIndex];
const currentLabel = isAllDone
? "전 단계 완료"
: isCurrentRejected
? `${currentStep.label} 반려`
: `${currentStep.label} 대기중`;
const currentDescription = isAllDone
? "모든 절차가 완료되었습니다."
: isCurrentRejected
? currentStep.rejectedDescription
: currentStep.description;
return (
<div className="card card-border !m-0 flex h-full flex-col border-border bg-background text-foreground">
@@ -76,7 +137,9 @@ export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
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"
: isCurrentRejected
? "bg-red-50 text-red-600"
: "bg-amber-50 text-amber-600"
}`}
>
{currentLabel}
@@ -94,6 +157,7 @@ export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
{STEPS.map((step, index) => {
const isChecked = checkedList[index];
const isCurrent = index === currentIndex;
const isRejectedStep = isCurrent && rejectedFlags[index];
const isLast = index === STEPS.length - 1;
const lineFilled = checkedList[index];
@@ -101,16 +165,29 @@ export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
<Fragment key={step.key}>
<div className="flex flex-col items-center">
<div className="relative flex flex-col items-center">
{isCurrent && (
{isRejectedStep && (
<div className="absolute -top-11 flex flex-col items-center">
<span className="whitespace-nowrap rounded-lg bg-slate-700 px-3 py-1.5 text-base font-bold text-white shadow-md">
반려
</span>
<span className="-mt-px h-0 w-0 border-x-[6px] border-t-[6px] border-x-transparent border-t-slate-700" />
</div>
)}
{isCurrent && !isRejectedStep && (
<span className="absolute inset-0 -m-1.5 animate-ping rounded-full bg-blue-400 opacity-40" />
)}
{isRejectedStep && (
<span className="absolute inset-0 -m-1.5 animate-ping rounded-full bg-red-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"
: isRejectedStep
? "border-red-500 bg-white"
: isCurrent
? "border-blue-500 bg-white"
: "border-gray-200 bg-white"
}`}
>
{isChecked ? (
@@ -128,7 +205,11 @@ export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
) : (
<span
className={`size-4 rounded-full ${
isCurrent ? "bg-blue-500" : "bg-gray-200"
isRejectedStep
? "bg-red-500"
: isCurrent
? "bg-blue-500"
: "bg-gray-200"
}`}
/>
)}
@@ -139,9 +220,11 @@ export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
className={`text-xl font-bold ${
isChecked
? "text-blue-700"
: isCurrent
? "text-blue-600"
: "text-gray-400"
: isRejectedStep
? "text-red-600"
: isCurrent
? "text-blue-600"
: "text-gray-400"
}`}
>
{step.label}
@@ -164,15 +247,22 @@ export function ThreeStageStatusPanel({ params }: ThreeStageStatusPanelProps) {
})}
</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">
<div
className={`w-full rounded-2xl border px-7 py-6 text-center ${
isCurrentRejected
? "border-red-200 bg-red-50/50"
: "border-border bg-blue-50/40"
}`}
>
<p
className={`text-base font-medium ${
isCurrentRejected ? "text-red-600" : "text-blue-600"
}`}
>
{isAllDone ? "현재 단계" : "다음 진행 단계"} · {currentStep.label}
{isCurrentRejected ? " (반려)" : ""}
</p>
<p className="mt-2 text-lg text-foreground">
{isAllDone
? "모든 절차가 완료되었습니다."
: currentStep.description}
</p>
<p className="mt-2 text-lg text-foreground">{currentDescription}</p>
</div>
</div>
+1 -1
View File
@@ -80,7 +80,7 @@ class CSPMonitor {
// 인라인 스크립트 테스트
try {
eval('console.log("CSP test: inline script execution")');
console.log('console.log("CSP test: inline script execution")');
} catch {
console.log('CSP blocked inline script execution');
}