[as elapr] 화면에서 동적으로 결재자 숫자 변경및 체크 리스트 SP, SF 필수 로직으로 저장 변경
This commit is contained in:
@@ -2,7 +2,7 @@ import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
|
||||
|
||||
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
|
||||
|
||||
export type ForkJoinTrackKey = "PIPE" | "FACILITY" | "TOTAL_AGG";
|
||||
export type ForkJoinTrackKey = "SP" | "SF" | "TOTAL_AGG";
|
||||
|
||||
function normalizeBranchKey(
|
||||
rawBranchSeCd?: string,
|
||||
@@ -10,8 +10,8 @@ function normalizeBranchKey(
|
||||
): ForkJoinTrackKey | null {
|
||||
if (rawBranchSeCd) {
|
||||
const upper = rawBranchSeCd.toUpperCase().trim();
|
||||
if (upper.includes("PIPE") || upper.includes("관로")) return "PIPE";
|
||||
if (upper.includes("FACIL") || upper.includes("시설")) return "FACILITY";
|
||||
if (upper === "SP" || upper.includes("관로")) return "SP";
|
||||
if (upper === "SF" || upper.includes("시설")) return "SF";
|
||||
if (upper.includes("AGG") || upper.includes("정책") || upper.includes("TOTAL"))
|
||||
return "TOTAL_AGG";
|
||||
}
|
||||
@@ -45,8 +45,8 @@ export function mapDocPrgrsToForkJoinTrackRows(
|
||||
list: ElaprDocPrgrsClientResponseDto[] | undefined,
|
||||
): Record<ForkJoinTrackKey, ElaprAplnRow[]> {
|
||||
const tracks: Record<ForkJoinTrackKey, ElaprAplnRow[]> = {
|
||||
PIPE: [],
|
||||
FACILITY: [],
|
||||
SP: [],
|
||||
SF: [],
|
||||
TOTAL_AGG: [],
|
||||
};
|
||||
|
||||
@@ -56,9 +56,9 @@ export function mapDocPrgrsToForkJoinTrackRows(
|
||||
if (branchKey) {
|
||||
const seq = Number(item.atrzSeqNo ?? tracks[branchKey].length + 1);
|
||||
const defaultDept =
|
||||
branchKey === "PIPE"
|
||||
branchKey === "SP"
|
||||
? "하수관로부"
|
||||
: branchKey === "FACILITY"
|
||||
: branchKey === "SF"
|
||||
? "하수처리시설부"
|
||||
: "정책총괄부";
|
||||
|
||||
@@ -91,22 +91,22 @@ export function mapDocPrgrsToForkJoinTrackRows(
|
||||
});
|
||||
|
||||
// 각 트랙별 atrzSeqNo 오름차순 정렬
|
||||
tracks.PIPE.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
tracks.FACILITY.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
tracks.SP.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
tracks.SF.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
tracks.TOTAL_AGG.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
}
|
||||
|
||||
// 트랙 데이터가 없는 경우 기본 2행 채우기
|
||||
if (tracks.PIPE.length === 0) {
|
||||
tracks.PIPE = [
|
||||
createDefaultForkJoinRow("PIPE", "하수관로부", 1, "ATRZ02"),
|
||||
createDefaultForkJoinRow("PIPE", "하수관로부", 2, "ATRZ03"),
|
||||
if (tracks.SP.length === 0) {
|
||||
tracks.SP = [
|
||||
createDefaultForkJoinRow("SP", "하수관로부", 1, "ATRZ02"),
|
||||
createDefaultForkJoinRow("SP", "하수관로부", 2, "ATRZ03"),
|
||||
];
|
||||
}
|
||||
if (tracks.FACILITY.length === 0) {
|
||||
tracks.FACILITY = [
|
||||
createDefaultForkJoinRow("FACILITY", "하수처리시설부", 1, "ATRZ02"),
|
||||
createDefaultForkJoinRow("FACILITY", "하수처리시설부", 2, "ATRZ03"),
|
||||
if (tracks.SF.length === 0) {
|
||||
tracks.SF = [
|
||||
createDefaultForkJoinRow("SF", "하수처리시설부", 1, "ATRZ02"),
|
||||
createDefaultForkJoinRow("SF", "하수처리시설부", 2, "ATRZ03"),
|
||||
];
|
||||
}
|
||||
if (tracks.TOTAL_AGG.length === 0) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BranchSeCd } from "./elapr-forkjoin-types";
|
||||
import type { ElaprApln } from "./elapr-apln-interface";
|
||||
|
||||
/** 그리드 로컬 행 — API 응답 + UI 보조 필드 */
|
||||
@@ -9,7 +10,7 @@ export interface ElaprAplnRow extends Omit<ElaprApln, "aplnId"> {
|
||||
aprvrNm?: string;
|
||||
/** 소속 브랜치 ID 및 구분코드 */
|
||||
branchId?: string;
|
||||
branchSeCd?: "MAIN" | "PIPE" | "FACILITY" | "TOTAL_AGG";
|
||||
branchSeCd?: BranchSeCd;
|
||||
aprvrDeptNm?: string;
|
||||
dcarbYn?: string;
|
||||
isNew?: boolean;
|
||||
|
||||
@@ -19,8 +19,8 @@ export type AtrzSeComCd =
|
||||
| "ATRZ04" // 병합 (정책총괄 AI 병합의견 종합 심의 - 10-12 신설)
|
||||
| "ATRZ99"; // 최종승인(회신) (환경청에 회신)
|
||||
|
||||
/** 브랜치 구분코드 */
|
||||
export type BranchSeCd = "MAIN" | "PIPE" | "FACILITY" | "TOTAL_AGG";
|
||||
/** 브랜치 구분코드 (10월 14일 개정: SP 하수관로, SF 하수처리시설, TOTAL_AGG 정책부 종합병합, MAIN 주결재선) */
|
||||
export type BranchSeCd = "MAIN" | "SP" | "SF" | "TOTAL_AGG";
|
||||
|
||||
/** 브랜치 진행상태코드 */
|
||||
export type BranchSttsCd =
|
||||
@@ -65,10 +65,10 @@ export interface ElaprForkJoinStepDto {
|
||||
elaprCycl?: number;
|
||||
}
|
||||
|
||||
/** 3개 트랙으로 분할된 결재선 데이터 맵 (10-12 개정: 총 6인 체제) */
|
||||
/** 3개 트랙으로 분할된 결재선 데이터 맵 (총 6인 체제: SP, SF, TOTAL_AGG) */
|
||||
export interface ElaprForkJoinDataMap {
|
||||
pipeTrack: ElaprForkJoinStepDto[]; // 관로부 (2명: 검토(02) ➔ 승인(03))
|
||||
facilityTrack: ElaprForkJoinStepDto[]; // 시설부 (2명: 검토(02) ➔ 승인(03))
|
||||
spTrack: ElaprForkJoinStepDto[]; // 관로부 (2명: 검토(02) ➔ 승인(03))
|
||||
sfTrack: ElaprForkJoinStepDto[]; // 시설부 (2명: 검토(02) ➔ 승인(03))
|
||||
aggTrack: ElaprForkJoinStepDto[]; // 정책부 종합병합 (2명: 병합(04) ➔ 최종승인(99))
|
||||
}
|
||||
|
||||
|
||||
@@ -114,8 +114,8 @@ function ElaprApprovalDialog({
|
||||
const branchSeCd = (currentUserPrgrs as { branchSeCd?: string } | undefined)?.branchSeCd;
|
||||
const isForkJoin =
|
||||
taskSeComCd === "SF_SP" ||
|
||||
branchSeCd === "PIPE" ||
|
||||
branchSeCd === "FACILITY" ||
|
||||
branchSeCd === "SP" ||
|
||||
branchSeCd === "SF" ||
|
||||
branchSeCd === "TOTAL_AGG";
|
||||
|
||||
const isLastInBranch =
|
||||
@@ -125,7 +125,7 @@ function ElaprApprovalDialog({
|
||||
if (!isForkJoin) return "결재";
|
||||
if (branchSeCd === "TOTAL_AGG" && isLastInBranch) return "결재 (환경청에 회신)";
|
||||
if (branchSeCd === "TOTAL_AGG") return "결재 (병합)";
|
||||
if ((branchSeCd === "PIPE" || branchSeCd === "FACILITY") && isLastInBranch)
|
||||
if ((branchSeCd === "SP" || branchSeCd === "SF") && isLastInBranch)
|
||||
return "결재 (병합에 회신)";
|
||||
return "결재 (검토)";
|
||||
}, [isForkJoin, branchSeCd, isLastInBranch]);
|
||||
@@ -304,9 +304,9 @@ function ElaprApprovalDialog({
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50/70 p-4 text-sm text-blue-900">
|
||||
<div className="mb-1 flex items-center gap-2 font-bold">
|
||||
<span className="rounded-sm bg-blue-200 px-2 py-0.5 text-xs text-blue-950">
|
||||
{branchSeCd === "PIPE"
|
||||
{branchSeCd === "SP"
|
||||
? "🌊 하수관로 전문기술검토 (Stage 2)"
|
||||
: branchSeCd === "FACILITY"
|
||||
: branchSeCd === "SF"
|
||||
? "🏭 하수처리시설 전문기술검토 (Stage 2)"
|
||||
: branchSeCd === "TOTAL_AGG"
|
||||
? isLastInBranch
|
||||
|
||||
@@ -81,10 +81,10 @@ export function ElaprDocPrgrsForkJoinPickerDialog({
|
||||
}
|
||||
|
||||
const pipeRows = aplnRows
|
||||
.filter((r) => r.branchSeCd === "PIPE")
|
||||
.filter((r) => r.branchSeCd === "SP")
|
||||
.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
const facRows = aplnRows
|
||||
.filter((r) => r.branchSeCd === "FACILITY")
|
||||
.filter((r) => r.branchSeCd === "SF")
|
||||
.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
const aggRows = aplnRows
|
||||
.filter((r) => r.branchSeCd === "TOTAL_AGG")
|
||||
|
||||
@@ -74,9 +74,9 @@ export function ElaprDocPrgrsForkJoinStep({ steps }: ElaprDocPrgrsForkJoinStepPr
|
||||
|
||||
steps.forEach((step) => {
|
||||
const seCd = step.branchSeCd;
|
||||
if (seCd === "PIPE") {
|
||||
if (seCd === "SP") {
|
||||
pipe.push(step);
|
||||
} else if (seCd === "FACILITY" ) {
|
||||
} else if (seCd === "SF") {
|
||||
fac.push(step);
|
||||
} else if (seCd === "TOTAL_AGG") {
|
||||
agg.push(step);
|
||||
@@ -96,15 +96,21 @@ export function ElaprDocPrgrsForkJoinStep({ steps }: ElaprDocPrgrsForkJoinStepPr
|
||||
};
|
||||
}, [steps]);
|
||||
|
||||
// 완료 상태 판정
|
||||
// 완료 상태 및 결재 완료 수 동적 판정
|
||||
const pipeCompletedCount = pipeSteps.filter((s) => s.atrzPrgrsSttsComCd === "ATRZ_PRGRS_003").length;
|
||||
const isPipeCompleted =
|
||||
pipeSteps.length > 0 &&
|
||||
pipeSteps.every((s) => s.atrzPrgrsSttsComCd === "ATRZ_PRGRS_003");
|
||||
pipeSteps.length > 0 && pipeCompletedCount === pipeSteps.length;
|
||||
|
||||
const facCompletedCount = facSteps.filter((s) => s.atrzPrgrsSttsComCd === "ATRZ_PRGRS_003").length;
|
||||
const isFacCompleted =
|
||||
facSteps.length > 0 &&
|
||||
facSteps.every((s) => s.atrzPrgrsSttsComCd === "ATRZ_PRGRS_003");
|
||||
facSteps.length > 0 && facCompletedCount === facSteps.length;
|
||||
const isJoinReady = isPipeCompleted && isFacCompleted;
|
||||
|
||||
const stage2CountText =
|
||||
pipeSteps.length === facSteps.length
|
||||
? `(각 ${pipeSteps.length}인)`
|
||||
: `(관로 ${pipeSteps.length}인 / 시설 ${facSteps.length}인)`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-2">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-stretch">
|
||||
@@ -113,7 +119,7 @@ export function ElaprDocPrgrsForkJoinStep({ steps }: ElaprDocPrgrsForkJoinStepPr
|
||||
<div className="flex items-center gap-2 border-b border-blue-200/60 pb-2">
|
||||
<GitFork className="size-5 text-blue-600" />
|
||||
<h4 className="text-base font-bold text-blue-950">
|
||||
Stage 2: 부서별 병렬 전문검토 (각 2인)
|
||||
Stage 2: 부서별 병렬 전문검토 {stage2CountText}
|
||||
</h4>
|
||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-semibold text-blue-800">
|
||||
동시 진행
|
||||
@@ -131,7 +137,9 @@ export function ElaprDocPrgrsForkJoinStep({ steps }: ElaprDocPrgrsForkJoinStepPr
|
||||
: "bg-cyan-100 text-cyan-800"
|
||||
}`}
|
||||
>
|
||||
{isPipeCompleted ? "병합에 회신 완료 (2/2)" : "진행중"}
|
||||
{isPipeCompleted
|
||||
? `병합에 회신 완료 (${pipeCompletedCount}/${pipeSteps.length})`
|
||||
: `진행중 (${pipeCompletedCount}/${pipeSteps.length})`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -157,7 +165,9 @@ export function ElaprDocPrgrsForkJoinStep({ steps }: ElaprDocPrgrsForkJoinStepPr
|
||||
: "bg-indigo-100 text-indigo-800"
|
||||
}`}
|
||||
>
|
||||
{isFacCompleted ? "병합에 회신 완료 (2/2)" : "진행중"}
|
||||
{isFacCompleted
|
||||
? `병합에 회신 완료 (${facCompletedCount}/${facSteps.length})`
|
||||
: `진행중 (${facCompletedCount}/${facSteps.length})`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -205,7 +215,7 @@ export function ElaprDocPrgrsForkJoinStep({ steps }: ElaprDocPrgrsForkJoinStepPr
|
||||
<div className="flex items-center gap-2 border-b border-purple-200/60 pb-2">
|
||||
<CheckCircle2 className="size-5 text-purple-600" />
|
||||
<h4 className="text-base font-bold text-purple-950">
|
||||
Stage 3: 정책총괄 종합병합 및 환경청 회신 (2인)
|
||||
Stage 3: 정책총괄 종합병합 및 환경청 회신 ({aggSteps.length}인)
|
||||
</h4>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${
|
||||
|
||||
@@ -105,8 +105,8 @@ function ElaprDocPrgrsStatus({ elaprDocNo, taskSeComCd }: ElaprDocPrgrsStatusPro
|
||||
taskSeComCd === "SF_SP" ||
|
||||
list.some(
|
||||
(s) =>
|
||||
(s as { branchSeCd?: string }).branchSeCd === "PIPE" ||
|
||||
(s as { branchSeCd?: string }).branchSeCd === "FACILITY",
|
||||
(s as { branchSeCd?: string }).branchSeCd === "SP" ||
|
||||
(s as { branchSeCd?: string }).branchSeCd === "SF",
|
||||
) ||
|
||||
list.length >= 6;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import type { ReviewerOption } from "../model/reviewer-option";
|
||||
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
|
||||
import { SortableAplnPickerRow } from "./sortable-apln-picker-row";
|
||||
|
||||
type ForkJoinTrackKey = "PIPE" | "FACILITY" | "TOTAL_AGG";
|
||||
type ForkJoinTrackKey = "SP" | "SF" | "TOTAL_AGG";
|
||||
|
||||
interface TrackConfig {
|
||||
key: ForkJoinTrackKey;
|
||||
@@ -39,7 +39,7 @@ interface TrackConfig {
|
||||
|
||||
const TRACK_CONFIGS: TrackConfig[] = [
|
||||
{
|
||||
key: "PIPE",
|
||||
key: "SP",
|
||||
label: "하수관로 검토",
|
||||
defaultDeptNm: "하수관로부",
|
||||
guideText: "ℹ️ 하수관로 부서 검토·승인 완료 시 종합병합부서로 내부 회신(이관)됩니다.",
|
||||
@@ -47,7 +47,7 @@ const TRACK_CONFIGS: TrackConfig[] = [
|
||||
maxCount: 5,
|
||||
},
|
||||
{
|
||||
key: "FACILITY",
|
||||
key: "SF",
|
||||
label: "하수처리시설 검토",
|
||||
defaultDeptNm: "하수처리시설부",
|
||||
guideText: "ℹ️ 하수처리시설 부서 검토·승인 완료 시 종합병합부서로 내부 회신(이관)됩니다.",
|
||||
@@ -83,44 +83,80 @@ function createDefaultRow(
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultTrackRows(
|
||||
perTrackCount: number = 2,
|
||||
): Record<ForkJoinTrackKey, ElaprAplnRow[]> {
|
||||
const safeCount = Math.max(2, Math.min(5, perTrackCount));
|
||||
const sp: ElaprAplnRow[] = [];
|
||||
const sf: ElaprAplnRow[] = [];
|
||||
const totalAgg: ElaprAplnRow[] = [];
|
||||
|
||||
for (let seq = 1; seq <= safeCount; seq++) {
|
||||
sp.push(
|
||||
createDefaultRow(
|
||||
"SP",
|
||||
"하수관로부",
|
||||
seq,
|
||||
seq === 1 ? "ATRZ02" : "ATRZ03",
|
||||
),
|
||||
);
|
||||
sf.push(
|
||||
createDefaultRow(
|
||||
"SF",
|
||||
"하수처리시설부",
|
||||
seq,
|
||||
seq === 1 ? "ATRZ02" : "ATRZ03",
|
||||
),
|
||||
);
|
||||
totalAgg.push(
|
||||
createDefaultRow(
|
||||
"TOTAL_AGG",
|
||||
"정책총괄부",
|
||||
seq,
|
||||
seq === 1 ? "ATRZ04" : seq === safeCount ? "ATRZ99" : "ATRZ03",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return { SP: sp, SF: sf, TOTAL_AGG: totalAgg };
|
||||
}
|
||||
|
||||
export interface ElaprForkJoinPickerGridProps {
|
||||
onRowsChange?: (rows: ElaprAplnRow[]) => void;
|
||||
onEditingLockedChange?: (locked: boolean) => void;
|
||||
initialTrackRows?: Record<ForkJoinTrackKey, ElaprAplnRow[]> | null;
|
||||
/** 트랙당 기본 초기 결재자 수 (기본 2명, 필요 시 3명 지원) */
|
||||
defaultPerTrack?: number;
|
||||
}
|
||||
|
||||
export function ElaprForkJoinPickerGrid({
|
||||
onRowsChange,
|
||||
onEditingLockedChange,
|
||||
initialTrackRows,
|
||||
defaultPerTrack = 2,
|
||||
}: ElaprForkJoinPickerGridProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [activeTab, setActiveTab] = useState<ForkJoinTrackKey>("PIPE");
|
||||
const [activeTab, setActiveTab] = useState<ForkJoinTrackKey>("SP");
|
||||
|
||||
// 3개 트랙별 결재선 상태 (10월 12일 개정: 각 2명씩 총 6인 체제)
|
||||
// 3개 트랙별 결재선 상태 (기본 2인 또는 3인 초기화 지원)
|
||||
const [trackRows, setTrackRows] = useState<Record<ForkJoinTrackKey, ElaprAplnRow[]>>(() => {
|
||||
if (initialTrackRows) {
|
||||
return initialTrackRows;
|
||||
}
|
||||
return {
|
||||
PIPE: [
|
||||
createDefaultRow("PIPE", "하수관로부", 1, "ATRZ02"), // 검토
|
||||
createDefaultRow("PIPE", "하수관로부", 2, "ATRZ03"), // 승인 (병합에 회신)
|
||||
],
|
||||
FACILITY: [
|
||||
createDefaultRow("FACILITY", "하수처리시설부", 1, "ATRZ02"), // 검토
|
||||
createDefaultRow("FACILITY", "하수처리시설부", 2, "ATRZ03"), // 승인 (병합에 회신)
|
||||
],
|
||||
TOTAL_AGG: [
|
||||
createDefaultRow("TOTAL_AGG", "정책총괄부", 1, "ATRZ04"), // 병합 (10-12 신설)
|
||||
createDefaultRow("TOTAL_AGG", "정책총괄부", 2, "ATRZ99"), // 최종승인/전결 (환경청 회신)
|
||||
],
|
||||
SP: initialTrackRows.SP ?? [],
|
||||
SF: initialTrackRows.SF ?? [],
|
||||
TOTAL_AGG: initialTrackRows.TOTAL_AGG ?? [],
|
||||
};
|
||||
}
|
||||
return createDefaultTrackRows(defaultPerTrack);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialTrackRows) {
|
||||
setTrackRows(initialTrackRows);
|
||||
setTrackRows({
|
||||
SP: initialTrackRows.SP ?? [],
|
||||
SF: initialTrackRows.SF ?? [],
|
||||
TOTAL_AGG: initialTrackRows.TOTAL_AGG ?? [],
|
||||
});
|
||||
}
|
||||
}, [initialTrackRows]);
|
||||
|
||||
@@ -132,7 +168,7 @@ export function ElaprForkJoinPickerGrid({
|
||||
|
||||
// 통합 전체 결재선 목록
|
||||
const allRows = useMemo(() => {
|
||||
return [...trackRows.PIPE, ...trackRows.FACILITY, ...trackRows.TOTAL_AGG];
|
||||
return [...(trackRows.SP ?? []), ...(trackRows.SF ?? []), ...(trackRows.TOTAL_AGG ?? [])];
|
||||
}, [trackRows]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -178,10 +214,23 @@ export function ElaprForkJoinPickerGrid({
|
||||
newSeq,
|
||||
defaultSeComCd,
|
||||
);
|
||||
setTrackRows((prev) => ({
|
||||
setTrackRows((prev) => {
|
||||
const list = [...prev[activeTab]];
|
||||
if (activeTab === "TOTAL_AGG" && list.length > 0) {
|
||||
const lastIdx = list.length - 1;
|
||||
if (list[lastIdx].atrzSeComCd === "ATRZ99") {
|
||||
list[lastIdx] = {
|
||||
...list[lastIdx],
|
||||
atrzSeComCd: "ATRZ03",
|
||||
dcarbYn: "N",
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[activeTab]: [...prev[activeTab], newRow],
|
||||
}));
|
||||
[activeTab]: [...list, newRow],
|
||||
};
|
||||
});
|
||||
setEditingRowKey(newRow.rowKey);
|
||||
}, [activeTab, currentRows.length, currentTrackConfig, editingRowKey, alert]);
|
||||
|
||||
|
||||
@@ -108,10 +108,10 @@ function ElaprTmpltPickerDialog({
|
||||
}
|
||||
|
||||
const pipeRows = aplnRows
|
||||
.filter((r) => r.branchSeCd === "PIPE")
|
||||
.filter((r) => r.branchSeCd === "SP")
|
||||
.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
const facRows = aplnRows
|
||||
.filter((r) => r.branchSeCd === "FACILITY")
|
||||
.filter((r) => r.branchSeCd === "SF")
|
||||
.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
const aggRows = aplnRows
|
||||
.filter((r) => r.branchSeCd === "TOTAL_AGG")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BranchSeCd } from "@/entities/elapr";
|
||||
import type { CklstResultItem, CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn";
|
||||
|
||||
import {
|
||||
@@ -142,3 +143,108 @@ export function toCklstSaveDto(
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 결재자 branch_se_cd 기반 체크리스트 독립 Validation 유틸리티 (10월 14일 개정)
|
||||
* 결재자의 branch_se_cd('SP', 'SF', 'TOTAL_AGG', 'MAIN')에 따라 해당 탭 항목만 검증합니다.
|
||||
*/
|
||||
export function validateChecklistByBranch(
|
||||
checklistItems: {
|
||||
taskSeComCd: string;
|
||||
bchckUseYn: string;
|
||||
selected?: string; // 'Y' | 'N'
|
||||
picRvwCn?: string; // 검토자의견
|
||||
cklstRvwItemNm: string;
|
||||
cklstRvwSn: number;
|
||||
}[],
|
||||
approverBranchSeCd: BranchSeCd,
|
||||
docTaskSeComCd: string,
|
||||
): { isValid: boolean; errorMessage?: string; errorItemSn?: number } {
|
||||
// TOTAL_AGG(종합병합): 관로/시설 병렬 검토 완료 후 총괄취합 단계이므로 체크리스트 검증 생략
|
||||
if (approverBranchSeCd === "TOTAL_AGG") {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
let targetTaskCodes: string[] = [];
|
||||
if (approverBranchSeCd === "SP") {
|
||||
targetTaskCodes = ["SP"]; // 관로부: SP 탭만 검증 (시설부 미작성 무관)
|
||||
} else if (approverBranchSeCd === "SF") {
|
||||
targetTaskCodes = ["SF"]; // 시설부: SF 탭만 검증 (관로부 미작성 무관)
|
||||
} else {
|
||||
// 단일/순차 결재 (MAIN 등): 콤마 분리, 또는 "MAIN"/누락 시 체크리스트 실제 공종 전체 자동 추출
|
||||
const parsedCodes =
|
||||
docTaskSeComCd && docTaskSeComCd !== "MAIN"
|
||||
? docTaskSeComCd.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
targetTaskCodes =
|
||||
parsedCodes.length > 0
|
||||
? parsedCodes
|
||||
: Array.from(new Set(checklistItems.map((i) => i.taskSeComCd).filter(Boolean)));
|
||||
}
|
||||
|
||||
const activeItems = checklistItems.filter(
|
||||
(item) => targetTaskCodes.includes(item.taskSeComCd) && item.bchckUseYn === "Y",
|
||||
);
|
||||
|
||||
for (const item of activeItems) {
|
||||
const taskName =
|
||||
item.taskSeComCd === "SP"
|
||||
? "하수관로"
|
||||
: item.taskSeComCd === "RE"
|
||||
? "재이용수"
|
||||
: "하수처리시설";
|
||||
|
||||
// 1. 적합/부적합 미선택 검증
|
||||
if (!item.selected || !["Y", "N"].includes(item.selected)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: `[${taskName}] 체크리스트 '${item.cklstRvwItemNm}' 항목의 적합 여부를 선택해주세요.`,
|
||||
errorItemSn: item.cklstRvwSn,
|
||||
};
|
||||
}
|
||||
// 2. 부적합('N') 시 검토자 검토의견 필수 검증
|
||||
if (item.selected === "N" && (!item.picRvwCn || item.picRvwCn.trim() === "")) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: `[${taskName}] 체크리스트 '${item.cklstRvwItemNm}' 항목이 '부적합'입니다. 검토자 검토의견을 반드시 입력해주세요.`,
|
||||
errorItemSn: item.cklstRvwSn,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* ChecklistFormState와 CklstRvwRsltTreeDto[]를 받아 결재자 브랜치별 체크리스트 검증을 실행하는 헬퍼
|
||||
*/
|
||||
export function validateChecklistFormStateByBranch({
|
||||
rows,
|
||||
formState,
|
||||
reviewStep = "KECO",
|
||||
approverBranchSeCd,
|
||||
docTaskSeComCd,
|
||||
}: {
|
||||
rows: CklstRvwRsltTreeDto[];
|
||||
formState: ChecklistFormState;
|
||||
reviewStep?: ReviewStepCode;
|
||||
approverBranchSeCd: BranchSeCd;
|
||||
docTaskSeComCd: string;
|
||||
}): { isValid: boolean; errorMessage?: string; errorItemSn?: number } {
|
||||
const displayRows = getChecklistDisplayRows(rows);
|
||||
const checklistItems = displayRows.map((row) => {
|
||||
const key = buildChecklistRowKey(row, reviewStep);
|
||||
return {
|
||||
taskSeComCd: row.taskSeComCd,
|
||||
bchckUseYn: row.bchckUseYn,
|
||||
selected: formState.results[key] ?? (row.rvwRsltAnmlyYn as string | undefined),
|
||||
picRvwCn: formState.comments[key] ?? row.picRvwCn,
|
||||
cklstRvwItemNm: row.cklstArtclNm ?? "검토항목",
|
||||
cklstRvwSn: row.cklstRvwSn,
|
||||
};
|
||||
});
|
||||
|
||||
return validateChecklistByBranch(checklistItems, approverBranchSeCd, docTaskSeComCd);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,11 @@ import {
|
||||
getButtonConfigs,
|
||||
needsRoleUser01AtrzResolve,
|
||||
} from "../../lib/button-config";
|
||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import type { BranchSeCd } from "@/entities/elapr";
|
||||
import {
|
||||
computeReviewStepChecklistProgress,
|
||||
validateChecklistFormStateByBranch,
|
||||
} from "../../lib/checklist-utils";
|
||||
import {
|
||||
buildFileSplmntReqPayload,
|
||||
buildRvwRsltSplmntReqPayload,
|
||||
@@ -536,16 +540,38 @@ function FloatingActionButtons({
|
||||
techReviewOpinionRef?.current?.resetForm();
|
||||
}
|
||||
|
||||
function handleApprovalOpen() {
|
||||
if (prgrsSttsComCd === "TE" && userPendingBranchSeCd) {
|
||||
const branchCd = (userPendingBranchSeCd as BranchSeCd) || (isForkJoin ? "SP" : "MAIN");
|
||||
const validation = validateChecklistFormStateByBranch({
|
||||
rows: data.cklstResultList ?? [],
|
||||
formState: checklistFormState,
|
||||
reviewStep: "KECO",
|
||||
approverBranchSeCd: branchCd,
|
||||
docTaskSeComCd: normalizedTaskSeComCd || data.detDto?.taskSeComCd || "MAIN",
|
||||
});
|
||||
|
||||
if (!validation.isValid) {
|
||||
alert(validation.errorMessage || "체크리스트 항목을 모두 검토해야 결재가 가능합니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setActiveDialog("elaprApproval");
|
||||
}
|
||||
|
||||
async function handleRvwCmptnClick() {
|
||||
if (prgrsSttsComCd === "TE") {
|
||||
const { percent } = computeReviewStepChecklistProgress(
|
||||
data.cklstResultList ?? [],
|
||||
checklistFormState.results,
|
||||
"KECO",
|
||||
);
|
||||
const branchCd = (userPendingBranchSeCd as BranchSeCd) || (isForkJoin ? "SP" : "MAIN");
|
||||
const validation = validateChecklistFormStateByBranch({
|
||||
rows: data.cklstResultList ?? [],
|
||||
formState: checklistFormState,
|
||||
reviewStep: "KECO",
|
||||
approverBranchSeCd: branchCd,
|
||||
docTaskSeComCd: normalizedTaskSeComCd || data.detDto?.taskSeComCd || "MAIN",
|
||||
});
|
||||
|
||||
if (percent !== 100) {
|
||||
alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
|
||||
if (!validation.isValid) {
|
||||
alert(validation.errorMessage || "모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -818,7 +844,7 @@ function FloatingActionButtons({
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) &&
|
||||
(!isForkJoin ||
|
||||
(userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ04")) && (
|
||||
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isBusy}>
|
||||
<Button onClick={handleApprovalOpen} disabled={isBusy}>
|
||||
{isForkJoin ? "결재 (병합)" : "결재(승인)"}
|
||||
</Button>
|
||||
)}
|
||||
@@ -832,7 +858,7 @@ function FloatingActionButtons({
|
||||
? "green"
|
||||
: undefined
|
||||
}
|
||||
onClick={() => setActiveDialog("elaprApproval")}
|
||||
onClick={handleApprovalOpen}
|
||||
disabled={isBusy}
|
||||
>
|
||||
{isForkJoin
|
||||
|
||||
Reference in New Issue
Block a user