Merge branch 'iipbs/as/dev'

This commit is contained in:
2026-06-26 09:14:22 +09:00
14 changed files with 481 additions and 144 deletions
+3
View File
@@ -5,6 +5,7 @@ export {
useInstlSncnList,
useInstlSncnSidoCombo,
useInstlSncnSigunguCombo,
useInstlSncnTechReview,
} from "./api/queries";
export {
type AdstZoneResponseDto,
@@ -26,5 +27,7 @@ export {
type InstlSncnMngSaveRequestDto,
type InstlSncnMngSearchParams,
type InstlSncnRvwOpnnResponseDto,
type InstlSncnRvwOpnnSaveRequestDto,
type InstlSncnSplmntDmndSaveDto,
type ReviewStepCode,
} from "./model/instl-sncn-interface";
@@ -114,14 +114,14 @@ export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
export interface InstlSncnBzScaleSaveDto {
/** 공종구분코드 */
cnstpSeCd: string;
/** 기본계획(A) */
/** 기본계획수량 */
bscPl: number;
/** 예산반영(B) */
/** 예산반영수량 */
budgetReflct: number;
/** 금회설계시(C) */
/** 금회설계수량 */
thtiDsgn: number;
/** 증감(C-B) */
diff: number;
/** 증감(C-B) — 클라이언트 계산용, API 미전송 */
diff?: number;
}
/** 설치인가 신청 - 총사업비 저장 DTO(배열 원소) */
@@ -138,27 +138,92 @@ export interface InstlSncnBizCstSaveRequestDto {
/** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */
export interface InstlSncnCklstResultSaveDto {
/** 점검표일련번호 */
cklstSn: number;
/** 점검표항목일련번호 */
cklstArtclSn: number;
/** 점검표검토일련번호 */
cklstRvwSn: number;
/** 검토단계공통코드 */
rvwStpComCd: string;
/** 선택값(Y/N/null) */
selected: "Y" | "N" | null;
/** 담당자검토내용(기술검토의견) */
picRvwCn?: string | null;
}
/** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */
export interface InstlSncnEsntlAtchFileSaveDto {
/** 필수첨부파일아이디 */
esntlAtchFileId: string;
/** 첨부파일그룹아이디 */
atchFileGroupId: string;
}
/** 설치인가검토의견 저장 요청 DTO */
export interface InstlSncnRvwOpnnSaveRequestDto {
/** 설치인가문서번호 */
instlSncnDocNo: string;
/** 검토의견일련번호 */
rvwOpnnSn?: number;
/** 의견자구분공통코드 */
opnrSeComCd?: string;
/** 의견자명 */
opnrNm?: string;
/** 검토결과내용 */
rvwRsltCn?: string;
/** 검토결과이상여부 */
rvwRsltAnmlyYn?: string;
/** 첨부파일그룹아이디 */
atchFileGroupId?: string;
}
/** 설치인가 보완요구 저장 DTO(배열 원소) */
export interface InstlSncnSplmntDmndSaveDto {
/** 보완요구 일련번호 */
splmntDmndSn?: number;
/** 보완항목 공통코드 */
splmntArtclComCd?: string;
/** 보완요구 내용 */
splmntDmndCn?: string;
/** 보완요구 일시 */
splmntDmndDt?: string;
/** 보완요구 첨부파일 그룹 ID */
splmntDmndAtchFileGroupId?: string;
/** 보완 내용 */
splmntCn?: string;
/** 보완 담당자 명 */
splmntPicNm?: string;
/** 보완완료 일시 */
splmntCmptnDt?: string;
/** 보완 첨부파일 그룹 ID */
splmntAtchFileGroupId?: string;
/** 완료 여부 */
cmptnYn?: string;
}
/** 설치인가 신청 - 통합 저장 요청 DTO */
export interface InstlSncnApplySaveRequestDto {
/** 기술검토저장구분(SV.저장, CP.검토완료) */
techReViewSaveCd?: string;
/** 검토단계공통코드(ENV.환경청.EP, KECO.환경공단.TE) */
rvwStpCd?: string;
/** 설치인가관리 정보 */
instlSncnMng: InstlSncnMngSaveRequestDto;
/** 사업규모 목록 */
bzScaleList: InstlSncnBzScaleSaveDto[];
/** 총사업비 목록 */
bizCstList: InstlSncnBizCstSaveRequestDto[];
/** 체크리스트 검토결과 목록 */
cklstResultList: InstlSncnCklstResultSaveDto[];
/** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */
cklstResultMap?: Record<string, InstlSncnCklstResultSaveDto[]>;
/** 필수첨부파일 목록 */
esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[];
/** 검토의견 목록 */
rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[];
/** 보완요구 목록 */
splmntDmndList?: InstlSncnSplmntDmndSaveDto[];
}
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
@@ -92,7 +92,7 @@ function ElaprTmpltFormDialog({
<div className="space-y-2">
<Label>문서구분</Label>
<CodeSelect
upCd="ASDOCSECD"
upCd="ASDOCSE"
value={docSeComCd}
onValueChange={(v) => setValue("docSeComCd", v)}
placeholder="문서구분 선택"
@@ -94,7 +94,7 @@ function EsntlAtchFileFormDialog({
<div className="space-y-2">
<Label>문서구분</Label>
<CodeSelect
upCd="ASDOCSECD"
upCd="ASDOCSE"
value={docSeComCd}
onValueChange={(v) => setValue("docSeComCd", v, { shouldValidate: true })}
placeholder="문서구분 선택"
@@ -36,7 +36,7 @@ function EsntlAtchFileList({
onPaginationChange,
}: EsntlAtchFileListProps) {
const { confirm } = useConfirm();
const { data: docSeCodes } = useCodeList("ASDOCSECD");
const { data: docSeCodes } = useCodeList("ASDOCSE");
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
const [editingRow, setEditingRow] = useState<EsntlAtchFileMng | null>(null);
@@ -32,7 +32,7 @@ const DEFAULT_SEARCH_PARAMS = {
};
function EsntlAtchFileMngPage() {
const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSECD");
const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSE");
const [docSeComCd, setDocSeComCd] = useState(ESNTL_ATCH_FILE_ALL_DOC_SE);
const [pagination, setPagination] = useState<PaginationState>({
@@ -50,7 +50,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
},
};
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "EE"] as const;
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
string,
@@ -59,7 +59,7 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
ER: {
ATRZ01: [INSTL_SNCN_VIEW_BUTTON_ACTION.dsgntRvwr],
},
EE: {
TE: {
ATRZ02: [
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg,
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn,
@@ -15,6 +15,7 @@ import type {
import {
type ChecklistFormState,
type ChecklistProgress,
type ChecklistRowKey,
type ChecklistStepMap,
INSTL_PROGRESS_ORDER,
@@ -23,8 +24,12 @@ import {
ROLE_TO_REVIEW_STEP,
} from "../model/view-types";
export function buildChecklistRowKey(row: CklstRvwRsltTreeDto): ChecklistRowKey {
return `${row.taskSeComCd}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
export function buildChecklistRowKey(
row: CklstRvwRsltTreeDto,
reviewStep?: ReviewStepCode,
): ChecklistRowKey {
const step = reviewStep ?? (row.rvwStpComCd as ReviewStepCode) ?? "LOCAL";
return `${row.taskSeComCd}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${step}`;
}
export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap {
@@ -37,11 +42,9 @@ export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap
}
export function getProgressStepCount(prgrsSttsComCd?: string): number {
const idx = INSTL_PROGRESS_ORDER.indexOf(
(prgrsSttsComCd ?? "ST") as (typeof INSTL_PROGRESS_ORDER)[number],
);
if (idx <= INSTL_PROGRESS_ORDER.indexOf("ST")) return 1;
if (idx <= INSTL_PROGRESS_ORDER.indexOf("TP")) return 2;
const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd as (typeof INSTL_PROGRESS_ORDER)[number]);
if (idx <= INSTL_PROGRESS_ORDER.indexOf("SV")) return 1;
if (idx <= INSTL_PROGRESS_ORDER.indexOf("LE")) return 2;
return 3;
}
@@ -77,6 +80,62 @@ export function filterChecklistRows(
);
}
export function getChecklistDisplayRows(
rows: CklstRvwRsltTreeDto[],
taskCode?: string | null,
): CklstRvwRsltTreeDto[] {
const filtered = filterChecklistRows(rows, taskCode ?? null);
const seen = new Set<string>();
const deduped: CklstRvwRsltTreeDto[] = [];
for (const row of filtered) {
const dedupeKey = `${row.taskSeComCd}_${row.cklstRvwSn}`;
if (seen.has(dedupeKey)) continue;
seen.add(dedupeKey);
deduped.push(row);
}
return deduped;
}
export function computeChecklistProgress(
rows: CklstRvwRsltTreeDto[],
results: ChecklistFormState["results"],
): ChecklistProgress {
const activeRows = rows.filter((row) => row.bchckUseYn === "Y");
const total = activeRows.length;
const completed = activeRows.reduce((acc, row) => {
const key = buildChecklistRowKey(row);
const val = results[key] ?? row.rvwRsltAnmlyYn ?? "";
return acc + (val ? 1 : 0);
}, 0);
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, completed, percent };
}
export function filterChecklistRowsByReviewStep(
rows: CklstRvwRsltTreeDto[],
reviewStep: ReviewStepCode,
): CklstRvwRsltTreeDto[] {
return rows.filter((row) => row.rvwStpComCd === reviewStep);
}
export function computeReviewStepChecklistProgress(
rows: CklstRvwRsltTreeDto[],
results: ChecklistFormState["results"],
reviewStep: ReviewStepCode,
): ChecklistProgress {
const activeRows = getChecklistDisplayRows(rows).filter((row) => row.bchckUseYn === "Y");
const total = activeRows.length;
const completed = activeRows.reduce((acc, row) => {
const key = buildChecklistRowKey(row, reviewStep);
const val = results[key];
return acc + (val === "Y" || val === "N" ? 1 : 0);
}, 0);
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, completed, percent };
}
export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistFormState {
const results: ChecklistFormState["results"] = {};
const comments: ChecklistFormState["comments"] = {};
@@ -87,7 +146,57 @@ export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistF
}
if (row.picRvwCn) comments[key] = row.picRvwCn;
});
return { results, comments };
return {
results,
comments,
progress: computeChecklistProgress(rows, results),
};
}
export function mergeChecklistFormStateFromServer(
prev: ChecklistFormState,
allRows: CklstRvwRsltTreeDto[],
): ChecklistFormState {
const results = { ...prev.results };
const comments = { ...prev.comments };
for (const row of allRows) {
const step = row.rvwStpComCd as ReviewStepCode | undefined;
if (!step || !REVIEW_STEP_CODES.includes(step)) continue;
const key = buildChecklistRowKey(row);
if ((row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") && results[key] === undefined) {
results[key] = row.rvwRsltAnmlyYn;
}
if (row.picRvwCn && !comments[key]) {
comments[key] = row.picRvwCn;
}
}
return {
results,
comments,
progress: computeChecklistProgress(getChecklistDisplayRows(allRows), results),
};
}
export function patchChecklistFormState(
prev: ChecklistFormState,
patch: Partial<Pick<ChecklistFormState, "results" | "comments">>,
allRows: CklstRvwRsltTreeDto[],
reviewStep?: ReviewStepCode,
): ChecklistFormState {
const results = patch.results ?? prev.results;
const comments = patch.comments ?? prev.comments;
const progress = reviewStep
? computeReviewStepChecklistProgress(allRows, results, reviewStep)
: computeChecklistProgress(getChecklistDisplayRows(allRows), results);
return {
results,
comments,
progress,
};
}
export function toCklstSaveDto(
@@ -95,20 +204,18 @@ export function toCklstSaveDto(
formState: ChecklistFormState,
reviewStep: ReviewStepCode,
): InstlSncnCklstResultSaveDto[] {
return rows
return getChecklistDisplayRows(rows)
.filter((row) => row.bchckUseYn === "Y")
.map((row) => {
const key = buildChecklistRowKey(row);
const selected =
formState.results[key] ??
(row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N" ? row.rvwRsltAnmlyYn : null);
const key = buildChecklistRowKey(row, reviewStep);
const selected = formState.results[key] ?? null;
return {
cklstSn: row.cklstSn,
cklstArtclSn: row.cklstArtclSn,
cklstRvwSn: row.cklstRvwSn,
rvwStpComCd: reviewStep,
selected,
picRvwCn: formState.comments[key] ?? row.picRvwCn ?? null,
picRvwCn: formState.comments[key] ?? null,
};
});
}
@@ -169,6 +276,20 @@ export function buildSavePayload(
};
}
export function buildTechReviewReplyPayload(
data: InstlSncnMngDetResponse,
checklistFormState: ChecklistFormState,
reviewStep: ReviewStepCode,
prgrsSttsComCd: string,
techReViewSaveCd: string,
): InstlSncnApplySaveRequestDto {
return {
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
techReViewSaveCd,
rvwStpCd: reviewStep,
};
}
export function buildTechReviewPayload(
det: InstlSncnMngResponseDto,
cklstResultList: InstlSncnCklstResultSaveDto[],
@@ -0,0 +1,57 @@
import { useCallback, useEffect, useState } from "react";
import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn";
import { type ChecklistFormState, DEFAULT_CHECKLIST_PROGRESS } from "../model/view-types";
import { mergeChecklistFormStateFromServer, patchChecklistFormState } from "./checklist-utils";
const EMPTY_CKLST_RESULT_LIST: CklstRvwRsltTreeDto[] = [];
function isSameChecklistFormState(a: ChecklistFormState, b: ChecklistFormState): boolean {
if (
a.progress.total !== b.progress.total ||
a.progress.completed !== b.progress.completed ||
a.progress.percent !== b.progress.percent
) {
return false;
}
const aResultKeys = Object.keys(a.results);
if (aResultKeys.length !== Object.keys(b.results).length) return false;
if (!aResultKeys.every((key) => a.results[key] === b.results[key])) return false;
const aCommentKeys = Object.keys(a.comments);
if (aCommentKeys.length !== Object.keys(b.comments).length) return false;
return aCommentKeys.every((key) => a.comments[key] === b.comments[key]);
}
function useChecklistFormState(cklstResultList: CklstRvwRsltTreeDto[] = EMPTY_CKLST_RESULT_LIST) {
const [formState, setFormState] = useState<ChecklistFormState>({
results: {},
comments: {},
progress: DEFAULT_CHECKLIST_PROGRESS,
});
useEffect(() => {
setFormState((prev) => {
const next = mergeChecklistFormStateFromServer(prev, cklstResultList);
return isSameChecklistFormState(prev, next) ? prev : next;
});
}, [cklstResultList]);
const patchFormState = useCallback(
(
patch: Partial<Pick<ChecklistFormState, "results" | "comments">>,
reviewStep?: ReviewStepCode,
) => {
setFormState((prev) => patchChecklistFormState(prev, patch, cklstResultList, reviewStep));
},
[cklstResultList],
);
return { formState, setFormState, patchFormState };
}
export { EMPTY_CKLST_RESULT_LIST, useChecklistFormState };
@@ -26,9 +26,22 @@ export type {
export type ChecklistRowKey = string;
export interface ChecklistProgress {
total: number;
completed: number;
percent: number;
}
export const DEFAULT_CHECKLIST_PROGRESS: ChecklistProgress = {
total: 0,
completed: 0,
percent: 0,
};
export interface ChecklistFormState {
results: Record<ChecklistRowKey, "Y" | "N">;
comments: Record<ChecklistRowKey, string>;
progress: ChecklistProgress;
}
export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
@@ -48,17 +61,23 @@ export const TASK_CODE_LABELS: Record<string, string> = {
};
export const INSTL_PROGRESS_ORDER = [
// ROLE_USER05
"SV",
"ST",
"LE",
"ER",
"EE",
"TE",
"TS",
"TR",
"TP",
"EP",
"SL",
"TA",
// ROLE_USER04
"ST",
"EP",
// ROLE_USER03
"LE",
"TP",
// ROLE_USER01
"ER",
"TE",
"TB",
] as const;
@@ -12,12 +12,13 @@ import {
Textarea,
} from "@common/components";
import { buildChecklistRowKey } from "../../lib/checklist-utils";
import { buildChecklistRowKey, patchChecklistFormState } from "../../lib/checklist-utils";
import { type ChecklistFormState, ROLE_TO_REVIEW_STEP } from "../../model/view-types";
interface ChecklistTaskPanelProps {
selectedReviewStep: ReviewStepCode;
selectedRows: CklstRvwRsltTreeDto[];
allRows: CklstRvwRsltTreeDto[];
formState: ChecklistFormState;
setFormState: Dispatch<SetStateAction<ChecklistFormState>>;
userRoleCode?: string;
@@ -26,6 +27,7 @@ interface ChecklistTaskPanelProps {
function ChecklistTaskPanel({
selectedReviewStep,
selectedRows,
allRows,
formState,
setFormState,
userRoleCode,
@@ -64,7 +66,7 @@ function ChecklistTaskPanel({
</TableRow>
) : (
selectedRows.map((row) => {
const key = buildChecklistRowKey(row);
const key = buildChecklistRowKey(row, selectedReviewStep);
const isFieldVisible = row.bchckUseYn === "Y";
const isLocalReviewStep = selectedReviewStep === "LOCAL";
@@ -74,16 +76,11 @@ function ChecklistTaskPanel({
<TableCell
className="pl-2 !text-left text-[1.2rem] text-gray-600"
style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}
colSpan={isFieldVisible ? 0 : isLocalReviewStep ? 3 : 4}
>
{row.treeLevel > 1 ? "└ " : ""}
{row.rvwCn}
</TableCell>
{!isFieldVisible ? (
<>
<TableCell className="bg-muted" />
<TableCell className="bg-muted" />
</>
) : (
{isFieldVisible && (
<>
<TableCell className="text-center">
<input
@@ -94,10 +91,14 @@ function ChecklistTaskPanel({
readOnly={!isStepEditable}
onChange={() => {
if (!isStepEditable) return;
setFormState((prev) => ({
...prev,
results: { ...prev.results, [key]: "Y" },
}));
setFormState((prev) =>
patchChecklistFormState(
prev,
{ results: { ...prev.results, [key]: "Y" } },
allRows,
selectedReviewStep,
),
);
}}
className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")}
/>
@@ -111,37 +112,43 @@ function ChecklistTaskPanel({
readOnly={!isStepEditable}
onChange={() => {
if (!isStepEditable) return;
setFormState((prev) => ({
...prev,
results: { ...prev.results, [key]: "N" },
}));
setFormState((prev) =>
patchChecklistFormState(
prev,
{ results: { ...prev.results, [key]: "N" } },
allRows,
selectedReviewStep,
),
);
}}
className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")}
/>
</TableCell>
</>
)}
{!isLocalReviewStep ? (
<TableCell className={`text-left ${isFieldVisible ? "" : "bg-muted"}`}>
{isFieldVisible && (
<Textarea
className={cn(
"min-h-[6rem] !text-xl",
isStepEditable ? "cursor-pointer" : "cursor-not-allowed",
)}
value={formState.comments[key] ?? ""}
readOnly={!isStepEditable}
onChange={(event) => {
if (!isStepEditable) return;
setFormState((prev) => ({
...prev,
comments: { ...prev.comments, [key]: event.target.value },
}));
}}
/>
)}
{!isLocalReviewStep && isFieldVisible && (
<TableCell className="text-left">
<Textarea
className={cn(
"min-h-[6rem] !text-xl",
isStepEditable ? "cursor-pointer" : "cursor-not-allowed",
)}
value={formState.comments[key] ?? ""}
readOnly={!isStepEditable}
onChange={(event) => {
if (!isStepEditable) return;
setFormState((prev) =>
patchChecklistFormState(
prev,
{ comments: { ...prev.comments, [key]: event.target.value } },
allRows,
selectedReviewStep,
),
);
}}
/>
</TableCell>
) : null}
)}
</TableRow>
);
})
@@ -24,8 +24,12 @@ import {
type InstlSncnViewButtonAction,
needsRoleUser01AtrzResolve,
} from "../../lib/button-config";
import { buildSavePayload } from "../../lib/checklist-utils";
import { type ChecklistFormState, ROLE_TO_REVIEW_STEP } from "../../model/view-types";
import {
buildSavePayload,
buildTechReviewReplyPayload,
computeReviewStepChecklistProgress,
} from "../../lib/checklist-utils";
import { type ChecklistFormState } from "../../model/view-types";
import { ElaprTmpltPickerDialog } from "./elapr-tmplt-picker-dialog";
import { TextInputDialog } from "./text-input-dialog";
@@ -57,6 +61,7 @@ function FloatingActionButtons({
const [isProcessing, setIsProcessing] = useState(false);
const [activeDialog, setActiveDialog] = useState<ActiveDialog>(null);
const docNo = data.detDto.instlSncnDocNo;
const isRegister = data.detDto.rgtr === userInfo?.userUnqId;
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
@@ -123,11 +128,10 @@ function FloatingActionButtons({
}
async function handleStrgClick() {
const userReviewStep = dtlInstCd ? (ROLE_TO_REVIEW_STEP[dtlInstCd] ?? "LOCAL") : "LOCAL";
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
data,
checklistFormState,
userReviewStep,
"LOCAL",
"SV",
);
@@ -139,16 +143,20 @@ function FloatingActionButtons({
}
async function handleAplyClick() {
const userReviewStep = dtlInstCd ? (ROLE_TO_REVIEW_STEP[dtlInstCd] ?? "LOCAL") : "LOCAL";
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"LOCAL",
);
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
data,
checklistFormState,
userReviewStep,
"LOCAL",
"ST",
);
if (payload.cklstResultList?.some((item) => item.selected === null)) {
alert("모든 검토항목을 검토 완료해야 신청 가능합니다.");
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
}
@@ -171,9 +179,25 @@ function FloatingActionButtons({
router.push("/instl-sncn");
}
async function handleInstlSncnAncmntClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
docSeComCd,
prgrsSttsComCd: "TB",
};
await executeAction({
action: INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt,
request: payload,
successMessage: "고시되었습니다.",
});
queryClient.invalidateQueries({
queryKey: instlSncnKeys.list(),
});
}
async function handleEnvoSbmsnClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo: data.detDto.instlSncnDocNo,
docNo,
docSeComCd,
prgrsSttsComCd: "LE",
};
@@ -191,7 +215,7 @@ function FloatingActionButtons({
if (!(await confirm("반려하시겠습니까?"))) return;
const payload: DocPrgrsUpdateRequestDto = {
docNo: data.detDto.instlSncnDocNo,
docNo,
docSeComCd,
prgrsSttsComCd: "SV",
};
@@ -205,9 +229,48 @@ function FloatingActionButtons({
});
}
async function handleSncnAprvClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
docSeComCd,
prgrsSttsComCd: "TA",
};
await executeAction({
action: INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv,
request: payload,
successMessage: "인가승인되었습니다.",
});
queryClient.invalidateQueries({
queryKey: instlSncnKeys.list(),
});
}
async function handleRvwRplyClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"ENV",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
return;
}
if (!(await confirm("검토결과를 회신하시겠습니까?"))) return;
const payload = buildTechReviewReplyPayload(data, checklistFormState, "ENV", "EP", "CP");
await executeAction({
action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply,
request: payload,
successMessage: "검토결과 회신이 완료되었습니다.",
});
}
async function handleRqstTechRvwClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo: data.detDto.instlSncnDocNo,
docNo,
docSeComCd,
prgrsSttsComCd: "ER",
};
@@ -235,6 +298,16 @@ function FloatingActionButtons({
});
}
async function handleRvwStrgClick() {
const payload = buildTechReviewReplyPayload(data, checklistFormState, "KECO", "TE", "SV");
await executeAction({
action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg,
request: payload,
successMessage: "저장되었습니다.",
});
}
function handleDmndSplmntClick() {
setActiveDialog("dmndSplmnt");
}
@@ -286,12 +359,7 @@ function FloatingActionButtons({
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt) && (
<Button
onClick={() =>
void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt })
}
disabled={isProcessing}
>
<Button onClick={handleInstlSncnAncmntClick} disabled={isProcessing}>
설치인가 고시
</Button>
)}
@@ -306,19 +374,12 @@ function FloatingActionButtons({
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv) && (
<Button
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv })}
disabled={isProcessing}
>
<Button onClick={handleSncnAprvClick} disabled={isProcessing}>
인가승인
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply) && (
<Button
color="green"
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply })}
disabled={isProcessing}
>
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
검토결과 회신
</Button>
)}
@@ -333,10 +394,7 @@ function FloatingActionButtons({
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg) && (
<Button
onClick={() => void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg })}
disabled={isProcessing}
>
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
저장
</Button>
)}
@@ -434,8 +492,12 @@ async function handleSplmntCmptn(context: ActionHandlerContext): Promise<void> {
}
async function handleInstlSncnAncmnt(context: ActionHandlerContext): Promise<void> {
// TODO: 고시 처리 API 호출
context.onSuccess?.();
try {
await instlSncnService.changePrgrsSttsComCd(context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleEnvoSbmsn(
@@ -459,18 +521,32 @@ async function handleRjct(context: ActionHandlerContext<DocPrgrsUpdateRequestDto
}
async function handleSncnAprv(context: ActionHandlerContext): Promise<void> {
// TODO: 인가승인 API 호출
context.onSuccess?.();
try {
await instlSncnService.changePrgrsSttsComCd(context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleRvwRply(context: ActionHandlerContext): Promise<void> {
// TODO: 검토결과 회신 API 호출
context.onSuccess?.();
async function handleRvwRply(
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
): Promise<void> {
try {
await instlSncnService.techReview(context.data.detDto.instlSncnDocNo, context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleRqstTechRvw(context: ActionHandlerContext): Promise<void> {
// TODO: 기술검토 의뢰 API 호출
context.onSuccess?.();
try {
await instlSncnService.changePrgrsSttsComCd(context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleDsgntRvwr(context: ActionHandlerContext<ElaprDmndSaveRequest>): Promise<void> {
@@ -482,9 +558,15 @@ async function handleDsgntRvwr(context: ActionHandlerContext<ElaprDmndSaveReques
}
}
async function handleRvwStrg(context: ActionHandlerContext): Promise<void> {
// TODO: 검토저장 API 호출
context.onSuccess?.();
async function handleRvwStrg(
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
): Promise<void> {
try {
await instlSncnService.techReview(context.data.detDto.instlSncnDocNo, context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleRvwCmptn(context: ActionHandlerContext): Promise<void> {
@@ -6,7 +6,7 @@ import { useUserStore } from "@/shared/lib";
import { useInstlSncnDetail } from "@/entities/instl-sncn";
import { Skeleton } from "@common/components";
import { type ChecklistFormState } from "../model/view-types";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { FloatingActionButtons } from "./components/floating-action-buttons";
import { AttachmentsSection } from "./sections/attachments-section";
@@ -24,10 +24,8 @@ interface InstlViewPageProps {
function InstlViewPage({ id }: InstlViewPageProps) {
const userInfo = useUserStore((state) => state.userInfo);
const { data, isLoading, isError } = useInstlSncnDetail(id);
const [checklistFormState, setChecklistFormState] = useState<ChecklistFormState>({
results: {},
comments: {},
});
const { formState: checklistFormState, setFormState: setChecklistFormState } =
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
if (isLoading) {
return (
@@ -6,10 +6,9 @@ import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import {
buildChecklistFormState,
buildChecklistRowKey,
computeReviewStepChecklistProgress,
extractTaskCodes,
filterChecklistRows,
getChecklistDisplayRows,
getVisibleReviewSteps,
} from "../../lib/checklist-utils";
import {
@@ -39,7 +38,6 @@ function ChecklistSection({
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
[prgrsSttsComCd, userRoleCode],
);
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
const [selectedReviewStep, setSelectedReviewStep] = useState<ReviewStepCode>("LOCAL");
@@ -62,26 +60,13 @@ function ChecklistSection({
}
}, [taskCodes, selectedTaskCode]);
const selectedRows = useMemo(
() => filterChecklistRows(cklstResultList, selectedTaskCode),
[cklstResultList, selectedTaskCode],
const progress = useMemo(
() =>
computeReviewStepChecklistProgress(cklstResultList, formState.results, selectedReviewStep),
[cklstResultList, formState.results, selectedReviewStep],
);
useEffect(() => {
setFormState(buildChecklistFormState(selectedRows));
}, [selectedRows]);
const cklstStats = useMemo(() => {
const rows = selectedRows.filter((row) => row.bchckUseYn === "Y");
const total = rows.length;
const completed = rows.reduce((acc, row) => {
const key = buildChecklistRowKey(row);
const val = formState.results[key] ?? row.rvwRsltAnmlyYn ?? "";
return acc + (val ? 1 : 0);
}, 0);
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, completed, percent };
}, [selectedRows, formState.results]);
const { total, completed, percent } = progress;
return (
<SectionCollapsible title="체크리스트">
@@ -122,9 +107,8 @@ function ChecklistSection({
))}
</TabsList>
<div className="shrink-0 pb-1 text-lg text-muted-foreground">
총합: <strong>{cklstStats.total}</strong> · 완료:{" "}
<strong>{cklstStats.completed}</strong> · 진행률:{" "}
<strong>{cklstStats.percent}%</strong>
총합: <strong>{total}</strong> · 완료: <strong>{completed}</strong> · 진행률:{" "}
<strong>{percent}%</strong>
</div>
</div>
@@ -132,7 +116,8 @@ function ChecklistSection({
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
<ChecklistTaskPanel
selectedReviewStep={selectedReviewStep}
selectedRows={selectedRows}
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
allRows={cklstResultList}
formState={formState}
setFormState={setFormState}
userRoleCode={userRoleCode}