@@ -248,12 +253,12 @@ function CnfmListPage() {
className="cnfm-row-clickable"
onClick={() => router.push(`/instl-sncn/${row.instlSncnDocNo}`)}
>
- | {row.sidoNm || "-"} |
- {row.sggNm || "-"} |
+ {row.sido || "-"} |
+ {row.gugun|| "-"} |
{row.bizNm || "-"} |
{row.taskSeCdNm || row.taskSeComCd || "-"} |
- {row.chrgrNm || row.aplcntNm || "-"} |
- {row.instNm || row.aplcntOgdpNm || "-"} |
+ {row.aplcntNm || "-"} |
+ {row.envoSeCdNm || "-"} |
@@ -367,11 +372,15 @@ function CnfmListPage() {
{fnrsCodeList.map((item) => (
@@ -387,11 +396,11 @@ function CnfmListPage() {
>
취소
-
diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-modify-page.tsx b/apps/as/src/pages/instl-sncn-old/ui/cnfm-modify-page.tsx
similarity index 100%
rename from apps/as/src/pages/instl-sncn/ui/cnfm-modify-page.tsx
rename to apps/as/src/pages/instl-sncn-old/ui/cnfm-modify-page.tsx
diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-page.css b/apps/as/src/pages/instl-sncn-old/ui/cnfm-page.css
similarity index 100%
rename from apps/as/src/pages/instl-sncn/ui/cnfm-page.css
rename to apps/as/src/pages/instl-sncn-old/ui/cnfm-page.css
diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx b/apps/as/src/pages/instl-sncn-old/ui/cnfm-regist-page.tsx
similarity index 80%
rename from apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx
rename to apps/as/src/pages/instl-sncn-old/ui/cnfm-regist-page.tsx
index 7f815ea84..0f178f101 100644
--- a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx
+++ b/apps/as/src/pages/instl-sncn-old/ui/cnfm-regist-page.tsx
@@ -12,10 +12,12 @@ import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT, PROXY_HOST } from "@/shared/config"
import { getCookie, setCookie, useAlertStore, useUserStore } from "@/shared/lib";
import { deleteAttachment } from "@/shared/lib/attach";
import { formatFileSize } from "@/shared/lib/file-utils";
-import { type AcntgSeMngTreeDto, useAcntgSeMngTree } from "@/entities/acntg";
+import type { AcntgSeMngTreeDto } from "@/entities/acntg";
+import { acntgSeMngService } from "@/entities/acntg";
import type { CklstTreeNode } from "@/entities/cklst";
import { cklstMngService } from "@/entities/cklst";
-import { type CnstpSeMngTreeDto, useCnstpSeMngTree } from "@/entities/cnstp";
+import type { CnstpSeMngTreeDto } from "@/entities/cnstp";
+import { cnstpSeMngService } from "@/entities/cnstp";
import { esntlAtchFileMngService, type EsntlAtchFileMngTreeResponseDto } from "@/entities/esntl";
import {
INSTL_SNCN_REGISTER_SELECTION_KEY,
@@ -23,7 +25,8 @@ import {
type InstlSncnApplySaveRequestDto,
type InstlSncnBizCstSaveRequestDto,
type InstlSncnBzScaleSaveDto,
- type InstlSncnMng,
+ type InstlSncnMngResponseDto,
+ InstlSncnMngSaveRequestDto,
} from "@/entities/instl-sncn";
import { EtcAttachments, NumCell, PrivacyConsent, ReadCell } from "@/widgets/cnfm";
import { Button, Input } from "@common/components";
@@ -97,7 +100,7 @@ const INSTL_PROGRESS_ORDER = [
const getProgressStepCount = (prgrsSttsComCd?: string): number => {
const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd ?? "ST");
if (idx <= INSTL_PROGRESS_ORDER.indexOf("ST")) return 1;
- if (idx <= INSTL_PROGRESS_ORDER.indexOf("TP")) return 2;
+ if (idx <= INSTL_PROGRESS_ORDER.indexOf("ER")) return 2;
return 3;
};
@@ -106,14 +109,17 @@ const getRoleStepCount = (dtlInstCd?: string): number => {
return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0;
};
+// 업무구분 코드 -> 표시명 매핑 (체크리스트 그룹 표시용)
+const TASK_CODE_LABELS: Record = {
+ RE: "재이용수",
+ SP: "하수관로",
+ SF: "하수처리시설",
+};
+
const normalizeChecklistResultRows = (payload: any): any[] => {
- // support new API field `cklstResultMap` while keeping backward-compatibility
- const raw = payload?.cklstResultMap ?? payload?.cklstResultList;
- if (Array.isArray(raw)) return raw;
- if (raw && typeof raw === "object") {
- return Object.values(raw).flatMap((rows) => (Array.isArray(rows) ? rows : []));
- }
- return [];
+ // expect flat `cklstResultList` only
+ const raw = payload?.cklstResultList ?? [];
+ return Array.isArray(raw) ? raw : [];
};
/* ------------------------------------------------------------------ */
@@ -148,6 +154,13 @@ const normalizeChecklistResultRows = (payload: any): any[] => {
function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
+ // userInfo?.dtlInstCd 테스트 코드 ENV.ROLE_USER03 , KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
+ /*
+ const userInfo = {
+ ...userInfo2,
+ dtlInstCd: "ROLE_USER03", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
+ };
+ */
const alert = useAlertStore((state) => state.setMessage);
const qc = useQueryClient();
const currentDet = useMemo(() => initialData?.detDto ?? initialData, [initialData]);
@@ -172,37 +185,51 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
useEffect(() => {
if (visibleReviewStepCodes.length === 0) return;
- if (editableReviewStep && selectedReviewStep !== editableReviewStep) {
- setSelectedReviewStep(editableReviewStep);
- return;
- }
- if (!visibleReviewStepCodes.includes(selectedReviewStep)) {
- setSelectedReviewStep(visibleReviewStepCodes[visibleReviewStepCodes.length - 1]);
- }
- }, [editableReviewStep, selectedReviewStep, visibleReviewStepCodes]);
+ // Only set default/initial selection when visible steps or editable step change.
+ // Do NOT include `selectedReviewStep` in deps to avoid overwriting user clicks.
+ setSelectedReviewStep((current) => {
+ // keep current if still visible
+ if (current && visibleReviewStepCodes.includes(current)) return current;
+ // prefer editable step if available
+ if (editableReviewStep) return editableReviewStep;
+ // otherwise pick the last visible step
+ return visibleReviewStepCodes[visibleReviewStepCodes.length - 1];
+ });
+ }, [editableReviewStep, visibleReviewStepCodes]);
const docId = useMemo(() => {
return currentDet?.instlSncnDocNo ?? "";
}, [currentDet]);
const [docSeComCd] = useState("INSTL");
- const [taskSeComCdNm, setTaskSeComCdNm] = useState("재이용수");
- const [taskSeComCd, setTaskSeComCd] = useState("RE");
+ const [taskSeComCdNm, setTaskSeComCdNm] = useState(
+ () => (currentDet?.taskSeComCdNm as string) ?? "",
+ );
+ const [taskSeComCd, setTaskSeComCd] = useState(
+ () => (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 { taskSeComCd?: string; taskSeComCdNm?: string };
- setTaskSeComCd(parsed.taskSeComCd || "RE");
- setTaskSeComCdNm(parsed.taskSeComCdNm || "재이용수");
+ const parsed = JSON.parse(raw) as {
+ taskSeComCdList?: string[];
+ taskSeComCdNmList?: string[];
+ };
+
+ setTaskSeComCd(parsed.taskSeComCdList?.join(",") || "RE");
+ setTaskSeComCdNm(parsed.taskSeComCdNmList?.join(",") || "재이용수");
} catch {
// noop
} finally {
sessionStorage.removeItem(INSTL_SNCN_REGISTER_SELECTION_KEY);
}
- }, []);
+ }, [currentDet]);
/* ── 기본 정보 ── */
const [bzNm, setBzNm] = useState("");
const [bzBgngYr, setBzBgngYr] = useState("");
@@ -232,6 +259,9 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
staleTime: Infinity,
});
+ // selected task code for header filtering (null = none selected)
+ const [selectedTaskCode, setSelectedTaskCode] = useState(null);
+
const [requiredFiles, setRequiredFiles] = useState>(
{},
);
@@ -405,6 +435,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
}
// console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current);
+ // 수정모드인 경우 서버가 전달한 업무구분(단일 혹은 콤마로 결합된 복수)을 상태로 복원
+ setTaskSeComCd(det.taskSeComCd);
+ setTaskSeComCdNm(det.taskSeCdNm);
+
// 매핑 완료 플래그 설정 — 이후 동일 컴포넌트 인스턴스에서 재매핑 방지
initialMappedRef.current = true;
@@ -597,18 +631,47 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
/* ── 체크리스트 ── */
type CheckResultVal = "Y" | "N" | null;
const [checkResults, setCheckResults] = useState>({});
+ const [checkComments, setCheckComments] = useState>({});
const cklstRowRefs = useRef>({});
+ const [pendingInvalidKey, setPendingInvalidKey] = useState(null);
const { data: cklstTree = [] } = useQuery({
queryKey: ["cklstTree", docSeComCd, taskSeComCd],
- queryFn: () => cklstMngService.findTree({ docSeComCd, taskSeComCd }),
+ queryFn: async () => {
+ // support comma-separated task codes (e.g. "RE,SP") by splitting
+ const raw = taskSeComCd ?? "";
+ const codes = raw
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean);
+
+ if (codes.length === 0) return [] as CklstTreeNode[];
+ if (codes.length === 1) {
+ return await cklstMngService.findTree({ docSeComCd, taskSeComCd: codes[0] });
+ }
+
+ const results = await Promise.all(
+ codes.map((code) => cklstMngService.findTree({ docSeComCd, taskSeComCd: code })),
+ );
+ const flat = results.flat();
+ // dedupe by cklstRvwSn, keep first occurrence
+ const map = new Map();
+ for (const row of flat) {
+ if (!map.has(row.cklstRvwSn)) map.set(row.cklstRvwSn, row);
+ }
+ return Array.from(map.values());
+ },
enabled: Boolean(docSeComCd) && Boolean(taskSeComCd),
staleTime: Infinity,
});
const cklstRows = useMemo(() => {
// bchckUseYn 필터 제거: 부모가 있는 자식 노드(bchckUseYn=N)도 트리에 포함
- const useRows = cklstTree;
+ // cklstTree 내의 피드 taskSeComCd 에 따라서 그룹핑 되도록 해줘
+
+ const useRows = selectedTaskCode
+ ? cklstTree.filter((r) => (r as any).taskSeComCd === selectedTaskCode)
+ : cklstTree;
if (useRows.length === 0) return [] as CklstTreeNode[];
// artcl 단위로 분리하지 않고 전체 rows에서 단일 childMap을 구성한다.
@@ -642,11 +705,30 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
walk(null);
return ordered;
- }, [cklstTree]);
+ }, [cklstTree, selectedTaskCode]);
+
+ // when cklstTree changes, default selectedTaskCode to first available code
+ useEffect(() => {
+ const codes = Array.from(
+ new Set((cklstTree ?? []).map((r) => (r as any).taskSeComCd).filter(Boolean)),
+ );
+
+ if (codes.length === 0) {
+ if (selectedTaskCode !== null) setSelectedTaskCode(null);
+ return;
+ }
+
+ // if there's no selection yet, or the current selection is not in the new codes,
+ // pick the first available code
+ if (!selectedTaskCode || !codes.includes(selectedTaskCode)) {
+ setSelectedTaskCode(codes[0]);
+ }
+ }, [cklstTree, selectedTaskCode]);
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(() => {
@@ -664,9 +746,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();
- cklstRows.forEach((row) => {
+ (cklstTree ?? []).forEach((row) => {
visibleReviewStepCodes.forEach((stepCode) => {
liveKeys.add(cklstKey(row, stepCode));
});
@@ -701,7 +784,23 @@ 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(
+ `input[name="chk_${pendingInvalidKey}"]`,
+ );
+ firstRadio?.focus();
+ setPendingInvalidKey(null);
+ }
+ // otherwise, wait for cklstRows update (effect will re-run when pendingInvalidKey remains set)
+ }, [pendingInvalidKey, cklstRows]);
// 수정 모드에서 서버의 체크리스트 결과값을 폼 상태로 반영
useEffect(() => {
@@ -710,13 +809,15 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
if (!Array.isArray(results) || results.length === 0) return;
const lookup = new Map();
+ const commentLookup = new Map();
const stepCodesInResult = new Set();
for (const r of results) {
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));
}
setCheckResults((prev) => {
@@ -730,7 +831,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
targetStepCodes.forEach((stepCode) => {
const k = cklstKey(row, stepCode);
const v = lookup.get(k);
- if (v && next[k] !== v) {
+ // only apply server-provided value when we don't already have a user-entered value
+ if (v && (next[k] === null || next[k] === undefined)) {
next[k] = v as CheckResultVal;
changed = true;
}
@@ -738,14 +840,29 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
}
return changed ? next : prev;
});
+ // set comment state separately to avoid rerendering conflicts
+ setCheckComments((prev) => {
+ const next: Record = { ...prev };
+ let changed = false;
+ for (const [k, v] of commentLookup.entries()) {
+ // only set comment if none exists yet (preserve user edits)
+ if (!next[k]) {
+ next[k] = v;
+ changed = true;
+ }
+ }
+ return changed ? next : prev;
+ });
}, [initialData, cklstRows, visibleReviewStepCodes]);
const cklstStats = useMemo(() => {
if (!selectedReviewStep) {
return { total: 0, completed: 0, percent: 0 };
}
- const total = cklstRows.length;
- const completed = cklstRows.reduce((acc, row) => {
+ // only include rows that are marked for checkbox use (bchckUseYn === "Y")
+ const relevantRows = cklstRows.filter((row) => row.bchckUseYn === "Y");
+ const total = relevantRows.length;
+ const completed = relevantRows.reduce((acc, row) => {
const val = checkResults[cklstKey(row, selectedReviewStep)] ?? "";
return acc + (val ? 1 : 0);
}, 0);
@@ -759,48 +876,35 @@ function CnfmRegistPage({ initialData }: { initialData?: 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));
- return Array.from(persistedStepCodeSet).flatMap((stepCode) =>
- cklstRows.map((row) => ({
- cklstSn: row.cklstSn,
- cklstArtclSn: row.cklstArtclSn,
- cklstRvwSn: row.cklstRvwSn,
- rvwStpComCd: stepCode,
- selected: checkResults[cklstKey(row, stepCode)] ?? null,
- })),
+ // only persist rows that are enabled for checkbox (`bchckUseYn === "Y")
+ // only persist rows that are enabled for checkbox (`bchckUseYn === "Y")
+ // 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,
+ })),
+ ),
);
};
- // 맵 형식으로 저장할 경우(업서트 처리용) - 키는 상세 코드(EX: LOCAL.지자체) 포맷
- const REVIEW_STEP_FULL: Record = {
- LOCAL: "LOCAL.지자체",
- ENV: "ENV.환경청",
- KECO: "KECO.환경공단",
- };
-
- const toCklstSaveDtoMap = () => {
- const persistedStepCodeSet = new Set();
- 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 result: Record = {};
- Array.from(persistedStepCodeSet).forEach((stepCode) => {
- const fullKey = REVIEW_STEP_FULL[stepCode];
- result[fullKey] = cklstRows.map((row) => ({
- cklstSn: row.cklstSn,
- cklstArtclSn: row.cklstArtclSn,
- cklstRvwSn: row.cklstRvwSn,
- rvwStpComCd: fullKey,
- selected: checkResults[cklstKey(row, stepCode)] ?? null,
- }));
- });
-
- return result;
- };
+ // removed map-shaped payload generation — server now expects `cklstResultList` only
const toEsntlAtchFileSaveDtoList = () =>
Object.values(requiredFiles)
@@ -815,7 +919,11 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const [bzScaleMap, setBzScaleMap] = useState>({});
/* ── 공종구분 트리 로드 ── */
- const { data: cnstpTree = [] } = useCnstpSeMngTree();
+ const { data: cnstpTree = [] } = useQuery({
+ queryKey: ["cnstpTree"],
+ queryFn: () => cnstpSeMngService.findTree(),
+ staleTime: Infinity,
+ });
/* ── 공종구분 리프 노드 집합 ── */
const cnstpLeafSet = useMemo(() => {
@@ -865,7 +973,11 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const [amtMap, setAmtMap] = useState>({});
/* ── 회계구분 트리 로드 ── */
- const { data: acntgTree = [] } = useAcntgSeMngTree();
+ const { data: acntgTree = [] } = useQuery({
+ queryKey: ["acntgTree"],
+ queryFn: () => acntgSeMngService.findTree(),
+ staleTime: Infinity, // 코드 데이터 — 앱 수명 동안 재요청 불필요
+ });
/* ── 리프 노드 집합 (자식이 없는 노드 = 직접 입력 가능) ── */
const leafSet = useMemo(() => {
@@ -962,13 +1074,15 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
}
// 현재 입력 단계의 체크리스트 미선택 항목이 있으면 진행 불가
+ /*
const hasUncheckedChecklist = cklstRows.some(
(row) => !checkResults[cklstKey(row, editableReviewStep)],
);
if (hasUncheckedChecklist) {
alert("체크리스트를 모두 체크하세요.");
return false;
- }
+ }
+ */
// 저장 시: 필수 첨부파일(esntlAtchYn=Y) 업로드 여부 검증
if (mode === "SAVE") {
const missingRequiredFile = requiredAttachmentDefs.find((item) => {
@@ -981,19 +1095,36 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
return false;
}
- // 저장 시: 사용 항목(Y)은 검토 결과가 반드시 예(Y)여야 함
- const invalidYesRow = cklstRows.find(
- (row) => row.bchckUseYn === "Y" && checkResults[cklstKey(row, editableReviewStep)] !== "Y",
+ // 저장 시: 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);
+ 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" });
- const yesRadio = invalidRowEl?.querySelector(
- `input[name="chk_${invalidKey}"][value="Y"]`,
- );
- yesRadio?.focus();
- alert(`${invalidYesRow.rvwCn} 검토내용은 예(Y)여야 합니다.`);
+ if (invalidRowEl) {
+ invalidRowEl.scrollIntoView({ behavior: "smooth", block: "center" });
+ const firstRadio = invalidRowEl.querySelector |