Compare commits
8
Commits
8a98cd7d19
...
26d8410649
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d8410649 | ||
|
|
8403ad556c | ||
|
|
d26c15f857 | ||
|
|
86ed664e3e | ||
|
|
a105f395dd | ||
|
|
a5ebc12580 | ||
|
|
270f6e366f | ||
|
|
acf215c1bf |
@@ -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>>;
|
||||
}
|
||||
|
||||
+1
-3
@@ -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 {
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
-4
@@ -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;
|
||||
|
||||
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user