[as] feat: 재원협의 기술검토 로직 작업
This commit is contained in:
@@ -8,11 +8,17 @@ import type {
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_MNG;
|
||||
|
||||
async function findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto> {
|
||||
const response = await client.get<PwrkCnsltnMngDetResponseDto>(`${BASE}/${pwrkCnsltnDocNo}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto> {
|
||||
const response = await client.get<PwrkCnsltnMngDetResponseDto>(`${BASE}/updocno/${upDocNo}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const pwrkCnsltnMngService = {
|
||||
findById,
|
||||
findByUpDocNo,
|
||||
} satisfies PwrkCnsltnMngService;
|
||||
|
||||
@@ -4,9 +4,21 @@ import { pwrkCnsltnMngService } from "./pwrk-cnsltn-mng-service";
|
||||
|
||||
export const pwrkCnsltnMngKeys = {
|
||||
all: ["pwrk-cnsltn-mng"] as const,
|
||||
byId: (pwrkCnsltnDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-id", pwrkCnsltnDocNo] as const,
|
||||
byUpDocNo: (upDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-up-doc-no", upDocNo] as const,
|
||||
};
|
||||
|
||||
export function usePwrkCnsltnMng(
|
||||
pwrkCnsltnDocNo: string | null | undefined,
|
||||
isEnabled = true,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: pwrkCnsltnMngKeys.byId(pwrkCnsltnDocNo!),
|
||||
queryFn: () => pwrkCnsltnMngService.findById(pwrkCnsltnDocNo!),
|
||||
enabled: Boolean(pwrkCnsltnDocNo) && isEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePwrkCnsltnMngByUpDocNo(upDocNo: string | null | undefined, isEnabled = true) {
|
||||
return useQuery({
|
||||
queryKey: pwrkCnsltnMngKeys.byUpDocNo(upDocNo!),
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export { pwrkCnsltnMngService } from "./api/pwrk-cnsltn-mng-service";
|
||||
export { pwrkCnsltnMngKeys, usePwrkCnsltnMngByUpDocNo } from "./api/queries";
|
||||
export {
|
||||
pwrkCnsltnMngKeys,
|
||||
usePwrkCnsltnMng,
|
||||
usePwrkCnsltnMngByUpDocNo,
|
||||
} from "./api/queries";
|
||||
export { findCompletedPwrkDraft } from "./lib/find-completed-draft";
|
||||
export type {
|
||||
PwrkCnsltnCnsltnResponseDto,
|
||||
PwrkCnsltnDocCrtDmndResponseDto,
|
||||
PwrkCnsltnDocCrtRsltResponseDto,
|
||||
PwrkCnsltnMngDetResponseDto,
|
||||
PwrkCnsltnMngService,
|
||||
PwrkCnsltnPjtcoAjmtResponseDto,
|
||||
} from "./model/pwrk-cnsltn-mng-interface";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { AtflMngClientResponseDto } from "@/shared/model";
|
||||
|
||||
export interface PwrkCnsltnDocCrtRsltResponseDto {
|
||||
crtDmndUuid: string;
|
||||
verSn: number;
|
||||
@@ -10,25 +12,80 @@ export interface PwrkCnsltnDocCrtRsltResponseDto {
|
||||
export interface PwrkCnsltnDocCrtDmndResponseDto {
|
||||
crtDmndUuid: string;
|
||||
pwrkCnsltnDocNo: string;
|
||||
orgnlDocNo?: string;
|
||||
orgnlDocSeComCd?: string;
|
||||
orgnlDocSeComCdNm?: string;
|
||||
crtSysDmndId?: string;
|
||||
crtSttsComCd?: string;
|
||||
crtSttsComCdNm?: string;
|
||||
failRsn?: string;
|
||||
dmndDt?: string;
|
||||
cmptnDt?: string;
|
||||
rqstrUuid?: string;
|
||||
rqstrNm?: string;
|
||||
crtRslt?: PwrkCnsltnDocCrtRsltResponseDto | null;
|
||||
}
|
||||
|
||||
/** 백엔드 PwrkCnsltnMngDetResponseDto.CnsltnResponseDto */
|
||||
export interface PwrkCnsltnCnsltnResponseDto {
|
||||
pwrkCnsltnDocNo?: string;
|
||||
rvwOpnnSn?: number;
|
||||
rvwOpnnClsfComCd?: string;
|
||||
rvwOpnnClsfComCdNm?: string;
|
||||
rvwOpnn?: string;
|
||||
cnsltnCn?: string;
|
||||
rfltYn?: string;
|
||||
rmrk?: string;
|
||||
atchFileGroupId?: string;
|
||||
atchFileInfo?: AtflMngClientResponseDto;
|
||||
rvwrNm?: string;
|
||||
rvwrMblTelno?: string;
|
||||
rvwrOgdpNm?: string;
|
||||
rvwrEmlAddr?: string;
|
||||
lclgvPicNm?: string;
|
||||
lclgvPicTelno?: string;
|
||||
lclgvPicOgdpNm?: string;
|
||||
lclgvPicEmlAddr?: string;
|
||||
crtDmndUuid?: string;
|
||||
crtYn?: string;
|
||||
hwrtChgYn?: string;
|
||||
verSn?: number;
|
||||
dtlSn?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 PwrkCnsltnMngDetResponseDto.PjtcoAjmtResponseDto */
|
||||
export interface PwrkCnsltnPjtcoAjmtResponseDto {
|
||||
pwrkCnsltnDocNo?: string;
|
||||
rvwOpnnSn?: number;
|
||||
cnstpSeCd?: string;
|
||||
rvwOpnnClsfComCd?: string;
|
||||
rvwOpnnClsfComCdNm?: string;
|
||||
ognTpcoAmt?: number;
|
||||
ognGvexpAmt?: number;
|
||||
ognLcexpAmt?: number;
|
||||
chgTpcoAmt?: number;
|
||||
chgGvexpAmt?: number;
|
||||
chgLcexpAmt?: number;
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnMngDetResponseDto {
|
||||
pwrkCnsltnDocNo: string;
|
||||
upDocNo: string;
|
||||
upDocSeComCd?: string;
|
||||
upDocSeComCdNm?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
prgrsSttsComCdNm?: string;
|
||||
bizNm?: string;
|
||||
atchFileGroupId?: string;
|
||||
regDt?: string;
|
||||
rgtrNm?: string;
|
||||
cnsltnList?: PwrkCnsltnCnsltnResponseDto[];
|
||||
pjtcoAjmtList?: PwrkCnsltnPjtcoAjmtResponseDto[];
|
||||
crtDmndList?: PwrkCnsltnDocCrtDmndResponseDto[];
|
||||
crtRsltList?: PwrkCnsltnDocCrtRsltResponseDto[];
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnMngService {
|
||||
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,9 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<SectionCollapsibleToggle />
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.designCnstnDocNo && (
|
||||
<ProgressStatusSection
|
||||
|
||||
@@ -12,6 +12,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
|
||||
dsgntRvwr: "dsgntRvwr",
|
||||
pwrkCnsltnDocCrt: "pwrkCnsltnDocCrt",
|
||||
pwrkCnsltnDocMng: "pwrkCnsltnDocMng",
|
||||
rvwRsltInq: "rvwRsltInq",
|
||||
rvwStrg: "rvwStrg",
|
||||
rvwCmptn: "rvwCmptn",
|
||||
dmndSplmnt: "dmndSplmnt",
|
||||
@@ -59,11 +60,15 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||
ATRZ02: [
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||
],
|
||||
ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv],
|
||||
ATRZ03: [
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
|
||||
],
|
||||
ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply],
|
||||
},
|
||||
RK: {
|
||||
@@ -81,7 +86,7 @@ interface GetButtonConfigsOptions {
|
||||
}
|
||||
|
||||
const PENDING_APPROVAL_ACTIONS = new Set<FnrsCnsltnViewButtonAction>([
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||
...ROLE_USER01_ATRZ_STATUS_ACTIONS.TE.ATRZ02,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
|
||||
]);
|
||||
@@ -109,8 +114,14 @@ function resolveRoleUser01AtrzButtonActions(
|
||||
}
|
||||
|
||||
return actions.filter((action) => {
|
||||
const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action);
|
||||
if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false;
|
||||
|
||||
if (prgrsSttsComCd === "TE" && options?.hasCompletedDraft != null) {
|
||||
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) {
|
||||
if (
|
||||
action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng ||
|
||||
action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq
|
||||
) {
|
||||
return options.hasCompletedDraft;
|
||||
}
|
||||
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) {
|
||||
@@ -118,9 +129,7 @@ function resolveRoleUser01AtrzButtonActions(
|
||||
}
|
||||
}
|
||||
|
||||
const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action);
|
||||
|
||||
return !needsPendingApproval || options?.hasPendingApprovalRequest !== false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export const fnrsCnsltnRegistSchema = z.object({
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().nullish(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntArtclComCd: z.string().nullable(),
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
splmntDmndDt: z.string().optional(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnSplmntDmndItem,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import type { PwrkCnsltnCnsltnResponseDto } from "@/entities/pwrk-cnsltn-mng";
|
||||
import {
|
||||
mapFnrsSharDetToFormList,
|
||||
type SplmntDmndDialogConfirmPayload,
|
||||
@@ -129,6 +130,26 @@ export function buildFileSplmntReqPayload(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRvwRsltSplmntReqPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
selected: PwrkCnsltnCnsltnResponseDto[],
|
||||
checklistFormState: ChecklistFormState,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
const existingItems: FnrsCnsltnSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) =>
|
||||
toSplmntDmndSaveItem(item),
|
||||
);
|
||||
|
||||
const newItems: FnrsCnsltnSplmntDmndItem[] = selected.map((row) => ({
|
||||
splmntDmndCn: row.rvwOpnn,
|
||||
cmptnYn: "N",
|
||||
}));
|
||||
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
|
||||
splmntDmndList: [...existingItems, ...newItems],
|
||||
};
|
||||
}
|
||||
|
||||
export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
|
||||
return {
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
|
||||
@@ -28,7 +28,11 @@ import {
|
||||
fnrsCnsltnMngService,
|
||||
type FnrsExamineRequestDto,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng";
|
||||
import {
|
||||
findCompletedPwrkDraft,
|
||||
type PwrkCnsltnCnsltnResponseDto,
|
||||
usePwrkCnsltnMngByUpDocNo,
|
||||
} from "@/entities/pwrk-cnsltn-mng";
|
||||
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
@@ -41,6 +45,7 @@ import {
|
||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import {
|
||||
buildFileSplmntReqPayload,
|
||||
buildRvwRsltSplmntReqPayload,
|
||||
buildSavePayload,
|
||||
buildSupplementReqPayload,
|
||||
buildTechReviewReplyPayload,
|
||||
@@ -51,6 +56,7 @@ import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opi
|
||||
|
||||
import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog";
|
||||
import { PwrkCnsltnDocMngDialog } from "./pwrk-cnsltn-doc-mng-dialog";
|
||||
import { PwrkCnsltnRvwRsltInqDialog } from "./pwrk-cnsltn-rvw-rslt-inq-dialog";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: FnrsCnsltnMngDetResponseDto;
|
||||
@@ -73,6 +79,7 @@ type ActiveDialog =
|
||||
| "fileSplmnt"
|
||||
| "elaprApproval"
|
||||
| "pwrkCnsltnDocMng"
|
||||
| "rvwRsltInq"
|
||||
| null;
|
||||
|
||||
function FloatingActionButtons({
|
||||
@@ -311,7 +318,7 @@ function FloatingActionButtons({
|
||||
data,
|
||||
checklistFormState,
|
||||
"ENV",
|
||||
"EP",
|
||||
"RM",
|
||||
"CP",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
@@ -320,7 +327,7 @@ function FloatingActionButtons({
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
|
||||
request: payload,
|
||||
successMessage: "검토결과 회신이 완료되었습니다.",
|
||||
successMessage: "재원협의 결과 회신이 완료되었습니다.",
|
||||
});
|
||||
techReviewOpinionRef?.current?.resetForm();
|
||||
}
|
||||
@@ -435,6 +442,16 @@ function FloatingActionButtons({
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRvwRsltSplmntConfirm(selected: PwrkCnsltnCnsltnResponseDto[]) {
|
||||
setActiveDialog(null);
|
||||
const requestPayload = buildRvwRsltSplmntReqPayload(data, selected, checklistFormState);
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||
request: requestPayload,
|
||||
successMessage: "보완요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
@@ -486,7 +503,7 @@ function FloatingActionButtons({
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && (
|
||||
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
|
||||
검토결과 회신
|
||||
재원협의 결과 회신
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
|
||||
@@ -518,6 +535,15 @@ function FloatingActionButtons({
|
||||
실무협의서 관리
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && (
|
||||
<Button
|
||||
color="sky"
|
||||
onClick={() => setActiveDialog("rvwRsltInq")}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
검토결과조회
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && (
|
||||
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
|
||||
저장
|
||||
@@ -588,8 +614,17 @@ function FloatingActionButtons({
|
||||
<PwrkCnsltnDocMngDialog
|
||||
open={activeDialog === "pwrkCnsltnDocMng"}
|
||||
fnrsCnsltnDocNo={docNo}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
onClose={() => setActiveDialog(null)}
|
||||
/>
|
||||
|
||||
<PwrkCnsltnRvwRsltInqDialog
|
||||
open={activeDialog === "rvwRsltInq"}
|
||||
fnrsCnsltnDocNo={docNo}
|
||||
isSubmitting={isProcessing}
|
||||
onClose={() => setActiveDialog(null)}
|
||||
onConfirm={handleRvwRsltSplmntConfirm}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -704,6 +739,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
|
||||
dsgntRvwr: handleDsgntRvwr,
|
||||
pwrkCnsltnDocCrt: () => Promise.resolve(),
|
||||
pwrkCnsltnDocMng: () => Promise.resolve(),
|
||||
rvwRsltInq: () => Promise.resolve(),
|
||||
rvwStrg: handleTechReview,
|
||||
rvwCmptn: handleTechReview,
|
||||
dmndSplmnt: handleDmndSplmnt,
|
||||
|
||||
@@ -42,9 +42,12 @@ const HangulGiangi = dynamic(
|
||||
|
||||
const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
|
||||
|
||||
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
|
||||
|
||||
interface PwrkCnsltnDocMngDialogProps {
|
||||
open: boolean;
|
||||
fnrsCnsltnDocNo: string;
|
||||
atrzSeComCdList: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -59,7 +62,12 @@ function createPwrkAtchUploadKey(file: File) {
|
||||
return `${PWRK_ATCH_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDocMngDialogProps) {
|
||||
function PwrkCnsltnDocMngDialog({
|
||||
open,
|
||||
fnrsCnsltnDocNo,
|
||||
atrzSeComCdList,
|
||||
onClose,
|
||||
}: PwrkCnsltnDocMngDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const downloadByAtflId = useDownloadAttachmentByAtflId();
|
||||
@@ -235,6 +243,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo
|
||||
}
|
||||
|
||||
const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]);
|
||||
const canSave = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code));
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
@@ -263,6 +272,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo
|
||||
documentBlob={documentBlob}
|
||||
documentKey={completedDraft.crtDmndUuid}
|
||||
documentFileName={documentFileName}
|
||||
canSave={canSave}
|
||||
onSaveDocument={handleSaveDocument}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
type PwrkCnsltnCnsltnResponseDto,
|
||||
usePwrkCnsltnMngByUpDocNo,
|
||||
} from "@/entities/pwrk-cnsltn-mng";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
interface PwrkCnsltnRvwRsltInqDialogProps {
|
||||
open: boolean;
|
||||
fnrsCnsltnDocNo: string;
|
||||
isSubmitting?: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (selected: PwrkCnsltnCnsltnResponseDto[]) => void;
|
||||
}
|
||||
|
||||
interface CnsltnListTableProps {
|
||||
rows: PwrkCnsltnCnsltnResponseDto[];
|
||||
selectedRowKeys: Set<string>;
|
||||
onToggleAll: (checked: boolean) => void;
|
||||
onToggleRow: (rowKey: string, checked: boolean) => void;
|
||||
}
|
||||
|
||||
function PwrkCnsltnRvwRsltInqDialog({
|
||||
open,
|
||||
fnrsCnsltnDocNo,
|
||||
isSubmitting = false,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: PwrkCnsltnRvwRsltInqDialogProps) {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<Set<string>>(new Set());
|
||||
const {
|
||||
data: pwrkCnsltnData,
|
||||
isLoading,
|
||||
isError,
|
||||
} = usePwrkCnsltnMngByUpDocNo(fnrsCnsltnDocNo, open);
|
||||
|
||||
const cnsltnList = pwrkCnsltnData?.cnsltnList ?? [];
|
||||
const selectedRows = cnsltnList.filter((row, index) =>
|
||||
selectedRowKeys.has(getCnsltnRowKey(row, index)),
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
setSelectedRowKeys(new Set());
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleOpenChange(nextOpen: boolean) {
|
||||
if (!nextOpen) handleClose();
|
||||
}
|
||||
|
||||
function handleToggleAll(checked: boolean) {
|
||||
if (!checked) {
|
||||
setSelectedRowKeys(new Set());
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedRowKeys(new Set(cnsltnList.map((row, index) => getCnsltnRowKey(row, index))));
|
||||
}
|
||||
|
||||
function handleToggleRow(rowKey: string, checked: boolean) {
|
||||
setSelectedRowKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (checked) next.add(rowKey);
|
||||
else next.delete(rowKey);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (selectedRows.length === 0) return;
|
||||
onConfirm(selectedRows);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
className="max-h-[96vh] max-w-[120rem] overflow-y-auto"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>검토결과조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isError ? (
|
||||
<p className="py-16 text-center text-lg text-destructive">
|
||||
실무협의 검토결과를 불러오지 못했습니다.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!isLoading && !isError && pwrkCnsltnData ? (
|
||||
<div className="h-[64rem] overflow-x-auto">
|
||||
{cnsltnList.length === 0 ? (
|
||||
<p className="py-10 text-center text-muted-foreground">조회된 검토의견이 없습니다.</p>
|
||||
) : (
|
||||
<CnsltnListTable
|
||||
rows={cnsltnList}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onToggleAll={handleToggleAll}
|
||||
onToggleRow={handleToggleRow}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={handleClose} disabled={isSubmitting}>
|
||||
닫기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleConfirm}
|
||||
disabled={isSubmitting || selectedRows.length === 0}
|
||||
>
|
||||
보완요청
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CnsltnListTable({
|
||||
rows,
|
||||
selectedRowKeys,
|
||||
onToggleAll,
|
||||
onToggleRow,
|
||||
}: CnsltnListTableProps) {
|
||||
const selectedCount = rows.filter((row, index) =>
|
||||
selectedRowKeys.has(getCnsltnRowKey(row, index)),
|
||||
).length;
|
||||
const isAllSelected = rows.length > 0 && selectedCount === rows.length;
|
||||
const isSomeSelected = selectedCount > 0 && !isAllSelected;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="42%" />
|
||||
<col width="50%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky top-0">
|
||||
<TableRow>
|
||||
<TableHead className="w-14" align="center">
|
||||
<Checkbox
|
||||
checked={isAllSelected ? true : isSomeSelected ? "indeterminate" : false}
|
||||
onCheckedChange={(checked) => onToggleAll(checked === true)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>분류</TableHead>
|
||||
<TableHead>검토의견</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row, index) => {
|
||||
const rowKey = getCnsltnRowKey(row, index);
|
||||
const isSelected = selectedRowKeys.has(rowKey);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={rowKey}
|
||||
data-state={isSelected ? "selected" : undefined}
|
||||
className={isSelected ? "!bg-primary-transparent" : undefined}
|
||||
>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => onToggleRow(rowKey, checked === true)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<p className="whitespace-pre-wrap">{formatOptional(row.rmrk)}</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<p className="whitespace-pre-wrap">{formatOptional(row.rvwOpnn)}</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function getCnsltnRowKey(row: PwrkCnsltnCnsltnResponseDto, index: number) {
|
||||
return [row.pwrkCnsltnDocNo, row.verSn, row.dtlSn, row.rvwOpnnSn, String(index)]
|
||||
.filter((value) => value !== undefined && value !== null && value !== "")
|
||||
.join("::");
|
||||
}
|
||||
|
||||
function formatOptional(value: string | number | undefined | null) {
|
||||
if (value === undefined || value === null) return "-";
|
||||
const text = String(value).trim();
|
||||
return text ? text : "-";
|
||||
}
|
||||
|
||||
export { PwrkCnsltnRvwRsltInqDialog };
|
||||
export type { PwrkCnsltnRvwRsltInqDialogProps };
|
||||
@@ -225,7 +225,9 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<SectionCollapsibleToggle />
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto.instlSncnDocNo && (
|
||||
<ProgressStatusSection
|
||||
|
||||
@@ -242,7 +242,9 @@ function SewerBscPlanFormPage({
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<SectionCollapsibleToggle />
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.sewerBscPlanDocNo && (
|
||||
<ProgressStatusSection
|
||||
|
||||
@@ -203,7 +203,7 @@ export interface EsntlAtchFileSaveBase {
|
||||
/** 보완요구 저장 공통 필드 */
|
||||
export interface SplmntDmndSaveBase {
|
||||
splmntDmndSn?: number | null;
|
||||
splmntArtclComCd?: string;
|
||||
splmntArtclComCd?: string | null;
|
||||
splmntDmndCn?: string;
|
||||
splmntDmndDt?: string;
|
||||
splmntDmndAtchFileGroupId?: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@common/components";
|
||||
|
||||
interface HangulGiangiToolbarProps {
|
||||
isBusy?: boolean;
|
||||
canSave?: boolean;
|
||||
onSave: () => void;
|
||||
onDownload: () => void;
|
||||
onFullscreen: () => void;
|
||||
@@ -13,16 +14,19 @@ interface HangulGiangiToolbarProps {
|
||||
|
||||
function HangulGiangiToolbar({
|
||||
isBusy = false,
|
||||
canSave = true,
|
||||
onSave,
|
||||
onDownload,
|
||||
onFullscreen,
|
||||
}: HangulGiangiToolbarProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-nowrap items-center gap-2 border-b border-border bg-background p-3">
|
||||
<Button type="button" color="white" size="sm" disabled={isBusy} onClick={onSave}>
|
||||
<Save className="!size-5" />
|
||||
저장
|
||||
</Button>
|
||||
{canSave && (
|
||||
<Button type="button" color="white" size="sm" disabled={isBusy} onClick={onSave}>
|
||||
<Save className="!size-5" />
|
||||
저장
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" color="white" size="sm" disabled={isBusy} onClick={onDownload}>
|
||||
<Download className="!size-5" />
|
||||
다운로드
|
||||
|
||||
@@ -21,6 +21,7 @@ function HangulGiangi({
|
||||
documentFileName,
|
||||
onSaveDocument,
|
||||
onDownload,
|
||||
canSave = true,
|
||||
bridgeUrl = DEFAULT_BRIDGE_URL,
|
||||
ctrlBaseUrl = WEBHYP_CTRL_URL,
|
||||
className,
|
||||
@@ -183,6 +184,7 @@ function HangulGiangi({
|
||||
>
|
||||
<HangulGiangiToolbar
|
||||
isBusy={isBusy || !isCtrlReady}
|
||||
canSave={canSave}
|
||||
onSave={handleSave}
|
||||
onDownload={handleDownload}
|
||||
onFullscreen={handleFullscreen}
|
||||
@@ -296,6 +298,7 @@ interface HangulGiangiProps {
|
||||
documentFileName: string;
|
||||
onSaveDocument: (file: Blob) => Promise<void>;
|
||||
onDownload: () => Promise<void>;
|
||||
canSave?: boolean;
|
||||
bridgeUrl?: string;
|
||||
ctrlBaseUrl?: string;
|
||||
className?: string;
|
||||
|
||||
Reference in New Issue
Block a user