[as elapr] 화면에서 동적으로 결재자 숫자 변경및 체크 리스트 SP, SF 필수 로직으로 저장 변경

This commit is contained in:
2026-10-14 18:00:37 +09:00
parent 6c71bccc27
commit dd4ee1dcb5
11 changed files with 273 additions and 81 deletions
@@ -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