Merge branch 'iipbs/as/dev'

This commit is contained in:
2026-06-04 15:56:06 +09:00
19 changed files with 703 additions and 390 deletions
@@ -0,0 +1 @@
export { AcntgCodeManagementPage as default } from "@/pages/acntg-code-management";
@@ -0,0 +1 @@
export { CnstpCodeManagementPage as default } from "@/pages/cnstp-code-management";
@@ -1 +0,0 @@
export { CodeManagementPage as default } from "@/pages/code-management";
@@ -24,6 +24,8 @@ export interface CklstTreeNode {
rvwCn: string;
bchckUseYn: "Y" | "N";
sortSeq: number;
// optional: backend may include the taskSeComCd per node
taskSeComCd?: string;
}
export interface CklstTreeSearchParams {
@@ -29,6 +29,13 @@ export class InstlSncnApiService implements InstlSncnMngService {
);
return response.data;
}
public async techReview(payload: InstlSncnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(
`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/tech-review`,
payload,
);
return response.data;
}
}
export const InstlSncnApi = new InstlSncnApiService();
@@ -88,6 +88,7 @@ export interface InstlSncnCklstResultSaveDto {
cklstRvwSn: number;
rvwStpComCd: string;
selected: "Y" | "N" | null;
picRvwCn?: string | null;
}
/** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */
@@ -111,7 +112,7 @@ export interface InstlSncnMngDetResponse {
detDto: InstlSncnMng;
bizSclList: any[];
bizCstList: any[];
cklstResultMap?: Record<ReviewStepCode, any[]>;
cklstResultList?: any[];
esntlFileList: any[];
etcFileList: any[];
}
@@ -0,0 +1 @@
export { AcntgCodeManagementPage } from "./ui/acntg-code-management-page";
@@ -20,13 +20,15 @@ import { createAcntgSeColumns } from "../model/acntg-se-table-columns";
import { AcntgSeFormDialog, type AcntgSeFormValues } from "./acntg-se-form-dialog";
interface AcntgSeSectionProps {
pagination: PaginationState;
onPaginationChange: (pagination: PaginationState) => void;
}
const DEFAULT_PAGE_INDEX = 0;
const DEFAULT_PAGE_SIZE = 10;
function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps) {
function AcntgCodeManagementPage() {
const { confirm } = useConfirm();
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: DEFAULT_PAGE_INDEX,
pageSize: DEFAULT_PAGE_SIZE,
});
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
const [editingCd, setEditingCd] = useState<string | null>(null);
@@ -67,7 +69,7 @@ function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps)
state: { pagination },
onPaginationChange: (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
onPaginationChange(next);
setPagination(next);
},
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
@@ -114,31 +116,33 @@ function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps)
};
return (
<section className="card card-border mx-0 bg-background pb-6 pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">회계코드 관리</h3>
<Button type="button" onClick={handleOpenCreate}>
등록
</Button>
</div>
{tableComponent}
{dialogMode ? (
<AcntgSeFormDialog
mode={dialogMode}
open={Boolean(dialogMode)}
acntgSeCd={editingCd}
onOpenChange={(open) => {
if (!open) {
setDialogMode(null);
setEditingCd(null);
}
}}
isSubmitting={saveMutation.isPending || updateMutation.isPending}
onSubmit={handleFormSubmit}
/>
) : null}
</section>
<div className="space-y-6 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background pb-6 pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">회계코드 관리</h3>
<Button type="button" onClick={handleOpenCreate}>
등록
</Button>
</div>
{tableComponent}
{dialogMode ? (
<AcntgSeFormDialog
mode={dialogMode}
open={Boolean(dialogMode)}
acntgSeCd={editingCd}
onOpenChange={(open) => {
if (!open) {
setDialogMode(null);
setEditingCd(null);
}
}}
isSubmitting={saveMutation.isPending || updateMutation.isPending}
onSubmit={handleFormSubmit}
/>
) : null}
</section>
</div>
);
}
export { AcntgSeSection };
export { AcntgCodeManagementPage };
@@ -0,0 +1 @@
export { CnstpCodeManagementPage } from "./ui/cnstp-code-management-page";
@@ -23,13 +23,12 @@ import { CnstpSeFormDialog, type CnstpSeFormValues } from "./cnstp-se-form-dialo
const DEFAULT_PAGE_INDEX = 0;
const DEFAULT_PAGE_SIZE = 10;
interface CnstpSeSectionProps {
pagination: PaginationState;
onPaginationChange: (pagination: PaginationState) => void;
}
function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps) {
function CnstpCodeManagementPage() {
const { confirm } = useConfirm();
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: DEFAULT_PAGE_INDEX,
pageSize: DEFAULT_PAGE_SIZE,
});
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
const [editingCd, setEditingCd] = useState<string | null>(null);
@@ -70,7 +69,7 @@ function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps)
state: { pagination },
onPaginationChange: (updater) => {
const next = typeof updater === "function" ? updater(pagination) : updater;
onPaginationChange(next);
setPagination(next);
},
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
@@ -117,31 +116,33 @@ function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps)
};
return (
<section className="card card-border mx-0 bg-background pb-6 pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">공종코드 관리</h3>
<Button type="button" onClick={handleOpenCreate}>
등록
</Button>
</div>
{tableComponent}
{dialogMode ? (
<CnstpSeFormDialog
mode={dialogMode}
open={Boolean(dialogMode)}
cnstpSeCd={editingCd}
onOpenChange={(open) => {
if (!open) {
setDialogMode(null);
setEditingCd(null);
}
}}
isSubmitting={saveMutation.isPending || updateMutation.isPending}
onSubmit={handleFormSubmit}
/>
) : null}
</section>
<div className="space-y-6 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background pb-6 pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">공종코드 관리</h3>
<Button type="button" onClick={handleOpenCreate}>
등록
</Button>
</div>
{tableComponent}
{dialogMode ? (
<CnstpSeFormDialog
mode={dialogMode}
open={Boolean(dialogMode)}
cnstpSeCd={editingCd}
onOpenChange={(open) => {
if (!open) {
setDialogMode(null);
setEditingCd(null);
}
}}
isSubmitting={saveMutation.isPending || updateMutation.isPending}
onSubmit={handleFormSubmit}
/>
) : null}
</section>
</div>
);
}
export { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE };
export { CnstpCodeManagementPage };
@@ -1 +0,0 @@
export { CodeManagementPage } from "./ui/code-management-page";
@@ -1,30 +0,0 @@
"use client";
import { useState } from "react";
import type { PaginationState } from "@tanstack/react-table";
import { AcntgSeSection } from "./acntg-se-section";
import { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from "./cnstp-se-section";
function CodeManagementPage() {
const [cnstpPagination, setCnstpPagination] = useState<PaginationState>({
pageIndex: DEFAULT_PAGE_INDEX,
pageSize: DEFAULT_PAGE_SIZE,
});
const [acntgPagination, setAcntgPagination] = useState<PaginationState>({
pageIndex: DEFAULT_PAGE_INDEX,
pageSize: DEFAULT_PAGE_SIZE,
});
return (
<div className="space-y-6 px-[1.8rem] pb-6">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<CnstpSeSection pagination={cnstpPagination} onPaginationChange={setCnstpPagination} />
<AcntgSeSection pagination={acntgPagination} onPaginationChange={setAcntgPagination} />
</div>
</div>
);
}
export { CodeManagementPage };
@@ -71,15 +71,15 @@ function CnfmListPage() {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
const [selectedTaskSeComCd, setSelectedTaskSeComCd] = useState("");
const [selectedTaskSeComCd, setSelectedTaskSeComCd] = useState<string[]>([]);
const { data: fnrsCodeList = [] } = useCodeList("INSTL");
useEffect(() => {
if (!taskDialogOpen) return;
if (selectedTaskSeComCd) return;
if (fnrsCodeList.length === 0) return;
setSelectedTaskSeComCd(fnrsCodeList[0].lwpoCd);
if (selectedTaskSeComCd.length) return;
if (fnrsCodeList.length === 0) return;
// setSelectedTaskSeComCd([fnrsCodeList[0].lwpoCd]);
}, [taskDialogOpen, selectedTaskSeComCd, fnrsCodeList]);
const query = useQuery({
@@ -108,20 +108,25 @@ function CnfmListPage() {
};
const handleOpenTaskDialog = () => {
setSelectedTaskSeComCd((prev) => prev || fnrsCodeList[0]?.lwpoCd || "");
//setSelectedTaskSeComCd((prev) => (prev && prev.length ? prev : [fnrsCodeList[0]?.lwpoCd || ""]));
setTaskDialogOpen(true);
};
const handleConfirmTaskDialog = () => {
const selected = fnrsCodeList.find((item) => item.lwpoCd === selectedTaskSeComCd);
if (!selected) return;
if (!selectedTaskSeComCd || selectedTaskSeComCd.length === 0) {
alert("하나 이상의 업무구분을 선택하세요.");
return;
}
// prepare arrays of selected codes and names
const selectedCodes = selectedTaskSeComCd;
const selectedNames = selectedTaskSeComCd.map((code) => fnrsCodeList.find((item) => item.lwpoCd === code)?.lwpoCdNm || "");
sessionStorage.setItem(
INSTL_SNCN_REGISTER_SELECTION_KEY,
JSON.stringify({
taskSeComCd: selected.lwpoCd,
taskSeComCdNm: selected.lwpoCdNm,
}),
taskSeComCdList: selectedCodes,
taskSeComCdNmList: selectedNames
})
);
setTaskDialogOpen(false);
router.push("/instl-sncn/register");
@@ -367,11 +372,15 @@ function CnfmListPage() {
{fnrsCodeList.map((item) => (
<label key={item.lwpoCd} className="flex cursor-pointer items-center gap-2">
<input
type="radio"
type="checkbox"
name="taskSeComCdSelect"
value={item.lwpoCd}
checked={selectedTaskSeComCd === item.lwpoCd}
onChange={() => setSelectedTaskSeComCd(item.lwpoCd)}
checked={selectedTaskSeComCd.includes(item.lwpoCd)}
onChange={() => {
setSelectedTaskSeComCd((prev) =>
prev.includes(item.lwpoCd) ? prev.filter((v) => v !== item.lwpoCd) : [...prev, item.lwpoCd],
);
}}
/>
{item.lwpoCdNm}
</label>
@@ -387,11 +396,11 @@ function CnfmListPage() {
>
취소
</button>
<button
<button
type="button"
className="rounded bg-blue-500 px-5 py-2 text-[1.3rem] font-semibold text-white hover:bg-blue-600 disabled:cursor-not-allowed disabled:bg-gray-300"
onClick={handleConfirmTaskDialog}
disabled={!selectedTaskSeComCd}
disabled={!selectedTaskSeComCd || selectedTaskSeComCd.length === 0}
>
확인
</button>
File diff suppressed because it is too large Load Diff
@@ -7,10 +7,12 @@ import { Button } from "@common/components";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { InstlSncnApi } from "@/entities/instl-sncn";
// InstlSncnApi imported below with types
import { PROXY_HOST } from "@/shared/config";
import { formatFileSize } from "@/shared/lib/file-utils";
import { dayjs, useUserStore } from "@/shared/lib";
import axios from "axios";
import { dayjs, useUserStore, useAlertStore } from "@/shared/lib";
import { InstlSncnApi, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
import "./cnfm-page.css";
@@ -55,6 +57,13 @@ const getRoleStepCount = (dtlInstCd?: string): number => {
return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0;
};
// 업무구분 코드 -> 표시명 매핑 (체크리스트 그룹 표시용)
const TASK_CODE_LABELS: Record<string, string> = {
RE: "재이용수",
SP: "하수관로",
SF: "하수처리시설",
};
const normalizeChecklistResultMap = (
raw: unknown,
): Record<ReviewStepCode, any[]> => {
@@ -85,6 +94,8 @@ function CnfmViewPage({ id }: { id: string }) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
const query = useQuery({
queryKey: ["instl-sncn-detail", id],
queryFn: () => InstlSncnApi.getById(id),
@@ -96,11 +107,21 @@ function CnfmViewPage({ id }: { id: string }) {
const det = data.detDto ?? data;
const bizSclList = data.bizSclList ?? [];
const bizCstList = data.bizCstList ?? [];
// prefer new API field `cklstResultMap`, fallback to legacy `cklstResultList`
const cklstResultMap = useMemo(
() => normalizeChecklistResultMap(data.cklstResultMap ?? data.cklstResultList),
[data.cklstResultMap, data.cklstResultList],
);
// API now provides a flat checklist result list: `cklstResultList`
const rawCklst = useMemo(() => data.cklstResultList ?? [], [data.cklstResultList]);
// ensure we have a flat array
const cklstAll = useMemo(() => (Array.isArray(rawCklst) ? (rawCklst as any[]) : []), [rawCklst]);
// build a step-keyed map from the flat list so the rest of the view can use same access pattern
const cklstResultMap = useMemo(() => {
const base: Record<ReviewStepCode, any[]> = { LOCAL: [], ENV: [], KECO: [] };
(cklstAll ?? []).forEach((row: any) => {
const step = (row?.rvwStpComCd ?? "LOCAL") as ReviewStepCode;
if (REVIEW_STEP_CODES.includes(step)) base[step].push(row);
});
return base;
}, [cklstAll]);
const esntlFileList = data.esntlFileList ?? [];
const etcFileList = data.etcFileList ?? [];
const currentPrgrsSttsComCd = (det.prgrsSttsComCd as string | undefined) ?? "ST";
@@ -112,6 +133,13 @@ function CnfmViewPage({ id }: { id: string }) {
[visibleStepCount],
);
const [selectedReviewStep, setSelectedReviewStep] = useState<ReviewStepCode>("LOCAL");
// selected task code for header filtering (null = none selected)
const [selectedTaskCode, setSelectedTaskCode] = useState<string | null>(null);
// derive available task codes from checklist results
const taskCodes = useMemo(() => {
return Array.from(new Set((cklstAll ?? []).map((r: any) => (r as any).taskSeComCd).filter(Boolean)));
}, [cklstAll]);
useEffect(() => {
if (visibleReviewStepCodes.length === 0) return;
@@ -120,10 +148,103 @@ function CnfmViewPage({ id }: { id: string }) {
}
}, [selectedReviewStep, visibleReviewStepCodes]);
// when checklist result task codes change, default selectedTaskCode to first available code
useEffect(() => {
if (!taskCodes || taskCodes.length === 0) {
if (selectedTaskCode !== null) setSelectedTaskCode(null);
return;
}
if (!selectedTaskCode || !taskCodes.includes(selectedTaskCode)) {
setSelectedTaskCode(taskCodes[0]);
}
}, [taskCodes, selectedTaskCode]);
const selectedChecklistRows = useMemo(() => {
if (visibleReviewStepCodes.length === 0) return [];
return cklstResultMap[selectedReviewStep] ?? [];
}, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length]);
const rows = (cklstResultMap[selectedReviewStep] ?? []).filter((r: any) =>
selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true,
);
return rows
.slice()
.sort((a: any, b: any) =>
(a.cklstArtclSn ?? 0) - (b.cklstArtclSn ?? 0) || (a.sortSeq ?? 0) - (b.sortSeq ?? 0) || (a.cklstRvwSn ?? 0) - (b.cklstRvwSn ?? 0),
);
}, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length, selectedTaskCode]);
const [viewCheckResults, setViewCheckResults] = useState<Record<string, string>>({});
const [viewCheckComments, setViewCheckComments] = useState<Record<string, string>>({});
const cklstStats = useMemo(() => {
if (!selectedReviewStep) return { total: 0, completed: 0, percent: 0 };
const rows = (cklstResultMap[selectedReviewStep] ?? []).filter((r: any) => r.bchckUseYn === "Y" && (selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true));
const total = rows.length;
const completed = rows.reduce((acc: number, row: any) => {
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`;
const val = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? "";
return acc + (val ? 1 : 0);
}, 0);
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, completed, percent };
}, [cklstResultMap, viewCheckResults, selectedReviewStep, selectedTaskCode]);
const fnValidation = (mode: "SAVE" | "SUBMIT") => {
// 기본 검증: 필요하면 확장
return true;
};
const hadleTechReviewSubmit = async () => {
if (!fnValidation("SAVE")) return;
const cklstResultList = (cklstAll ?? [])
.filter((r: any) => r.bchckUseYn === "Y")
.map((row: any) => {
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`;
const rvwRsltAnmlyYn = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? null;
const picRvwCn = (viewCheckComments[key] ?? row.picRvwCn) ?? null;
return {
taskSeComCd: row.taskSeComCd ?? "",
cklstSn: row.cklstSn,
cklstArtclSn: row.cklstArtclSn,
cklstRvwSn: row.cklstRvwSn,
rvwStpComCd: selectedReviewStep,
rvwRsltAnmlyYn,
picRvwCn,
};
});
const payload: InstlSncnApplySaveRequestDto = {
// only map core management DTO and checklist results; other lists empty per request
instlSncnMng: { ...(det ?? {}), prgrsSttsComCd: "ER" } as any,
bzScaleList: [],
bizCstList: [],
cklstResultList,
esntlAtchFileList: [],
} as unknown as InstlSncnApplySaveRequestDto;
try {
const issuedDocNo = await InstlSncnApi.techReview(payload);
alert(`기술검토 등록이 완료되었습니다. 문서번호: ${issuedDocNo}`);
} catch (error) {
console.error("기술검토 등록 API 호출 실패", error);
const serverMessage =
axios.isAxiosError(error) && error.response?.data?.message
? (error.response.data as { message: string }).message
: "저장 중 오류가 발생했습니다.";
alert(serverMessage);
}
};
useEffect(() => {
const results: Record<string, string> = {};
const comments: Record<string, string> = {};
(selectedChecklistRows ?? []).forEach((row: any) => {
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") results[key] = row.rvwRsltAnmlyYn;
if (row.picRvwCn) comments[key] = String(row.picRvwCn);
});
setViewCheckResults(results);
setViewCheckComments(comments);
}, [selectedChecklistRows]);
if (query.isLoading) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 불러오는 중입니다...</div>;
@@ -360,9 +481,32 @@ function CnfmViewPage({ id }: { id: string }) {
</div>
{/* 체크리스트 */}
<div className="cnfm-card p-4">
<h3 className="text-[1.5rem] font-bold text-gray-700">체크리스트</h3>
<div className="mb-3 mt-2 flex flex-wrap gap-2">
<div className="cnfm-card p-4">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-[1.5rem] font-bold text-gray-700">체크리스트</h3>
<div className="flex items-center gap-4">
<div className="flex gap-2 items-center">
{Array.from(new Set(taskCodes)).map((code) => {
const name = TASK_CODE_LABELS[code] ?? code;
const active = selectedTaskCode === code;
return (
<button
key={code}
type="button"
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${
active ? "border-blue-500 bg-blue-50 text-blue-700" : "border-gray-300 bg-white text-gray-600"
}`}
onClick={() => setSelectedTaskCode(code)}
>
{name}
</button>
);
})}
</div>
<div className="text-[1.1rem] text-gray-500">{`(${cklstStats.completed}/${cklstStats.total})`}</div>
</div>
</div>
<div className="mb-3 flex flex-wrap gap-2">
{visibleReviewStepCodes.length === 0 ? (
<span className="text-[1.2rem] text-gray-500">
현재 사용자 권한/진행상태 조합에서 표시 가능한 체크리스트 단계가 없습니다.
@@ -384,17 +528,21 @@ function CnfmViewPage({ id }: { id: string }) {
))
)}
</div>
<div className="mb-2 text-[1.25rem] text-gray-500">
체크리스트 진행상황 [{selectedReviewStep}] — 총합: <strong>{cklstStats.total}</strong> · 완료: <strong>{cklstStats.completed}</strong> · 진행률: <strong>{cklstStats.percent}%</strong>
</div>
<div className="cnfm-table-wrap mt-3">
<table className="cnfm-table">
<thead>
<tr>
<th className="w-[20rem]">검토항목</th>
<th>주요 검토내용</th>
<th colSpan={2}>검토 결과(-)</th>
<th className="w-[20rem]" rowSpan={2}>검토항목</th>
<th rowSpan={2}>주요 검토내용</th>
<th colSpan={2}>검토 결과(✓)</th>
{selectedReviewStep !== "LOCAL" ? (
<th rowSpan={2} className="w-[20rem]">기술검토 의견</th>
) : null}
</tr>
<tr>
<th />
<th />
<th className="w-[9rem]">
<button
type="button"
@@ -414,33 +562,56 @@ function CnfmViewPage({ id }: { id: string }) {
<tbody>
{selectedChecklistRows.length === 0 ? (
<tr>
<td colSpan={3} className="cnfm-td-empty">조회된 체크리스트 항목이 없습니다.</td>
<td colSpan={5} className="cnfm-td-empty">조회된 체크리스트 항목이 없습니다.</td>
</tr>
) : (
selectedChecklistRows.map((row: any) => {
const key = `${row.cklstSn}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "STEP01"}`;
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
const hasResult = row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N";
const showRadios = row.bchckUseYn === "Y" || hasResult;
const showRadios = row.bchckUseYn === "Y" ;
return (
<tr key={key}>
<td className="text-center text-[1.2rem]">{row.cklstArtclNm}</td>
<td className="!text-left pl-2 text-[1.2rem] text-gray-600" style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}>{row.treeLevel > 1 ? "└ " : ""}{row.rvwCn}</td>
{!showRadios ? (
<>
<td className="text-center">-</td>
<td className="text-center">-</td>
</>
) : (
<>
<td className="text-center">
<input type="radio" name={`chk_${key}`} value="Y" checked={row.rvwRsltAnmlyYn === "Y"} disabled />
</td>
<td className="text-center">
<input type="radio" name={`chk_${key}`} value="N" checked={row.rvwRsltAnmlyYn === "N"} disabled />
</td>
</>
)}
{!showRadios ? (
<>
<td className="text-center">-</td>
<td className="text-center">-</td>
</>
) : (
<>
<td className="text-center">
<input
type="radio"
name={`chk_${key}`}
value="Y"
checked={(viewCheckResults[key] ?? "") === "Y"}
disabled={selectedReviewStep === "LOCAL"}
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "Y" }))}
/>
</td>
<td className="text-center">
<input
type="radio"
name={`chk_${key}`}
value="N"
checked={(viewCheckResults[key] ?? "") === "N"}
disabled={selectedReviewStep === "LOCAL"}
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "N" }))}
/>
</td>
</>
)}
{showRadios && selectedReviewStep !== "LOCAL" ? (
<td className="text-left">
<textarea
className="cnfm-input w-full"
value={viewCheckComments[key] ?? ""}
onChange={(e) => setViewCheckComments((prev) => ({ ...prev, [key]: e.target.value }))}
/>
</td>
) : null}
</tr>
);
})
@@ -530,6 +701,7 @@ function CnfmViewPage({ id }: { id: string }) {
<div className="cnfm-action-row">
<Button color="white" onClick={() => router.push("/instl-sncn")}>목록</Button>
<Button onClick={() => router.push(`/instl-sncn/${id}/modify`)}>수정</Button>
<Button onClick={() => void hadleTechReviewSubmit()}>기술검토등록</Button>
</div>
</div>
);