Compare commits

...
10 Commits
41 changed files with 2393 additions and 2047 deletions
@@ -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,3 +1,4 @@
export { fnrsCnsltnRvwOpnnService } from "./api/fnrs-cnsltn-rvw-opnn-service";
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
export { fnrsCnsltnKeys } from "./api/queries";
export {
@@ -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
View File
@@ -1,3 +1,4 @@
export { instlSncnRvwOpnnService } from "./api/instl-sncn-rvw-opnn-service";
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
export {
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,
};
@@ -11,6 +11,7 @@ export {
useTechReviewSewerBscPlan,
useUpdateSewerBscPlan,
} from "./api/queries";
export { sewerBscPlanRvwOpnnService } from "./api/sewer-bsc-plan-rvw-opnn-service";
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
export type {
CklstResultItem,
@@ -2,18 +2,24 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
import { attachService } from "@/entities/attach";
import {
designCnstnKeys,
type DesignCnstnRvwOpnnResponseDto,
designCnstnRvwOpnnService,
} from "@/entities/design-cnstn";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
@@ -172,18 +178,27 @@ const TechReviewOpinionSection = forwardRef<
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
return editableReviewStep === reviewStep && canWriteOpinion;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
<TableHead scope="row" className="w-[12rem]">
의견내용
</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
@@ -279,59 +294,40 @@ const TechReviewOpinionSection = forwardRef<
return (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
);
});
function getRvwOpnnIds(item: DesignCnstnRvwOpnnResponseDto) {
if (!item.designCnstnDocNo || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.designCnstnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.designCnstnDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.designCnstnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await designCnstnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
}}
onDelete={async (item) => {
await designCnstnRvwOpnnService.delete(...getRvwOpnnIds(item));
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
}}
/>
) : (
"-"
)}
</TableCell>
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
@@ -2,20 +2,22 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import {
AttachmentDownloadLink,
attachService,
useDownloadAttachmentByAtflId,
} from "@/entities/attach";
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
fnrsCnsltnKeys,
type FnrsCnsltnRvwOpnnResponseDto,
fnrsCnsltnRvwOpnnService,
} from "@/entities/fnrs-cnsltn";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
@@ -180,16 +182,24 @@ const TechReviewOpinionSection = forwardRef<
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return false;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
return true;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return null;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead className="w-[12rem]">의견내용</TableHead>
@@ -280,7 +290,9 @@ const TechReviewOpinionSection = forwardRef<
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
@@ -288,7 +300,9 @@ const TechReviewOpinionSection = forwardRef<
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{/*
재원협의에서 환경공단 검토의견은 자동 생성
{renderOpinionForm("KECO")}
@@ -300,52 +314,31 @@ const TechReviewOpinionSection = forwardRef<
);
});
function getRvwOpnnIds(item: FnrsCnsltnRvwOpnnResponseDto) {
if (!item.fnrsCnsltnDocNo || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.fnrsCnsltnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await fnrsCnsltnRvwOpnnService.update(...getRvwOpnnIds(item), body);
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>
))}
</TableBody>
</Table>
</div>
);
}
@@ -3,7 +3,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
aply: "aply",
del: "del",
instlSncnAncmnt: "instlSncnAncmnt",
envoSbmsn: "envoSbmsn",
rjct: "rjct",
sncnAprv: "sncnAprv",
rvwRply: "rvwRply",
@@ -33,13 +32,15 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
],
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
},
// 환경청 단계 제거: 기존 환경청(LE·TP) 기능을 광역자치단체가 수행
ROLE_USER04: {
ST: [INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn, INSTL_SNCN_VIEW_BUTTON_ACTION.rjct],
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
},
ROLE_USER03: {
LE: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply, INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw],
ST: [
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],
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
},
};
@@ -55,16 +55,8 @@ export const instlSncnRegistSchema = z.object({
tstmDesignAmt: z.number(),
}),
),
cklstResultList: z.array(
instlSncnCklstResultItemSchema.extend({
cklstSn: z.number(),
cklstArtclSn: z.number(),
cklstRvwSn: z.number(),
rvwStpComCd: z.string(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
}),
),
// 설치인가 화면에서 체크리스트 미사용: 숨겨진 값 때문에 신청·저장 검증이 막히지 않도록 필수값을 두지 않음
cklstResultList: z.array(instlSncnCklstResultItemSchema),
esntlAtchFileList: z.array(
instlSncnEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(),
@@ -50,7 +50,7 @@ export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
): ReviewStepCode | undefined {
if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) {
if (dtlInstCd === "ROLE_USER04" && (prgrsSttsComCd === "ST" || prgrsSttsComCd === "TP")) {
return "ENV";
}
@@ -50,13 +50,13 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "환경청검토",
ENV: "광역자치단체검토",
KECO: "기술검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
ENV: "광역자치단체",
KECO: "환경공단",
};
@@ -89,7 +89,7 @@ export const INSTL_PROGRESS_ORDER = [
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
ROLE_USER05: "LOCAL",
ROLE_USER03: "ENV",
ROLE_USER04: "ENV",
ROLE_USER01: "KECO",
};
@@ -35,7 +35,6 @@ import {
type InstlSncnViewButtonAction,
needsRoleUser01AtrzResolve,
} from "../../lib/button-config";
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
import {
buildSavePayload,
buildSupplementReqPayload,
@@ -179,11 +178,6 @@ function FloatingActionButtons({
}
async function handleAplyClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"LOCAL",
);
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
data,
checklistFormState,
@@ -191,11 +185,6 @@ function FloatingActionButtons({
"ST",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
}
await executeAction({
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
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() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
@@ -284,16 +257,6 @@ function FloatingActionButtons({
}
async function handleRvwRplyClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"ENV",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
return;
}
const payload = buildTechReviewReplyPayload(
data,
@@ -369,16 +332,6 @@ function FloatingActionButtons({
}
async function handleRvwCmptnClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"KECO",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
return;
}
const payload = {
...buildTechReviewReplyPayload(
@@ -447,11 +400,6 @@ function FloatingActionButtons({
설치인가 고시
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn) && (
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
환경청제출
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rjct) && (
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
반려
@@ -618,7 +566,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
aply: handleSaveApply,
del: handleDel,
instlSncnAncmnt: handleChangePrgrsSttsComCd,
envoSbmsn: handleChangePrgrsSttsComCd,
rjct: handleChangePrgrsSttsComCd,
sncnAprv: handleChangePrgrsSttsComCd,
rvwRply: handleTechReview,
@@ -20,9 +20,7 @@ import {
AttachmentsFormSection,
BusinessCostFormSection,
BusinessScaleFormSection,
ChecklistFormSection,
ContactInfoFormSection,
isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection,
ProgressStatusSection,
@@ -154,11 +152,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
return;
}
if (!isFormChecklistComplete(data.cklstResultList)) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
}
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
return;
@@ -228,7 +221,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
<PrivacyConsentSection />
<BusinessScaleFormSection />
<BusinessCostFormSection />
<ChecklistFormSection docSeComCd="INSTL" fieldPrefix="instlSncnMng" />
<AttachmentsFormSection
docSeComCd="INSTL"
fieldPrefix="instlSncnMng"
@@ -23,7 +23,6 @@ import { FloatingActionButtons } from "./components/floating-action-buttons";
import { BasicInfoSection } from "./sections/basic-info-section";
import { BusinessCostSection } from "./sections/business-cost-section";
import { BusinessScaleSection } from "./sections/business-scale-section";
import { ChecklistSection } from "./sections/checklist-section";
import { ContactInfoSection } from "./sections/contact-info-section";
import {
TechReviewOpinionSection,
@@ -38,8 +37,9 @@ function InstlViewPage({ id }: InstlViewPageProps) {
const userInfo = useUserStore((state) => state.userInfo);
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
const { data, isLoading, isError } = useInstlSncnDetail(id);
const { formState: checklistFormState, setFormState: setChecklistFormState } =
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
const { formState: checklistFormState } = useChecklistFormState(
data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST,
);
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
@@ -79,7 +79,6 @@ function InstlViewPage({ id }: InstlViewPageProps) {
detDto,
bizSclList,
bizCstList,
cklstResultList = [],
esntlFileList,
etcFileList,
rvwList = [],
@@ -109,13 +108,6 @@ function InstlViewPage({ id }: InstlViewPageProps) {
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
<BusinessScaleSection bizSclList={bizSclList} />
<BusinessCostSection bizCstList={bizCstList} />
<ChecklistSection
cklstResultList={cklstResultList}
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
userRoleCode={userInfo?.dtlInstCd}
formState={checklistFormState}
setFormState={setChecklistFormState}
/>
<AttachmentsViewSection
esntlFileList={esntlFileList ?? []}
etcFileList={etcFileList ?? []}
@@ -2,18 +2,25 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
import { attachService } from "@/entities/attach";
import {
instlSncnKeys,
type InstlSncnRvwOpnnResponseDto,
instlSncnRvwOpnnService,
type ReviewStepCode,
} from "@/entities/instl-sncn";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
@@ -171,19 +178,29 @@ const TechReviewOpinionSection = forwardRef<
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return false;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
return true;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return null;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
<TableHead scope="row" className="w-[12rem]">
의견내용
</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
@@ -281,7 +298,9 @@ const TechReviewOpinionSection = forwardRef<
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
@@ -289,7 +308,9 @@ const TechReviewOpinionSection = forwardRef<
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
@@ -298,52 +319,31 @@ const TechReviewOpinionSection = forwardRef<
);
});
function getRvwOpnnIds(item: InstlSncnRvwOpnnResponseDto) {
if (!item.instlSncnDocNo || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.instlSncnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.instlSncnDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.instlSncnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await instlSncnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
}}
onDelete={async (item) => {
await instlSncnRvwOpnnService.delete(...getRvwOpnnIds(item));
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
}}
/>
) : (
"-"
)}
</TableCell>
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
@@ -2,18 +2,24 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { SewerBscPlanRvwOpnnResponseDto } from "@/entities/sewer-bsc-plan";
import { attachService } from "@/entities/attach";
import {
sewerBscPlanKeys,
type SewerBscPlanRvwOpnnResponseDto,
sewerBscPlanRvwOpnnService,
} from "@/entities/sewer-bsc-plan";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
@@ -178,16 +184,24 @@ const TechReviewOpinionSection = forwardRef<
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return false;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
return true;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return null;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead className="w-[12rem]">의견내용</TableHead>
@@ -288,7 +302,9 @@ const TechReviewOpinionSection = forwardRef<
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
@@ -296,7 +312,9 @@ const TechReviewOpinionSection = forwardRef<
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
@@ -305,52 +323,31 @@ const TechReviewOpinionSection = forwardRef<
);
});
function getRvwOpnnIds(item: SewerBscPlanRvwOpnnResponseDto) {
if (!item.sewerBscPlanDocNo || item.chgCycl == null || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.sewerBscPlanDocNo, item.chgCycl, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await sewerBscPlanRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
}}
onDelete={async (item) => {
await sewerBscPlanRvwOpnnService.delete(...getRvwOpnnIds(item));
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
}}
/>
) : (
"-"
)}
</TableCell>
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
+3
View File
@@ -12,11 +12,13 @@ export const ENDPOINT = {
RVW_MNPW: "/as-service/api/v1/rvw-mnpw",
BNS_IF: "/as-service/api/v1/if",
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_ARTCL: "/as-service/api/v1/cklst-artcl",
CKLST_RVW: "/as-service/api/v1/cklst-rvw",
ADSTDSTR: "/as-service/api/v1/adstdstr",
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",
DESIGN_CNSTN: "/as-service/api/v1/design-cnstn",
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_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
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",
DCRAG_TO: "/as-service/api/v1/dcrag-to",
AI_AUTO_PRCS_MNG: "/as-service/api/v1/ai-auto-prcs-mng",
+1
View File
@@ -46,6 +46,7 @@ export type {
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
RvwOpnnSaveBase,
RvwOpnnUpdateRequestDto,
SplmntDmndResponseExtension,
SplmntDmndSaveBase,
} from "./pbadms-sprt";
+11
View File
@@ -231,11 +231,22 @@ export interface PbadmsApplySaveHeader {
rvwStpCd?: string;
}
/** 검토의견 수정 공통 요청 DTO */
export interface RvwOpnnUpdateRequestDto {
opnrSeComCd?: string;
opnrNm?: string;
rvwRsltCn?: string;
rvwRsltAnmlyYn?: string;
atchFileGroupId?: string;
}
/** 검토의견 응답 공통 확장 필드 */
export interface RvwOpnnResponseExtension {
opnrSeCdNm?: string;
atchFileInfo?: AtflMngClientResponseDto;
regDt?: string;
rgtr?: string;
rgtrNm?: string;
}
/** 보완요구 응답 공통 확장 필드 */
+1
View File
@@ -51,6 +51,7 @@ export {
} from "./ui/pbadms-task-apply-dialog";
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
export { ProgressStatusSection } from "./ui/progress-status-section";
export { RvwOpnnListTable, type RvwOpnnListTableProps } from "./ui/rvw-opnn-list-table";
export {
SplmntDmndDialog,
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,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable,
submitList,
deleteList: deleteListForSubmit,
@@ -797,7 +797,6 @@ const PrivateSewageFacilityInp = () => {
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "",
@@ -842,7 +841,6 @@ const PrivateSewageFacilityInp = () => {
...submitInfoBase,
userUuid: userInfo.userUnqId,
submitList: buildSewageTreatFactStatusSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus(
@@ -853,7 +851,6 @@ const PrivateSewageFacilityInp = () => {
...submitInfoBase,
userUuid: userInfo.userUnqId,
submitList: buildSepticTankStatusSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param);
@@ -862,7 +859,6 @@ const PrivateSewageFacilityInp = () => {
...submitInfoBase,
userUuid: userInfo.userUnqId,
submitList: buildPrivateSewageDesigStatusSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus(
@@ -768,7 +768,6 @@ const PumpingStationInp = () => {
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable,
submitList,
deleteList,
@@ -1410,7 +1410,6 @@ const SewageExcretaRelatedInp = () => {
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
...(isPublicInstitutionTab ? { noApplicable } : {}),
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
};
const verificationQuery = {
@@ -1357,11 +1357,6 @@ const SewageExcretaSlurgeInp = () => {
: rowsForSubmit.map((row) =>
buildExcretaSlurgeProcessingStatusSaveItem(row, list),
);
const slurgeDeleteList = isSlurgeFacTab
? (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list))
: [];
const sewageSlurgeFacDeleteList = isSewageSlurgeFacTab
? (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
@@ -1432,7 +1427,6 @@ const SewageExcretaSlurgeInp = () => {
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
noApplicable,
};
@@ -1445,7 +1439,6 @@ const SewageExcretaSlurgeInp = () => {
...submitRequestBase,
userUuid: userInfo.userUnqId,
submitList: slurgeSubmitList,
deleteList: slurgeDeleteList,
})
: isDigestorTab
? await statisticsInputSewageExcretaSlurgeApi.submitDigestorOperatingStatus({
@@ -1655,9 +1648,6 @@ const SewageExcretaSlurgeInp = () => {
: rowsForSave.map((row) =>
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)) {
alert("해당사항 없음을 체크해주세요.");
@@ -1670,7 +1660,6 @@ const SewageExcretaSlurgeInp = () => {
...baseSaveRequest,
...userFields,
saveList,
deleteList,
},
);
} else if (tabKey === DigestorOperatingStatusInpColumn.tabKey) {
@@ -350,7 +350,6 @@ function buildRegionalFactSubmitListFromGrid(
};
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: nullableTrimmedString(d.goalYr),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
@@ -1264,7 +1263,6 @@ const SewagePipeInp = () => {
};
const submitList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
@@ -1637,7 +1635,6 @@ const SewagePipeInp = () => {
const saveList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: nullableTrimmedString(d.goalYr),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
@@ -1728,7 +1725,6 @@ const SewagePipeInp = () => {
const saveList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
@@ -218,7 +218,6 @@ function buildRepairRowList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
dfrpWholPrlg: formatNumber(d.dfrpWholPrlg, "dfrpWholPrlg"),
cbsysDfrpPrlg: formatNumber(d.cbsysDfrpPrlg, "cbsysDfrpPrlg"),
@@ -247,7 +246,6 @@ function buildDredgingRowList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"),
@@ -215,7 +215,6 @@ function buildSewerageFinanceSettleSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
soaGovSprtAmt: formatNumber(d.soaGovSprtAmt, "soaGovSprtAmt"),
mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"),
@@ -257,7 +256,6 @@ function buildSewerageChargeSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"),
sewerLevyAmt: formatNumber(d.sewerLevyAmt, "sewerLevyAmt"),
@@ -276,7 +274,6 @@ function buildSewerageUsageFeeSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"),
hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"),
@@ -312,7 +309,6 @@ function buildCauserBurdenSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
brdnAmtLevyNocs: formatNumber(d.brdnAmtLevyNocs, "brdnAmtLevyNocs"),
brdnAmtLevyAmt: formatNumber(d.brdnAmtLevyAmt, "brdnAmtLevyAmt"),
@@ -849,7 +845,6 @@ const SewerageFinanceInp = () => {
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "",
@@ -894,7 +889,6 @@ const SewerageFinanceInp = () => {
...submitInfoBase,
userUuid: userInfo.userUnqId,
submitList: buildSewerageFinanceSettleSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputSewerageFinanceApi.submitSewerageFinanceSettle(
@@ -905,7 +899,6 @@ const SewerageFinanceInp = () => {
...submitInfoBase,
userUuid: userInfo.userUnqId,
submitList: buildSewerageChargeSaveList(currentData),
...adminOpt,
};
response = await statisticsInputSewerageFinanceApi.submitSewerageCharge(param);
} else if (isUsageFeeTab) {
@@ -913,7 +906,6 @@ const SewerageFinanceInp = () => {
...submitInfoBase,
userUuid: userInfo.userUnqId,
submitList: buildSewerageUsageFeeSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputSewerageFinanceApi.submitSewerageUsageFee(param);
@@ -922,7 +914,6 @@ const SewerageFinanceInp = () => {
...submitInfoBase,
userUuid: userInfo.userUnqId,
submitList: buildCauserBurdenSaveList(currentData),
...adminOpt,
};
response = await statisticsInputSewerageFinanceApi.submitCauserBurden(param);
}
@@ -399,10 +399,6 @@ const SeweragePenetrationRateInp = () => {
statsCd: statsCd,
};
if (isAdminInfo) {
params.superAdmin = 'superAdmin';
}
try {
setIsLoading(true);
@@ -498,7 +494,6 @@ const SeweragePenetrationRateInp = () => {
};
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
sggNm: nullableTrimmedString(d.sggNm),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm),
@@ -677,8 +672,6 @@ const SeweragePenetrationRateInp = () => {
};
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
// 행정구역
sggNm: nullableTrimmedString(d.sggNm), // 시군구
admdstCd: nullableTrimmedString(d.admdstCd), // 행정코드
admdstNm: nullableTrimmedString(d.admdstNm), // 행정구역명
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm), // 지류
@@ -808,13 +808,11 @@ const SewerageServiceLevelInp = () => {
? await statisticsInputSewerageServiceLevelApi.submitComplaintList({
...submitInfoBase,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
submitList: complaintSubmitList,
})
: await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({
...submitInfoBase,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable,
submitList: residentSubmitList,
});
@@ -1153,7 +1153,6 @@ const WaterReuseInp = () => {
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
let articleSggOptions: Sgg[] | null = null;
@@ -27,8 +27,8 @@ export interface VrfcReportSelect {
statsNm: string;
rsnExpln: string;
admdstNm: string;
nowVlCn: string;
compVlCn: string;
nowCN: string;
cmprCn: string | null;
}
export interface VrfcErrCountResponse {
@@ -117,7 +117,7 @@ export type SlurgeProcessingFacStatusApiResponse =
};
/**
* ST00110001 저장 — `saveList` / `deleteList` 항목.
* ST00110001 저장 — `saveList` 항목.
* `sggNm`·계산필드·검증결과는 저장 요청에서 제외.
*/
export type SlurgeProcessingFacStatusSaveItem = Omit<
@@ -167,7 +167,6 @@ export interface SlurgeProcessingFacStatusSaveRequest {
superAdmin?: string;
noApplicable?: boolean;
saveList: SlurgeProcessingFacStatusSaveItem[];
deleteList: SlurgeProcessingFacStatusSaveItem[];
}
export type SlurgeProcessingFacStatusSaveResponse = ApiResponse<unknown> & {
@@ -194,7 +193,6 @@ export interface SlurgeProcessingFacStatusSubmitRequest {
superAdmin?: string;
noApplicable?: boolean;
submitList: SlurgeProcessingFacStatusSubmitItem[];
deleteList: SlurgeProcessingFacStatusSubmitItem[];
}
export interface SlurgeProcessingFacStatusSubmitResponse {
@@ -98,7 +98,6 @@ export type SewagePipeMaintainDredgingResponse = ApiResponse<SewagePipeMaintainD
/** ST00030001 하수관로 유지관리 > 개보수 저장·제출 API 행 */
export interface SewagePipeMaintainRepairSaveItem {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
dfrpWholPrlg: number;
cbsysDfrpPrlg: number;
@@ -167,7 +166,6 @@ export interface SewagePipeMaintainRepairSubmitResponse {
/** ST00030002 하수관로 유지관리 > 관로준설 저장·제출 API 행 */
export interface SewagePipeMaintainDredgingSaveItem {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
wholFcltPrlg: number;
wholSwagePpl: number;
@@ -92,7 +92,6 @@ export interface SewerageFinanceSettleRequestParams {
*/
export interface SewerageFinanceSettleSaveItem {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
soaGovSprtAmt: number;
mctpvLcexpAmt: number;
@@ -255,7 +254,6 @@ export interface SewerageChargeRequestParams {
*/
export interface SewerageChargeSaveItem {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
anlLevyQnt: number;
sewerLevyAmt: number;
@@ -362,7 +360,6 @@ export interface SewerageUsageFeeRequestParams {
*/
export interface SewerageUsageFeeSaveItem {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
hshusLevyAmt: number;
hshusLevyQnt: number;
@@ -472,7 +469,6 @@ export interface CauserBurdenRequestParams {
*/
export interface CauserBurdenSaveItem {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
brdnAmtLevyNocs: number;
brdnAmtLevyAmt: number;
@@ -99,7 +99,6 @@ export type SeweragePipeInputRegionalFactStatusSaveItem = Omit<
"verificationResult" | "admdstCd" | "sggNm" | "admdstNm" | "goalYr"
> & {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
goalYr: string | null;
};
@@ -118,7 +117,6 @@ export type SeweragePipeInputFactInstallStatusSaveItem = Omit<
| "rmrk"
> & {
admdstCd: string | null;
sggNm: string | null;
admdstNm: string | null;
fcltSn: string | null;
swtrmtFcltCd: string | null;
@@ -86,7 +86,6 @@ export interface SewerageSupplyRateRequestParams {
/** 하수도 보급률 저장 API 요청 body (한 행) */
export interface SewerageSupplyRateSaveItem {
sggNm: string | null;
admdstCd: string | null;
admdstNm: string | null;
statsTrbtrNm: string | null;
@@ -113,10 +113,10 @@ export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
현재: {item.nowVlCn ?? "-"}{" "}
{/* TBD: 증감 표시 방법 (nowCN - cmprCn 계산?) */}
현재: {item.nowCN ?? "-"}{" "}
/ 비교:{" "}
{item.compVlCn ?? "-"}
{item.cmprCn ?? "-"}
</span>
</div>
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
@@ -16,6 +16,7 @@ function StatisticsPageLayout({ children }: { children: React.ReactNode }) {
<TabBar />
<PageTitle
title={pageTitle?.label ?? ""}
menuCd={pageTitle?.menuItem?.menuCd}
menuPaths={menuPaths ?? []}
/>
{children}