Compare commits
10
Commits
d735ad7dd7
...
26d8410649
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d8410649 | ||
|
|
8403ad556c | ||
|
|
d26c15f857 | ||
|
|
86ed664e3e | ||
|
|
a105f395dd | ||
|
|
a5ebc12580 | ||
|
|
270f6e366f | ||
|
|
acf215c1bf | ||
|
|
8a98cd7d19 | ||
|
|
3a5b48b608 |
@@ -0,0 +1,22 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.FNRS_CNSLTN_RVW_OPNN;
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
fnrsCnsltnDocNo: string,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
body: RvwOpnnUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${fnrsCnsltnDocNo}/${rvwOpnnSn}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFn(fnrsCnsltnDocNo: string, rvwOpnnSn: number): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${fnrsCnsltnDocNo}/${rvwOpnnSn}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fnrsCnsltnRvwOpnnService = {
|
||||||
|
update,
|
||||||
|
delete: deleteFn,
|
||||||
|
};
|
||||||
@@ -1,65 +1,66 @@
|
|||||||
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
export { fnrsCnsltnRvwOpnnService } from "./api/fnrs-cnsltn-rvw-opnn-service";
|
||||||
export { fnrsCnsltnKeys } from "./api/queries";
|
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
||||||
export {
|
export { fnrsCnsltnKeys } from "./api/queries";
|
||||||
useChangePrgrsSttsComCd,
|
export {
|
||||||
useDeleteFnrsCnsltnMng,
|
useChangePrgrsSttsComCd,
|
||||||
useFnrsCnsltnFileVrfcResults,
|
useDeleteFnrsCnsltnMng,
|
||||||
useFnrsCnsltnMng,
|
useFnrsCnsltnFileVrfcResults,
|
||||||
useFnrsCnsltnMngPage,
|
useFnrsCnsltnMng,
|
||||||
useRequestActnReviewFnrsCnsltnMng,
|
useFnrsCnsltnMngPage,
|
||||||
useRequestExamineFnrsCnsltnMng,
|
useRequestActnReviewFnrsCnsltnMng,
|
||||||
useSaveApplyFnrsCnsltnMng,
|
useRequestExamineFnrsCnsltnMng,
|
||||||
useSaveFnrsCnsltnMng,
|
useSaveApplyFnrsCnsltnMng,
|
||||||
useSubmitReqSupplementFnrsCnsltnMng,
|
useSaveFnrsCnsltnMng,
|
||||||
useSubmitSupplementFnrsCnsltnMng,
|
useSubmitReqSupplementFnrsCnsltnMng,
|
||||||
useTechReviewFnrsCnsltnMng,
|
useSubmitSupplementFnrsCnsltnMng,
|
||||||
useUpdateFnrsCnsltnMng,
|
useTechReviewFnrsCnsltnMng,
|
||||||
} from "./api/queries";
|
useUpdateFnrsCnsltnMng,
|
||||||
export type {
|
} from "./api/queries";
|
||||||
BizCstItem,
|
export type {
|
||||||
BzScaleItem,
|
BizCstItem,
|
||||||
CklstResultItem,
|
BzScaleItem,
|
||||||
EsntlAtchFileItem,
|
CklstResultItem,
|
||||||
FnrsCnsltnApplySaveRequestDto,
|
EsntlAtchFileItem,
|
||||||
FnrsCnsltnMngSaveRequestDto,
|
FnrsCnsltnApplySaveRequestDto,
|
||||||
FnrsCnsltnRvwOpnnSaveRequestDto,
|
FnrsCnsltnMngSaveRequestDto,
|
||||||
FnrsCnsltnSplmntDmndItem,
|
FnrsCnsltnRvwOpnnSaveRequestDto,
|
||||||
FnrsSharItem,
|
FnrsCnsltnSplmntDmndItem,
|
||||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
FnrsSharItem,
|
||||||
export {
|
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||||
fnrsCnsltnApplySaveSchema,
|
export {
|
||||||
fnrsCnsltnBizCstItemSchema,
|
fnrsCnsltnApplySaveSchema,
|
||||||
fnrsCnsltnBzScaleItemSchema,
|
fnrsCnsltnBizCstItemSchema,
|
||||||
fnrsCnsltnCklstResultItemSchema,
|
fnrsCnsltnBzScaleItemSchema,
|
||||||
fnrsCnsltnEsntlAtchFileItemSchema,
|
fnrsCnsltnCklstResultItemSchema,
|
||||||
fnrsCnsltnMngSaveSchema,
|
fnrsCnsltnEsntlAtchFileItemSchema,
|
||||||
fnrsCnsltnRvwOpnnSaveSchema,
|
fnrsCnsltnMngSaveSchema,
|
||||||
fnrsCnsltnSplmntDmndItemSchema,
|
fnrsCnsltnRvwOpnnSaveSchema,
|
||||||
fnrsSharItemSchema,
|
fnrsCnsltnSplmntDmndItemSchema,
|
||||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
fnrsSharItemSchema,
|
||||||
export type {
|
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||||
AtflMngClientResponseDto,
|
export type {
|
||||||
BizCstTreeDto,
|
AtflMngClientResponseDto,
|
||||||
BizSclTreeDto,
|
BizCstTreeDto,
|
||||||
CklstRvwRsltTreeDto,
|
BizSclTreeDto,
|
||||||
DocPrgrsUpdateRequestDto,
|
CklstRvwRsltTreeDto,
|
||||||
ElaprDocPrgrsClientResponseDto,
|
DocPrgrsUpdateRequestDto,
|
||||||
EsntlAtchFileMngTreeResponseDto,
|
ElaprDocPrgrsClientResponseDto,
|
||||||
FnrsCnsltnFileVrfcRsltResponseDto,
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
FnrsCnsltnMngDetResponseDto,
|
FnrsCnsltnFileVrfcRsltResponseDto,
|
||||||
FnrsCnsltnMngResponseDto,
|
FnrsCnsltnMngDetResponseDto,
|
||||||
FnrsCnsltnMngSearchDto,
|
FnrsCnsltnMngResponseDto,
|
||||||
FnrsCnsltnMngService,
|
FnrsCnsltnMngSearchDto,
|
||||||
FnrsCnsltnMngUpdateRequestDto,
|
FnrsCnsltnMngService,
|
||||||
FnrsCnsltnRvwOpnnResponseDto,
|
FnrsCnsltnMngUpdateRequestDto,
|
||||||
FnrsCnsltnSplmntDmndResponseDto,
|
FnrsCnsltnRvwOpnnResponseDto,
|
||||||
RagIndexingInfoDto,
|
FnrsCnsltnSplmntDmndResponseDto,
|
||||||
} from "./model/fnrs-cnsltn-interface";
|
RagIndexingInfoDto,
|
||||||
export type {
|
} from "./model/fnrs-cnsltn-interface";
|
||||||
EnvoActnReviewReqDto,
|
export type {
|
||||||
FnrsExamineRequestDto,
|
EnvoActnReviewReqDto,
|
||||||
} from "./model/fnrs-cnsltn-payload-schema";
|
FnrsExamineRequestDto,
|
||||||
export {
|
} from "./model/fnrs-cnsltn-payload-schema";
|
||||||
envoActnReviewReqSchema,
|
export {
|
||||||
fnrsExamineRequestSchema,
|
envoActnReviewReqSchema,
|
||||||
} from "./model/fnrs-cnsltn-payload-schema";
|
fnrsExamineRequestSchema,
|
||||||
|
} from "./model/fnrs-cnsltn-payload-schema";
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.INSTL_SNCN_RVW_OPNN;
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
instlSncnDocNo: string,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
body: RvwOpnnUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFn(instlSncnDocNo: string, rvwOpnnSn: number): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const instlSncnRvwOpnnService = {
|
||||||
|
update,
|
||||||
|
delete: deleteFn,
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { instlSncnRvwOpnnService } from "./api/instl-sncn-rvw-opnn-service";
|
||||||
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
|
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
|
||||||
export {
|
export {
|
||||||
instlSncnKeys,
|
instlSncnKeys,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.SEWER_BSC_PLAN_RVW_OPNNS;
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
sewerBscPlanDocNo: string,
|
||||||
|
chgCycl: number,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
body: RvwOpnnUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${sewerBscPlanDocNo}/${chgCycl}/${rvwOpnnSn}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFn(sewerBscPlanDocNo: string, chgCycl: number, rvwOpnnSn: number): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${sewerBscPlanDocNo}/${chgCycl}/${rvwOpnnSn}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sewerBscPlanRvwOpnnService = {
|
||||||
|
update,
|
||||||
|
delete: deleteFn,
|
||||||
|
};
|
||||||
@@ -1,64 +1,65 @@
|
|||||||
export { sewerBscPlanKeys } from "./api/queries";
|
export { sewerBscPlanKeys } from "./api/queries";
|
||||||
export {
|
export {
|
||||||
useDeleteSewerBscPlan,
|
useDeleteSewerBscPlan,
|
||||||
useSaveApplySewerBscPlan,
|
useSaveApplySewerBscPlan,
|
||||||
useSaveSewerBscPlan,
|
useSaveSewerBscPlan,
|
||||||
useSewerBscPlan,
|
useSewerBscPlan,
|
||||||
useSewerBscPlanDetails,
|
useSewerBscPlanDetails,
|
||||||
useSewerBscPlanPage,
|
useSewerBscPlanPage,
|
||||||
useSubmitReqSupplementSewerBscPlan,
|
useSubmitReqSupplementSewerBscPlan,
|
||||||
useSubmitSupplementSewerBscPlan,
|
useSubmitSupplementSewerBscPlan,
|
||||||
useTechReviewSewerBscPlan,
|
useTechReviewSewerBscPlan,
|
||||||
useUpdateSewerBscPlan,
|
useUpdateSewerBscPlan,
|
||||||
} from "./api/queries";
|
} from "./api/queries";
|
||||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
export { sewerBscPlanRvwOpnnService } from "./api/sewer-bsc-plan-rvw-opnn-service";
|
||||||
export type {
|
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||||
CklstResultItem,
|
export type {
|
||||||
EsntlAtchFileItem,
|
CklstResultItem,
|
||||||
SewerBscPlanApplySaveRequestDto,
|
EsntlAtchFileItem,
|
||||||
SewerBscPlanBizCstItem,
|
SewerBscPlanApplySaveRequestDto,
|
||||||
SewerBscPlanDtlSaveRequestDto,
|
SewerBscPlanBizCstItem,
|
||||||
SewerBscPlanFnrsCstItem,
|
SewerBscPlanDtlSaveRequestDto,
|
||||||
SewerBscPlanOtlnItem,
|
SewerBscPlanFnrsCstItem,
|
||||||
SewerBscPlanRvwOpnnItem,
|
SewerBscPlanOtlnItem,
|
||||||
SewerBscPlanSaveRequestDto,
|
SewerBscPlanRvwOpnnItem,
|
||||||
SewerBscPlanSplmntDmndItem,
|
SewerBscPlanSaveRequestDto,
|
||||||
SewerBscPlanTmplntDtlItem,
|
SewerBscPlanSplmntDmndItem,
|
||||||
SewerBscPlanTmplntItem,
|
SewerBscPlanTmplntDtlItem,
|
||||||
SewerBscPlanTmplntTmprStrgItem,
|
SewerBscPlanTmplntItem,
|
||||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
SewerBscPlanTmplntTmprStrgItem,
|
||||||
export {
|
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||||
sewerBscPlanApplySaveSchema,
|
export {
|
||||||
sewerBscPlanBizCstItemSchema,
|
sewerBscPlanApplySaveSchema,
|
||||||
sewerBscPlanCklstResultItemSchema,
|
sewerBscPlanBizCstItemSchema,
|
||||||
sewerBscPlanDtlSaveSchema,
|
sewerBscPlanCklstResultItemSchema,
|
||||||
sewerBscPlanEsntlAtchFileItemSchema,
|
sewerBscPlanDtlSaveSchema,
|
||||||
sewerBscPlanFnrsCstItemSchema,
|
sewerBscPlanEsntlAtchFileItemSchema,
|
||||||
sewerBscPlanOtlnItemSchema,
|
sewerBscPlanFnrsCstItemSchema,
|
||||||
sewerBscPlanRvwOpnnItemSchema,
|
sewerBscPlanOtlnItemSchema,
|
||||||
sewerBscPlanSaveSchema,
|
sewerBscPlanRvwOpnnItemSchema,
|
||||||
sewerBscPlanSplmntDmndItemSchema,
|
sewerBscPlanSaveSchema,
|
||||||
sewerBscPlanTmplntDtlItemSchema,
|
sewerBscPlanSplmntDmndItemSchema,
|
||||||
sewerBscPlanTmplntItemSchema,
|
sewerBscPlanTmplntDtlItemSchema,
|
||||||
sewerBscPlanTmplntTmprStrgItemSchema,
|
sewerBscPlanTmplntItemSchema,
|
||||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
sewerBscPlanTmplntTmprStrgItemSchema,
|
||||||
export type {
|
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||||
AtflMngClientResponseDto,
|
export type {
|
||||||
CklstRvwRsltTreeDto,
|
AtflMngClientResponseDto,
|
||||||
EsntlAtchFileMngTreeResponseDto,
|
CklstRvwRsltTreeDto,
|
||||||
SewerBscPlanApplySaveResponseDto,
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
SewerBscPlanBizCstResponseDto,
|
SewerBscPlanApplySaveResponseDto,
|
||||||
SewerBscPlanDetResponseDto,
|
SewerBscPlanBizCstResponseDto,
|
||||||
SewerBscPlanDtlResponseDto,
|
SewerBscPlanDetResponseDto,
|
||||||
SewerBscPlanFnrsCstResponseDto,
|
SewerBscPlanDtlResponseDto,
|
||||||
SewerBscPlanOtlnResponseDto,
|
SewerBscPlanFnrsCstResponseDto,
|
||||||
SewerBscPlanResponseDto,
|
SewerBscPlanOtlnResponseDto,
|
||||||
SewerBscPlanRvwOpnnResponseDto,
|
SewerBscPlanResponseDto,
|
||||||
SewerBscPlanSearchRequestDto,
|
SewerBscPlanRvwOpnnResponseDto,
|
||||||
SewerBscPlanService,
|
SewerBscPlanSearchRequestDto,
|
||||||
SewerBscPlanSplmntDmndResponseDto,
|
SewerBscPlanService,
|
||||||
SewerBscPlanTmplntDtlResponseDto,
|
SewerBscPlanSplmntDmndResponseDto,
|
||||||
SewerBscPlanTmplntResponseDto,
|
SewerBscPlanTmplntDtlResponseDto,
|
||||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
SewerBscPlanTmplntResponseDto,
|
||||||
SewerBscPlanUpdateRequestDto,
|
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||||
} from "./model/sewer-bsc-plan-interface";
|
SewerBscPlanUpdateRequestDto,
|
||||||
|
} from "./model/sewer-bsc-plan-interface";
|
||||||
|
|||||||
@@ -1,338 +1,334 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { TextShimmer } from "@/shared/ui";
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import {
|
import { attachService } from "@/entities/attach";
|
||||||
Button,
|
import {
|
||||||
RadioGroup,
|
designCnstnKeys,
|
||||||
RadioGroupItem,
|
type DesignCnstnRvwOpnnResponseDto,
|
||||||
Table,
|
designCnstnRvwOpnnService,
|
||||||
TableBody,
|
} from "@/entities/design-cnstn";
|
||||||
TableCell,
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
TableHead,
|
import {
|
||||||
TableHeader,
|
Button,
|
||||||
TableRow,
|
RadioGroup,
|
||||||
Textarea,
|
RadioGroupItem,
|
||||||
} from "@common/components";
|
Table,
|
||||||
|
TableCell,
|
||||||
import type {
|
TableHead,
|
||||||
ReviewOpinionFormData,
|
TableHeader,
|
||||||
ReviewOpinionGetFormDataResult,
|
TableRow,
|
||||||
} from "../../lib/review-opinion-utils";
|
Textarea,
|
||||||
import type { ReviewStepCode } from "../../model/view-types";
|
} from "@common/components";
|
||||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
|
||||||
|
import type {
|
||||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
ReviewOpinionFormData,
|
||||||
|
ReviewOpinionGetFormDataResult,
|
||||||
interface TechReviewOpinionSectionProps {
|
} from "../../lib/review-opinion-utils";
|
||||||
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
editableReviewStep?: ReviewStepCode;
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
atrzSeComCdList?: string[];
|
|
||||||
isAtrzLoading?: boolean;
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
}
|
|
||||||
|
interface TechReviewOpinionSectionProps {
|
||||||
export interface TechReviewOpinionSectionHandle {
|
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
editableReviewStep?: ReviewStepCode;
|
||||||
resetForm: () => void;
|
atrzSeComCdList?: string[];
|
||||||
}
|
isAtrzLoading?: boolean;
|
||||||
|
}
|
||||||
interface TechReviewOpinionTableProps {
|
|
||||||
items: DesignCnstnRvwOpnnResponseDto[];
|
export interface TechReviewOpinionSectionHandle {
|
||||||
}
|
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||||
|
resetForm: () => void;
|
||||||
interface UploadedOpinionFile {
|
}
|
||||||
fileName: string;
|
|
||||||
fileKey: string;
|
interface TechReviewOpinionTableProps {
|
||||||
atflId?: string;
|
items: DesignCnstnRvwOpnnResponseDto[];
|
||||||
atflCd?: string;
|
}
|
||||||
}
|
|
||||||
|
interface UploadedOpinionFile {
|
||||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
fileName: string;
|
||||||
rvwRsltCn: "",
|
fileKey: string;
|
||||||
rvwRsltAnmlyYn: "",
|
atflId?: string;
|
||||||
atchFileGroupId: "",
|
atflCd?: string;
|
||||||
opnrSeComCd: "",
|
}
|
||||||
};
|
|
||||||
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
function createRvwOpnnUploadKey(file: File) {
|
rvwRsltCn: "",
|
||||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
rvwRsltAnmlyYn: "",
|
||||||
}
|
atchFileGroupId: "",
|
||||||
|
opnrSeComCd: "",
|
||||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
};
|
||||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
|
||||||
}
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
const TechReviewOpinionSection = forwardRef<
|
}
|
||||||
TechReviewOpinionSectionHandle,
|
|
||||||
TechReviewOpinionSectionProps
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
>(function TechReviewOpinionSection(
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
}
|
||||||
ref,
|
|
||||||
) {
|
const TechReviewOpinionSection = forwardRef<
|
||||||
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
TechReviewOpinionSectionHandle,
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
TechReviewOpinionSectionProps
|
||||||
...EMPTY_FORM_DATA,
|
>(function TechReviewOpinionSection(
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
});
|
ref,
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
) {
|
||||||
|
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
useEffect(() => {
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
setFormData((prev) => ({
|
...EMPTY_FORM_DATA,
|
||||||
...prev,
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
});
|
||||||
}));
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
}, [editableReviewStep]);
|
|
||||||
|
useEffect(() => {
|
||||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
setFormData((prev) => ({
|
||||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
...prev,
|
||||||
if (!atflCd || !atflId) return;
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
}));
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
}, [editableReviewStep]);
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
|
||||||
},
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
onError: (_error, _file, fileKey) => {
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
if (!atflCd || !atflId) return;
|
||||||
},
|
|
||||||
});
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
useImperativeHandle(
|
},
|
||||||
ref,
|
onError: (_error, _file, fileKey) => {
|
||||||
() => ({
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
getFormData: () => {
|
},
|
||||||
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
});
|
||||||
return null;
|
|
||||||
}
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
return {
|
() => ({
|
||||||
formData: {
|
getFormData: () => {
|
||||||
...formData,
|
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
||||||
opnrSeComCd: editableReviewStep,
|
return null;
|
||||||
},
|
}
|
||||||
rvwList,
|
|
||||||
};
|
return {
|
||||||
},
|
formData: {
|
||||||
resetForm: () => {
|
...formData,
|
||||||
setFormData({
|
opnrSeComCd: editableReviewStep,
|
||||||
...EMPTY_FORM_DATA,
|
},
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
rvwList,
|
||||||
});
|
};
|
||||||
setUploadedFile(null);
|
},
|
||||||
},
|
resetForm: () => {
|
||||||
}),
|
setFormData({
|
||||||
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
...EMPTY_FORM_DATA,
|
||||||
);
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
});
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
setUploadedFile(null);
|
||||||
const hasKecoSection =
|
},
|
||||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
}),
|
||||||
|
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
||||||
if (!hasKecoSection) {
|
);
|
||||||
return null;
|
|
||||||
}
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
|
const hasKecoSection =
|
||||||
async function handleFileUpload(files: FileList | null) {
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
||||||
if (!files || files.length === 0) return;
|
|
||||||
|
if (!hasKecoSection) {
|
||||||
const file = files[0];
|
return null;
|
||||||
const fileKey = createRvwOpnnUploadKey(file);
|
}
|
||||||
const existingAtflCd = formData.atchFileGroupId;
|
|
||||||
|
async function handleFileUpload(files: FileList | null) {
|
||||||
setUploadedFile({ fileName: file.name, fileKey });
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
try {
|
const file = files[0];
|
||||||
await uploadFileAsync(file, {
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
fileKey,
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
|
||||||
});
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
} catch {
|
|
||||||
// onError callback handles cleanup
|
try {
|
||||||
}
|
await uploadFileAsync(file, {
|
||||||
}
|
fileKey,
|
||||||
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
async function handleFileRemove() {
|
});
|
||||||
if (!uploadedFile) return;
|
} catch {
|
||||||
|
// onError callback handles cleanup
|
||||||
uploads[uploadedFile.fileKey]?.abort();
|
}
|
||||||
|
}
|
||||||
if (uploadedFile.atflId) {
|
|
||||||
try {
|
async function handleFileRemove() {
|
||||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
if (!uploadedFile) return;
|
||||||
} catch (error) {
|
|
||||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
}
|
|
||||||
}
|
if (uploadedFile.atflId) {
|
||||||
|
try {
|
||||||
setUploadedFile(null);
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
} catch (error) {
|
||||||
}
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
|
}
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
}
|
||||||
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
|
|
||||||
|
setUploadedFile(null);
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
|
}
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
return editableReviewStep === reviewStep && canWriteOpinion;
|
||||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
}
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
<TableCell>
|
|
||||||
<Textarea
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
value={formData.rvwRsltCn}
|
|
||||||
onChange={(event) =>
|
return (
|
||||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
<div className="space-y-2">
|
||||||
}
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
placeholder="의견내용을 입력하세요"
|
<Table
|
||||||
rows={4}
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
className="min-h-[8rem] !text-xl"
|
variant="secondary"
|
||||||
/>
|
>
|
||||||
</TableCell>
|
<TableHeader>
|
||||||
</TableRow>
|
<TableRow>
|
||||||
<TableRow>
|
<TableHead scope="row" className="w-[12rem]">
|
||||||
<TableHead scope="row">검토결과</TableHead>
|
의견내용
|
||||||
<TableCell>
|
</TableHead>
|
||||||
<FieldSet>
|
<TableCell>
|
||||||
<RadioGroup
|
<Textarea
|
||||||
value={formData.rvwRsltAnmlyYn}
|
value={formData.rvwRsltCn}
|
||||||
onValueChange={(value) =>
|
onChange={(event) =>
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
...prev,
|
}
|
||||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
placeholder="의견내용을 입력하세요"
|
||||||
}))
|
rows={4}
|
||||||
}
|
className="min-h-[8rem] !text-xl"
|
||||||
>
|
/>
|
||||||
<div className="flex flex-wrap gap-4">
|
</TableCell>
|
||||||
<FieldLabel
|
</TableRow>
|
||||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
<TableRow>
|
||||||
className="!w-auto border-none"
|
<TableHead scope="row">검토결과</TableHead>
|
||||||
>
|
<TableCell>
|
||||||
<Field orientation="horizontal" className="w-auto">
|
<FieldSet>
|
||||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
<RadioGroup
|
||||||
<span className="font-normal">정상</span>
|
value={formData.rvwRsltAnmlyYn}
|
||||||
</Field>
|
onValueChange={(value) =>
|
||||||
</FieldLabel>
|
setFormData((prev) => ({
|
||||||
<FieldLabel
|
...prev,
|
||||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
className="!w-auto border-none"
|
}))
|
||||||
>
|
}
|
||||||
<Field orientation="horizontal" className="w-auto">
|
>
|
||||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
<div className="flex flex-wrap gap-4">
|
||||||
<span className="font-normal">이상</span>
|
<FieldLabel
|
||||||
</Field>
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
</FieldLabel>
|
className="!w-auto border-none"
|
||||||
</div>
|
>
|
||||||
</RadioGroup>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
</FieldSet>
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
</TableCell>
|
<span className="font-normal">정상</span>
|
||||||
</TableRow>
|
</Field>
|
||||||
<TableRow>
|
</FieldLabel>
|
||||||
<TableHead scope="row">첨부파일</TableHead>
|
<FieldLabel
|
||||||
<TableCell>
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
{uploadedFile ? (
|
className="!w-auto border-none"
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<TextShimmer className="text-xl" active={uploading}>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
{uploadedFile.fileName}
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
</TextShimmer>
|
<span className="font-normal">이상</span>
|
||||||
<Button
|
</Field>
|
||||||
size="icon"
|
</FieldLabel>
|
||||||
onClick={(e) => {
|
</div>
|
||||||
e.preventDefault();
|
</RadioGroup>
|
||||||
void handleFileRemove();
|
</FieldSet>
|
||||||
}}
|
</TableCell>
|
||||||
disabled={uploading}
|
</TableRow>
|
||||||
className="size-8 !bg-destructive"
|
<TableRow>
|
||||||
>
|
<TableHead scope="row">첨부파일</TableHead>
|
||||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
<TableCell>
|
||||||
</Button>
|
{uploadedFile ? (
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
) : (
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
<label>
|
{uploadedFile.fileName}
|
||||||
<input
|
</TextShimmer>
|
||||||
type="file"
|
<Button
|
||||||
className="hidden"
|
size="icon"
|
||||||
onChange={(event) => {
|
onClick={(e) => {
|
||||||
void handleFileUpload(event.target.files);
|
e.preventDefault();
|
||||||
event.target.value = "";
|
void handleFileRemove();
|
||||||
}}
|
}}
|
||||||
/>
|
disabled={uploading}
|
||||||
<Button size="sm" variant="outline" type="button" asChild>
|
className="size-8 !bg-destructive"
|
||||||
<span>파일 선택</span>
|
>
|
||||||
</Button>
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
</label>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</TableCell>
|
) : (
|
||||||
</TableRow>
|
<label>
|
||||||
</TableHeader>
|
<input
|
||||||
</Table>
|
type="file"
|
||||||
</div>
|
className="hidden"
|
||||||
);
|
onChange={(event) => {
|
||||||
}
|
void handleFileUpload(event.target.files);
|
||||||
|
event.target.value = "";
|
||||||
return (
|
}}
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
/>
|
||||||
<div className="space-y-6">
|
<Button size="sm" variant="outline" type="button" asChild>
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
<span>파일 선택</span>
|
||||||
{renderOpinionForm("KECO")}
|
</Button>
|
||||||
</div>
|
</label>
|
||||||
</SectionCollapsible>
|
)}
|
||||||
);
|
</TableCell>
|
||||||
});
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
</Table>
|
||||||
return (
|
</div>
|
||||||
<div className="overflow-x-auto">
|
);
|
||||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
}
|
||||||
<colgroup>
|
|
||||||
<col width="10%" />
|
return (
|
||||||
<col width="55%" />
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
<col width="5%" />
|
<div className="space-y-6">
|
||||||
<col width="20%" />
|
{kecoRvwList.length > 0 ? (
|
||||||
<col width="10%" />
|
<TechReviewOpinionTable items={kecoRvwList} />
|
||||||
</colgroup>
|
) : null}
|
||||||
<TableHeader>
|
{renderOpinionForm("KECO")}
|
||||||
<TableRow>
|
</div>
|
||||||
<TableHead>의견자명</TableHead>
|
</SectionCollapsible>
|
||||||
<TableHead>의견내용</TableHead>
|
);
|
||||||
<TableHead>검토결과</TableHead>
|
});
|
||||||
<TableHead>첨부파일</TableHead>
|
|
||||||
<TableHead>등록일시</TableHead>
|
function getRvwOpnnIds(item: DesignCnstnRvwOpnnResponseDto) {
|
||||||
</TableRow>
|
if (!item.designCnstnDocNo || item.rvwOpnnSn == null) {
|
||||||
</TableHeader>
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
<TableBody>
|
}
|
||||||
{items.map((item) => (
|
return [item.designCnstnDocNo, item.rvwOpnnSn] as const;
|
||||||
<TableRow key={`${item.designCnstnDocNo}_${item.rvwOpnnSn}`}>
|
}
|
||||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
|
||||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
<TableCell align="center">
|
const queryClient = useQueryClient();
|
||||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
return (
|
||||||
{item.atchFileInfo?.atflCd ? (
|
<RvwOpnnListTable
|
||||||
<AttachmentDownloadLink
|
items={items}
|
||||||
atflCd={item.atchFileInfo.atflCd}
|
getRowKey={(item) => `${item.designCnstnDocNo}_${item.rvwOpnnSn}`}
|
||||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
currentUserUuid={currentUserUuid}
|
||||||
/>
|
onUpdate={async (item, body) => {
|
||||||
) : (
|
await designCnstnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
"-"
|
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||||
)}
|
}}
|
||||||
</TableCell>
|
onDelete={async (item) => {
|
||||||
<TableCell align="center">
|
await designCnstnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||||
</TableCell>
|
}}
|
||||||
</TableRow>
|
/>
|
||||||
))}
|
);
|
||||||
</TableBody>
|
}
|
||||||
</Table>
|
|
||||||
</div>
|
export { TechReviewOpinionSection };
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { TechReviewOpinionSection };
|
|
||||||
|
|||||||
@@ -1,352 +1,345 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import {
|
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
||||||
AttachmentDownloadLink,
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
attachService,
|
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
|
||||||
useDownloadAttachmentByAtflId,
|
import {
|
||||||
} from "@/entities/attach";
|
fnrsCnsltnKeys,
|
||||||
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
|
type FnrsCnsltnRvwOpnnResponseDto,
|
||||||
import {
|
fnrsCnsltnRvwOpnnService,
|
||||||
RadioGroup,
|
} from "@/entities/fnrs-cnsltn";
|
||||||
RadioGroupItem,
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
Table,
|
import {
|
||||||
TableBody,
|
RadioGroup,
|
||||||
TableCell,
|
RadioGroupItem,
|
||||||
TableHead,
|
Table,
|
||||||
TableHeader,
|
TableCell,
|
||||||
TableRow,
|
TableHead,
|
||||||
Textarea,
|
TableHeader,
|
||||||
} from "@common/components";
|
TableRow,
|
||||||
|
Textarea,
|
||||||
import type {
|
} from "@common/components";
|
||||||
ReviewOpinionFormData,
|
|
||||||
ReviewOpinionGetFormDataResult,
|
import type {
|
||||||
} from "../../lib/review-opinion-utils";
|
ReviewOpinionFormData,
|
||||||
import type { ReviewStepCode } from "../../model/view-types";
|
ReviewOpinionGetFormDataResult,
|
||||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
} from "../../lib/review-opinion-utils";
|
||||||
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
|
|
||||||
interface TechReviewOpinionSectionProps {
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
|
||||||
editableReviewStep?: ReviewStepCode;
|
interface TechReviewOpinionSectionProps {
|
||||||
atrzSeComCdList?: string[];
|
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
||||||
isAtrzLoading?: boolean;
|
editableReviewStep?: ReviewStepCode;
|
||||||
}
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
export interface TechReviewOpinionSectionHandle {
|
}
|
||||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
|
||||||
resetForm: () => void;
|
export interface TechReviewOpinionSectionHandle {
|
||||||
}
|
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||||
|
resetForm: () => void;
|
||||||
interface TechReviewOpinionTableProps {
|
}
|
||||||
items: FnrsCnsltnRvwOpnnResponseDto[];
|
|
||||||
}
|
interface TechReviewOpinionTableProps {
|
||||||
|
items: FnrsCnsltnRvwOpnnResponseDto[];
|
||||||
interface UploadedOpinionFile {
|
}
|
||||||
fileName: string;
|
|
||||||
fileKey: string;
|
interface UploadedOpinionFile {
|
||||||
atflId?: string;
|
fileName: string;
|
||||||
atflCd?: string;
|
fileKey: string;
|
||||||
}
|
atflId?: string;
|
||||||
|
atflCd?: string;
|
||||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
}
|
||||||
rvwRsltCn: "",
|
|
||||||
rvwRsltAnmlyYn: "",
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
atchFileGroupId: "",
|
rvwRsltCn: "",
|
||||||
opnrSeComCd: "",
|
rvwRsltAnmlyYn: "",
|
||||||
};
|
atchFileGroupId: "",
|
||||||
|
opnrSeComCd: "",
|
||||||
function createRvwOpnnUploadKey(file: File) {
|
};
|
||||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
|
||||||
}
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
}
|
||||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
|
||||||
}
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
const TechReviewOpinionSection = forwardRef<
|
}
|
||||||
TechReviewOpinionSectionHandle,
|
|
||||||
TechReviewOpinionSectionProps
|
const TechReviewOpinionSection = forwardRef<
|
||||||
>(function TechReviewOpinionSection(
|
TechReviewOpinionSectionHandle,
|
||||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
TechReviewOpinionSectionProps
|
||||||
ref,
|
>(function TechReviewOpinionSection(
|
||||||
) {
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
ref,
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
) {
|
||||||
...EMPTY_FORM_DATA,
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
});
|
...EMPTY_FORM_DATA,
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
});
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
useEffect(() => {
|
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
useEffect(() => {
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
setFormData((prev) => ({
|
||||||
}));
|
...prev,
|
||||||
}, [editableReviewStep]);
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
}));
|
||||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
}, [editableReviewStep]);
|
||||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
|
||||||
if (!atflCd || !atflId) return;
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
if (!atflCd || !atflId) return;
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
|
||||||
},
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
onError: (_error, _file, fileKey) => {
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
},
|
||||||
},
|
onError: (_error, _file, fileKey) => {
|
||||||
});
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
|
},
|
||||||
useImperativeHandle(
|
});
|
||||||
ref,
|
|
||||||
() => ({
|
useImperativeHandle(
|
||||||
getFormData: () => {
|
ref,
|
||||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
() => ({
|
||||||
return null;
|
getFormData: () => {
|
||||||
}
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
|
return null;
|
||||||
return {
|
}
|
||||||
formData: {
|
|
||||||
...formData,
|
return {
|
||||||
opnrSeComCd: editableReviewStep,
|
formData: {
|
||||||
},
|
...formData,
|
||||||
rvwList,
|
opnrSeComCd: editableReviewStep,
|
||||||
};
|
},
|
||||||
},
|
rvwList,
|
||||||
resetForm: () => {
|
};
|
||||||
setFormData({
|
},
|
||||||
...EMPTY_FORM_DATA,
|
resetForm: () => {
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
setFormData({
|
||||||
});
|
...EMPTY_FORM_DATA,
|
||||||
setUploadedFile(null);
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
},
|
});
|
||||||
}),
|
setUploadedFile(null);
|
||||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
},
|
||||||
);
|
}),
|
||||||
|
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
);
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
|
||||||
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
|
||||||
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
if (!hasEnvSection && !hasKecoSection) {
|
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
||||||
return null;
|
|
||||||
}
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
|
return null;
|
||||||
async function handleFileUpload(files: FileList | null) {
|
}
|
||||||
if (!files || files.length === 0) return;
|
|
||||||
|
async function handleFileUpload(files: FileList | null) {
|
||||||
const file = files[0];
|
if (!files || files.length === 0) return;
|
||||||
const fileKey = createRvwOpnnUploadKey(file);
|
|
||||||
const existingAtflCd = formData.atchFileGroupId;
|
const file = files[0];
|
||||||
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
setUploadedFile({ fileName: file.name, fileKey });
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
|
|
||||||
try {
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
await uploadFileAsync(file, {
|
|
||||||
fileKey,
|
try {
|
||||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
await uploadFileAsync(file, {
|
||||||
});
|
fileKey,
|
||||||
} catch {
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
// onError callback handles cleanup
|
});
|
||||||
}
|
} catch {
|
||||||
}
|
// onError callback handles cleanup
|
||||||
|
}
|
||||||
async function handleFileRemove() {
|
}
|
||||||
if (!uploadedFile) return;
|
|
||||||
|
async function handleFileRemove() {
|
||||||
uploads[uploadedFile.fileKey]?.abort();
|
if (!uploadedFile) return;
|
||||||
|
|
||||||
if (uploadedFile.atflId) {
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
try {
|
|
||||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
if (uploadedFile.atflId) {
|
||||||
} catch (error) {
|
try {
|
||||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
}
|
} catch (error) {
|
||||||
}
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
|
}
|
||||||
setUploadedFile(null);
|
}
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
|
||||||
}
|
setUploadedFile(null);
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
}
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
|
||||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
|
if (editableReviewStep !== reviewStep) return false;
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||||
|
return true;
|
||||||
return (
|
}
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
|
||||||
<TableCell>
|
return (
|
||||||
<Textarea
|
<div className="space-y-2">
|
||||||
value={formData.rvwRsltCn}
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
onChange={(event) =>
|
<Table
|
||||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
}
|
variant="secondary"
|
||||||
placeholder="의견내용을 입력하세요"
|
>
|
||||||
rows={4}
|
<TableHeader>
|
||||||
className="min-h-[8rem] !text-xl"
|
<TableRow>
|
||||||
/>
|
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
</TableRow>
|
<Textarea
|
||||||
<TableRow>
|
value={formData.rvwRsltCn}
|
||||||
<TableHead>검토결과</TableHead>
|
onChange={(event) =>
|
||||||
<TableCell>
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
<FieldSet>
|
}
|
||||||
<RadioGroup
|
placeholder="의견내용을 입력하세요"
|
||||||
value={formData.rvwRsltAnmlyYn}
|
rows={4}
|
||||||
onValueChange={(value) =>
|
className="min-h-[8rem] !text-xl"
|
||||||
setFormData((prev) => ({
|
/>
|
||||||
...prev,
|
</TableCell>
|
||||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
</TableRow>
|
||||||
}))
|
<TableRow>
|
||||||
}
|
<TableHead>검토결과</TableHead>
|
||||||
>
|
<TableCell>
|
||||||
<div className="flex flex-wrap gap-4">
|
<FieldSet>
|
||||||
<FieldLabel
|
<RadioGroup
|
||||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
value={formData.rvwRsltAnmlyYn}
|
||||||
className="!w-auto border-none"
|
onValueChange={(value) =>
|
||||||
>
|
setFormData((prev) => ({
|
||||||
<Field orientation="horizontal" className="w-auto">
|
...prev,
|
||||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
<span className="font-normal">정상</span>
|
}))
|
||||||
</Field>
|
}
|
||||||
</FieldLabel>
|
>
|
||||||
<FieldLabel
|
<div className="flex flex-wrap gap-4">
|
||||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
<FieldLabel
|
||||||
className="!w-auto border-none"
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
>
|
className="!w-auto border-none"
|
||||||
<Field orientation="horizontal" className="w-auto">
|
>
|
||||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
<Field orientation="horizontal" className="w-auto">
|
||||||
<span className="font-normal">이상</span>
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
</Field>
|
<span className="font-normal">정상</span>
|
||||||
</FieldLabel>
|
</Field>
|
||||||
</div>
|
</FieldLabel>
|
||||||
</RadioGroup>
|
<FieldLabel
|
||||||
</FieldSet>
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
</TableCell>
|
className="!w-auto border-none"
|
||||||
</TableRow>
|
>
|
||||||
<TableRow>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
<TableHead>첨부파일</TableHead>
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
<TableCell>
|
<span className="font-normal">이상</span>
|
||||||
<AttachmentFileField
|
</Field>
|
||||||
files={
|
</FieldLabel>
|
||||||
uploadedFile
|
</div>
|
||||||
? [
|
</RadioGroup>
|
||||||
{
|
</FieldSet>
|
||||||
fileKey: uploadedFile.fileKey,
|
</TableCell>
|
||||||
fileName: uploadedFile.fileName,
|
</TableRow>
|
||||||
onDownload: uploadedFile.atflId
|
<TableRow>
|
||||||
? () => downloadByAtflId(uploadedFile.atflId!)
|
<TableHead>첨부파일</TableHead>
|
||||||
: undefined,
|
<TableCell>
|
||||||
},
|
<AttachmentFileField
|
||||||
]
|
files={
|
||||||
: []
|
uploadedFile
|
||||||
}
|
? [
|
||||||
inputId={`rvw-opnn-${reviewStep}-file-input`}
|
{
|
||||||
isUploadingByKey={isUploading}
|
fileKey: uploadedFile.fileKey,
|
||||||
multiple={false}
|
fileName: uploadedFile.fileName,
|
||||||
showAddButton={!uploadedFile}
|
onDownload: uploadedFile.atflId
|
||||||
addDisabled={uploading}
|
? () => downloadByAtflId(uploadedFile.atflId!)
|
||||||
onAdd={(fileList) => void handleFileUpload(fileList)}
|
: undefined,
|
||||||
onRemove={() => void handleFileRemove()}
|
},
|
||||||
/>
|
]
|
||||||
</TableCell>
|
: []
|
||||||
</TableRow>
|
}
|
||||||
</TableHeader>
|
inputId={`rvw-opnn-${reviewStep}-file-input`}
|
||||||
</Table>
|
isUploadingByKey={isUploading}
|
||||||
</div>
|
multiple={false}
|
||||||
);
|
showAddButton={!uploadedFile}
|
||||||
}
|
addDisabled={uploading}
|
||||||
|
onAdd={(fileList) => void handleFileUpload(fileList)}
|
||||||
return (
|
onRemove={() => void handleFileRemove()}
|
||||||
<>
|
/>
|
||||||
{hasEnvSection ? (
|
</TableCell>
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
</TableRow>
|
||||||
<div className="space-y-6">
|
</TableHeader>
|
||||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
</Table>
|
||||||
{renderOpinionForm("ENV")}
|
</div>
|
||||||
</div>
|
);
|
||||||
</SectionCollapsible>
|
}
|
||||||
) : null}
|
|
||||||
{hasKecoSection ? (
|
return (
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
<>
|
||||||
<div className="space-y-6">
|
{hasEnvSection ? (
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
{/*
|
<div className="space-y-6">
|
||||||
재원협의에서 환경공단 검토의견은 자동 생성
|
{envRvwList.length > 0 ? (
|
||||||
{renderOpinionForm("KECO")}
|
<TechReviewOpinionTable items={envRvwList} />
|
||||||
*/}
|
) : null}
|
||||||
</div>
|
{renderOpinionForm("ENV")}
|
||||||
</SectionCollapsible>
|
</div>
|
||||||
) : null}
|
</SectionCollapsible>
|
||||||
</>
|
) : null}
|
||||||
);
|
{hasKecoSection ? (
|
||||||
});
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
|
<div className="space-y-6">
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
{kecoRvwList.length > 0 ? (
|
||||||
return (
|
<TechReviewOpinionTable items={kecoRvwList} />
|
||||||
<div className="overflow-x-auto">
|
) : null}
|
||||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
{/*
|
||||||
<colgroup>
|
재원협의에서 환경공단 검토의견은 자동 생성
|
||||||
<col width="10%" />
|
{renderOpinionForm("KECO")}
|
||||||
<col width="55%" />
|
*/}
|
||||||
<col width="5%" />
|
</div>
|
||||||
<col width="20%" />
|
</SectionCollapsible>
|
||||||
<col width="10%" />
|
) : null}
|
||||||
</colgroup>
|
</>
|
||||||
<TableHeader>
|
);
|
||||||
<TableRow>
|
});
|
||||||
<TableHead>의견자명</TableHead>
|
|
||||||
<TableHead>의견내용</TableHead>
|
function getRvwOpnnIds(item: FnrsCnsltnRvwOpnnResponseDto) {
|
||||||
<TableHead>검토결과</TableHead>
|
if (!item.fnrsCnsltnDocNo || item.rvwOpnnSn == null) {
|
||||||
<TableHead>첨부파일</TableHead>
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
<TableHead>등록일시</TableHead>
|
}
|
||||||
</TableRow>
|
return [item.fnrsCnsltnDocNo, item.rvwOpnnSn] as const;
|
||||||
</TableHeader>
|
}
|
||||||
<TableBody>
|
|
||||||
{items.map((item) => (
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
<TableRow key={`${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}>
|
const queryClient = useQueryClient();
|
||||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
|
||||||
<TableCell align="center">
|
return (
|
||||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
<RvwOpnnListTable
|
||||||
</TableCell>
|
items={items}
|
||||||
<TableCell align="center">
|
getRowKey={(item) => `${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}
|
||||||
{item.atchFileInfo?.atflCd ? (
|
currentUserUuid={currentUserUuid}
|
||||||
<AttachmentDownloadLink
|
onUpdate={async (item, body) => {
|
||||||
atflCd={item.atchFileInfo.atflCd}
|
await fnrsCnsltnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||||
/>
|
}}
|
||||||
) : (
|
onDelete={async (item) => {
|
||||||
"-"
|
await fnrsCnsltnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
)}
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||||
</TableCell>
|
}}
|
||||||
<TableCell align="center">
|
/>
|
||||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
);
|
||||||
</TableCell>
|
}
|
||||||
</TableRow>
|
|
||||||
))}
|
export { TechReviewOpinionSection };
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { TechReviewOpinionSection };
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
|
|||||||
aply: "aply",
|
aply: "aply",
|
||||||
del: "del",
|
del: "del",
|
||||||
instlSncnAncmnt: "instlSncnAncmnt",
|
instlSncnAncmnt: "instlSncnAncmnt",
|
||||||
envoSbmsn: "envoSbmsn",
|
|
||||||
rjct: "rjct",
|
rjct: "rjct",
|
||||||
sncnAprv: "sncnAprv",
|
sncnAprv: "sncnAprv",
|
||||||
rvwRply: "rvwRply",
|
rvwRply: "rvwRply",
|
||||||
@@ -33,14 +32,16 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
|||||||
],
|
],
|
||||||
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
|
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
|
||||||
},
|
},
|
||||||
ROLE_USER04: {
|
// 환경청 단계 제거: 기존 환경청(LE·TP) 기능을 광역자치단체가 수행
|
||||||
ST: [INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn, INSTL_SNCN_VIEW_BUTTON_ACTION.rjct],
|
ROLE_USER04: {
|
||||||
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
|
ST: [
|
||||||
},
|
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply,
|
||||||
ROLE_USER03: {
|
INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw,
|
||||||
LE: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply, INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
INSTL_SNCN_VIEW_BUTTON_ACTION.rjct,
|
||||||
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
],
|
||||||
},
|
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
||||||
|
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||||
|
|||||||
@@ -55,16 +55,8 @@ export const instlSncnRegistSchema = z.object({
|
|||||||
tstmDesignAmt: z.number(),
|
tstmDesignAmt: z.number(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
cklstResultList: z.array(
|
// 설치인가 화면에서 체크리스트 미사용: 숨겨진 값 때문에 신청·저장 검증이 막히지 않도록 필수값을 두지 않음
|
||||||
instlSncnCklstResultItemSchema.extend({
|
cklstResultList: z.array(instlSncnCklstResultItemSchema),
|
||||||
cklstSn: z.number(),
|
|
||||||
cklstArtclSn: z.number(),
|
|
||||||
cklstRvwSn: z.number(),
|
|
||||||
rvwStpComCd: z.string(),
|
|
||||||
selected: z.enum(["Y", "N"]).nullish(),
|
|
||||||
picRvwCn: z.string().nullish(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
esntlAtchFileList: z.array(
|
esntlAtchFileList: z.array(
|
||||||
instlSncnEsntlAtchFileItemSchema.extend({
|
instlSncnEsntlAtchFileItemSchema.extend({
|
||||||
esntlAtchFileId: z.string(),
|
esntlAtchFileId: z.string(),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function getEditableReviewStep(
|
|||||||
dtlInstCd?: string,
|
dtlInstCd?: string,
|
||||||
prgrsSttsComCd?: string,
|
prgrsSttsComCd?: string,
|
||||||
): ReviewStepCode | undefined {
|
): ReviewStepCode | undefined {
|
||||||
if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) {
|
if (dtlInstCd === "ROLE_USER04" && (prgrsSttsComCd === "ST" || prgrsSttsComCd === "TP")) {
|
||||||
return "ENV";
|
return "ENV";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
|
|||||||
|
|
||||||
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
|
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
|
||||||
LOCAL: "자가체크",
|
LOCAL: "자가체크",
|
||||||
ENV: "환경청검토",
|
ENV: "광역자치단체검토",
|
||||||
KECO: "기술검토",
|
KECO: "기술검토",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
||||||
LOCAL: "지자체",
|
LOCAL: "지자체",
|
||||||
ENV: "환경청",
|
ENV: "광역자치단체",
|
||||||
KECO: "환경공단",
|
KECO: "환경공단",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export const INSTL_PROGRESS_ORDER = [
|
|||||||
|
|
||||||
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
||||||
ROLE_USER05: "LOCAL",
|
ROLE_USER05: "LOCAL",
|
||||||
ROLE_USER03: "ENV",
|
ROLE_USER04: "ENV",
|
||||||
ROLE_USER01: "KECO",
|
ROLE_USER01: "KECO",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import {
|
|||||||
type InstlSncnViewButtonAction,
|
type InstlSncnViewButtonAction,
|
||||||
needsRoleUser01AtrzResolve,
|
needsRoleUser01AtrzResolve,
|
||||||
} from "../../lib/button-config";
|
} from "../../lib/button-config";
|
||||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
|
||||||
import {
|
import {
|
||||||
buildSavePayload,
|
buildSavePayload,
|
||||||
buildSupplementReqPayload,
|
buildSupplementReqPayload,
|
||||||
@@ -179,11 +178,6 @@ function FloatingActionButtons({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleAplyClick() {
|
async function handleAplyClick() {
|
||||||
const { percent } = computeReviewStepChecklistProgress(
|
|
||||||
data.cklstResultList ?? [],
|
|
||||||
checklistFormState.results,
|
|
||||||
"LOCAL",
|
|
||||||
);
|
|
||||||
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
|
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
|
||||||
data,
|
data,
|
||||||
checklistFormState,
|
checklistFormState,
|
||||||
@@ -191,11 +185,6 @@ function FloatingActionButtons({
|
|||||||
"ST",
|
"ST",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (percent !== 100) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await executeAction({
|
await executeAction({
|
||||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
||||||
request: payload,
|
request: payload,
|
||||||
@@ -231,22 +220,6 @@ function FloatingActionButtons({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEnvoSbmsnClick() {
|
|
||||||
const payload: DocPrgrsUpdateRequestDto = {
|
|
||||||
docNo,
|
|
||||||
docSeComCd,
|
|
||||||
prgrsSttsComCd: "LE",
|
|
||||||
};
|
|
||||||
await executeAction({
|
|
||||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn,
|
|
||||||
request: payload,
|
|
||||||
successMessage: "제출되었습니다.",
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: instlSncnKeys.list(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRjctClick() {
|
async function handleRjctClick() {
|
||||||
const payload: DocPrgrsUpdateRequestDto = {
|
const payload: DocPrgrsUpdateRequestDto = {
|
||||||
docNo,
|
docNo,
|
||||||
@@ -284,16 +257,6 @@ function FloatingActionButtons({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRvwRplyClick() {
|
async function handleRvwRplyClick() {
|
||||||
const { percent } = computeReviewStepChecklistProgress(
|
|
||||||
data.cklstResultList ?? [],
|
|
||||||
checklistFormState.results,
|
|
||||||
"ENV",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (percent !== 100) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = buildTechReviewReplyPayload(
|
const payload = buildTechReviewReplyPayload(
|
||||||
data,
|
data,
|
||||||
@@ -369,16 +332,6 @@ function FloatingActionButtons({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRvwCmptnClick() {
|
async function handleRvwCmptnClick() {
|
||||||
const { percent } = computeReviewStepChecklistProgress(
|
|
||||||
data.cklstResultList ?? [],
|
|
||||||
checklistFormState.results,
|
|
||||||
"KECO",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (percent !== 100) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...buildTechReviewReplyPayload(
|
...buildTechReviewReplyPayload(
|
||||||
@@ -447,11 +400,6 @@ function FloatingActionButtons({
|
|||||||
설치인가 고시
|
설치인가 고시
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn) && (
|
|
||||||
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
|
|
||||||
환경청제출
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rjct) && (
|
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rjct) && (
|
||||||
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
|
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
|
||||||
반려
|
반려
|
||||||
@@ -618,7 +566,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
|||||||
aply: handleSaveApply,
|
aply: handleSaveApply,
|
||||||
del: handleDel,
|
del: handleDel,
|
||||||
instlSncnAncmnt: handleChangePrgrsSttsComCd,
|
instlSncnAncmnt: handleChangePrgrsSttsComCd,
|
||||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
|
||||||
rjct: handleChangePrgrsSttsComCd,
|
rjct: handleChangePrgrsSttsComCd,
|
||||||
sncnAprv: handleChangePrgrsSttsComCd,
|
sncnAprv: handleChangePrgrsSttsComCd,
|
||||||
rvwRply: handleTechReview,
|
rvwRply: handleTechReview,
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ import {
|
|||||||
AttachmentsFormSection,
|
AttachmentsFormSection,
|
||||||
BusinessCostFormSection,
|
BusinessCostFormSection,
|
||||||
BusinessScaleFormSection,
|
BusinessScaleFormSection,
|
||||||
ChecklistFormSection,
|
|
||||||
ContactInfoFormSection,
|
ContactInfoFormSection,
|
||||||
isFormChecklistComplete,
|
|
||||||
isFormEsntlAtchComplete,
|
isFormEsntlAtchComplete,
|
||||||
PrivacyConsentSection,
|
PrivacyConsentSection,
|
||||||
ProgressStatusSection,
|
ProgressStatusSection,
|
||||||
@@ -154,11 +152,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFormChecklistComplete(data.cklstResultList)) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||||
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||||
return;
|
return;
|
||||||
@@ -228,7 +221,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
|||||||
<PrivacyConsentSection />
|
<PrivacyConsentSection />
|
||||||
<BusinessScaleFormSection />
|
<BusinessScaleFormSection />
|
||||||
<BusinessCostFormSection />
|
<BusinessCostFormSection />
|
||||||
<ChecklistFormSection docSeComCd="INSTL" fieldPrefix="instlSncnMng" />
|
|
||||||
<AttachmentsFormSection
|
<AttachmentsFormSection
|
||||||
docSeComCd="INSTL"
|
docSeComCd="INSTL"
|
||||||
fieldPrefix="instlSncnMng"
|
fieldPrefix="instlSncnMng"
|
||||||
|
|||||||
@@ -1,149 +1,141 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { useUserStore } from "@/shared/lib";
|
import { useUserStore } from "@/shared/lib";
|
||||||
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||||
import {
|
import {
|
||||||
AttachmentsViewSection,
|
AttachmentsViewSection,
|
||||||
ProgressStatusSection,
|
ProgressStatusSection,
|
||||||
SupplementDemandSection,
|
SupplementDemandSection,
|
||||||
} from "@/widgets/pbadms-sprt";
|
} from "@/widgets/pbadms-sprt";
|
||||||
import { Skeleton } from "@common/components";
|
import { Skeleton } from "@common/components";
|
||||||
|
|
||||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||||
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
||||||
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
||||||
|
|
||||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||||
import { BusinessCostSection } from "./sections/business-cost-section";
|
import { BusinessCostSection } from "./sections/business-cost-section";
|
||||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||||
import { ChecklistSection } from "./sections/checklist-section";
|
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
import {
|
||||||
import {
|
TechReviewOpinionSection,
|
||||||
TechReviewOpinionSection,
|
type TechReviewOpinionSectionHandle,
|
||||||
type TechReviewOpinionSectionHandle,
|
} from "./sections/tech-review-opinion-section";
|
||||||
} from "./sections/tech-review-opinion-section";
|
|
||||||
|
interface InstlViewPageProps {
|
||||||
interface InstlViewPageProps {
|
id: string;
|
||||||
id: string;
|
}
|
||||||
}
|
|
||||||
|
function InstlViewPage({ id }: InstlViewPageProps) {
|
||||||
function InstlViewPage({ id }: InstlViewPageProps) {
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
const userInfo = useUserStore((state) => state.userInfo);
|
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||||
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
const { formState: checklistFormState } = useChecklistFormState(
|
||||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST,
|
||||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
);
|
||||||
|
|
||||||
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||||
{},
|
{},
|
||||||
{ enabled: needsAtrzResolve },
|
{ enabled: needsAtrzResolve },
|
||||||
);
|
);
|
||||||
const atrzSeComCdList = useMemo(
|
const atrzSeComCdList = useMemo(
|
||||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||||
[rvwLoginUserData],
|
[rvwLoginUserData],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Skeleton className="h-[4rem] rounded-xl" />
|
<Skeleton className="h-[4rem] rounded-xl" />
|
||||||
<Skeleton className="h-[16rem] rounded-xl" />
|
<Skeleton className="h-[16rem] rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Skeleton className="h-[4rem] rounded-xl" />
|
<Skeleton className="h-[4rem] rounded-xl" />
|
||||||
<Skeleton className="h-[16rem] rounded-xl" />
|
<Skeleton className="h-[16rem] rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Skeleton className="h-[4rem] rounded-xl" />
|
<Skeleton className="h-[4rem] rounded-xl" />
|
||||||
<Skeleton className="h-[32rem] rounded-xl" />
|
<Skeleton className="h-[32rem] rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !data) {
|
if (isError || !data) {
|
||||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
detDto,
|
detDto,
|
||||||
bizSclList,
|
bizSclList,
|
||||||
bizCstList,
|
bizCstList,
|
||||||
cklstResultList = [],
|
esntlFileList,
|
||||||
esntlFileList,
|
etcFileList,
|
||||||
etcFileList,
|
rvwList = [],
|
||||||
rvwList = [],
|
splmntDmndList = [],
|
||||||
splmntDmndList = [],
|
sttsChgHstryList = [],
|
||||||
sttsChgHstryList = [],
|
} = data;
|
||||||
} = data;
|
|
||||||
|
const editableReviewStep = getEditableReviewStep(
|
||||||
const editableReviewStep = getEditableReviewStep(
|
userInfo?.dtlInstCd,
|
||||||
userInfo?.dtlInstCd,
|
detDto.prgrsSttsComCd || "",
|
||||||
detDto.prgrsSttsComCd || "",
|
);
|
||||||
);
|
|
||||||
|
return (
|
||||||
return (
|
<div className="mt-4">
|
||||||
<div className="mt-4">
|
<SectionCollapsibleGroup>
|
||||||
<SectionCollapsibleGroup>
|
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
<SectionCollapsibleToggle />
|
||||||
<SectionCollapsibleToggle />
|
</div>
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<ProgressStatusSection
|
||||||
<ProgressStatusSection
|
columns={PROGRESS_STATUS_COLUMNS}
|
||||||
columns={PROGRESS_STATUS_COLUMNS}
|
sttsChgHstryList={sttsChgHstryList}
|
||||||
sttsChgHstryList={sttsChgHstryList}
|
elaprDocNo={detDto.instlSncnDocNo}
|
||||||
elaprDocNo={detDto.instlSncnDocNo}
|
/>
|
||||||
/>
|
<BasicInfoSection det={detDto} />
|
||||||
<BasicInfoSection det={detDto} />
|
<ContactInfoSection det={detDto} />
|
||||||
<ContactInfoSection det={detDto} />
|
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
||||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
<BusinessScaleSection bizSclList={bizSclList} />
|
||||||
<BusinessScaleSection bizSclList={bizSclList} />
|
<BusinessCostSection bizCstList={bizCstList} />
|
||||||
<BusinessCostSection bizCstList={bizCstList} />
|
<AttachmentsViewSection
|
||||||
<ChecklistSection
|
esntlFileList={esntlFileList ?? []}
|
||||||
cklstResultList={cklstResultList}
|
etcFileList={etcFileList ?? []}
|
||||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
/>
|
||||||
userRoleCode={userInfo?.dtlInstCd}
|
<TechReviewOpinionSection
|
||||||
formState={checklistFormState}
|
ref={techReviewOpinionRef}
|
||||||
setFormState={setChecklistFormState}
|
rvwList={rvwList}
|
||||||
/>
|
editableReviewStep={editableReviewStep}
|
||||||
<AttachmentsViewSection
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
esntlFileList={esntlFileList ?? []}
|
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
etcFileList={etcFileList ?? []}
|
/>
|
||||||
/>
|
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||||
<TechReviewOpinionSection
|
<FloatingActionButtons
|
||||||
ref={techReviewOpinionRef}
|
data={data}
|
||||||
rvwList={rvwList}
|
docSeComCd="INSTL"
|
||||||
editableReviewStep={editableReviewStep}
|
instlSncnDocNo={detDto.instlSncnDocNo}
|
||||||
atrzSeComCdList={atrzSeComCdList}
|
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||||
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
dtlInstCd={userInfo?.dtlInstCd}
|
||||||
/>
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
<FloatingActionButtons
|
checklistFormState={checklistFormState}
|
||||||
data={data}
|
techReviewOpinionRef={techReviewOpinionRef}
|
||||||
docSeComCd="INSTL"
|
opnrNm={userInfo?.userName}
|
||||||
instlSncnDocNo={detDto.instlSncnDocNo}
|
/>
|
||||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
</div>
|
||||||
dtlInstCd={userInfo?.dtlInstCd}
|
</SectionCollapsibleGroup>
|
||||||
atrzSeComCdList={atrzSeComCdList}
|
</div>
|
||||||
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
);
|
||||||
checklistFormState={checklistFormState}
|
}
|
||||||
techReviewOpinionRef={techReviewOpinionRef}
|
|
||||||
opnrNm={userInfo?.userName}
|
export { InstlViewPage };
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SectionCollapsibleGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { InstlViewPage };
|
|
||||||
|
|||||||
@@ -1,350 +1,350 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { TextShimmer } from "@/shared/ui";
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import {
|
import { attachService } from "@/entities/attach";
|
||||||
Button,
|
import {
|
||||||
RadioGroup,
|
instlSncnKeys,
|
||||||
RadioGroupItem,
|
type InstlSncnRvwOpnnResponseDto,
|
||||||
Table,
|
instlSncnRvwOpnnService,
|
||||||
TableBody,
|
type ReviewStepCode,
|
||||||
TableCell,
|
} from "@/entities/instl-sncn";
|
||||||
TableHead,
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
TableHeader,
|
import {
|
||||||
TableRow,
|
Button,
|
||||||
Textarea,
|
RadioGroup,
|
||||||
} from "@common/components";
|
RadioGroupItem,
|
||||||
|
Table,
|
||||||
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
|
TableCell,
|
||||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
TableRow,
|
||||||
|
Textarea,
|
||||||
interface TechReviewOpinionSectionProps {
|
} from "@common/components";
|
||||||
rvwList: InstlSncnRvwOpnnResponseDto[];
|
|
||||||
editableReviewStep?: ReviewStepCode;
|
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
|
||||||
atrzSeComCdList?: string[];
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
isAtrzLoading?: boolean;
|
|
||||||
}
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
|
|
||||||
export interface TechReviewOpinionSectionHandle {
|
interface TechReviewOpinionSectionProps {
|
||||||
getFormData: () => ReviewOpinionFormData | null;
|
rvwList: InstlSncnRvwOpnnResponseDto[];
|
||||||
resetForm: () => void;
|
editableReviewStep?: ReviewStepCode;
|
||||||
}
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
interface TechReviewOpinionTableProps {
|
}
|
||||||
items: InstlSncnRvwOpnnResponseDto[];
|
|
||||||
}
|
export interface TechReviewOpinionSectionHandle {
|
||||||
|
getFormData: () => ReviewOpinionFormData | null;
|
||||||
interface UploadedOpinionFile {
|
resetForm: () => void;
|
||||||
fileName: string;
|
}
|
||||||
fileKey: string;
|
|
||||||
atflId?: string;
|
interface TechReviewOpinionTableProps {
|
||||||
atflCd?: string;
|
items: InstlSncnRvwOpnnResponseDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
interface UploadedOpinionFile {
|
||||||
rvwRsltCn: "",
|
fileName: string;
|
||||||
rvwRsltAnmlyYn: "",
|
fileKey: string;
|
||||||
atchFileGroupId: "",
|
atflId?: string;
|
||||||
opnrSeComCd: "",
|
atflCd?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
function createRvwOpnnUploadKey(file: File) {
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
rvwRsltCn: "",
|
||||||
}
|
rvwRsltAnmlyYn: "",
|
||||||
|
atchFileGroupId: "",
|
||||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
opnrSeComCd: "",
|
||||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
};
|
||||||
}
|
|
||||||
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
const TechReviewOpinionSection = forwardRef<
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
TechReviewOpinionSectionHandle,
|
}
|
||||||
TechReviewOpinionSectionProps
|
|
||||||
>(function TechReviewOpinionSection(
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
ref,
|
}
|
||||||
) {
|
|
||||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
const TechReviewOpinionSection = forwardRef<
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
TechReviewOpinionSectionHandle,
|
||||||
...EMPTY_FORM_DATA,
|
TechReviewOpinionSectionProps
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
>(function TechReviewOpinionSection(
|
||||||
});
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
ref,
|
||||||
|
) {
|
||||||
useEffect(() => {
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
setFormData((prev) => ({
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
...prev,
|
...EMPTY_FORM_DATA,
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
}));
|
});
|
||||||
}, [editableReviewStep]);
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
|
|
||||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
useEffect(() => {
|
||||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
setFormData((prev) => ({
|
||||||
if (!atflCd || !atflId) return;
|
...prev,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
}));
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
}, [editableReviewStep]);
|
||||||
},
|
|
||||||
onError: (_error, _file, fileKey) => {
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
},
|
if (!atflCd || !atflId) return;
|
||||||
});
|
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
useImperativeHandle(
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
ref,
|
},
|
||||||
() => ({
|
onError: (_error, _file, fileKey) => {
|
||||||
getFormData: () => {
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
},
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
useImperativeHandle(
|
||||||
return null;
|
ref,
|
||||||
}
|
() => ({
|
||||||
|
getFormData: () => {
|
||||||
return {
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
...formData,
|
return null;
|
||||||
opnrSeComCd: editableReviewStep,
|
}
|
||||||
};
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
},
|
return null;
|
||||||
resetForm: () => {
|
}
|
||||||
setFormData({
|
|
||||||
...EMPTY_FORM_DATA,
|
return {
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
...formData,
|
||||||
});
|
opnrSeComCd: editableReviewStep,
|
||||||
setUploadedFile(null);
|
};
|
||||||
},
|
},
|
||||||
}),
|
resetForm: () => {
|
||||||
[canWriteKecoOpinion, editableReviewStep, formData],
|
setFormData({
|
||||||
);
|
...EMPTY_FORM_DATA,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
});
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
setUploadedFile(null);
|
||||||
|
},
|
||||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
}),
|
||||||
const hasKecoSection =
|
[canWriteKecoOpinion, editableReviewStep, formData],
|
||||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
);
|
||||||
|
|
||||||
if (!hasEnvSection && !hasKecoSection) {
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
return null;
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
}
|
|
||||||
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
async function handleFileUpload(files: FileList | null) {
|
const hasKecoSection =
|
||||||
if (!files || files.length === 0) return;
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||||
|
|
||||||
const file = files[0];
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
const fileKey = createRvwOpnnUploadKey(file);
|
return null;
|
||||||
const existingAtflCd = formData.atchFileGroupId;
|
}
|
||||||
|
|
||||||
setUploadedFile({ fileName: file.name, fileKey });
|
async function handleFileUpload(files: FileList | null) {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
try {
|
|
||||||
await uploadFileAsync(file, {
|
const file = files[0];
|
||||||
fileKey,
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
});
|
|
||||||
} catch {
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
// onError callback handles cleanup
|
|
||||||
}
|
try {
|
||||||
}
|
await uploadFileAsync(file, {
|
||||||
|
fileKey,
|
||||||
async function handleFileRemove() {
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
if (!uploadedFile) return;
|
});
|
||||||
|
} catch {
|
||||||
uploads[uploadedFile.fileKey]?.abort();
|
// onError callback handles cleanup
|
||||||
|
}
|
||||||
if (uploadedFile.atflId) {
|
}
|
||||||
try {
|
|
||||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
async function handleFileRemove() {
|
||||||
} catch (error) {
|
if (!uploadedFile) return;
|
||||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
|
||||||
}
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
}
|
|
||||||
|
if (uploadedFile.atflId) {
|
||||||
setUploadedFile(null);
|
try {
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
}
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
}
|
||||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
|
||||||
|
setUploadedFile(null);
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
|
}
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
if (editableReviewStep !== reviewStep) return false;
|
||||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||||
<TableHeader>
|
return true;
|
||||||
<TableRow>
|
}
|
||||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
|
||||||
<TableCell>
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
<Textarea
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
value={formData.rvwRsltCn}
|
|
||||||
onChange={(event) =>
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
|
||||||
}
|
return (
|
||||||
placeholder="의견내용을 입력하세요"
|
<div className="space-y-2">
|
||||||
rows={4}
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
className="min-h-[8rem] !text-xl"
|
<Table
|
||||||
/>
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
</TableCell>
|
variant="secondary"
|
||||||
</TableRow>
|
>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead scope="row">검토결과</TableHead>
|
<TableRow>
|
||||||
<TableCell>
|
<TableHead scope="row" className="w-[12rem]">
|
||||||
<FieldSet>
|
의견내용
|
||||||
<RadioGroup
|
</TableHead>
|
||||||
value={formData.rvwRsltAnmlyYn}
|
<TableCell>
|
||||||
onValueChange={(value) =>
|
<Textarea
|
||||||
setFormData((prev) => ({
|
value={formData.rvwRsltCn}
|
||||||
...prev,
|
onChange={(event) =>
|
||||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
}))
|
}
|
||||||
}
|
placeholder="의견내용을 입력하세요"
|
||||||
>
|
rows={4}
|
||||||
<div className="flex flex-wrap gap-4">
|
className="min-h-[8rem] !text-xl"
|
||||||
<FieldLabel
|
/>
|
||||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
</TableCell>
|
||||||
className="!w-auto border-none"
|
</TableRow>
|
||||||
>
|
<TableRow>
|
||||||
<Field orientation="horizontal" className="w-auto">
|
<TableHead scope="row">검토결과</TableHead>
|
||||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
<TableCell>
|
||||||
<span className="font-normal">정상</span>
|
<FieldSet>
|
||||||
</Field>
|
<RadioGroup
|
||||||
</FieldLabel>
|
value={formData.rvwRsltAnmlyYn}
|
||||||
<FieldLabel
|
onValueChange={(value) =>
|
||||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
setFormData((prev) => ({
|
||||||
className="!w-auto border-none"
|
...prev,
|
||||||
>
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
<Field orientation="horizontal" className="w-auto">
|
}))
|
||||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
}
|
||||||
<span className="font-normal">이상</span>
|
>
|
||||||
</Field>
|
<div className="flex flex-wrap gap-4">
|
||||||
</FieldLabel>
|
<FieldLabel
|
||||||
</div>
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
</RadioGroup>
|
className="!w-auto border-none"
|
||||||
</FieldSet>
|
>
|
||||||
</TableCell>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
</TableRow>
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
<TableRow>
|
<span className="font-normal">정상</span>
|
||||||
<TableHead scope="row">첨부파일</TableHead>
|
</Field>
|
||||||
<TableCell>
|
</FieldLabel>
|
||||||
{uploadedFile ? (
|
<FieldLabel
|
||||||
<div className="flex items-center gap-2">
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
<TextShimmer className="text-xl" active={uploading}>
|
className="!w-auto border-none"
|
||||||
{uploadedFile.fileName}
|
>
|
||||||
</TextShimmer>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
<Button
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
size="icon"
|
<span className="font-normal">이상</span>
|
||||||
onClick={(e) => {
|
</Field>
|
||||||
e.preventDefault();
|
</FieldLabel>
|
||||||
void handleFileRemove();
|
</div>
|
||||||
}}
|
</RadioGroup>
|
||||||
disabled={uploading}
|
</FieldSet>
|
||||||
className="size-8 !bg-destructive"
|
</TableCell>
|
||||||
>
|
</TableRow>
|
||||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
<TableRow>
|
||||||
</Button>
|
<TableHead scope="row">첨부파일</TableHead>
|
||||||
</div>
|
<TableCell>
|
||||||
) : (
|
{uploadedFile ? (
|
||||||
<label>
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
type="file"
|
{uploadedFile.fileName}
|
||||||
className="hidden"
|
</TextShimmer>
|
||||||
onChange={(event) => {
|
<Button
|
||||||
void handleFileUpload(event.target.files);
|
size="icon"
|
||||||
event.target.value = "";
|
onClick={(e) => {
|
||||||
}}
|
e.preventDefault();
|
||||||
/>
|
void handleFileRemove();
|
||||||
<Button size="sm" variant="outline" type="button" asChild>
|
}}
|
||||||
<span>파일 선택</span>
|
disabled={uploading}
|
||||||
</Button>
|
className="size-8 !bg-destructive"
|
||||||
</label>
|
>
|
||||||
)}
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
</TableCell>
|
</Button>
|
||||||
</TableRow>
|
</div>
|
||||||
</TableHeader>
|
) : (
|
||||||
</Table>
|
<label>
|
||||||
</div>
|
<input
|
||||||
);
|
type="file"
|
||||||
}
|
className="hidden"
|
||||||
|
onChange={(event) => {
|
||||||
return (
|
void handleFileUpload(event.target.files);
|
||||||
<>
|
event.target.value = "";
|
||||||
{hasEnvSection ? (
|
}}
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
/>
|
||||||
<div className="space-y-6">
|
<Button size="sm" variant="outline" type="button" asChild>
|
||||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
<span>파일 선택</span>
|
||||||
{renderOpinionForm("ENV")}
|
</Button>
|
||||||
</div>
|
</label>
|
||||||
</SectionCollapsible>
|
)}
|
||||||
) : null}
|
</TableCell>
|
||||||
{hasKecoSection ? (
|
</TableRow>
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
</TableHeader>
|
||||||
<div className="space-y-6">
|
</Table>
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
</div>
|
||||||
{renderOpinionForm("KECO")}
|
);
|
||||||
</div>
|
}
|
||||||
</SectionCollapsible>
|
|
||||||
) : null}
|
return (
|
||||||
</>
|
<>
|
||||||
);
|
{hasEnvSection ? (
|
||||||
});
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
|
<div className="space-y-6">
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
{envRvwList.length > 0 ? (
|
||||||
return (
|
<TechReviewOpinionTable items={envRvwList} />
|
||||||
<div className="overflow-x-auto">
|
) : null}
|
||||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
{renderOpinionForm("ENV")}
|
||||||
<colgroup>
|
</div>
|
||||||
<col width="10%" />
|
</SectionCollapsible>
|
||||||
<col width="55%" />
|
) : null}
|
||||||
<col width="5%" />
|
{hasKecoSection ? (
|
||||||
<col width="20%" />
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
<col width="10%" />
|
<div className="space-y-6">
|
||||||
</colgroup>
|
{kecoRvwList.length > 0 ? (
|
||||||
<TableHeader>
|
<TechReviewOpinionTable items={kecoRvwList} />
|
||||||
<TableRow>
|
) : null}
|
||||||
<TableHead>의견자명</TableHead>
|
{renderOpinionForm("KECO")}
|
||||||
<TableHead>의견내용</TableHead>
|
</div>
|
||||||
<TableHead>검토결과</TableHead>
|
</SectionCollapsible>
|
||||||
<TableHead>첨부파일</TableHead>
|
) : null}
|
||||||
<TableHead>등록일시</TableHead>
|
</>
|
||||||
</TableRow>
|
);
|
||||||
</TableHeader>
|
});
|
||||||
<TableBody>
|
|
||||||
{items.map((item) => (
|
function getRvwOpnnIds(item: InstlSncnRvwOpnnResponseDto) {
|
||||||
<TableRow key={`${item.instlSncnDocNo}_${item.rvwOpnnSn}`}>
|
if (!item.instlSncnDocNo || item.rvwOpnnSn == null) {
|
||||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
}
|
||||||
<TableCell align="center">
|
return [item.instlSncnDocNo, item.rvwOpnnSn] as const;
|
||||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
}
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
{item.atchFileInfo?.atflCd ? (
|
const queryClient = useQueryClient();
|
||||||
<AttachmentDownloadLink
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
atflCd={item.atchFileInfo.atflCd}
|
|
||||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
return (
|
||||||
/>
|
<RvwOpnnListTable
|
||||||
) : (
|
items={items}
|
||||||
"-"
|
getRowKey={(item) => `${item.instlSncnDocNo}_${item.rvwOpnnSn}`}
|
||||||
)}
|
currentUserUuid={currentUserUuid}
|
||||||
</TableCell>
|
onUpdate={async (item, body) => {
|
||||||
<TableCell align="center">
|
await instlSncnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||||
</TableCell>
|
}}
|
||||||
</TableRow>
|
onDelete={async (item) => {
|
||||||
))}
|
await instlSncnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
</TableBody>
|
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||||
</Table>
|
}}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { TechReviewOpinionSection };
|
export { TechReviewOpinionSection };
|
||||||
|
|||||||
@@ -1,357 +1,354 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { TextShimmer } from "@/shared/ui";
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { SewerBscPlanRvwOpnnResponseDto } from "@/entities/sewer-bsc-plan";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import {
|
import { attachService } from "@/entities/attach";
|
||||||
Button,
|
import {
|
||||||
RadioGroup,
|
sewerBscPlanKeys,
|
||||||
RadioGroupItem,
|
type SewerBscPlanRvwOpnnResponseDto,
|
||||||
Table,
|
sewerBscPlanRvwOpnnService,
|
||||||
TableBody,
|
} from "@/entities/sewer-bsc-plan";
|
||||||
TableCell,
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
TableHead,
|
import {
|
||||||
TableHeader,
|
Button,
|
||||||
TableRow,
|
RadioGroup,
|
||||||
Textarea,
|
RadioGroupItem,
|
||||||
} from "@common/components";
|
Table,
|
||||||
|
TableCell,
|
||||||
import type {
|
TableHead,
|
||||||
ReviewOpinionFormData,
|
TableHeader,
|
||||||
ReviewOpinionGetFormDataResult,
|
TableRow,
|
||||||
} from "../../lib/review-opinion-utils";
|
Textarea,
|
||||||
import type { ReviewStepCode } from "../../model/view-types";
|
} from "@common/components";
|
||||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
|
||||||
|
import type {
|
||||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
ReviewOpinionFormData,
|
||||||
|
ReviewOpinionGetFormDataResult,
|
||||||
interface TechReviewOpinionSectionProps {
|
} from "../../lib/review-opinion-utils";
|
||||||
rvwList: SewerBscPlanRvwOpnnResponseDto[];
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
editableReviewStep?: ReviewStepCode;
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
atrzSeComCdList?: string[];
|
|
||||||
isAtrzLoading?: boolean;
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
}
|
|
||||||
|
interface TechReviewOpinionSectionProps {
|
||||||
export interface TechReviewOpinionSectionHandle {
|
rvwList: SewerBscPlanRvwOpnnResponseDto[];
|
||||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
editableReviewStep?: ReviewStepCode;
|
||||||
resetForm: () => void;
|
atrzSeComCdList?: string[];
|
||||||
}
|
isAtrzLoading?: boolean;
|
||||||
|
}
|
||||||
interface TechReviewOpinionTableProps {
|
|
||||||
items: SewerBscPlanRvwOpnnResponseDto[];
|
export interface TechReviewOpinionSectionHandle {
|
||||||
}
|
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||||
|
resetForm: () => void;
|
||||||
interface UploadedOpinionFile {
|
}
|
||||||
fileName: string;
|
|
||||||
fileKey: string;
|
interface TechReviewOpinionTableProps {
|
||||||
atflId?: string;
|
items: SewerBscPlanRvwOpnnResponseDto[];
|
||||||
atflCd?: string;
|
}
|
||||||
}
|
|
||||||
|
interface UploadedOpinionFile {
|
||||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
fileName: string;
|
||||||
rvwRsltCn: "",
|
fileKey: string;
|
||||||
rvwRsltAnmlyYn: "",
|
atflId?: string;
|
||||||
atchFileGroupId: "",
|
atflCd?: string;
|
||||||
opnrSeComCd: "",
|
}
|
||||||
};
|
|
||||||
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
function createRvwOpnnUploadKey(file: File) {
|
rvwRsltCn: "",
|
||||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
rvwRsltAnmlyYn: "",
|
||||||
}
|
atchFileGroupId: "",
|
||||||
|
opnrSeComCd: "",
|
||||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
};
|
||||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
|
||||||
}
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
const TechReviewOpinionSection = forwardRef<
|
}
|
||||||
TechReviewOpinionSectionHandle,
|
|
||||||
TechReviewOpinionSectionProps
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
>(function TechReviewOpinionSection(
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
}
|
||||||
ref,
|
|
||||||
) {
|
const TechReviewOpinionSection = forwardRef<
|
||||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
TechReviewOpinionSectionHandle,
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
TechReviewOpinionSectionProps
|
||||||
...EMPTY_FORM_DATA,
|
>(function TechReviewOpinionSection(
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
});
|
ref,
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
) {
|
||||||
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
useEffect(() => {
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
setFormData((prev) => ({
|
...EMPTY_FORM_DATA,
|
||||||
...prev,
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
});
|
||||||
}));
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
}, [editableReviewStep]);
|
|
||||||
|
useEffect(() => {
|
||||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
setFormData((prev) => ({
|
||||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
...prev,
|
||||||
if (!atflCd || !atflId) return;
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
}));
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
}, [editableReviewStep]);
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
|
||||||
},
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
onError: (_error, _file, fileKey) => {
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
if (!atflCd || !atflId) return;
|
||||||
},
|
|
||||||
});
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
useImperativeHandle(
|
},
|
||||||
ref,
|
onError: (_error, _file, fileKey) => {
|
||||||
() => ({
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
getFormData: () => {
|
},
|
||||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
});
|
||||||
return null;
|
|
||||||
}
|
useImperativeHandle(
|
||||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
ref,
|
||||||
return null;
|
() => ({
|
||||||
}
|
getFormData: () => {
|
||||||
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
return {
|
return null;
|
||||||
formData: {
|
}
|
||||||
...formData,
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
opnrSeComCd: editableReviewStep,
|
return null;
|
||||||
},
|
}
|
||||||
rvwList,
|
|
||||||
};
|
return {
|
||||||
},
|
formData: {
|
||||||
resetForm: () => {
|
...formData,
|
||||||
setFormData({
|
opnrSeComCd: editableReviewStep,
|
||||||
...EMPTY_FORM_DATA,
|
},
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
rvwList,
|
||||||
});
|
};
|
||||||
setUploadedFile(null);
|
},
|
||||||
},
|
resetForm: () => {
|
||||||
}),
|
setFormData({
|
||||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
...EMPTY_FORM_DATA,
|
||||||
);
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
});
|
||||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
setUploadedFile(null);
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
},
|
||||||
|
}),
|
||||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||||
const hasKecoSection =
|
);
|
||||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
|
||||||
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
if (!hasEnvSection && !hasKecoSection) {
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
return null;
|
|
||||||
}
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
|
const hasKecoSection =
|
||||||
async function handleFileUpload(files: FileList | null) {
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||||
if (!files || files.length === 0) return;
|
|
||||||
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
const file = files[0];
|
return null;
|
||||||
const fileKey = createRvwOpnnUploadKey(file);
|
}
|
||||||
const existingAtflCd = formData.atchFileGroupId;
|
|
||||||
|
async function handleFileUpload(files: FileList | null) {
|
||||||
setUploadedFile({ fileName: file.name, fileKey });
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
try {
|
const file = files[0];
|
||||||
await uploadFileAsync(file, {
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
fileKey,
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
|
||||||
});
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
} catch {
|
|
||||||
// onError callback handles cleanup
|
try {
|
||||||
}
|
await uploadFileAsync(file, {
|
||||||
}
|
fileKey,
|
||||||
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
async function handleFileRemove() {
|
});
|
||||||
if (!uploadedFile) return;
|
} catch {
|
||||||
|
// onError callback handles cleanup
|
||||||
uploads[uploadedFile.fileKey]?.abort();
|
}
|
||||||
|
}
|
||||||
if (uploadedFile.atflId) {
|
|
||||||
try {
|
async function handleFileRemove() {
|
||||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
if (!uploadedFile) return;
|
||||||
} catch (error) {
|
|
||||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
}
|
|
||||||
}
|
if (uploadedFile.atflId) {
|
||||||
|
try {
|
||||||
setUploadedFile(null);
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
} catch (error) {
|
||||||
}
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
|
}
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
}
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
|
||||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
setUploadedFile(null);
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
}
|
||||||
|
|
||||||
return (
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
<div className="space-y-2">
|
if (editableReviewStep !== reviewStep) return false;
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
return true;
|
||||||
<TableHeader>
|
}
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
<TableCell>
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
<Textarea
|
|
||||||
value={formData.rvwRsltCn}
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
onChange={(event) =>
|
|
||||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
return (
|
||||||
}
|
<div className="space-y-2">
|
||||||
placeholder="의견내용을 입력하세요"
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
rows={4}
|
<Table
|
||||||
className="min-h-[8rem] !text-xl"
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
/>
|
variant="secondary"
|
||||||
</TableCell>
|
>
|
||||||
</TableRow>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>검토결과</TableHead>
|
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<FieldSet>
|
<Textarea
|
||||||
<RadioGroup
|
value={formData.rvwRsltCn}
|
||||||
value={formData.rvwRsltAnmlyYn}
|
onChange={(event) =>
|
||||||
onValueChange={(value) =>
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
setFormData((prev) => ({
|
}
|
||||||
...prev,
|
placeholder="의견내용을 입력하세요"
|
||||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
rows={4}
|
||||||
}))
|
className="min-h-[8rem] !text-xl"
|
||||||
}
|
/>
|
||||||
>
|
</TableCell>
|
||||||
<div className="flex flex-wrap gap-4">
|
</TableRow>
|
||||||
<FieldLabel
|
<TableRow>
|
||||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
<TableHead>검토결과</TableHead>
|
||||||
className="!w-auto border-none"
|
<TableCell>
|
||||||
>
|
<FieldSet>
|
||||||
<Field orientation="horizontal" className="w-auto">
|
<RadioGroup
|
||||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
value={formData.rvwRsltAnmlyYn}
|
||||||
<span className="font-normal">정상</span>
|
onValueChange={(value) =>
|
||||||
</Field>
|
setFormData((prev) => ({
|
||||||
</FieldLabel>
|
...prev,
|
||||||
<FieldLabel
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
}))
|
||||||
className="!w-auto border-none"
|
}
|
||||||
>
|
>
|
||||||
<Field orientation="horizontal" className="w-auto">
|
<div className="flex flex-wrap gap-4">
|
||||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
<FieldLabel
|
||||||
<span className="font-normal">이상</span>
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
</Field>
|
className="!w-auto border-none"
|
||||||
</FieldLabel>
|
>
|
||||||
</div>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
</RadioGroup>
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
</FieldSet>
|
<span className="font-normal">정상</span>
|
||||||
</TableCell>
|
</Field>
|
||||||
</TableRow>
|
</FieldLabel>
|
||||||
<TableRow>
|
<FieldLabel
|
||||||
<TableHead>첨부파일</TableHead>
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
<TableCell>
|
className="!w-auto border-none"
|
||||||
{uploadedFile ? (
|
>
|
||||||
<div className="flex items-center gap-2">
|
<Field orientation="horizontal" className="w-auto">
|
||||||
<TextShimmer className="text-xl" active={uploading}>
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
{uploadedFile.fileName}
|
<span className="font-normal">이상</span>
|
||||||
</TextShimmer>
|
</Field>
|
||||||
<Button
|
</FieldLabel>
|
||||||
size="icon"
|
</div>
|
||||||
onClick={(e) => {
|
</RadioGroup>
|
||||||
e.preventDefault();
|
</FieldSet>
|
||||||
void handleFileRemove();
|
</TableCell>
|
||||||
}}
|
</TableRow>
|
||||||
disabled={uploading}
|
<TableRow>
|
||||||
className="size-8 !bg-destructive"
|
<TableHead>첨부파일</TableHead>
|
||||||
>
|
<TableCell>
|
||||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
{uploadedFile ? (
|
||||||
</Button>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
) : (
|
{uploadedFile.fileName}
|
||||||
<label>
|
</TextShimmer>
|
||||||
<input
|
<Button
|
||||||
type="file"
|
size="icon"
|
||||||
className="hidden"
|
onClick={(e) => {
|
||||||
onChange={(event) => {
|
e.preventDefault();
|
||||||
void handleFileUpload(event.target.files);
|
void handleFileRemove();
|
||||||
event.target.value = "";
|
}}
|
||||||
}}
|
disabled={uploading}
|
||||||
/>
|
className="size-8 !bg-destructive"
|
||||||
<Button size="sm" variant="outline" type="button" asChild>
|
>
|
||||||
<span>파일 선택</span>
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
</Button>
|
</Button>
|
||||||
</label>
|
</div>
|
||||||
)}
|
) : (
|
||||||
</TableCell>
|
<label>
|
||||||
</TableRow>
|
<input
|
||||||
</TableHeader>
|
type="file"
|
||||||
</Table>
|
className="hidden"
|
||||||
</div>
|
onChange={(event) => {
|
||||||
);
|
void handleFileUpload(event.target.files);
|
||||||
}
|
event.target.value = "";
|
||||||
|
}}
|
||||||
return (
|
/>
|
||||||
<>
|
<Button size="sm" variant="outline" type="button" asChild>
|
||||||
{hasEnvSection ? (
|
<span>파일 선택</span>
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
</Button>
|
||||||
<div className="space-y-6">
|
</label>
|
||||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
)}
|
||||||
{renderOpinionForm("ENV")}
|
</TableCell>
|
||||||
</div>
|
</TableRow>
|
||||||
</SectionCollapsible>
|
</TableHeader>
|
||||||
) : null}
|
</Table>
|
||||||
{hasKecoSection ? (
|
</div>
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
);
|
||||||
<div className="space-y-6">
|
}
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
|
||||||
{renderOpinionForm("KECO")}
|
return (
|
||||||
</div>
|
<>
|
||||||
</SectionCollapsible>
|
{hasEnvSection ? (
|
||||||
) : null}
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
</>
|
<div className="space-y-6">
|
||||||
);
|
{envRvwList.length > 0 ? (
|
||||||
});
|
<TechReviewOpinionTable items={envRvwList} />
|
||||||
|
) : null}
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
{renderOpinionForm("ENV")}
|
||||||
return (
|
</div>
|
||||||
<div className="overflow-x-auto">
|
</SectionCollapsible>
|
||||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
) : null}
|
||||||
<colgroup>
|
{hasKecoSection ? (
|
||||||
<col width="10%" />
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
<col width="55%" />
|
<div className="space-y-6">
|
||||||
<col width="5%" />
|
{kecoRvwList.length > 0 ? (
|
||||||
<col width="20%" />
|
<TechReviewOpinionTable items={kecoRvwList} />
|
||||||
<col width="10%" />
|
) : null}
|
||||||
</colgroup>
|
{renderOpinionForm("KECO")}
|
||||||
<TableHeader>
|
</div>
|
||||||
<TableRow>
|
</SectionCollapsible>
|
||||||
<TableHead>의견자명</TableHead>
|
) : null}
|
||||||
<TableHead>의견내용</TableHead>
|
</>
|
||||||
<TableHead>검토결과</TableHead>
|
);
|
||||||
<TableHead>첨부파일</TableHead>
|
});
|
||||||
<TableHead>등록일시</TableHead>
|
|
||||||
</TableRow>
|
function getRvwOpnnIds(item: SewerBscPlanRvwOpnnResponseDto) {
|
||||||
</TableHeader>
|
if (!item.sewerBscPlanDocNo || item.chgCycl == null || item.rvwOpnnSn == null) {
|
||||||
<TableBody>
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
{items.map((item) => (
|
}
|
||||||
<TableRow key={`${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}>
|
return [item.sewerBscPlanDocNo, item.chgCycl, item.rvwOpnnSn] as const;
|
||||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
}
|
||||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
|
||||||
<TableCell align="center">
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
const queryClient = useQueryClient();
|
||||||
</TableCell>
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
<TableCell align="center">
|
|
||||||
{item.atchFileInfo?.atflCd ? (
|
return (
|
||||||
<AttachmentDownloadLink
|
<RvwOpnnListTable
|
||||||
atflCd={item.atchFileInfo.atflCd}
|
items={items}
|
||||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
getRowKey={(item) => `${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}
|
||||||
/>
|
currentUserUuid={currentUserUuid}
|
||||||
) : (
|
onUpdate={async (item, body) => {
|
||||||
"-"
|
await sewerBscPlanRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
)}
|
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||||
</TableCell>
|
}}
|
||||||
<TableCell align="center">
|
onDelete={async (item) => {
|
||||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
await sewerBscPlanRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
</TableCell>
|
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||||
</TableRow>
|
}}
|
||||||
))}
|
/>
|
||||||
</TableBody>
|
);
|
||||||
</Table>
|
}
|
||||||
</div>
|
|
||||||
);
|
export { TechReviewOpinionSection };
|
||||||
}
|
|
||||||
|
|
||||||
export { TechReviewOpinionSection };
|
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export const ENDPOINT = {
|
|||||||
RVW_MNPW: "/as-service/api/v1/rvw-mnpw",
|
RVW_MNPW: "/as-service/api/v1/rvw-mnpw",
|
||||||
BNS_IF: "/as-service/api/v1/if",
|
BNS_IF: "/as-service/api/v1/if",
|
||||||
INSTL_SNCN: "/as-service/api/v1/instl-sncn",
|
INSTL_SNCN: "/as-service/api/v1/instl-sncn",
|
||||||
|
INSTL_SNCN_RVW_OPNN: "/as-service/api/v1/instl-sncn-rvw-opnn",
|
||||||
CKLST: "/as-service/api/v1/cklst",
|
CKLST: "/as-service/api/v1/cklst",
|
||||||
CKLST_ARTCL: "/as-service/api/v1/cklst-artcl",
|
CKLST_ARTCL: "/as-service/api/v1/cklst-artcl",
|
||||||
CKLST_RVW: "/as-service/api/v1/cklst-rvw",
|
CKLST_RVW: "/as-service/api/v1/cklst-rvw",
|
||||||
ADSTDSTR: "/as-service/api/v1/adstdstr",
|
ADSTDSTR: "/as-service/api/v1/adstdstr",
|
||||||
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
||||||
|
FNRS_CNSLTN_RVW_OPNN: "/as-service/api/v1/fnrs-cnsltn-rvw-opnn",
|
||||||
FNRS_SHAR_SE: "/as-service/api/v1/fnrs-shar-se-mng",
|
FNRS_SHAR_SE: "/as-service/api/v1/fnrs-shar-se-mng",
|
||||||
DESIGN_CNSTN: "/as-service/api/v1/design-cnstn",
|
DESIGN_CNSTN: "/as-service/api/v1/design-cnstn",
|
||||||
DESIGN_CNSTN_BIZ_CST: "/as-service/api/v1/design-cnstn-biz-cst",
|
DESIGN_CNSTN_BIZ_CST: "/as-service/api/v1/design-cnstn-biz-cst",
|
||||||
@@ -24,6 +26,7 @@ export const ENDPOINT = {
|
|||||||
DESIGN_CNSTN_ESNTL_ATCH_FILE: "/as-service/api/v1/design-cnstn-esntl-atch-file",
|
DESIGN_CNSTN_ESNTL_ATCH_FILE: "/as-service/api/v1/design-cnstn-esntl-atch-file",
|
||||||
DESIGN_CNSTN_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
|
DESIGN_CNSTN_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
|
||||||
SEWER_BSC_PLAN: "/as-service/api/v1/sewer-bsc-plans",
|
SEWER_BSC_PLAN: "/as-service/api/v1/sewer-bsc-plans",
|
||||||
|
SEWER_BSC_PLAN_RVW_OPNNS: "/as-service/api/v1/sewer-bsc-plan-rvw-opnns",
|
||||||
DASHBOARD: "/as-service/api/v1/dashboard",
|
DASHBOARD: "/as-service/api/v1/dashboard",
|
||||||
DCRAG_TO: "/as-service/api/v1/dcrag-to",
|
DCRAG_TO: "/as-service/api/v1/dcrag-to",
|
||||||
AI_AUTO_PRCS_MNG: "/as-service/api/v1/ai-auto-prcs-mng",
|
AI_AUTO_PRCS_MNG: "/as-service/api/v1/ai-auto-prcs-mng",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export type {
|
|||||||
PbadmsMngDetResponseBase,
|
PbadmsMngDetResponseBase,
|
||||||
RvwOpnnResponseExtension,
|
RvwOpnnResponseExtension,
|
||||||
RvwOpnnSaveBase,
|
RvwOpnnSaveBase,
|
||||||
|
RvwOpnnUpdateRequestDto,
|
||||||
SplmntDmndResponseExtension,
|
SplmntDmndResponseExtension,
|
||||||
SplmntDmndSaveBase,
|
SplmntDmndSaveBase,
|
||||||
} from "./pbadms-sprt";
|
} from "./pbadms-sprt";
|
||||||
|
|||||||
@@ -231,11 +231,22 @@ export interface PbadmsApplySaveHeader {
|
|||||||
rvwStpCd?: string;
|
rvwStpCd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 검토의견 수정 공통 요청 DTO */
|
||||||
|
export interface RvwOpnnUpdateRequestDto {
|
||||||
|
opnrSeComCd?: string;
|
||||||
|
opnrNm?: string;
|
||||||
|
rvwRsltCn?: string;
|
||||||
|
rvwRsltAnmlyYn?: string;
|
||||||
|
atchFileGroupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 검토의견 응답 공통 확장 필드 */
|
/** 검토의견 응답 공통 확장 필드 */
|
||||||
export interface RvwOpnnResponseExtension {
|
export interface RvwOpnnResponseExtension {
|
||||||
opnrSeCdNm?: string;
|
opnrSeCdNm?: string;
|
||||||
atchFileInfo?: AtflMngClientResponseDto;
|
atchFileInfo?: AtflMngClientResponseDto;
|
||||||
regDt?: string;
|
regDt?: string;
|
||||||
|
rgtr?: string;
|
||||||
|
rgtrNm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 보완요구 응답 공통 확장 필드 */
|
/** 보완요구 응답 공통 확장 필드 */
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export {
|
|||||||
} from "./ui/pbadms-task-apply-dialog";
|
} from "./ui/pbadms-task-apply-dialog";
|
||||||
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
|
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
|
||||||
export { ProgressStatusSection } from "./ui/progress-status-section";
|
export { ProgressStatusSection } from "./ui/progress-status-section";
|
||||||
|
export { RvwOpnnListTable, type RvwOpnnListTableProps } from "./ui/rvw-opnn-list-table";
|
||||||
export {
|
export {
|
||||||
SplmntDmndDialog,
|
SplmntDmndDialog,
|
||||||
type SplmntDmndDialogConfirmItem,
|
type SplmntDmndDialogConfirmItem,
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
|
||||||
|
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import type { AtflMngClientResponseDto, RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||||
|
import { AttachmentFileField } from "@/shared/ui";
|
||||||
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
|
import {
|
||||||
|
AttachmentDownloadLink,
|
||||||
|
attachService,
|
||||||
|
useDownloadAttachmentByAtflId,
|
||||||
|
} from "@/entities/attach";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
const RVW_OPNN_EDIT_FILE_KEY_PREFIX = "rvw-opnn-edit::";
|
||||||
|
|
||||||
|
/** 업무별 검토의견 응답 DTO 공통 필드 (업무마다 null 허용 여부가 달라 느슨하게 정의) */
|
||||||
|
interface RvwOpnnListItem {
|
||||||
|
opnrSeComCd?: string | null;
|
||||||
|
opnrNm?: string | null;
|
||||||
|
rvwRsltCn?: string | null;
|
||||||
|
rvwRsltAnmlyYn?: string | null;
|
||||||
|
atchFileGroupId?: string | null;
|
||||||
|
atchFileInfo?: AtflMngClientResponseDto | null;
|
||||||
|
regDt?: string | null;
|
||||||
|
rgtr?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RvwOpnnListTableProps<T extends RvwOpnnListItem> {
|
||||||
|
items: T[];
|
||||||
|
getRowKey: (item: T) => string;
|
||||||
|
/** 로그인 사용자 UUID — 검토의견 등록자(rgtr)와 같을 때만 수정·삭제 버튼 노출 */
|
||||||
|
currentUserUuid?: string;
|
||||||
|
onUpdate: (item: T, body: RvwOpnnUpdateRequestDto) => Promise<void>;
|
||||||
|
onDelete: (item: T) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditDraft {
|
||||||
|
rvwRsltCn: string;
|
||||||
|
rvwRsltAnmlyYn: string;
|
||||||
|
atchFileGroupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditFile {
|
||||||
|
fileName: string;
|
||||||
|
fileKey: string;
|
||||||
|
atflId?: string;
|
||||||
|
atflCd?: string;
|
||||||
|
/** 이번 수정 중 새로 업로드한 파일 여부 (취소 시 정리 대상) */
|
||||||
|
isNew: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAnmlyYn(value?: string | null) {
|
||||||
|
if (value === "Y") return "이상";
|
||||||
|
if (value === "N") return "정상";
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function RvwOpnnListTable<T extends RvwOpnnListItem>({
|
||||||
|
items,
|
||||||
|
getRowKey,
|
||||||
|
currentUserUuid,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
}: RvwOpnnListTableProps<T>) {
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||||
|
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||||
|
const [draft, setDraft] = useState<EditDraft | null>(null);
|
||||||
|
const [editFile, setEditFile] = useState<EditFile | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
|
if (!atflCd || !atflId) return;
|
||||||
|
|
||||||
|
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: atflCd } : prev));
|
||||||
|
setEditFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
|
},
|
||||||
|
onError: (_error, _file, fileKey) => {
|
||||||
|
setEditFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const editFileUploading = editFile ? (isUploading[editFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
|
function canModify(item: T) {
|
||||||
|
return Boolean(currentUserUuid) && item.rgtr === currentUserUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discardNewFile(file: EditFile | null) {
|
||||||
|
if (!file?.isNew) return;
|
||||||
|
|
||||||
|
uploads[file.fileKey]?.abort();
|
||||||
|
if (file.atflId) {
|
||||||
|
try {
|
||||||
|
await attachService.deleteAttachmentByAtflId(file.atflId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEdit() {
|
||||||
|
setEditingKey(null);
|
||||||
|
setDraft(null);
|
||||||
|
setEditFile(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit(item: T) {
|
||||||
|
void discardNewFile(editFile);
|
||||||
|
|
||||||
|
setEditingKey(getRowKey(item));
|
||||||
|
setDraft({
|
||||||
|
rvwRsltCn: item.rvwRsltCn ?? "",
|
||||||
|
rvwRsltAnmlyYn: item.rvwRsltAnmlyYn ?? "",
|
||||||
|
atchFileGroupId: item.atchFileGroupId ?? "",
|
||||||
|
});
|
||||||
|
setEditFile(
|
||||||
|
item.atchFileInfo?.atflCd
|
||||||
|
? {
|
||||||
|
fileName: item.atchFileInfo.orgnlFileNm || "첨부파일",
|
||||||
|
fileKey: `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${item.atchFileInfo.atflCd}`,
|
||||||
|
atflId: item.atchFileInfo.atflId,
|
||||||
|
atflCd: item.atchFileInfo.atflCd,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
void discardNewFile(editFile);
|
||||||
|
resetEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileAdd(files: FileList | null) {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const file = files[0];
|
||||||
|
const fileKey = `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
|
|
||||||
|
setEditFile({ fileName: file.name, fileKey, isNew: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadFileAsync(file, { fileKey });
|
||||||
|
} catch {
|
||||||
|
// onError callback handles cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileRemove() {
|
||||||
|
// 기존 파일은 저장 시점에 연결만 해제하고, 새로 올린 파일은 즉시 정리
|
||||||
|
void discardNewFile(editFile);
|
||||||
|
setEditFile(null);
|
||||||
|
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: "" } : prev));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(item: T) {
|
||||||
|
if (!draft) return;
|
||||||
|
if (!draft.rvwRsltCn.trim()) {
|
||||||
|
alert("의견내용을 입력하세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!draft.rvwRsltAnmlyYn) {
|
||||||
|
alert("검토결과를 선택하세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(await confirm("검토의견을 수정하시겠습니까?"))) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onUpdate(item, {
|
||||||
|
opnrSeComCd: item.opnrSeComCd ?? undefined,
|
||||||
|
opnrNm: item.opnrNm ?? undefined,
|
||||||
|
rvwRsltCn: draft.rvwRsltCn,
|
||||||
|
rvwRsltAnmlyYn: draft.rvwRsltAnmlyYn,
|
||||||
|
atchFileGroupId: draft.atchFileGroupId,
|
||||||
|
});
|
||||||
|
alert("수정되었습니다.");
|
||||||
|
resetEdit();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("검토의견 수정 실패:", error);
|
||||||
|
alert("검토의견 수정 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(item: T) {
|
||||||
|
if (!(await confirm("검토의견을 삭제하시겠습니까?"))) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onDelete(item);
|
||||||
|
alert("삭제되었습니다.");
|
||||||
|
if (editingKey === getRowKey(item)) resetEdit();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("검토의견 삭제 실패:", error);
|
||||||
|
alert("검토의견 삭제 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderViewRow(item: T) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||||
|
<TableCell align="center">{formatAnmlyYn(item.rvwRsltAnmlyYn)}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.atchFileInfo?.atflCd ? (
|
||||||
|
<AttachmentDownloadLink
|
||||||
|
atflCd={item.atchFileInfo.atflCd}
|
||||||
|
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEditRow(rowKey: string, editDraft: EditDraft) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableCell align="left">
|
||||||
|
<Textarea
|
||||||
|
value={editDraft.rvwRsltCn}
|
||||||
|
onChange={(event) =>
|
||||||
|
setDraft((prev) => (prev ? { ...prev, rvwRsltCn: event.target.value } : prev))
|
||||||
|
}
|
||||||
|
placeholder="의견내용을 입력하세요"
|
||||||
|
rows={4}
|
||||||
|
className="min-h-[8rem] !text-xl"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<FieldSet>
|
||||||
|
<RadioGroup
|
||||||
|
value={editDraft.rvwRsltAnmlyYn}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setDraft((prev) => (prev ? { ...prev, rvwRsltAnmlyYn: value } : prev))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<FieldLabel
|
||||||
|
htmlFor={`rvw-opnn-edit-normal-${rowKey}`}
|
||||||
|
className="!w-auto border-none"
|
||||||
|
>
|
||||||
|
<Field orientation="horizontal" className="w-auto">
|
||||||
|
<RadioGroupItem value="N" id={`rvw-opnn-edit-normal-${rowKey}`} />
|
||||||
|
<span className="font-normal">정상</span>
|
||||||
|
</Field>
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel
|
||||||
|
htmlFor={`rvw-opnn-edit-abnormal-${rowKey}`}
|
||||||
|
className="!w-auto border-none"
|
||||||
|
>
|
||||||
|
<Field orientation="horizontal" className="w-auto">
|
||||||
|
<RadioGroupItem value="Y" id={`rvw-opnn-edit-abnormal-${rowKey}`} />
|
||||||
|
<span className="font-normal">이상</span>
|
||||||
|
</Field>
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<AttachmentFileField
|
||||||
|
files={
|
||||||
|
editFile
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
fileKey: editFile.fileKey,
|
||||||
|
fileName: editFile.fileName,
|
||||||
|
onDownload: editFile.atflId
|
||||||
|
? () => downloadByAtflId(editFile.atflId!)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
inputId={`rvw-opnn-edit-${rowKey}-file-input`}
|
||||||
|
isUploadingByKey={isUploading}
|
||||||
|
multiple={false}
|
||||||
|
showAddButton={!editFile}
|
||||||
|
addDisabled={editFileUploading}
|
||||||
|
onAdd={(fileList) => void handleFileAdd(fileList)}
|
||||||
|
onRemove={handleFileRemove}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table
|
||||||
|
caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시, 관리 정보 제공"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col width="10%" />
|
||||||
|
<col width="47%" />
|
||||||
|
<col width="6%" />
|
||||||
|
<col width="17%" />
|
||||||
|
<col width="10%" />
|
||||||
|
<col width="10%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>의견자명</TableHead>
|
||||||
|
<TableHead>의견내용</TableHead>
|
||||||
|
<TableHead>검토결과</TableHead>
|
||||||
|
<TableHead>첨부파일</TableHead>
|
||||||
|
<TableHead>등록일시</TableHead>
|
||||||
|
<TableHead>관리</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
const rowKey = getRowKey(item);
|
||||||
|
const isEditing = editingKey === rowKey && draft !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={rowKey}>
|
||||||
|
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||||
|
{isEditing ? renderEditRow(rowKey, draft) : renderViewRow(item)}
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{!canModify(item) ? (
|
||||||
|
"-"
|
||||||
|
) : isEditing ? (
|
||||||
|
<div className="flex flex-wrap justify-center gap-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void handleSave(item)}
|
||||||
|
disabled={isSubmitting || editFileUploading}
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
color="white"
|
||||||
|
onClick={cancelEdit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap justify-center gap-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
color="white"
|
||||||
|
onClick={() => startEdit(item)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => void handleDelete(item)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RvwOpnnListTable, type RvwOpnnListTableProps };
|
||||||
@@ -784,7 +784,6 @@ const ExcretaTreatmentFacilityInp = () => {
|
|||||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
||||||
noApplicable,
|
noApplicable,
|
||||||
submitList,
|
submitList,
|
||||||
deleteList: deleteListForSubmit,
|
deleteList: deleteListForSubmit,
|
||||||
|
|||||||
@@ -797,7 +797,6 @@ const PrivateSewageFacilityInp = () => {
|
|||||||
|
|
||||||
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
||||||
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
||||||
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
|
|
||||||
|
|
||||||
const submitInfoBase = {
|
const submitInfoBase = {
|
||||||
statsYr: lastSearchedValues.year ?? "",
|
statsYr: lastSearchedValues.year ?? "",
|
||||||
@@ -842,7 +841,6 @@ const PrivateSewageFacilityInp = () => {
|
|||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: buildSewageTreatFactStatusSaveList(currentData),
|
submitList: buildSewageTreatFactStatusSaveList(currentData),
|
||||||
...adminOpt,
|
|
||||||
};
|
};
|
||||||
response =
|
response =
|
||||||
await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus(
|
await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus(
|
||||||
@@ -853,7 +851,6 @@ const PrivateSewageFacilityInp = () => {
|
|||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: buildSepticTankStatusSaveList(currentData),
|
submitList: buildSepticTankStatusSaveList(currentData),
|
||||||
...adminOpt,
|
|
||||||
};
|
};
|
||||||
response =
|
response =
|
||||||
await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param);
|
await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param);
|
||||||
@@ -862,7 +859,6 @@ const PrivateSewageFacilityInp = () => {
|
|||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: buildPrivateSewageDesigStatusSaveList(currentData),
|
submitList: buildPrivateSewageDesigStatusSaveList(currentData),
|
||||||
...adminOpt,
|
|
||||||
};
|
};
|
||||||
response =
|
response =
|
||||||
await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus(
|
await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus(
|
||||||
|
|||||||
@@ -768,7 +768,6 @@ const PumpingStationInp = () => {
|
|||||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
||||||
noApplicable,
|
noApplicable,
|
||||||
submitList,
|
submitList,
|
||||||
deleteList,
|
deleteList,
|
||||||
|
|||||||
@@ -1410,7 +1410,6 @@ const SewageExcretaRelatedInp = () => {
|
|||||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||||
...(isPublicInstitutionTab ? { noApplicable } : {}),
|
...(isPublicInstitutionTab ? { noApplicable } : {}),
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const verificationQuery = {
|
const verificationQuery = {
|
||||||
|
|||||||
@@ -1357,11 +1357,6 @@ const SewageExcretaSlurgeInp = () => {
|
|||||||
: rowsForSubmit.map((row) =>
|
: rowsForSubmit.map((row) =>
|
||||||
buildExcretaSlurgeProcessingStatusSaveItem(row, list),
|
buildExcretaSlurgeProcessingStatusSaveItem(row, list),
|
||||||
);
|
);
|
||||||
const slurgeDeleteList = isSlurgeFacTab
|
|
||||||
? (deleteListByTab.get(tabKey) ?? [])
|
|
||||||
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
|
|
||||||
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list))
|
|
||||||
: [];
|
|
||||||
const sewageSlurgeFacDeleteList = isSewageSlurgeFacTab
|
const sewageSlurgeFacDeleteList = isSewageSlurgeFacTab
|
||||||
? (deleteListByTab.get(tabKey) ?? [])
|
? (deleteListByTab.get(tabKey) ?? [])
|
||||||
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
|
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
|
||||||
@@ -1432,7 +1427,6 @@ const SewageExcretaSlurgeInp = () => {
|
|||||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
|
|
||||||
noApplicable,
|
noApplicable,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1445,7 +1439,6 @@ const SewageExcretaSlurgeInp = () => {
|
|||||||
...submitRequestBase,
|
...submitRequestBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: slurgeSubmitList,
|
submitList: slurgeSubmitList,
|
||||||
deleteList: slurgeDeleteList,
|
|
||||||
})
|
})
|
||||||
: isDigestorTab
|
: isDigestorTab
|
||||||
? await statisticsInputSewageExcretaSlurgeApi.submitDigestorOperatingStatus({
|
? await statisticsInputSewageExcretaSlurgeApi.submitDigestorOperatingStatus({
|
||||||
@@ -1655,9 +1648,6 @@ const SewageExcretaSlurgeInp = () => {
|
|||||||
: rowsForSave.map((row) =>
|
: rowsForSave.map((row) =>
|
||||||
buildSlurgeProcessingFacStatusSaveItem(row, list),
|
buildSlurgeProcessingFacStatusSaveItem(row, list),
|
||||||
);
|
);
|
||||||
const deleteList = (deleteListByTab.get(tabKey) ?? [])
|
|
||||||
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
|
|
||||||
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list));
|
|
||||||
|
|
||||||
if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
|
if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
|
||||||
alert("해당사항 없음을 체크해주세요.");
|
alert("해당사항 없음을 체크해주세요.");
|
||||||
@@ -1670,7 +1660,6 @@ const SewageExcretaSlurgeInp = () => {
|
|||||||
...baseSaveRequest,
|
...baseSaveRequest,
|
||||||
...userFields,
|
...userFields,
|
||||||
saveList,
|
saveList,
|
||||||
deleteList,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (tabKey === DigestorOperatingStatusInpColumn.tabKey) {
|
} else if (tabKey === DigestorOperatingStatusInpColumn.tabKey) {
|
||||||
|
|||||||
@@ -350,7 +350,6 @@ function buildRegionalFactSubmitListFromGrid(
|
|||||||
};
|
};
|
||||||
return currentData.map((d) => ({
|
return currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
goalYr: nullableTrimmedString(d.goalYr),
|
goalYr: nullableTrimmedString(d.goalYr),
|
||||||
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
||||||
@@ -1264,7 +1263,6 @@ const SewagePipeInp = () => {
|
|||||||
};
|
};
|
||||||
const submitList = currentData.map((d) => ({
|
const submitList = currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
fcltSn: nullableTrimmedString(d.fcltSn),
|
fcltSn: nullableTrimmedString(d.fcltSn),
|
||||||
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
|
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
|
||||||
@@ -1637,7 +1635,6 @@ const SewagePipeInp = () => {
|
|||||||
|
|
||||||
const saveList = currentData.map((d) => ({
|
const saveList = currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
goalYr: nullableTrimmedString(d.goalYr),
|
goalYr: nullableTrimmedString(d.goalYr),
|
||||||
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
||||||
@@ -1728,7 +1725,6 @@ const SewagePipeInp = () => {
|
|||||||
|
|
||||||
const saveList = currentData.map((d) => ({
|
const saveList = currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
fcltSn: nullableTrimmedString(d.fcltSn),
|
fcltSn: nullableTrimmedString(d.fcltSn),
|
||||||
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
|
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ function buildRepairRowList(
|
|||||||
|
|
||||||
return currentData.map((d) => ({
|
return currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
dfrpWholPrlg: formatNumber(d.dfrpWholPrlg, "dfrpWholPrlg"),
|
dfrpWholPrlg: formatNumber(d.dfrpWholPrlg, "dfrpWholPrlg"),
|
||||||
cbsysDfrpPrlg: formatNumber(d.cbsysDfrpPrlg, "cbsysDfrpPrlg"),
|
cbsysDfrpPrlg: formatNumber(d.cbsysDfrpPrlg, "cbsysDfrpPrlg"),
|
||||||
@@ -247,7 +246,6 @@ function buildDredgingRowList(
|
|||||||
|
|
||||||
return currentData.map((d) => ({
|
return currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
|
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
|
||||||
wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"),
|
wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"),
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ function buildSewerageFinanceSettleSaveList(
|
|||||||
|
|
||||||
return currentData.map((d) => ({
|
return currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
soaGovSprtAmt: formatNumber(d.soaGovSprtAmt, "soaGovSprtAmt"),
|
soaGovSprtAmt: formatNumber(d.soaGovSprtAmt, "soaGovSprtAmt"),
|
||||||
mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"),
|
mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"),
|
||||||
@@ -257,7 +256,6 @@ function buildSewerageChargeSaveList(
|
|||||||
|
|
||||||
return currentData.map((d) => ({
|
return currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"),
|
anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"),
|
||||||
sewerLevyAmt: formatNumber(d.sewerLevyAmt, "sewerLevyAmt"),
|
sewerLevyAmt: formatNumber(d.sewerLevyAmt, "sewerLevyAmt"),
|
||||||
@@ -276,7 +274,6 @@ function buildSewerageUsageFeeSaveList(
|
|||||||
|
|
||||||
return currentData.map((d) => ({
|
return currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"),
|
hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"),
|
||||||
hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"),
|
hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"),
|
||||||
@@ -312,7 +309,6 @@ function buildCauserBurdenSaveList(
|
|||||||
|
|
||||||
return currentData.map((d) => ({
|
return currentData.map((d) => ({
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
brdnAmtLevyNocs: formatNumber(d.brdnAmtLevyNocs, "brdnAmtLevyNocs"),
|
brdnAmtLevyNocs: formatNumber(d.brdnAmtLevyNocs, "brdnAmtLevyNocs"),
|
||||||
brdnAmtLevyAmt: formatNumber(d.brdnAmtLevyAmt, "brdnAmtLevyAmt"),
|
brdnAmtLevyAmt: formatNumber(d.brdnAmtLevyAmt, "brdnAmtLevyAmt"),
|
||||||
@@ -849,7 +845,6 @@ const SewerageFinanceInp = () => {
|
|||||||
|
|
||||||
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
||||||
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
||||||
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
|
|
||||||
|
|
||||||
const submitInfoBase = {
|
const submitInfoBase = {
|
||||||
statsYr: lastSearchedValues.year ?? "",
|
statsYr: lastSearchedValues.year ?? "",
|
||||||
@@ -894,7 +889,6 @@ const SewerageFinanceInp = () => {
|
|||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: buildSewerageFinanceSettleSaveList(currentData),
|
submitList: buildSewerageFinanceSettleSaveList(currentData),
|
||||||
...adminOpt,
|
|
||||||
};
|
};
|
||||||
response =
|
response =
|
||||||
await statisticsInputSewerageFinanceApi.submitSewerageFinanceSettle(
|
await statisticsInputSewerageFinanceApi.submitSewerageFinanceSettle(
|
||||||
@@ -905,7 +899,6 @@ const SewerageFinanceInp = () => {
|
|||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: buildSewerageChargeSaveList(currentData),
|
submitList: buildSewerageChargeSaveList(currentData),
|
||||||
...adminOpt,
|
|
||||||
};
|
};
|
||||||
response = await statisticsInputSewerageFinanceApi.submitSewerageCharge(param);
|
response = await statisticsInputSewerageFinanceApi.submitSewerageCharge(param);
|
||||||
} else if (isUsageFeeTab) {
|
} else if (isUsageFeeTab) {
|
||||||
@@ -913,7 +906,6 @@ const SewerageFinanceInp = () => {
|
|||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: buildSewerageUsageFeeSaveList(currentData),
|
submitList: buildSewerageUsageFeeSaveList(currentData),
|
||||||
...adminOpt,
|
|
||||||
};
|
};
|
||||||
response =
|
response =
|
||||||
await statisticsInputSewerageFinanceApi.submitSewerageUsageFee(param);
|
await statisticsInputSewerageFinanceApi.submitSewerageUsageFee(param);
|
||||||
@@ -922,7 +914,6 @@ const SewerageFinanceInp = () => {
|
|||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
submitList: buildCauserBurdenSaveList(currentData),
|
submitList: buildCauserBurdenSaveList(currentData),
|
||||||
...adminOpt,
|
|
||||||
};
|
};
|
||||||
response = await statisticsInputSewerageFinanceApi.submitCauserBurden(param);
|
response = await statisticsInputSewerageFinanceApi.submitCauserBurden(param);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,10 +399,6 @@ const SeweragePenetrationRateInp = () => {
|
|||||||
statsCd: statsCd,
|
statsCd: statsCd,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isAdminInfo) {
|
|
||||||
params.superAdmin = 'superAdmin';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
@@ -498,7 +494,6 @@ const SeweragePenetrationRateInp = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||||
sggNm: nullableTrimmedString(d.sggNm),
|
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||||
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm),
|
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm),
|
||||||
@@ -677,8 +672,6 @@ const SeweragePenetrationRateInp = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||||
// 행정구역
|
|
||||||
sggNm: nullableTrimmedString(d.sggNm), // 시군구
|
|
||||||
admdstCd: nullableTrimmedString(d.admdstCd), // 행정코드
|
admdstCd: nullableTrimmedString(d.admdstCd), // 행정코드
|
||||||
admdstNm: nullableTrimmedString(d.admdstNm), // 행정구역명
|
admdstNm: nullableTrimmedString(d.admdstNm), // 행정구역명
|
||||||
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm), // 지류
|
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm), // 지류
|
||||||
|
|||||||
@@ -808,13 +808,11 @@ const SewerageServiceLevelInp = () => {
|
|||||||
? await statisticsInputSewerageServiceLevelApi.submitComplaintList({
|
? await statisticsInputSewerageServiceLevelApi.submitComplaintList({
|
||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
||||||
submitList: complaintSubmitList,
|
submitList: complaintSubmitList,
|
||||||
})
|
})
|
||||||
: await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({
|
: await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({
|
||||||
...submitInfoBase,
|
...submitInfoBase,
|
||||||
userUuid: userInfo.userUnqId,
|
userUuid: userInfo.userUnqId,
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
||||||
noApplicable,
|
noApplicable,
|
||||||
submitList: residentSubmitList,
|
submitList: residentSubmitList,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1153,7 +1153,6 @@ const WaterReuseInp = () => {
|
|||||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let articleSggOptions: Sgg[] | null = null;
|
let articleSggOptions: Sgg[] | null = null;
|
||||||
|
|||||||
@@ -1,100 +1,100 @@
|
|||||||
export interface DashboardSearchParams {
|
export interface DashboardSearchParams {
|
||||||
statsYr: string;
|
statsYr: string;
|
||||||
dtlInstCd: string;
|
dtlInstCd: string;
|
||||||
enoCd?: string;
|
enoCd?: string;
|
||||||
laeSeCd?: string;
|
laeSeCd?: string;
|
||||||
sewerBzmnCd?: string;
|
sewerBzmnCd?: string;
|
||||||
sido?: string;
|
sido?: string;
|
||||||
sggNm?: string;
|
sggNm?: string;
|
||||||
unSubmitOnly?: boolean;
|
unSubmitOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardApiResponse<T> {
|
export interface DashboardApiResponse<T> {
|
||||||
status: string;
|
status: string;
|
||||||
message: string;
|
message: string;
|
||||||
data: T;
|
data: T;
|
||||||
code: string;
|
code: string;
|
||||||
httpStatus: number;
|
httpStatus: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VrfcReportResponse {
|
export interface VrfcReportResponse {
|
||||||
typeCounts: Record<string, number>;
|
typeCounts: Record<string, number>;
|
||||||
list: VrfcReportSelect[];
|
list: VrfcReportSelect[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VrfcReportSelect {
|
export interface VrfcReportSelect {
|
||||||
vrfcCd: string;
|
vrfcCd: string;
|
||||||
statsNm: string;
|
statsNm: string;
|
||||||
rsnExpln: string;
|
rsnExpln: string;
|
||||||
admdstNm: string;
|
admdstNm: string;
|
||||||
nowVlCn: string;
|
nowCN: string;
|
||||||
compVlCn: string;
|
cmprCn: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VrfcErrCountResponse {
|
export interface VrfcErrCountResponse {
|
||||||
statsYr: string;
|
statsYr: string;
|
||||||
vrfcErrCount: number;
|
vrfcErrCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnSubmitCountResponse {
|
export interface UnSubmitCountResponse {
|
||||||
statsYr: string;
|
statsYr: string;
|
||||||
statusList: UnSubmitStatsResponse[];
|
statusList: UnSubmitStatsResponse[];
|
||||||
submittedCount: number;
|
submittedCount: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
unSubmittedCount: number;
|
unSubmittedCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnSubmitStatsResponse {
|
export interface UnSubmitStatsResponse {
|
||||||
sbmsnYn: string;
|
sbmsnYn: string;
|
||||||
sidoSggNm: string;
|
sidoSggNm: string;
|
||||||
statsCd: string;
|
statsCd: string;
|
||||||
statsNm: string;
|
statsNm: string;
|
||||||
statusCd: string;
|
statusCd: string;
|
||||||
statusNm: string;
|
statusNm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StatsSubmissionResponse {
|
export interface StatsSubmissionResponse {
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
submittedCount: number;
|
submittedCount: number;
|
||||||
submissionRate: number;
|
submissionRate: number;
|
||||||
statusLevel: string;
|
statusLevel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EcptReqCountResponse {
|
export interface EcptReqCountResponse {
|
||||||
statsYr: string;
|
statsYr: string;
|
||||||
ecptReqCount: number;
|
ecptReqCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThreeStageStatusResponse {
|
export interface ThreeStageStatusResponse {
|
||||||
statsYr: string;
|
statsYr: string;
|
||||||
lclgvSbmsnSttsCd: string;
|
lclgvSbmsnSttsCd: string;
|
||||||
lclgvSbmsnSttsNm: string;
|
lclgvSbmsnSttsNm: string;
|
||||||
lclgvRejected: boolean;
|
lclgvRejected: boolean;
|
||||||
mctpvPicSbmsnSttsCd: string;
|
mctpvPicSbmsnSttsCd: string;
|
||||||
mctpvPicSbmsnSttsNm: string;
|
mctpvPicSbmsnSttsNm: string;
|
||||||
mctpvRejected: boolean;
|
mctpvRejected: boolean;
|
||||||
pbcpSbmsnSttsCd: string;
|
pbcpSbmsnSttsCd: string;
|
||||||
pbcpSbmsnSttsNm: string;
|
pbcpSbmsnSttsNm: string;
|
||||||
pbcpRejected: boolean;
|
pbcpRejected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardService {
|
export interface DashboardService {
|
||||||
getVrfcReport(
|
getVrfcReport(
|
||||||
params: DashboardSearchParams,
|
params: DashboardSearchParams,
|
||||||
): Promise<DashboardApiResponse<VrfcReportResponse>>;
|
): Promise<DashboardApiResponse<VrfcReportResponse>>;
|
||||||
getVrfcErrCount(
|
getVrfcErrCount(
|
||||||
params: DashboardSearchParams,
|
params: DashboardSearchParams,
|
||||||
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
|
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
|
||||||
getUnSubmitCount(
|
getUnSubmitCount(
|
||||||
params: DashboardSearchParams,
|
params: DashboardSearchParams,
|
||||||
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
|
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
|
||||||
getStatsSubmissionRate(
|
getStatsSubmissionRate(
|
||||||
params: DashboardSearchParams,
|
params: DashboardSearchParams,
|
||||||
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
|
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
|
||||||
getEcptReqCount(
|
getEcptReqCount(
|
||||||
params: DashboardSearchParams,
|
params: DashboardSearchParams,
|
||||||
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
|
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
|
||||||
getThreeStageStatus(
|
getThreeStageStatus(
|
||||||
params: DashboardSearchParams,
|
params: DashboardSearchParams,
|
||||||
): Promise<DashboardApiResponse<ThreeStageStatusResponse>>;
|
): Promise<DashboardApiResponse<ThreeStageStatusResponse>>;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -117,7 +117,7 @@ export type SlurgeProcessingFacStatusApiResponse =
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ST00110001 저장 — `saveList` / `deleteList` 항목.
|
* ST00110001 저장 — `saveList` 항목.
|
||||||
* `sggNm`·계산필드·검증결과는 저장 요청에서 제외.
|
* `sggNm`·계산필드·검증결과는 저장 요청에서 제외.
|
||||||
*/
|
*/
|
||||||
export type SlurgeProcessingFacStatusSaveItem = Omit<
|
export type SlurgeProcessingFacStatusSaveItem = Omit<
|
||||||
@@ -167,7 +167,6 @@ export interface SlurgeProcessingFacStatusSaveRequest {
|
|||||||
superAdmin?: string;
|
superAdmin?: string;
|
||||||
noApplicable?: boolean;
|
noApplicable?: boolean;
|
||||||
saveList: SlurgeProcessingFacStatusSaveItem[];
|
saveList: SlurgeProcessingFacStatusSaveItem[];
|
||||||
deleteList: SlurgeProcessingFacStatusSaveItem[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SlurgeProcessingFacStatusSaveResponse = ApiResponse<unknown> & {
|
export type SlurgeProcessingFacStatusSaveResponse = ApiResponse<unknown> & {
|
||||||
@@ -194,7 +193,6 @@ export interface SlurgeProcessingFacStatusSubmitRequest {
|
|||||||
superAdmin?: string;
|
superAdmin?: string;
|
||||||
noApplicable?: boolean;
|
noApplicable?: boolean;
|
||||||
submitList: SlurgeProcessingFacStatusSubmitItem[];
|
submitList: SlurgeProcessingFacStatusSubmitItem[];
|
||||||
deleteList: SlurgeProcessingFacStatusSubmitItem[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SlurgeProcessingFacStatusSubmitResponse {
|
export interface SlurgeProcessingFacStatusSubmitResponse {
|
||||||
|
|||||||
-2
@@ -98,7 +98,6 @@ export type SewagePipeMaintainDredgingResponse = ApiResponse<SewagePipeMaintainD
|
|||||||
/** ST00030001 하수관로 유지관리 > 개보수 저장·제출 API 행 */
|
/** ST00030001 하수관로 유지관리 > 개보수 저장·제출 API 행 */
|
||||||
export interface SewagePipeMaintainRepairSaveItem {
|
export interface SewagePipeMaintainRepairSaveItem {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
dfrpWholPrlg: number;
|
dfrpWholPrlg: number;
|
||||||
cbsysDfrpPrlg: number;
|
cbsysDfrpPrlg: number;
|
||||||
@@ -167,7 +166,6 @@ export interface SewagePipeMaintainRepairSubmitResponse {
|
|||||||
/** ST00030002 하수관로 유지관리 > 관로준설 저장·제출 API 행 */
|
/** ST00030002 하수관로 유지관리 > 관로준설 저장·제출 API 행 */
|
||||||
export interface SewagePipeMaintainDredgingSaveItem {
|
export interface SewagePipeMaintainDredgingSaveItem {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
wholFcltPrlg: number;
|
wholFcltPrlg: number;
|
||||||
wholSwagePpl: number;
|
wholSwagePpl: number;
|
||||||
|
|||||||
-4
@@ -92,7 +92,6 @@ export interface SewerageFinanceSettleRequestParams {
|
|||||||
*/
|
*/
|
||||||
export interface SewerageFinanceSettleSaveItem {
|
export interface SewerageFinanceSettleSaveItem {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
soaGovSprtAmt: number;
|
soaGovSprtAmt: number;
|
||||||
mctpvLcexpAmt: number;
|
mctpvLcexpAmt: number;
|
||||||
@@ -255,7 +254,6 @@ export interface SewerageChargeRequestParams {
|
|||||||
*/
|
*/
|
||||||
export interface SewerageChargeSaveItem {
|
export interface SewerageChargeSaveItem {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
anlLevyQnt: number;
|
anlLevyQnt: number;
|
||||||
sewerLevyAmt: number;
|
sewerLevyAmt: number;
|
||||||
@@ -362,7 +360,6 @@ export interface SewerageUsageFeeRequestParams {
|
|||||||
*/
|
*/
|
||||||
export interface SewerageUsageFeeSaveItem {
|
export interface SewerageUsageFeeSaveItem {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
hshusLevyAmt: number;
|
hshusLevyAmt: number;
|
||||||
hshusLevyQnt: number;
|
hshusLevyQnt: number;
|
||||||
@@ -472,7 +469,6 @@ export interface CauserBurdenRequestParams {
|
|||||||
*/
|
*/
|
||||||
export interface CauserBurdenSaveItem {
|
export interface CauserBurdenSaveItem {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
brdnAmtLevyNocs: number;
|
brdnAmtLevyNocs: number;
|
||||||
brdnAmtLevyAmt: number;
|
brdnAmtLevyAmt: number;
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export type SeweragePipeInputRegionalFactStatusSaveItem = Omit<
|
|||||||
"verificationResult" | "admdstCd" | "sggNm" | "admdstNm" | "goalYr"
|
"verificationResult" | "admdstCd" | "sggNm" | "admdstNm" | "goalYr"
|
||||||
> & {
|
> & {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
goalYr: string | null;
|
goalYr: string | null;
|
||||||
};
|
};
|
||||||
@@ -118,7 +117,6 @@ export type SeweragePipeInputFactInstallStatusSaveItem = Omit<
|
|||||||
| "rmrk"
|
| "rmrk"
|
||||||
> & {
|
> & {
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
sggNm: string | null;
|
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
fcltSn: string | null;
|
fcltSn: string | null;
|
||||||
swtrmtFcltCd: string | null;
|
swtrmtFcltCd: string | null;
|
||||||
|
|||||||
-1
@@ -86,7 +86,6 @@ export interface SewerageSupplyRateRequestParams {
|
|||||||
|
|
||||||
/** 하수도 보급률 저장 API 요청 body (한 행) */
|
/** 하수도 보급률 저장 API 요청 body (한 행) */
|
||||||
export interface SewerageSupplyRateSaveItem {
|
export interface SewerageSupplyRateSaveItem {
|
||||||
sggNm: string | null;
|
|
||||||
admdstCd: string | null;
|
admdstCd: string | null;
|
||||||
admdstNm: string | null;
|
admdstNm: string | null;
|
||||||
statsTrbtrNm: string | null;
|
statsTrbtrNm: string | null;
|
||||||
|
|||||||
@@ -1,133 +1,133 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useDashboardVrfcReport } from "@/entities/statistics";
|
import { useDashboardVrfcReport } from "@/entities/statistics";
|
||||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
type BadgeProps,
|
type BadgeProps,
|
||||||
LoadingWithAnimation,
|
LoadingWithAnimation,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
import { useDragHandle } from "../sortable-panel";
|
import { useDragHandle } from "../sortable-panel";
|
||||||
|
|
||||||
interface VrfcReportPanelProps {
|
interface VrfcReportPanelProps {
|
||||||
params: DashboardSearchParams | null;
|
params: DashboardSearchParams | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||||
NV: "일반",
|
NV: "일반",
|
||||||
IMR: "I-MR",
|
IMR: "I-MR",
|
||||||
HB: "HB",
|
HB: "HB",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
||||||
NV: "gray",
|
NV: "gray",
|
||||||
IMR: "red",
|
IMR: "red",
|
||||||
HB: "blue",
|
HB: "blue",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
|
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
|
||||||
const { dragHandleProps, isDragging } = useDragHandle();
|
const { dragHandleProps, isDragging } = useDragHandle();
|
||||||
const { data, isLoading, error } = useDashboardVrfcReport(params);
|
const { data, isLoading, error } = useDashboardVrfcReport(params);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||||
<div
|
<div
|
||||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||||
{...dragHandleProps}
|
{...dragHandleProps}
|
||||||
title="헤더를 드래그하여 패널 순서 변경"
|
title="헤더를 드래그하여 패널 순서 변경"
|
||||||
>
|
>
|
||||||
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
|
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="relative size-[5rem]">
|
<div className="relative size-[5rem]">
|
||||||
<LoadingWithAnimation color="primary" />
|
<LoadingWithAnimation color="primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
데이터를 불러올 수 없습니다.
|
데이터를 불러올 수 없습니다.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data && (
|
{data && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
|
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
|
||||||
{data.typeCounts &&
|
{data.typeCounts &&
|
||||||
Object.keys(data.typeCounts).length > 0 && (
|
Object.keys(data.typeCounts).length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{Object.entries(data.typeCounts).map(
|
{Object.entries(data.typeCounts).map(
|
||||||
([type, count]) => (
|
([type, count]) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={type}
|
key={type}
|
||||||
color={
|
color={
|
||||||
TYPE_CODE_COLOR_MAP[type]
|
TYPE_CODE_COLOR_MAP[type]
|
||||||
}
|
}
|
||||||
className="rounded-full px-2 py-1 text-lg"
|
className="rounded-full px-2 py-1 text-lg"
|
||||||
>
|
>
|
||||||
{TYPE_CODE_NAME_MAP[type]}:{" "}
|
{TYPE_CODE_NAME_MAP[type]}:{" "}
|
||||||
{count}
|
{count}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
|
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
|
||||||
<div className="max-h-[44rem] overflow-y-auto">
|
<div className="max-h-[44rem] overflow-y-auto">
|
||||||
{data.list.length === 0 ? (
|
{data.list.length === 0 ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
이상치 항목이 없습니다.
|
이상치 항목이 없습니다.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2 tracking-tight">
|
<div className="space-y-2 tracking-tight">
|
||||||
{data.list.map((item, index) => (
|
{data.list.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex flex-col rounded-sm border border-border p-3"
|
className="flex flex-col rounded-sm border border-border p-3"
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{item.statsNm}
|
{item.statsNm}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
color={
|
color={
|
||||||
TYPE_CODE_COLOR_MAP[
|
TYPE_CODE_COLOR_MAP[
|
||||||
item.vrfcCd
|
item.vrfcCd
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
TYPE_CODE_NAME_MAP[
|
TYPE_CODE_NAME_MAP[
|
||||||
item.vrfcCd
|
item.vrfcCd
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
{item.rsnExpln}
|
{item.rsnExpln}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-between text-lg text-muted-foreground">
|
<div className="flex justify-between text-lg text-muted-foreground">
|
||||||
<span>{item.admdstNm}</span>
|
<span>{item.admdstNm}</span>
|
||||||
<span>
|
<span>
|
||||||
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
|
{/* TBD: 증감 표시 방법 (nowCN - cmprCn 계산?) */}
|
||||||
현재: {item.nowVlCn ?? "-"}{" "}
|
현재: {item.nowCN ?? "-"}{" "}
|
||||||
/ 비교:{" "}
|
/ 비교:{" "}
|
||||||
{item.compVlCn ?? "-"}
|
{item.cmprCn ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
|
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function StatisticsPageLayout({ children }: { children: React.ReactNode }) {
|
|||||||
<TabBar />
|
<TabBar />
|
||||||
<PageTitle
|
<PageTitle
|
||||||
title={pageTitle?.label ?? ""}
|
title={pageTitle?.label ?? ""}
|
||||||
|
menuCd={pageTitle?.menuItem?.menuCd}
|
||||||
menuPaths={menuPaths ?? []}
|
menuPaths={menuPaths ?? []}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user