Merge branch 'main' into deploy
This commit is contained in:
@@ -1,17 +1,13 @@
|
||||
import Link from "next/link";
|
||||
import { SewerBscPlanViewPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
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;
|
||||
return (
|
||||
<div className="mt-6 px-4 text-[1.4rem]">
|
||||
하수도기본계획 상세({id})는 후속 구현 예정입니다.{" "}
|
||||
<Link className="underline" href={`/sewer-bsc-plan/${id}/edit`}>
|
||||
수정 화면으로 이동
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
const { chgCycl } = await searchParams;
|
||||
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
|
||||
return <SewerBscPlanViewPage id={id} propChgCycl={propChgCycl} />;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ export interface ElaprPrgrsSttsMyDocSearchParams extends ElaprPrgrsSttsSearchDto
|
||||
export interface ElaprProcessRequestDto {
|
||||
atrzPrgrsSttsComCd: string;
|
||||
rjctRsn?: string;
|
||||
finalApprovalYn?: boolean;
|
||||
}
|
||||
|
||||
/** 백엔드 ElaprApproverChangeRequestDto */
|
||||
|
||||
@@ -5,6 +5,8 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
} from "../model/sewer-bsc-plan-apply-payload-schema";
|
||||
import type {
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "../model/sewer-bsc-plan-interface";
|
||||
|
||||
@@ -4,10 +4,12 @@ import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
} from "../model/sewer-bsc-plan-apply-payload-schema";
|
||||
import type {
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
@@ -75,8 +77,11 @@ async function submitReqSupplement(
|
||||
id: string,
|
||||
chgCycl: number,
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/${chgCycl}/reqsupplement`, body);
|
||||
): Promise<SewerBscPlanApplySaveResponseDto> {
|
||||
const response = await client.post<SewerBscPlanApplySaveResponseDto>(
|
||||
`${BASE}/${id}/${chgCycl}/reqsupplement`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,35 +13,52 @@ export {
|
||||
} from "./api/queries";
|
||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstResultItem,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
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,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanDtlResponseDto,
|
||||
SewerBscPlanDtlSaveRequestDto,
|
||||
SewerBscPlanFnrsCstItem,
|
||||
SewerBscPlanFnrsCstResponseDto,
|
||||
SewerBscPlanOtlnItem,
|
||||
SewerBscPlanOtlnResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanRvwOpnnItem,
|
||||
SewerBscPlanRvwOpnnResponseDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanSplmntDmndItem,
|
||||
SewerBscPlanSplmntDmndResponseDto,
|
||||
SewerBscPlanTmplntDtlItem,
|
||||
SewerBscPlanTmplntDtlResponseDto,
|
||||
SewerBscPlanTmplntItem,
|
||||
SewerBscPlanTmplntResponseDto,
|
||||
SewerBscPlanTmplntTmprStrgItem,
|
||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} 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 {
|
||||
AtflMngClientResponseDto,
|
||||
CklstResultSaveBase,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
EsntlAtchFileSaveBase,
|
||||
PageableParams,
|
||||
PageResponse,
|
||||
PbadmsApplySaveHeader,
|
||||
RvwOpnnResponseExtension,
|
||||
RvwOpnnSaveBase,
|
||||
SplmntDmndResponseExtension,
|
||||
SplmntDmndSaveBase,
|
||||
} from "@/shared/model";
|
||||
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
} from "./sewer-bsc-plan-apply-payload-schema";
|
||||
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/shared/model";
|
||||
|
||||
/** 백엔드 SewerBscPlanSaveRequestDto */
|
||||
export interface SewerBscPlanSaveRequestDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanNm?: string;
|
||||
taskSeComCd?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanUpdateRequestDto */
|
||||
export interface SewerBscPlanUpdateRequestDto {
|
||||
admdstCd?: string;
|
||||
@@ -72,28 +63,6 @@ export interface SewerBscPlanResponseDto {
|
||||
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 */
|
||||
export interface SewerBscPlanDtlResponseDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
@@ -210,103 +179,6 @@ export interface SewerBscPlanTmplntTmprStrgResponseDto {
|
||||
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 */
|
||||
export interface SewerBscPlanApplySaveResponseDto {
|
||||
sewerBscPlanDocNo: string;
|
||||
@@ -346,6 +218,6 @@ export interface SewerBscPlanService {
|
||||
id: string,
|
||||
chgCycl: number,
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<string>;
|
||||
): Promise<SewerBscPlanApplySaveResponseDto>;
|
||||
techReview(id: string, body: SewerBscPlanApplySaveRequestDto): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
type DesignCnstnMngDetResponseDto,
|
||||
designCnstnMngService,
|
||||
} from "@/entities/design-cnstn";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
AttachmentsFormSection,
|
||||
BusinessScaleFormSection,
|
||||
ContactInfoFormSection,
|
||||
isFormEsntlAtchComplete,
|
||||
PrivacyConsentSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
@@ -53,6 +55,8 @@ function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPag
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const isEditMode = mode === "edit";
|
||||
|
||||
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "CNFM" });
|
||||
|
||||
const form = useForm<DesignCnstnRegistFormData>({
|
||||
resolver: zodResolver(designCnstnRegistSchema),
|
||||
mode: "onChange",
|
||||
@@ -113,6 +117,11 @@ function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPag
|
||||
async function onApply(data: DesignCnstnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const designCnstnDocNo = await submitForm(data, "CT");
|
||||
|
||||
@@ -62,49 +62,51 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
<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 ? (
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</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)}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : (
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
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 { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
||||
@@ -71,12 +74,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection({
|
||||
rvwList,
|
||||
editableReviewStep,
|
||||
atrzSeComCdList = [],
|
||||
isAtrzLoading = false,
|
||||
}, ref) {
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
@@ -240,7 +241,9 @@ const TechReviewOpinionSection = forwardRef<
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{uploadedFile.fileName}</span>
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
@@ -317,11 +320,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<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}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Paperclip className="my-1 size-5 shrink-0" />
|
||||
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
|
||||
@@ -85,7 +85,7 @@ export const fnrsCnsltnRegistSchema = z.object({
|
||||
),
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().optional(),
|
||||
splmntDmndSn: z.number().nullish(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
|
||||
@@ -46,12 +46,7 @@ function ChecklistTaskPanel({
|
||||
.forEach((row) => {
|
||||
nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value;
|
||||
});
|
||||
return patchChecklistFormState(
|
||||
prev,
|
||||
{ results: nextResults },
|
||||
allRows,
|
||||
selectedReviewStep,
|
||||
);
|
||||
return patchChecklistFormState(prev, { results: nextResults }, allRows, selectedReviewStep);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,6 +72,7 @@ function ChecklistTaskPanel({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("Y")}
|
||||
>
|
||||
@@ -87,9 +83,9 @@ function ChecklistTaskPanel({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("N")}
|
||||
className="!bg-destructive"
|
||||
>
|
||||
전체 아니오
|
||||
</Button>
|
||||
|
||||
@@ -498,6 +498,7 @@ function FloatingActionButtons({
|
||||
/>
|
||||
|
||||
<SplmntDmndDialog
|
||||
splmntArtclUpCd="SPLMNTARTCL"
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
isSubmitting={isProcessing}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
type FnrsCnsltnApplySaveRequestDto,
|
||||
fnrsCnsltnMngBaseKey,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
ContactInfoFormSection,
|
||||
FnrsSharFormSection,
|
||||
isFormChecklistComplete,
|
||||
isFormEsntlAtchComplete,
|
||||
PrivacyConsentSection,
|
||||
recalculateFnrsSharTotals,
|
||||
SplmntDmndFormSection,
|
||||
@@ -67,6 +69,7 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro
|
||||
|
||||
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
|
||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "FNRS" });
|
||||
const sharTree =
|
||||
isEditMode && initialData?.fnrsSharList?.length ? initialData.fnrsSharList : masterSharTree;
|
||||
const sharItems = useMemo(() => buildFnrsSharItems(sharTree), [sharTree]);
|
||||
@@ -210,6 +213,11 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const fnrsCnsltnDocNo = await submitForm(data, "ST");
|
||||
|
||||
@@ -19,98 +19,104 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 ? (
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</TableCell>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</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}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</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>
|
||||
) : (
|
||||
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="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="기타 첨부파일">
|
||||
<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 ? (
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</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)}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : (
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ function ChecklistSection({
|
||||
|
||||
const { total, completed, percent } = progress;
|
||||
|
||||
if (cklstResultList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="체크리스트">
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
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 { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
@@ -71,12 +74,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection({
|
||||
rvwList,
|
||||
editableReviewStep,
|
||||
atrzSeComCdList = [],
|
||||
isAtrzLoading = false,
|
||||
}, ref) {
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
@@ -247,7 +248,9 @@ const TechReviewOpinionSection = forwardRef<
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{uploadedFile.fileName}</span>
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
@@ -336,11 +339,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<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}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Paperclip className="my-1 size-5 shrink-0" />
|
||||
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
|
||||
@@ -77,7 +77,7 @@ export const instlSncnRegistSchema = z.object({
|
||||
),
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().optional(),
|
||||
splmntDmndSn: z.number().nullish(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
|
||||
@@ -45,12 +45,7 @@ function ChecklistTaskPanel({
|
||||
.forEach((row) => {
|
||||
nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value;
|
||||
});
|
||||
return patchChecklistFormState(
|
||||
prev,
|
||||
{ results: nextResults },
|
||||
allRows,
|
||||
selectedReviewStep,
|
||||
);
|
||||
return patchChecklistFormState(prev, { results: nextResults }, allRows, selectedReviewStep);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +71,7 @@ function ChecklistTaskPanel({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("Y")}
|
||||
>
|
||||
@@ -86,9 +82,9 @@ function ChecklistTaskPanel({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("N")}
|
||||
className="!bg-destructive"
|
||||
>
|
||||
전체 아니오
|
||||
</Button>
|
||||
|
||||
@@ -511,6 +511,7 @@ function FloatingActionButtons({
|
||||
/>
|
||||
|
||||
<SplmntDmndDialog
|
||||
splmntArtclUpCd="SPLMNTARTCL"
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
isSubmitting={isProcessing}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
type InstlSncnApplySaveRequestDto,
|
||||
instlSncnKeys,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
ChecklistFormSection,
|
||||
ContactInfoFormSection,
|
||||
isFormChecklistComplete,
|
||||
isFormEsntlAtchComplete,
|
||||
PrivacyConsentSection,
|
||||
SplmntDmndFormSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
@@ -57,6 +59,8 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP
|
||||
const isEditMode = mode === "edit";
|
||||
const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL";
|
||||
|
||||
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "INSTL" });
|
||||
|
||||
const form = useForm<InstlSncnRegistFormData>({
|
||||
resolver: zodResolver(instlSncnRegistSchema),
|
||||
mode: "onChange",
|
||||
@@ -172,6 +176,11 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const instlSncnDocNo = await submitForm(data, "ST");
|
||||
|
||||
@@ -19,98 +19,104 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 ? (
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</TableCell>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</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}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</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>
|
||||
) : (
|
||||
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="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="기타 첨부파일">
|
||||
<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 ? (
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</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)}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : (
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ function ChecklistSection({
|
||||
|
||||
const { total, completed, percent } = progress;
|
||||
|
||||
if (cklstResultList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="체크리스트">
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
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 { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
|
||||
@@ -67,12 +70,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection({
|
||||
rvwList,
|
||||
editableReviewStep,
|
||||
atrzSeComCdList = [],
|
||||
isAtrzLoading = false,
|
||||
}, ref) {
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
@@ -240,7 +241,9 @@ const TechReviewOpinionSection = forwardRef<
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{uploadedFile.fileName}</span>
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
@@ -329,11 +332,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<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}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Paperclip className="my-1 size-5 shrink-0" />
|
||||
{item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
|
||||
@@ -2,3 +2,4 @@ export { SewerBscPlanEditPage } from "./ui/sewer-bsc-plan-edit-page";
|
||||
export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page";
|
||||
export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-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,
|
||||
SewerBscPlanDetResponseDto,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import { sewerBscPlanApplySaveSchema } from "@/entities/sewer-bsc-plan";
|
||||
|
||||
import type { BizCstMatrixItem } from "./biz-cst-matrix-utils";
|
||||
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
|
||||
@@ -244,43 +245,33 @@ export function buildApplyPayload(
|
||||
prgrsSttsComCd: "SV" | "LE" | "TE",
|
||||
options?: { isEditMode?: boolean; docNo?: string },
|
||||
): SewerBscPlanApplySaveRequestDto {
|
||||
const { taskSeComCd, ...sewerBscPlanRest } = data.sewerBscPlan;
|
||||
const docNo = options?.isEditMode ? options.docNo : undefined;
|
||||
|
||||
return {
|
||||
return sewerBscPlanApplySaveSchema.parse({
|
||||
sewerBscPlan: {
|
||||
...sewerBscPlanRest,
|
||||
taskSeComCd: taskSeComCd ?? "",
|
||||
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined,
|
||||
...data.sewerBscPlan,
|
||||
sewerBscPlanDocNo: docNo,
|
||||
taskSeComCd: data.sewerBscPlan.taskSeComCd ?? "",
|
||||
prgrsSttsComCd,
|
||||
},
|
||||
sewerBscPlanDtl: {
|
||||
...data.sewerBscPlanDtl,
|
||||
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined,
|
||||
sewerBscPlanDocNo: docNo,
|
||||
},
|
||||
sewerBscPlanOtlnList: data.sewerBscPlanOtlnList,
|
||||
bizCstList: data.bizCstList,
|
||||
fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)),
|
||||
sewerBscPlanTmplntList: data.sewerBscPlanTmplntList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ atchFileNm, swtrmtFcltNm, ...item }) => ({
|
||||
swtrmtFcltCd: item.swtrmtFcltCd,
|
||||
atchFileGroupId: item.atchFileGroupId,
|
||||
sewerBscPlanTmplntDtlList: item.sewerBscPlanTmplntDtlList,
|
||||
}),
|
||||
),
|
||||
sewerBscPlanTmplntList: data.sewerBscPlanTmplntList,
|
||||
cklstResultList: data.cklstResultList.map((item) => ({
|
||||
...item,
|
||||
selected: item.selected ?? undefined,
|
||||
})),
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
splmntDmndList: data.splmntDmndList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
|
||||
...item,
|
||||
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
|
||||
}),
|
||||
),
|
||||
};
|
||||
splmntDmndList: data.splmntDmndList.map((item) => ({
|
||||
...item,
|
||||
cmptnYn: prgrsSttsComCd === "TE" ? "Y" : item.cmptnYn,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
const tmplntDtlSchema = z.object({
|
||||
planStpNo: z.number(),
|
||||
swtrmtFcltCd: z.string(),
|
||||
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(),
|
||||
});
|
||||
import {
|
||||
sewerBscPlanBizCstItemSchema,
|
||||
sewerBscPlanCklstResultItemSchema,
|
||||
sewerBscPlanDtlSaveSchema,
|
||||
sewerBscPlanEsntlAtchFileItemSchema,
|
||||
sewerBscPlanFnrsCstItemSchema,
|
||||
sewerBscPlanOtlnItemSchema,
|
||||
sewerBscPlanSaveSchema,
|
||||
sewerBscPlanSplmntDmndItemSchema,
|
||||
sewerBscPlanTmplntDtlItemSchema,
|
||||
sewerBscPlanTmplntItemSchema,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
|
||||
export const sewerBscPlanRegistSchema = z.object({
|
||||
sewerBscPlan: z.object({
|
||||
sewerBscPlanDocNo: z.string().optional(),
|
||||
sewerBscPlan: sewerBscPlanSaveSchema.extend({
|
||||
sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"),
|
||||
sewerBscPlanSeComCd: z.string().optional(),
|
||||
sewerBscPlanSeCdNm: z.string().optional(),
|
||||
taskSeComCd: z.string().optional(),
|
||||
taskSeCdNm: z.string().optional(),
|
||||
admdstCd: z.string().optional(),
|
||||
envoSeComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().optional(),
|
||||
sido: z.string().optional(),
|
||||
gugun: z.string().optional(),
|
||||
gugun: z.string().nullish(),
|
||||
}),
|
||||
sewerBscPlanDtl: z.object({
|
||||
chgCycl: z.number().optional(),
|
||||
sewerBscPlanDtl: sewerBscPlanDtlSaveSchema.extend({
|
||||
planYr: z.string().min(1, "계획년도는 필수입니다"),
|
||||
crtrYr: z.string().min(1, "기준년도는 필수입니다"),
|
||||
frlvlGoalYr: z.string().min(1, "1단계 목표연도는 필수입니다"),
|
||||
@@ -40,10 +29,6 @@ export const sewerBscPlanRegistSchema = z.object({
|
||||
thlvlGoalYr: z.string().min(1, "3단계 목표연도는 필수입니다"),
|
||||
folvlGoalYr: z.string().min(1, "4단계 목표연도는 필수입니다"),
|
||||
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, "설계자 이름은 필수입니다"),
|
||||
dsgnrTelno: z
|
||||
.string()
|
||||
@@ -54,74 +39,60 @@ export const sewerBscPlanRegistSchema = z.object({
|
||||
.string()
|
||||
.min(1, "설계자 이메일은 필수입니다")
|
||||
.email("올바른 이메일 형식이 아닙니다"),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
}),
|
||||
sewerBscPlanOtlnList: z.array(
|
||||
z.object({
|
||||
sewerBscPlanOtlnItemSchema.extend({
|
||||
planStpNo: z.number(),
|
||||
sewerPlanPpltnCnt: z.number().optional(),
|
||||
sewerPrcsPpltnCnt: z.number().optional(),
|
||||
sewerPsplyRt: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
bizCstList: z.array(
|
||||
z.object({
|
||||
sewerBscPlanBizCstItemSchema.extend({
|
||||
planStpNo: z.number(),
|
||||
acntgSeCd: z.string(),
|
||||
cstrnAmt: z.number(),
|
||||
}),
|
||||
),
|
||||
fnrsCstList: z.array(
|
||||
z.object({
|
||||
sewerBscPlanFnrsCstItemSchema.extend({
|
||||
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(
|
||||
z.object({
|
||||
sewerBscPlanTmplntItemSchema.extend({
|
||||
swtrmtFcltCd: z.string(),
|
||||
swtrmtFcltNm: z.string().nullish(),
|
||||
atchFileGroupId: 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(
|
||||
z.object({
|
||||
sewerBscPlanCklstResultItemSchema.extend({
|
||||
cklstSn: z.number(),
|
||||
cklstArtclSn: z.number(),
|
||||
cklstRvwSn: z.number(),
|
||||
rvwStpComCd: z.string(),
|
||||
selected: z.enum(["Y", "N"]).nullable(),
|
||||
picRvwCn: z.string().nullable().optional(),
|
||||
}),
|
||||
),
|
||||
esntlAtchFileList: z.array(
|
||||
z.object({
|
||||
sewerBscPlanEsntlAtchFileItemSchema.extend({
|
||||
esntlAtchFileId: z.string(),
|
||||
atchFileGroupId: z.string(),
|
||||
}),
|
||||
),
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().optional(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
sewerBscPlanSplmntDmndItemSchema.extend({
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
splmntDmndDt: z.string().optional(),
|
||||
splmntDmndAtchFileGroupId: 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(),
|
||||
splmntAtchFileAtflId: z.string().optional(),
|
||||
cmptnYn: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
privacyConsent: z.enum(["Y", "N"], {
|
||||
@@ -130,6 +101,3 @@ export const sewerBscPlanRegistSchema = z.object({
|
||||
});
|
||||
|
||||
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: "보완요청",
|
||||
} 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";
|
||||
|
||||
@@ -42,7 +42,7 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, 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") {
|
||||
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: "pending" },
|
||||
{ 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") {
|
||||
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") {
|
||||
if (prgrsSttsComCd === "TA") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, 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" }];
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
@@ -33,7 +33,7 @@ interface PreviousPlanLookupDialogProps {
|
||||
/** 부모 폼 `sewerBscPlan.sido` 표시명 */
|
||||
initialSidoNm?: string;
|
||||
/** 부모 폼 `sewerBscPlan.gugun` 표시명 */
|
||||
initialGugunNm?: string;
|
||||
initialGugunNm?: string | null;
|
||||
}
|
||||
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
@@ -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 { useUserStore } from "@/shared/lib";
|
||||
import { Field, FieldError } from "@/shared/ui/field";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
@@ -23,6 +24,7 @@ import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist
|
||||
import { getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions } from "../../lib/year-options";
|
||||
|
||||
function BasicInfoFormSection() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { control, setValue, watch } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const planYearOptions = getPlanYearOptions();
|
||||
const goalYearOptions = getGoalYearOptions();
|
||||
@@ -265,7 +267,7 @@ function BasicInfoFormSection() {
|
||||
<TableHead>구분</TableHead>
|
||||
<TableCell>{sewerBscPlanSeLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
<TableCell>{localGovLabel || userInfo?.laeSeNm}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</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 { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import { useFacilityList } from "@/entities/bns-if";
|
||||
import { useNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
@@ -226,7 +226,6 @@ function TmplntFacilityAttachments({
|
||||
}: TmplntFacilityAttachmentsProps) {
|
||||
const formId = useId();
|
||||
const { setValue } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedTmplntFile | null>(() =>
|
||||
atchFileGroupId && atchFileNm
|
||||
? {
|
||||
@@ -254,7 +253,6 @@ function TmplntFacilityAttachments({
|
||||
const fileInputId = `${formId}-tmplnt-atch-${facilityIndex}`;
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
const displayFileName = uploadedFile?.fileName ?? atchFileNm ?? "";
|
||||
const downloadAtflCd = uploadedFile?.atflCd || atchFileGroupId;
|
||||
const hasFile = Boolean(uploadedFile || atchFileNm);
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
@@ -315,22 +313,16 @@ function TmplntFacilityAttachments({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>{facilityName} 계획평면도</TableCell>
|
||||
<TableHead data-required align="left">
|
||||
{facilityName} 계획평면도
|
||||
</TableHead>
|
||||
<TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell>
|
||||
<TableCell>
|
||||
{hasFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{downloadAtflCd ? (
|
||||
<a
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(downloadAtflCd)}
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0" />
|
||||
{displayFileName}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-xl">{displayFileName}</span>
|
||||
)}
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{displayFileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
type="button"
|
||||
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("");
|
||||
}, [open]);
|
||||
|
||||
const canConfirm = Boolean(sidoComCd && sewerBscPlanSeComCd && taskSeComCd);
|
||||
const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
|
||||
const canConfirm =
|
||||
!!sewerBscPlanSeComCd && !!taskSeComCd && (!canSidoGugunSelectable || !!sidoComCd);
|
||||
|
||||
function handleConfirm() {
|
||||
if (!canConfirm) return;
|
||||
@@ -118,48 +120,50 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={setSggComCd}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{canSidoGugunSelectable && (
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={setSggComCd}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead data-required>계획구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
sewerBscPlanBaseKey,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
ChecklistFormSection,
|
||||
ContactInfoFormSection,
|
||||
isFormChecklistComplete,
|
||||
isFormEsntlAtchComplete,
|
||||
PrivacyConsentSection,
|
||||
SplmntDmndFormSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
@@ -34,7 +36,6 @@ import {
|
||||
mapDetailToFormData,
|
||||
} from "../lib/form-data-mapper";
|
||||
import {
|
||||
SEWER_BSC_PLAN_DOC_SE_COM_CD,
|
||||
type SewerBscPlanRegistFormData,
|
||||
sewerBscPlanRegistSchema,
|
||||
} from "../lib/sewer-bsc-plan-regist-schema";
|
||||
@@ -76,6 +77,7 @@ function SewerBscPlanFormPage({
|
||||
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
|
||||
|
||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "SEWER" });
|
||||
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
||||
|
||||
const form = useForm<SewerBscPlanRegistFormData>({
|
||||
@@ -140,18 +142,28 @@ function SewerBscPlanFormPage({
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.sewerBscPlanTmplntList.some((item) => !item.atchFileGroupId?.trim())) {
|
||||
alert("처리구역 필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFormChecklistComplete(data.cklstResultList)) {
|
||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const docNo = await submitForm(data, "LE");
|
||||
if (!docNo) return;
|
||||
const result = await submitForm(data, "LE");
|
||||
if (!result?.sewerBscPlanDocNo) return;
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
router.push(`/sewer-bsc-plan/${docNo}`);
|
||||
router.push(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl ?? 1}`);
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
} catch (error) {
|
||||
console.error("신청 실패:", error);
|
||||
@@ -165,10 +177,10 @@ function SewerBscPlanFormPage({
|
||||
if (isSubmitting) return;
|
||||
|
||||
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
||||
(item) => !item.splmntCn || !item.splmntCn.trim(),
|
||||
(item) => item.cmptnYn === "N" && (!item.splmntCn || !item.splmntCn.trim()),
|
||||
);
|
||||
if (hasEmptySplmntCn) {
|
||||
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
|
||||
alert("보완요청 항목이 있습니다.\n보완완료 내용을 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,7 +199,8 @@ function SewerBscPlanFormPage({
|
||||
if (!result) return;
|
||||
|
||||
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 });
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
@@ -236,12 +249,9 @@ function SewerBscPlanFormPage({
|
||||
<BizCstMatrixFormSection />
|
||||
<FnrsCstFormSection />
|
||||
<TmplntFormSection />
|
||||
<ChecklistFormSection
|
||||
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD}
|
||||
fieldPrefix="sewerBscPlan"
|
||||
/>
|
||||
<ChecklistFormSection docSeComCd="SEWER" fieldPrefix="sewerBscPlan" />
|
||||
<AttachmentsFormSection
|
||||
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD}
|
||||
docSeComCd="SEWER"
|
||||
fieldPrefix="sewerBscPlanDtl"
|
||||
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||
|
||||
@@ -55,7 +55,7 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
|
||||
router.push(
|
||||
canEdit
|
||||
? `/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 {
|
||||
splmntDmndSn?: number;
|
||||
splmntDmndSn?: number | null;
|
||||
splmntArtclComCd?: string;
|
||||
splmntDmndCn?: string;
|
||||
splmntDmndDt?: string;
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
} from "@common/components";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogFooter } from "@common/components";
|
||||
|
||||
import { useAlertStore } from "../lib/use-alert-store";
|
||||
|
||||
@@ -20,7 +14,7 @@ function AlertDialog() {
|
||||
<Dialog open={!!message} onOpenChange={handleOpenChange}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export { PageLayout } from "./page-layout";
|
||||
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,
|
||||
createUploadKey,
|
||||
isEtcFileKey,
|
||||
isFormEsntlAtchComplete,
|
||||
} from "./lib/attachments-form-utils";
|
||||
export {
|
||||
buildAcntgCostItems,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getDefaultFileKey } from "@/shared/lib";
|
||||
import type { EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
|
||||
|
||||
export type FieldPrefix =
|
||||
| "fnrsCnsltnMng"
|
||||
@@ -9,6 +10,11 @@ export type FieldPrefix =
|
||||
|
||||
export const ETC_FILE_KEY_PREFIX = "etc::";
|
||||
|
||||
interface EsntlAtchFileListItem {
|
||||
esntlAtchFileId: string;
|
||||
atchFileGroupId: string;
|
||||
}
|
||||
|
||||
export function atchFileGroupIdPath(fieldPrefix: FieldPrefix): string {
|
||||
return `${fieldPrefix}.atchFileGroupId`;
|
||||
}
|
||||
@@ -35,3 +41,21 @@ export function toFileAccept(rmrk: string | null | undefined): string {
|
||||
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
||||
.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 { useTus } from "@/shared/lib";
|
||||
import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
@@ -413,20 +414,21 @@ function AttachmentsFormSection({
|
||||
|
||||
return (
|
||||
<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}
|
||||
</span>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEssentialFile(file.esntlAtchFileId, idx);
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</TextShimmer>
|
||||
{!uploading && (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEssentialFile(file.esntlAtchFileId, idx);
|
||||
}}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -489,7 +491,9 @@ function AttachmentsFormSection({
|
||||
key={item.fileKey}
|
||||
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
|
||||
size="sm"
|
||||
color="gray"
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
TabsTrigger,
|
||||
} 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";
|
||||
|
||||
@@ -49,10 +53,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
const { watch, setValue, control, getValues } = useFormContext();
|
||||
const [activeTab, setActiveTab] = useState("");
|
||||
const taskSeComCd = watch(`${fieldPrefix}.taskSeComCd`) as string | string[] | undefined;
|
||||
const taskSeComCdArray = useMemo(
|
||||
() => normalizeTaskSeComCdArray(taskSeComCd),
|
||||
[taskSeComCd],
|
||||
);
|
||||
const taskSeComCdArray = useMemo(() => normalizeTaskSeComCdArray(taskSeComCd), [taskSeComCd]);
|
||||
const cklstResultList =
|
||||
(useWatch({ control, name: "cklstResultList" }) as CklstResultListItem[] | undefined) ?? [];
|
||||
|
||||
@@ -206,6 +207,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelectAll(tab.code, "Y");
|
||||
@@ -218,11 +220,11 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelectAll(tab.code, "N");
|
||||
}}
|
||||
className="!bg-destructive"
|
||||
>
|
||||
전체 아니오
|
||||
</Button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import type { AtflMngClientResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
@@ -257,12 +258,18 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
||||
<div className="mt-6 px-2 pb-2">
|
||||
<h3 className="mb-2 text-xl font-semibold">첨부파일</h3>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
value={uploadedFileName}
|
||||
readOnly
|
||||
placeholder="총사업비 신청서(.hwp) 파일을 등록해주세요."
|
||||
className="max-w-lg rounded-sm"
|
||||
/>
|
||||
{isSharUploading && uploadedFileName ? (
|
||||
<TextShimmer className="max-w-lg text-xl" active>
|
||||
{uploadedFileName}
|
||||
</TextShimmer>
|
||||
) : (
|
||||
<Input
|
||||
value={uploadedFileName}
|
||||
readOnly
|
||||
placeholder="총사업비 신청서(.hwp) 파일을 등록해주세요."
|
||||
className="max-w-lg rounded-sm"
|
||||
/>
|
||||
)}
|
||||
<Button type="button" color="white" disabled={isSharUploading} asChild>
|
||||
<label className="cursor-pointer">
|
||||
첨부파일
|
||||
|
||||
@@ -72,7 +72,8 @@ function PbadmsTaskApplyDialog({
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
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(() => {
|
||||
if (!open) return;
|
||||
@@ -121,48 +122,50 @@ function PbadmsTaskApplyDialog({
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={setSggComCd}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{canSidoGugunSelectable && (
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={setSggComCd}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead data-required>업무구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useId, useState } from "react";
|
||||
|
||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
const SPLMNT_ARTCL_UP_CD = "SPLMNTARTCL";
|
||||
const SPLMNT_DMND_FILE_KEY_PREFIX = "splmnt-dmnd::";
|
||||
|
||||
interface UploadedSplmntFile {
|
||||
@@ -50,6 +50,7 @@ interface SplmntDmndDialogConfirmPayload {
|
||||
}
|
||||
|
||||
interface SplmntDmndDialogProps {
|
||||
splmntArtclUpCd: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
isSubmitting?: boolean;
|
||||
@@ -86,7 +87,13 @@ function isRowsValid(rows: SplmntDmndRow[]) {
|
||||
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 [rows, setRows] = useState<SplmntDmndRow[]>([createEmptyRow()]);
|
||||
|
||||
@@ -239,7 +246,7 @@ function SplmntDmndDialog({ open, onOpenChange, isSubmitting, onConfirm }: Splmn
|
||||
<TableRow key={row.rowKey}>
|
||||
<TableCell align="center" className="align-top">
|
||||
<CodeSelect
|
||||
upCd={SPLMNT_ARTCL_UP_CD}
|
||||
upCd={splmntArtclUpCd}
|
||||
value={row.splmntArtclComCd || undefined}
|
||||
onValueChange={(value) =>
|
||||
updateRow(row.rowKey, { splmntArtclComCd: value })
|
||||
@@ -263,7 +270,9 @@ function SplmntDmndDialog({ open, onOpenChange, isSubmitting, onConfirm }: Splmn
|
||||
<div className="flex items-center gap-2">
|
||||
{row.uploadedFile ? (
|
||||
<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
|
||||
type="button"
|
||||
size="icon"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import {
|
||||
Badge,
|
||||
@@ -272,7 +273,9 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
|
||||
{uploaded.fileName}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-xl">{uploaded.fileName}</span>
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploaded.fileName}
|
||||
</TextShimmer>
|
||||
)}
|
||||
{!isRowReadOnly ? (
|
||||
<Button
|
||||
|
||||
@@ -26,6 +26,10 @@ interface SupplementDemandSectionProps {
|
||||
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
if (splmntDmndList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="보완요청 및 완료 현황">
|
||||
<div className="overflow-x-auto">
|
||||
|
||||
@@ -83,12 +83,17 @@ export default {
|
||||
from: { transform: "translate(0,0) rotate(0deg)" },
|
||||
to: { transform: "translate(0,0) rotate(360deg)" },
|
||||
},
|
||||
"text-shimmer": {
|
||||
"0%": { backgroundPosition: "200% center" },
|
||||
"100%": { backgroundPosition: "-200% center" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"spin-parent": "spin-parent 6s linear infinite",
|
||||
"spin-child": "spin-child 1s ease-in-out infinite",
|
||||
"text-shimmer": "text-shimmer 1.5s linear infinite",
|
||||
},
|
||||
backgroundImage: {
|
||||
"gradient-blue": "linear-gradient(.25turn, #2094fa, #00e5dd)",
|
||||
|
||||
Reference in New Issue
Block a user