Merge branch 'iipbs/as/dev'
This commit is contained in:
@@ -1,17 +1,13 @@
|
|||||||
import Link from "next/link";
|
import { SewerBscPlanViewPage } from "@/pages/sewer-bsc-plan";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
|
searchParams: Promise<{ chgCycl?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params }: PageProps) {
|
export default async function Page({ params, searchParams }: PageProps) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
return (
|
const { chgCycl } = await searchParams;
|
||||||
<div className="mt-6 px-4 text-[1.4rem]">
|
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
|
||||||
하수도기본계획 상세({id})는 후속 구현 예정입니다.{" "}
|
return <SewerBscPlanViewPage id={id} propChgCycl={propChgCycl} />;
|
||||||
<Link className="underline" href={`/sewer-bsc-plan/${id}/edit`}>
|
|
||||||
수정 화면으로 이동
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export interface ElaprPrgrsSttsMyDocSearchParams extends ElaprPrgrsSttsSearchDto
|
|||||||
export interface ElaprProcessRequestDto {
|
export interface ElaprProcessRequestDto {
|
||||||
atrzPrgrsSttsComCd: string;
|
atrzPrgrsSttsComCd: string;
|
||||||
rjctRsn?: string;
|
rjctRsn?: string;
|
||||||
finalApprovalYn?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 백엔드 ElaprApproverChangeRequestDto */
|
/** 백엔드 ElaprApproverChangeRequestDto */
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { ENDPOINT } from "@/shared/config";
|
|||||||
import type {
|
import type {
|
||||||
SewerBscPlanApplySaveRequestDto,
|
SewerBscPlanApplySaveRequestDto,
|
||||||
SewerBscPlanSaveRequestDto,
|
SewerBscPlanSaveRequestDto,
|
||||||
|
} from "../model/sewer-bsc-plan-apply-payload-schema";
|
||||||
|
import type {
|
||||||
SewerBscPlanSearchRequestDto,
|
SewerBscPlanSearchRequestDto,
|
||||||
SewerBscPlanUpdateRequestDto,
|
SewerBscPlanUpdateRequestDto,
|
||||||
} from "../model/sewer-bsc-plan-interface";
|
} from "../model/sewer-bsc-plan-interface";
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import type { PageResponse } from "@/shared/model";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
SewerBscPlanApplySaveRequestDto,
|
SewerBscPlanApplySaveRequestDto,
|
||||||
|
SewerBscPlanSaveRequestDto,
|
||||||
|
} from "../model/sewer-bsc-plan-apply-payload-schema";
|
||||||
|
import type {
|
||||||
SewerBscPlanApplySaveResponseDto,
|
SewerBscPlanApplySaveResponseDto,
|
||||||
SewerBscPlanDetResponseDto,
|
SewerBscPlanDetResponseDto,
|
||||||
SewerBscPlanResponseDto,
|
SewerBscPlanResponseDto,
|
||||||
SewerBscPlanSaveRequestDto,
|
|
||||||
SewerBscPlanSearchRequestDto,
|
SewerBscPlanSearchRequestDto,
|
||||||
SewerBscPlanService,
|
SewerBscPlanService,
|
||||||
SewerBscPlanUpdateRequestDto,
|
SewerBscPlanUpdateRequestDto,
|
||||||
@@ -75,8 +77,11 @@ async function submitReqSupplement(
|
|||||||
id: string,
|
id: string,
|
||||||
chgCycl: number,
|
chgCycl: number,
|
||||||
body: SewerBscPlanApplySaveRequestDto,
|
body: SewerBscPlanApplySaveRequestDto,
|
||||||
): Promise<string> {
|
): Promise<SewerBscPlanApplySaveResponseDto> {
|
||||||
const response = await client.post<string>(`${BASE}/${id}/${chgCycl}/reqsupplement`, body);
|
const response = await client.post<SewerBscPlanApplySaveResponseDto>(
|
||||||
|
`${BASE}/${id}/${chgCycl}/reqsupplement`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,35 +13,52 @@ export {
|
|||||||
} from "./api/queries";
|
} from "./api/queries";
|
||||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||||
export type {
|
export type {
|
||||||
AtflMngClientResponseDto,
|
|
||||||
CklstResultItem,
|
CklstResultItem,
|
||||||
CklstRvwRsltTreeDto,
|
|
||||||
EsntlAtchFileItem,
|
EsntlAtchFileItem,
|
||||||
EsntlAtchFileMngTreeResponseDto,
|
|
||||||
SewerBscPlanApplySaveRequestDto,
|
SewerBscPlanApplySaveRequestDto,
|
||||||
SewerBscPlanApplySaveResponseDto,
|
|
||||||
SewerBscPlanBizCstItem,
|
SewerBscPlanBizCstItem,
|
||||||
|
SewerBscPlanDtlSaveRequestDto,
|
||||||
|
SewerBscPlanFnrsCstItem,
|
||||||
|
SewerBscPlanOtlnItem,
|
||||||
|
SewerBscPlanRvwOpnnItem,
|
||||||
|
SewerBscPlanSaveRequestDto,
|
||||||
|
SewerBscPlanSplmntDmndItem,
|
||||||
|
SewerBscPlanTmplntDtlItem,
|
||||||
|
SewerBscPlanTmplntItem,
|
||||||
|
SewerBscPlanTmplntTmprStrgItem,
|
||||||
|
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||||
|
export {
|
||||||
|
sewerBscPlanApplySaveSchema,
|
||||||
|
sewerBscPlanBizCstItemSchema,
|
||||||
|
sewerBscPlanCklstResultItemSchema,
|
||||||
|
sewerBscPlanDtlSaveSchema,
|
||||||
|
sewerBscPlanEsntlAtchFileItemSchema,
|
||||||
|
sewerBscPlanFnrsCstItemSchema,
|
||||||
|
sewerBscPlanOtlnItemSchema,
|
||||||
|
sewerBscPlanRvwOpnnItemSchema,
|
||||||
|
sewerBscPlanSaveSchema,
|
||||||
|
sewerBscPlanSplmntDmndItemSchema,
|
||||||
|
sewerBscPlanTmplntDtlItemSchema,
|
||||||
|
sewerBscPlanTmplntItemSchema,
|
||||||
|
sewerBscPlanTmplntTmprStrgItemSchema,
|
||||||
|
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||||
|
export type {
|
||||||
|
AtflMngClientResponseDto,
|
||||||
|
CklstRvwRsltTreeDto,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
SewerBscPlanApplySaveResponseDto,
|
||||||
SewerBscPlanBizCstResponseDto,
|
SewerBscPlanBizCstResponseDto,
|
||||||
SewerBscPlanDetResponseDto,
|
SewerBscPlanDetResponseDto,
|
||||||
SewerBscPlanDtlResponseDto,
|
SewerBscPlanDtlResponseDto,
|
||||||
SewerBscPlanDtlSaveRequestDto,
|
|
||||||
SewerBscPlanFnrsCstItem,
|
|
||||||
SewerBscPlanFnrsCstResponseDto,
|
SewerBscPlanFnrsCstResponseDto,
|
||||||
SewerBscPlanOtlnItem,
|
|
||||||
SewerBscPlanOtlnResponseDto,
|
SewerBscPlanOtlnResponseDto,
|
||||||
SewerBscPlanResponseDto,
|
SewerBscPlanResponseDto,
|
||||||
SewerBscPlanRvwOpnnItem,
|
|
||||||
SewerBscPlanRvwOpnnResponseDto,
|
SewerBscPlanRvwOpnnResponseDto,
|
||||||
SewerBscPlanSaveRequestDto,
|
|
||||||
SewerBscPlanSearchRequestDto,
|
SewerBscPlanSearchRequestDto,
|
||||||
SewerBscPlanService,
|
SewerBscPlanService,
|
||||||
SewerBscPlanSplmntDmndItem,
|
|
||||||
SewerBscPlanSplmntDmndResponseDto,
|
SewerBscPlanSplmntDmndResponseDto,
|
||||||
SewerBscPlanTmplntDtlItem,
|
|
||||||
SewerBscPlanTmplntDtlResponseDto,
|
SewerBscPlanTmplntDtlResponseDto,
|
||||||
SewerBscPlanTmplntItem,
|
|
||||||
SewerBscPlanTmplntResponseDto,
|
SewerBscPlanTmplntResponseDto,
|
||||||
SewerBscPlanTmplntTmprStrgItem,
|
|
||||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||||
SewerBscPlanUpdateRequestDto,
|
SewerBscPlanUpdateRequestDto,
|
||||||
} from "./model/sewer-bsc-plan-interface";
|
} from "./model/sewer-bsc-plan-interface";
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanSaveRequestDto */
|
||||||
|
export const sewerBscPlanSaveSchema = z.object({
|
||||||
|
sewerBscPlanDocNo: z.string().optional(),
|
||||||
|
admdstCd: z.string().optional(),
|
||||||
|
envoSeComCd: z.string().optional(),
|
||||||
|
sewerBscPlanSeComCd: z.string().optional(),
|
||||||
|
sewerBscPlanNm: z.string().optional(),
|
||||||
|
taskSeComCd: z.string().optional(),
|
||||||
|
prgrsSttsComCd: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanDtlSaveRequestDto */
|
||||||
|
export const sewerBscPlanDtlSaveSchema = z.object({
|
||||||
|
sewerBscPlanDocNo: z.string().optional(),
|
||||||
|
chgCycl: z.number().optional(),
|
||||||
|
aplcntNm: z.string().optional(),
|
||||||
|
aplcntMblTelno: z.string().optional(),
|
||||||
|
aplcntOgdpNm: z.string().optional(),
|
||||||
|
aplcntEmlAddr: z.string().optional(),
|
||||||
|
dsgnrNm: z.string().optional(),
|
||||||
|
dsgnrTelno: z.string().optional(),
|
||||||
|
dsgnrOgdpNm: z.string().optional(),
|
||||||
|
dsgnrEmlAddr: z.string().optional(),
|
||||||
|
planYr: z.string().optional(),
|
||||||
|
crtrYr: z.string().optional(),
|
||||||
|
bizOtlnCn: z.string().optional(),
|
||||||
|
frlvlGoalYr: z.string().optional(),
|
||||||
|
sclvlGoalYr: z.string().optional(),
|
||||||
|
thlvlGoalYr: z.string().optional(),
|
||||||
|
folvlGoalYr: z.string().optional(),
|
||||||
|
atchFileGroupId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
|
||||||
|
export const sewerBscPlanOtlnItemSchema = z.object({
|
||||||
|
planStpNo: z.number().optional(),
|
||||||
|
sewerPlanPpltnCnt: z.number().optional(),
|
||||||
|
sewerPrcsPpltnCnt: z.number().optional(),
|
||||||
|
sewerPsplyRt: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
|
||||||
|
export const sewerBscPlanCklstResultItemSchema = z.object({
|
||||||
|
cklstRvwSn: z.number().optional(),
|
||||||
|
cklstArtclSn: z.number().optional(),
|
||||||
|
cklstSn: z.number().optional(),
|
||||||
|
rvwStpComCd: z.string(),
|
||||||
|
selected: z.string().optional(),
|
||||||
|
picRvwCn: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanFnrsCstItem */
|
||||||
|
export const sewerBscPlanFnrsCstItemSchema = z.object({
|
||||||
|
planStpNo: z.number().optional(),
|
||||||
|
gvexpAmt: z.number().optional(),
|
||||||
|
lcexpAmt: z.number().optional(),
|
||||||
|
orgprBdchgAmt: z.number().optional(),
|
||||||
|
etcAmt: z.number().optional(),
|
||||||
|
sumAmt: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
|
||||||
|
export const sewerBscPlanSplmntDmndItemSchema = z.object({
|
||||||
|
splmntDmndSn: z.number().nullish(),
|
||||||
|
splmntArtclComCd: z.string().optional(),
|
||||||
|
splmntDmndCn: z.string().optional(),
|
||||||
|
splmntDmndDt: z.string().optional(),
|
||||||
|
splmntDmndAtchFileGroupId: z.string().optional(),
|
||||||
|
splmntCn: z.string().optional(),
|
||||||
|
splmntPicNm: z.string().optional(),
|
||||||
|
splmntCmptnDt: z.string().optional(),
|
||||||
|
splmntAtchFileGroupId: z.string().optional(),
|
||||||
|
cmptnYn: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanRvwOpnnItem */
|
||||||
|
export const sewerBscPlanRvwOpnnItemSchema = z.object({
|
||||||
|
rvwOpnnSn: z.number().optional(),
|
||||||
|
opnrSeComCd: z.string().optional(),
|
||||||
|
opnrNm: z.string().optional(),
|
||||||
|
rvwRsltCn: z.string().optional(),
|
||||||
|
rvwRsltAnmlyYn: z.string().optional(),
|
||||||
|
atchFileGroupId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
|
||||||
|
export const sewerBscPlanEsntlAtchFileItemSchema = z.object({
|
||||||
|
esntlAtchFileId: z.string().optional(),
|
||||||
|
atchFileGroupId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
|
||||||
|
export const sewerBscPlanBizCstItemSchema = z.object({
|
||||||
|
planStpNo: z.number().optional(),
|
||||||
|
acntgSeCd: z.string().optional(),
|
||||||
|
cstrnAmt: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntDtlItem */
|
||||||
|
export const sewerBscPlanTmplntDtlItemSchema = z.object({
|
||||||
|
planStpNo: z.number().optional(),
|
||||||
|
swtrmtFcltCd: z.string().optional(),
|
||||||
|
prcsZoneArea: z.number().optional(),
|
||||||
|
swtrmtPpltnCnt: z.number().optional(),
|
||||||
|
planSwqty: z.number().optional(),
|
||||||
|
fcltCpct: z.number().optional(),
|
||||||
|
sprsysDrtwPplLen: z.number().optional(),
|
||||||
|
sprsysExlnPplLen: z.number().optional(),
|
||||||
|
cbsysPplLen: z.number().optional(),
|
||||||
|
icptgPplLen: z.number().optional(),
|
||||||
|
drgCnt: z.number().optional(),
|
||||||
|
drtwRlyPumpCnt: z.number().optional(),
|
||||||
|
drtwPumpCnt: z.number().optional(),
|
||||||
|
exlnPumpCnt: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
|
||||||
|
export const sewerBscPlanTmplntItemSchema = z.object({
|
||||||
|
swtrmtFcltCd: z.string().optional(),
|
||||||
|
atchFileGroupId: z.string().optional(),
|
||||||
|
sewerBscPlanTmplntDtlList: z.array(sewerBscPlanTmplntDtlItemSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntTmprStrgItem */
|
||||||
|
export const sewerBscPlanTmplntTmprStrgItemSchema = z.object({
|
||||||
|
dtStrgDt: z.string().optional(),
|
||||||
|
swtrmtFcltCd: z.string().optional(),
|
||||||
|
prcsZoneArea: z.number().optional(),
|
||||||
|
swtrmtPpltnCnt: z.number().optional(),
|
||||||
|
planSwqty: z.number().optional(),
|
||||||
|
fcltCpct: z.number().optional(),
|
||||||
|
sprsysDrtwPplLen: z.number().optional(),
|
||||||
|
sprsysExlnPplLen: z.number().optional(),
|
||||||
|
cbsysPplLen: z.number().optional(),
|
||||||
|
icptgPplLen: z.number().optional(),
|
||||||
|
drgCnt: z.number().optional(),
|
||||||
|
drtwRlyPumpCnt: z.number().optional(),
|
||||||
|
drtwPumpCnt: z.number().optional(),
|
||||||
|
exlnPumpCnt: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 백엔드 SewerBscPlanApplySaveRequestDto */
|
||||||
|
export const sewerBscPlanApplySaveSchema = z.object({
|
||||||
|
techReViewSaveCd: z.string().optional(),
|
||||||
|
rvwStpCd: z.string().optional(),
|
||||||
|
sewerBscPlan: sewerBscPlanSaveSchema,
|
||||||
|
sewerBscPlanDtl: sewerBscPlanDtlSaveSchema,
|
||||||
|
sewerBscPlanOtlnList: z.array(sewerBscPlanOtlnItemSchema).optional(),
|
||||||
|
cklstResultList: z.array(sewerBscPlanCklstResultItemSchema).optional(),
|
||||||
|
fnrsCstList: z.array(sewerBscPlanFnrsCstItemSchema).optional(),
|
||||||
|
splmntDmndList: z.array(sewerBscPlanSplmntDmndItemSchema).optional(),
|
||||||
|
rvwOpnnList: z.array(sewerBscPlanRvwOpnnItemSchema).optional(),
|
||||||
|
esntlAtchFileList: z.array(sewerBscPlanEsntlAtchFileItemSchema).optional(),
|
||||||
|
bizCstList: z.array(sewerBscPlanBizCstItemSchema).optional(),
|
||||||
|
sewerBscPlanTmplntList: z.array(sewerBscPlanTmplntItemSchema).optional(),
|
||||||
|
tmplntTmprStrgList: z.array(sewerBscPlanTmplntTmprStrgItemSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SewerBscPlanSaveRequestDto = z.infer<typeof sewerBscPlanSaveSchema>;
|
||||||
|
export type SewerBscPlanDtlSaveRequestDto = z.infer<typeof sewerBscPlanDtlSaveSchema>;
|
||||||
|
export type SewerBscPlanOtlnItem = z.infer<typeof sewerBscPlanOtlnItemSchema>;
|
||||||
|
export type CklstResultItem = z.infer<typeof sewerBscPlanCklstResultItemSchema>;
|
||||||
|
export type SewerBscPlanFnrsCstItem = z.infer<typeof sewerBscPlanFnrsCstItemSchema>;
|
||||||
|
export type SewerBscPlanSplmntDmndItem = z.infer<typeof sewerBscPlanSplmntDmndItemSchema>;
|
||||||
|
export type SewerBscPlanRvwOpnnItem = z.infer<typeof sewerBscPlanRvwOpnnItemSchema>;
|
||||||
|
export type EsntlAtchFileItem = z.infer<typeof sewerBscPlanEsntlAtchFileItemSchema>;
|
||||||
|
export type SewerBscPlanBizCstItem = z.infer<typeof sewerBscPlanBizCstItemSchema>;
|
||||||
|
export type SewerBscPlanTmplntDtlItem = z.infer<typeof sewerBscPlanTmplntDtlItemSchema>;
|
||||||
|
export type SewerBscPlanTmplntItem = z.infer<typeof sewerBscPlanTmplntItemSchema>;
|
||||||
|
export type SewerBscPlanTmplntTmprStrgItem = z.infer<typeof sewerBscPlanTmplntTmprStrgItemSchema>;
|
||||||
|
export type SewerBscPlanApplySaveRequestDto = z.infer<typeof sewerBscPlanApplySaveSchema>;
|
||||||
@@ -1,35 +1,26 @@
|
|||||||
import type {
|
import type {
|
||||||
AtflMngClientResponseDto,
|
AtflMngClientResponseDto,
|
||||||
CklstResultSaveBase,
|
|
||||||
CklstRvwRsltTreeDto,
|
CklstRvwRsltTreeDto,
|
||||||
EsntlAtchFileMngTreeResponseDto,
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
EsntlAtchFileSaveBase,
|
|
||||||
PageableParams,
|
PageableParams,
|
||||||
PageResponse,
|
PageResponse,
|
||||||
PbadmsApplySaveHeader,
|
|
||||||
RvwOpnnResponseExtension,
|
RvwOpnnResponseExtension,
|
||||||
RvwOpnnSaveBase,
|
RvwOpnnSaveBase,
|
||||||
SplmntDmndResponseExtension,
|
SplmntDmndResponseExtension,
|
||||||
SplmntDmndSaveBase,
|
SplmntDmndSaveBase,
|
||||||
} from "@/shared/model";
|
} from "@/shared/model";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SewerBscPlanApplySaveRequestDto,
|
||||||
|
SewerBscPlanSaveRequestDto,
|
||||||
|
} from "./sewer-bsc-plan-apply-payload-schema";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AtflMngClientResponseDto,
|
AtflMngClientResponseDto,
|
||||||
CklstRvwRsltTreeDto,
|
CklstRvwRsltTreeDto,
|
||||||
EsntlAtchFileMngTreeResponseDto,
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
} from "@/shared/model";
|
} from "@/shared/model";
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanSaveRequestDto */
|
|
||||||
export interface SewerBscPlanSaveRequestDto {
|
|
||||||
sewerBscPlanDocNo?: string;
|
|
||||||
admdstCd?: string;
|
|
||||||
envoSeComCd?: string;
|
|
||||||
sewerBscPlanSeComCd?: string;
|
|
||||||
sewerBscPlanNm?: string;
|
|
||||||
taskSeComCd?: string;
|
|
||||||
prgrsSttsComCd?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanUpdateRequestDto */
|
/** 백엔드 SewerBscPlanUpdateRequestDto */
|
||||||
export interface SewerBscPlanUpdateRequestDto {
|
export interface SewerBscPlanUpdateRequestDto {
|
||||||
admdstCd?: string;
|
admdstCd?: string;
|
||||||
@@ -72,28 +63,6 @@ export interface SewerBscPlanResponseDto {
|
|||||||
mdfcnDt?: string;
|
mdfcnDt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanDtlSaveRequestDto */
|
|
||||||
export interface SewerBscPlanDtlSaveRequestDto {
|
|
||||||
sewerBscPlanDocNo?: string;
|
|
||||||
chgCycl?: number;
|
|
||||||
aplcntNm?: string;
|
|
||||||
aplcntMblTelno?: string;
|
|
||||||
aplcntOgdpNm?: string;
|
|
||||||
aplcntEmlAddr?: string;
|
|
||||||
dsgnrNm?: string;
|
|
||||||
dsgnrTelno?: string;
|
|
||||||
dsgnrOgdpNm?: string;
|
|
||||||
dsgnrEmlAddr?: string;
|
|
||||||
planYr?: string;
|
|
||||||
crtrYr?: string;
|
|
||||||
bizOtlnCn?: string;
|
|
||||||
frlvlGoalYr?: string;
|
|
||||||
sclvlGoalYr?: string;
|
|
||||||
thlvlGoalYr?: string;
|
|
||||||
folvlGoalYr?: string;
|
|
||||||
atchFileGroupId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanDtlResponseDto */
|
/** 백엔드 SewerBscPlanDtlResponseDto */
|
||||||
export interface SewerBscPlanDtlResponseDto {
|
export interface SewerBscPlanDtlResponseDto {
|
||||||
sewerBscPlanDocNo?: string;
|
sewerBscPlanDocNo?: string;
|
||||||
@@ -210,103 +179,6 @@ export interface SewerBscPlanTmplntTmprStrgResponseDto {
|
|||||||
exlnPumpCnt?: number;
|
exlnPumpCnt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
|
|
||||||
export interface SewerBscPlanOtlnItem {
|
|
||||||
planStpNo?: number;
|
|
||||||
sewerPlanPpltnCnt?: number;
|
|
||||||
sewerPrcsPpltnCnt?: number;
|
|
||||||
sewerPsplyRt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
|
|
||||||
export interface CklstResultItem extends CklstResultSaveBase {
|
|
||||||
selected?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanFnrsCstItem */
|
|
||||||
export interface SewerBscPlanFnrsCstItem {
|
|
||||||
planStpNo?: number;
|
|
||||||
gvexpAmt?: number;
|
|
||||||
lcexpAmt?: number;
|
|
||||||
orgprBdchgAmt?: number;
|
|
||||||
etcAmt?: number;
|
|
||||||
sumAmt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
|
|
||||||
export type SewerBscPlanSplmntDmndItem = SplmntDmndSaveBase;
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanRvwOpnnItem */
|
|
||||||
export type SewerBscPlanRvwOpnnItem = RvwOpnnSaveBase;
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
|
|
||||||
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
|
|
||||||
export interface SewerBscPlanBizCstItem {
|
|
||||||
planStpNo?: number;
|
|
||||||
acntgSeCd?: string;
|
|
||||||
cstrnAmt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntDtlItem */
|
|
||||||
export interface SewerBscPlanTmplntDtlItem {
|
|
||||||
planStpNo?: number;
|
|
||||||
swtrmtFcltCd?: string;
|
|
||||||
prcsZoneArea?: number;
|
|
||||||
swtrmtPpltnCnt?: number;
|
|
||||||
planSwqty?: number;
|
|
||||||
fcltCpct?: number;
|
|
||||||
sprsysDrtwPplLen?: number;
|
|
||||||
sprsysExlnPplLen?: number;
|
|
||||||
cbsysPplLen?: number;
|
|
||||||
icptgPplLen?: number;
|
|
||||||
drgCnt?: number;
|
|
||||||
drtwRlyPumpCnt?: number;
|
|
||||||
drtwPumpCnt?: number;
|
|
||||||
exlnPumpCnt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
|
|
||||||
export interface SewerBscPlanTmplntItem {
|
|
||||||
swtrmtFcltCd?: string;
|
|
||||||
atchFileGroupId?: string;
|
|
||||||
sewerBscPlanTmplntDtlList?: SewerBscPlanTmplntDtlItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntTmprStrgItem */
|
|
||||||
export interface SewerBscPlanTmplntTmprStrgItem {
|
|
||||||
dtStrgDt?: string;
|
|
||||||
swtrmtFcltCd?: string;
|
|
||||||
prcsZoneArea?: number;
|
|
||||||
swtrmtPpltnCnt?: number;
|
|
||||||
planSwqty?: number;
|
|
||||||
fcltCpct?: number;
|
|
||||||
sprsysDrtwPplLen?: number;
|
|
||||||
sprsysExlnPplLen?: number;
|
|
||||||
cbsysPplLen?: number;
|
|
||||||
icptgPplLen?: number;
|
|
||||||
drgCnt?: number;
|
|
||||||
drtwRlyPumpCnt?: number;
|
|
||||||
drtwPumpCnt?: number;
|
|
||||||
exlnPumpCnt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveRequestDto */
|
|
||||||
export interface SewerBscPlanApplySaveRequestDto extends PbadmsApplySaveHeader {
|
|
||||||
sewerBscPlan: SewerBscPlanSaveRequestDto;
|
|
||||||
sewerBscPlanDtl: SewerBscPlanDtlSaveRequestDto;
|
|
||||||
sewerBscPlanOtlnList?: SewerBscPlanOtlnItem[];
|
|
||||||
cklstResultList?: CklstResultItem[];
|
|
||||||
fnrsCstList?: SewerBscPlanFnrsCstItem[];
|
|
||||||
splmntDmndList?: SewerBscPlanSplmntDmndItem[];
|
|
||||||
rvwOpnnList?: SewerBscPlanRvwOpnnItem[];
|
|
||||||
esntlAtchFileList?: EsntlAtchFileItem[];
|
|
||||||
bizCstList?: SewerBscPlanBizCstItem[];
|
|
||||||
sewerBscPlanTmplntList?: SewerBscPlanTmplntItem[];
|
|
||||||
tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 백엔드 SewerBscPlanApplySaveResponseDto */
|
/** 백엔드 SewerBscPlanApplySaveResponseDto */
|
||||||
export interface SewerBscPlanApplySaveResponseDto {
|
export interface SewerBscPlanApplySaveResponseDto {
|
||||||
sewerBscPlanDocNo: string;
|
sewerBscPlanDocNo: string;
|
||||||
@@ -346,6 +218,6 @@ export interface SewerBscPlanService {
|
|||||||
id: string,
|
id: string,
|
||||||
chgCycl: number,
|
chgCycl: number,
|
||||||
body: SewerBscPlanApplySaveRequestDto,
|
body: SewerBscPlanApplySaveRequestDto,
|
||||||
): Promise<string>;
|
): Promise<SewerBscPlanApplySaveResponseDto>;
|
||||||
techReview(id: string, body: SewerBscPlanApplySaveRequestDto): Promise<void>;
|
techReview(id: string, body: SewerBscPlanApplySaveRequestDto): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ import {
|
|||||||
type DesignCnstnMngDetResponseDto,
|
type DesignCnstnMngDetResponseDto,
|
||||||
designCnstnMngService,
|
designCnstnMngService,
|
||||||
} from "@/entities/design-cnstn";
|
} from "@/entities/design-cnstn";
|
||||||
|
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||||
import {
|
import {
|
||||||
AttachmentsFormSection,
|
AttachmentsFormSection,
|
||||||
BusinessScaleFormSection,
|
BusinessScaleFormSection,
|
||||||
ContactInfoFormSection,
|
ContactInfoFormSection,
|
||||||
|
isFormEsntlAtchComplete,
|
||||||
PrivacyConsentSection,
|
PrivacyConsentSection,
|
||||||
} from "@/widgets/pbadms-sprt";
|
} from "@/widgets/pbadms-sprt";
|
||||||
import { Button } from "@common/components";
|
import { Button } from "@common/components";
|
||||||
@@ -53,6 +55,8 @@ function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPag
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const isEditMode = mode === "edit";
|
const isEditMode = mode === "edit";
|
||||||
|
|
||||||
|
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "CNFM" });
|
||||||
|
|
||||||
const form = useForm<DesignCnstnRegistFormData>({
|
const form = useForm<DesignCnstnRegistFormData>({
|
||||||
resolver: zodResolver(designCnstnRegistSchema),
|
resolver: zodResolver(designCnstnRegistSchema),
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
@@ -113,6 +117,11 @@ function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPag
|
|||||||
async function onApply(data: DesignCnstnRegistFormData) {
|
async function onApply(data: DesignCnstnRegistFormData) {
|
||||||
if (isSubmitting) return;
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||||
|
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
const designCnstnDocNo = await submitForm(data, "CT");
|
const designCnstnDocNo = await submitForm(data, "CT");
|
||||||
|
|||||||
@@ -62,49 +62,51 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
|
|||||||
</SectionCollapsible>
|
</SectionCollapsible>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SectionCollapsible title="기타 첨부파일">
|
{etcFileList.length > 0 && (
|
||||||
<div className="overflow-x-auto">
|
<SectionCollapsible title="기타 첨부파일">
|
||||||
<Table variant="secondary">
|
<div className="overflow-x-auto">
|
||||||
<TableHeader>
|
<Table variant="secondary">
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>파일명</TableHead>
|
|
||||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{etcFileList.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
<TableHead>파일명</TableHead>
|
||||||
등록된 기타첨부파일이 없습니다.
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
etcFileList.map((file) => (
|
<TableBody>
|
||||||
<TableRow key={file.atflId}>
|
{etcFileList.length === 0 ? (
|
||||||
<TableCell className="text-left">
|
<TableRow>
|
||||||
{file.atflCd ? (
|
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||||
<a
|
등록된 기타첨부파일이 없습니다.
|
||||||
className="cursor-pointer text-blue-600 hover:underline"
|
|
||||||
onClick={() => downloadAttachment(file.atflCd)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-gray-500">
|
|
||||||
{formatFileSize(file.atflFlieSz)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
etcFileList.map((file) => (
|
||||||
</TableBody>
|
<TableRow key={file.atflId}>
|
||||||
</Table>
|
<TableCell className="text-left">
|
||||||
</div>
|
{file.atflCd ? (
|
||||||
</SectionCollapsible>
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-gray-500">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
|
import { Paperclip } from "lucide-react";
|
||||||
|
|
||||||
import { PROXY_HOST } from "@/shared/config";
|
import { PROXY_HOST } from "@/shared/config";
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import { attachService } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
||||||
@@ -71,12 +74,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
|||||||
const TechReviewOpinionSection = forwardRef<
|
const TechReviewOpinionSection = forwardRef<
|
||||||
TechReviewOpinionSectionHandle,
|
TechReviewOpinionSectionHandle,
|
||||||
TechReviewOpinionSectionProps
|
TechReviewOpinionSectionProps
|
||||||
>(function TechReviewOpinionSection({
|
>(function TechReviewOpinionSection(
|
||||||
rvwList,
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
editableReviewStep,
|
ref,
|
||||||
atrzSeComCdList = [],
|
) {
|
||||||
isAtrzLoading = false,
|
|
||||||
}, ref) {
|
|
||||||
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
...EMPTY_FORM_DATA,
|
...EMPTY_FORM_DATA,
|
||||||
@@ -240,7 +241,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{uploadedFile ? (
|
{uploadedFile ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xl">{uploadedFile.fileName}</span>
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
|
{uploadedFile.fileName}
|
||||||
|
</TextShimmer>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -317,11 +320,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
|||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{item.atchFileInfo?.atflCd ? (
|
{item.atchFileInfo?.atflCd ? (
|
||||||
<a
|
<a
|
||||||
className="text-blue-600 hover:underline"
|
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
|
||||||
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
<Paperclip className="my-1 size-5 shrink-0" />
|
||||||
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const fnrsCnsltnRegistSchema = z.object({
|
|||||||
),
|
),
|
||||||
splmntDmndList: z.array(
|
splmntDmndList: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
splmntDmndSn: z.number().optional(),
|
splmntDmndSn: z.number().nullish(),
|
||||||
splmntArtclComCd: z.string().optional(),
|
splmntArtclComCd: z.string().optional(),
|
||||||
splmntArtclCdNm: z.string().nullish(),
|
splmntArtclCdNm: z.string().nullish(),
|
||||||
splmntDmndCn: z.string().optional(),
|
splmntDmndCn: z.string().optional(),
|
||||||
|
|||||||
@@ -46,12 +46,7 @@ function ChecklistTaskPanel({
|
|||||||
.forEach((row) => {
|
.forEach((row) => {
|
||||||
nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value;
|
nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value;
|
||||||
});
|
});
|
||||||
return patchChecklistFormState(
|
return patchChecklistFormState(prev, { results: nextResults }, allRows, selectedReviewStep);
|
||||||
prev,
|
|
||||||
{ results: nextResults },
|
|
||||||
allRows,
|
|
||||||
selectedReviewStep,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +72,7 @@ function ChecklistTaskPanel({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
color="white"
|
||||||
disabled={!isStepEditable}
|
disabled={!isStepEditable}
|
||||||
onClick={() => handleSelectAll("Y")}
|
onClick={() => handleSelectAll("Y")}
|
||||||
>
|
>
|
||||||
@@ -87,9 +83,9 @@ function ChecklistTaskPanel({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
color="gray"
|
||||||
disabled={!isStepEditable}
|
disabled={!isStepEditable}
|
||||||
onClick={() => handleSelectAll("N")}
|
onClick={() => handleSelectAll("N")}
|
||||||
className="!bg-destructive"
|
|
||||||
>
|
>
|
||||||
전체 아니오
|
전체 아니오
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ function FloatingActionButtons({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SplmntDmndDialog
|
<SplmntDmndDialog
|
||||||
|
splmntArtclUpCd="SPLMNTARTCL"
|
||||||
open={activeDialog === "dmndSplmnt"}
|
open={activeDialog === "dmndSplmnt"}
|
||||||
onOpenChange={() => setActiveDialog(null)}
|
onOpenChange={() => setActiveDialog(null)}
|
||||||
isSubmitting={isProcessing}
|
isSubmitting={isProcessing}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||||
|
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||||
import {
|
import {
|
||||||
type FnrsCnsltnApplySaveRequestDto,
|
type FnrsCnsltnApplySaveRequestDto,
|
||||||
fnrsCnsltnMngBaseKey,
|
fnrsCnsltnMngBaseKey,
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
ContactInfoFormSection,
|
ContactInfoFormSection,
|
||||||
FnrsSharFormSection,
|
FnrsSharFormSection,
|
||||||
isFormChecklistComplete,
|
isFormChecklistComplete,
|
||||||
|
isFormEsntlAtchComplete,
|
||||||
PrivacyConsentSection,
|
PrivacyConsentSection,
|
||||||
recalculateFnrsSharTotals,
|
recalculateFnrsSharTotals,
|
||||||
SplmntDmndFormSection,
|
SplmntDmndFormSection,
|
||||||
@@ -67,6 +69,7 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro
|
|||||||
|
|
||||||
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
|
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
|
||||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||||
|
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "FNRS" });
|
||||||
const sharTree =
|
const sharTree =
|
||||||
isEditMode && initialData?.fnrsSharList?.length ? initialData.fnrsSharList : masterSharTree;
|
isEditMode && initialData?.fnrsSharList?.length ? initialData.fnrsSharList : masterSharTree;
|
||||||
const sharItems = useMemo(() => buildFnrsSharItems(sharTree), [sharTree]);
|
const sharItems = useMemo(() => buildFnrsSharItems(sharTree), [sharTree]);
|
||||||
@@ -210,6 +213,11 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||||
|
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
const fnrsCnsltnDocNo = await submitForm(data, "ST");
|
const fnrsCnsltnDocNo = await submitForm(data, "ST");
|
||||||
|
|||||||
@@ -19,98 +19,104 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SectionCollapsible title="필수 첨부파일">
|
{esntlFileList.length > 0 && (
|
||||||
<div className="overflow-x-auto">
|
<SectionCollapsible title="필수 첨부파일">
|
||||||
<Table variant="secondary">
|
<div className="overflow-x-auto">
|
||||||
<TableHeader>
|
<Table variant="secondary">
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>필수 자료명</TableHead>
|
|
||||||
<TableHead>파일명</TableHead>
|
|
||||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{esntlFileList.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
<TableHead>필수 자료명</TableHead>
|
||||||
조회된 필수 첨부파일 항목이 없습니다.
|
<TableHead>파일명</TableHead>
|
||||||
</TableCell>
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
esntlFileList.map((file) => (
|
<TableBody>
|
||||||
<TableRow key={file.esntlAtchFileId}>
|
{esntlFileList.length === 0 ? (
|
||||||
<TableCell className="pl-2 text-left">
|
<TableRow>
|
||||||
{file.esntlAtchFileNm}
|
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||||
{file.esntlAtchYn === "Y" ? (
|
조회된 필수 첨부파일 항목이 없습니다.
|
||||||
<span className="ml-1 text-red-500">*</span>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-left">
|
|
||||||
{file.atflCd ? (
|
|
||||||
<a
|
|
||||||
className="cursor-pointer text-blue-600 hover:underline"
|
|
||||||
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{file.orgnlFileNm || "다운로드"}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-500">첨부없음</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
esntlFileList.map((file) => (
|
||||||
</TableBody>
|
<TableRow key={file.esntlAtchFileId}>
|
||||||
</Table>
|
<TableCell className="pl-2 text-left">
|
||||||
</div>
|
{file.esntlAtchFileNm}
|
||||||
</SectionCollapsible>
|
{file.esntlAtchYn === "Y" ? (
|
||||||
|
<span className="ml-1 text-red-500">*</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-left">
|
||||||
|
{file.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || "다운로드"}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500">첨부없음</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionCollapsible title="기타 첨부파일">
|
{etcFileList.length > 0 && (
|
||||||
<div className="overflow-x-auto">
|
<SectionCollapsible title="기타 첨부파일">
|
||||||
<Table variant="secondary">
|
<div className="overflow-x-auto">
|
||||||
<TableHeader>
|
<Table variant="secondary">
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>파일명</TableHead>
|
|
||||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{etcFileList.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
<TableHead>파일명</TableHead>
|
||||||
등록된 기타첨부파일이 없습니다.
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
etcFileList.map((file) => (
|
<TableBody>
|
||||||
<TableRow key={file.atflId}>
|
{etcFileList.length === 0 ? (
|
||||||
<TableCell className="text-left">
|
<TableRow>
|
||||||
{file.atflCd ? (
|
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||||
<a
|
등록된 기타첨부파일이 없습니다.
|
||||||
className="cursor-pointer text-blue-600 hover:underline"
|
|
||||||
onClick={() => downloadAttachment(file.atflCd)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-gray-500">
|
|
||||||
{formatFileSize(file.atflFlieSz)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
etcFileList.map((file) => (
|
||||||
</TableBody>
|
<TableRow key={file.atflId}>
|
||||||
</Table>
|
<TableCell className="text-left">
|
||||||
</div>
|
{file.atflCd ? (
|
||||||
</SectionCollapsible>
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-gray-500">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ function ChecklistSection({
|
|||||||
|
|
||||||
const { total, completed, percent } = progress;
|
const { total, completed, percent } = progress;
|
||||||
|
|
||||||
|
if (cklstResultList.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="체크리스트">
|
<SectionCollapsible title="체크리스트">
|
||||||
<div className="flex flex-col gap-4 p-2">
|
<div className="flex flex-col gap-4 p-2">
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
|
import { Paperclip } from "lucide-react";
|
||||||
|
|
||||||
import { PROXY_HOST } from "@/shared/config";
|
import { PROXY_HOST } from "@/shared/config";
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import { attachService } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
|
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
|
||||||
@@ -71,12 +74,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
|||||||
const TechReviewOpinionSection = forwardRef<
|
const TechReviewOpinionSection = forwardRef<
|
||||||
TechReviewOpinionSectionHandle,
|
TechReviewOpinionSectionHandle,
|
||||||
TechReviewOpinionSectionProps
|
TechReviewOpinionSectionProps
|
||||||
>(function TechReviewOpinionSection({
|
>(function TechReviewOpinionSection(
|
||||||
rvwList,
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
editableReviewStep,
|
ref,
|
||||||
atrzSeComCdList = [],
|
) {
|
||||||
isAtrzLoading = false,
|
|
||||||
}, ref) {
|
|
||||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
...EMPTY_FORM_DATA,
|
...EMPTY_FORM_DATA,
|
||||||
@@ -247,7 +248,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{uploadedFile ? (
|
{uploadedFile ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xl">{uploadedFile.fileName}</span>
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
|
{uploadedFile.fileName}
|
||||||
|
</TextShimmer>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -336,11 +339,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
|||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{item.atchFileInfo?.atflCd ? (
|
{item.atchFileInfo?.atflCd ? (
|
||||||
<a
|
<a
|
||||||
className="text-blue-600 hover:underline"
|
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
|
||||||
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
<Paperclip className="my-1 size-5 shrink-0" />
|
||||||
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const instlSncnRegistSchema = z.object({
|
|||||||
),
|
),
|
||||||
splmntDmndList: z.array(
|
splmntDmndList: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
splmntDmndSn: z.number().optional(),
|
splmntDmndSn: z.number().nullish(),
|
||||||
splmntArtclComCd: z.string().optional(),
|
splmntArtclComCd: z.string().optional(),
|
||||||
splmntArtclCdNm: z.string().nullish(),
|
splmntArtclCdNm: z.string().nullish(),
|
||||||
splmntDmndCn: z.string().optional(),
|
splmntDmndCn: z.string().optional(),
|
||||||
|
|||||||
@@ -45,12 +45,7 @@ function ChecklistTaskPanel({
|
|||||||
.forEach((row) => {
|
.forEach((row) => {
|
||||||
nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value;
|
nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value;
|
||||||
});
|
});
|
||||||
return patchChecklistFormState(
|
return patchChecklistFormState(prev, { results: nextResults }, allRows, selectedReviewStep);
|
||||||
prev,
|
|
||||||
{ results: nextResults },
|
|
||||||
allRows,
|
|
||||||
selectedReviewStep,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +71,7 @@ function ChecklistTaskPanel({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
color="white"
|
||||||
disabled={!isStepEditable}
|
disabled={!isStepEditable}
|
||||||
onClick={() => handleSelectAll("Y")}
|
onClick={() => handleSelectAll("Y")}
|
||||||
>
|
>
|
||||||
@@ -86,9 +82,9 @@ function ChecklistTaskPanel({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
color="gray"
|
||||||
disabled={!isStepEditable}
|
disabled={!isStepEditable}
|
||||||
onClick={() => handleSelectAll("N")}
|
onClick={() => handleSelectAll("N")}
|
||||||
className="!bg-destructive"
|
|
||||||
>
|
>
|
||||||
전체 아니오
|
전체 아니오
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -511,6 +511,7 @@ function FloatingActionButtons({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SplmntDmndDialog
|
<SplmntDmndDialog
|
||||||
|
splmntArtclUpCd="SPLMNTARTCL"
|
||||||
open={activeDialog === "dmndSplmnt"}
|
open={activeDialog === "dmndSplmnt"}
|
||||||
onOpenChange={() => setActiveDialog(null)}
|
onOpenChange={() => setActiveDialog(null)}
|
||||||
isSubmitting={isProcessing}
|
isSubmitting={isProcessing}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||||
|
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||||
import {
|
import {
|
||||||
type InstlSncnApplySaveRequestDto,
|
type InstlSncnApplySaveRequestDto,
|
||||||
instlSncnKeys,
|
instlSncnKeys,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
ChecklistFormSection,
|
ChecklistFormSection,
|
||||||
ContactInfoFormSection,
|
ContactInfoFormSection,
|
||||||
isFormChecklistComplete,
|
isFormChecklistComplete,
|
||||||
|
isFormEsntlAtchComplete,
|
||||||
PrivacyConsentSection,
|
PrivacyConsentSection,
|
||||||
SplmntDmndFormSection,
|
SplmntDmndFormSection,
|
||||||
} from "@/widgets/pbadms-sprt";
|
} from "@/widgets/pbadms-sprt";
|
||||||
@@ -57,6 +59,8 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP
|
|||||||
const isEditMode = mode === "edit";
|
const isEditMode = mode === "edit";
|
||||||
const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL";
|
const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL";
|
||||||
|
|
||||||
|
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "INSTL" });
|
||||||
|
|
||||||
const form = useForm<InstlSncnRegistFormData>({
|
const form = useForm<InstlSncnRegistFormData>({
|
||||||
resolver: zodResolver(instlSncnRegistSchema),
|
resolver: zodResolver(instlSncnRegistSchema),
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
@@ -172,6 +176,11 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||||
|
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
const instlSncnDocNo = await submitForm(data, "ST");
|
const instlSncnDocNo = await submitForm(data, "ST");
|
||||||
|
|||||||
@@ -19,98 +19,104 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SectionCollapsible title="필수 첨부파일">
|
{esntlFileList.length > 0 && (
|
||||||
<div className="overflow-x-auto">
|
<SectionCollapsible title="필수 첨부파일">
|
||||||
<Table variant="secondary">
|
<div className="overflow-x-auto">
|
||||||
<TableHeader>
|
<Table variant="secondary">
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>필수 자료명</TableHead>
|
|
||||||
<TableHead>파일명</TableHead>
|
|
||||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{esntlFileList.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
<TableHead>필수 자료명</TableHead>
|
||||||
조회된 필수 첨부파일 항목이 없습니다.
|
<TableHead>파일명</TableHead>
|
||||||
</TableCell>
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
esntlFileList.map((file) => (
|
<TableBody>
|
||||||
<TableRow key={file.esntlAtchFileId}>
|
{esntlFileList.length === 0 ? (
|
||||||
<TableCell className="pl-2 text-left">
|
<TableRow>
|
||||||
{file.esntlAtchFileNm}
|
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||||
{file.esntlAtchYn === "Y" ? (
|
조회된 필수 첨부파일 항목이 없습니다.
|
||||||
<span className="ml-1 text-red-500">*</span>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-left">
|
|
||||||
{file.atflCd ? (
|
|
||||||
<a
|
|
||||||
className="cursor-pointer text-blue-600 hover:underline"
|
|
||||||
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{file.orgnlFileNm || "다운로드"}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-500">첨부없음</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
esntlFileList.map((file) => (
|
||||||
</TableBody>
|
<TableRow key={file.esntlAtchFileId}>
|
||||||
</Table>
|
<TableCell className="pl-2 text-left">
|
||||||
</div>
|
{file.esntlAtchFileNm}
|
||||||
</SectionCollapsible>
|
{file.esntlAtchYn === "Y" ? (
|
||||||
|
<span className="ml-1 text-red-500">*</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-left">
|
||||||
|
{file.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || "다운로드"}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500">첨부없음</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionCollapsible title="기타 첨부파일">
|
{etcFileList.length > 0 && (
|
||||||
<div className="overflow-x-auto">
|
<SectionCollapsible title="기타 첨부파일">
|
||||||
<Table variant="secondary">
|
<div className="overflow-x-auto">
|
||||||
<TableHeader>
|
<Table variant="secondary">
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>파일명</TableHead>
|
|
||||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{etcFileList.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
<TableHead>파일명</TableHead>
|
||||||
등록된 기타첨부파일이 없습니다.
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
etcFileList.map((file) => (
|
<TableBody>
|
||||||
<TableRow key={file.atflId}>
|
{etcFileList.length === 0 ? (
|
||||||
<TableCell className="text-left">
|
<TableRow>
|
||||||
{file.atflCd ? (
|
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||||
<a
|
등록된 기타첨부파일이 없습니다.
|
||||||
className="cursor-pointer text-blue-600 hover:underline"
|
|
||||||
onClick={() => downloadAttachment(file.atflCd)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-gray-500">
|
|
||||||
{formatFileSize(file.atflFlieSz)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
etcFileList.map((file) => (
|
||||||
</TableBody>
|
<TableRow key={file.atflId}>
|
||||||
</Table>
|
<TableCell className="text-left">
|
||||||
</div>
|
{file.atflCd ? (
|
||||||
</SectionCollapsible>
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-gray-500">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ function ChecklistSection({
|
|||||||
|
|
||||||
const { total, completed, percent } = progress;
|
const { total, completed, percent } = progress;
|
||||||
|
|
||||||
|
if (cklstResultList.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="체크리스트">
|
<SectionCollapsible title="체크리스트">
|
||||||
<div className="flex flex-col gap-4 p-2">
|
<div className="flex flex-col gap-4 p-2">
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
|
import { Paperclip } from "lucide-react";
|
||||||
|
|
||||||
import { PROXY_HOST } from "@/shared/config";
|
import { PROXY_HOST } from "@/shared/config";
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import { attachService } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
|
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
|
||||||
@@ -67,12 +70,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
|||||||
const TechReviewOpinionSection = forwardRef<
|
const TechReviewOpinionSection = forwardRef<
|
||||||
TechReviewOpinionSectionHandle,
|
TechReviewOpinionSectionHandle,
|
||||||
TechReviewOpinionSectionProps
|
TechReviewOpinionSectionProps
|
||||||
>(function TechReviewOpinionSection({
|
>(function TechReviewOpinionSection(
|
||||||
rvwList,
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
editableReviewStep,
|
ref,
|
||||||
atrzSeComCdList = [],
|
) {
|
||||||
isAtrzLoading = false,
|
|
||||||
}, ref) {
|
|
||||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
...EMPTY_FORM_DATA,
|
...EMPTY_FORM_DATA,
|
||||||
@@ -240,7 +241,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{uploadedFile ? (
|
{uploadedFile ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xl">{uploadedFile.fileName}</span>
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
|
{uploadedFile.fileName}
|
||||||
|
</TextShimmer>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -329,11 +332,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
|||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{item.atchFileInfo?.atflCd ? (
|
{item.atchFileInfo?.atflCd ? (
|
||||||
<a
|
<a
|
||||||
className="text-blue-600 hover:underline"
|
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
|
||||||
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
<Paperclip className="my-1 size-5 shrink-0" />
|
||||||
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export { SewerBscPlanEditPage } from "./ui/sewer-bsc-plan-edit-page";
|
|||||||
export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page";
|
export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page";
|
||||||
export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-page";
|
export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-page";
|
||||||
export { SewerBscPlanRegistPage } from "./ui/sewer-bsc-plan-regist-page";
|
export { SewerBscPlanRegistPage } from "./ui/sewer-bsc-plan-regist-page";
|
||||||
|
export { SewerBscPlanViewPage } from "./ui/sewer-bsc-plan-view-page";
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
export const SEWER_BSC_PLAN_VIEW_BUTTON_ACTION = {
|
||||||
|
strg: "strg",
|
||||||
|
aply: "aply",
|
||||||
|
del: "del",
|
||||||
|
rvwRply: "rvwRply",
|
||||||
|
rqstTechRvw: "rqstTechRvw",
|
||||||
|
dsgntRvwr: "dsgntRvwr",
|
||||||
|
rvwStrg: "rvwStrg",
|
||||||
|
rvwCmptn: "rvwCmptn",
|
||||||
|
dmndSplmnt: "dmndSplmnt",
|
||||||
|
atrzAprv: "atrzAprv",
|
||||||
|
atrzRply: "atrzRply",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SewerBscPlanViewButtonAction =
|
||||||
|
(typeof SEWER_BSC_PLAN_VIEW_BUTTON_ACTION)[keyof typeof SEWER_BSC_PLAN_VIEW_BUTTON_ACTION];
|
||||||
|
|
||||||
|
const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, SewerBscPlanViewButtonAction[]>> = {
|
||||||
|
ROLE_USER05: {
|
||||||
|
"": [
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg,
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.aply,
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.del,
|
||||||
|
],
|
||||||
|
SV: [
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg,
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.aply,
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.del,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ROLE_USER03: {
|
||||||
|
LE: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwRply, SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
||||||
|
TP: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwRply],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||||
|
|
||||||
|
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||||
|
string,
|
||||||
|
Record<string, SewerBscPlanViewButtonAction[]>
|
||||||
|
> = {
|
||||||
|
ER: {
|
||||||
|
ATRZ01: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dsgntRvwr],
|
||||||
|
},
|
||||||
|
TE: {
|
||||||
|
ATRZ02: [
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwStrg,
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||||
|
],
|
||||||
|
ATRZ03: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv],
|
||||||
|
ATRZ99: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveButtonConfigs(
|
||||||
|
actions: SewerBscPlanViewButtonAction[],
|
||||||
|
): SewerBscPlanViewButtonAction[] {
|
||||||
|
return actions.map((action) => SEWER_BSC_PLAN_VIEW_BUTTON_ACTION[action]);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetButtonConfigsOptions {
|
||||||
|
hasPendingApprovalRequest?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRoleUser01AtrzButtonActions(
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
atrzSeComCdList: string[],
|
||||||
|
options?: GetButtonConfigsOptions,
|
||||||
|
): SewerBscPlanViewButtonAction[] {
|
||||||
|
if (prgrsSttsComCd === "TE" && options?.hasPendingApprovalRequest === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd];
|
||||||
|
if (!atrzStatusMap || atrzSeComCdList.length === 0) return [];
|
||||||
|
|
||||||
|
const seen = new Set<SewerBscPlanViewButtonAction>();
|
||||||
|
const actions: SewerBscPlanViewButtonAction[] = [];
|
||||||
|
|
||||||
|
for (const atrzSeComCd of atrzSeComCdList) {
|
||||||
|
const mappedActions = atrzStatusMap[atrzSeComCd];
|
||||||
|
if (!mappedActions) continue;
|
||||||
|
|
||||||
|
for (const action of mappedActions) {
|
||||||
|
if (seen.has(action)) continue;
|
||||||
|
seen.add(action);
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsRoleUser01AtrzResolve(
|
||||||
|
dtlInstCd: string | undefined,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
dtlInstCd === "ROLE_USER01" &&
|
||||||
|
ROLE_USER01_ATRZ_STATUS_CODES.includes(
|
||||||
|
prgrsSttsComCd as (typeof ROLE_USER01_ATRZ_STATUS_CODES)[number],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getButtonConfigs(
|
||||||
|
dtlInstCd: string | undefined,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
atrzSeComCdList?: string[],
|
||||||
|
options?: GetButtonConfigsOptions,
|
||||||
|
): SewerBscPlanViewButtonAction[] {
|
||||||
|
if (!dtlInstCd) return [];
|
||||||
|
|
||||||
|
if (needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd)) {
|
||||||
|
return resolveButtonConfigs(
|
||||||
|
resolveRoleUser01AtrzButtonActions(prgrsSttsComCd, atrzSeComCdList ?? [], options),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap = ROLE_STATUS_BUTTON_ACTIONS[dtlInstCd];
|
||||||
|
if (!statusMap) return [];
|
||||||
|
|
||||||
|
const actions = statusMap[prgrsSttsComCd];
|
||||||
|
if (!actions) return [];
|
||||||
|
|
||||||
|
return resolveButtonConfigs(actions);
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import type { CklstResultItem, CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ChecklistFormState,
|
||||||
|
type ChecklistProgress,
|
||||||
|
type ChecklistRowKey,
|
||||||
|
type ChecklistStepMap,
|
||||||
|
REVIEW_STEP_CODES,
|
||||||
|
type ReviewStepCode,
|
||||||
|
} from "../model/view-types";
|
||||||
|
|
||||||
|
export function buildChecklistRowKey(
|
||||||
|
row: CklstRvwRsltTreeDto,
|
||||||
|
reviewStep?: ReviewStepCode,
|
||||||
|
): ChecklistRowKey {
|
||||||
|
const step = reviewStep ?? (row.rvwStpComCd as ReviewStepCode) ?? "LOCAL";
|
||||||
|
return `${row.taskSeComCd}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${step}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap {
|
||||||
|
const base: ChecklistStepMap = { LOCAL: [], ENV: [], KECO: [] };
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const step = (row.rvwStpComCd ?? "LOCAL") as ReviewStepCode;
|
||||||
|
if (REVIEW_STEP_CODES.includes(step)) base[step].push(row);
|
||||||
|
});
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTaskCodes(rows: CklstRvwRsltTreeDto[]): string[] {
|
||||||
|
return Array.from(new Set(rows.map((row) => row.taskSeComCd).filter(Boolean))) as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterChecklistRows(
|
||||||
|
rows: CklstRvwRsltTreeDto[],
|
||||||
|
taskCode: string | null,
|
||||||
|
): CklstRvwRsltTreeDto[] {
|
||||||
|
return rows.filter((row) => (taskCode ? row.taskSeComCd === taskCode : true)).slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChecklistDisplayRows(
|
||||||
|
rows: CklstRvwRsltTreeDto[],
|
||||||
|
taskCode?: string | null,
|
||||||
|
): CklstRvwRsltTreeDto[] {
|
||||||
|
const filtered = filterChecklistRows(rows, taskCode ?? null);
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const deduped: CklstRvwRsltTreeDto[] = [];
|
||||||
|
|
||||||
|
for (const row of filtered) {
|
||||||
|
const dedupeKey = `${row.taskSeComCd}_${row.cklstRvwSn}`;
|
||||||
|
if (seen.has(dedupeKey)) continue;
|
||||||
|
seen.add(dedupeKey);
|
||||||
|
deduped.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeChecklistProgress(
|
||||||
|
rows: CklstRvwRsltTreeDto[],
|
||||||
|
results: ChecklistFormState["results"],
|
||||||
|
): ChecklistProgress {
|
||||||
|
const activeRows = rows.filter((row) => row.bchckUseYn === "Y");
|
||||||
|
const total = activeRows.length;
|
||||||
|
const completed = activeRows.reduce((acc, row) => {
|
||||||
|
const key = buildChecklistRowKey(row);
|
||||||
|
const val = results[key] ?? row.rvwRsltAnmlyYn ?? "";
|
||||||
|
return acc + (val ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||||
|
return { total, completed, percent };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterChecklistRowsByReviewStep(
|
||||||
|
rows: CklstRvwRsltTreeDto[],
|
||||||
|
reviewStep: ReviewStepCode,
|
||||||
|
): CklstRvwRsltTreeDto[] {
|
||||||
|
return rows.filter((row) => row.rvwStpComCd === reviewStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeReviewStepChecklistProgress(
|
||||||
|
rows: CklstRvwRsltTreeDto[],
|
||||||
|
results: ChecklistFormState["results"],
|
||||||
|
reviewStep: ReviewStepCode,
|
||||||
|
): ChecklistProgress {
|
||||||
|
const activeRows = getChecklistDisplayRows(rows).filter((row) => row.bchckUseYn === "Y");
|
||||||
|
const total = activeRows.length;
|
||||||
|
const completed = activeRows.reduce((acc, row) => {
|
||||||
|
const key = buildChecklistRowKey(row, reviewStep);
|
||||||
|
const val = results[key];
|
||||||
|
return acc + (val === "Y" || val === "N" ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
const percent = total > 0 ? Math.round((completed / total) * 100) : 100;
|
||||||
|
return { total, completed, percent };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistFormState {
|
||||||
|
const results: ChecklistFormState["results"] = {};
|
||||||
|
const comments: ChecklistFormState["comments"] = {};
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const key = buildChecklistRowKey(row);
|
||||||
|
if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") {
|
||||||
|
results[key] = row.rvwRsltAnmlyYn;
|
||||||
|
}
|
||||||
|
if (row.picRvwCn) comments[key] = row.picRvwCn;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
comments,
|
||||||
|
progress: computeChecklistProgress(rows, results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeChecklistFormStateFromServer(
|
||||||
|
prev: ChecklistFormState,
|
||||||
|
allRows: CklstRvwRsltTreeDto[],
|
||||||
|
): ChecklistFormState {
|
||||||
|
const results = { ...prev.results };
|
||||||
|
const comments = { ...prev.comments };
|
||||||
|
|
||||||
|
for (const row of allRows) {
|
||||||
|
const step = row.rvwStpComCd as ReviewStepCode | undefined;
|
||||||
|
if (!step || !REVIEW_STEP_CODES.includes(step)) continue;
|
||||||
|
|
||||||
|
const key = buildChecklistRowKey(row);
|
||||||
|
if ((row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") && results[key] === undefined) {
|
||||||
|
results[key] = row.rvwRsltAnmlyYn;
|
||||||
|
}
|
||||||
|
if (row.picRvwCn && !comments[key]) {
|
||||||
|
comments[key] = row.picRvwCn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
comments,
|
||||||
|
progress: computeChecklistProgress(getChecklistDisplayRows(allRows), results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchChecklistFormState(
|
||||||
|
prev: ChecklistFormState,
|
||||||
|
patch: Partial<Pick<ChecklistFormState, "results" | "comments">>,
|
||||||
|
allRows: CklstRvwRsltTreeDto[],
|
||||||
|
reviewStep?: ReviewStepCode,
|
||||||
|
): ChecklistFormState {
|
||||||
|
const results = patch.results ?? prev.results;
|
||||||
|
const comments = patch.comments ?? prev.comments;
|
||||||
|
const progress = reviewStep
|
||||||
|
? computeReviewStepChecklistProgress(allRows, results, reviewStep)
|
||||||
|
: computeChecklistProgress(getChecklistDisplayRows(allRows), results);
|
||||||
|
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
comments,
|
||||||
|
progress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCklstSaveDto(
|
||||||
|
rows: CklstRvwRsltTreeDto[],
|
||||||
|
formState: ChecklistFormState,
|
||||||
|
reviewStep: ReviewStepCode,
|
||||||
|
): CklstResultItem[] {
|
||||||
|
return getChecklistDisplayRows(rows)
|
||||||
|
.filter((row) => row.bchckUseYn === "Y")
|
||||||
|
.map((row) => {
|
||||||
|
const key = buildChecklistRowKey(row, reviewStep);
|
||||||
|
const selected = formState.results[key] ?? null;
|
||||||
|
return {
|
||||||
|
cklstSn: row.cklstSn,
|
||||||
|
cklstArtclSn: row.cklstArtclSn,
|
||||||
|
cklstRvwSn: row.cklstRvwSn,
|
||||||
|
rvwStpComCd: reviewStep,
|
||||||
|
selected: selected ?? undefined,
|
||||||
|
picRvwCn: formState.comments[key] ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
SewerBscPlanApplySaveRequestDto,
|
SewerBscPlanApplySaveRequestDto,
|
||||||
SewerBscPlanDetResponseDto,
|
SewerBscPlanDetResponseDto,
|
||||||
} from "@/entities/sewer-bsc-plan";
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { sewerBscPlanApplySaveSchema } from "@/entities/sewer-bsc-plan";
|
||||||
|
|
||||||
import type { BizCstMatrixItem } from "./biz-cst-matrix-utils";
|
import type { BizCstMatrixItem } from "./biz-cst-matrix-utils";
|
||||||
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
|
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
|
||||||
@@ -244,43 +245,33 @@ export function buildApplyPayload(
|
|||||||
prgrsSttsComCd: "SV" | "LE" | "TE",
|
prgrsSttsComCd: "SV" | "LE" | "TE",
|
||||||
options?: { isEditMode?: boolean; docNo?: string },
|
options?: { isEditMode?: boolean; docNo?: string },
|
||||||
): SewerBscPlanApplySaveRequestDto {
|
): SewerBscPlanApplySaveRequestDto {
|
||||||
const { taskSeComCd, ...sewerBscPlanRest } = data.sewerBscPlan;
|
const docNo = options?.isEditMode ? options.docNo : undefined;
|
||||||
|
|
||||||
return {
|
return sewerBscPlanApplySaveSchema.parse({
|
||||||
sewerBscPlan: {
|
sewerBscPlan: {
|
||||||
...sewerBscPlanRest,
|
...data.sewerBscPlan,
|
||||||
taskSeComCd: taskSeComCd ?? "",
|
sewerBscPlanDocNo: docNo,
|
||||||
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined,
|
taskSeComCd: data.sewerBscPlan.taskSeComCd ?? "",
|
||||||
prgrsSttsComCd,
|
prgrsSttsComCd,
|
||||||
},
|
},
|
||||||
sewerBscPlanDtl: {
|
sewerBscPlanDtl: {
|
||||||
...data.sewerBscPlanDtl,
|
...data.sewerBscPlanDtl,
|
||||||
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined,
|
sewerBscPlanDocNo: docNo,
|
||||||
},
|
},
|
||||||
sewerBscPlanOtlnList: data.sewerBscPlanOtlnList,
|
sewerBscPlanOtlnList: data.sewerBscPlanOtlnList,
|
||||||
bizCstList: data.bizCstList,
|
bizCstList: data.bizCstList,
|
||||||
fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)),
|
fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)),
|
||||||
sewerBscPlanTmplntList: data.sewerBscPlanTmplntList.map(
|
sewerBscPlanTmplntList: data.sewerBscPlanTmplntList,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
({ atchFileNm, swtrmtFcltNm, ...item }) => ({
|
|
||||||
swtrmtFcltCd: item.swtrmtFcltCd,
|
|
||||||
atchFileGroupId: item.atchFileGroupId,
|
|
||||||
sewerBscPlanTmplntDtlList: item.sewerBscPlanTmplntDtlList,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
cklstResultList: data.cklstResultList.map((item) => ({
|
cklstResultList: data.cklstResultList.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
selected: item.selected ?? undefined,
|
selected: item.selected ?? undefined,
|
||||||
})),
|
})),
|
||||||
esntlAtchFileList: data.esntlAtchFileList,
|
esntlAtchFileList: data.esntlAtchFileList,
|
||||||
splmntDmndList: data.splmntDmndList.map(
|
splmntDmndList: data.splmntDmndList.map((item) => ({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
...item,
|
||||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
|
cmptnYn: prgrsSttsComCd === "TE" ? "Y" : item.cmptnYn,
|
||||||
...item,
|
})),
|
||||||
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
|
});
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createEmptyTmplntDtlList };
|
export { createEmptyTmplntDtlList };
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import type {
|
||||||
|
EsntlAtchFileItem,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
SewerBscPlanApplySaveRequestDto,
|
||||||
|
SewerBscPlanDetResponseDto,
|
||||||
|
SewerBscPlanSplmntDmndItem,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import type { SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||||
|
|
||||||
|
import type { ChecklistFormState, ReviewStepCode } from "../model/view-types";
|
||||||
|
|
||||||
|
import { toCklstSaveDto } from "./checklist-utils";
|
||||||
|
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
|
||||||
|
import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||||
|
|
||||||
|
export function toEsntlAtchFileSaveDto(
|
||||||
|
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||||
|
): EsntlAtchFileItem[] {
|
||||||
|
const esntlMap = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const item of esntlFileList) {
|
||||||
|
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||||
|
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||||
|
esntlAtchFileId,
|
||||||
|
atchFileGroupId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSavePayload(
|
||||||
|
data: SewerBscPlanDetResponseDto,
|
||||||
|
checklistFormState: ChecklistFormState,
|
||||||
|
reviewStep: ReviewStepCode,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
): SewerBscPlanApplySaveRequestDto {
|
||||||
|
const detDto = data.detDto;
|
||||||
|
const dtl = data.activeDtlDto;
|
||||||
|
if (!detDto) {
|
||||||
|
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cklstResultRows = data.cklstResultRows ?? [];
|
||||||
|
const esntlFileList = data.esntlFileList ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
sewerBscPlan: {
|
||||||
|
sewerBscPlanDocNo: detDto.sewerBscPlanDocNo,
|
||||||
|
admdstCd: detDto.admdstCd,
|
||||||
|
envoSeComCd: detDto.envoSeComCd,
|
||||||
|
sewerBscPlanSeComCd: detDto.sewerBscPlanSeComCd,
|
||||||
|
sewerBscPlanNm: detDto.sewerBscPlanNm,
|
||||||
|
taskSeComCd: detDto.taskSeComCd,
|
||||||
|
prgrsSttsComCd,
|
||||||
|
},
|
||||||
|
sewerBscPlanDtl: {
|
||||||
|
sewerBscPlanDocNo: detDto.sewerBscPlanDocNo,
|
||||||
|
chgCycl: dtl?.chgCycl ?? detDto.chgCycl,
|
||||||
|
aplcntNm: dtl?.aplcntNm,
|
||||||
|
aplcntMblTelno: dtl?.aplcntMblTelno,
|
||||||
|
aplcntOgdpNm: dtl?.aplcntOgdpNm,
|
||||||
|
aplcntEmlAddr: dtl?.aplcntEmlAddr,
|
||||||
|
dsgnrNm: dtl?.dsgnrNm,
|
||||||
|
dsgnrTelno: dtl?.dsgnrTelno,
|
||||||
|
dsgnrOgdpNm: dtl?.dsgnrOgdpNm,
|
||||||
|
dsgnrEmlAddr: dtl?.dsgnrEmlAddr,
|
||||||
|
planYr: dtl?.planYr,
|
||||||
|
crtrYr: dtl?.crtrYr,
|
||||||
|
bizOtlnCn: dtl?.bizOtlnCn,
|
||||||
|
frlvlGoalYr: dtl?.frlvlGoalYr,
|
||||||
|
sclvlGoalYr: dtl?.sclvlGoalYr,
|
||||||
|
thlvlGoalYr: dtl?.thlvlGoalYr,
|
||||||
|
folvlGoalYr: dtl?.folvlGoalYr,
|
||||||
|
atchFileGroupId: dtl?.atchFileGroupId,
|
||||||
|
},
|
||||||
|
sewerBscPlanOtlnList: (data.otlnList ?? []).map((item) => ({
|
||||||
|
planStpNo: item.planStpNo,
|
||||||
|
sewerPlanPpltnCnt: item.sewerPlanPpltnCnt,
|
||||||
|
sewerPrcsPpltnCnt: item.sewerPrcsPpltnCnt,
|
||||||
|
sewerPsplyRt: item.sewerPsplyRt,
|
||||||
|
})),
|
||||||
|
bizCstList: (data.bizCstList ?? []).map((item) => ({
|
||||||
|
planStpNo: item.planStpNo,
|
||||||
|
acntgSeCd: item.acntgSeCd,
|
||||||
|
cstrnAmt: item.cstrnAmt,
|
||||||
|
})),
|
||||||
|
fnrsCstList: (data.fnrsCstList ?? []).map((item) =>
|
||||||
|
recalcFnrsCstSumAmt({
|
||||||
|
planStpNo: item.planStpNo ?? 1,
|
||||||
|
gvexpAmt: item.gvexpAmt ?? 0,
|
||||||
|
lcexpAmt: item.lcexpAmt ?? 0,
|
||||||
|
orgprBdchgAmt: item.orgprBdchgAmt ?? 0,
|
||||||
|
etcAmt: item.etcAmt ?? 0,
|
||||||
|
sumAmt: item.sumAmt ?? 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
sewerBscPlanTmplntList: (data.tmplntList ?? []).map((item) => ({
|
||||||
|
swtrmtFcltCd: item.swtrmtFcltCd,
|
||||||
|
atchFileGroupId: item.atchFileGroupId,
|
||||||
|
sewerBscPlanTmplntDtlList: (item.sewerBscPlanTmplntDtlList ?? []).map((dtlItem) => ({
|
||||||
|
planStpNo: dtlItem.planStpNo,
|
||||||
|
swtrmtFcltCd: dtlItem.swtrmtFcltCd ?? item.swtrmtFcltCd,
|
||||||
|
prcsZoneArea: dtlItem.prcsZoneArea,
|
||||||
|
swtrmtPpltnCnt: dtlItem.swtrmtPpltnCnt,
|
||||||
|
planSwqty: dtlItem.planSwqty,
|
||||||
|
fcltCpct: dtlItem.fcltCpct,
|
||||||
|
sprsysDrtwPplLen: dtlItem.sprsysDrtwPplLen,
|
||||||
|
sprsysExlnPplLen: dtlItem.sprsysExlnPplLen,
|
||||||
|
cbsysPplLen: dtlItem.cbsysPplLen,
|
||||||
|
icptgPplLen: dtlItem.icptgPplLen,
|
||||||
|
drgCnt: dtlItem.drgCnt,
|
||||||
|
drtwRlyPumpCnt: dtlItem.drtwRlyPumpCnt,
|
||||||
|
drtwPumpCnt: dtlItem.drtwPumpCnt,
|
||||||
|
exlnPumpCnt: dtlItem.exlnPumpCnt,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
cklstResultList: toCklstSaveDto(cklstResultRows, checklistFormState, reviewStep),
|
||||||
|
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTechReviewReplyPayload(
|
||||||
|
data: SewerBscPlanDetResponseDto,
|
||||||
|
checklistFormState: ChecklistFormState,
|
||||||
|
reviewStep: ReviewStepCode,
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
techReViewSaveCd: string,
|
||||||
|
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
|
||||||
|
opnrNm?: string,
|
||||||
|
): SewerBscPlanApplySaveRequestDto {
|
||||||
|
return {
|
||||||
|
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||||
|
techReViewSaveCd,
|
||||||
|
rvwStpCd: reviewStep,
|
||||||
|
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSupplementReqPayload(
|
||||||
|
data: SewerBscPlanDetResponseDto,
|
||||||
|
payload: SplmntDmndDialogConfirmPayload,
|
||||||
|
checklistFormState: ChecklistFormState,
|
||||||
|
): SewerBscPlanApplySaveRequestDto {
|
||||||
|
const existingItems: SewerBscPlanSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) =>
|
||||||
|
toSplmntDmndSaveItem(item),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newItems: SewerBscPlanSplmntDmndItem[] = payload.items.map((item) => ({
|
||||||
|
splmntDmndSn: null,
|
||||||
|
splmntArtclComCd: item.splmntArtclComCd,
|
||||||
|
splmntDmndCn: item.splmntDmndCn,
|
||||||
|
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId,
|
||||||
|
cmptnYn: "N",
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
|
||||||
|
splmntDmndList: [...existingItems, ...newItems],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSplmntDmndSaveItem(item: SewerBscPlanSplmntDmndItem): SewerBscPlanSplmntDmndItem {
|
||||||
|
return {
|
||||||
|
splmntDmndSn: item.splmntDmndSn,
|
||||||
|
splmntArtclComCd: item.splmntArtclComCd,
|
||||||
|
splmntDmndCn: item.splmntDmndCn,
|
||||||
|
splmntDmndDt: item.splmntDmndDt,
|
||||||
|
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId,
|
||||||
|
splmntCn: item.splmntCn,
|
||||||
|
splmntPicNm: item.splmntPicNm,
|
||||||
|
splmntCmptnDt: item.splmntCmptnDt,
|
||||||
|
splmntAtchFileGroupId: item.splmntAtchFileGroupId,
|
||||||
|
cmptnYn: item.cmptnYn,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type {
|
||||||
|
SewerBscPlanDetResponseDto,
|
||||||
|
SewerBscPlanRvwOpnnItem,
|
||||||
|
SewerBscPlanRvwOpnnResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
|
||||||
|
export interface ReviewOpinionFormData {
|
||||||
|
rvwRsltCn: string;
|
||||||
|
rvwRsltAnmlyYn: "Y" | "N" | "";
|
||||||
|
atchFileGroupId: string;
|
||||||
|
opnrSeComCd: string;
|
||||||
|
rvwOpnnSn?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewOpinionGetFormDataResult {
|
||||||
|
formData: ReviewOpinionFormData;
|
||||||
|
rvwList: SewerBscPlanRvwOpnnResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReviewOpinionContent(formData: ReviewOpinionFormData) {
|
||||||
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapServerItemToSaveDto(item: SewerBscPlanRvwOpnnResponseDto): SewerBscPlanRvwOpnnItem | null {
|
||||||
|
if (item.rvwOpnnSn == null) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rvwOpnnSn: item.rvwOpnnSn,
|
||||||
|
opnrSeComCd: item.opnrSeComCd,
|
||||||
|
opnrNm: item.opnrNm,
|
||||||
|
rvwRsltCn: item.rvwRsltCn,
|
||||||
|
rvwRsltAnmlyYn: item.rvwRsltAnmlyYn || undefined,
|
||||||
|
atchFileGroupId: item.atchFileGroupId || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFormDataToSaveDto(
|
||||||
|
formData: ReviewOpinionFormData,
|
||||||
|
opnrNm?: string,
|
||||||
|
): SewerBscPlanRvwOpnnItem {
|
||||||
|
const base: SewerBscPlanRvwOpnnItem = {
|
||||||
|
opnrSeComCd: formData.opnrSeComCd,
|
||||||
|
opnrNm: opnrNm ?? "",
|
||||||
|
rvwRsltCn: formData.rvwRsltCn,
|
||||||
|
rvwRsltAnmlyYn: formData.rvwRsltAnmlyYn || undefined,
|
||||||
|
atchFileGroupId: formData.atchFileGroupId || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formData.rvwOpnnSn != null) {
|
||||||
|
return { ...base, rvwOpnnSn: formData.rvwOpnnSn };
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRvwOpnnSaveDto(
|
||||||
|
data: SewerBscPlanDetResponseDto,
|
||||||
|
result: ReviewOpinionGetFormDataResult | null | undefined,
|
||||||
|
opnrNm?: string,
|
||||||
|
): SewerBscPlanRvwOpnnItem[] | undefined {
|
||||||
|
if (!result || !data.detDto) return undefined;
|
||||||
|
|
||||||
|
const { formData, rvwList } = result;
|
||||||
|
|
||||||
|
const list: SewerBscPlanRvwOpnnItem[] = rvwList
|
||||||
|
.map((item) => mapServerItemToSaveDto(item))
|
||||||
|
.filter((item): item is SewerBscPlanRvwOpnnItem => item != null);
|
||||||
|
|
||||||
|
if (!hasReviewOpinionContent(formData)) {
|
||||||
|
return list.length > 0 ? list : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formDto = mapFormDataToSaveDto(formData, opnrNm);
|
||||||
|
|
||||||
|
if (formData.rvwOpnnSn != null) {
|
||||||
|
const existingIndex = list.findIndex((item) => item.rvwOpnnSn === formData.rvwOpnnSn);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
list[existingIndex] = formDto;
|
||||||
|
} else {
|
||||||
|
list.push(formDto);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list.push(formDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.length > 0 ? list : undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
PBADMS_PROGRESS_ORDER,
|
||||||
|
REVIEW_STEP_CODES,
|
||||||
|
REVIEW_STEP_ROLE_RANK,
|
||||||
|
type ReviewStepCode,
|
||||||
|
ROLE_TO_REVIEW_STEP,
|
||||||
|
} from "../model/view-types";
|
||||||
|
|
||||||
|
export function getProgressStepCount(prgrsSttsComCd?: string): number {
|
||||||
|
const idx = PBADMS_PROGRESS_ORDER.indexOf(
|
||||||
|
prgrsSttsComCd as (typeof PBADMS_PROGRESS_ORDER)[number],
|
||||||
|
);
|
||||||
|
if (idx <= PBADMS_PROGRESS_ORDER.indexOf("SV")) return 1;
|
||||||
|
if (idx <= PBADMS_PROGRESS_ORDER.indexOf("LE")) return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRoleStepCount(dtlInstCd?: string): number {
|
||||||
|
const mapped = dtlInstCd ? ROLE_TO_REVIEW_STEP[dtlInstCd] : undefined;
|
||||||
|
return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVisibleReviewSteps(
|
||||||
|
prgrsSttsComCd: string,
|
||||||
|
userRoleCode?: string,
|
||||||
|
): ReviewStepCode[] {
|
||||||
|
const count = Math.min(getProgressStepCount(prgrsSttsComCd), getRoleStepCount(userRoleCode));
|
||||||
|
return REVIEW_STEP_CODES.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEditableReviewStep(
|
||||||
|
dtlInstCd?: string,
|
||||||
|
prgrsSttsComCd?: string,
|
||||||
|
): ReviewStepCode | undefined {
|
||||||
|
if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) {
|
||||||
|
return "ENV";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dtlInstCd === "ROLE_USER01" && prgrsSttsComCd === "TE") {
|
||||||
|
return "KECO";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -1,38 +1,27 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const tmplntDtlSchema = z.object({
|
import {
|
||||||
planStpNo: z.number(),
|
sewerBscPlanBizCstItemSchema,
|
||||||
swtrmtFcltCd: z.string(),
|
sewerBscPlanCklstResultItemSchema,
|
||||||
prcsZoneArea: z.number().optional(),
|
sewerBscPlanDtlSaveSchema,
|
||||||
swtrmtPpltnCnt: z.number().optional(),
|
sewerBscPlanEsntlAtchFileItemSchema,
|
||||||
planSwqty: z.number().optional(),
|
sewerBscPlanFnrsCstItemSchema,
|
||||||
fcltCpct: z.number().optional(),
|
sewerBscPlanOtlnItemSchema,
|
||||||
sprsysDrtwPplLen: z.number().optional(),
|
sewerBscPlanSaveSchema,
|
||||||
sprsysExlnPplLen: z.number().optional(),
|
sewerBscPlanSplmntDmndItemSchema,
|
||||||
cbsysPplLen: z.number().optional(),
|
sewerBscPlanTmplntDtlItemSchema,
|
||||||
icptgPplLen: z.number().optional(),
|
sewerBscPlanTmplntItemSchema,
|
||||||
drgCnt: z.number().optional(),
|
} from "@/entities/sewer-bsc-plan";
|
||||||
drtwRlyPumpCnt: z.number().optional(),
|
|
||||||
drtwPumpCnt: z.number().optional(),
|
|
||||||
exlnPumpCnt: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sewerBscPlanRegistSchema = z.object({
|
export const sewerBscPlanRegistSchema = z.object({
|
||||||
sewerBscPlan: z.object({
|
sewerBscPlan: sewerBscPlanSaveSchema.extend({
|
||||||
sewerBscPlanDocNo: z.string().optional(),
|
|
||||||
sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"),
|
sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"),
|
||||||
sewerBscPlanSeComCd: z.string().optional(),
|
|
||||||
sewerBscPlanSeCdNm: z.string().optional(),
|
sewerBscPlanSeCdNm: z.string().optional(),
|
||||||
taskSeComCd: z.string().optional(),
|
|
||||||
taskSeCdNm: z.string().optional(),
|
taskSeCdNm: z.string().optional(),
|
||||||
admdstCd: z.string().optional(),
|
|
||||||
envoSeComCd: z.string().optional(),
|
|
||||||
prgrsSttsComCd: z.string().optional(),
|
|
||||||
sido: z.string().optional(),
|
sido: z.string().optional(),
|
||||||
gugun: z.string().optional(),
|
gugun: z.string().nullish(),
|
||||||
}),
|
}),
|
||||||
sewerBscPlanDtl: z.object({
|
sewerBscPlanDtl: sewerBscPlanDtlSaveSchema.extend({
|
||||||
chgCycl: z.number().optional(),
|
|
||||||
planYr: z.string().min(1, "계획년도는 필수입니다"),
|
planYr: z.string().min(1, "계획년도는 필수입니다"),
|
||||||
crtrYr: z.string().min(1, "기준년도는 필수입니다"),
|
crtrYr: z.string().min(1, "기준년도는 필수입니다"),
|
||||||
frlvlGoalYr: z.string().min(1, "1단계 목표연도는 필수입니다"),
|
frlvlGoalYr: z.string().min(1, "1단계 목표연도는 필수입니다"),
|
||||||
@@ -40,10 +29,6 @@ export const sewerBscPlanRegistSchema = z.object({
|
|||||||
thlvlGoalYr: z.string().min(1, "3단계 목표연도는 필수입니다"),
|
thlvlGoalYr: z.string().min(1, "3단계 목표연도는 필수입니다"),
|
||||||
folvlGoalYr: z.string().min(1, "4단계 목표연도는 필수입니다"),
|
folvlGoalYr: z.string().min(1, "4단계 목표연도는 필수입니다"),
|
||||||
bizOtlnCn: z.string().min(1, "계획개요는 필수입니다"),
|
bizOtlnCn: z.string().min(1, "계획개요는 필수입니다"),
|
||||||
aplcntNm: z.string().optional(),
|
|
||||||
aplcntMblTelno: z.string().optional(),
|
|
||||||
aplcntOgdpNm: z.string().optional(),
|
|
||||||
aplcntEmlAddr: z.string().optional(),
|
|
||||||
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
|
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
|
||||||
dsgnrTelno: z
|
dsgnrTelno: z
|
||||||
.string()
|
.string()
|
||||||
@@ -54,74 +39,60 @@ export const sewerBscPlanRegistSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, "설계자 이메일은 필수입니다")
|
.min(1, "설계자 이메일은 필수입니다")
|
||||||
.email("올바른 이메일 형식이 아닙니다"),
|
.email("올바른 이메일 형식이 아닙니다"),
|
||||||
atchFileGroupId: z.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
sewerBscPlanOtlnList: z.array(
|
sewerBscPlanOtlnList: z.array(
|
||||||
z.object({
|
sewerBscPlanOtlnItemSchema.extend({
|
||||||
planStpNo: z.number(),
|
planStpNo: z.number(),
|
||||||
sewerPlanPpltnCnt: z.number().optional(),
|
|
||||||
sewerPrcsPpltnCnt: z.number().optional(),
|
|
||||||
sewerPsplyRt: z.number().optional(),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
bizCstList: z.array(
|
bizCstList: z.array(
|
||||||
z.object({
|
sewerBscPlanBizCstItemSchema.extend({
|
||||||
planStpNo: z.number(),
|
planStpNo: z.number(),
|
||||||
acntgSeCd: z.string(),
|
acntgSeCd: z.string(),
|
||||||
cstrnAmt: z.number(),
|
cstrnAmt: z.number(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
fnrsCstList: z.array(
|
fnrsCstList: z.array(
|
||||||
z.object({
|
sewerBscPlanFnrsCstItemSchema.extend({
|
||||||
planStpNo: z.number(),
|
planStpNo: z.number(),
|
||||||
gvexpAmt: z.number().optional(),
|
|
||||||
lcexpAmt: z.number().optional(),
|
|
||||||
orgprBdchgAmt: z.number().optional(),
|
|
||||||
etcAmt: z.number().optional(),
|
|
||||||
sumAmt: z.number().optional(),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
sewerBscPlanTmplntList: z.array(
|
sewerBscPlanTmplntList: z.array(
|
||||||
z.object({
|
sewerBscPlanTmplntItemSchema.extend({
|
||||||
swtrmtFcltCd: z.string(),
|
swtrmtFcltCd: z.string(),
|
||||||
swtrmtFcltNm: z.string().nullish(),
|
swtrmtFcltNm: z.string().nullish(),
|
||||||
atchFileGroupId: z.string().optional(),
|
|
||||||
atchFileNm: z.string().optional(),
|
atchFileNm: z.string().optional(),
|
||||||
sewerBscPlanTmplntDtlList: z.array(tmplntDtlSchema).optional(),
|
sewerBscPlanTmplntDtlList: z
|
||||||
|
.array(
|
||||||
|
sewerBscPlanTmplntDtlItemSchema.extend({
|
||||||
|
planStpNo: z.number(),
|
||||||
|
swtrmtFcltCd: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
cklstResultList: z.array(
|
cklstResultList: z.array(
|
||||||
z.object({
|
sewerBscPlanCklstResultItemSchema.extend({
|
||||||
cklstSn: z.number(),
|
cklstSn: z.number(),
|
||||||
cklstArtclSn: z.number(),
|
cklstArtclSn: z.number(),
|
||||||
cklstRvwSn: z.number(),
|
cklstRvwSn: z.number(),
|
||||||
rvwStpComCd: z.string(),
|
|
||||||
selected: z.enum(["Y", "N"]).nullable(),
|
selected: z.enum(["Y", "N"]).nullable(),
|
||||||
picRvwCn: z.string().nullable().optional(),
|
picRvwCn: z.string().nullable().optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
esntlAtchFileList: z.array(
|
esntlAtchFileList: z.array(
|
||||||
z.object({
|
sewerBscPlanEsntlAtchFileItemSchema.extend({
|
||||||
esntlAtchFileId: z.string(),
|
esntlAtchFileId: z.string(),
|
||||||
atchFileGroupId: z.string(),
|
atchFileGroupId: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
splmntDmndList: z.array(
|
splmntDmndList: z.array(
|
||||||
z.object({
|
sewerBscPlanSplmntDmndItemSchema.extend({
|
||||||
splmntDmndSn: z.number().optional(),
|
|
||||||
splmntArtclComCd: z.string().optional(),
|
|
||||||
splmntArtclCdNm: z.string().nullish(),
|
splmntArtclCdNm: z.string().nullish(),
|
||||||
splmntDmndCn: z.string().optional(),
|
|
||||||
splmntDmndDt: z.string().optional(),
|
|
||||||
splmntDmndAtchFileGroupId: z.string().optional(),
|
|
||||||
splmntDmndAtchFileNm: z.string().optional(),
|
splmntDmndAtchFileNm: z.string().optional(),
|
||||||
splmntCn: z.string().optional(),
|
|
||||||
splmntPicNm: z.string().optional(),
|
|
||||||
splmntCmptnDt: z.string().optional(),
|
|
||||||
splmntAtchFileGroupId: z.string().optional(),
|
|
||||||
splmntAtchFileNm: z.string().optional(),
|
splmntAtchFileNm: z.string().optional(),
|
||||||
splmntAtchFileAtflId: z.string().optional(),
|
splmntAtchFileAtflId: z.string().optional(),
|
||||||
cmptnYn: z.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
privacyConsent: z.enum(["Y", "N"], {
|
privacyConsent: z.enum(["Y", "N"], {
|
||||||
@@ -130,6 +101,3 @@ export const sewerBscPlanRegistSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type SewerBscPlanRegistFormData = z.infer<typeof sewerBscPlanRegistSchema>;
|
export type SewerBscPlanRegistFormData = z.infer<typeof sewerBscPlanRegistSchema>;
|
||||||
|
|
||||||
/** 체크리스트·필수첨부 docSeComCd — CMS 합의 전 임시값 */
|
|
||||||
export const SEWER_BSC_PLAN_DOC_SE_COM_CD = "SEWER";
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ChecklistFormState,
|
||||||
|
DEFAULT_CHECKLIST_PROGRESS,
|
||||||
|
type ReviewStepCode,
|
||||||
|
} from "../model/view-types";
|
||||||
|
|
||||||
|
import { mergeChecklistFormStateFromServer, patchChecklistFormState } from "./checklist-utils";
|
||||||
|
|
||||||
|
const EMPTY_CKLST_RESULT_LIST: CklstRvwRsltTreeDto[] = [];
|
||||||
|
|
||||||
|
function isSameChecklistFormState(a: ChecklistFormState, b: ChecklistFormState): boolean {
|
||||||
|
if (
|
||||||
|
a.progress.total !== b.progress.total ||
|
||||||
|
a.progress.completed !== b.progress.completed ||
|
||||||
|
a.progress.percent !== b.progress.percent
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aResultKeys = Object.keys(a.results);
|
||||||
|
if (aResultKeys.length !== Object.keys(b.results).length) return false;
|
||||||
|
if (!aResultKeys.every((key) => a.results[key] === b.results[key])) return false;
|
||||||
|
|
||||||
|
const aCommentKeys = Object.keys(a.comments);
|
||||||
|
if (aCommentKeys.length !== Object.keys(b.comments).length) return false;
|
||||||
|
|
||||||
|
return aCommentKeys.every((key) => a.comments[key] === b.comments[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useChecklistFormState(cklstResultList: CklstRvwRsltTreeDto[] = EMPTY_CKLST_RESULT_LIST) {
|
||||||
|
const [formState, setFormState] = useState<ChecklistFormState>({
|
||||||
|
results: {},
|
||||||
|
comments: {},
|
||||||
|
progress: DEFAULT_CHECKLIST_PROGRESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormState((prev) => {
|
||||||
|
const next = mergeChecklistFormStateFromServer(prev, cklstResultList);
|
||||||
|
return isSameChecklistFormState(prev, next) ? prev : next;
|
||||||
|
});
|
||||||
|
}, [cklstResultList]);
|
||||||
|
|
||||||
|
const patchFormState = useCallback(
|
||||||
|
(
|
||||||
|
patch: Partial<Pick<ChecklistFormState, "results" | "comments">>,
|
||||||
|
reviewStep?: ReviewStepCode,
|
||||||
|
) => {
|
||||||
|
setFormState((prev) => patchChecklistFormState(prev, patch, cklstResultList, reviewStep));
|
||||||
|
},
|
||||||
|
[cklstResultList],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { formState, setFormState, patchFormState };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { EMPTY_CKLST_RESULT_LIST, useChecklistFormState };
|
||||||
@@ -19,7 +19,7 @@ const MAIN_STEP_LABELS = {
|
|||||||
supplement: "보완요청",
|
supplement: "보완요청",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const TECH_REVIEW_STATUS_CODES = new Set(["EE", "ER", "TE", "TS", "TR", "TP"]);
|
const TECH_REVIEW_STATUS_CODES = new Set(["ER", "TE", "TP"]);
|
||||||
|
|
||||||
type BadgeState = "completed" | "current" | "pending" | "action";
|
type BadgeState = "completed" | "current" | "pending" | "action";
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "ST") {
|
if (prgrsSttsComCd === "LE") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
@@ -52,22 +52,13 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "LE") {
|
|
||||||
return [
|
|
||||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "action" },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "SL") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
|
||||||
{ label: MAIN_STEP_LABELS.supplement, state: "action" },
|
{ label: MAIN_STEP_LABELS.supplement, state: "action" },
|
||||||
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,17 +72,7 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "EP") {
|
if (prgrsSttsComCd === "TA") {
|
||||||
return [
|
|
||||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
|
||||||
{ label: replyLabel, state: "action" },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
|
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
@@ -101,6 +82,16 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prgrsSttsComCd === "EP") {
|
||||||
|
return [
|
||||||
|
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||||
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
|
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
||||||
|
{ label: replyLabel, state: "completed" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
|
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type {
|
||||||
|
AtflMngClientResponseDto,
|
||||||
|
CklstRvwRsltTreeDto,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
SewerBscPlanDetResponseDto,
|
||||||
|
SewerBscPlanResponseDto,
|
||||||
|
SewerBscPlanRvwOpnnResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
|
||||||
|
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AtflMngClientResponseDto,
|
||||||
|
CklstRvwRsltTreeDto,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
SewerBscPlanDetResponseDto,
|
||||||
|
SewerBscPlanResponseDto,
|
||||||
|
SewerBscPlanRvwOpnnResponseDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChecklistRowKey = string;
|
||||||
|
|
||||||
|
export interface ChecklistProgress {
|
||||||
|
total: number;
|
||||||
|
completed: number;
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_CHECKLIST_PROGRESS: ChecklistProgress = {
|
||||||
|
total: 0,
|
||||||
|
completed: 0,
|
||||||
|
percent: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ChecklistFormState {
|
||||||
|
results: Record<ChecklistRowKey, "Y" | "N">;
|
||||||
|
comments: Record<ChecklistRowKey, string>;
|
||||||
|
progress: ChecklistProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
|
||||||
|
|
||||||
|
export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
|
||||||
|
|
||||||
|
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
||||||
|
LOCAL: "지자체",
|
||||||
|
ENV: "환경청",
|
||||||
|
KECO: "환경공단",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TASK_CODE_LABELS: Record<string, string> = {
|
||||||
|
CP: "부분변경",
|
||||||
|
AP: "전체변경",
|
||||||
|
MP: "병합",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PBADMS_PROGRESS_ORDER = ["SV", "SL", "TA", "EP", "LE", "TP", "ER", "TE"] as const;
|
||||||
|
|
||||||
|
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
||||||
|
ROLE_USER05: "LOCAL",
|
||||||
|
ROLE_USER03: "ENV",
|
||||||
|
ROLE_USER01: "KECO",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const REVIEW_STEP_ROLE_RANK: Record<ReviewStepCode, number> = {
|
||||||
|
LOCAL: 1,
|
||||||
|
ENV: 2,
|
||||||
|
KECO: 3,
|
||||||
|
};
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
cn,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { buildChecklistRowKey, patchChecklistFormState } from "../../lib/checklist-utils";
|
||||||
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
|
import { type ChecklistFormState, ROLE_TO_REVIEW_STEP } from "../../model/view-types";
|
||||||
|
|
||||||
|
interface ChecklistTaskPanelProps {
|
||||||
|
selectedReviewStep: ReviewStepCode;
|
||||||
|
selectedRows: CklstRvwRsltTreeDto[];
|
||||||
|
allRows: CklstRvwRsltTreeDto[];
|
||||||
|
formState: ChecklistFormState;
|
||||||
|
setFormState: Dispatch<SetStateAction<ChecklistFormState>>;
|
||||||
|
userRoleCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChecklistTaskPanel({
|
||||||
|
selectedReviewStep,
|
||||||
|
selectedRows,
|
||||||
|
allRows,
|
||||||
|
formState,
|
||||||
|
setFormState,
|
||||||
|
userRoleCode,
|
||||||
|
}: ChecklistTaskPanelProps) {
|
||||||
|
const currentUserReviewStep = userRoleCode ? ROLE_TO_REVIEW_STEP[userRoleCode] : undefined;
|
||||||
|
const isStepEditable = currentUserReviewStep === selectedReviewStep;
|
||||||
|
|
||||||
|
function handleSelectAll(value: "Y" | "N") {
|
||||||
|
if (!isStepEditable) return;
|
||||||
|
setFormState((prev) => {
|
||||||
|
const nextResults = { ...prev.results };
|
||||||
|
selectedRows
|
||||||
|
.filter((row) => row.bchckUseYn === "Y")
|
||||||
|
.forEach((row) => {
|
||||||
|
nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value;
|
||||||
|
});
|
||||||
|
return patchChecklistFormState(prev, { results: nextResults }, allRows, selectedReviewStep);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||||
|
검토항목
|
||||||
|
</TableHead>
|
||||||
|
<TableHead rowSpan={2}>주요 검토내용</TableHead>
|
||||||
|
<TableHead colSpan={2}>검토 결과</TableHead>
|
||||||
|
{selectedReviewStep !== "LOCAL" ? (
|
||||||
|
<TableHead rowSpan={2} className="w-[20rem]">
|
||||||
|
기술검토 의견
|
||||||
|
</TableHead>
|
||||||
|
) : null}
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[12rem]">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
color="white"
|
||||||
|
disabled={!isStepEditable}
|
||||||
|
onClick={() => handleSelectAll("Y")}
|
||||||
|
>
|
||||||
|
전체 예
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-[12rem]">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
disabled={!isStepEditable}
|
||||||
|
onClick={() => handleSelectAll("N")}
|
||||||
|
>
|
||||||
|
전체 아니오
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{selectedRows.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="py-12 text-center text-muted-foreground">
|
||||||
|
조회된 체크리스트 항목이 없습니다.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
selectedRows.map((row) => {
|
||||||
|
const key = buildChecklistRowKey(row, selectedReviewStep);
|
||||||
|
const isFieldVisible = row.bchckUseYn === "Y";
|
||||||
|
const isLocalReviewStep = selectedReviewStep === "LOCAL";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={key}>
|
||||||
|
<TableCell className="text-center text-[1.2rem]">{row.cklstArtclNm}</TableCell>
|
||||||
|
<TableCell
|
||||||
|
className="pl-2 !text-left text-[1.2rem] text-gray-600"
|
||||||
|
style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}
|
||||||
|
colSpan={isFieldVisible ? 0 : isLocalReviewStep ? 3 : 4}
|
||||||
|
>
|
||||||
|
{row.rvwCn}
|
||||||
|
</TableCell>
|
||||||
|
{isFieldVisible && (
|
||||||
|
<>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`chk_${key}`}
|
||||||
|
value="Y"
|
||||||
|
checked={formState.results[key] === "Y"}
|
||||||
|
readOnly={!isStepEditable}
|
||||||
|
onChange={() => {
|
||||||
|
if (!isStepEditable) return;
|
||||||
|
setFormState((prev) =>
|
||||||
|
patchChecklistFormState(
|
||||||
|
prev,
|
||||||
|
{ results: { ...prev.results, [key]: "Y" } },
|
||||||
|
allRows,
|
||||||
|
selectedReviewStep,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`chk_${key}`}
|
||||||
|
value="N"
|
||||||
|
checked={formState.results[key] === "N"}
|
||||||
|
readOnly={!isStepEditable}
|
||||||
|
onChange={() => {
|
||||||
|
if (!isStepEditable) return;
|
||||||
|
setFormState((prev) =>
|
||||||
|
patchChecklistFormState(
|
||||||
|
prev,
|
||||||
|
{ results: { ...prev.results, [key]: "N" } },
|
||||||
|
allRows,
|
||||||
|
selectedReviewStep,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isLocalReviewStep && isFieldVisible && (
|
||||||
|
<TableCell className="text-left">
|
||||||
|
<Textarea
|
||||||
|
className={cn(
|
||||||
|
"min-h-[6rem] w-[32rem] !text-xl",
|
||||||
|
isStepEditable ? "cursor-pointer" : "cursor-not-allowed",
|
||||||
|
)}
|
||||||
|
value={formState.comments[key] ?? ""}
|
||||||
|
readOnly={!isStepEditable}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!isStepEditable) return;
|
||||||
|
setFormState((prev) =>
|
||||||
|
patchChecklistFormState(
|
||||||
|
prev,
|
||||||
|
{ comments: { ...prev.comments, [key]: event.target.value } },
|
||||||
|
allRows,
|
||||||
|
selectedReviewStep,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChecklistTaskPanel };
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||||
|
import {
|
||||||
|
buildElaprPrgrsSttsListFromAplnRows,
|
||||||
|
ElaprApprovalDialog,
|
||||||
|
type ElaprDmndSaveRequestDto,
|
||||||
|
elaprDmndService,
|
||||||
|
type ElaprTmpltPickerConfirmPayload,
|
||||||
|
ElaprTmpltPickerDialog,
|
||||||
|
hasCurrentUserPendingApprovalRequest,
|
||||||
|
useElaprDocPrgrsByDocNo,
|
||||||
|
} from "@/entities/elapr";
|
||||||
|
import { instlSncnService } from "@/entities/instl-sncn";
|
||||||
|
import {
|
||||||
|
type SewerBscPlanApplySaveRequestDto,
|
||||||
|
sewerBscPlanBaseKey,
|
||||||
|
type SewerBscPlanDetResponseDto,
|
||||||
|
sewerBscPlanService,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getButtonConfigs,
|
||||||
|
needsRoleUser01AtrzResolve,
|
||||||
|
SEWER_BSC_PLAN_VIEW_BUTTON_ACTION,
|
||||||
|
type SewerBscPlanViewButtonAction,
|
||||||
|
} from "../../lib/button-config";
|
||||||
|
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||||
|
import {
|
||||||
|
buildSavePayload,
|
||||||
|
buildSupplementReqPayload,
|
||||||
|
buildTechReviewReplyPayload,
|
||||||
|
toSplmntDmndSaveItem,
|
||||||
|
} from "../../lib/payload-utils";
|
||||||
|
import { type ChecklistFormState } from "../../model/view-types";
|
||||||
|
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||||
|
|
||||||
|
interface FloatingActionButtonsProps {
|
||||||
|
data: SewerBscPlanDetResponseDto;
|
||||||
|
docSeComCd: string;
|
||||||
|
sewerBscPlanDocNo: string;
|
||||||
|
chgCycl: number;
|
||||||
|
prgrsSttsComCd: string;
|
||||||
|
dtlInstCd: string | undefined;
|
||||||
|
atrzSeComCdList: string[];
|
||||||
|
isRvwLoginUserLoading: boolean;
|
||||||
|
checklistFormState: ChecklistFormState;
|
||||||
|
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||||
|
opnrNm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveDialog = "elaprTmplt" | "dmndSplmnt" | "elaprApproval" | null;
|
||||||
|
|
||||||
|
function FloatingActionButtons({
|
||||||
|
data,
|
||||||
|
docSeComCd,
|
||||||
|
sewerBscPlanDocNo,
|
||||||
|
chgCycl,
|
||||||
|
prgrsSttsComCd,
|
||||||
|
dtlInstCd,
|
||||||
|
atrzSeComCdList,
|
||||||
|
isRvwLoginUserLoading,
|
||||||
|
checklistFormState,
|
||||||
|
techReviewOpinionRef,
|
||||||
|
opnrNm,
|
||||||
|
}: FloatingActionButtonsProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [activeDialog, setActiveDialog] = useState<ActiveDialog>(null);
|
||||||
|
|
||||||
|
const docNo = data.detDto?.sewerBscPlanDocNo ?? sewerBscPlanDocNo;
|
||||||
|
const isRegister = data.detDto?.rgtr === userInfo?.userUnqId;
|
||||||
|
|
||||||
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
|
||||||
|
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
|
||||||
|
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
|
||||||
|
useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
|
||||||
|
|
||||||
|
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
|
||||||
|
elaprDocPrgrsList,
|
||||||
|
userInfo?.userUnqId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionButtons = useMemo(() => {
|
||||||
|
if (needsAtrzResolve && isRvwLoginUserLoading) return [];
|
||||||
|
if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return [];
|
||||||
|
return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, {
|
||||||
|
hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
atrzSeComCdList,
|
||||||
|
dtlInstCd,
|
||||||
|
hasPendingApprovalRequest,
|
||||||
|
isElaprDocPrgrsLoading,
|
||||||
|
isRvwLoginUserLoading,
|
||||||
|
needsAtrzResolve,
|
||||||
|
needsTeApprovalCheck,
|
||||||
|
prgrsSttsComCd,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const canEdit =
|
||||||
|
isRegister && actionButtons.some((button) => button === SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg);
|
||||||
|
|
||||||
|
function handleGoToList() {
|
||||||
|
router.push("/sewer-bsc-plan");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
router.push(`/sewer-bsc-plan/${sewerBscPlanDocNo}/edit?chgCycl=${chgCycl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowButton(action: SewerBscPlanViewButtonAction) {
|
||||||
|
return actionButtons.some((button) => button === action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invalidateDetail() {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [...sewerBscPlanBaseKey, "findDetails", sewerBscPlanDocNo, chgCycl],
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeAction({
|
||||||
|
action,
|
||||||
|
request,
|
||||||
|
successMessage,
|
||||||
|
}: {
|
||||||
|
action: SewerBscPlanViewButtonAction;
|
||||||
|
request?: unknown;
|
||||||
|
successMessage?: string;
|
||||||
|
}) {
|
||||||
|
const handler = buttonActionHandlers[action];
|
||||||
|
if (!handler) return;
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
try {
|
||||||
|
await handler({
|
||||||
|
request,
|
||||||
|
data,
|
||||||
|
chgCycl,
|
||||||
|
router,
|
||||||
|
queryClient,
|
||||||
|
onSuccess: () => {
|
||||||
|
if (successMessage) alert(successMessage);
|
||||||
|
void invalidateDetail();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
let errorMessage = "";
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
errorMessage = error.response?.data?.message ?? "";
|
||||||
|
} else {
|
||||||
|
errorMessage = error.message ?? "처리 중 오류가 발생했습니다.";
|
||||||
|
}
|
||||||
|
alert(errorMessage);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStrgClick() {
|
||||||
|
const payload = buildSavePayload(data, checklistFormState, "LOCAL", "SV");
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAplyClick() {
|
||||||
|
const { percent } = computeReviewStepChecklistProgress(
|
||||||
|
data.cklstResultRows ?? [],
|
||||||
|
checklistFormState.results,
|
||||||
|
"LOCAL",
|
||||||
|
);
|
||||||
|
const payload = buildSavePayload(data, checklistFormState, "LOCAL", "LE");
|
||||||
|
|
||||||
|
if (percent !== 100) {
|
||||||
|
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.aply,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "신청 완료되었습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelClick() {
|
||||||
|
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.del,
|
||||||
|
successMessage: "삭제되었습니다.",
|
||||||
|
});
|
||||||
|
router.push("/sewer-bsc-plan");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReviewOpinionFormData() {
|
||||||
|
return techReviewOpinionRef?.current?.getFormData() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRvwRplyClick() {
|
||||||
|
const { percent } = computeReviewStepChecklistProgress(
|
||||||
|
data.cklstResultRows ?? [],
|
||||||
|
checklistFormState.results,
|
||||||
|
"ENV",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (percent !== 100) {
|
||||||
|
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buildTechReviewReplyPayload(
|
||||||
|
data,
|
||||||
|
checklistFormState,
|
||||||
|
"ENV",
|
||||||
|
"EP",
|
||||||
|
"CP",
|
||||||
|
getReviewOpinionFormData(),
|
||||||
|
opnrNm,
|
||||||
|
);
|
||||||
|
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwRply,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "검토결과 회신이 완료되었습니다.",
|
||||||
|
});
|
||||||
|
techReviewOpinionRef?.current?.resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRqstTechRvwClick() {
|
||||||
|
const payload = buildSavePayload(data, checklistFormState, "LOCAL", "ER");
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rqstTechRvw,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "기술검토 의뢰가 완료되었습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleElaprTmpltConfirm({ aplnRows }: ElaprTmpltPickerConfirmPayload) {
|
||||||
|
const payload: ElaprDmndSaveRequestDto = {
|
||||||
|
elaprDocNo: docNo,
|
||||||
|
dmndTtl: data.detDto?.sewerBscPlanNm ?? "",
|
||||||
|
dmndCn: data.activeDtlDto?.bizOtlnCn ?? "",
|
||||||
|
elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows),
|
||||||
|
};
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dsgntRvwr,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "검토자가 지정되었습니다.",
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRvwStrgClick() {
|
||||||
|
const payload = buildTechReviewReplyPayload(
|
||||||
|
data,
|
||||||
|
checklistFormState,
|
||||||
|
"KECO",
|
||||||
|
"TE",
|
||||||
|
"SV",
|
||||||
|
getReviewOpinionFormData(),
|
||||||
|
opnrNm,
|
||||||
|
);
|
||||||
|
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwStrg,
|
||||||
|
request: payload,
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
});
|
||||||
|
techReviewOpinionRef?.current?.resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRvwCmptnClick() {
|
||||||
|
const { percent } = computeReviewStepChecklistProgress(
|
||||||
|
data.cklstResultRows ?? [],
|
||||||
|
checklistFormState.results,
|
||||||
|
"KECO",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (percent !== 100) {
|
||||||
|
alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...buildTechReviewReplyPayload(
|
||||||
|
data,
|
||||||
|
checklistFormState,
|
||||||
|
"KECO",
|
||||||
|
"TE",
|
||||||
|
"SV",
|
||||||
|
getReviewOpinionFormData(),
|
||||||
|
opnrNm,
|
||||||
|
),
|
||||||
|
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
|
||||||
|
};
|
||||||
|
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||||
|
request: payload,
|
||||||
|
});
|
||||||
|
techReviewOpinionRef?.current?.resetForm();
|
||||||
|
setActiveDialog("elaprApproval");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDmndSplmntClick() {
|
||||||
|
setActiveDialog("dmndSplmnt");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) {
|
||||||
|
setActiveDialog(null);
|
||||||
|
const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState);
|
||||||
|
await executeAction({
|
||||||
|
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||||
|
request: requestPayload,
|
||||||
|
successMessage: "보완요청이 처리되었습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||||
|
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||||
|
<Button color="white" type="button" onClick={handleGoToList} disabled={isProcessing}>
|
||||||
|
목록
|
||||||
|
</Button>
|
||||||
|
{canEdit && (
|
||||||
|
<Button color="gray" onClick={handleEdit} disabled={isProcessing}>
|
||||||
|
수정
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg) && (
|
||||||
|
<Button onClick={handleStrgClick} disabled={isProcessing}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.aply) && (
|
||||||
|
<Button color="green" onClick={handleAplyClick} disabled={isProcessing}>
|
||||||
|
신청완료
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.del) && (
|
||||||
|
<Button color="red" onClick={handleDelClick} disabled={isProcessing}>
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwRply) && (
|
||||||
|
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
|
||||||
|
검토결과 회신
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
|
||||||
|
<Button onClick={handleRqstTechRvwClick} disabled={isProcessing}>
|
||||||
|
기술검토 의뢰
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dsgntRvwr) && (
|
||||||
|
<Button onClick={() => setActiveDialog("elaprTmplt")} disabled={isProcessing}>
|
||||||
|
검토자지정
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwStrg) && (
|
||||||
|
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwCmptn) && (
|
||||||
|
<Button color="green" onClick={handleRvwCmptnClick} disabled={isProcessing}>
|
||||||
|
검토완료
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dmndSplmnt) && (
|
||||||
|
<Button color="gray" onClick={handleDmndSplmntClick} disabled={isProcessing}>
|
||||||
|
보완요청
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv) && (
|
||||||
|
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
|
||||||
|
결재(승인)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply) && (
|
||||||
|
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
|
||||||
|
결재(회신)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElaprTmpltPickerDialog
|
||||||
|
open={activeDialog === "elaprTmplt"}
|
||||||
|
onClose={() => setActiveDialog(null)}
|
||||||
|
docSeComCd={docSeComCd}
|
||||||
|
onConfirm={handleElaprTmpltConfirm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SplmntDmndDialog
|
||||||
|
splmntArtclUpCd="SEWERSPLMNTARTCL"
|
||||||
|
open={activeDialog === "dmndSplmnt"}
|
||||||
|
onOpenChange={() => setActiveDialog(null)}
|
||||||
|
isSubmitting={isProcessing}
|
||||||
|
onConfirm={handleDmndSplmntConfirm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElaprApprovalDialog
|
||||||
|
open={activeDialog === "elaprApproval"}
|
||||||
|
onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)}
|
||||||
|
elaprDocNo={docNo}
|
||||||
|
currentUserUuid={userInfo?.userUnqId ?? ""}
|
||||||
|
onSuccess={() => {
|
||||||
|
void invalidateDetail();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FloatingActionButtons };
|
||||||
|
|
||||||
|
async function handleSaveApply(
|
||||||
|
context: ActionHandlerContext<SewerBscPlanApplySaveRequestDto>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await sewerBscPlanService.saveApply(context.request);
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||||
|
try {
|
||||||
|
await sewerBscPlanService.delete(context.data.detDto?.sewerBscPlanDocNo ?? "");
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTechReview(
|
||||||
|
context: ActionHandlerContext<SewerBscPlanApplySaveRequestDto>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await sewerBscPlanService.techReview(
|
||||||
|
context.data.detDto?.sewerBscPlanDocNo ?? "",
|
||||||
|
context.request,
|
||||||
|
);
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRequestTechReview(context: ActionHandlerContext): Promise<void> {
|
||||||
|
try {
|
||||||
|
await instlSncnService.changePrgrsSttsComCd({
|
||||||
|
docNo: context.data.detDto?.sewerBscPlanDocNo ?? "",
|
||||||
|
docSeComCd: "SEWER",
|
||||||
|
prgrsSttsComCd: "ER",
|
||||||
|
});
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDsgntRvwr(
|
||||||
|
context: ActionHandlerContext<ElaprDmndSaveRequestDto>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await elaprDmndService.save(context.request);
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDmndSplmnt(
|
||||||
|
context: ActionHandlerContext<SewerBscPlanApplySaveRequestDto>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const docNo = context.data.detDto?.sewerBscPlanDocNo;
|
||||||
|
if (!docNo) throw new Error("문서번호가 없습니다.");
|
||||||
|
const result = await sewerBscPlanService.submitReqSupplement(
|
||||||
|
docNo,
|
||||||
|
context.chgCycl,
|
||||||
|
context.request,
|
||||||
|
);
|
||||||
|
await context.queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||||
|
context.router.replace(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl}`);
|
||||||
|
context.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
context.onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAtrzAprv(context: ActionHandlerContext): Promise<void> {
|
||||||
|
context.onSuccess?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
|
||||||
|
context.onSuccess?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonActionHandlers: Record<SewerBscPlanViewButtonAction, ActionHandler> = {
|
||||||
|
strg: handleSaveApply,
|
||||||
|
aply: handleSaveApply,
|
||||||
|
del: handleDel,
|
||||||
|
rvwRply: handleTechReview,
|
||||||
|
rqstTechRvw: handleRequestTechReview,
|
||||||
|
dsgntRvwr: handleDsgntRvwr,
|
||||||
|
rvwStrg: handleTechReview,
|
||||||
|
rvwCmptn: handleTechReview,
|
||||||
|
dmndSplmnt: handleDmndSplmnt,
|
||||||
|
atrzAprv: handleAtrzAprv,
|
||||||
|
atrzRply: handleAtrzRply,
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
interface ActionHandlerContext<T = any> {
|
||||||
|
request: T;
|
||||||
|
data: SewerBscPlanDetResponseDto;
|
||||||
|
chgCycl: number;
|
||||||
|
router: ReturnType<typeof useRouter>;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionHandler = (context: ActionHandlerContext) => Promise<void>;
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Paperclip } from "lucide-react";
|
||||||
|
|
||||||
|
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||||
|
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||||
|
import type {
|
||||||
|
AtflMngClientResponseDto,
|
||||||
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface AttachmentsSectionProps {
|
||||||
|
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||||
|
etcFileList: AtflMngClientResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
|
||||||
|
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{esntlFileList.length > 0 && (
|
||||||
|
<SectionCollapsible title="필수 첨부파일">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>필수 자료명</TableHead>
|
||||||
|
<TableHead>파일명</TableHead>
|
||||||
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{esntlFileList.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||||
|
조회된 필수 첨부파일 항목이 없습니다.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
esntlFileList.map((file) => (
|
||||||
|
<TableRow key={file.esntlAtchFileId}>
|
||||||
|
<TableCell className="pl-2 text-left">
|
||||||
|
{file.esntlAtchFileNm}
|
||||||
|
{file.esntlAtchYn === "Y" ? (
|
||||||
|
<span className="ml-1 text-red-500">*</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-left">
|
||||||
|
{file.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<Paperclip className="size-5 shrink-0" />
|
||||||
|
{file.orgnlFileNm || "다운로드"}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500">첨부없음</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{etcFileList.length > 0 && (
|
||||||
|
<SectionCollapsible title="기타 첨부파일">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>파일명</TableHead>
|
||||||
|
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{etcFileList.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||||
|
등록된 기타첨부파일이 없습니다.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
etcFileList.map((file) => (
|
||||||
|
<TableRow key={file.atflId}>
|
||||||
|
<TableCell className="text-left">
|
||||||
|
{file.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="cursor-pointer text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(file.atflCd)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-gray-500">
|
||||||
|
{formatFileSize(file.atflFlieSz)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AttachmentsSection };
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useUserStore } from "@/shared/lib";
|
||||||
import { Field, FieldError } from "@/shared/ui/field";
|
import { Field, FieldError } from "@/shared/ui/field";
|
||||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +24,7 @@ import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist
|
|||||||
import { getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions } from "../../lib/year-options";
|
import { getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions } from "../../lib/year-options";
|
||||||
|
|
||||||
function BasicInfoFormSection() {
|
function BasicInfoFormSection() {
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
const { control, setValue, watch } = useFormContext<SewerBscPlanRegistFormData>();
|
const { control, setValue, watch } = useFormContext<SewerBscPlanRegistFormData>();
|
||||||
const planYearOptions = getPlanYearOptions();
|
const planYearOptions = getPlanYearOptions();
|
||||||
const goalYearOptions = getGoalYearOptions();
|
const goalYearOptions = getGoalYearOptions();
|
||||||
@@ -265,7 +267,7 @@ function BasicInfoFormSection() {
|
|||||||
<TableHead>구분</TableHead>
|
<TableHead>구분</TableHead>
|
||||||
<TableCell>{sewerBscPlanSeLabel}</TableCell>
|
<TableCell>{sewerBscPlanSeLabel}</TableCell>
|
||||||
<TableHead>신청 지자체</TableHead>
|
<TableHead>신청 지자체</TableHead>
|
||||||
<TableCell>{localGovLabel}</TableCell>
|
<TableCell>{localGovLabel || userInfo?.laeSeNm}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SewerBscPlanDtlResponseDto,
|
||||||
|
SewerBscPlanResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface BasicInfoSectionProps {
|
||||||
|
det: SewerBscPlanResponseDto;
|
||||||
|
dtl?: SewerBscPlanDtlResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BasicInfoSection({ det, dtl }: BasicInfoSectionProps) {
|
||||||
|
const sewerBscPlanSeLabel = [det.sewerBscPlanSeCdNm, det.taskSeCdNm].filter(Boolean).join(" ");
|
||||||
|
const localGovLabel = [det.sido, det.gugun].filter(Boolean).join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="기본계획 신청">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
<col width="15%" />
|
||||||
|
<col width="35%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>기본계획명</TableHead>
|
||||||
|
<TableCell colSpan={3}>{det.sewerBscPlanNm || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>계획년도</TableHead>
|
||||||
|
<TableCell>{dtl?.planYr || "-"}</TableCell>
|
||||||
|
<TableHead>기준년도</TableHead>
|
||||||
|
<TableCell>{dtl?.crtrYr || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>1단계 목표연도</TableHead>
|
||||||
|
<TableCell>{dtl?.frlvlGoalYr || "-"}</TableCell>
|
||||||
|
<TableHead>2단계 목표연도</TableHead>
|
||||||
|
<TableCell>{dtl?.sclvlGoalYr || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>3단계 목표연도</TableHead>
|
||||||
|
<TableCell>{dtl?.thlvlGoalYr || "-"}</TableCell>
|
||||||
|
<TableHead>4단계 목표연도</TableHead>
|
||||||
|
<TableCell>{dtl?.folvlGoalYr || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>계획개요</TableHead>
|
||||||
|
<TableCell colSpan={3} className="whitespace-pre-wrap">
|
||||||
|
{dtl?.bizOtlnCn || "-"}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>구분</TableHead>
|
||||||
|
<TableCell>{sewerBscPlanSeLabel || "-"}</TableCell>
|
||||||
|
<TableHead>신청 지자체</TableHead>
|
||||||
|
<TableCell>{localGovLabel || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
</Table>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BasicInfoSection };
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||||
|
import type {
|
||||||
|
SewerBscPlanBizCstResponseDto,
|
||||||
|
SewerBscPlanDtlResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { buildAcntgCostItems, fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import {
|
||||||
|
Skeleton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { getAmt } from "../../lib/biz-cst-matrix-utils";
|
||||||
|
|
||||||
|
interface BizCstMatrixSectionProps {
|
||||||
|
bizCstList: SewerBscPlanBizCstResponseDto[];
|
||||||
|
dtl?: SewerBscPlanDtlResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BizCstMatrixSection({ bizCstList, dtl }: BizCstMatrixSectionProps) {
|
||||||
|
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||||
|
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
||||||
|
|
||||||
|
const matrixList = useMemo(
|
||||||
|
() =>
|
||||||
|
bizCstList.map((item) => ({
|
||||||
|
planStpNo: item.planStpNo ?? 1,
|
||||||
|
acntgSeCd: item.acntgSeCd ?? "",
|
||||||
|
cstrnAmt: item.cstrnAmt ?? 0,
|
||||||
|
})),
|
||||||
|
[bizCstList],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stepHeaders = [
|
||||||
|
{ planStpNo: 1, label: "1단계", year: dtl?.frlvlGoalYr },
|
||||||
|
{ planStpNo: 2, label: "2단계", year: dtl?.sclvlGoalYr },
|
||||||
|
{ planStpNo: 3, label: "3단계", year: dtl?.thlvlGoalYr },
|
||||||
|
{ planStpNo: 4, label: "4단계", year: dtl?.folvlGoalYr },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (costItems.length === 0) {
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="총사업비">
|
||||||
|
<div className="space-y-2 p-2">
|
||||||
|
<Skeleton className="h-[8rem] rounded-xl" />
|
||||||
|
<Skeleton className="h-[32rem] rounded-xl" />
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="총사업비">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||||
|
<Table variant="secondary" className="table-fixed">
|
||||||
|
<colgroup>
|
||||||
|
<col width="12%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[28rem]">구분</TableHead>
|
||||||
|
{stepHeaders.map((step) => (
|
||||||
|
<TableHead key={step.planStpNo} className="text-center">
|
||||||
|
{step.label}
|
||||||
|
<br />
|
||||||
|
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{costItems.map((item) => {
|
||||||
|
const paddingLeft = `${item.level * 1.6}rem`;
|
||||||
|
const showSubIcon = item.level >= 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={item.code}>
|
||||||
|
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||||
|
<div className="relative">
|
||||||
|
{showSubIcon && (
|
||||||
|
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||||
|
)}
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
{stepHeaders.map((step) => (
|
||||||
|
<TableCell key={step.planStpNo} className="text-right">
|
||||||
|
{fmtNumber(getAmt(matrixList, item.code, step.planStpNo))}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BizCstMatrixSection };
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
computeReviewStepChecklistProgress,
|
||||||
|
extractTaskCodes,
|
||||||
|
getChecklistDisplayRows,
|
||||||
|
} from "../../lib/checklist-utils";
|
||||||
|
import { getVisibleReviewSteps } from "../../lib/review-step-utils";
|
||||||
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
|
import {
|
||||||
|
type ChecklistFormState,
|
||||||
|
REVIEW_STEP_LABELS,
|
||||||
|
ROLE_TO_REVIEW_STEP,
|
||||||
|
TASK_CODE_LABELS,
|
||||||
|
} from "../../model/view-types";
|
||||||
|
import { ChecklistTaskPanel } from "../components/checklist-task-panel";
|
||||||
|
|
||||||
|
interface ChecklistSectionProps {
|
||||||
|
cklstResultList: CklstRvwRsltTreeDto[];
|
||||||
|
prgrsSttsComCd: string;
|
||||||
|
userRoleCode?: string;
|
||||||
|
formState: ChecklistFormState;
|
||||||
|
setFormState: Dispatch<SetStateAction<ChecklistFormState>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChecklistSection({
|
||||||
|
cklstResultList,
|
||||||
|
prgrsSttsComCd,
|
||||||
|
userRoleCode,
|
||||||
|
formState,
|
||||||
|
setFormState,
|
||||||
|
}: ChecklistSectionProps) {
|
||||||
|
const visibleReviewSteps = useMemo(
|
||||||
|
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
|
||||||
|
[prgrsSttsComCd, userRoleCode],
|
||||||
|
);
|
||||||
|
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
|
||||||
|
|
||||||
|
const defaultReviewStep = ROLE_TO_REVIEW_STEP[userRoleCode ?? ""] ?? "LOCAL";
|
||||||
|
const [selectedReviewStep, setSelectedReviewStep] = useState<ReviewStepCode>(defaultReviewStep);
|
||||||
|
const [selectedTaskCode, setSelectedTaskCode] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visibleReviewSteps.length === 0) return;
|
||||||
|
if (!visibleReviewSteps.includes(selectedReviewStep)) {
|
||||||
|
setSelectedReviewStep(visibleReviewSteps[visibleReviewSteps.length - 1]);
|
||||||
|
}
|
||||||
|
}, [selectedReviewStep, visibleReviewSteps]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (taskCodes.length === 0) {
|
||||||
|
setSelectedTaskCode(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedTaskCode || !taskCodes.includes(selectedTaskCode)) {
|
||||||
|
setSelectedTaskCode(taskCodes[0]);
|
||||||
|
}
|
||||||
|
}, [taskCodes, selectedTaskCode]);
|
||||||
|
|
||||||
|
const progress = useMemo(
|
||||||
|
() =>
|
||||||
|
computeReviewStepChecklistProgress(cklstResultList, formState.results, selectedReviewStep),
|
||||||
|
[cklstResultList, formState.results, selectedReviewStep],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { total, completed, percent } = progress;
|
||||||
|
|
||||||
|
if (cklstResultList.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="체크리스트">
|
||||||
|
<div className="flex flex-col gap-4 p-2">
|
||||||
|
{visibleReviewSteps.length > 0 ? (
|
||||||
|
<Tabs
|
||||||
|
value={selectedReviewStep}
|
||||||
|
onValueChange={(value) => setSelectedReviewStep(value as ReviewStepCode)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
{visibleReviewSteps.map((stepCode) => (
|
||||||
|
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
|
||||||
|
{REVIEW_STEP_LABELS[stepCode]}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{taskCodes.length > 0 ? (
|
||||||
|
<Tabs
|
||||||
|
value={selectedTaskCode ?? taskCodes[0]}
|
||||||
|
onValueChange={setSelectedTaskCode}
|
||||||
|
className="w-full"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<div className="flex items-end justify-between gap-4">
|
||||||
|
<TabsList className="p-0">
|
||||||
|
{taskCodes.map((code) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={code}
|
||||||
|
value={code}
|
||||||
|
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
||||||
|
>
|
||||||
|
{TASK_CODE_LABELS[code] ?? code}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
<div className="shrink-0 pb-1 text-lg text-muted-foreground">
|
||||||
|
총합: <strong>{total}</strong> · 완료: <strong>{completed}</strong> · 진행률:{" "}
|
||||||
|
<strong>{percent}%</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{taskCodes.map((code) => (
|
||||||
|
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
|
||||||
|
<ChecklistTaskPanel
|
||||||
|
selectedReviewStep={selectedReviewStep}
|
||||||
|
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
||||||
|
allRows={cklstResultList}
|
||||||
|
formState={formState}
|
||||||
|
setFormState={setFormState}
|
||||||
|
userRoleCode={userRoleCode}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChecklistSection };
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { SewerBscPlanDtlResponseDto } from "@/entities/sewer-bsc-plan";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface ContactInfoSectionProps {
|
||||||
|
dtl?: SewerBscPlanDtlResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContactInfoSection({ dtl }: ContactInfoSectionProps) {
|
||||||
|
if (!dtl) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="담당자 정보">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
{dtl.aplcntNm ? (
|
||||||
|
<>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead rowSpan={2}>신청자</TableHead>
|
||||||
|
<TableHead>이름</TableHead>
|
||||||
|
<TableCell>{dtl.aplcntNm || "-"}</TableCell>
|
||||||
|
<TableHead>연락처</TableHead>
|
||||||
|
<TableCell>{dtl.aplcntMblTelno || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>소속</TableHead>
|
||||||
|
<TableCell>{dtl.aplcntOgdpNm || "-"}</TableCell>
|
||||||
|
<TableHead>이메일</TableHead>
|
||||||
|
<TableCell>{dtl.aplcntEmlAddr || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{dtl.dsgnrNm ? (
|
||||||
|
<>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead rowSpan={2}>설계자</TableHead>
|
||||||
|
<TableHead>이름</TableHead>
|
||||||
|
<TableCell>{dtl.dsgnrNm || "-"}</TableCell>
|
||||||
|
<TableHead>연락처</TableHead>
|
||||||
|
<TableCell>{dtl.dsgnrTelno || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>소속</TableHead>
|
||||||
|
<TableCell>{dtl.dsgnrOgdpNm || "-"}</TableCell>
|
||||||
|
<TableHead>이메일</TableHead>
|
||||||
|
<TableCell>{dtl.dsgnrEmlAddr || "-"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</TableHeader>
|
||||||
|
</Table>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ContactInfoSection };
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||||
|
import type {
|
||||||
|
SewerBscPlanDtlResponseDto,
|
||||||
|
SewerBscPlanFnrsCstResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { buildAcntgCostItems, fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import {
|
||||||
|
Skeleton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import { PLAN_STEPS } from "../../lib/biz-cst-matrix-utils";
|
||||||
|
|
||||||
|
interface FnrsCstSectionProps {
|
||||||
|
fnrsCstList: SewerBscPlanFnrsCstResponseDto[];
|
||||||
|
dtl?: SewerBscPlanDtlResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FNRS_CST_ROWS = [
|
||||||
|
{ key: "sumAmt" as const, label: "총사업비" },
|
||||||
|
{ key: "gvexpAmt" as const, label: "국고" },
|
||||||
|
{ key: "lcexpAmt" as const, label: "지방비" },
|
||||||
|
{ key: "orgprBdchgAmt" as const, label: "원인자부담금" },
|
||||||
|
{ key: "etcAmt" as const, label: "기타" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function FnrsCstSection({ fnrsCstList, dtl }: FnrsCstSectionProps) {
|
||||||
|
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||||
|
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
||||||
|
|
||||||
|
const matrixFnrsList = useMemo(
|
||||||
|
() =>
|
||||||
|
PLAN_STEPS.map((planStpNo) => {
|
||||||
|
const item = fnrsCstList.find((row) => row.planStpNo === planStpNo);
|
||||||
|
return {
|
||||||
|
planStpNo,
|
||||||
|
gvexpAmt: item?.gvexpAmt ?? 0,
|
||||||
|
lcexpAmt: item?.lcexpAmt ?? 0,
|
||||||
|
orgprBdchgAmt: item?.orgprBdchgAmt ?? 0,
|
||||||
|
etcAmt: item?.etcAmt ?? 0,
|
||||||
|
sumAmt: item?.sumAmt ?? 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[fnrsCstList],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stepHeaders = [
|
||||||
|
{ planStpNo: 1, label: "1단계", year: dtl?.frlvlGoalYr },
|
||||||
|
{ planStpNo: 2, label: "2단계", year: dtl?.sclvlGoalYr },
|
||||||
|
{ planStpNo: 3, label: "3단계", year: dtl?.thlvlGoalYr },
|
||||||
|
{ planStpNo: 4, label: "4단계", year: dtl?.folvlGoalYr },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (costItems.length === 0) {
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="재원분담">
|
||||||
|
<div className="space-y-2 p-2">
|
||||||
|
<Skeleton className="h-[8rem] rounded-xl" />
|
||||||
|
<Skeleton className="h-[32rem] rounded-xl" />
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="재원분담">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||||
|
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||||
|
</div>
|
||||||
|
<Table variant="secondary" className="table-fixed">
|
||||||
|
<colgroup>
|
||||||
|
<col width="12%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>구분</TableHead>
|
||||||
|
{stepHeaders.map((step) => (
|
||||||
|
<TableHead key={step.planStpNo} className="text-center">
|
||||||
|
{step.label}
|
||||||
|
<br />
|
||||||
|
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{FNRS_CST_ROWS.map((row) => (
|
||||||
|
<TableRow key={row.key}>
|
||||||
|
<TableHead>{row.label}</TableHead>
|
||||||
|
{stepHeaders.map((step) => {
|
||||||
|
const item = matrixFnrsList.find((fnrs) => fnrs.planStpNo === step.planStpNo);
|
||||||
|
return (
|
||||||
|
<TableCell key={step.planStpNo} className="text-right">
|
||||||
|
{fmtNumber(item?.[row.key])}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FnrsCstSection };
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SewerBscPlanDtlResponseDto,
|
||||||
|
SewerBscPlanOtlnResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
|
interface OutlineSectionProps {
|
||||||
|
otlnList: SewerBscPlanOtlnResponseDto[];
|
||||||
|
dtl?: SewerBscPlanDtlResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OTLN_ROWS = [
|
||||||
|
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구" },
|
||||||
|
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구" },
|
||||||
|
{ key: "sewerPsplyRt" as const, label: "하수도보급율" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||||
|
const stepHeaders = [
|
||||||
|
{ planStpNo: 1, label: "1단계", year: dtl?.frlvlGoalYr },
|
||||||
|
{ planStpNo: 2, label: "2단계", year: dtl?.sclvlGoalYr },
|
||||||
|
{ planStpNo: 3, label: "3단계", year: dtl?.thlvlGoalYr },
|
||||||
|
{ planStpNo: 4, label: "4단계", year: dtl?.folvlGoalYr },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="사업개요">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="12%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
<col width="22%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[20rem]">구분</TableHead>
|
||||||
|
{stepHeaders.map((step) => (
|
||||||
|
<TableHead key={step.planStpNo} className="text-center">
|
||||||
|
{step.label}
|
||||||
|
<br />
|
||||||
|
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{OTLN_ROWS.map((row) => (
|
||||||
|
<TableRow key={row.key}>
|
||||||
|
<TableHead>{row.label}</TableHead>
|
||||||
|
{stepHeaders.map((step) => {
|
||||||
|
const item = otlnList.find((otln) => otln.planStpNo === step.planStpNo);
|
||||||
|
return (
|
||||||
|
<TableCell key={step.planStpNo} className="text-right">
|
||||||
|
{fmtNumber(item?.[row.key])}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { OutlineSection };
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
|
import { Paperclip } from "lucide-react";
|
||||||
|
|
||||||
|
import { PROXY_HOST } from "@/shared/config";
|
||||||
|
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
|
import { attachService } from "@/entities/attach";
|
||||||
|
import type { SewerBscPlanRvwOpnnResponseDto } from "@/entities/sewer-bsc-plan";
|
||||||
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ReviewOpinionFormData,
|
||||||
|
ReviewOpinionGetFormDataResult,
|
||||||
|
} from "../../lib/review-opinion-utils";
|
||||||
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
|
|
||||||
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
|
|
||||||
|
interface TechReviewOpinionSectionProps {
|
||||||
|
rvwList: SewerBscPlanRvwOpnnResponseDto[];
|
||||||
|
editableReviewStep?: ReviewStepCode;
|
||||||
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TechReviewOpinionSectionHandle {
|
||||||
|
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||||
|
resetForm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TechReviewOpinionTableProps {
|
||||||
|
items: SewerBscPlanRvwOpnnResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadedOpinionFile {
|
||||||
|
fileName: string;
|
||||||
|
fileKey: string;
|
||||||
|
atflId?: string;
|
||||||
|
atflCd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
|
rvwRsltCn: "",
|
||||||
|
rvwRsltAnmlyYn: "",
|
||||||
|
atchFileGroupId: "",
|
||||||
|
opnrSeComCd: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TechReviewOpinionSection = forwardRef<
|
||||||
|
TechReviewOpinionSectionHandle,
|
||||||
|
TechReviewOpinionSectionProps
|
||||||
|
>(function TechReviewOpinionSection(
|
||||||
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
|
...EMPTY_FORM_DATA,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
});
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
}));
|
||||||
|
}, [editableReviewStep]);
|
||||||
|
|
||||||
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
|
if (!atflCd || !atflId) return;
|
||||||
|
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
|
},
|
||||||
|
onError: (_error, _file, fileKey) => {
|
||||||
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
getFormData: () => {
|
||||||
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
...formData,
|
||||||
|
opnrSeComCd: editableReviewStep,
|
||||||
|
},
|
||||||
|
rvwList,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resetForm: () => {
|
||||||
|
setFormData({
|
||||||
|
...EMPTY_FORM_DATA,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
});
|
||||||
|
setUploadedFile(null);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||||
|
);
|
||||||
|
|
||||||
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
|
|
||||||
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
|
const hasKecoSection =
|
||||||
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||||
|
|
||||||
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileUpload(files: FileList | null) {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const file = files[0];
|
||||||
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
|
|
||||||
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadFileAsync(file, {
|
||||||
|
fileKey,
|
||||||
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// onError callback handles cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileRemove() {
|
||||||
|
if (!uploadedFile) return;
|
||||||
|
|
||||||
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
|
|
||||||
|
if (uploadedFile.atflId) {
|
||||||
|
try {
|
||||||
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadedFile(null);
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
|
if (editableReviewStep !== reviewStep) return null;
|
||||||
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||||
|
|
||||||
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
|
<Table variant="secondary">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<Textarea
|
||||||
|
value={formData.rvwRsltCn}
|
||||||
|
onChange={(event) =>
|
||||||
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="의견내용을 입력하세요"
|
||||||
|
rows={4}
|
||||||
|
className="min-h-[8rem] !text-xl"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>검토결과</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FieldSet>
|
||||||
|
<RadioGroup
|
||||||
|
value={formData.rvwRsltAnmlyYn}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<FieldLabel
|
||||||
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
|
className="!w-auto border-none"
|
||||||
|
>
|
||||||
|
<Field orientation="horizontal" className="w-auto">
|
||||||
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
|
<span className="font-normal">정상</span>
|
||||||
|
</Field>
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel
|
||||||
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
|
className="!w-auto border-none"
|
||||||
|
>
|
||||||
|
<Field orientation="horizontal" className="w-auto">
|
||||||
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
|
<span className="font-normal">이상</span>
|
||||||
|
</Field>
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>첨부파일</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
{uploadedFile ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
|
{uploadedFile.fileName}
|
||||||
|
</TextShimmer>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleFileRemove();
|
||||||
|
}}
|
||||||
|
disabled={uploading}
|
||||||
|
className="size-8 !bg-destructive"
|
||||||
|
>
|
||||||
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(event) => {
|
||||||
|
void handleFileUpload(event.target.files);
|
||||||
|
event.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button size="sm" variant="outline" type="button" asChild>
|
||||||
|
<span>파일 선택</span>
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasEnvSection ? (
|
||||||
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
||||||
|
{renderOpinionForm("ENV")}
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
) : null}
|
||||||
|
{hasKecoSection ? (
|
||||||
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||||
|
{renderOpinionForm("KECO")}
|
||||||
|
</div>
|
||||||
|
</SectionCollapsible>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="10%" />
|
||||||
|
<col width="55%" />
|
||||||
|
<col width="5%" />
|
||||||
|
<col width="20%" />
|
||||||
|
<col width="10%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>의견자명</TableHead>
|
||||||
|
<TableHead>의견내용</TableHead>
|
||||||
|
<TableHead>검토결과</TableHead>
|
||||||
|
<TableHead>첨부파일</TableHead>
|
||||||
|
<TableHead>등록일시</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => (
|
||||||
|
<TableRow key={`${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}>
|
||||||
|
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||||
|
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.atchFileInfo?.atflCd ? (
|
||||||
|
<a
|
||||||
|
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
|
||||||
|
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<Paperclip className="my-1 size-5 shrink-0" />
|
||||||
|
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TechReviewOpinionSection };
|
||||||
@@ -4,11 +4,11 @@ import { useId, useMemo, useState } from "react";
|
|||||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
import { Paperclip } from "lucide-react";
|
|
||||||
|
|
||||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { Field } from "@/shared/ui/field";
|
import { Field } from "@/shared/ui/field";
|
||||||
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import { useFacilityList } from "@/entities/bns-if";
|
import { useFacilityList } from "@/entities/bns-if";
|
||||||
import { useNowDmgMtncGnsvy } from "@/entities/dc";
|
import { useNowDmgMtncGnsvy } from "@/entities/dc";
|
||||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
@@ -226,7 +226,6 @@ function TmplntFacilityAttachments({
|
|||||||
}: TmplntFacilityAttachmentsProps) {
|
}: TmplntFacilityAttachmentsProps) {
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
const { setValue } = useFormContext<SewerBscPlanRegistFormData>();
|
const { setValue } = useFormContext<SewerBscPlanRegistFormData>();
|
||||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedTmplntFile | null>(() =>
|
const [uploadedFile, setUploadedFile] = useState<UploadedTmplntFile | null>(() =>
|
||||||
atchFileGroupId && atchFileNm
|
atchFileGroupId && atchFileNm
|
||||||
? {
|
? {
|
||||||
@@ -254,7 +253,6 @@ function TmplntFacilityAttachments({
|
|||||||
const fileInputId = `${formId}-tmplnt-atch-${facilityIndex}`;
|
const fileInputId = `${formId}-tmplnt-atch-${facilityIndex}`;
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
const displayFileName = uploadedFile?.fileName ?? atchFileNm ?? "";
|
const displayFileName = uploadedFile?.fileName ?? atchFileNm ?? "";
|
||||||
const downloadAtflCd = uploadedFile?.atflCd || atchFileGroupId;
|
|
||||||
const hasFile = Boolean(uploadedFile || atchFileNm);
|
const hasFile = Boolean(uploadedFile || atchFileNm);
|
||||||
|
|
||||||
async function handleFileUpload(files: FileList | null) {
|
async function handleFileUpload(files: FileList | null) {
|
||||||
@@ -315,22 +313,16 @@ function TmplntFacilityAttachments({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{facilityName} 계획평면도</TableCell>
|
<TableHead data-required align="left">
|
||||||
|
{facilityName} 계획평면도
|
||||||
|
</TableHead>
|
||||||
<TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell>
|
<TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{hasFile ? (
|
{hasFile ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{downloadAtflCd ? (
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
<a
|
{displayFileName}
|
||||||
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
</TextShimmer>
|
||||||
onClick={() => downloadAttachment(downloadAtflCd)}
|
|
||||||
>
|
|
||||||
<Paperclip className="size-5 shrink-0" />
|
|
||||||
{displayFileName}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="text-xl">{displayFileName}</span>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { Paperclip } from "lucide-react";
|
||||||
|
|
||||||
|
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||||
|
import { useFacilityList } from "@/entities/bns-if";
|
||||||
|
import { useNowDmgMtncGnsvy } from "@/entities/dc";
|
||||||
|
import type {
|
||||||
|
SewerBscPlanDtlResponseDto,
|
||||||
|
SewerBscPlanResponseDto,
|
||||||
|
SewerBscPlanTmplntResponseDto,
|
||||||
|
} from "@/entities/sewer-bsc-plan";
|
||||||
|
import { fmtNumber, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getOriginalValue,
|
||||||
|
getOriginalYear,
|
||||||
|
PLAN_STP_NOS,
|
||||||
|
type TmplntFieldKey,
|
||||||
|
} from "../../lib/tmplnt-original-mapper";
|
||||||
|
|
||||||
|
interface FacilityOption {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TmplntSectionProps {
|
||||||
|
tmplntList: SewerBscPlanTmplntResponseDto[];
|
||||||
|
det: SewerBscPlanResponseDto;
|
||||||
|
dtl?: SewerBscPlanDtlResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFacilityName(
|
||||||
|
item: SewerBscPlanTmplntResponseDto,
|
||||||
|
facilityOptions: FacilityOption[],
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
item.swtrmtFcltNm || facilityOptions.find((option) => option.code === item.swtrmtFcltCd)?.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TMPLNT_FIELD_ROWS: { key: TmplntFieldKey; label: string }[] = [
|
||||||
|
{ key: "prcsZoneArea", label: "처리구역면적(km²)" },
|
||||||
|
{ key: "swtrmtPpltnCnt", label: "하수처리인구(명)" },
|
||||||
|
{ key: "planSwqty", label: "계획하수량(㎥/일)" },
|
||||||
|
{ key: "fcltCpct", label: "시설용량(㎥/일)" },
|
||||||
|
{ key: "sprsysDrtwPplLen", label: "분류식 오수(km)" },
|
||||||
|
{ key: "sprsysExlnPplLen", label: "분류식 우수(km)" },
|
||||||
|
{ key: "cbsysPplLen", label: "합류식(km)" },
|
||||||
|
{ key: "icptgPplLen", label: "차집관로(km)" },
|
||||||
|
{ key: "drgCnt", label: "배수설비(개소)" },
|
||||||
|
{ key: "drtwRlyPumpCnt", label: "오수 중계 펌프(개소)" },
|
||||||
|
{ key: "drtwPumpCnt", label: "오수 일반 펌프(개소)" },
|
||||||
|
{ key: "exlnPumpCnt", label: "우수 펌프(개소)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getDtlValue(
|
||||||
|
item: SewerBscPlanTmplntResponseDto,
|
||||||
|
planStpNo: number,
|
||||||
|
fieldKey: TmplntFieldKey,
|
||||||
|
): number | undefined {
|
||||||
|
return item.sewerBscPlanTmplntDtlList?.find((dtl) => dtl.planStpNo === planStpNo)?.[fieldKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TmplntFacilityMatrixProps {
|
||||||
|
item: SewerBscPlanTmplntResponseDto;
|
||||||
|
sido?: string;
|
||||||
|
gugun?: string;
|
||||||
|
crtrYr?: string;
|
||||||
|
changeStepHeaders: { planStpNo: number; year?: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function TmplntFacilityMatrix({
|
||||||
|
item,
|
||||||
|
sido,
|
||||||
|
gugun,
|
||||||
|
crtrYr,
|
||||||
|
changeStepHeaders,
|
||||||
|
}: TmplntFacilityMatrixProps) {
|
||||||
|
const searchParams = useMemo(
|
||||||
|
() => ({
|
||||||
|
swtrmtFcltCd: item.swtrmtFcltCd ?? "",
|
||||||
|
ctpvNm: sido ?? "",
|
||||||
|
sggNm: gugun ?? "",
|
||||||
|
}),
|
||||||
|
[item.swtrmtFcltCd, sido, gugun],
|
||||||
|
);
|
||||||
|
const { data } = useNowDmgMtncGnsvy(searchParams);
|
||||||
|
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="12%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
<col width="10%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
<col width="9.75%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[22rem]" rowSpan={2}>
|
||||||
|
구분
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center" colSpan={4}>
|
||||||
|
당초
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center" rowSpan={2}>
|
||||||
|
현재
|
||||||
|
<br />
|
||||||
|
<span className="font-normal text-muted-foreground">({crtrYr || "-"}년)</span>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center" colSpan={4}>
|
||||||
|
변경
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
{PLAN_STP_NOS.map((planStpNo) => (
|
||||||
|
<TableHead key={`original-${planStpNo}`} className="text-center">
|
||||||
|
{planStpNo}단계
|
||||||
|
<br />
|
||||||
|
<span className="font-normal text-muted-foreground">
|
||||||
|
({getOriginalYear(originalData, planStpNo)})
|
||||||
|
</span>
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
{changeStepHeaders.map((step) => (
|
||||||
|
<TableHead key={`change-${step.planStpNo}`} className="text-center">
|
||||||
|
{step.planStpNo}단계
|
||||||
|
<br />
|
||||||
|
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{TMPLNT_FIELD_ROWS.map((row) => (
|
||||||
|
<TableRow key={row.key}>
|
||||||
|
<TableHead>{row.label}</TableHead>
|
||||||
|
{PLAN_STP_NOS.map((planStpNo) => (
|
||||||
|
<TableCell key={`original-${row.key}-${planStpNo}`} className="text-right">
|
||||||
|
{getOriginalValue(originalData, row.key, planStpNo)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
<TableCell className="text-right">{fmtNumber(getDtlValue(item, 0, row.key))}</TableCell>
|
||||||
|
{changeStepHeaders.map((step) => (
|
||||||
|
<TableCell key={`change-${row.key}-${step.planStpNo}`} className="text-right">
|
||||||
|
{fmtNumber(getDtlValue(item, step.planStpNo, row.key))}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
|
||||||
|
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||||
|
const [selectedCode, setSelectedCode] = useState(tmplntList[0]?.swtrmtFcltCd ?? "");
|
||||||
|
const { data: facilityListData } = useFacilityList(
|
||||||
|
{ sido: det.sido ?? "", gugun: det.gugun ?? "" },
|
||||||
|
{ enabled: Boolean(det.sido) },
|
||||||
|
);
|
||||||
|
|
||||||
|
const facilityOptions =
|
||||||
|
facilityListData?.fctList
|
||||||
|
?.filter((facility) => facility.bscFctCd)
|
||||||
|
.map((facility) => ({
|
||||||
|
code: facility.bscFctCd!,
|
||||||
|
name: facility.bscFctNm ?? facility.bscFctCd!,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const changeStepHeaders = [
|
||||||
|
{ planStpNo: 1, year: dtl?.frlvlGoalYr },
|
||||||
|
{ planStpNo: 2, year: dtl?.sclvlGoalYr },
|
||||||
|
{ planStpNo: 3, year: dtl?.thlvlGoalYr },
|
||||||
|
{ planStpNo: 4, year: dtl?.folvlGoalYr },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (tmplntList.length === 0) {
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="처리시설별 하수도정비기본계획">
|
||||||
|
<p className="py-8 text-center text-muted-foreground">선택된 처리시설이 없습니다.</p>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCollapsible title="처리시설별 하수도정비기본계획">
|
||||||
|
<Tabs
|
||||||
|
value={selectedCode || tmplntList[0]?.swtrmtFcltCd}
|
||||||
|
onValueChange={setSelectedCode}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
{tmplntList.map((item) => {
|
||||||
|
const facilityName = resolveFacilityName(item, facilityOptions);
|
||||||
|
return (
|
||||||
|
<TabsTrigger
|
||||||
|
key={item.swtrmtFcltCd}
|
||||||
|
value={item.swtrmtFcltCd ?? ""}
|
||||||
|
className="!px-6 !py-3 text-xl"
|
||||||
|
>
|
||||||
|
{facilityName}
|
||||||
|
</TabsTrigger>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{tmplntList.map((item) => {
|
||||||
|
const facilityName = resolveFacilityName(item, facilityOptions);
|
||||||
|
return (
|
||||||
|
<TabsContent key={item.swtrmtFcltCd} value={item.swtrmtFcltCd ?? ""}>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<TmplntFacilityMatrix
|
||||||
|
item={item}
|
||||||
|
sido={det.sido}
|
||||||
|
gugun={det.gugun}
|
||||||
|
crtrYr={dtl?.crtrYr}
|
||||||
|
changeStepHeaders={changeStepHeaders}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(item.atchFileGroupId || item.atchFileInfo?.orgnlFileNm) && (
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="35%" />
|
||||||
|
<col width="65%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead align="left">파일명</TableHead>
|
||||||
|
<TableHead>파일첨부</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{facilityName} 계획평면도</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.atchFileGroupId ? (
|
||||||
|
<a
|
||||||
|
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||||
|
onClick={() => downloadAttachment(item.atchFileGroupId ?? "")}
|
||||||
|
>
|
||||||
|
<Paperclip className="size-5 shrink-0" />
|
||||||
|
{item.atchFileInfo?.orgnlFileNm || "다운로드"}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
</SectionCollapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TmplntSection };
|
||||||
@@ -79,7 +79,9 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
|||||||
setTaskSeComCd("");
|
setTaskSeComCd("");
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const canConfirm = Boolean(sidoComCd && sewerBscPlanSeComCd && taskSeComCd);
|
const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
|
||||||
|
const canConfirm =
|
||||||
|
!!sewerBscPlanSeComCd && !!taskSeComCd && (!canSidoGugunSelectable || !!sidoComCd);
|
||||||
|
|
||||||
function handleConfirm() {
|
function handleConfirm() {
|
||||||
if (!canConfirm) return;
|
if (!canConfirm) return;
|
||||||
@@ -118,48 +120,50 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
|||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
{canSidoGugunSelectable && (
|
||||||
<TableHead data-required>시도</TableHead>
|
<TableRow>
|
||||||
<TableCell>
|
<TableHead data-required>시도</TableHead>
|
||||||
<Select
|
<TableCell>
|
||||||
value={sidoComCd || undefined}
|
<Select
|
||||||
onValueChange={(value) => {
|
value={sidoComCd || undefined}
|
||||||
setSidoComCd(value);
|
onValueChange={(value) => {
|
||||||
setSggComCd("");
|
setSidoComCd(value);
|
||||||
}}
|
setSggComCd("");
|
||||||
>
|
}}
|
||||||
<SelectTrigger className="w-full">
|
>
|
||||||
<SelectValue placeholder="선택" />
|
<SelectTrigger className="w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="선택" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{sidoOptions.map((option) => (
|
<SelectContent>
|
||||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
{sidoOptions.map((option) => (
|
||||||
{option.adstDstrNm}
|
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||||
</SelectItem>
|
{option.adstDstrNm}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
</TableCell>
|
</Select>
|
||||||
<TableHead>시군구</TableHead>
|
</TableCell>
|
||||||
<TableCell>
|
<TableHead>시군구</TableHead>
|
||||||
<Select
|
<TableCell>
|
||||||
value={sggComCd || undefined}
|
<Select
|
||||||
onValueChange={setSggComCd}
|
value={sggComCd || undefined}
|
||||||
disabled={!sidoComCd || isGugunPending}
|
onValueChange={setSggComCd}
|
||||||
>
|
disabled={!sidoComCd || isGugunPending}
|
||||||
<SelectTrigger className="w-full">
|
>
|
||||||
<SelectValue placeholder="선택" />
|
<SelectTrigger className="w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="선택" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{gugunOptions.map((option) => (
|
<SelectContent>
|
||||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
{gugunOptions.map((option) => (
|
||||||
{option.adstDstrNm}
|
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||||
</SelectItem>
|
{option.adstDstrNm}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
</TableCell>
|
</Select>
|
||||||
</TableRow>
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead data-required>계획구분</TableHead>
|
<TableHead data-required>계획구분</TableHead>
|
||||||
<TableCell colSpan={3}>
|
<TableCell colSpan={3}>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||||
|
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||||
import {
|
import {
|
||||||
SewerBscPlanApplySaveResponseDto,
|
SewerBscPlanApplySaveResponseDto,
|
||||||
sewerBscPlanBaseKey,
|
sewerBscPlanBaseKey,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
ChecklistFormSection,
|
ChecklistFormSection,
|
||||||
ContactInfoFormSection,
|
ContactInfoFormSection,
|
||||||
isFormChecklistComplete,
|
isFormChecklistComplete,
|
||||||
|
isFormEsntlAtchComplete,
|
||||||
PrivacyConsentSection,
|
PrivacyConsentSection,
|
||||||
SplmntDmndFormSection,
|
SplmntDmndFormSection,
|
||||||
} from "@/widgets/pbadms-sprt";
|
} from "@/widgets/pbadms-sprt";
|
||||||
@@ -34,7 +36,6 @@ import {
|
|||||||
mapDetailToFormData,
|
mapDetailToFormData,
|
||||||
} from "../lib/form-data-mapper";
|
} from "../lib/form-data-mapper";
|
||||||
import {
|
import {
|
||||||
SEWER_BSC_PLAN_DOC_SE_COM_CD,
|
|
||||||
type SewerBscPlanRegistFormData,
|
type SewerBscPlanRegistFormData,
|
||||||
sewerBscPlanRegistSchema,
|
sewerBscPlanRegistSchema,
|
||||||
} from "../lib/sewer-bsc-plan-regist-schema";
|
} from "../lib/sewer-bsc-plan-regist-schema";
|
||||||
@@ -76,6 +77,7 @@ function SewerBscPlanFormPage({
|
|||||||
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
|
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
|
||||||
|
|
||||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||||
|
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "SEWER" });
|
||||||
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
||||||
|
|
||||||
const form = useForm<SewerBscPlanRegistFormData>({
|
const form = useForm<SewerBscPlanRegistFormData>({
|
||||||
@@ -140,18 +142,28 @@ function SewerBscPlanFormPage({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.sewerBscPlanTmplntList.some((item) => !item.atchFileGroupId?.trim())) {
|
||||||
|
alert("처리구역 필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFormChecklistComplete(data.cklstResultList)) {
|
if (!isFormChecklistComplete(data.cklstResultList)) {
|
||||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||||
|
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
const docNo = await submitForm(data, "LE");
|
const result = await submitForm(data, "LE");
|
||||||
if (!docNo) return;
|
if (!result?.sewerBscPlanDocNo) return;
|
||||||
|
|
||||||
alert("신청 완료되었습니다.");
|
alert("신청 완료되었습니다.");
|
||||||
router.push(`/sewer-bsc-plan/${docNo}`);
|
router.push(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl ?? 1}`);
|
||||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("신청 실패:", error);
|
console.error("신청 실패:", error);
|
||||||
@@ -165,10 +177,10 @@ function SewerBscPlanFormPage({
|
|||||||
if (isSubmitting) return;
|
if (isSubmitting) return;
|
||||||
|
|
||||||
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
||||||
(item) => !item.splmntCn || !item.splmntCn.trim(),
|
(item) => item.cmptnYn === "N" && (!item.splmntCn || !item.splmntCn.trim()),
|
||||||
);
|
);
|
||||||
if (hasEmptySplmntCn) {
|
if (hasEmptySplmntCn) {
|
||||||
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
|
alert("보완요청 항목이 있습니다.\n보완완료 내용을 입력해 주세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +199,8 @@ function SewerBscPlanFormPage({
|
|||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
alert("보완완료 처리되었습니다.");
|
alert("보완완료 처리되었습니다.");
|
||||||
router.push(`/sewer-bsc-plan/${docNo}`);
|
const chgCycl = data.sewerBscPlanDtl.chgCycl ?? initialData?.detDto?.chgCycl ?? 1;
|
||||||
|
router.push(`/sewer-bsc-plan/${docNo}?chgCycl=${chgCycl}`);
|
||||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("보완완료 실패:", error);
|
console.error("보완완료 실패:", error);
|
||||||
@@ -236,12 +249,9 @@ function SewerBscPlanFormPage({
|
|||||||
<BizCstMatrixFormSection />
|
<BizCstMatrixFormSection />
|
||||||
<FnrsCstFormSection />
|
<FnrsCstFormSection />
|
||||||
<TmplntFormSection />
|
<TmplntFormSection />
|
||||||
<ChecklistFormSection
|
<ChecklistFormSection docSeComCd="SEWER" fieldPrefix="sewerBscPlan" />
|
||||||
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD}
|
|
||||||
fieldPrefix="sewerBscPlan"
|
|
||||||
/>
|
|
||||||
<AttachmentsFormSection
|
<AttachmentsFormSection
|
||||||
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD}
|
docSeComCd="SEWER"
|
||||||
fieldPrefix="sewerBscPlanDtl"
|
fieldPrefix="sewerBscPlanDtl"
|
||||||
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||||
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
|
|||||||
router.push(
|
router.push(
|
||||||
canEdit
|
canEdit
|
||||||
? `/sewer-bsc-plan/${row.sewerBscPlanDocNo}/edit?chgCycl=${row.chgCycl}`
|
? `/sewer-bsc-plan/${row.sewerBscPlanDocNo}/edit?chgCycl=${row.chgCycl}`
|
||||||
: `/sewer-bsc-plan/${row.sewerBscPlanDocNo}`,
|
: `/sewer-bsc-plan/${row.sewerBscPlanDocNo}?chgCycl=${row.chgCycl}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useRef } from "react";
|
||||||
|
|
||||||
|
import { useUserStore } from "@/shared/lib";
|
||||||
|
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||||
|
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||||
|
import { useSewerBscPlanDetails } from "@/entities/sewer-bsc-plan";
|
||||||
|
import { SectionCollapsible, 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 { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||||
|
import { AttachmentsSection } from "./sections/attachments-section";
|
||||||
|
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||||
|
import { BizCstMatrixSection } from "./sections/biz-cst-matrix-section";
|
||||||
|
import { ChecklistSection } from "./sections/checklist-section";
|
||||||
|
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||||
|
import { FnrsCstSection } from "./sections/fnrs-cst-section";
|
||||||
|
import { OutlineSection } from "./sections/outline-section";
|
||||||
|
import {
|
||||||
|
TechReviewOpinionSection,
|
||||||
|
type TechReviewOpinionSectionHandle,
|
||||||
|
} from "./sections/tech-review-opinion-section";
|
||||||
|
import { TmplntSection } from "./sections/tmplnt-section";
|
||||||
|
|
||||||
|
interface SewerBscPlanViewPageProps {
|
||||||
|
id: string;
|
||||||
|
propChgCycl?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
|
||||||
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
|
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||||
|
const chgCycl = propChgCycl ?? 1;
|
||||||
|
const { data, isLoading, isError } = useSewerBscPlanDetails(id, chgCycl);
|
||||||
|
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||||
|
useChecklistFormState(data?.cklstResultRows ?? 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?.detDto) {
|
||||||
|
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detDto = data.detDto;
|
||||||
|
const activeDtlDto = data.activeDtlDto;
|
||||||
|
const otlnList = data.otlnList ?? [];
|
||||||
|
const bizCstList = data.bizCstList ?? [];
|
||||||
|
const fnrsCstList = data.fnrsCstList ?? [];
|
||||||
|
const tmplntList = data.tmplntList ?? [];
|
||||||
|
const cklstResultRows = data.cklstResultRows ?? [];
|
||||||
|
const esntlFileList = data.esntlFileList ?? [];
|
||||||
|
const etcFileList = data.etcFileList ?? [];
|
||||||
|
const rvwOpnnList = data.rvwOpnnList ?? [];
|
||||||
|
const splmntDmndList = data.splmntDmndList ?? [];
|
||||||
|
|
||||||
|
const editableReviewStep = getEditableReviewStep(
|
||||||
|
userInfo?.dtlInstCd,
|
||||||
|
detDto.prgrsSttsComCd || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<BasicInfoSection det={detDto} dtl={activeDtlDto} />
|
||||||
|
<ContactInfoSection dtl={activeDtlDto} />
|
||||||
|
<SectionCollapsible title="결재현황">
|
||||||
|
<ElaprDocPrgrsStatus elaprDocNo={detDto.sewerBscPlanDocNo} />
|
||||||
|
</SectionCollapsible>
|
||||||
|
<OutlineSection otlnList={otlnList} dtl={activeDtlDto} />
|
||||||
|
<BizCstMatrixSection bizCstList={bizCstList} dtl={activeDtlDto} />
|
||||||
|
<FnrsCstSection fnrsCstList={fnrsCstList} dtl={activeDtlDto} />
|
||||||
|
<TmplntSection tmplntList={tmplntList} det={detDto} dtl={activeDtlDto} />
|
||||||
|
<ChecklistSection
|
||||||
|
cklstResultList={cklstResultRows}
|
||||||
|
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||||
|
userRoleCode={userInfo?.dtlInstCd}
|
||||||
|
formState={checklistFormState}
|
||||||
|
setFormState={setChecklistFormState}
|
||||||
|
/>
|
||||||
|
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||||
|
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||||
|
<TechReviewOpinionSection
|
||||||
|
ref={techReviewOpinionRef}
|
||||||
|
rvwList={rvwOpnnList}
|
||||||
|
editableReviewStep={editableReviewStep}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
|
/>
|
||||||
|
<FloatingActionButtons
|
||||||
|
data={data}
|
||||||
|
docSeComCd="SEWER"
|
||||||
|
sewerBscPlanDocNo={detDto.sewerBscPlanDocNo}
|
||||||
|
chgCycl={chgCycl}
|
||||||
|
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||||
|
dtlInstCd={userInfo?.dtlInstCd}
|
||||||
|
atrzSeComCdList={atrzSeComCdList}
|
||||||
|
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||||
|
checklistFormState={checklistFormState}
|
||||||
|
techReviewOpinionRef={techReviewOpinionRef}
|
||||||
|
opnrNm={userInfo?.userName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SewerBscPlanViewPage };
|
||||||
@@ -188,7 +188,7 @@ export interface EsntlAtchFileSaveBase {
|
|||||||
|
|
||||||
/** 보완요구 저장 공통 필드 */
|
/** 보완요구 저장 공통 필드 */
|
||||||
export interface SplmntDmndSaveBase {
|
export interface SplmntDmndSaveBase {
|
||||||
splmntDmndSn?: number;
|
splmntDmndSn?: number | null;
|
||||||
splmntArtclComCd?: string;
|
splmntArtclComCd?: string;
|
||||||
splmntDmndCn?: string;
|
splmntDmndCn?: string;
|
||||||
splmntDmndDt?: string;
|
splmntDmndDt?: string;
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Button, Dialog, DialogClose, DialogContent, DialogFooter } from "@common/components";
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
} from "@common/components";
|
|
||||||
|
|
||||||
import { useAlertStore } from "../lib/use-alert-store";
|
import { useAlertStore } from "../lib/use-alert-store";
|
||||||
|
|
||||||
@@ -20,7 +14,7 @@ function AlertDialog() {
|
|||||||
<Dialog open={!!message} onOpenChange={handleOpenChange}>
|
<Dialog open={!!message} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent aria-describedby={undefined}>
|
<DialogContent aria-describedby={undefined}>
|
||||||
<div className="py-10">
|
<div className="py-10">
|
||||||
<p className="text-foreground whitespace-pre-line text-center text-[1.4rem]">
|
<p className="whitespace-pre-line break-keep text-center text-[1.4rem] text-foreground">
|
||||||
{message}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export { PageLayout } from "./page-layout";
|
|
||||||
export { NoData } from "./no-data";
|
export { NoData } from "./no-data";
|
||||||
|
export { PageLayout } from "./page-layout";
|
||||||
|
export type { TextShimmerProps } from "./text-shimmer";
|
||||||
|
export { TEXT_SHIMMER_CLASS, TextShimmer } from "./text-shimmer";
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { cn } from "@common/components";
|
||||||
|
|
||||||
|
const TEXT_SHIMMER_CLASS =
|
||||||
|
"animate-text-shimmer bg-gradient-to-r from-muted-foreground via-muted to-muted-foreground bg-[length:200%_100%] bg-clip-text text-transparent";
|
||||||
|
|
||||||
|
interface TextShimmerProps extends React.ComponentPropsWithoutRef<"span"> {
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextShimmer({ active = false, className, children, ...props }: TextShimmerProps) {
|
||||||
|
return (
|
||||||
|
<span className={cn(className, active && TEXT_SHIMMER_CLASS)} {...props}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TEXT_SHIMMER_CLASS, TextShimmer };
|
||||||
|
export type { TextShimmerProps };
|
||||||
@@ -3,6 +3,7 @@ export {
|
|||||||
createEtcUploadKey,
|
createEtcUploadKey,
|
||||||
createUploadKey,
|
createUploadKey,
|
||||||
isEtcFileKey,
|
isEtcFileKey,
|
||||||
|
isFormEsntlAtchComplete,
|
||||||
} from "./lib/attachments-form-utils";
|
} from "./lib/attachments-form-utils";
|
||||||
export {
|
export {
|
||||||
buildAcntgCostItems,
|
buildAcntgCostItems,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getDefaultFileKey } from "@/shared/lib";
|
import { getDefaultFileKey } from "@/shared/lib";
|
||||||
|
import type { EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
|
||||||
|
|
||||||
export type FieldPrefix =
|
export type FieldPrefix =
|
||||||
| "fnrsCnsltnMng"
|
| "fnrsCnsltnMng"
|
||||||
@@ -9,6 +10,11 @@ export type FieldPrefix =
|
|||||||
|
|
||||||
export const ETC_FILE_KEY_PREFIX = "etc::";
|
export const ETC_FILE_KEY_PREFIX = "etc::";
|
||||||
|
|
||||||
|
interface EsntlAtchFileListItem {
|
||||||
|
esntlAtchFileId: string;
|
||||||
|
atchFileGroupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function atchFileGroupIdPath(fieldPrefix: FieldPrefix): string {
|
export function atchFileGroupIdPath(fieldPrefix: FieldPrefix): string {
|
||||||
return `${fieldPrefix}.atchFileGroupId`;
|
return `${fieldPrefix}.atchFileGroupId`;
|
||||||
}
|
}
|
||||||
@@ -35,3 +41,21 @@ export function toFileAccept(rmrk: string | null | undefined): string {
|
|||||||
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
||||||
.join(",");
|
.join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFormEsntlAtchComplete(
|
||||||
|
essentialFiles: EsntlAtchFileMngTreeResponseDto[],
|
||||||
|
esntlAtchFileList: EsntlAtchFileListItem[],
|
||||||
|
): boolean {
|
||||||
|
if (essentialFiles.length === 0) return true;
|
||||||
|
|
||||||
|
const requiredFiles = essentialFiles.filter((file) => file.esntlAtchYn === "Y");
|
||||||
|
if (requiredFiles.length === 0) return true;
|
||||||
|
|
||||||
|
const attachedIds = new Set(
|
||||||
|
esntlAtchFileList
|
||||||
|
.filter((item) => Boolean(item.atchFileGroupId))
|
||||||
|
.map((item) => item.esntlAtchFileId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return requiredFiles.every((file) => attachedIds.has(file.esntlAtchFileId));
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { useTus } from "@/shared/lib";
|
import { useTus } from "@/shared/lib";
|
||||||
import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
|
import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { attachService } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||||
import {
|
import {
|
||||||
@@ -413,20 +414,21 @@ function AttachmentsFormSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.fileKey} className="flex items-center gap-2">
|
<div key={item.fileKey} className="flex items-center gap-2">
|
||||||
<span className="text-xl">
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
{item.file?.name ?? item.fileName}
|
{item.file?.name ?? item.fileName}
|
||||||
</span>
|
</TextShimmer>
|
||||||
<Button
|
{!uploading && (
|
||||||
size="icon"
|
<Button
|
||||||
onClick={(e) => {
|
size="icon"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
void removeEssentialFile(file.esntlAtchFileId, idx);
|
e.preventDefault();
|
||||||
}}
|
void removeEssentialFile(file.esntlAtchFileId, idx);
|
||||||
disabled={uploading}
|
}}
|
||||||
className="size-8 !bg-destructive"
|
className="size-8 !bg-destructive"
|
||||||
>
|
>
|
||||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -489,7 +491,9 @@ function AttachmentsFormSection({
|
|||||||
key={item.fileKey}
|
key={item.fileKey}
|
||||||
className="flex items-center justify-between rounded-sm border p-2"
|
className="flex items-center justify-between rounded-sm border p-2"
|
||||||
>
|
>
|
||||||
<span className="px-2 text-xl">{item.file?.name ?? item.fileName}</span>
|
<TextShimmer className="px-2 text-xl" active={uploading}>
|
||||||
|
{item.file?.name ?? item.fileName}
|
||||||
|
</TextShimmer>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="gray"
|
color="gray"
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
import { buildChecklistTabs, normalizeTaskSeComCdArray, syncCklstResultListFromTree } from "../lib/checklist-form-utils";
|
import {
|
||||||
|
buildChecklistTabs,
|
||||||
|
normalizeTaskSeComCdArray,
|
||||||
|
syncCklstResultListFromTree,
|
||||||
|
} from "../lib/checklist-form-utils";
|
||||||
|
|
||||||
import { SectionCollapsible } from "./section-collapsible";
|
import { SectionCollapsible } from "./section-collapsible";
|
||||||
|
|
||||||
@@ -49,10 +53,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
|||||||
const { watch, setValue, control, getValues } = useFormContext();
|
const { watch, setValue, control, getValues } = useFormContext();
|
||||||
const [activeTab, setActiveTab] = useState("");
|
const [activeTab, setActiveTab] = useState("");
|
||||||
const taskSeComCd = watch(`${fieldPrefix}.taskSeComCd`) as string | string[] | undefined;
|
const taskSeComCd = watch(`${fieldPrefix}.taskSeComCd`) as string | string[] | undefined;
|
||||||
const taskSeComCdArray = useMemo(
|
const taskSeComCdArray = useMemo(() => normalizeTaskSeComCdArray(taskSeComCd), [taskSeComCd]);
|
||||||
() => normalizeTaskSeComCdArray(taskSeComCd),
|
|
||||||
[taskSeComCd],
|
|
||||||
);
|
|
||||||
const cklstResultList =
|
const cklstResultList =
|
||||||
(useWatch({ control, name: "cklstResultList" }) as CklstResultListItem[] | undefined) ?? [];
|
(useWatch({ control, name: "cklstResultList" }) as CklstResultListItem[] | undefined) ?? [];
|
||||||
|
|
||||||
@@ -206,6 +207,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
color="white"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSelectAll(tab.code, "Y");
|
handleSelectAll(tab.code, "Y");
|
||||||
@@ -218,11 +220,11 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
color="gray"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSelectAll(tab.code, "N");
|
handleSelectAll(tab.code, "N");
|
||||||
}}
|
}}
|
||||||
className="!bg-destructive"
|
|
||||||
>
|
>
|
||||||
전체 아니오
|
전체 아니오
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { Field } from "@/shared/ui/field";
|
import { Field } from "@/shared/ui/field";
|
||||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||||
import type { AtflMngClientResponseDto } from "@/entities/fnrs-cnsltn";
|
import type { AtflMngClientResponseDto } from "@/entities/fnrs-cnsltn";
|
||||||
@@ -257,12 +258,18 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
|||||||
<div className="mt-6 px-2 pb-2">
|
<div className="mt-6 px-2 pb-2">
|
||||||
<h3 className="mb-2 text-xl font-semibold">첨부파일</h3>
|
<h3 className="mb-2 text-xl font-semibold">첨부파일</h3>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Input
|
{isSharUploading && uploadedFileName ? (
|
||||||
value={uploadedFileName}
|
<TextShimmer className="max-w-lg text-xl" active>
|
||||||
readOnly
|
{uploadedFileName}
|
||||||
placeholder="총사업비 신청서(.hwp) 파일을 등록해주세요."
|
</TextShimmer>
|
||||||
className="max-w-lg rounded-sm"
|
) : (
|
||||||
/>
|
<Input
|
||||||
|
value={uploadedFileName}
|
||||||
|
readOnly
|
||||||
|
placeholder="총사업비 신청서(.hwp) 파일을 등록해주세요."
|
||||||
|
className="max-w-lg rounded-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button type="button" color="white" disabled={isSharUploading} asChild>
|
<Button type="button" color="white" disabled={isSharUploading} asChild>
|
||||||
<label className="cursor-pointer">
|
<label className="cursor-pointer">
|
||||||
첨부파일
|
첨부파일
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ function PbadmsTaskApplyDialog({
|
|||||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||||
const isPending = isSidoPending || isTaskSePending;
|
const isPending = isSidoPending || isTaskSePending;
|
||||||
const canConfirm = Boolean(sidoComCd && selectedCodes.length > 0);
|
const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
|
||||||
|
const canConfirm = selectedCodes.length > 0 && (!canSidoGugunSelectable || !!sidoComCd);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -121,48 +122,50 @@ function PbadmsTaskApplyDialog({
|
|||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
{canSidoGugunSelectable && (
|
||||||
<TableHead data-required>시도</TableHead>
|
<TableRow>
|
||||||
<TableCell>
|
<TableHead data-required>시도</TableHead>
|
||||||
<Select
|
<TableCell>
|
||||||
value={sidoComCd || undefined}
|
<Select
|
||||||
onValueChange={(value) => {
|
value={sidoComCd || undefined}
|
||||||
setSidoComCd(value);
|
onValueChange={(value) => {
|
||||||
setSggComCd("");
|
setSidoComCd(value);
|
||||||
}}
|
setSggComCd("");
|
||||||
>
|
}}
|
||||||
<SelectTrigger className="w-full">
|
>
|
||||||
<SelectValue placeholder="선택" />
|
<SelectTrigger className="w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="선택" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{sidoOptions.map((option) => (
|
<SelectContent>
|
||||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
{sidoOptions.map((option) => (
|
||||||
{option.adstDstrNm}
|
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||||
</SelectItem>
|
{option.adstDstrNm}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
</TableCell>
|
</Select>
|
||||||
<TableHead>시군구</TableHead>
|
</TableCell>
|
||||||
<TableCell>
|
<TableHead>시군구</TableHead>
|
||||||
<Select
|
<TableCell>
|
||||||
value={sggComCd || undefined}
|
<Select
|
||||||
onValueChange={setSggComCd}
|
value={sggComCd || undefined}
|
||||||
disabled={!sidoComCd || isGugunPending}
|
onValueChange={setSggComCd}
|
||||||
>
|
disabled={!sidoComCd || isGugunPending}
|
||||||
<SelectTrigger className="w-full">
|
>
|
||||||
<SelectValue placeholder="선택" />
|
<SelectTrigger className="w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="선택" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{gugunOptions.map((option) => (
|
<SelectContent>
|
||||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
{gugunOptions.map((option) => (
|
||||||
{option.adstDstrNm}
|
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||||
</SelectItem>
|
{option.adstDstrNm}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
</TableCell>
|
</Select>
|
||||||
</TableRow>
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead data-required>업무구분</TableHead>
|
<TableHead data-required>업무구분</TableHead>
|
||||||
<TableCell colSpan={3}>
|
<TableCell colSpan={3}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useId, useState } from "react";
|
import { useEffect, useId, useState } from "react";
|
||||||
|
|
||||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { attachService } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import { CodeSelect } from "@/entities/code";
|
import { CodeSelect } from "@/entities/code";
|
||||||
import {
|
import {
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
const SPLMNT_ARTCL_UP_CD = "SPLMNTARTCL";
|
|
||||||
const SPLMNT_DMND_FILE_KEY_PREFIX = "splmnt-dmnd::";
|
const SPLMNT_DMND_FILE_KEY_PREFIX = "splmnt-dmnd::";
|
||||||
|
|
||||||
interface UploadedSplmntFile {
|
interface UploadedSplmntFile {
|
||||||
@@ -50,6 +50,7 @@ interface SplmntDmndDialogConfirmPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SplmntDmndDialogProps {
|
interface SplmntDmndDialogProps {
|
||||||
|
splmntArtclUpCd: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
@@ -86,7 +87,13 @@ function isRowsValid(rows: SplmntDmndRow[]) {
|
|||||||
return rows.every((row) => row.splmntArtclComCd.trim() && row.splmntDmndCn.trim());
|
return rows.every((row) => row.splmntArtclComCd.trim() && row.splmntDmndCn.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function SplmntDmndDialog({ open, onOpenChange, isSubmitting, onConfirm }: SplmntDmndDialogProps) {
|
function SplmntDmndDialog({
|
||||||
|
splmntArtclUpCd,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
isSubmitting,
|
||||||
|
onConfirm,
|
||||||
|
}: SplmntDmndDialogProps) {
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
const [rows, setRows] = useState<SplmntDmndRow[]>([createEmptyRow()]);
|
const [rows, setRows] = useState<SplmntDmndRow[]>([createEmptyRow()]);
|
||||||
|
|
||||||
@@ -239,7 +246,7 @@ function SplmntDmndDialog({ open, onOpenChange, isSubmitting, onConfirm }: Splmn
|
|||||||
<TableRow key={row.rowKey}>
|
<TableRow key={row.rowKey}>
|
||||||
<TableCell align="center" className="align-top">
|
<TableCell align="center" className="align-top">
|
||||||
<CodeSelect
|
<CodeSelect
|
||||||
upCd={SPLMNT_ARTCL_UP_CD}
|
upCd={splmntArtclUpCd}
|
||||||
value={row.splmntArtclComCd || undefined}
|
value={row.splmntArtclComCd || undefined}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateRow(row.rowKey, { splmntArtclComCd: value })
|
updateRow(row.rowKey, { splmntArtclComCd: value })
|
||||||
@@ -263,7 +270,9 @@ function SplmntDmndDialog({ open, onOpenChange, isSubmitting, onConfirm }: Splmn
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{row.uploadedFile ? (
|
{row.uploadedFile ? (
|
||||||
<div className="flex items-stretch gap-2">
|
<div className="flex items-stretch gap-2">
|
||||||
<span className="text-xl">{row.uploadedFile.fileName}</span>
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
|
{row.uploadedFile.fileName}
|
||||||
|
</TextShimmer>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
|
|||||||
import { Paperclip } from "lucide-react";
|
import { Paperclip } from "lucide-react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -272,7 +273,9 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
|
|||||||
{uploaded.fileName}
|
{uploaded.fileName}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xl">{uploaded.fileName}</span>
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
|
{uploaded.fileName}
|
||||||
|
</TextShimmer>
|
||||||
)}
|
)}
|
||||||
{!isRowReadOnly ? (
|
{!isRowReadOnly ? (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ interface SupplementDemandSectionProps {
|
|||||||
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
|
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
|
||||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||||
|
|
||||||
|
if (splmntDmndList.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="보완요청 및 완료 현황">
|
<SectionCollapsible title="보완요청 및 완료 현황">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|||||||
@@ -83,12 +83,17 @@ export default {
|
|||||||
from: { transform: "translate(0,0) rotate(0deg)" },
|
from: { transform: "translate(0,0) rotate(0deg)" },
|
||||||
to: { transform: "translate(0,0) rotate(360deg)" },
|
to: { transform: "translate(0,0) rotate(360deg)" },
|
||||||
},
|
},
|
||||||
|
"text-shimmer": {
|
||||||
|
"0%": { backgroundPosition: "200% center" },
|
||||||
|
"100%": { backgroundPosition: "-200% center" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
"spin-parent": "spin-parent 6s linear infinite",
|
"spin-parent": "spin-parent 6s linear infinite",
|
||||||
"spin-child": "spin-child 1s ease-in-out infinite",
|
"spin-child": "spin-child 1s ease-in-out infinite",
|
||||||
|
"text-shimmer": "text-shimmer 1.5s linear infinite",
|
||||||
},
|
},
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
"gradient-blue": "linear-gradient(.25turn, #2094fa, #00e5dd)",
|
"gradient-blue": "linear-gradient(.25turn, #2094fa, #00e5dd)",
|
||||||
|
|||||||
Reference in New Issue
Block a user