Merge branch 'main' into feature/1007_forkjoin

This commit is contained in:
2026-10-21 11:43:24 +09:00
30 changed files with 403 additions and 544 deletions
@@ -50,7 +50,6 @@ export interface TechReviewOpinionSectionHandle {
interface TechReviewOpinionTableProps {
items: DesignCnstnRvwOpnnResponseDto[];
isReviewTurn: boolean;
}
interface UploadedOpinionFile {
@@ -296,7 +295,7 @@ const TechReviewOpinionSection = forwardRef<
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} isReviewTurn={isReviewTurn("KECO")} />
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
@@ -311,7 +310,7 @@ function getRvwOpnnIds(item: DesignCnstnRvwOpnnResponseDto) {
return [item.designCnstnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableProps) {
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
@@ -320,7 +319,6 @@ function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableP
items={items}
getRowKey={(item) => `${item.designCnstnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
isReviewTurn={isReviewTurn}
onUpdate={async (item, body) => {
await designCnstnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
@@ -48,7 +48,6 @@ export interface TechReviewOpinionSectionHandle {
interface TechReviewOpinionTableProps {
items: FnrsCnsltnRvwOpnnResponseDto[];
isReviewTurn: boolean;
}
interface UploadedOpinionFile {
@@ -292,7 +291,7 @@ const TechReviewOpinionSection = forwardRef<
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} isReviewTurn={isReviewTurn("ENV")} />
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
@@ -302,7 +301,7 @@ const TechReviewOpinionSection = forwardRef<
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} isReviewTurn={isReviewTurn("KECO")} />
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{/*
재원협의에서 환경공단 검토의견은 자동 생성
@@ -322,7 +321,7 @@ function getRvwOpnnIds(item: FnrsCnsltnRvwOpnnResponseDto) {
return [item.fnrsCnsltnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableProps) {
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
@@ -331,7 +330,6 @@ function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableP
items={items}
getRowKey={(item) => `${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
isReviewTurn={isReviewTurn}
onUpdate={async (item, body) => {
await fnrsCnsltnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
@@ -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,14 +32,16 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
],
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
},
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],
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
},
// 환경청 단계 제거: 기존 환경청(LE·TP) 기능을 광역자치단체가 수행
ROLE_USER04: {
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],
},
};
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
@@ -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"
@@ -1,149 +1,141 @@
"use client";
import { useMemo, useRef } from "react";
import { useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useInstlSncnDetail } from "@/entities/instl-sncn";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import {
AttachmentsViewSection,
ProgressStatusSection,
SupplementDemandSection,
} from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
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,
type TechReviewOpinionSectionHandle,
} from "./sections/tech-review-opinion-section";
interface InstlViewPageProps {
id: string;
}
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 prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
{},
{ enabled: needsAtrzResolve },
);
const atrzSeComCdList = useMemo(
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
[rvwLoginUserData],
);
if (isLoading) {
return (
<div className="mt-6 flex flex-col gap-4 px-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[32rem] rounded-xl" />
</div>
</div>
);
}
if (isError || !data) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const {
detDto,
bizSclList,
bizCstList,
cklstResultList = [],
esntlFileList,
etcFileList,
rvwList = [],
splmntDmndList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
userInfo?.dtlInstCd,
detDto.prgrsSttsComCd || "",
);
return (
<div className="mt-4">
<SectionCollapsibleGroup>
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
<SectionCollapsibleToggle />
</div>
<div className="space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.instlSncnDocNo}
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<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 ?? []}
/>
<TechReviewOpinionSection
ref={techReviewOpinionRef}
rvwList={rvwList}
editableReviewStep={editableReviewStep}
atrzSeComCdList={atrzSeComCdList}
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
/>
<SupplementDemandSection splmntDmndList={splmntDmndList} />
<FloatingActionButtons
data={data}
docSeComCd="INSTL"
instlSncnDocNo={detDto.instlSncnDocNo}
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
dtlInstCd={userInfo?.dtlInstCd}
atrzSeComCdList={atrzSeComCdList}
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
checklistFormState={checklistFormState}
techReviewOpinionRef={techReviewOpinionRef}
opnrNm={userInfo?.userName}
/>
</div>
</SectionCollapsibleGroup>
</div>
);
}
export { InstlViewPage };
"use client";
import { useMemo, useRef } from "react";
import { useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useInstlSncnDetail } from "@/entities/instl-sncn";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import {
AttachmentsViewSection,
ProgressStatusSection,
SupplementDemandSection,
} from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
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 { ContactInfoSection } from "./sections/contact-info-section";
import {
TechReviewOpinionSection,
type TechReviewOpinionSectionHandle,
} from "./sections/tech-review-opinion-section";
interface InstlViewPageProps {
id: string;
}
function InstlViewPage({ id }: InstlViewPageProps) {
const userInfo = useUserStore((state) => state.userInfo);
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
const { data, isLoading, isError } = useInstlSncnDetail(id);
const { formState: checklistFormState } = useChecklistFormState(
data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST,
);
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
{},
{ enabled: needsAtrzResolve },
);
const atrzSeComCdList = useMemo(
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
[rvwLoginUserData],
);
if (isLoading) {
return (
<div className="mt-6 flex flex-col gap-4 px-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[32rem] rounded-xl" />
</div>
</div>
);
}
if (isError || !data) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const {
detDto,
bizSclList,
bizCstList,
esntlFileList,
etcFileList,
rvwList = [],
splmntDmndList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
userInfo?.dtlInstCd,
detDto.prgrsSttsComCd || "",
);
return (
<div className="mt-4">
<SectionCollapsibleGroup>
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
<SectionCollapsibleToggle />
</div>
<div className="space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.instlSncnDocNo}
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
<BusinessScaleSection bizSclList={bizSclList} />
<BusinessCostSection bizCstList={bizCstList} />
<AttachmentsViewSection
esntlFileList={esntlFileList ?? []}
etcFileList={etcFileList ?? []}
/>
<TechReviewOpinionSection
ref={techReviewOpinionRef}
rvwList={rvwList}
editableReviewStep={editableReviewStep}
atrzSeComCdList={atrzSeComCdList}
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
/>
<SupplementDemandSection splmntDmndList={splmntDmndList} />
<FloatingActionButtons
data={data}
docSeComCd="INSTL"
instlSncnDocNo={detDto.instlSncnDocNo}
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
dtlInstCd={userInfo?.dtlInstCd}
atrzSeComCdList={atrzSeComCdList}
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
checklistFormState={checklistFormState}
techReviewOpinionRef={techReviewOpinionRef}
opnrNm={userInfo?.userName}
/>
</div>
</SectionCollapsibleGroup>
</div>
);
}
export { InstlViewPage };
@@ -47,7 +47,6 @@ export interface TechReviewOpinionSectionHandle {
interface TechReviewOpinionTableProps {
items: InstlSncnRvwOpnnResponseDto[];
isReviewTurn: boolean;
}
interface UploadedOpinionFile {
@@ -300,7 +299,7 @@ const TechReviewOpinionSection = forwardRef<
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} isReviewTurn={isReviewTurn("ENV")} />
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
@@ -310,7 +309,7 @@ const TechReviewOpinionSection = forwardRef<
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} isReviewTurn={isReviewTurn("KECO")} />
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
@@ -327,7 +326,7 @@ function getRvwOpnnIds(item: InstlSncnRvwOpnnResponseDto) {
return [item.instlSncnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableProps) {
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
@@ -336,7 +335,6 @@ function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableP
items={items}
getRowKey={(item) => `${item.instlSncnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
isReviewTurn={isReviewTurn}
onUpdate={async (item, body) => {
await instlSncnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
@@ -50,7 +50,6 @@ export interface TechReviewOpinionSectionHandle {
interface TechReviewOpinionTableProps {
items: SewerBscPlanRvwOpnnResponseDto[];
isReviewTurn: boolean;
}
interface UploadedOpinionFile {
@@ -304,7 +303,7 @@ const TechReviewOpinionSection = forwardRef<
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} isReviewTurn={isReviewTurn("ENV")} />
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
@@ -314,7 +313,7 @@ const TechReviewOpinionSection = forwardRef<
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} isReviewTurn={isReviewTurn("KECO")} />
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
@@ -331,7 +330,7 @@ function getRvwOpnnIds(item: SewerBscPlanRvwOpnnResponseDto) {
return [item.sewerBscPlanDocNo, item.chgCycl, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableProps) {
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
@@ -340,7 +339,6 @@ function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableP
items={items}
getRowKey={(item) => `${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
isReviewTurn={isReviewTurn}
onUpdate={async (item, body) => {
await sewerBscPlanRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
@@ -45,8 +45,6 @@ interface RvwOpnnListTableProps<T extends RvwOpnnListItem> {
getRowKey: (item: T) => string;
/** 로그인 사용자 UUID — 검토의견 등록자(rgtr)와 같을 때만 수정·삭제 버튼 노출 */
currentUserUuid?: string;
/** 현재 검토 단계가 로그인 사용자 차례인지 여부 — false면 등록자라도 수정·삭제 불가 */
isReviewTurn?: boolean;
onUpdate: (item: T, body: RvwOpnnUpdateRequestDto) => Promise<void>;
onDelete: (item: T) => Promise<void>;
}
@@ -76,7 +74,6 @@ function RvwOpnnListTable<T extends RvwOpnnListItem>({
items,
getRowKey,
currentUserUuid,
isReviewTurn = false,
onUpdate,
onDelete,
}: RvwOpnnListTableProps<T>) {
@@ -102,7 +99,7 @@ function RvwOpnnListTable<T extends RvwOpnnListItem>({
const editFileUploading = editFile ? (isUploading[editFile.fileKey] ?? false) : false;
function canModify(item: T) {
return isReviewTurn && Boolean(currentUserUuid) && item.rgtr === currentUserUuid;
return Boolean(currentUserUuid) && item.rgtr === currentUserUuid;
}
async function discardNewFile(file: EditFile | null) {
@@ -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;
@@ -1,100 +1,100 @@
export interface DashboardSearchParams {
statsYr: string;
dtlInstCd: string;
enoCd?: string;
laeSeCd?: string;
sewerBzmnCd?: string;
sido?: string;
sggNm?: string;
unSubmitOnly?: boolean;
}
export interface DashboardApiResponse<T> {
status: string;
message: string;
data: T;
code: string;
httpStatus: number;
}
export interface VrfcReportResponse {
typeCounts: Record<string, number>;
list: VrfcReportSelect[];
}
export interface VrfcReportSelect {
vrfcCd: string;
statsNm: string;
rsnExpln: string;
admdstNm: string;
nowVlCn: string;
compVlCn: string;
}
export interface VrfcErrCountResponse {
statsYr: string;
vrfcErrCount: number;
}
export interface UnSubmitCountResponse {
statsYr: string;
statusList: UnSubmitStatsResponse[];
submittedCount: number;
totalCount: number;
unSubmittedCount: number;
}
export interface UnSubmitStatsResponse {
sbmsnYn: string;
sidoSggNm: string;
statsCd: string;
statsNm: string;
statusCd: string;
statusNm: string;
}
export interface StatsSubmissionResponse {
totalCount: number;
submittedCount: number;
submissionRate: number;
statusLevel: string;
}
export interface EcptReqCountResponse {
statsYr: string;
ecptReqCount: number;
}
export interface ThreeStageStatusResponse {
statsYr: string;
lclgvSbmsnSttsCd: string;
lclgvSbmsnSttsNm: string;
lclgvRejected: boolean;
mctpvPicSbmsnSttsCd: string;
mctpvPicSbmsnSttsNm: string;
mctpvRejected: boolean;
pbcpSbmsnSttsCd: string;
pbcpSbmsnSttsNm: string;
pbcpRejected: boolean;
}
export interface DashboardService {
getVrfcReport(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcReportResponse>>;
getVrfcErrCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
getUnSubmitCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
getStatsSubmissionRate(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
getEcptReqCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
getThreeStageStatus(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<ThreeStageStatusResponse>>;
}
export interface DashboardSearchParams {
statsYr: string;
dtlInstCd: string;
enoCd?: string;
laeSeCd?: string;
sewerBzmnCd?: string;
sido?: string;
sggNm?: string;
unSubmitOnly?: boolean;
}
export interface DashboardApiResponse<T> {
status: string;
message: string;
data: T;
code: string;
httpStatus: number;
}
export interface VrfcReportResponse {
typeCounts: Record<string, number>;
list: VrfcReportSelect[];
}
export interface VrfcReportSelect {
vrfcCd: string;
statsNm: string;
rsnExpln: string;
admdstNm: string;
nowCN: string;
cmprCn: string | null;
}
export interface VrfcErrCountResponse {
statsYr: string;
vrfcErrCount: number;
}
export interface UnSubmitCountResponse {
statsYr: string;
statusList: UnSubmitStatsResponse[];
submittedCount: number;
totalCount: number;
unSubmittedCount: number;
}
export interface UnSubmitStatsResponse {
sbmsnYn: string;
sidoSggNm: string;
statsCd: string;
statsNm: string;
statusCd: string;
statusNm: string;
}
export interface StatsSubmissionResponse {
totalCount: number;
submittedCount: number;
submissionRate: number;
statusLevel: string;
}
export interface EcptReqCountResponse {
statsYr: string;
ecptReqCount: number;
}
export interface ThreeStageStatusResponse {
statsYr: string;
lclgvSbmsnSttsCd: string;
lclgvSbmsnSttsNm: string;
lclgvRejected: boolean;
mctpvPicSbmsnSttsCd: string;
mctpvPicSbmsnSttsNm: string;
mctpvRejected: boolean;
pbcpSbmsnSttsCd: string;
pbcpSbmsnSttsNm: string;
pbcpRejected: boolean;
}
export interface DashboardService {
getVrfcReport(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcReportResponse>>;
getVrfcErrCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
getUnSubmitCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
getStatsSubmissionRate(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
getEcptReqCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
getThreeStageStatus(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<ThreeStageStatusResponse>>;
}
@@ -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;
@@ -1,133 +1,133 @@
"use client";
import { useDashboardVrfcReport } from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import {
Badge,
type BadgeProps,
LoadingWithAnimation,
} from "@common/components";
import { useDragHandle } from "../sortable-panel";
interface VrfcReportPanelProps {
params: DashboardSearchParams | null;
}
const TYPE_CODE_NAME_MAP: Record<string, string> = {
NV: "일반",
IMR: "I-MR",
HB: "HB",
};
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
NV: "gray",
IMR: "red",
HB: "blue",
};
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useDashboardVrfcReport(params);
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="flex h-full items-center justify-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<div className="space-y-2">
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
{data.typeCounts &&
Object.keys(data.typeCounts).length > 0 && (
<div className="flex flex-wrap gap-2">
{Object.entries(data.typeCounts).map(
([type, count]) => (
<Badge
key={type}
color={
TYPE_CODE_COLOR_MAP[type]
}
className="rounded-full px-2 py-1 text-lg"
>
{TYPE_CODE_NAME_MAP[type]}:{" "}
{count}
</Badge>
),
)}
</div>
)}
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
<div className="max-h-[44rem] overflow-y-auto">
{data.list.length === 0 ? (
<div className="flex h-full items-center justify-center">
이상치 항목이 없습니다.
</div>
) : (
<div className="space-y-2 tracking-tight">
{data.list.map((item, index) => (
<div
key={index}
className="flex flex-col rounded-sm border border-border p-3"
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
</span>
<Badge
color={
TYPE_CODE_COLOR_MAP[
item.vrfcCd
]
}
className="text-sm"
>
{
TYPE_CODE_NAME_MAP[
item.vrfcCd
]
}
</Badge>
</div>
<p className="text-lg text-muted-foreground">
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
현재: {item.nowVlCn ?? "-"}{" "}
/ 비교:{" "}
{item.compVlCn ?? "-"}
</span>
</div>
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}
"use client";
import { useDashboardVrfcReport } from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import {
Badge,
type BadgeProps,
LoadingWithAnimation,
} from "@common/components";
import { useDragHandle } from "../sortable-panel";
interface VrfcReportPanelProps {
params: DashboardSearchParams | null;
}
const TYPE_CODE_NAME_MAP: Record<string, string> = {
NV: "일반",
IMR: "I-MR",
HB: "HB",
};
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
NV: "gray",
IMR: "red",
HB: "blue",
};
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useDashboardVrfcReport(params);
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="flex h-full items-center justify-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<div className="space-y-2">
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
{data.typeCounts &&
Object.keys(data.typeCounts).length > 0 && (
<div className="flex flex-wrap gap-2">
{Object.entries(data.typeCounts).map(
([type, count]) => (
<Badge
key={type}
color={
TYPE_CODE_COLOR_MAP[type]
}
className="rounded-full px-2 py-1 text-lg"
>
{TYPE_CODE_NAME_MAP[type]}:{" "}
{count}
</Badge>
),
)}
</div>
)}
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
<div className="max-h-[44rem] overflow-y-auto">
{data.list.length === 0 ? (
<div className="flex h-full items-center justify-center">
이상치 항목이 없습니다.
</div>
) : (
<div className="space-y-2 tracking-tight">
{data.list.map((item, index) => (
<div
key={index}
className="flex flex-col rounded-sm border border-border p-3"
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
</span>
<Badge
color={
TYPE_CODE_COLOR_MAP[
item.vrfcCd
]
}
className="text-sm"
>
{
TYPE_CODE_NAME_MAP[
item.vrfcCd
]
}
</Badge>
</div>
<p className="text-lg text-muted-foreground">
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
{/* TBD: 증감 표시 방법 (nowCN - cmprCn 계산?) */}
현재: {item.nowCN ?? "-"}{" "}
/ 비교:{" "}
{item.cmprCn ?? "-"}
</span>
</div>
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}