[as frontend] 체크리스트 입력 저장시 validation 및 추가로 서버 저장로직으로 처리

This commit is contained in:
2026-06-02 19:10:56 +09:00
parent 56f52194a6
commit 2a8c2ae542
@@ -159,7 +159,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
// userInfo?.dtlInstCd 테스트 코드 ENV.ROLE_USER03 , KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
const userInfo = {
...userInfo2,
dtlInstCd: "ROLE_USER03", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
dtlInstCd: "ROLE_USER05", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
};
const alert = useAlertStore((state) => state.setMessage);
const qc = useQueryClient();
@@ -203,17 +203,22 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
}, [currentDet]);
const [docSeComCd] = useState("INSTL");
const [taskSeComCdNm, setTaskSeComCdNm] = useState("재이용수");
const [taskSeComCd, setTaskSeComCd] = useState("RE");
const [taskSeComCdNm, setTaskSeComCdNm] = useState<string>(
() => (currentDet?.taskSeComCdNm as string) ?? "재이용수",
);
const [taskSeComCd, setTaskSeComCd] = useState<string>(() => (currentDet?.taskSeComCd as string) ?? "RE");
useEffect(() => {
// do not override server-provided initialData (modify mode)
if (currentDet) return;
const raw = sessionStorage.getItem(INSTL_SNCN_REGISTER_SELECTION_KEY);
if (!raw) return;
try {
const parsed = JSON.parse(raw) as {
const parsed = JSON.parse(raw) as {
taskSeComCdList?: string[];
taskSeComCdNmList?: string[];
taskSeComCdNmList?: string[];
};
setTaskSeComCd(parsed.taskSeComCdList?.join(",") || "RE");
@@ -223,7 +228,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
} finally {
sessionStorage.removeItem(INSTL_SNCN_REGISTER_SELECTION_KEY);
}
}, []);
}, [currentDet]);
/* ── 기본 정보 ── */
const [bzNm, setBzNm] = useState("");
const [bzBgngYr, setBzBgngYr] = useState("");
@@ -429,6 +434,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
}
// console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current);
// 수정모드인 경우 서버가 전달한 업무구분(단일 혹은 콤마로 결합된 복수)을 상태로 복원
setTaskSeComCd(det.taskSeComCd);
setTaskSeComCdNm(det.taskSeComCdNm );
// 매핑 완료 플래그 설정 — 이후 동일 컴포넌트 인스턴스에서 재매핑 방지
initialMappedRef.current = true;
@@ -620,6 +629,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const [checkResults, setCheckResults] = useState<Record<string, CheckResultVal>>({});
const [checkComments, setCheckComments] = useState<Record<string, string>>({});
const cklstRowRefs = useRef<Record<string, HTMLTableRowElement | null>>({});
const [pendingInvalidKey, setPendingInvalidKey] = useState<string | null>(null);
const { data: cklstTree = [] } = useQuery<CklstTreeNode[]>({
queryKey: ["cklstTree", docSeComCd, taskSeComCd],
@@ -699,8 +709,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const codes = Array.from(
new Set((cklstTree ?? []).map((r) => (r as any).taskSeComCd).filter(Boolean)),
);
console.log("Available taskSeComCd codes in checklist tree:", codes);
if (codes.length === 0) {
if (selectedTaskCode !== null) setSelectedTaskCode(null);
return;
@@ -715,7 +724,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const cklstKey = (row: CklstTreeNode, rvwStpComCd?: string) => {
const stepCode = rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD;
return `${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${stepCode}`;
const taskCode = (row as any).taskSeComCd ?? "";
return `${taskCode}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${stepCode}`;
};
useEffect(() => {
@@ -733,9 +743,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
});
});
// 행이 없어졌을 때 stale key만 정리 (현재 화면에 노출되는 step 기준)
// 행이 없어졌을 때 stale key만 정리
// build liveKeys from full cklstTree so inputs for other taskSeComCd are preserved
const liveKeys = new Set<string>();
cklstRows.forEach((row) => {
(cklstTree ?? []).forEach((row) => {
visibleReviewStepCodes.forEach((stepCode) => {
liveKeys.add(cklstKey(row, stepCode));
});
@@ -770,7 +781,21 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
return changed ? next : prev;
});
}, [cklstRows, visibleReviewStepCodes]);
}, [cklstTree, visibleReviewStepCodes]);
// when a previously-missing invalid key is set (due to switching task code),
// try to scroll/focus it after the rows have re-rendered
useEffect(() => {
if (!pendingInvalidKey) return;
const el = cklstRowRefs.current[pendingInvalidKey];
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
const firstRadio = el.querySelector<HTMLInputElement>(`input[name="chk_${pendingInvalidKey}"]`);
firstRadio?.focus();
setPendingInvalidKey(null);
}
// otherwise, wait for cklstRows update (effect will re-run when pendingInvalidKey remains set)
}, [pendingInvalidKey, cklstRows]);
// 수정 모드에서 서버의 체크리스트 결과값을 폼 상태로 반영
useEffect(() => {
@@ -785,7 +810,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const stepCode = (r.rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD) as ReviewStepCode;
if (!REVIEW_STEP_CODES.includes(stepCode)) continue;
stepCodesInResult.add(stepCode);
const key = `${r.cklstSn}_${r.cklstArtclSn ?? ""}_${r.cklstRvwSn}_${stepCode}`;
const key = `${r.taskSeComCd ?? ""}_${r.cklstSn}_${r.cklstArtclSn ?? ""}_${r.cklstRvwSn}_${stepCode}`;
if (r.rvwRsltAnmlyYn === "Y" || r.rvwRsltAnmlyYn === "N") lookup.set(key, r.rvwRsltAnmlyYn);
if (r.picRvwCn) commentLookup.set(key, String(r.picRvwCn));
}
@@ -839,28 +864,39 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
}, [cklstRows, checkResults, selectedReviewStep]);
const toCklstSaveDtoList = () =>
{
const persistedStepCodeSet = new Set<ReviewStepCode>();
normalizeChecklistResultRows(initialData).forEach((r: any) => {
const code = r?.rvwStpComCd as ReviewStepCode | undefined;
if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code);
});
visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code));
const toCklstSaveDtoList = () => {
const persistedStepCodeSet = new Set<ReviewStepCode>();
normalizeChecklistResultRows(initialData).forEach((r: any) => {
const code = r?.rvwStpComCd as ReviewStepCode | undefined;
if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code);
});
// Always persist currently visible review steps as well
visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code));
// only persist rows that are enabled for checkbox (`bchckUseYn === "Y")
// only persist rows that are enabled for checkbox (`bchckUseYn === "Y")
const relevantRows = cklstRows.filter((row) => row.bchckUseYn === "Y");
return Array.from(persistedStepCodeSet).flatMap((rvwStpComCd) =>
relevantRows.map((row) => ({
cklstSn: row.cklstSn,
cklstArtclSn: row.cklstArtclSn,
cklstRvwSn: row.cklstRvwSn,
rvwStpComCd: rvwStpComCd,
selected: checkResults[cklstKey(row, rvwStpComCd)] ?? null,
picRvwCn: checkComments[cklstKey(row, rvwStpComCd)] ?? null,
})),
);
};
// Use full cklstTree so items not currently visible (other task codes) are included
const relevantRows = (cklstTree ?? []).filter((row) => row.bchckUseYn === "Y");
// Ensure we generate DTOs grouped by all known task codes (TASK_CODE_LABELS keys)
const allTaskCodes = Object.keys(TASK_CODE_LABELS);
return allTaskCodes.flatMap((taskCode) =>
Array.from(persistedStepCodeSet).flatMap((rvwStpComCd) =>
relevantRows
.filter((row) => ((row as any).taskSeComCd ?? "") === taskCode)
.map((row) => ({
taskSeComCd: taskCode,
cklstSn: row.cklstSn,
cklstArtclSn: row.cklstArtclSn,
cklstRvwSn: row.cklstRvwSn,
rvwStpComCd: rvwStpComCd,
selected: checkResults[cklstKey(row, rvwStpComCd)] ?? null,
picRvwCn: checkComments[cklstKey(row, rvwStpComCd)] ?? null,
})),
),
);
};
// removed map-shaped payload generation — server now expects `cklstResultList` only
@@ -1053,19 +1089,32 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
return false;
}
// 저장 시: bchckUseYn =="Y" 일경우 검토 결과 라디오 박스가 반드시 선택되어야 함
const invalidYesRow = cklstRows.find(
(row) => row.bchckUseYn === "Y" && !checkResults[cklstKey(row, editableReviewStep)],
// 저장 시: bchckUseYn =="Y" 인 경우, 화면에 표시되는 모든 review step에 대해
// 검토 결과가 선택되어 있어야 함 (visibleReviewStepCodes 기준)
// check across all checklist items (not only currently visible rows)
const allRelevant = (cklstTree ?? []).filter((row) => row.bchckUseYn === "Y");
const missingRow = allRelevant.find((row) =>
visibleReviewStepCodes.some((step) => !checkResults[cklstKey(row, step)]),
);
if (invalidYesRow) {
const invalidKey = cklstKey(invalidYesRow, editableReviewStep);
console.warn("검토 결과 미선택 항목 발견:", invalidKey, invalidYesRow);
if (missingRow) {
const missingStep = visibleReviewStepCodes.find((step) => !checkResults[cklstKey(missingRow, step)])!;
const invalidKey = cklstKey(missingRow, missingStep);
console.warn("검토 결과 미선택 항목 발견:", invalidKey, missingRow, missingStep);
const invalidRowEl = cklstRowRefs.current[invalidKey];
invalidRowEl?.scrollIntoView({ behavior: "smooth", block: "center" });
// 라디오 그룹의 첫 입력에 포커스(예:Y 또는 아니오:N)
const firstRadio = invalidRowEl?.querySelector<HTMLInputElement>(`input[name="chk_${invalidKey}"]`);
firstRadio?.focus();
alert(`체크리스트 항목 "${invalidYesRow.rvwCn}"의 검토결과를 선택하세요.`);
if (invalidRowEl) {
invalidRowEl.scrollIntoView({ behavior: "smooth", block: "center" });
const firstRadio = invalidRowEl.querySelector<HTMLInputElement>(`input[name="chk_${invalidKey}"]`);
firstRadio?.focus();
} else {
// row not visible: switch header filter to the missing row's task code,
// then focus after render via pendingInvalidKey
const taskCode = (missingRow as any).taskSeComCd ?? null;
if (taskCode) setSelectedTaskCode(taskCode);
setPendingInvalidKey(invalidKey);
}
alert(`체크리스트 항목 "${missingRow.rvwCn}"의 검토결과를 선택하세요.`);
return false;
}
}