Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -159,7 +159,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
|
||||
// userInfo?.dtlInstCd 테스트 코드 ENV.ROLE_USER03 , KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
|
||||
const userInfo = {
|
||||
...userInfo2,
|
||||
dtlInstCd: "ROLE_USER05", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
|
||||
dtlInstCd: "ROLE_USER03", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요)
|
||||
};
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const qc = useQueryClient();
|
||||
@@ -826,7 +826,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;
|
||||
}
|
||||
@@ -839,7 +840,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
|
||||
const next: Record<string, string> = { ...prev };
|
||||
let changed = false;
|
||||
for (const [k, v] of commentLookup.entries()) {
|
||||
if (next[k] !== v) {
|
||||
// only set comment if none exists yet (preserve user edits)
|
||||
if (!next[k]) {
|
||||
next[k] = v;
|
||||
changed = true;
|
||||
}
|
||||
@@ -1122,7 +1124,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"): InstlSncnMng => ({
|
||||
const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"| "ER"): InstlSncnMng => ({
|
||||
instlSncnDocNo: docId ?? "", // 수정 시 기존 문서번호 유지, 신규 저장 시 빈값
|
||||
admdstCd: "",
|
||||
sidoNm: "",
|
||||
@@ -1180,16 +1182,28 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!fnValidation("PRESENTN")) return;
|
||||
const payload = {
|
||||
instlSncnMng: toInstlSncnMngPayload("ST"),
|
||||
async function handleSubmit() {
|
||||
if (!fnValidation("SAVE")) return;
|
||||
|
||||
const payload: InstlSncnApplySaveRequestDto = {
|
||||
instlSncnMng: toInstlSncnMngPayload("SV"), // 저장 시 진행상태는 SV (임시저장)
|
||||
bzScaleList: toBzScaleSaveDtoList(),
|
||||
bizCstList: toBizCstSaveDtoList(),
|
||||
cklstRvwRsltList: toCklstSaveDtoList(),
|
||||
};
|
||||
void payload;
|
||||
alert("제출 API 연동 후 progrsSttus=ST 전송");
|
||||
cklstResultList: toCklstSaveDtoList(),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDtoList(),
|
||||
};
|
||||
|
||||
try {
|
||||
const issuedDocNo = await InstlSncnApi.saveApply(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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
@@ -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),
|
||||
@@ -122,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;
|
||||
@@ -130,19 +148,97 @@ 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.cklstSn}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
|
||||
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);
|
||||
});
|
||||
@@ -385,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">
|
||||
현재 사용자 권한/진행상태 조합에서 표시 가능한 체크리스트 단계가 없습니다.
|
||||
@@ -409,20 +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 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"
|
||||
@@ -446,7 +566,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
</tr>
|
||||
) : (
|
||||
selectedChecklistRows.map((row: any) => {
|
||||
const key = `${row.cklstSn}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
|
||||
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" ;
|
||||
|
||||
@@ -581,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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user