Compare commits

...
61 Commits
Author SHA1 Message Date
chanii8 f5ce74d0ee Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-09-23 14:39:57 +09:00
chanii8 3114280e83 [as] chore: 알림이력 배지 크기 조정 2026-09-23 14:39:24 +09:00
chanii8 c7b8b36f4a Merge branch 'iipbs/common/dev' 2026-09-23 14:17:15 +09:00
chanii8 60ee194cc9 [common/components] design: NoticePopover 스타일 수정 2026-09-23 13:55:16 +09:00
chanii8 db8ee298d9 Merge branch 'iipbs/cs/dev' 2026-09-23 11:40:04 +09:00
chanii8 9eef99a435 [cs] chore: 게시판 댓글 사용 여부 적용 2026-09-23 11:39:32 +09:00
chanii8 3d027ed374 [common/features] chore: 게시판 댓글 사용 여부 적용 2026-09-23 11:37:16 +09:00
chanii8 d70eb17a72 Merge branch 'iipbs/as/dev' 2026-09-23 09:50:30 +09:00
chanii8 ddd619babc [as] feat: 행정지원 문서 체크리스트 레이블 변경 2026-09-23 09:48:08 +09:00
chanii8 5d27ee830e [as] feat: 행정지원 문서 체크리스트 탭 노출 알고리즘 수정 2026-09-23 09:35:35 +09:00
mind-one005 212b9a43a9 Merge branch 'iipbs/wr/dev' 2026-09-22 20:49:40 +09:00
mind-one005 f092a23ee4 wr: font 크기 조정 2026-09-22 20:45:22 +09:00
chanii8 25c357a423 [as] chore: 보완요청 섹션 첨부파일 관련 코드 수정 2026-09-22 16:21:13 +09:00
chanii8 a77246a1db [as] fix: 타입 에러 2026-09-22 15:39:06 +09:00
chanii8 f00ce31648 [as] refactor: zod 스키마 .optional()-> .nullish() 변경 2026-09-22 15:12:28 +09:00
chanii8 dc57a491dd [as] chore: 재원협의 코드 수정 2026-09-22 15:02:36 +09:00
mind-one005 c9b7266959 wr: 재이용실적 수정 2026-09-22 11:24:32 +09:00
chanii8 0dd2111cb9 [as] refactor: 재원협의 폼 useWatch + Controller 방식 제거 2026-09-22 10:57:41 +09:00
mind-one005 c39b048398 Merge branch 'iipbs/wr/dev' 2026-09-22 10:25:49 +09:00
chanii8 87fbf15e2b [as] feat: 재원협의 수정 2026-09-22 10:23:10 +09:00
mind-one005 db1cfaa1a6 wr: label 위치 조정 2026-09-22 10:05:49 +09:00
mind-one005 905b750877 Merge branch 'iipbs/sn/dev' 2026-09-22 09:28:50 +09:00
mind-one005 be14fe2603 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-22 09:06:39 +09:00
mind-one005 d7f101ee30 wr: 시도,시군구 전체"0" 2건 조회현상 수정 2026-09-21 17:51:09 +09:00
chanii8 20f7d595a1 Merge branch 'iipbs/as/dev' 2026-09-21 17:50:59 +09:00
chanii8 e909dc81ad [as] chore: 재원협의 버튼 노출 조건 수정 2026-09-21 17:50:23 +09:00
chanii8 a510779ea1 [as] feat: AI 만족도 평가 기능 구현 2026-09-21 17:24:01 +09:00
chanii8 0818e9ade2 Merge branch 'iipbs/as/dev' 2026-09-21 16:57:46 +09:00
chanii8 e60a8bed8b fix: 빌드 에러 2026-09-21 16:57:16 +09:00
chanii8 17fdcd7d1f fix: 빌드 에러 2026-09-21 16:56:46 +09:00
chanii8 6412e1a997 Merge branch 'iipbs/as/dev' 2026-09-21 16:36:58 +09:00
chanii8 1674598480 [as] design: RHFDevtool 스타일 수정 2026-09-21 16:36:28 +09:00
chanii8 23ebf36244 [as] feat: "@hookform/devtools" 추가 2026-09-21 16:04:19 +09:00
chanii8 342db06685 [as] refactor: 행정지원 문서 저장 payload 스키마 수정 2026-09-21 15:18:42 +09:00
mind-one001 35b9f813c4 로그삭제 2026-09-21 15:08:07 +09:00
mind-one005 aafb326662 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-21 14:46:02 +09:00
mind-one005 aa1b309b26 wr: 중수도 단위, 모달 조회 순서, 시설구분 조회 2026-09-21 14:45:50 +09:00
chanii8 bd20a499d3 [as] feat: AI 수동 검증 버튼 추가 2026-09-21 13:34:26 +09:00
chanii8 5fec5d5239 [as] feat: RAG 인덱싱 상태일 때 검토자 지정 버튼 분기 처리 2026-09-21 11:33:59 +09:00
kiminchan bf0aef59cd Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev_kic 2026-09-21 10:06:26 +09:00
kiminchan b2ddcd0cf3 sn : 통수위험 결과 안내문에 위험 판정기준 및 상태 색상 표시 2026-09-21 09:55:55 +09:00
mind-one001 49ace44c9b 테스트 파일 삭제 2026-09-18 16:35:21 +09:00
mind-one005 91576f401b sn: 침수예측오버레이 수정 2026-09-17 15:41:08 +09:00
mind-one005 febcd18eda Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-17 14:05:11 +09:00
mind-one005 8cc9f0d291 sn: 침수예측현황 침수위험도 수정 2026-09-17 14:04:51 +09:00
kiminchan a8f2c8f41a GIS 편집 신규 관로 미리보기 표시 보완 2026-09-16 17:32:02 +09:00
mind-one001 4d685d5a67 Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev 2026-09-16 11:15:30 +09:00
mind-one001 96196229f4 fix(sn): align business form limits with standardized database 2026-09-16 11:15:18 +09:00
mind-one005 94b2e84c10 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-15 16:28:23 +09:00
mind-one001 80d2230c9a 불필요 파일 삭제 2026-09-15 10:00:42 +09:00
mind-one005 d5f0c3bc95 sn: 침수발생이력, 침수예측현황 팝업크기 변경 외 2026-09-11 11:01:55 +09:00
mind-one001 b7134cb3e4 노후관로현황 그리드 수정 2026-09-10 14:06:24 +09:00
mind-one001 ec78658e46 시도별 노후관로현황 그리드 수정 2026-09-10 14:01:58 +09:00
mind-one001 44361af392 그리드 css 통합 2026-09-10 13:51:42 +09:00
mind-one001 76130caa6d Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-10 11:30:58 +09:00
mind-one001 352a78a0c5 gis 업로드 오류 기능 수정 2026-09-10 11:30:52 +09:00
kiminchan 65bee9586d 도시침수지도 목록 이동 운영 보완 기록 2026-09-09 16:45:12 +09:00
mind-one001 31cda8579a Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-09 15:42:53 +09:00
mind-one001 1dc40b9efc 빈 레이어 트리로 지도만 재사용하는 화면에서는 패널 외곽과 제목도 표시하지 않는다. 2026-09-09 15:42:31 +09:00
mind-one005 6cb3d8ad65 Merge branch 'iipbs/wr/dev' into iipbs/sn/dev 2026-09-09 14:06:31 +09:00
LEEKYUHYUN 225e2732d1 사이드패널 패딩 조정 및 스크롤바 수정 2026-09-09 11:35:59 +09:00
338 changed files with 13792 additions and 19647 deletions
+1
View File
@@ -36,6 +36,7 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@hookform/devtools": "^4.4.0",
"@types/js-cookie": "^3.0.6",
"eslint": "^9.0.0",
"eslint-config-next": "15.3.8",
@@ -0,0 +1,24 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
AiAutoPrcsLatestResponseDto,
AiAutoPrcsMngService,
} from "../model/ai-auto-prcs-mng-interface";
const BASE = ENDPOINT.AS_SERVICE.AI_AUTO_PRCS_MNG;
async function findLatestPerAutoSeByFnrsDocNo(
fnrsCnsltnDocNo: string,
atmtnSeComCd?: string,
): Promise<AiAutoPrcsLatestResponseDto[]> {
const response = await client.get<AiAutoPrcsLatestResponseDto[]>(
`${BASE}/fnrs/${fnrsCnsltnDocNo}/latest`,
{ params: atmtnSeComCd ? { atmtnSeComCd } : undefined },
);
return response.data;
}
export const aiAutoPrcsMngService = {
findLatestPerAutoSeByFnrsDocNo,
} satisfies AiAutoPrcsMngService;
@@ -0,0 +1,30 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { aiAutoPrcsMngService } from "./ai-auto-prcs-mng-service";
export const aiAutoPrcsMngKeys = {
all: [ENDPOINT.AS_SERVICE.AI_AUTO_PRCS_MNG] as const,
latest: (fnrsCnsltnDocNo: string, atmtnSeComCd?: string) =>
[...aiAutoPrcsMngKeys.all, "latest", fnrsCnsltnDocNo, atmtnSeComCd ?? "all"] as const,
};
interface UseAiAutoPrcsLatestOptions {
enabled?: boolean;
atmtnSeComCd?: string;
}
export function useAiAutoPrcsLatest(
fnrsCnsltnDocNo: string | null | undefined,
options: UseAiAutoPrcsLatestOptions = {},
) {
const { enabled = true, atmtnSeComCd } = options;
return useQuery({
queryKey: aiAutoPrcsMngKeys.latest(fnrsCnsltnDocNo!, atmtnSeComCd),
queryFn: () =>
aiAutoPrcsMngService.findLatestPerAutoSeByFnrsDocNo(fnrsCnsltnDocNo!, atmtnSeComCd),
enabled: Boolean(fnrsCnsltnDocNo) && enabled,
});
}
@@ -0,0 +1,8 @@
export { aiAutoPrcsMngService } from "./api/ai-auto-prcs-mng-service";
export { aiAutoPrcsMngKeys, useAiAutoPrcsLatest } from "./api/queries";
export type {
AiAutoPrcsLatestResponseDto,
AiAutoPrcsMngService,
AiAutoSeComCd,
} from "./model/ai-auto-prcs-mng-interface";
export { AI_AUTO_SE } from "./model/ai-auto-prcs-mng-interface";
@@ -0,0 +1,44 @@
/** 자동화구분 공통코드 (AIAUTOSE) */
export const AI_AUTO_SE = {
FILE_VRFC: "FILE_VRFC",
DRAFT_GEN: "DRAFT_GEN",
COST_VRFC: "COST_VRFC",
} as const;
export type AiAutoSeComCd = (typeof AI_AUTO_SE)[keyof typeof AI_AUTO_SE];
/** 백엔드 AiAutoPrcsLatestResponseDto */
export interface AiAutoPrcsLatestResponseDto {
atmtnPrcsUuid?: string;
fnrsCnsltnDocNo?: string;
pwrkCnsltnDocNo?: string;
crtDmndUuid?: string;
atmtnSeComCd?: string;
atmtnSeComNm?: string;
bfrDocSttsComCd?: string;
bfrDocSttsComNm?: string;
aftrDocSttsComCd?: string;
aftrDocSttsComNm?: string;
atmtnPrcsSttsComCd?: string;
atmtnPrcsSttsComNm?: string;
dmndDt?: string;
dmndSysId?: string;
rplyDt?: string;
rplyRsltComCd?: string;
prcsErrCn?: string | null;
lastSttsInqDt?: string;
sttsInqRsltComCd?: string;
rtrsmCtpvNmtm?: number;
lastRtrsmDt?: string;
lastFrcSttsChgYn?: string;
isFailed?: boolean;
isRetryable?: boolean;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface AiAutoPrcsMngService {
findLatestPerAutoSeByFnrsDocNo(
fnrsCnsltnDocNo: string,
atmtnSeComCd?: string,
): Promise<AiAutoPrcsLatestResponseDto[]>;
}
@@ -46,9 +46,40 @@ async function requestCostVerification(
return response.data;
}
async function reTransferDocument(
body: DcRagDocumentTransferRequestDto,
): Promise<DcRagCommonResponseDto> {
const response = await client.post<DcRagCommonResponseDto>(
`${BASE}/documents/re-transfer`,
body,
);
return response.data;
}
async function reGenerateDraft(
body: DcRagDraftGenerateRequestDto,
): Promise<DcRagDraftGenerateResponseDto> {
const response = await client.post<DcRagDraftGenerateResponseDto>(
`${BASE}/documents/draft/re-generate`,
body,
);
return response.data;
}
async function reVerifyCost(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonResponseDto> {
const response = await client.post<DcRagCommonResponseDto>(
`${BASE}/documents/cost/re-verify`,
body,
);
return response.data;
}
export const dcRagApiToService = {
requestDocumentTransfer,
requestDraftGeneration,
getDraftGenerationStatus,
requestCostVerification,
reTransferDocument,
reGenerateDraft,
reVerifyCost,
} satisfies DcRagApiToService;
@@ -1,6 +1,9 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { useMutationWrapper } from "@/shared/lib";
import { aiAutoPrcsMngKeys } from "@/entities/ai-auto-prcs-mng";
import { fnrsCnsltnKeys } from "@/entities/fnrs-cnsltn";
import type { DcRagDraftStatusResponseDto } from "../model/dcrag-to-interface";
import type {
@@ -21,6 +24,18 @@ function isDraftGenerationSettled(data: DcRagDraftStatusResponseDto | undefined)
return data.events?.some((event) => event.eventType === "COMPLETED") ?? false;
}
function invalidateAfterAiRetry(
queryClient: ReturnType<typeof useQueryClient>,
fnrsCnsltnDocNo?: string,
) {
queryClient.invalidateQueries({ queryKey: dcRagApiToBaseKey });
queryClient.invalidateQueries({ queryKey: aiAutoPrcsMngKeys.all });
if (fnrsCnsltnDocNo) {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo) });
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
}
}
export function useDraftGenerationStatus(crtDmndUuid: string | null | undefined) {
return useQuery({
queryKey: [...dcRagApiToBaseKey, "getDraftGenerationStatus", crtDmndUuid],
@@ -68,3 +83,39 @@ export function useRequestCostVerification() {
},
});
}
export function useReTransferDocument() {
const queryClient = useQueryClient();
return useMutationWrapper({
mutationFn: (body: DcRagDocumentTransferRequestDto) =>
dcRagApiToService.reTransferDocument(body),
onSuccess: (_data, body) => {
invalidateAfterAiRetry(queryClient, body.fnrsCnsltnDocNo);
},
});
}
export function useReGenerateDraft() {
const queryClient = useQueryClient();
return useMutationWrapper({
mutationFn: (body: DcRagDraftGenerateRequestDto) => dcRagApiToService.reGenerateDraft(body),
onSuccess: (_data, body) => {
invalidateAfterAiRetry(queryClient, body.fnrsCnsltnDocNo);
if (body.crtDmndUuid) {
queryClient.invalidateQueries({
queryKey: [...dcRagApiToBaseKey, "getDraftGenerationStatus", body.crtDmndUuid],
});
}
},
});
}
export function useReVerifyCost() {
const queryClient = useQueryClient();
return useMutationWrapper({
mutationFn: (body: DcRagCostVerifyRequestDto) => dcRagApiToService.reVerifyCost(body),
onSuccess: (_data, body) => {
invalidateAfterAiRetry(queryClient, body.fnrsCnsltnDocNo);
},
});
}
+3
View File
@@ -2,9 +2,12 @@ export { dcRagApiToService } from "./api/dcrag-to-service";
export { dcRagApiToBaseKey } from "./api/queries";
export {
useDraftGenerationStatus,
useReGenerateDraft,
useRequestCostVerification,
useRequestDocumentTransfer,
useRequestDraftGeneration,
useReTransferDocument,
useReVerifyCost,
} from "./api/queries";
export type {
DcRagApiToService,
@@ -49,4 +49,7 @@ export interface DcRagApiToService {
): Promise<DcRagDraftGenerateResponseDto>;
getDraftGenerationStatus(crtDmndUuid: string): Promise<DcRagDraftStatusResponseDto>;
requestCostVerification(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonResponseDto>;
reTransferDocument(body: DcRagDocumentTransferRequestDto): Promise<DcRagCommonResponseDto>;
reGenerateDraft(body: DcRagDraftGenerateRequestDto): Promise<DcRagDraftGenerateResponseDto>;
reVerifyCost(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonResponseDto>;
}
@@ -2,35 +2,35 @@ import { z } from "zod";
/** 백엔드 DcRagFileInfoDto */
export const dcRagFileInfoSchema = z.object({
fileId: z.string().optional(),
fileGroupId: z.string().optional(),
fileName: z.string().optional(),
physicalPath: z.string().optional(),
downloadUrl: z.string().optional(),
fileSize: z.number().optional(),
esntlAtchFileId: z.string().optional(),
esntlAtchFileNm: z.string().optional(),
fileId: z.string().nullish(),
fileGroupId: z.string().nullish(),
fileName: z.string().nullish(),
physicalPath: z.string().nullish(),
downloadUrl: z.string().nullish(),
fileSize: z.number().nullish(),
esntlAtchFileId: z.string().nullish(),
esntlAtchFileNm: z.string().nullish(),
});
/** 백엔드 DcRagDocumentTransferRequestDto */
export const dcRagDocumentTransferRequestSchema = z.object({
fnrsCnsltnDocNo: z.string(),
taskSeComCd: z.string().optional(),
taskSeCdNm: z.string().optional(),
essentialFiles: z.array(dcRagFileInfoSchema).optional(),
etcFiles: z.array(dcRagFileInfoSchema).optional(),
callbackUrl: z.string().optional(),
taskSeComCd: z.string().nullish(),
taskSeCdNm: z.string().nullish(),
essentialFiles: z.array(dcRagFileInfoSchema).nullish(),
etcFiles: z.array(dcRagFileInfoSchema).nullish(),
callbackUrl: z.string().nullish(),
});
/** 백엔드 DcRagDraftGenerateRequestDto */
export const dcRagDraftGenerateRequestSchema = z.object({
fnrsCnsltnDocNo: z.string(),
pwrkCnsltnDocNo: z.string(),
crtDmndUuid: z.string().optional(),
essentialFiles: z.array(dcRagFileInfoSchema).optional(),
etcFiles: z.array(dcRagFileInfoSchema).optional(),
rqstrUuid: z.string().optional(),
callbackUrl: z.string().optional(),
crtDmndUuid: z.string().nullish(),
essentialFiles: z.array(dcRagFileInfoSchema).nullish(),
etcFiles: z.array(dcRagFileInfoSchema).nullish(),
rqstrUuid: z.string().nullish(),
callbackUrl: z.string().nullish(),
});
/** 백엔드 DcRagCostVerifyRequestDto */
@@ -4,9 +4,11 @@ import type { PageResponse } from "@/shared/model";
import type {
DesignCnstnApplySaveRequestDto,
DesignCnstnMngSaveRequestDto,
} from "../model/design-cnstn-apply-payload-schema";
import type {
DesignCnstnMngDetResponseDto,
DesignCnstnMngResponseDto,
DesignCnstnMngSaveRequestDto,
DesignCnstnMngSearchDto,
DesignCnstnMngService,
DesignCnstnMngUpdateRequestDto,
@@ -2,10 +2,10 @@ import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type { DesignCnstnRvwOpnnSaveRequestDto } from "../model/design-cnstn-apply-payload-schema";
import type {
DesignCnstnRvwOpnnId,
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
DesignCnstnRvwOpnnSearchDto,
DesignCnstnRvwOpnnService,
DesignCnstnRvwOpnnUpdateRequestDto,
@@ -18,11 +18,13 @@ import type {
import type {
DesignCnstnApplySaveRequestDto,
DesignCnstnMngSaveRequestDto,
DesignCnstnRvwOpnnSaveRequestDto,
} from "../model/design-cnstn-apply-payload-schema";
import type {
DesignCnstnMngSearchDto,
DesignCnstnMngUpdateRequestDto,
} from "../model/design-cnstn-mng-interface";
import type {
DesignCnstnRvwOpnnSaveRequestDto,
DesignCnstnRvwOpnnSearchDto,
DesignCnstnRvwOpnnUpdateRequestDto,
} from "../model/design-cnstn-rvw-opnn-interface";
+16 -6
View File
@@ -37,6 +37,22 @@ export {
useUpdateDesignCnstnMng,
useUpdateDesignCnstnRvwOpnn,
} from "./api/queries";
export type {
BizCstItem,
BzScaleItem,
DesignCnstnApplySaveRequestDto,
DesignCnstnMngSaveRequestDto,
DesignCnstnRvwOpnnSaveRequestDto,
EsntlAtchFileItem,
} from "./model/design-cnstn-apply-payload-schema";
export {
designCnstnApplySaveSchema,
designCnstnBizCstItemSchema,
designCnstnBzScaleItemSchema,
designCnstnEsntlAtchFileItemSchema,
designCnstnMngSaveSchema,
designCnstnRvwOpnnSaveSchema,
} from "./model/design-cnstn-apply-payload-schema";
export type {
DesignCnstnBizCstId,
DesignCnstnBizCstResponseDto,
@@ -63,21 +79,15 @@ export type {
} from "./model/design-cnstn-esntl-atch-file-interface";
export type {
AtflMngClientResponseDto,
BizCstItem,
BizCstTreeDto,
BizSclTreeDto,
BzScaleItem,
DesignCnstnApplySaveRequestDto,
DesignCnstnMngDetResponseDto,
DesignCnstnMngResponseDto,
DesignCnstnMngSaveRequestDto,
DesignCnstnMngSearchDto,
DesignCnstnMngService,
DesignCnstnMngUpdateRequestDto,
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
} from "./model/design-cnstn-mng-interface";
export type {
@@ -0,0 +1,74 @@
import { z } from "zod";
/** 백엔드 DesignCnstnMngSaveRequestDto */
export const designCnstnMngSaveSchema = z.object({
designCnstnDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
taskSeComCd: z.string().nullish(),
bizNm: z.string().nullish(),
bizBgngYr: z.string().nullish(),
bizEndYr: z.string().nullish(),
bizCn: z.string().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
export const designCnstnBzScaleItemSchema = z.object({
cnstpSeCd: z.string().nullish(),
bscPl: z.number().nullish(),
budgetReflct: z.number().nullish(),
thtiDsgn: z.number().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
export const designCnstnBizCstItemSchema = z.object({
acntgSeCd: z.string().nullish(),
bscPlanAmt: z.number().nullish(),
bgtRfltAmt: z.number().nullish(),
tstmDesignAmt: z.number().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
export const designCnstnEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
export const designCnstnRvwOpnnSaveSchema = z.object({
designCnstnDocNo: z.string(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto */
export const designCnstnApplySaveSchema = z.object({
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
designCnstnMng: designCnstnMngSaveSchema,
bzScaleList: z.array(designCnstnBzScaleItemSchema).nullish(),
bizCstList: z.array(designCnstnBizCstItemSchema).nullish(),
esntlAtchFileList: z.array(designCnstnEsntlAtchFileItemSchema).nullish(),
rvwOpnnList: z.array(designCnstnRvwOpnnSaveSchema).nullish(),
});
export type DesignCnstnMngSaveRequestDto = z.infer<typeof designCnstnMngSaveSchema>;
export type BzScaleItem = z.infer<typeof designCnstnBzScaleItemSchema>;
export type BizCstItem = z.infer<typeof designCnstnBizCstItemSchema>;
export type EsntlAtchFileItem = z.infer<typeof designCnstnEsntlAtchFileItemSchema>;
export type DesignCnstnRvwOpnnSaveRequestDto = z.infer<typeof designCnstnRvwOpnnSaveSchema>;
export type DesignCnstnApplySaveRequestDto = z.infer<typeof designCnstnApplySaveSchema>;
@@ -1,22 +1,12 @@
import type {
BizCstSaveBase,
BzScaleSaveBase,
EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
} from "@/shared/model";
import type { PageableParams, PageResponse, PbadmsMngDetResponseBase } from "@/shared/model";
import type {
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
} from "./design-cnstn-rvw-opnn-interface";
DesignCnstnApplySaveRequestDto,
DesignCnstnMngSaveRequestDto,
} from "./design-cnstn-apply-payload-schema";
import type { DesignCnstnRvwOpnnResponseDto } from "./design-cnstn-rvw-opnn-interface";
export type {
DesignCnstnRvwOpnnResponseDto,
DesignCnstnRvwOpnnSaveRequestDto,
} from "./design-cnstn-rvw-opnn-interface";
export type { DesignCnstnRvwOpnnResponseDto } from "./design-cnstn-rvw-opnn-interface";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
@@ -25,28 +15,6 @@ export type {
EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model";
/** 백엔드 DesignCnstnMngSaveRequestDto */
export interface DesignCnstnMngSaveRequestDto {
designCnstnDocNo?: string;
admdstCd?: string;
envoSeComCd?: string;
taskSeComCd?: string;
bizNm?: string;
bizBgngYr?: string;
bizEndYr?: string;
bizCn?: string;
aplcntNm?: string;
aplcntMblTelno?: string;
aplcntOgdpNm?: string;
aplcntEmlAddr?: string;
dsgnrNm?: string;
dsgnrTelno?: string;
dsgnrOgdpNm?: string;
dsgnrEmlAddr?: string;
atchFileGroupId?: string;
prgrsSttsComCd?: string;
}
/** 백엔드 DesignCnstnMngUpdateRequestDto */
export type DesignCnstnMngUpdateRequestDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo">;
@@ -88,30 +56,12 @@ interface DesignCnstnMngResponseExtension {
export type DesignCnstnMngResponseDto = Omit<DesignCnstnMngSaveRequestDto, "designCnstnDocNo"> &
DesignCnstnMngResponseExtension;
/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
export type BzScaleItem = BzScaleSaveBase;
/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
export type BizCstItem = BizCstSaveBase;
/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
/** 백엔드 DesignCnstnMngDetResponseDto */
export type DesignCnstnMngDetResponseDto = PbadmsMngDetResponseBase<
DesignCnstnMngResponseDto,
DesignCnstnRvwOpnnResponseDto
>;
/** 백엔드 DesignCnstnApplySaveRequestDto */
export interface DesignCnstnApplySaveRequestDto extends PbadmsApplySaveHeader {
designCnstnMng: DesignCnstnMngSaveRequestDto;
bzScaleList?: BzScaleItem[];
bizCstList?: BizCstItem[];
esntlAtchFileList?: EsntlAtchFileItem[];
rvwOpnnList?: DesignCnstnRvwOpnnSaveRequestDto[];
}
/** Controller public 메서드명·시그니처와 동일 */
export interface DesignCnstnMngService {
findPage(searchDto: DesignCnstnMngSearchDto): Promise<PageResponse<DesignCnstnMngResponseDto>>;
@@ -1,22 +1,13 @@
import type { PageableParams, PageResponse, RvwOpnnResponseExtension } from "@/shared/model";
import type { DesignCnstnRvwOpnnSaveRequestDto } from "./design-cnstn-apply-payload-schema";
/** 백엔드 DesignCnstnRvwOpnnId */
export interface DesignCnstnRvwOpnnId {
designCnstnMng: string;
rvwOpnnSn: number;
}
/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
export interface DesignCnstnRvwOpnnSaveRequestDto {
designCnstnDocNo: string;
rvwOpnnSn?: number;
opnrSeComCd?: string;
opnrNm?: string;
rvwRsltCn?: string;
rvwRsltAnmlyYn?: string;
atchFileGroupId?: string;
}
/** 백엔드 DesignCnstnRvwOpnnUpdateRequestDto */
export interface DesignCnstnRvwOpnnUpdateRequestDto {
opnrSeComCd?: string;
+1
View File
@@ -41,6 +41,7 @@ export {
validateAllRowsForSave,
validateAplnRow,
validateAtrzOrder,
validateUniqueAtrzAprvr,
validateUniqueSeqNo,
} from "./lib/apln-validators";
export {
@@ -76,3 +76,16 @@ export function validateUniqueSeqNo(rows: ElaprAplnRow[]): string | null {
}
return null;
}
export function validateUniqueAtrzAprvr(rows: ElaprAplnRow[]): string | null {
const pairSet = new Set<string>();
for (const row of getVisibleRows(rows)) {
const atrz = row.atrzSeComCd?.trim();
const aprvr = row.aprvrUuid?.trim();
if (!atrz || !aprvr) continue;
const key = `${atrz}::${aprvr}`;
if (pairSet.has(key)) return "동일한 결재구분과 결재자가 중복됩니다.";
pairSet.add(key);
}
return null;
}
@@ -7,8 +7,8 @@ export interface ElaprDmndSaveRequestDto {
elaprDocNo: string;
dmndTtl: string;
dmndCn: string;
elaprCycl?: number;
docPrgrsSttsComCd?: string;
elaprCycl?: number | null;
docPrgrsSttsComCd?: string | null;
elaprPrgrsSttsList?: ElaprPrgrsSttsSaveRequestDto[];
}
@@ -17,7 +17,7 @@ import {
} from "@common/components";
import { useElaprAplnPage } from "../api/queries";
import { validateAplnRow } from "../lib/apln-validators";
import { validateAplnRow, validateUniqueAtrzAprvr } from "../lib/apln-validators";
import { useAplnRows } from "../lib/use-apln-rows";
import { useDndSensors } from "../lib/use-dnd-sensors";
import type { ElaprApln } from "../model/elapr-apln-interface";
@@ -110,6 +110,12 @@ function ElaprAplnPickerGrid({
return;
}
const duplicateError = validateUniqueAtrzAprvr(rows);
if (duplicateError) {
alert(duplicateError);
return;
}
commitEdit(row.rowKey);
}
@@ -17,6 +17,7 @@ import {
getVisibleRows,
validateAllRowsForSave,
validateAtrzOrder,
validateUniqueAtrzAprvr,
validateUniqueSeqNo,
} from "../lib/apln-validators";
import { mapDocPrgrsToAplnRows } from "../lib/doc-prgrs-to-apln-rows";
@@ -90,6 +91,12 @@ function ElaprDocPrgrsPickerDialog({
return;
}
const atrzAprvrError = validateUniqueAtrzAprvr(aplnRows);
if (atrzAprvrError) {
alert(atrzAprvrError);
return;
}
const visibleRows = getVisibleRows(aplnRows);
onConfirm?.({ aplnRows: visibleRows });
@@ -17,6 +17,7 @@ import {
getVisibleRows,
validateAllRowsForSave,
validateAtrzOrder,
validateUniqueAtrzAprvr,
validateUniqueSeqNo,
} from "../lib/apln-validators";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
@@ -106,6 +107,12 @@ function ElaprTmpltPickerDialog({
return;
}
const atrzAprvrError = validateUniqueAtrzAprvr(aplnRows);
if (atrzAprvrError) {
alert(atrzAprvrError);
return;
}
const visibleRows = getVisibleRows(aplnRows);
const payload: ElaprTmpltPickerConfirmPayload = {
@@ -3,12 +3,14 @@ import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
DocPrgrsUpdateRequestDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
} from "../model/fnrs-cnsltn-apply-payload-schema";
import type {
DocPrgrsUpdateRequestDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
@@ -1,9 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type {
DocPrgrsUpdateRequestDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
} from "../model/fnrs-cnsltn-apply-payload-schema";
import type {
DocPrgrsUpdateRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngUpdateRequestDto,
} from "../model/fnrs-cnsltn-interface";
+23 -9
View File
@@ -16,30 +16,44 @@ export {
useUpdateFnrsCnsltnMng,
} from "./api/queries";
export type {
AtflMngClientResponseDto,
BizCstItem,
BizCstTreeDto,
BizSclTreeDto,
BzScaleItem,
CklstResultItem,
EsntlAtchFileItem,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnRvwOpnnSaveRequestDto,
FnrsCnsltnSplmntDmndItem,
FnrsSharItem,
} from "./model/fnrs-cnsltn-apply-payload-schema";
export {
fnrsCnsltnApplySaveSchema,
fnrsCnsltnBizCstItemSchema,
fnrsCnsltnBzScaleItemSchema,
fnrsCnsltnCklstResultItemSchema,
fnrsCnsltnEsntlAtchFileItemSchema,
fnrsCnsltnMngSaveSchema,
fnrsCnsltnRvwOpnnSaveSchema,
fnrsCnsltnSplmntDmndItemSchema,
fnrsSharItemSchema,
} from "./model/fnrs-cnsltn-apply-payload-schema";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
BizSclTreeDto,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
FnrsCnsltnRvwOpnnResponseDto,
FnrsCnsltnRvwOpnnSaveRequestDto,
FnrsCnsltnSplmntDmndItem,
FnrsCnsltnSplmntDmndResponseDto,
FnrsSharItem,
RagIndexingInfoDto,
} from "./model/fnrs-cnsltn-interface";
export type {
EnvoActnReviewReqDto,
@@ -0,0 +1,120 @@
import { z } from "zod";
/** 백엔드 FnrsCnsltnMngSaveRequestDto */
export const fnrsCnsltnMngSaveSchema = z.object({
fnrsCnsltnDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
taskSeComCd: z.string().nullish(),
cfmtnBgtBizNo: z.string().nullish(),
bgtAplyYr: z.string().nullish(),
bizNm: z.string().nullish(),
bizBgngYr: z.string().nullish(),
bizEndYr: z.string().nullish(),
bizCn: z.string().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
sharAtchFileGroupId: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
rtlztnFcltSeComCd: z.string().nullish(),
rtlztnUsgSeComCd: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.BzScaleItem */
export const fnrsCnsltnBzScaleItemSchema = z.object({
cnstpSeCd: z.string().nullish(),
bscPl: z.number().nullish(),
budgetReflct: z.number().nullish(),
thtiDsgn: z.number().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.BizCstItem */
export const fnrsCnsltnBizCstItemSchema = z.object({
acntgSeCd: z.string().nullish(),
bscPlanAmt: z.number().nullish(),
bgtRfltAmt: z.number().nullish(),
tstmDesignAmt: z.number().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.CklstResultItem */
export const fnrsCnsltnCklstResultItemSchema = z.object({
cklstRvwSn: z.number().nullish(),
cklstArtclSn: z.number().nullish(),
cklstSn: z.number().nullish(),
rvwStpComCd: z.string().nullish(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.EsntlAtchFileItem */
export const fnrsCnsltnEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsCnsltnSplmntDmndItem */
export const fnrsCnsltnSplmntDmndItemSchema = z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
splmntDmndCn: z.string().nullish(),
splmntDmndDt: z.string().nullish(),
splmntDmndAtchFileGroupId: z.string().nullish(),
splmntCn: z.string().nullish(),
splmntPicNm: z.string().nullish(),
splmntCmptnDt: z.string().nullish(),
splmntAtchFileGroupId: z.string().nullish(),
cmptnYn: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnRvwOpnnSaveRequestDto */
export const fnrsCnsltnRvwOpnnSaveSchema = z.object({
fnrsCnsltnDocNo: z.string(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsSharItem */
export const fnrsSharItemSchema = z.object({
fnrsSharSeCd: z.string().nullish(),
exstnAmt: z.number().nullish(),
chgRqtAmt: z.number().nullish(),
ajmtAmt: z.number().nullish(),
rmrkCn: z.string().nullish(),
envoRmrkCn: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto */
export const fnrsCnsltnApplySaveSchema = z.object({
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
fnrsCnsltnMng: fnrsCnsltnMngSaveSchema,
bzScaleList: z.array(fnrsCnsltnBzScaleItemSchema).nullish(),
bizCstList: z.array(fnrsCnsltnBizCstItemSchema).nullish(),
cklstResultList: z.array(fnrsCnsltnCklstResultItemSchema).nullish(),
cklstResultMap: z.record(z.array(fnrsCnsltnCklstResultItemSchema)).nullish(),
esntlAtchFileList: z.array(fnrsCnsltnEsntlAtchFileItemSchema).nullish(),
rvwOpnnList: z.array(fnrsCnsltnRvwOpnnSaveSchema).nullish(),
splmntDmndList: z.array(fnrsCnsltnSplmntDmndItemSchema).nullish(),
fnrsSharList: z.array(fnrsSharItemSchema).nullish(),
});
export type FnrsCnsltnMngSaveRequestDto = z.infer<typeof fnrsCnsltnMngSaveSchema>;
export type BzScaleItem = z.infer<typeof fnrsCnsltnBzScaleItemSchema>;
export type BizCstItem = z.infer<typeof fnrsCnsltnBizCstItemSchema>;
export type CklstResultItem = z.infer<typeof fnrsCnsltnCklstResultItemSchema>;
export type EsntlAtchFileItem = z.infer<typeof fnrsCnsltnEsntlAtchFileItemSchema>;
export type FnrsCnsltnSplmntDmndItem = z.infer<typeof fnrsCnsltnSplmntDmndItemSchema>;
export type FnrsCnsltnRvwOpnnSaveRequestDto = z.infer<typeof fnrsCnsltnRvwOpnnSaveSchema>;
export type FnrsSharItem = z.infer<typeof fnrsSharItemSchema>;
export type FnrsCnsltnApplySaveRequestDto = z.infer<typeof fnrsCnsltnApplySaveSchema>;
@@ -1,21 +1,20 @@
import type {
AtflMngClientResponseDto,
BizCstSaveBase,
BzScaleSaveBase,
CklstResultSaveBase,
DocPrgrsUpdateRequestDto,
EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
RvwOpnnSaveBase,
SplmntDmndResponseExtension,
SplmntDmndSaveBase,
} from "@/shared/model";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
import type {
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnRvwOpnnSaveRequestDto,
FnrsCnsltnSplmntDmndItem,
} from "./fnrs-cnsltn-apply-payload-schema";
import type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
@@ -31,33 +30,6 @@ export type {
EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model";
/** 백엔드 FnrsCnsltnMngSaveRequestDto */
export interface FnrsCnsltnMngSaveRequestDto {
fnrsCnsltnDocNo?: string;
admdstCd?: string;
envoSeComCd?: string;
taskSeComCd?: string;
cfmtnBgtBizNo?: string;
bgtAplyYr?: string;
bizNm?: string;
bizBgngYr?: string;
bizEndYr?: string;
bizCn?: string;
aplcntNm?: string;
aplcntMblTelno?: string;
aplcntOgdpNm?: string;
aplcntEmlAddr?: string;
dsgnrNm?: string;
dsgnrTelno?: string;
dsgnrOgdpNm?: string;
dsgnrEmlAddr?: string;
atchFileGroupId?: string;
sharAtchFileGroupId?: string;
prgrsSttsComCd?: string;
rtlztnFcltSeComCd?: string;
rtlztnUsgSeComCd?: string;
}
/** 백엔드 FnrsCnsltnMngUpdateRequestDto */
export type FnrsCnsltnMngUpdateRequestDto = Omit<FnrsCnsltnMngSaveRequestDto, "fnrsCnsltnDocNo">;
@@ -110,34 +82,6 @@ interface FnrsCnsltnMngResponseExtension {
export type FnrsCnsltnMngResponseDto = Omit<FnrsCnsltnMngSaveRequestDto, "fnrsCnsltnDocNo"> &
FnrsCnsltnMngResponseExtension;
export type BzScaleItem = BzScaleSaveBase;
export type BizCstItem = BizCstSaveBase;
export interface CklstResultItem extends CklstResultSaveBase {
selected?: string;
}
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
export type FnrsCnsltnSplmntDmndItem = SplmntDmndSaveBase;
/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsSharItem */
export interface FnrsSharItem {
fnrsSharSeCd?: string;
exstnAmt?: number;
chgRqtAmt?: number;
ajmtAmt?: number;
rmrkCn?: string;
envoRmrkCn?: string;
}
/** 백엔드 FnrsCnsltnRvwOpnnSaveRequestDto */
export interface FnrsCnsltnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase {
fnrsCnsltnDocNo: string;
rvwOpnnSn: number;
}
/** 백엔드 FnrsCnsltnRvwOpnnResponseDto */
export type FnrsCnsltnRvwOpnnResponseDto = Partial<
Pick<FnrsCnsltnRvwOpnnSaveRequestDto, "fnrsCnsltnDocNo" | "rvwOpnnSn">
@@ -151,6 +95,14 @@ export type FnrsCnsltnSplmntDmndResponseDto = FnrsCnsltnSplmntDmndItem &
fnrsCnsltnDocNo?: string;
};
/** 백엔드 RagIndexingInfoDto */
export interface RagIndexingInfoDto {
isInProgress: boolean;
elapsedSeconds?: number;
estimatedRemainingSeconds?: number;
message?: string;
}
/** 백엔드 FnrsCnsltnMngDetResponseDto */
export type FnrsCnsltnMngDetResponseDto = PbadmsMngDetResponseBase<
FnrsCnsltnMngResponseDto,
@@ -159,21 +111,9 @@ export type FnrsCnsltnMngDetResponseDto = PbadmsMngDetResponseBase<
> & {
fnrsSharList?: FnrsSharBizCstTreeDto[];
sharAtchFileList?: AtflMngClientResponseDto[];
ragIndexingInfo?: RagIndexingInfoDto | null;
};
/** 백엔드 FnrsCnsltnApplySaveRequestDto */
export interface FnrsCnsltnApplySaveRequestDto extends PbadmsApplySaveHeader {
fnrsCnsltnMng: FnrsCnsltnMngSaveRequestDto;
bzScaleList?: BzScaleItem[];
bizCstList?: BizCstItem[];
cklstResultList?: CklstResultItem[];
cklstResultMap?: Record<string, CklstResultItem[]>;
esntlAtchFileList?: EsntlAtchFileItem[];
rvwOpnnList?: FnrsCnsltnRvwOpnnSaveRequestDto[];
splmntDmndList?: FnrsCnsltnSplmntDmndItem[];
fnrsSharList?: FnrsSharItem[];
}
/** 백엔드 FnrsCnsltnFileVrfcRsltResponseDto */
export interface FnrsCnsltnFileVrfcRsltResponseDto {
fileSeComCd?: string;
@@ -2,12 +2,12 @@ import { z } from "zod";
/** 백엔드 FnrsExamineRequestDto */
export const fnrsExamineRequestSchema = z.object({
prgrsSttsComCd: z.string().optional(),
prgrsSttsComCd: z.string().nullish(),
});
/** 백엔드 EnvoActnReviewReqDto */
export const envoActnReviewReqSchema = z.object({
specialRqstCn: z.string().optional(),
specialRqstCn: z.string().nullish(),
});
export type FnrsExamineRequestDto = z.infer<typeof fnrsExamineRequestSchema>;
@@ -2,13 +2,15 @@ import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
InstlSncnApplySaveRequestDto,
InstlSncnMngSaveRequestDto,
} from "../model/instl-sncn-apply-payload-schema";
import type {
AdstZoneResponseDto,
DocPrgrsUpdateRequestDto,
InstlSncnApplySaveRequestDto,
InstlSncnMngDetResponse,
InstlSncnMngResponseDto,
InstlSncnMngSaveRequestDto,
InstlSncnMngSearchParams,
InstlSncnMngService,
InstlSncnMngUpdateRequestDto,
@@ -3,9 +3,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
DocPrgrsUpdateRequestDto,
InstlSncnApplySaveRequestDto,
InstlSncnMngSaveRequestDto,
} from "../model/instl-sncn-apply-payload-schema";
import type {
DocPrgrsUpdateRequestDto,
InstlSncnMngSearchParams,
InstlSncnMngUpdateRequestDto,
} from "../model/instl-sncn-interface";
+20 -8
View File
@@ -9,6 +9,26 @@ export {
useInstlSncnSubmitSupplement,
useInstlSncnTechReview,
} from "./api/queries";
export type {
InstlSncnApplySaveRequestDto,
InstlSncnBizCstSaveRequestDto,
InstlSncnBzScaleSaveDto,
InstlSncnCklstResultSaveDto,
InstlSncnEsntlAtchFileSaveDto,
InstlSncnMngSaveRequestDto,
InstlSncnRvwOpnnSaveRequestDto,
InstlSncnSplmntDmndSaveDto,
} from "./model/instl-sncn-apply-payload-schema";
export {
instlSncnApplySaveSchema,
instlSncnBizCstItemSchema,
instlSncnBzScaleItemSchema,
instlSncnCklstResultItemSchema,
instlSncnEsntlAtchFileItemSchema,
instlSncnMngSaveSchema,
instlSncnRvwOpnnSaveSchema,
instlSncnSplmntDmndItemSchema,
} from "./model/instl-sncn-apply-payload-schema";
export {
type AdstZoneResponseDto,
type AtflMngClientResponseDto,
@@ -19,18 +39,10 @@ export {
type ElaprDocPrgrsClientResponseDto,
type EsntlAtchFileMngTreeResponseDto,
INSTL_SNCN_REGISTER_SELECTION_KEY,
type InstlSncnApplySaveRequestDto,
type InstlSncnBizCstSaveRequestDto,
type InstlSncnBzScaleSaveDto,
type InstlSncnCklstResultSaveDto,
type InstlSncnEsntlAtchFileSaveDto,
type InstlSncnMngDetResponse,
type InstlSncnMngResponseDto,
type InstlSncnMngSaveRequestDto,
type InstlSncnMngSearchParams,
type InstlSncnRvwOpnnResponseDto,
type InstlSncnRvwOpnnSaveRequestDto,
type InstlSncnSplmntDmndResponseDto,
type InstlSncnSplmntDmndSaveDto,
type ReviewStepCode,
} from "./model/instl-sncn-interface";
@@ -0,0 +1,108 @@
import { z } from "zod";
/** 백엔드 InstlSncnMngSaveRequestDto */
export const instlSncnMngSaveSchema = z.object({
instlSncnDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
sncnSeComCd: z.string().nullish(),
taskSeComCd: z.string().nullish(),
bgtBizCd: z.string().nullish(),
bgtAplyYr: z.string().nullish(),
bizNm: z.string().nullish(),
bizBgngYr: z.string().nullish(),
bizEndYr: z.string().nullish(),
bizCn: z.string().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
rtlztnFcltSeComCd: z.string().nullish(),
rtlztnUsgSeComCd: z.string().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.BzScaleItem (diff는 API 미전송) */
export const instlSncnBzScaleItemSchema = z.object({
cnstpSeCd: z.string().nullish(),
bscPl: z.number().nullish(),
budgetReflct: z.number().nullish(),
thtiDsgn: z.number().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.BizCstItem */
export const instlSncnBizCstItemSchema = z.object({
acntgSeCd: z.string().nullish(),
bscPlanAmt: z.number().nullish(),
bgtRfltAmt: z.number().nullish(),
tstmDesignAmt: z.number().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.CklstResultItem */
export const instlSncnCklstResultItemSchema = z.object({
cklstRvwSn: z.number().nullish(),
cklstArtclSn: z.number().nullish(),
cklstSn: z.number().nullish(),
rvwStpComCd: z.string().nullish(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.EsntlAtchFileItem */
export const instlSncnEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 InstlSncnRvwOpnnSaveRequestDto */
export const instlSncnRvwOpnnSaveSchema = z.object({
instlSncnDocNo: z.string(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.InstlSncnSplmntDmndItem */
export const instlSncnSplmntDmndItemSchema = z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
splmntDmndCn: z.string().nullish(),
splmntDmndDt: z.string().nullish(),
splmntDmndAtchFileGroupId: z.string().nullish(),
splmntCn: z.string().nullish(),
splmntPicNm: z.string().nullish(),
splmntCmptnDt: z.string().nullish(),
splmntAtchFileGroupId: z.string().nullish(),
cmptnYn: z.string().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto */
export const instlSncnApplySaveSchema = z.object({
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
instlSncnMng: instlSncnMngSaveSchema,
bzScaleList: z.array(instlSncnBzScaleItemSchema).nullish(),
bizCstList: z.array(instlSncnBizCstItemSchema).nullish(),
cklstResultList: z.array(instlSncnCklstResultItemSchema).nullish(),
cklstResultMap: z.record(z.array(instlSncnCklstResultItemSchema)).nullish(),
esntlAtchFileList: z.array(instlSncnEsntlAtchFileItemSchema).nullish(),
rvwOpnnList: z.array(instlSncnRvwOpnnSaveSchema).nullish(),
splmntDmndList: z.array(instlSncnSplmntDmndItemSchema).nullish(),
});
export type InstlSncnMngSaveRequestDto = z.infer<typeof instlSncnMngSaveSchema>;
export type InstlSncnBzScaleSaveDto = z.infer<typeof instlSncnBzScaleItemSchema>;
export type InstlSncnBizCstSaveRequestDto = z.infer<typeof instlSncnBizCstItemSchema>;
export type InstlSncnCklstResultSaveDto = z.infer<typeof instlSncnCklstResultItemSchema>;
export type InstlSncnEsntlAtchFileSaveDto = z.infer<typeof instlSncnEsntlAtchFileItemSchema>;
export type InstlSncnRvwOpnnSaveRequestDto = z.infer<typeof instlSncnRvwOpnnSaveSchema>;
export type InstlSncnSplmntDmndSaveDto = z.infer<typeof instlSncnSplmntDmndItemSchema>;
export type InstlSncnApplySaveRequestDto = z.infer<typeof instlSncnApplySaveSchema>;
@@ -1,24 +1,24 @@
import type {
AtflMngClientResponseDto,
BizCstSaveBase,
BizCstTreeDto,
BizSclTreeDto,
BzScaleSaveBase,
CklstResultSaveBase,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
RvwOpnnSaveBase,
SplmntDmndResponseExtension,
SplmntDmndSaveBase,
} from "@/shared/model";
import type {
InstlSncnApplySaveRequestDto,
InstlSncnMngSaveRequestDto,
InstlSncnRvwOpnnSaveRequestDto,
InstlSncnSplmntDmndSaveDto,
} from "./instl-sncn-apply-payload-schema";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
@@ -41,189 +41,48 @@ export const INSTL_SNCN_REGISTER_SELECTION_KEY = "instlSncnRegisterSelection";
/** 백엔드 InstlSncnMngSearchDto + Pageable */
export interface InstlSncnMngSearchParams extends PageableParams {
/** 설치인가문서번호 */
instlSncnDocNo?: string;
/** 행정구역코드 */
admdstCd?: string;
/** 환경청구분공통코드 */
envoSeComCd?: string;
/** 업무구분공통코드 */
taskSeComCd?: string;
/** 예산사업코드 */
bgtBizCd?: string;
/** 예산신청연도 */
bgtAplyYr?: string;
/** 사업명 */
bizNm?: string;
/** 사업시작연도 */
bizBgngYr?: string;
/** 사업종료연도 */
bizEndYr?: string;
/** 사업내용 */
bizCn?: string;
/** 신청자명 */
aplcntNm?: string;
/** 신청자휴대전화번호 */
aplcntMblTelno?: string;
/** 신청자소속명 */
aplcntOgdpNm?: string;
/** 신청자이메일주소 */
aplcntEmlAddr?: string;
/** 설계자명 */
dsgnrNm?: string;
/** 설계자전화번호 */
dsgnrTelno?: string;
/** 설계자소속명 */
dsgnrOgdpNm?: string;
/** 설계자이메일주소 */
dsgnrEmlAddr?: string;
/** 첨부파일그룹아이디 */
atchFileGroupId?: string;
/** 진행상태공통코드 */
prgrsSttsComCd?: string;
}
/** 설치인가관리 저장 요청 DTO */
export interface InstlSncnMngSaveRequestDto {
/** 설치인가문서번호 */
instlSncnDocNo?: string;
/** 행정구역코드 */
admdstCd?: string;
/** 환경청구분공통코드 */
envoSeComCd?: string;
/** 인가구분공통코드 */
sncnSeComCd: string;
/** 업무구분공통코드 */
taskSeComCd: string;
/** 예산사업코드 */
bgtBizCd?: string;
/** 예산신청연도 */
bgtAplyYr?: string;
/** 사업명 */
bizNm: string;
/** 사업시작연도 */
bizBgngYr: string;
/** 사업종료연도 */
bizEndYr: string;
/** 사업내용 */
bizCn: string;
/** 신청자명 */
aplcntNm?: string;
/** 신청자휴대전화번호 */
aplcntMblTelno?: string;
/** 신청자소속명 */
aplcntOgdpNm?: string;
/** 신청자이메일주소 */
aplcntEmlAddr?: string;
/** 설계자명 */
dsgnrNm: string;
/** 설계자전화번호 */
dsgnrTelno: string;
/** 설계자소속명 */
dsgnrOgdpNm: string;
/** 설계자이메일주소 */
dsgnrEmlAddr: string;
/** 첨부파일그룹아이디 */
atchFileGroupId?: string;
/** 진행상태공통코드 */
prgrsSttsComCd?: string;
/** 재이용시설구분공통코드 */
rtlztnFcltSeComCd?: string;
/** 재이용용도구분공통코드 */
rtlztnUsgSeComCd?: string;
}
/** 설치인가관리 수정 요청 DTO */
/** 백엔드 InstlSncnMngUpdateRequestDto */
export type InstlSncnMngUpdateRequestDto = InstlSncnMngSaveRequestDto;
/** 설치인가관리 응답 DTO */
export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
/** 설치인가문서번호 (필수로 재정의) */
/** 백엔드 InstlSncnMngResponseDto */
export type InstlSncnMngResponseDto = InstlSncnMngSaveRequestDto & {
instlSncnDocNo: string;
/** 환경청구분공통코드명 */
envoSeCdNm: string;
/** 인가구분공통코드명 */
sncnSeCdNm: string;
/** 업무구분공통코드명 */
taskSeCdNm: string;
/** 진행상태공통코드명 */
prgrsSttsCdNm: string;
/** 기술검토회신이력여부 */
techRplyHstryYn: "Y" | "N";
/** 재이용시설구분공통코드명 */
rtlztnFcltSeComNm: string;
/** 재이용용도구분공통코드명 */
rtlztnUsgSeComNm: string;
/** 등록일시 */
regDt: string;
/** 등록자 UUID */
rgtr: string;
/** 등록자명 */
rgtrNm: string;
/** 수정 일시 */
mdfcnDt: string;
/** 시도 */
sido: string;
/** 구군 */
gugun: string;
}
/** 설치인가 신청 - 사업규모 저장 DTO(배열 원소) */
export interface InstlSncnBzScaleSaveDto extends BzScaleSaveBase {
/** 공종구분코드 */
cnstpSeCd: string;
/** 기본계획수량 */
bscPl: number;
/** 예산반영수량 */
budgetReflct: number;
/** 금회설계수량 */
thtiDsgn: number;
/** 증감(C-B) — 클라이언트 계산용, API 미전송 */
diff?: number;
}
/** 설치인가 신청 - 총사업비 저장 DTO(배열 원소) */
export interface InstlSncnBizCstSaveRequestDto extends BizCstSaveBase {
/** 회계구분코드 */
acntgSeCd: string;
/** 기본계획금액 */
bscPlanAmt: number;
/** 예산반영금액 */
bgtRfltAmt: number;
/** 금회설계금액 */
tstmDesignAmt: number;
}
/** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */
export interface InstlSncnCklstResultSaveDto extends CklstResultSaveBase {
/** 점검표일련번호 */
cklstSn: number;
/** 점검표항목일련번호 */
cklstArtclSn: number;
/** 점검표검토일련번호 */
cklstRvwSn: number;
/** 선택값(Y/N/null) */
selected: "Y" | "N" | null;
/** 담당자검토내용(기술검토의견) */
picRvwCn?: string | null;
}
/** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */
export interface InstlSncnEsntlAtchFileSaveDto extends EsntlAtchFileSaveBase {
/** 필수첨부파일아이디 */
esntlAtchFileId: string;
/** 첨부파일그룹아이디 */
atchFileGroupId: string;
}
/** 설치인가검토의견 저장 요청 DTO */
export interface InstlSncnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase {
/** 설치인가문서번호 */
instlSncnDocNo: string;
}
/** 설치인가 보완요구 저장 DTO(배열 원소) */
export type InstlSncnSplmntDmndSaveDto = SplmntDmndSaveBase;
};
/** 설치인가 보완요구 응답 DTO */
export type InstlSncnSplmntDmndResponseDto = InstlSncnSplmntDmndSaveDto &
@@ -231,26 +90,6 @@ export type InstlSncnSplmntDmndResponseDto = InstlSncnSplmntDmndSaveDto &
instlSncnDocNo?: string;
};
/** 설치인가 신청 - 통합 저장 요청 DTO */
export interface InstlSncnApplySaveRequestDto extends PbadmsApplySaveHeader {
/** 설치인가관리 정보 */
instlSncnMng: InstlSncnMngSaveRequestDto;
/** 사업규모 목록 */
bzScaleList?: InstlSncnBzScaleSaveDto[];
/** 총사업비 목록 */
bizCstList?: InstlSncnBizCstSaveRequestDto[];
/** 체크리스트 검토결과 목록 */
cklstResultList?: InstlSncnCklstResultSaveDto[];
/** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */
cklstResultMap?: Record<string, InstlSncnCklstResultSaveDto[]>;
/** 필수첨부파일 목록 */
esntlAtchFileList?: InstlSncnEsntlAtchFileSaveDto[];
/** 검토의견 목록 */
rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[];
/** 보완요구 목록 */
splmntDmndList?: InstlSncnSplmntDmndSaveDto[];
}
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
/** 설치인가검토의견 응답 DTO */
@@ -4,7 +4,7 @@ import { z } from "zod";
export const pwrkCnsltnDocCrtRsltSaveRequestSchema = z.object({
crtDmndUuid: z.string().uuid(),
pwrkCnsltnDocNo: z.string().min(1).max(100),
atchFileGroupId: z.string().max(30).optional(),
atchFileGroupId: z.string().max(30).nullish(),
});
export type PwrkCnsltnDocCrtRsltSaveRequestDto = z.infer<
@@ -0,0 +1,19 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
PwrkCnsltnDocEvlId,
PwrkCnsltnDocEvlService,
} from "../model/pwrk-cnsltn-doc-evl-interface";
import type { PwrkCnsltnDocEvlSaveRequestDto } from "../model/pwrk-cnsltn-doc-evl-payload-schema";
const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_DOC_EVL;
async function save(body: PwrkCnsltnDocEvlSaveRequestDto): Promise<PwrkCnsltnDocEvlId> {
const response = await client.post<PwrkCnsltnDocEvlId>(BASE, body);
return response.data;
}
export const pwrkCnsltnDocEvlService = {
save,
} satisfies PwrkCnsltnDocEvlService;
@@ -0,0 +1,11 @@
import { useMutation } from "@tanstack/react-query";
import type { PwrkCnsltnDocEvlSaveRequestDto } from "../model/pwrk-cnsltn-doc-evl-payload-schema";
import { pwrkCnsltnDocEvlService } from "./pwrk-cnsltn-doc-evl-service";
export function useSavePwrkCnsltnDocEvl() {
return useMutation({
mutationFn: (body: PwrkCnsltnDocEvlSaveRequestDto) => pwrkCnsltnDocEvlService.save(body),
});
}
@@ -0,0 +1,10 @@
export { pwrkCnsltnDocEvlService } from "./api/pwrk-cnsltn-doc-evl-service";
export { useSavePwrkCnsltnDocEvl } from "./api/queries";
export type {
PwrkCnsltnDocEvlId,
PwrkCnsltnDocEvlService,
} from "./model/pwrk-cnsltn-doc-evl-interface";
export {
type PwrkCnsltnDocEvlSaveRequestDto,
pwrkCnsltnDocEvlSaveRequestSchema,
} from "./model/pwrk-cnsltn-doc-evl-payload-schema";
@@ -0,0 +1,12 @@
import type { PwrkCnsltnDocEvlSaveRequestDto } from "./pwrk-cnsltn-doc-evl-payload-schema";
/** 백엔드 PwrkCnsltnDocEvlId (IdClass) */
export interface PwrkCnsltnDocEvlId {
pwrkCnsltnDocMng: string;
pwrkCnsltnDocEvlUuid: string;
}
/** Controller public 메서드명·시그니처와 동일 (save만) */
export interface PwrkCnsltnDocEvlService {
save(body: PwrkCnsltnDocEvlSaveRequestDto): Promise<PwrkCnsltnDocEvlId>;
}
@@ -0,0 +1,12 @@
import { z } from "zod";
/** 백엔드 PwrkCnsltnDocEvlSaveRequestDto */
export const pwrkCnsltnDocEvlSaveRequestSchema = z.object({
pwrkCnsltnDocNo: z.string().min(1),
pwrkCnsltnDocEvlUuid: z.string().uuid().nullish(),
pwrkCnsltnDocEvlCn: z.string().max(2000).nullish(),
pwrkCnsltnDocEvlScr: z.number().nullish(),
pwrkCnsltnDocEvlDt: z.string().nullish(),
});
export type PwrkCnsltnDocEvlSaveRequestDto = z.infer<typeof pwrkCnsltnDocEvlSaveRequestSchema>;
@@ -3,18 +3,18 @@ import { z } from "zod";
/** 백엔드 PwrkActnResultSubmitDto.ActnItemDto */
export const pwrkActnResultActnItemSchema = z.object({
rvwOpnnSn: z.number(),
actnRsltCn: z.string().optional(),
rfltYn: z.string().optional(),
atchFileGroupId: z.string().optional(),
actnRsltCn: z.string().nullish(),
rfltYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 PwrkActnResultSubmitDto */
export const pwrkActnResultSubmitSchema = z.object({
isSubmit: z.boolean(),
actnList: z.array(pwrkActnResultActnItemSchema).optional(),
totActnSummary: z.string().optional(),
muniDocNo: z.string().optional(),
pwrkAtchFileGroupId: z.string().optional(),
actnList: z.array(pwrkActnResultActnItemSchema).nullish(),
totActnSummary: z.string().nullish(),
muniDocNo: z.string().nullish(),
pwrkAtchFileGroupId: z.string().nullish(),
});
export type PwrkActnResultSubmitDto = z.infer<typeof pwrkActnResultSubmitSchema>;
@@ -2,160 +2,160 @@ 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(),
sewerBscPlanDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
sewerBscPlanSeComCd: z.string().nullish(),
sewerBscPlanNm: z.string().nullish(),
taskSeComCd: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
});
/** 백엔드 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(),
sewerBscPlanDocNo: z.string().nullish(),
chgCycl: z.number().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
planYr: z.string().nullish(),
crtrYr: z.string().nullish(),
bizOtlnCn: z.string().nullish(),
frlvlGoalYr: z.string().nullish(),
sclvlGoalYr: z.string().nullish(),
thlvlGoalYr: z.string().nullish(),
folvlGoalYr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
export const sewerBscPlanOtlnItemSchema = z.object({
planStpNo: z.number().optional(),
sewerPlanPpltnCnt: z.number().optional(),
sewerPrcsPpltnCnt: z.number().optional(),
sewerPsplyRt: z.number().optional(),
planStpNo: z.number().nullish(),
sewerPlanPpltnCnt: z.number().nullish(),
sewerPrcsPpltnCnt: z.number().nullish(),
sewerPsplyRt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
export const sewerBscPlanCklstResultItemSchema = z.object({
cklstRvwSn: z.number().optional(),
cklstArtclSn: z.number().optional(),
cklstSn: z.number().optional(),
cklstRvwSn: z.number().nullish(),
cklstArtclSn: z.number().nullish(),
cklstSn: z.number().nullish(),
rvwStpComCd: z.string(),
selected: z.string().optional(),
selected: z.enum(["Y", "N"]).nullish(),
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(),
planStpNo: z.number().nullish(),
gvexpAmt: z.number().nullish(),
lcexpAmt: z.number().nullish(),
orgprBdchgAmt: z.number().nullish(),
etcAmt: z.number().nullish(),
sumAmt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
export const sewerBscPlanSplmntDmndItemSchema = z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
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(),
splmntDmndCn: z.string().nullish(),
splmntDmndDt: z.string().nullish(),
splmntDmndAtchFileGroupId: z.string().nullish(),
splmntCn: z.string().nullish(),
splmntPicNm: z.string().nullish(),
splmntCmptnDt: z.string().nullish(),
splmntAtchFileGroupId: z.string().nullish(),
cmptnYn: z.string().nullish(),
});
/** 백엔드 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(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
export const sewerBscPlanEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().optional(),
atchFileGroupId: z.string().optional(),
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
export const sewerBscPlanBizCstItemSchema = z.object({
planStpNo: z.number().optional(),
acntgSeCd: z.string().optional(),
cstrnAmt: z.number().optional(),
planStpNo: z.number().nullish(),
acntgSeCd: z.string().nullish(),
cstrnAmt: z.number().nullish(),
});
/** 백엔드 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(),
planStpNo: z.number().nullish(),
swtrmtFcltCd: z.string().nullish(),
prcsZoneArea: z.number().nullish(),
swtrmtPpltnCnt: z.number().nullish(),
planSwqty: z.number().nullish(),
fcltCpct: z.number().nullish(),
sprsysDrtwPplLen: z.number().nullish(),
sprsysExlnPplLen: z.number().nullish(),
cbsysPplLen: z.number().nullish(),
icptgPplLen: z.number().nullish(),
drgCnt: z.number().nullish(),
drtwRlyPumpCnt: z.number().nullish(),
drtwPumpCnt: z.number().nullish(),
exlnPumpCnt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
export const sewerBscPlanTmplntItemSchema = z.object({
swtrmtFcltCd: z.string().optional(),
atchFileGroupId: z.string().optional(),
sewerBscPlanTmplntDtlList: z.array(sewerBscPlanTmplntDtlItemSchema).optional(),
swtrmtFcltCd: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
sewerBscPlanTmplntDtlList: z.array(sewerBscPlanTmplntDtlItemSchema).nullish(),
});
/** 백엔드 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(),
dtStrgDt: z.string().nullish(),
swtrmtFcltCd: z.string().nullish(),
prcsZoneArea: z.number().nullish(),
swtrmtPpltnCnt: z.number().nullish(),
planSwqty: z.number().nullish(),
fcltCpct: z.number().nullish(),
sprsysDrtwPplLen: z.number().nullish(),
sprsysExlnPplLen: z.number().nullish(),
cbsysPplLen: z.number().nullish(),
icptgPplLen: z.number().nullish(),
drgCnt: z.number().nullish(),
drtwRlyPumpCnt: z.number().nullish(),
drtwPumpCnt: z.number().nullish(),
exlnPumpCnt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto */
export const sewerBscPlanApplySaveSchema = z.object({
techReViewSaveCd: z.string().optional(),
rvwStpCd: z.string().optional(),
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
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(),
sewerBscPlanOtlnList: z.array(sewerBscPlanOtlnItemSchema).nullish(),
cklstResultList: z.array(sewerBscPlanCklstResultItemSchema).nullish(),
fnrsCstList: z.array(sewerBscPlanFnrsCstItemSchema).nullish(),
splmntDmndList: z.array(sewerBscPlanSplmntDmndItemSchema).nullish(),
rvwOpnnList: z.array(sewerBscPlanRvwOpnnItemSchema).nullish(),
esntlAtchFileList: z.array(sewerBscPlanEsntlAtchFileItemSchema).nullish(),
bizCstList: z.array(sewerBscPlanBizCstItemSchema).nullish(),
sewerBscPlanTmplntList: z.array(sewerBscPlanTmplntItemSchema).nullish(),
tmplntTmprStrgList: z.array(sewerBscPlanTmplntTmprStrgItemSchema).nullish(),
});
export type SewerBscPlanSaveRequestDto = z.infer<typeof sewerBscPlanSaveSchema>;
@@ -1,7 +1,13 @@
import { z } from "zod";
import {
designCnstnBzScaleItemSchema,
designCnstnEsntlAtchFileItemSchema,
designCnstnMngSaveSchema,
} from "@/entities/design-cnstn";
export const designCnstnRegistSchema = z.object({
designCnstnMng: z.object({
designCnstnMng: designCnstnMngSaveSchema.extend({
bizNm: z.string().min(1, "사업명은 필수입니다"),
bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"),
bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"),
@@ -17,36 +23,20 @@ export const designCnstnRegistSchema = z.object({
.string()
.min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
designCnstnDocNo: z.string().optional(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
prgrsSttsComCd: z.string().optional(),
atchFileGroupId: z.string().optional(),
envoSeCdNm: z.string().optional(),
taskSeCdNm: z.string().optional(),
prgrsSttsCdNm: z.string().optional(),
sido: z.string().optional(),
gugun: z.string().optional(),
regDt: z.string().optional(),
rgtr: z.string().optional(),
rgtrNm: z.string().optional(),
mdfcnDt: z.string().optional(),
sido: z.string().nullish(),
gugun: z.string().nullish(),
}),
bzScaleList: z.array(
z.object({
designCnstnBzScaleItemSchema.extend({
cnstpSeCd: z.string(),
bscPl: z.number(),
budgetReflct: z.number(),
thtiDsgn: z.number(),
diff: z.number().optional(),
diff: z.number().nullish(),
}),
),
esntlAtchFileList: z.array(
z.object({
designCnstnEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(),
atchFileGroupId: z.string(),
}),
@@ -62,7 +52,7 @@ export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
return taskSeComCd.join(",");
}
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] {
if (!taskSeComCd) return [];
return taskSeComCd.split(",").filter(Boolean);
}
@@ -1,9 +1,13 @@
import type { IUserInfo } from "@/shared/store";
import type { DesignCnstnMngDetResponseDto } from "@/entities/design-cnstn";
import {
type DesignCnstnApplySaveRequestDto,
designCnstnApplySaveSchema,
type DesignCnstnMngDetResponseDto,
} from "@/entities/design-cnstn";
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import type { DesignCnstnRegistFormData } from "./design-cnstn-regist-schema";
import { parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
import { formatTaskSeComCdForApi, parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignCnstnRegistFormData {
const detDto = data.detDto;
@@ -33,15 +37,8 @@ export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignC
envoSeComCd: detDto.envoSeComCd,
prgrsSttsComCd: detDto.prgrsSttsComCd,
atchFileGroupId: detDto.atchFileGroupId ?? "",
envoSeCdNm: detDto.envoSeCdNm,
taskSeCdNm: detDto.taskSeCdNm,
prgrsSttsCdNm: detDto.prgrsSttsCdNm,
sido: detDto.sido,
gugun: detDto.gugun,
regDt: detDto.regDt,
rgtr: detDto.rgtr,
rgtrNm: detDto.rgtrNm,
mdfcnDt: detDto.mdfcnDt,
},
bzScaleList: bizSclList.map((item) => ({
cnstpSeCd: item.cnstpSeCd,
@@ -90,3 +87,20 @@ export function getCreateDefaultValues(
privacyConsent: "N",
};
}
export function buildFormPayload(
data: DesignCnstnRegistFormData,
prgrsSttsComCd: "CV" | "CT",
options: { isEditMode: boolean; docNo?: string },
): DesignCnstnApplySaveRequestDto {
return designCnstnApplySaveSchema.parse({
designCnstnMng: {
...data.designCnstnMng,
taskSeComCd: formatTaskSeComCdForApi(data.designCnstnMng.taskSeComCd),
designCnstnDocNo: options.isEditMode ? options.docNo : undefined,
prgrsSttsComCd,
},
bzScaleList: data.bzScaleList,
esntlAtchFileList: data.esntlAtchFileList,
});
}
@@ -4,6 +4,7 @@ import type {
DesignCnstnApplySaveRequestDto,
DesignCnstnMngDetResponseDto,
} from "@/entities/design-cnstn";
import { designCnstnApplySaveSchema } from "@/entities/design-cnstn";
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import type { ReviewStepCode } from "../model/view-types";
@@ -30,11 +31,11 @@ export function buildSavePayload(
throw new Error("상세 정보(detDto)가 없습니다.");
}
return {
return designCnstnApplySaveSchema.parse({
designCnstnMng: { ...detDto, prgrsSttsComCd },
bzScaleList: toBzScaleSaveDto(data.bizSclList ?? []),
esntlAtchFileList: toEsntlAtchFileSaveList(data.esntlFileList),
};
});
}
export function buildTechReviewPayload(
@@ -45,10 +46,10 @@ export function buildTechReviewPayload(
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
opnrNm?: string,
): DesignCnstnApplySaveRequestDto {
return {
return designCnstnApplySaveSchema.parse({
...buildSavePayload(data, prgrsSttsComCd),
techReViewSaveCd,
rvwStpCd: reviewStep,
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
};
});
}
@@ -72,7 +72,7 @@ function getBadgeColor(state: BadgeState) {
}
}
function formatOptional(value: string | undefined) {
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -9,9 +9,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import {
type DesignCnstnApplySaveRequestDto,
designCnstnKeys,
type DesignCnstnMngDetResponseDto,
designCnstnMngService,
@@ -27,12 +26,12 @@ import {
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { type DesignCnstnRegistFormData, designCnstnRegistSchema } from "../lib/design-cnstn-regist-schema";
import {
type DesignCnstnRegistFormData,
designCnstnRegistSchema,
formatTaskSeComCdForApi,
} from "../lib/design-cnstn-regist-schema";
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
buildFormPayload,
getCreateDefaultValues,
mapDetailToFormData,
} from "../lib/form-data-mapper";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
@@ -68,29 +67,18 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
: getCreateDefaultValues(userInfo, createDefaults),
});
function buildFormPayload(
data: DesignCnstnRegistFormData,
prgrsSttsComCd: "CV" | "CT",
): DesignCnstnApplySaveRequestDto {
const { taskSeComCd: formTaskSeComCd, ...designCnstnMngRest } = data.designCnstnMng;
return {
designCnstnMng: {
...designCnstnMngRest,
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
designCnstnDocNo: isEditMode ? initialData?.detDto?.designCnstnDocNo : undefined,
prgrsSttsComCd,
},
bzScaleList: data.bzScaleList,
esntlAtchFileList: data.esntlAtchFileList,
};
function createFormPayload(data: DesignCnstnRegistFormData, prgrsSttsComCd: "CV" | "CT") {
return buildFormPayload(data, prgrsSttsComCd, {
isEditMode,
docNo: initialData?.detDto?.designCnstnDocNo,
});
}
async function submitForm(
data: DesignCnstnRegistFormData,
prgrsSttsComCd: "CV" | "CT",
): Promise<string | undefined> {
return designCnstnMngService.saveApply(buildFormPayload(data, prgrsSttsComCd));
return designCnstnMngService.saveApply(createFormPayload(data, prgrsSttsComCd));
}
async function onSave(data: DesignCnstnRegistFormData) {
@@ -229,6 +217,7 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
</div>
</SectionCollapsibleGroup>
</form>
<RhfDevTool control={form.control} />
</FormProvider>
);
}
@@ -1,3 +1,5 @@
import type { RagIndexingInfoDto } from "@/entities/fnrs-cnsltn";
const AI_PENDING_STATUS_CODES = ["FV", "AC", "AV"] as const;
type AiPendingStatusCode = (typeof AI_PENDING_STATUS_CODES)[number];
@@ -17,5 +19,17 @@ function getAiPendingMessage(prgrsSttsComCd: string): string {
return AI_PENDING_MESSAGES[prgrsSttsComCd as AiPendingStatusCode];
}
export { AI_PENDING_STATUS_CODES, getAiPendingMessage, isAiPendingStatus };
function isRagIndexingInProgress(
prgrsSttsComCd: string,
ragIndexingInfo?: RagIndexingInfoDto | null,
): boolean {
return prgrsSttsComCd === "ER" && ragIndexingInfo?.isInProgress === true;
}
export {
AI_PENDING_STATUS_CODES,
getAiPendingMessage,
isAiPendingStatus,
isRagIndexingInProgress,
};
export type { AiPendingStatusCode };
@@ -9,6 +9,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
rvwOpnnRply: "rvwOpnnRply",
rqstTechRvw: "rqstTechRvw",
fileSplmnt: "fileSplmnt",
fileVrfc: "fileVrfc",
dsgntRvwr: "dsgntRvwr",
pwrkCnsltnDocCrt: "pwrkCnsltnDocCrt",
pwrkCnsltnDocMng: "pwrkCnsltnDocMng",
@@ -20,6 +21,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
atrzRply: "atrzRply",
actnCmptn: "actnCmptn",
rqstActnRvw: "rqstActnRvw",
costVrfc: "costVrfc",
} as const;
export type FnrsCnsltnViewButtonAction =
@@ -49,14 +51,19 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw],
TP: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply,
],
RB: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
RA: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw,
],
AV: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
EV: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
RK: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
RR: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply],
EP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
};
@@ -80,7 +87,11 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
Record<string, FnrsCnsltnViewButtonAction[]>
> = {
ER: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt, FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
ATRZ01: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr,
],
},
SL: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
@@ -135,6 +146,7 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
ATRZ01: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.costVrfc,
],
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
@@ -183,6 +195,9 @@ function resolveButtonConfigs(actions: FnrsCnsltnViewButtonAction[]): FnrsCnsltn
interface GetButtonConfigsOptions {
hasPendingApprovalRequest?: boolean;
hasCompletedDraft?: boolean;
canRetryFileVrfc?: boolean;
canRetryDraftGen?: boolean;
canRetryCostVrfc?: boolean;
}
const PENDING_APPROVAL_ACTIONS = new Set<FnrsCnsltnViewButtonAction>([
@@ -220,6 +235,19 @@ function resolveRoleUser01AtrzButtonActions(
const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action);
if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false;
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc) {
return options?.canRetryFileVrfc === true;
}
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt) {
return options?.canRetryFileVrfc !== true;
}
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) {
return options?.canRetryDraftGen === true;
}
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.costVrfc) {
return options?.canRetryCostVrfc === true;
}
if (options?.hasCompletedDraft != null) {
if (prgrsSttsComCd === "SL" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "RK") {
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) {
@@ -230,9 +258,6 @@ function resolveRoleUser01AtrzButtonActions(
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) {
return options.hasCompletedDraft;
}
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) {
return !options.hasCompletedDraft;
}
}
}
@@ -4,7 +4,6 @@ import {
type ChecklistFormState,
type ChecklistProgress,
type ChecklistRowKey,
type ChecklistStepMap,
REVIEW_STEP_CODES,
type ReviewStepCode,
} from "../model/view-types";
@@ -17,15 +16,6 @@ export function buildChecklistRowKey(
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)));
}
@@ -70,13 +60,6 @@ export function computeChecklistProgress(
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"],
@@ -93,23 +76,6 @@ export function computeReviewStepChecklistProgress(
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[],
@@ -1,14 +1,22 @@
import { z } from "zod";
import {
fnrsCnsltnBizCstItemSchema,
fnrsCnsltnBzScaleItemSchema,
fnrsCnsltnCklstResultItemSchema,
fnrsCnsltnEsntlAtchFileItemSchema,
fnrsCnsltnMngSaveSchema,
fnrsCnsltnSplmntDmndItemSchema,
fnrsSharItemSchema,
} from "@/entities/fnrs-cnsltn";
export const fnrsCnsltnRegistSchema = z.object({
fnrsCnsltnMng: z.object({
fnrsCnsltnMng: fnrsCnsltnMngSaveSchema.extend({
bizNm: z.string().min(1, "사업명은 필수입니다"),
bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"),
bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"),
bizCn: z.string().min(1, "사업내용은 필수입니다"),
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
cfmtnBgtBizNo: z.string().optional(),
bgtAplyYr: z.string().optional(),
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
dsgnrTelno: z
.string()
@@ -19,39 +27,20 @@ export const fnrsCnsltnRegistSchema = z.object({
.string()
.min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
fnrsCnsltnDocNo: z.string().optional(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
rtlztnFcltSeComCd: z.string().optional(),
rtlztnUsgSeComCd: z.string().optional(),
prgrsSttsComCd: z.string().optional(),
atchFileGroupId: z.string().optional(),
sharAtchFileGroupId: z.string().optional(),
envoSeCdNm: z.string().optional(),
taskSeCdNm: z.string().optional(),
prgrsSttsCdNm: z.string().optional(),
sido: z.string().optional(),
gugun: z.string().optional(),
regDt: z.string().optional(),
rgtr: z.string().optional(),
rgtrNm: z.string().optional(),
mdfcnDt: z.string().optional(),
sido: z.string().nullish(),
gugun: z.string().nullish(),
}),
bzScaleList: z.array(
z.object({
fnrsCnsltnBzScaleItemSchema.extend({
cnstpSeCd: z.string(),
bscPl: z.number(),
budgetReflct: z.number(),
thtiDsgn: z.number(),
diff: z.number().optional(),
diff: z.number().nullish(),
}),
),
bizCstList: z.array(
z.object({
fnrsCnsltnBizCstItemSchema.extend({
acntgSeCd: z.string(),
bscPlanAmt: z.number(),
bgtRfltAmt: z.number(),
@@ -59,46 +48,33 @@ export const fnrsCnsltnRegistSchema = z.object({
}),
),
fnrsSharList: z.array(
z.object({
fnrsSharItemSchema.extend({
fnrsSharSeCd: z.string(),
exstnAmt: z.number(),
chgRqtAmt: z.number(),
ajmtAmt: z.number().optional(),
rmrkCn: z.string().optional(),
ajmtAmt: z.number().nullish(),
rmrkCn: z.string().nullish(),
}),
),
cklstResultList: z.array(
z.object({
fnrsCnsltnCklstResultItemSchema.extend({
cklstSn: z.number(),
cklstArtclSn: z.number(),
cklstRvwSn: z.number(),
rvwStpComCd: z.string().nullish(),
selected: z.enum(["Y", "N"]).nullable(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
}),
),
esntlAtchFileList: z.array(
z.object({
fnrsCnsltnEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(),
atchFileGroupId: z.string(),
}),
),
splmntDmndList: z.array(
z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
fnrsCnsltnSplmntDmndItemSchema.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(),
splmntDmndAtchFileNm: z.string().nullish(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
@@ -112,7 +88,7 @@ export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
return taskSeComCd.join(",");
}
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] {
if (!taskSeComCd) return [];
return taskSeComCd.split(",").filter(Boolean);
}
@@ -1,9 +1,21 @@
import type { IUserInfo } from "@/shared/store";
import type { FnrsCnsltnMngDetResponseDto } from "@/entities/fnrs-cnsltn";
import { mapFnrsSharDetToFormList, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import {
type FnrsCnsltnApplySaveRequestDto,
fnrsCnsltnApplySaveSchema,
type FnrsCnsltnMngDetResponseDto,
} from "@/entities/fnrs-cnsltn";
import {
type CostItemDef,
type FnrsSharItemDef,
mapFnrsSharDetToFormList,
recalculateFnrsSharTotals,
toEsntlAtchFileSaveList,
toFnrsSharSaveList,
validateBizCstSharBalance,
} from "@/widgets/pbadms-sprt";
import type { FnrsCnsltnRegistFormData } from "./fnrs-cnsltn-regist-schema";
import { parseTaskSeComCdFromApi } from "./fnrs-cnsltn-regist-schema";
import { formatTaskSeComCdForApi, parseTaskSeComCdFromApi } from "./fnrs-cnsltn-regist-schema";
function mapSelectedValue(value?: string | null): "Y" | "N" | null {
if (value === "Y" || value === "N") return value;
@@ -47,15 +59,8 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
prgrsSttsComCd: detDto.prgrsSttsComCd,
atchFileGroupId: detDto.atchFileGroupId ?? "",
sharAtchFileGroupId: detDto.sharAtchFileGroupId ?? "",
envoSeCdNm: detDto.envoSeCdNm,
taskSeCdNm: detDto.taskSeCdNm,
prgrsSttsCdNm: detDto.prgrsSttsCdNm,
sido: detDto.sido,
gugun: detDto.gugun,
regDt: detDto.regDt,
rgtr: detDto.rgtr,
rgtrNm: detDto.rgtrNm,
mdfcnDt: detDto.mdfcnDt,
},
bzScaleList: bizSclList.map((item) => ({
cnstpSeCd: item.cnstpSeCd,
@@ -94,8 +99,6 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
splmntPicNm: item.splmntPicNm ?? "",
splmntCmptnDt: item.splmntCmptnDt ?? "",
splmntAtchFileGroupId: item.splmntAtchFileGroupId ?? "",
splmntAtchFileNm: item.splmntAtchFileInfo?.orgnlFileNm ?? "",
splmntAtchFileAtflId: item.splmntAtchFileInfo?.atflId ?? "",
cmptnYn: item.cmptnYn,
})) ?? [],
privacyConsent: "Y",
@@ -146,3 +149,46 @@ export function getCreateDefaultValues(
privacyConsent: "N",
};
}
export function buildFormPayload(
data: FnrsCnsltnRegistFormData,
prgrsSttsComCd: string,
options: {
isEditMode: boolean;
docNo?: string;
sharItems: FnrsSharItemDef[];
costItems: CostItemDef[];
},
): FnrsCnsltnApplySaveRequestDto | { validationMessage: string } {
const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, options.sharItems);
const validationMessage = validateBizCstSharBalance(
data.bizCstList,
withTotals,
options.sharItems,
options.costItems,
);
if (validationMessage) {
return { validationMessage };
}
return fnrsCnsltnApplySaveSchema.parse({
fnrsCnsltnMng: {
...data.fnrsCnsltnMng,
taskSeComCd: formatTaskSeComCdForApi(data.fnrsCnsltnMng.taskSeComCd),
fnrsCnsltnDocNo: options.isEditMode ? options.docNo : undefined,
prgrsSttsComCd,
},
bzScaleList: data.bzScaleList,
bizCstList: data.bizCstList,
fnrsSharList: toFnrsSharSaveList(withTotals),
cklstResultList: data.cklstResultList.map((item) => ({
...item,
selected: item.selected ?? undefined,
})),
esntlAtchFileList: data.esntlAtchFileList,
splmntDmndList: data.splmntDmndList.map((item) => ({
...item,
...(prgrsSttsComCd !== "SV" && prgrsSttsComCd !== "ST" && { cmptnYn: "Y" }),
})),
});
}
@@ -0,0 +1,32 @@
import { z } from "zod";
import { pwrkCnsltnDocEvlSaveRequestSchema } from "@/entities/pwrk-cnsltn-doc-evl";
export const PWRK_CNSLTN_DOC_EVL_SCR_OPTIONS = [
{ value: "5", label: "매우 만족" },
{ value: "4", label: "만족" },
{ value: "3", label: "보통" },
{ value: "2", label: "불만족" },
{ value: "1", label: "매우 불만족" },
] as const;
export const pwrkCnsltnDocEvlFormSchema = z.object({
pwrkCnsltnDocEvlScr: z.enum(["1", "2", "3", "4", "5"], {
required_error: "만족도를 선택해 주세요.",
invalid_type_error: "만족도를 선택해 주세요.",
}),
pwrkCnsltnDocEvlCn: z.string().max(2000).nullish(),
});
export type PwrkCnsltnDocEvlFormValues = z.infer<typeof pwrkCnsltnDocEvlFormSchema>;
export function buildPwrkCnsltnDocEvlSavePayload(
pwrkCnsltnDocNo: string,
form: PwrkCnsltnDocEvlFormValues,
) {
return pwrkCnsltnDocEvlSaveRequestSchema.parse({
pwrkCnsltnDocNo,
pwrkCnsltnDocEvlScr: Number(form.pwrkCnsltnDocEvlScr),
pwrkCnsltnDocEvlCn: form.pwrkCnsltnDocEvlCn?.trim() || undefined,
});
}
@@ -1,3 +1,5 @@
import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn";
import {
PBADMS_PROGRESS_ORDER,
REVIEW_STEP_CODES,
@@ -28,6 +30,25 @@ export function getVisibleReviewSteps(
return REVIEW_STEP_CODES.slice(0, count);
}
export function resolveVisibleReviewSteps(
prgrsSttsComCd: string,
userRoleCode: string | undefined,
cklstResultList: CklstRvwRsltTreeDto[],
): ReviewStepCode[] {
const byProgressAndRole = getVisibleReviewSteps(prgrsSttsComCd, userRoleCode);
const currentStep = byProgressAndRole.at(-1);
const stepsWithData = new Set<ReviewStepCode>();
for (const row of cklstResultList) {
const step = row.rvwStpComCd as ReviewStepCode | undefined;
if (step && REVIEW_STEP_CODES.includes(step)) stepsWithData.add(step);
}
return REVIEW_STEP_CODES.filter(
(step) => stepsWithData.has(step) || step === currentStep,
);
}
export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
@@ -10,6 +10,7 @@ import type {
FnrsCnsltnMngResponseDto,
FnrsCnsltnSplmntDmndItem,
} from "@/entities/fnrs-cnsltn";
import { fnrsCnsltnApplySaveSchema } from "@/entities/fnrs-cnsltn";
import type { PwrkCnsltnCnsltnResponseDto } from "@/entities/pwrk-cnsltn-mng";
import {
mapFnrsSharDetToFormList,
@@ -61,14 +62,14 @@ export function buildSavePayload(
const cklstResultList = data.cklstResultList ?? [];
const esntlFileList = data.esntlFileList ?? [];
return {
return fnrsCnsltnApplySaveSchema.parse({
fnrsCnsltnMng: { ...detDto, prgrsSttsComCd },
bzScaleList: toBzScaleSaveDto(bizSclList),
bizCstList: toBizCstSaveDto(bizCstList),
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])),
};
});
}
export function buildTechReviewReplyPayload(
@@ -80,12 +81,12 @@ export function buildTechReviewReplyPayload(
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
opnrNm?: string,
): FnrsCnsltnApplySaveRequestDto {
return {
return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
techReViewSaveCd,
rvwStpCd: reviewStep,
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
};
});
}
export function buildSupplementReqPayload(
@@ -107,10 +108,10 @@ export function buildSupplementReqPayload(
cmptnYn: "N",
}));
return {
return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...existingItems, ...newItems],
};
});
}
export function buildFileSplmntReqPayload(
@@ -124,10 +125,10 @@ export function buildFileSplmntReqPayload(
cmptnYn: "N",
}));
return {
return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...newItems],
};
});
}
export function buildRvwRsltSplmntReqPayload(
@@ -144,10 +145,10 @@ export function buildRvwRsltSplmntReqPayload(
cmptnYn: "N",
}));
return {
return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...existingItems, ...newItems],
};
});
}
export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
@@ -169,21 +170,21 @@ export function buildSupplementSubmitPayload(
data: FnrsCnsltnMngDetResponseDto,
checklistFormState: ChecklistFormState,
): FnrsCnsltnApplySaveRequestDto {
return {
return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: data.splmntDmndList,
};
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
});
}
export function buildTechReviewPayload(
det: FnrsCnsltnMngResponseDto,
cklstResultList: CklstResultItem[],
): FnrsCnsltnApplySaveRequestDto {
return {
return fnrsCnsltnApplySaveSchema.parse({
fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" },
bzScaleList: [],
bizCstList: [],
cklstResultList,
esntlAtchFileList: [],
};
});
}
@@ -113,7 +113,7 @@ function getBadgeColor(state: BadgeState) {
}
}
function formatOptional(value: string | undefined) {
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -44,10 +44,14 @@ export interface ChecklistFormState {
progress: ChecklistProgress;
}
export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
@@ -9,6 +9,12 @@ import { AxiosError } from "axios";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore } from "@/shared/lib";
import {
AI_AUTO_SE,
type AiAutoPrcsLatestResponseDto,
useAiAutoPrcsLatest,
} from "@/entities/ai-auto-prcs-mng";
import { useReGenerateDraft, useReTransferDocument, useReVerifyCost } from "@/entities/dcrag-to";
import {
buildElaprPrgrsSttsListFromAplnRows,
ElaprApprovalDialog,
@@ -41,6 +47,7 @@ import {
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { isRagIndexingInProgress } from "../../lib/ai-pending-utils";
import {
FNRS_CNSLTN_VIEW_BUTTON_ACTION,
type FnrsCnsltnViewButtonAction,
@@ -63,6 +70,15 @@ import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog";
import { PwrkCnsltnDocMngDialog } from "./pwrk-cnsltn-doc-mng-dialog";
import { PwrkCnsltnRvwRsltInqDialog } from "./pwrk-cnsltn-rvw-rslt-inq-dialog";
const AI_RETRY_PRGRS_STTS = new Set(["ER", "TE", "EV"]);
function findLatestByAutoSe(
list: AiAutoPrcsLatestResponseDto[] | undefined,
atmtnSeComCd: string,
): AiAutoPrcsLatestResponseDto | undefined {
return list?.find((item) => item.atmtnSeComCd === atmtnSeComCd);
}
interface FloatingActionButtonsProps {
data: FnrsCnsltnMngDetResponseDto;
docSeComCd: string;
@@ -117,7 +133,9 @@ function FloatingActionButtons({
const needsPwrkDraftCheck =
dtlInstCd === "ROLE_USER01" && ["SL", "TE", "RK"].includes(prgrsSttsComCd);
const needsPwrkForRb = dtlInstCd === "ROLE_USER05" && prgrsSttsComCd === "RB";
const needsPwrkLookup = needsPwrkDraftCheck || needsPwrkForRb;
const needsPwrkForAiRetry = dtlInstCd === "ROLE_USER01" && ["TE", "EV"].includes(prgrsSttsComCd);
const needsPwrkLookup = needsPwrkDraftCheck || needsPwrkForRb || needsPwrkForAiRetry;
const needsAiAutoLatest = dtlInstCd === "ROLE_USER01" && AI_RETRY_PRGRS_STTS.has(prgrsSttsComCd);
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
useElaprDocPrgrsByDocNo(needsElaprPrgrsCheck ? docNo : null);
@@ -127,8 +145,31 @@ function FloatingActionButtons({
needsPwrkLookup,
);
const { data: aiAutoLatestList, isLoading: isAiAutoLatestLoading } = useAiAutoPrcsLatest(docNo, {
enabled: needsAiAutoLatest,
});
const fileVrfcLatest = findLatestByAutoSe(aiAutoLatestList, AI_AUTO_SE.FILE_VRFC);
const draftGenLatest = findLatestByAutoSe(aiAutoLatestList, AI_AUTO_SE.DRAFT_GEN);
const costVrfcLatest = findLatestByAutoSe(aiAutoLatestList, AI_AUTO_SE.COST_VRFC);
const canRetryFileVrfc = fileVrfcLatest?.isRetryable === true;
const canRetryDraftGen = draftGenLatest?.isRetryable === true;
const canRetryCostVrfc = costVrfcLatest?.isRetryable === true;
const reTransferDocument = useReTransferDocument();
const reGenerateDraft = useReGenerateDraft();
const reVerifyCost = useReVerifyCost();
const isBusy =
isProcessing ||
reTransferDocument.isPending ||
reGenerateDraft.isPending ||
reVerifyCost.isPending;
const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData));
const pwrkCnsltnDocNo = pwrkCnsltnData?.pwrkCnsltnDocNo;
const pwrkCnsltnDocNo =
pwrkCnsltnData?.pwrkCnsltnDocNo ??
draftGenLatest?.pwrkCnsltnDocNo ??
costVrfcLatest?.pwrkCnsltnDocNo;
const elaprPrgrsForPending =
prgrsSttsComCd === "RK"
@@ -139,24 +180,35 @@ function FloatingActionButtons({
userInfo?.userUnqId,
);
const viewOnlyPwrkCnsltnDoc =
prgrsSttsComCd === "SL" || (prgrsSttsComCd === "RB" && dtlInstCd !== "ROLE_USER05");
prgrsSttsComCd === "SL" ||
(prgrsSttsComCd === "RB" && dtlInstCd !== "ROLE_USER05") ||
(prgrsSttsComCd === "RK" && dtlInstCd === "ROLE_USER03");
const actionButtons = useMemo(() => {
if (needsAtrzResolve && isRvwLoginUserLoading) return [];
if (needsElaprPrgrsCheck && isElaprDocPrgrsLoading) return [];
if (needsPwrkDraftCheck && isPwrkCnsltnLoading) return [];
if (needsAiAutoLatest && isAiAutoLatestLoading) return [];
return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, {
hasPendingApprovalRequest: needsElaprPrgrsCheck ? hasPendingApprovalRequest : undefined,
hasCompletedDraft: needsPwrkDraftCheck ? hasCompletedDraft : undefined,
canRetryFileVrfc: needsAiAutoLatest ? canRetryFileVrfc : undefined,
canRetryDraftGen: needsAiAutoLatest ? canRetryDraftGen : undefined,
canRetryCostVrfc: needsAiAutoLatest ? canRetryCostVrfc : undefined,
});
}, [
atrzSeComCdList,
canRetryCostVrfc,
canRetryDraftGen,
canRetryFileVrfc,
dtlInstCd,
hasCompletedDraft,
hasPendingApprovalRequest,
isAiAutoLatestLoading,
isElaprDocPrgrsLoading,
isPwrkCnsltnLoading,
isRvwLoginUserLoading,
needsAiAutoLatest,
needsAtrzResolve,
needsElaprPrgrsCheck,
needsPwrkDraftCheck,
@@ -369,19 +421,6 @@ function FloatingActionButtons({
}
async function handleRqstTechRvwClick() {
if (prgrsSttsComCd === "LE") {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"ENV",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 기술 검토 의뢰가 가능합니다.");
return;
}
}
const payload: FnrsExamineRequestDto = {
prgrsSttsComCd: "FV",
};
@@ -543,17 +582,60 @@ function FloatingActionButtons({
});
}
function handleFileVrfcClick() {
reTransferDocument.mutate(
{ fnrsCnsltnDocNo: docNo },
{ onSuccess: () => alert("파일 검증을 요청했습니다.") },
);
}
function handlePwrkCnsltnDocCrtClick() {
if (!pwrkCnsltnDocNo) {
alert("실무협의 문서번호를 찾을 수 없습니다.");
return;
}
reGenerateDraft.mutate(
{
fnrsCnsltnDocNo: docNo,
pwrkCnsltnDocNo,
crtDmndUuid: draftGenLatest?.crtDmndUuid,
},
{ onSuccess: () => alert("초안 생성을 요청했습니다.") },
);
}
function handleCostVrfcClick() {
if (!pwrkCnsltnDocNo) {
alert("실무협의 문서번호를 찾을 수 없습니다.");
return;
}
reVerifyCost.mutate(
{
fnrsCnsltnDocNo: docNo,
pwrkCnsltnDocNo,
},
{ onSuccess: () => alert("금액 검증을 요청했습니다.") },
);
}
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 color="white" type="button" onClick={handleGoToList} disabled={isBusy}>
목록
</Button>
{!isAiPending && (
<>
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) && (
<Button color="orange" onClick={() => {}} disabled={isProcessing}>
<Button
color="orange"
onClick={handlePwrkCnsltnDocCrtClick}
disabled={isBusy}
title={draftGenLatest?.prcsErrCn ?? undefined}
>
실무협의서 초안 생성
</Button>
)}
@@ -561,103 +643,120 @@ function FloatingActionButtons({
<Button
color="orange"
onClick={() => setActiveDialog("pwrkCnsltnDocMng")}
disabled={isProcessing}
disabled={isBusy}
>
실무협의서 관리
</Button>
)}
{canEdit && (
<Button color="gray" onClick={handleEdit} disabled={isProcessing}>
<Button color="gray" onClick={handleEdit} disabled={isBusy}>
수정
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg) && (
<Button onClick={handleStrgClick} disabled={isProcessing}>
<Button onClick={handleStrgClick} disabled={isBusy}>
저장
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply) && (
<Button color="green" onClick={handleAplyClick} disabled={isProcessing}>
<Button color="green" onClick={handleAplyClick} disabled={isBusy}>
신청 완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.del) && (
<Button color="red" onClick={handleDelClick} disabled={isProcessing}>
<Button color="red" onClick={handleDelClick} disabled={isBusy}>
삭제
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && (
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
<Button onClick={handleEnvoSbmsnClick} disabled={isBusy}>
환경청 제출
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && (
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
<Button color="red" onClick={handleRjctClick} disabled={isBusy}>
반려
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv) && (
<Button color="green" onClick={handleSncnAprvClick} disabled={isProcessing}>
<Button color="green" onClick={handleSncnAprvClick} disabled={isBusy}>
결과 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply) && (
<Button onClick={handleRvwOpnnRplyClick} disabled={isProcessing}>
<Button onClick={handleRvwOpnnRplyClick} disabled={isBusy}>
검토 의견 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && (
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
<Button color="green" onClick={handleRvwRplyClick} disabled={isBusy}>
검토 결과 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
<Button onClick={handleRqstTechRvwClick} disabled={isProcessing}>
<Button onClick={handleRqstTechRvwClick} disabled={isBusy}>
기술 검토 의뢰
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw) && (
<Button onClick={handleRqstActnRvwClick} disabled={isProcessing}>
<Button onClick={handleRqstActnRvwClick} disabled={isBusy}>
조치 결과 검토 의뢰
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt) && (
<Button color="gray" onClick={handleFileSplmntClick} disabled={isProcessing}>
<Button color="gray" onClick={handleFileSplmntClick} disabled={isBusy}>
파일 보완 요청
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && (
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc) && (
<Button
onClick={() =>
setActiveDialog(prgrsSttsComCd === "EV" ? "elaprDocPrgrs" : "elaprTmplt")
}
disabled={isProcessing}
color="gray"
onClick={handleFileVrfcClick}
disabled={isBusy}
title={fileVrfcLatest?.prcsErrCn ?? undefined}
>
검토자 지정
파일 검증
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.costVrfc) && (
<Button
color="gray"
onClick={handleCostVrfcClick}
disabled={isBusy}
title={costVrfcLatest?.prcsErrCn ?? undefined}
>
금액 검증
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) &&
!isRagIndexingInProgress(prgrsSttsComCd, data.ragIndexingInfo) && (
<Button
onClick={() =>
setActiveDialog(prgrsSttsComCd === "EV" ? "elaprDocPrgrs" : "elaprTmplt")
}
disabled={isBusy}
>
검토자 지정
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn) && (
<Button color="green" onClick={handleActnCmptnClick} disabled={isProcessing}>
<Button color="green" onClick={handleActnCmptnClick} disabled={isBusy}>
조치 완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && (
<Button
color="sky"
onClick={() => setActiveDialog("rvwRsltInq")}
disabled={isProcessing}
>
<Button color="sky" onClick={() => setActiveDialog("rvwRsltInq")} disabled={isBusy}>
검토 결과 조회
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && (
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
<Button onClick={handleRvwStrgClick} disabled={isBusy}>
저장
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn) && (
<Button color="green" onClick={handleRvwCmptnClick} disabled={isProcessing}>
<Button color="green" onClick={handleRvwCmptnClick} disabled={isBusy}>
검토 완료
</Button>
)}
@@ -665,18 +764,18 @@ function FloatingActionButtons({
<Button
color="gray"
onClick={() => setActiveDialog("dmndSplmnt")}
disabled={isProcessing}
disabled={isBusy}
>
보완 요청
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isBusy}>
결재(승인)
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isBusy}>
결재(회신)
</Button>
)}
@@ -705,14 +804,14 @@ function FloatingActionButtons({
splmntArtclUpCd="SPLMNTARTCL"
open={activeDialog === "dmndSplmnt"}
onOpenChange={() => setActiveDialog(null)}
isSubmitting={isProcessing}
isSubmitting={isBusy}
onConfirm={handleDmndSplmntConfirm}
/>
<FileVrfcSplmntDialog
open={activeDialog === "fileSplmnt"}
fnrsCnsltnDocNo={docNo}
isSubmitting={isProcessing}
isSubmitting={isBusy}
onClose={() => setActiveDialog(null)}
onConfirm={handleFileSplmntConfirm}
/>
@@ -746,7 +845,7 @@ function FloatingActionButtons({
<PwrkCnsltnRvwRsltInqDialog
open={activeDialog === "rvwRsltInq"}
fnrsCnsltnDocNo={docNo}
isSubmitting={isProcessing}
isSubmitting={isBusy}
onClose={() => setActiveDialog(null)}
onConfirm={handleRvwRsltSplmntConfirm}
/>
@@ -881,6 +980,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
rvwOpnnRply: handleTechReview,
rqstTechRvw: handleRequestExamine,
fileSplmnt: handleDmndSplmnt,
fileVrfc: () => Promise.resolve(),
dsgntRvwr: handleDsgntRvwr,
pwrkCnsltnDocCrt: () => Promise.resolve(),
pwrkCnsltnDocMng: () => Promise.resolve(),
@@ -892,6 +992,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
atrzRply: () => Promise.resolve(),
actnCmptn: handleActnCmptn,
rqstActnRvw: handleRequestActnReview,
costVrfc: () => Promise.resolve(),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -0,0 +1,137 @@
"use client";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAlertStore } from "@/shared/lib";
import { useSavePwrkCnsltnDocEvl } from "@/entities/pwrk-cnsltn-doc-evl";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
} from "@common/components";
import {
buildPwrkCnsltnDocEvlSavePayload,
PWRK_CNSLTN_DOC_EVL_SCR_OPTIONS,
pwrkCnsltnDocEvlFormSchema,
type PwrkCnsltnDocEvlFormValues,
} from "../../lib/pwrk-cnsltn-doc-evl-schema";
interface PwrkCnsltnDocEvlDialogProps {
open: boolean;
pwrkCnsltnDocNo: string;
onClose: () => void;
}
const DEFAULT_VALUES: Partial<PwrkCnsltnDocEvlFormValues> = {
pwrkCnsltnDocEvlScr: undefined,
pwrkCnsltnDocEvlCn: "",
};
function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDocEvlDialogProps) {
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: saveEvl, isPending } = useSavePwrkCnsltnDocEvl();
const { handleSubmit, reset, setValue, watch, register } = useForm<PwrkCnsltnDocEvlFormValues>({
resolver: zodResolver(pwrkCnsltnDocEvlFormSchema),
defaultValues: DEFAULT_VALUES,
});
const pwrkCnsltnDocEvlScr = watch("pwrkCnsltnDocEvlScr");
useEffect(() => {
if (!open) return;
reset(DEFAULT_VALUES);
}, [open, reset]);
function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) onClose();
}
async function onSubmit(values: PwrkCnsltnDocEvlFormValues) {
try {
const payload = buildPwrkCnsltnDocEvlSavePayload(pwrkCnsltnDocNo, values);
await saveEvl(payload);
alert("AI 초안 만족도 평가를 등록했습니다.");
onClose();
} catch (error) {
console.error("AI 초안 만족도 평가 등록 실패:", error);
alert("AI 초안 만족도 평가 등록에 실패했습니다.");
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-[40rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>AI 초안 만족도 평가</DialogTitle>
</DialogHeader>
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-2">
<Label>만족도</Label>
<Select
value={pwrkCnsltnDocEvlScr || undefined}
onValueChange={(value) =>
setValue(
"pwrkCnsltnDocEvlScr",
value as PwrkCnsltnDocEvlFormValues["pwrkCnsltnDocEvlScr"],
{
shouldValidate: true,
},
)
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="만족도를 선택해 주세요" />
</SelectTrigger>
<SelectContent>
{PWRK_CNSLTN_DOC_EVL_SCR_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="pwrkCnsltnDocEvlCn">의견</Label>
<Textarea
id="pwrkCnsltnDocEvlCn"
rows={4}
placeholder="의견을 입력해 주세요"
className="!text-xl"
{...register("pwrkCnsltnDocEvlCn")}
/>
</div>
<DialogFooter>
<Button type="button" color="white" onClick={onClose} disabled={isPending}>
닫기
</Button>
<Button type="submit" disabled={isPending}>
제출
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
export { PwrkCnsltnDocEvlDialog };
export type { PwrkCnsltnDocEvlDialogProps };
@@ -33,6 +33,7 @@ import {
Skeleton,
} from "@common/components";
import { PwrkCnsltnDocEvlDialog } from "./pwrk-cnsltn-doc-evl-dialog";
import { PwrkCnsltnDocPjtcoAjmtTable } from "./pwrk-cnsltn-doc-pjtco-ajmt-table";
const HangulGiangi = dynamic(
@@ -45,6 +46,8 @@ const HangulGiangi = dynamic(
const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
const AI_DRAFT_EVL_MAX_VER_SN = 2;
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["TP", "RA", "AV", "EV", "RR", "EP", "TA"]);
@@ -82,6 +85,7 @@ function PwrkCnsltnDocMngDialog({
const [atchFileGroupId, setAtchFileGroupId] = useState("");
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const {
@@ -138,6 +142,7 @@ function PwrkCnsltnDocMngDialog({
if (!open) {
setAtchFileGroupId("");
setUploadedFiles([]);
setIsEvlDialogOpen(false);
return;
}
@@ -194,6 +199,10 @@ function PwrkCnsltnDocMngDialog({
}),
]);
alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`);
if (savedDocument.verSn <= AI_DRAFT_EVL_MAX_VER_SN) {
setIsEvlDialogOpen(true);
}
}
async function handleDownload() {
@@ -269,105 +278,115 @@ function PwrkCnsltnDocMngDialog({
const canSave = !viewOnly && (canSaveProp ?? canSaveByAtrz);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
className="max-h-[96vh] max-w-[80vw] overflow-y-auto"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>실무협의서 관리</DialogTitle>
</DialogHeader>
<>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
className="max-h-[96vh] max-w-[80vw] overflow-y-auto"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>실무협의서 관리</DialogTitle>
</DialogHeader>
{isLoading ? <Skeleton className="h-[70vh] min-h-[40rem] w-full" /> : null}
{isError ? (
<p className="py-16 text-center text-lg text-muted-foreground">
실무협의서 정보를 불러오지 못했습니다.
</p>
) : null}
{!isLoading && !isError && !completedDraft ? (
<p className="py-16 text-center text-lg text-muted-foreground">
생성 완료된 실무협의서 초안이 없습니다.
</p>
) : null}
{completedDraft && documentBlob ? (
<div className="flex min-h-[40rem] items-stretch gap-2">
<div className="min-w-0 flex-1">
<HangulGiangi
documentBlob={documentBlob}
documentKey={completedDraft.crtDmndUuid}
documentFileName={documentFileName}
canSave={canSave}
onSaveDocument={handleSaveDocument}
onDownload={handleDownload}
/>
{isLoading ? <Skeleton className="h-[70vh] min-h-[40rem] w-full" /> : null}
{isError ? (
<p className="py-16 text-center text-lg text-muted-foreground">
실무협의서 정보를 불러오지 못했습니다.
</p>
) : null}
{!isLoading && !isError && !completedDraft ? (
<p className="py-16 text-center text-lg text-muted-foreground">
생성 완료된 실무협의서 초안이 없습니다.
</p>
) : null}
{completedDraft && documentBlob ? (
<div className="flex min-h-[40rem] items-stretch gap-2">
<div className="min-w-0 flex-1">
<HangulGiangi
documentBlob={documentBlob}
documentKey={completedDraft.crtDmndUuid}
documentFileName={documentFileName}
canSave={canSave}
onSaveDocument={handleSaveDocument}
onDownload={handleDownload}
/>
</div>
{pjtcoAjmtPage?.content?.length ? (
isPjtcoAjmtTableOpen ? (
<section className="flex h-[calc(64vh+4rem)] w-0 flex-[0.4] flex-col overflow-hidden rounded-md border border-border">
<header className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
<h3 className="text-xl font-semibold">금액검증결과</h3>
<button
type="button"
className="rounded-sm border border-border p-2 hover:opacity-90"
onClick={() => setIsPjtcoAjmtTableOpen(false)}
aria-label="접기"
>
<PanelRightClose className="size-6" />
</button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto">
<PwrkCnsltnDocPjtcoAjmtTable
rows={pjtcoAjmtPage?.content ?? []}
isLoading={isPjtcoAjmtLoading}
isError={isPjtcoAjmtError}
/>
</div>
</section>
) : (
<button
type="button"
className="flex items-center gap-1 self-start rounded-sm border border-border px-1 py-2 hover:opacity-90"
style={{ writingMode: "vertical-rl" }}
onClick={() => setIsPjtcoAjmtTableOpen(true)}
aria-label="펼치기"
>
<PanelRightOpen className="size-5" />
<span className="text-xl tracking-wide">금액검증결과</span>
</button>
)
) : null}
</div>
{pjtcoAjmtPage?.content?.length ? (
isPjtcoAjmtTableOpen ? (
<section className="flex h-[calc(64vh+4rem)] w-0 flex-[0.4] flex-col overflow-hidden rounded-md border border-border">
<header className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
<h3 className="text-xl font-semibold">금액검증결과</h3>
<button
type="button"
className="rounded-sm border border-border p-2 hover:opacity-90"
onClick={() => setIsPjtcoAjmtTableOpen(false)}
aria-label="접기"
>
<PanelRightClose className="size-6" />
</button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto">
<PwrkCnsltnDocPjtcoAjmtTable
rows={pjtcoAjmtPage?.content ?? []}
isLoading={isPjtcoAjmtLoading}
isError={isPjtcoAjmtError}
/>
</div>
</section>
) : (
<button
type="button"
className="flex items-center gap-1 self-start rounded-sm border border-border px-1 py-2 hover:opacity-90"
style={{ writingMode: "vertical-rl" }}
onClick={() => setIsPjtcoAjmtTableOpen(true)}
aria-label="펼치기"
>
<PanelRightOpen className="size-5" />
<span className="text-xl tracking-wide">금액검증결과</span>
</button>
)
) : null}
</div>
) : null}
) : null}
{completedDraft ? (
<section className="space-y-2 rounded-md border border-border p-4">
<h3 className="text-xl font-semibold">첨부파일</h3>
{uploadedFiles.length === 0 ? (
<p className="text-lg text-muted-foreground">등록된 첨부파일이 없습니다.</p>
) : null}
<AttachmentFileField
files={uploadedFiles.map((item) => ({
fileKey: item.fileKey,
fileName: item.fileName,
onDownload: item.atflId ? () => downloadByAtflId(item.atflId!) : undefined,
}))}
inputId="pwrk-cnsltn-doc-atch-file-input"
isUploadingByKey={isUploading}
addDisabled={!canSave || isAnyAtchUploading}
showAddButton={canSave}
onAdd={(files) => void handleAtchFileUpload(files)}
onRemove={canSave ? (index) => void handleAtchFileRemove(index) : undefined}
/>
</section>
) : null}
{completedDraft ? (
<section className="space-y-2 rounded-md border border-border p-4">
<h3 className="text-xl font-semibold">첨부파일</h3>
{uploadedFiles.length === 0 ? (
<p className="text-lg text-muted-foreground">등록된 첨부파일이 없습니다.</p>
) : null}
<AttachmentFileField
files={uploadedFiles.map((item) => ({
fileKey: item.fileKey,
fileName: item.fileName,
onDownload: item.atflId ? () => downloadByAtflId(item.atflId!) : undefined,
}))}
inputId="pwrk-cnsltn-doc-atch-file-input"
isUploadingByKey={isUploading}
addDisabled={!canSave || isAnyAtchUploading}
showAddButton={canSave}
onAdd={(files) => void handleAtchFileUpload(files)}
onRemove={canSave ? (index) => void handleAtchFileRemove(index) : undefined}
/>
</section>
) : null}
<DialogFooter>
<Button type="button" color="white" onClick={onClose}>
닫기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DialogFooter>
<Button type="button" color="white" onClick={onClose}>
닫기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{completedDraft ? (
<PwrkCnsltnDocEvlDialog
open={isEvlDialogOpen}
pwrkCnsltnDocNo={completedDraft.pwrkCnsltnDocNo}
onClose={() => setIsEvlDialogOpen(false)}
/>
) : null}
</>
);
}
@@ -9,16 +9,16 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
type FnrsCnsltnApplySaveRequestDto,
fnrsCnsltnKeys,
type FnrsCnsltnMngDetResponseDto,
fnrsCnsltnMngService,
} from "@/entities/fnrs-cnsltn";
import { useFnrsSharSeMngTree } from "@/entities/fnrs-shar-se";
import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng";
import {
AttachmentsFormSection,
buildAcntgCostItems,
@@ -32,19 +32,19 @@ import {
isFormEsntlAtchComplete,
PrivacyConsentSection,
ProgressStatusSection,
recalculateFnrsSharTotals,
SplmntDmndFormSection,
toFnrsSharSaveList,
validateBizCstSharBalance,
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import {
type FnrsCnsltnRegistFormData,
fnrsCnsltnRegistSchema,
formatTaskSeComCdForApi,
} from "../lib/fnrs-cnsltn-regist-schema";
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
import {
buildFormPayload,
getCreateDefaultValues,
mapDetailToFormData,
} from "../lib/form-data-mapper";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { PwrkCnsltnDocMngDialog } from "./components/pwrk-cnsltn-doc-mng-dialog";
@@ -72,6 +72,12 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
const fnrsCnsltnDocNo = initialData?.detDto?.fnrsCnsltnDocNo;
const { data: pwrkCnsltnData } = usePwrkCnsltnMngByUpDocNo(
fnrsCnsltnDocNo,
isSplmntPrgrsStts && Boolean(fnrsCnsltnDocNo),
);
const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData));
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree();
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "FNRS" });
@@ -89,54 +95,25 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
: getCreateDefaultValues(userInfo, createDefaults),
});
function buildFormPayload(
data: FnrsCnsltnRegistFormData,
prgrsSttsComCd: string,
): FnrsCnsltnApplySaveRequestDto | undefined {
const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, sharItems);
const validationMessage = validateBizCstSharBalance(
data.bizCstList,
withTotals,
function createFormPayload(data: FnrsCnsltnRegistFormData, prgrsSttsComCd: string) {
const result = buildFormPayload(data, prgrsSttsComCd, {
isEditMode,
docNo: initialData?.detDto?.fnrsCnsltnDocNo,
sharItems,
costItems,
);
if (validationMessage) {
alert(validationMessage);
});
if ("validationMessage" in result) {
alert(result.validationMessage);
return undefined;
}
const { taskSeComCd: formTaskSeComCd, ...fnrsCnsltnMngRest } = data.fnrsCnsltnMng;
return {
fnrsCnsltnMng: {
...fnrsCnsltnMngRest,
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
fnrsCnsltnDocNo: isEditMode ? initialData?.detDto?.fnrsCnsltnDocNo : undefined,
prgrsSttsComCd,
},
bzScaleList: data.bzScaleList,
bizCstList: data.bizCstList,
fnrsSharList: toFnrsSharSaveList(withTotals),
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 !== "SV" && prgrsSttsComCd !== "ST" && { cmptnYn: "Y" }),
}),
),
};
return result;
}
async function submitForm(
data: FnrsCnsltnRegistFormData,
prgrsSttsComCd: "SV" | "ST",
): Promise<string | undefined> {
const payload = buildFormPayload(data, prgrsSttsComCd);
const payload = createFormPayload(data, prgrsSttsComCd);
if (!payload) return undefined;
return fnrsCnsltnMngService.saveApply(payload);
}
@@ -146,7 +123,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
if (!beforePrgrsSttsComCd) return undefined;
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
const payload = buildFormPayload(data, beforePrgrsSttsComCd);
const payload = createFormPayload(data, beforePrgrsSttsComCd);
if (!docNo || !payload) return undefined;
return fnrsCnsltnMngService.submitSupplement(docNo, payload);
@@ -159,7 +136,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
(item) => !item.splmntCn || !item.splmntCn.trim(),
);
if (hasEmptySplmntCn) {
alert("보완요청내용이 있습니다. 보완 완료내용을 입력해 주세요.");
alert("보완 요청 내용이 있습니다.\n보완 완료 내용을 입력해 주세요.");
return;
}
@@ -207,6 +184,11 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
async function onApply(data: FnrsCnsltnRegistFormData) {
if (isSubmitting) return;
if (data.privacyConsent !== "Y") {
alert("개인정보 수집/이용에 동의해야 신청이 가능합니다.");
return;
}
if (!isFormChecklistComplete(data.cklstResultList)) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
@@ -293,7 +275,10 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
/>
<SplmntDmndFormSection readOnly={!isSplmntPrgrsStts} />
<SplmntDmndFormSection
initialData={initialData?.splmntDmndList}
readOnly={!isSplmntPrgrsStts}
/>
<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">
@@ -333,7 +318,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
</>
) : (
<>
{fnrsCnsltnDocNo ? (
{hasCompletedDraft ? (
<Button
color="orange"
type="button"
@@ -357,7 +342,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
</div>
</SectionCollapsibleGroup>
</form>
{fnrsCnsltnDocNo ? (
{hasCompletedDraft && fnrsCnsltnDocNo ? (
<PwrkCnsltnDocMngDialog
open={isPwrkCnsltnDocMngOpen}
fnrsCnsltnDocNo={fnrsCnsltnDocNo}
@@ -365,6 +350,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
onClose={() => setIsPwrkCnsltnDocMngOpen(false)}
/>
) : null}
<RhfDevTool control={form.control} />
</FormProvider>
);
}
@@ -14,7 +14,11 @@ import {
} from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { getAiPendingMessage, isAiPendingStatus } from "../lib/ai-pending-utils";
import {
getAiPendingMessage,
isAiPendingStatus,
isRagIndexingInProgress,
} from "../lib/ai-pending-utils";
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";
@@ -95,20 +99,25 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
detDto.prgrsSttsComCd || "",
);
const isAiPending = isAiPendingStatus(detDto.prgrsSttsComCd || "");
const isRagIndexing = isRagIndexingInProgress(detDto.prgrsSttsComCd || "", data.ragIndexingInfo);
const showPendingBanner = isAiPending || isRagIndexing;
const pendingMessage = isRagIndexing
? (data.ragIndexingInfo?.message ?? "")
: getAiPendingMessage(detDto.prgrsSttsComCd || "");
return (
<div className="mt-4">
<SectionCollapsibleGroup>
<div className="sticky top-[6rem] z-30 flex items-center gap-4 bg-container px-4 py-2">
<SectionCollapsibleToggle />
{isAiPending && (
{showPendingBanner && (
<div className="flex items-center gap-2">
<span className="relative flex size-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex size-3 rounded-full bg-primary"></span>
</span>
<TextShimmer active className="text-xl">
{getAiPendingMessage(detDto.prgrsSttsComCd || "")}
{pendingMessage}
</TextShimmer>
</div>
)}
@@ -11,11 +11,11 @@ import {
extractTaskCodes,
getChecklistDisplayRows,
} from "../../lib/checklist-utils";
import { getVisibleReviewSteps } from "../../lib/review-step-utils";
import { resolveVisibleReviewSteps } from "../../lib/review-step-utils";
import type { ReviewStepCode } from "../../model/view-types";
import {
CHECKLIST_TAB_LABELS,
type ChecklistFormState,
REVIEW_STEP_LABELS,
ROLE_TO_REVIEW_STEP,
TASK_CODE_LABELS,
} from "../../model/view-types";
@@ -37,8 +37,8 @@ function ChecklistSection({
setFormState,
}: ChecklistSectionProps) {
const visibleReviewSteps = useMemo(
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
[prgrsSttsComCd, userRoleCode],
() => resolveVisibleReviewSteps(prgrsSttsComCd, userRoleCode, cklstResultList),
[prgrsSttsComCd, userRoleCode, cklstResultList],
);
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
@@ -87,7 +87,7 @@ function ChecklistSection({
<TabsList>
{visibleReviewSteps.map((stepCode) => (
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
{REVIEW_STEP_LABELS[stepCode]}
{CHECKLIST_TAB_LABELS[stepCode]}
</TabsTrigger>
))}
</TabsList>
@@ -1,9 +1,13 @@
import type { IUserInfo } from "@/shared/store/user-store";
import type { InstlSncnMngDetResponse } from "@/entities/instl-sncn";
import {
type InstlSncnApplySaveRequestDto,
instlSncnApplySaveSchema,
type InstlSncnMngDetResponse,
} from "@/entities/instl-sncn";
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import type { InstlSncnRegistFormData } from "./instl-sncn-regist-schema";
import { parseTaskSeComCdFromApi } from "./instl-sncn-regist-schema";
import { formatTaskSeComCdForApi, parseTaskSeComCdFromApi } from "./instl-sncn-regist-schema";
function mapSelectedValue(value?: string | null): "Y" | "N" | null {
if (value === "Y" || value === "N") return value;
@@ -16,12 +20,12 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg
return {
instlSncnMng: {
instlSncnDocNo: detDto.instlSncnDocNo,
bizNm: detDto.bizNm,
bizBgngYr: detDto.bizBgngYr,
bizEndYr: detDto.bizEndYr,
bizCn: detDto.bizCn,
bizNm: detDto.bizNm ?? "",
bizBgngYr: detDto.bizBgngYr ?? "",
bizEndYr: detDto.bizEndYr ?? "",
bizCn: detDto.bizCn ?? "",
taskSeComCd: parseTaskSeComCdFromApi(detDto.taskSeComCd),
sncnSeComCd: detDto.sncnSeComCd,
sncnSeComCd: detDto.sncnSeComCd ?? "",
dsgnrNm: detDto.dsgnrNm ?? "",
dsgnrTelno: detDto.dsgnrTelno ?? "",
dsgnrOgdpNm: detDto.dsgnrOgdpNm ?? "",
@@ -38,16 +42,8 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg
atchFileGroupId: detDto.atchFileGroupId ?? "",
rtlztnFcltSeComCd: detDto.rtlztnFcltSeComCd ?? "",
rtlztnUsgSeComCd: detDto.rtlztnUsgSeComCd ?? "",
envoSeCdNm: detDto.envoSeCdNm,
sncnSeCdNm: detDto.sncnSeCdNm,
taskSeCdNm: detDto.taskSeCdNm,
prgrsSttsCdNm: detDto.prgrsSttsCdNm,
sido: detDto.sido,
gugun: detDto.gugun,
regDt: detDto.regDt,
rgtr: detDto.rgtr,
rgtrNm: detDto.rgtrNm,
mdfcnDt: detDto.mdfcnDt,
},
bzScaleList: bizSclList.map((item) => ({
cnstpSeCd: item.cnstpSeCd,
@@ -85,8 +81,6 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg
splmntPicNm: item.splmntPicNm ?? "",
splmntCmptnDt: item.splmntCmptnDt ?? "",
splmntAtchFileGroupId: item.splmntAtchFileGroupId ?? "",
splmntAtchFileNm: item.splmntAtchFileInfo?.orgnlFileNm ?? "",
splmntAtchFileAtflId: item.splmntAtchFileInfo?.atflId ?? "",
cmptnYn: item.cmptnYn,
})) ?? [],
privacyConsent: "Y",
@@ -132,3 +126,29 @@ export function getCreateDefaultValues(
privacyConsent: "N",
};
}
export function buildFormPayload(
data: InstlSncnRegistFormData,
prgrsSttsComCd: "SV" | "ST" | "TE",
options: { isEditMode: boolean; docNo?: string },
): InstlSncnApplySaveRequestDto {
return instlSncnApplySaveSchema.parse({
instlSncnMng: {
...data.instlSncnMng,
instlSncnDocNo: options.isEditMode ? options.docNo : undefined,
taskSeComCd: formatTaskSeComCdForApi(data.instlSncnMng.taskSeComCd),
prgrsSttsComCd,
},
bzScaleList: data.bzScaleList,
bizCstList: data.bizCstList,
cklstResultList: data.cklstResultList.map((item) => ({
...item,
selected: item.selected ?? undefined,
})),
esntlAtchFileList: data.esntlAtchFileList,
splmntDmndList: data.splmntDmndList.map((item) => ({
...item,
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
})),
});
}
@@ -1,7 +1,16 @@
import { z } from "zod";
import {
instlSncnBizCstItemSchema,
instlSncnBzScaleItemSchema,
instlSncnCklstResultItemSchema,
instlSncnEsntlAtchFileItemSchema,
instlSncnMngSaveSchema,
instlSncnSplmntDmndItemSchema,
} from "@/entities/instl-sncn";
export const instlSncnRegistSchema = z.object({
instlSncnMng: z.object({
instlSncnMng: instlSncnMngSaveSchema.extend({
bizNm: z.string().min(1, "사업명은 필수입니다"),
bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"),
bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"),
@@ -18,32 +27,13 @@ export const instlSncnRegistSchema = z.object({
.string()
.min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"),
bgtBizCd: z.string().optional(),
bgtAplyYr: z.string().optional(),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
instlSncnDocNo: z.string().nullish(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
prgrsSttsComCd: z.string().nullish(),
atchFileGroupId: z.string().optional(),
rtlztnFcltSeComCd: z.string().optional(),
rtlztnUsgSeComCd: z.string().optional(),
envoSeCdNm: z.string().nullish(),
sncnSeCdNm: z.string().nullish(),
taskSeCdNm: z.string().nullish(),
prgrsSttsCdNm: z.string().nullish(),
sido: z.string().nullish(),
gugun: z.string().nullish(),
regDt: z.string().nullish(),
rgtr: z.string().nullish(),
rgtrNm: z.string().nullish(),
mdfcnDt: z.string().nullish(),
}),
bzScaleList: z.array(
z.object({
instlSncnBzScaleItemSchema.extend({
cnstpSeCd: z.string(),
bscPl: z.number(),
budgetReflct: z.number(),
@@ -52,7 +42,7 @@ export const instlSncnRegistSchema = z.object({
}),
),
bizCstList: z.array(
z.object({
instlSncnBizCstItemSchema.extend({
acntgSeCd: z.string(),
bscPlanAmt: z.number(),
bgtRfltAmt: z.number(),
@@ -60,37 +50,25 @@ export const instlSncnRegistSchema = z.object({
}),
),
cklstResultList: z.array(
z.object({
instlSncnCklstResultItemSchema.extend({
cklstSn: z.number(),
cklstArtclSn: z.number(),
cklstRvwSn: z.number(),
rvwStpComCd: z.string(),
selected: z.enum(["Y", "N"]).nullable(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
}),
),
esntlAtchFileList: z.array(
z.object({
instlSncnEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(),
atchFileGroupId: z.string(),
}),
),
splmntDmndList: z.array(
z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
instlSncnSplmntDmndItemSchema.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(),
splmntDmndAtchFileNm: z.string().nullish(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
@@ -104,7 +82,7 @@ export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
return taskSeComCd.join(",");
}
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] {
if (!taskSeComCd) return [];
return taskSeComCd.split(",").filter(Boolean);
}
@@ -1,4 +1,4 @@
import type { ReviewStepCode } from "@/entities/instl-sncn";
import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn";
import {
INSTL_PROGRESS_ORDER,
@@ -27,6 +27,25 @@ export function getVisibleReviewSteps(
return REVIEW_STEP_CODES.slice(0, count);
}
export function resolveVisibleReviewSteps(
prgrsSttsComCd: string,
userRoleCode: string | undefined,
cklstResultList: CklstRvwRsltTreeDto[],
): ReviewStepCode[] {
const byProgressAndRole = getVisibleReviewSteps(prgrsSttsComCd, userRoleCode);
const currentStep = byProgressAndRole.at(-1);
const stepsWithData = new Set<ReviewStepCode>();
for (const row of cklstResultList) {
const step = row.rvwStpComCd as ReviewStepCode | undefined;
if (step && REVIEW_STEP_CODES.includes(step)) stepsWithData.add(step);
}
return REVIEW_STEP_CODES.filter(
(step) => stepsWithData.has(step) || step === currentStep,
);
}
export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
@@ -8,7 +8,11 @@ import type {
InstlSncnSplmntDmndSaveDto,
ReviewStepCode,
} from "@/entities/instl-sncn";
import { type SplmntDmndDialogConfirmPayload, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import { instlSncnApplySaveSchema } from "@/entities/instl-sncn";
import {
type SplmntDmndDialogConfirmPayload,
toEsntlAtchFileSaveList,
} from "@/widgets/pbadms-sprt";
import type { ChecklistFormState } from "../model/view-types";
@@ -23,7 +27,6 @@ export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): InstlSncnBzScaleS
bscPl: node.bscPlanQty,
budgetReflct: node.bgtRfltQty,
thtiDsgn: node.tstmDesignQty,
diff: node.tstmDesignQty - node.bgtRfltQty,
}));
}
@@ -46,13 +49,13 @@ export function buildSavePayload(
): InstlSncnApplySaveRequestDto {
const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data;
return {
return instlSncnApplySaveSchema.parse({
instlSncnMng: { ...detDto, prgrsSttsComCd },
bzScaleList: toBzScaleSaveDto(bizSclList),
bizCstList: toBizCstSaveDto(bizCstList),
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
};
});
}
export function buildTechReviewReplyPayload(
@@ -64,12 +67,12 @@ export function buildTechReviewReplyPayload(
reviewOpinionFormData?: ReviewOpinionFormData | null,
opnrNm?: string,
): InstlSncnApplySaveRequestDto {
return {
return instlSncnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
techReViewSaveCd,
rvwStpCd: reviewStep,
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
};
});
}
export function buildSupplementReqPayload(
@@ -91,10 +94,10 @@ export function buildSupplementReqPayload(
cmptnYn: "N",
}));
return {
return instlSncnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...existingItems, ...newItems],
};
});
}
export function toSplmntDmndSaveItem(item: InstlSncnSplmntDmndSaveDto): InstlSncnSplmntDmndSaveDto {
@@ -114,7 +114,7 @@ function getBadgeColor(state: BadgeState) {
}
}
function formatOptional(value: string | undefined) {
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -48,6 +48,12 @@ export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
@@ -9,10 +9,9 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
type InstlSncnApplySaveRequestDto,
instlSncnKeys,
type InstlSncnMngDetResponse,
instlSncnService,
@@ -31,12 +30,8 @@ import {
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
import {
formatTaskSeComCdForApi,
type InstlSncnRegistFormData,
instlSncnRegistSchema,
} from "../lib/instl-sncn-regist-schema";
import { buildFormPayload, getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
import { type InstlSncnRegistFormData, instlSncnRegistSchema } from "../lib/instl-sncn-regist-schema";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
@@ -72,47 +67,24 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
: getCreateDefaultValues(userInfo, createDefaults),
});
function buildFormPayload(
data: InstlSncnRegistFormData,
prgrsSttsComCd: "SV" | "ST" | "TE",
): InstlSncnApplySaveRequestDto {
const { taskSeComCd: formTaskSeComCd, ...instlSncnMngRest } = data.instlSncnMng;
return {
instlSncnMng: {
...instlSncnMngRest,
instlSncnDocNo: isEditMode ? initialData?.detDto.instlSncnDocNo : undefined,
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
prgrsSttsComCd,
},
bzScaleList: data.bzScaleList,
bizCstList: data.bizCstList,
cklstResultList: data.cklstResultList.map((item) => ({
...item,
selected: item.selected,
})),
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),
}),
),
};
function createFormPayload(data: InstlSncnRegistFormData, prgrsSttsComCd: "SV" | "ST" | "TE") {
return buildFormPayload(data, prgrsSttsComCd, {
isEditMode,
docNo: initialData?.detDto.instlSncnDocNo,
});
}
async function submitForm(
data: InstlSncnRegistFormData,
prgrsSttsComCd: "SV" | "ST",
): Promise<string | undefined> {
return instlSncnService.saveApply(buildFormPayload(data, prgrsSttsComCd));
return instlSncnService.saveApply(createFormPayload(data, prgrsSttsComCd));
}
async function submitSupplementForm(data: InstlSncnRegistFormData): Promise<string | undefined> {
const docNo = initialData?.detDto.instlSncnDocNo;
if (!docNo) return undefined;
return instlSncnService.submitSupplement(docNo, buildFormPayload(data, "TE"));
return instlSncnService.submitSupplement(docNo, createFormPayload(data, "TE"));
}
async function onSplmntCmptn(data: InstlSncnRegistFormData) {
@@ -300,6 +272,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
</div>
</SectionCollapsibleGroup>
</form>
<RhfDevTool control={form.control} />
</FormProvider>
);
}
@@ -11,10 +11,10 @@ import {
extractTaskCodes,
getChecklistDisplayRows,
} from "../../lib/checklist-utils";
import { getVisibleReviewSteps } from "../../lib/review-step-utils";
import { resolveVisibleReviewSteps } from "../../lib/review-step-utils";
import {
CHECKLIST_TAB_LABELS,
type ChecklistFormState,
REVIEW_STEP_LABELS,
ROLE_TO_REVIEW_STEP,
TASK_CODE_LABELS,
} from "../../model/view-types";
@@ -36,8 +36,8 @@ function ChecklistSection({
setFormState,
}: ChecklistSectionProps) {
const visibleReviewSteps = useMemo(
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
[prgrsSttsComCd, userRoleCode],
() => resolveVisibleReviewSteps(prgrsSttsComCd, userRoleCode, cklstResultList),
[prgrsSttsComCd, userRoleCode, cklstResultList],
);
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
@@ -86,7 +86,7 @@ function ChecklistSection({
<TabsList>
{visibleReviewSteps.map((stepCode) => (
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
{REVIEW_STEP_LABELS[stepCode]}
{CHECKLIST_TAB_LABELS[stepCode]}
</TabsTrigger>
))}
</TabsList>
@@ -55,7 +55,7 @@ export function resolveNotiStatus(
return {
label: "미조회",
badgeProps: { color: "yellow" },
badgeProps: { color: "yellow", size: "lg" },
actionLabel: "처리하기",
};
}
@@ -1,14 +1,14 @@
import { type CostItemDef, roundAmount } from "@/widgets/pbadms-sprt";
import { type BizCstMatrixItem,getAmt, PLAN_STEPS } from "./biz-cst-matrix-utils";
import { type BizCstMatrixItem, getAmt, PLAN_STEPS } from "./biz-cst-matrix-utils";
export interface FnrsCstItem {
planStpNo: number;
gvexpAmt?: number;
lcexpAmt?: number;
orgprBdchgAmt?: number;
etcAmt?: number;
sumAmt?: number;
gvexpAmt?: number | null;
lcexpAmt?: number | null;
orgprBdchgAmt?: number | null;
etcAmt?: number | null;
sumAmt?: number | null;
}
export interface FnrsCstStepRemaining {
@@ -32,10 +32,7 @@ function getBizCstStepTotal(
function sumFnrsCstLeaf(item: FnrsCstItem): number {
return roundAmount(
(item.gvexpAmt ?? 0) +
(item.lcexpAmt ?? 0) +
(item.orgprBdchgAmt ?? 0) +
(item.etcAmt ?? 0),
(item.gvexpAmt ?? 0) + (item.lcexpAmt ?? 0) + (item.orgprBdchgAmt ?? 0) + (item.etcAmt ?? 0),
);
}
@@ -3,6 +3,7 @@ import type {
SewerBscPlanDetResponseDto,
SewerBscPlanSplmntDmndItem,
} from "@/entities/sewer-bsc-plan";
import { sewerBscPlanApplySaveSchema } from "@/entities/sewer-bsc-plan";
import { type SplmntDmndDialogConfirmPayload, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import type { ChecklistFormState, ReviewStepCode } from "../model/view-types";
@@ -26,7 +27,7 @@ export function buildSavePayload(
const cklstResultRows = data.cklstResultRows ?? [];
const esntlFileList = data.esntlFileList ?? [];
return {
return sewerBscPlanApplySaveSchema.parse({
sewerBscPlan: {
sewerBscPlanDocNo: detDto.sewerBscPlanDocNo,
admdstCd: detDto.admdstCd,
@@ -99,7 +100,7 @@ export function buildSavePayload(
})),
cklstResultList: toCklstSaveDto(cklstResultRows, checklistFormState, reviewStep),
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
};
});
}
export function buildTechReviewReplyPayload(
@@ -111,12 +112,12 @@ export function buildTechReviewReplyPayload(
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
opnrNm?: string,
): SewerBscPlanApplySaveRequestDto {
return {
return sewerBscPlanApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
techReViewSaveCd,
rvwStpCd: reviewStep,
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
};
});
}
export function buildSupplementReqPayload(
@@ -136,10 +137,10 @@ export function buildSupplementReqPayload(
cmptnYn: "N",
}));
return {
return sewerBscPlanApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...existingItems, ...newItems],
};
});
}
export function toSplmntDmndSaveItem(item: SewerBscPlanSplmntDmndItem): SewerBscPlanSplmntDmndItem {
@@ -1,3 +1,5 @@
import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan";
import {
PBADMS_PROGRESS_ORDER,
REVIEW_STEP_CODES,
@@ -28,6 +30,25 @@ export function getVisibleReviewSteps(
return REVIEW_STEP_CODES.slice(0, count);
}
export function resolveVisibleReviewSteps(
prgrsSttsComCd: string,
userRoleCode: string | undefined,
cklstResultList: CklstRvwRsltTreeDto[],
): ReviewStepCode[] {
const byProgressAndRole = getVisibleReviewSteps(prgrsSttsComCd, userRoleCode);
const currentStep = byProgressAndRole.at(-1);
const stepsWithData = new Set<ReviewStepCode>();
for (const row of cklstResultList) {
const step = row.rvwStpComCd as ReviewStepCode | undefined;
if (step && REVIEW_STEP_CODES.includes(step)) stepsWithData.add(step);
}
return REVIEW_STEP_CODES.filter(
(step) => stepsWithData.has(step) || step === currentStep,
);
}
export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
@@ -16,9 +16,9 @@ import {
export const sewerBscPlanRegistSchema = z.object({
sewerBscPlan: sewerBscPlanSaveSchema.extend({
sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"),
sewerBscPlanSeCdNm: z.string().optional(),
taskSeCdNm: z.string().optional(),
sido: z.string().optional(),
sewerBscPlanSeCdNm: z.string().nullish(),
taskSeCdNm: z.string().nullish(),
sido: z.string().nullish(),
gugun: z.string().nullish(),
}),
sewerBscPlanDtl: sewerBscPlanDtlSaveSchema.extend({
@@ -61,7 +61,7 @@ export const sewerBscPlanRegistSchema = z.object({
sewerBscPlanTmplntItemSchema.extend({
swtrmtFcltCd: z.string(),
swtrmtFcltNm: z.string().nullish(),
atchFileNm: z.string().optional(),
atchFileNm: z.string().nullish(),
sewerBscPlanTmplntDtlList: z
.array(
sewerBscPlanTmplntDtlItemSchema.extend({
@@ -69,7 +69,7 @@ export const sewerBscPlanRegistSchema = z.object({
swtrmtFcltCd: z.string(),
}),
)
.optional(),
.nullish(),
}),
),
cklstResultList: z.array(
@@ -77,8 +77,8 @@ export const sewerBscPlanRegistSchema = z.object({
cklstSn: z.number(),
cklstArtclSn: z.number(),
cklstRvwSn: z.number(),
selected: z.enum(["Y", "N"]).nullable(),
picRvwCn: z.string().nullable().optional(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
}),
),
esntlAtchFileList: z.array(
@@ -90,9 +90,9 @@ export const sewerBscPlanRegistSchema = z.object({
splmntDmndList: z.array(
sewerBscPlanSplmntDmndItemSchema.extend({
splmntArtclCdNm: z.string().nullish(),
splmntDmndAtchFileNm: z.string().optional(),
splmntAtchFileNm: z.string().optional(),
splmntAtchFileAtflId: z.string().optional(),
splmntDmndAtchFileNm: z.string().nullish(),
splmntAtchFileNm: z.string().nullish(),
splmntAtchFileAtflId: z.string().nullish(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
@@ -42,6 +42,12 @@ export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
@@ -31,7 +31,7 @@ interface PreviousPlanLookupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 부모 폼 `sewerBscPlan.sido` 표시명 */
initialSidoNm?: string;
initialSidoNm?: string | null;
/** 부모 폼 `sewerBscPlan.gugun` 표시명 */
initialGugunNm?: string | null;
}
@@ -11,11 +11,11 @@ import {
extractTaskCodes,
getChecklistDisplayRows,
} from "../../lib/checklist-utils";
import { getVisibleReviewSteps } from "../../lib/review-step-utils";
import { resolveVisibleReviewSteps } from "../../lib/review-step-utils";
import type { ReviewStepCode } from "../../model/view-types";
import {
CHECKLIST_TAB_LABELS,
type ChecklistFormState,
REVIEW_STEP_LABELS,
ROLE_TO_REVIEW_STEP,
TASK_CODE_LABELS,
} from "../../model/view-types";
@@ -37,8 +37,8 @@ function ChecklistSection({
setFormState,
}: ChecklistSectionProps) {
const visibleReviewSteps = useMemo(
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
[prgrsSttsComCd, userRoleCode],
() => resolveVisibleReviewSteps(prgrsSttsComCd, userRoleCode, cklstResultList),
[prgrsSttsComCd, userRoleCode, cklstResultList],
);
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
@@ -87,7 +87,7 @@ function ChecklistSection({
<TabsList>
{visibleReviewSteps.map((stepCode) => (
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
{REVIEW_STEP_LABELS[stepCode]}
{CHECKLIST_TAB_LABELS[stepCode]}
</TabsTrigger>
))}
</TabsList>
@@ -214,8 +214,8 @@ function TmplntFacilityMatrix({
interface TmplntFacilityAttachmentsProps {
facilityIndex: number;
facilityName?: string;
atchFileGroupId?: string;
atchFileNm?: string;
atchFileGroupId?: string | null;
atchFileNm?: string | null;
}
function TmplntFacilityAttachments({
@@ -9,7 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
@@ -319,6 +319,7 @@ function SewerBscPlanFormPage({
</div>
</SectionCollapsibleGroup>
</form>
<RhfDevTool control={form.control} />
</FormProvider>
);
}
+2
View File
@@ -26,9 +26,11 @@ export const ENDPOINT = {
SEWER_BSC_PLAN: "/as-service/api/v1/sewer-bsc-plans",
DASHBOARD: "/as-service/api/v1/dashboard",
DCRAG_TO: "/as-service/api/v1/dcrag-to",
AI_AUTO_PRCS_MNG: "/as-service/api/v1/ai-auto-prcs-mng",
PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng",
PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt",
PWRK_CNSLTN_DOC_PJTCO_AJMT_MNG: "/as-service/api/v1/pwrk-cnsltn-doc-pjtco-ajmt-mng",
PWRK_CNSLTN_DOC_EVL: "/as-service/api/v1/pwrk-cnsltn-doc-evl",
},
USER_SERVICE: {
LOGIN: "/user-service/login",
@@ -1,3 +1,5 @@
"use client";
import type { HTMLAttributes } from "react";
import type { UseQueryResult } from "@tanstack/react-query";
+2
View File
@@ -1,3 +1,5 @@
"use client";
import { useCallback, useState } from "react";
import { type Upload } from "tus-js-client";
+8 -8
View File
@@ -205,14 +205,14 @@ export interface EsntlAtchFileSaveBase {
export interface SplmntDmndSaveBase {
splmntDmndSn?: number | null;
splmntArtclComCd?: string | null;
splmntDmndCn?: string;
splmntDmndDt?: string;
splmntDmndAtchFileGroupId?: string;
splmntCn?: string;
splmntPicNm?: string;
splmntCmptnDt?: string;
splmntAtchFileGroupId?: string;
cmptnYn?: string;
splmntDmndCn?: string | null;
splmntDmndDt?: string | null;
splmntDmndAtchFileGroupId?: string | null;
splmntCn?: string | null;
splmntPicNm?: string | null;
splmntCmptnDt?: string | null;
splmntAtchFileGroupId?: string | null;
cmptnYn?: string | null;
}
/** 검토의견 저장 공통 필드 (문서번호 제외) */
+2 -1
View File
@@ -4,6 +4,7 @@ export type { FileAddButtonProps } from "./file-add-button";
export { FileAddButton } from "./file-add-button";
export { NoData } from "./no-data";
export { PageLayout } from "./page-layout";
export { RhfDevTool } from "./rhf-devtool";
export { SectionCollapsible } from "./section-collapsible";
export {
SectionCollapsibleGroup,
@@ -12,4 +13,4 @@ export {
export type { TextShimmerProps } from "./text-shimmer";
export { TEXT_SHIMMER_CLASS, TextShimmer } from "./text-shimmer";
export type { UploadedFileChipProps } from "./uploaded-file-chip";
export { UploadedFileChip } from "./uploaded-file-chip";
export { UploadedFileChip } from "./uploaded-file-chip";
+23
View File
@@ -0,0 +1,23 @@
"use client";
import type { Control, FieldValues } from "react-hook-form";
import dynamic from "next/dynamic";
const DevTool = dynamic(() => import("@hookform/devtools").then((mod) => mod.DevTool), {
ssr: false,
});
interface RhfDevToolProps<T extends FieldValues> {
control: Control<T>;
}
export function RhfDevTool<T extends FieldValues>({ control }: RhfDevToolProps<T>) {
if (process.env.NODE_ENV !== "development") return null;
return (
<DevTool
control={control as Control<FieldValues>}
placement="bottom-right"
styles={{ button: { right: "80px", bottom: "22px", scale: 2.5 } }}
/>
);
}
@@ -1,8 +1,8 @@
interface CanEditPbadmsDocParams {
docSeComCd: string;
prgrsSttsComCd: string | undefined;
rgtr: string | undefined;
loggedInUserUnqId: string | undefined;
prgrsSttsComCd?: string | null;
rgtr?: string | null;
loggedInUserUnqId?: string | null;
}
function canEditPbadmsDoc({
@@ -22,7 +22,7 @@ interface CklstResultListItem {
cklstArtclSn: number;
cklstRvwSn: number;
rvwStpComCd?: string | null;
selected: "Y" | "N" | null;
selected?: "Y" | "N" | null;
picRvwCn?: string | null;
}
@@ -17,8 +17,8 @@ export interface FnrsSharListItem {
fnrsSharSeCd: string;
exstnAmt: number;
chgRqtAmt: number;
ajmtAmt?: number;
rmrkCn?: string;
ajmtAmt?: number | null;
rmrkCn?: string | null;
}
export type FnrsSharTreeNode = FnrsSharSeMngTreeDto | FnrsSharBizCstTreeDto;
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useFormContext } from "react-hook-form";
import { useQueryClient } from "@tanstack/react-query";
import { Download } from "lucide-react";
@@ -78,7 +78,7 @@ function AttachmentsFormSection({
initialEsntlFiles,
initialEtcFiles,
}: AttachmentsFormSectionProps) {
const { control, getValues, setValue } = useFormContext<AttachmentsFormData>();
const { getValues, setValue } = useFormContext<AttachmentsFormData>();
const queryClient = useQueryClient();
const [uploadedFiles, setUploadedFiles] = useState<Record<string, UploadedFileItem[]>>({});
const [etcFiles, setEtcFiles] = useState<EtcFileItem[]>([]);
@@ -373,89 +373,83 @@ function AttachmentsFormSection({
return (
<SectionCollapsible title="첨부파일">
<div className="space-y-6">
<Controller
name="esntlAtchFileList"
control={control}
render={() => (
<div className="px-2">
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
<Table variant="secondary" className="table-fixed">
<colgroup>
<col width="44%" />
<col width="12%" />
<col width="44%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[30rem]" align="left">
파일명
</TableHead>
<TableHead>파일 확장자</TableHead>
<TableHead>파일첨부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{essentialFiles.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
조회된 필수 첨부파일 항목이 없습니다.
<div className="px-2">
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
<Table variant="secondary" className="table-fixed">
<colgroup>
<col width="44%" />
<col width="12%" />
<col width="44%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[30rem]" align="left">
파일명
</TableHead>
<TableHead>파일 확장자</TableHead>
<TableHead>파일첨부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{essentialFiles.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
조회된 필수 첨부파일 항목이 없습니다.
</TableCell>
</TableRow>
) : (
essentialFiles.map((file) => {
const atflCd = file.atchFileGroupId ?? "";
return (
<TableRow key={file.esntlAtchFileId}>
<TableHead
className="group break-keep font-medium"
data-required={file.esntlAtchYn === "Y"}
align="left"
>
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
{file.esntlAtchFileNm}
{atflCd ? (
<Button
size="sm"
color="white"
onClick={() => downloadTemplate(atflCd)}
>
<Download className="!size-5 shrink-0" />
양식내려받기
</Button>
) : null}
</div>
</TableHead>
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
<TableCell className="space-y-2">
<AttachmentFileField
files={(uploadedFiles[file.esntlAtchFileId] ?? []).map((item) => ({
fileKey: item.fileKey,
fileName: item.file?.name ?? item.fileName,
onDownload: item.atflId
? () => downloadByAtflId(item.atflId!)
: undefined,
}))}
inputId={`${file.esntlAtchFileId}-file-input`}
isUploadingByKey={isUploading}
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
onAdd={(fileList) =>
void handleEssentialFileUpload(file.esntlAtchFileId, fileList)
}
onRemove={(index) =>
void removeEssentialFile(file.esntlAtchFileId, index)
}
/>
</TableCell>
</TableRow>
) : (
essentialFiles.map((file) => {
const atflCd = file.atchFileGroupId ?? "";
return (
<TableRow key={file.esntlAtchFileId}>
<TableHead
className="group break-keep font-medium"
data-required={file.esntlAtchYn === "Y"}
align="left"
>
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
{file.esntlAtchFileNm}
{atflCd ? (
<Button
size="sm"
color="white"
onClick={() => downloadTemplate(atflCd)}
>
<Download className="!size-5 shrink-0" />
양식내려받기
</Button>
) : null}
</div>
</TableHead>
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
<TableCell className="space-y-2">
<AttachmentFileField
files={(uploadedFiles[file.esntlAtchFileId] ?? []).map((item) => ({
fileKey: item.fileKey,
fileName: item.file?.name ?? item.fileName,
onDownload: item.atflId
? () => downloadByAtflId(item.atflId!)
: undefined,
}))}
inputId={`${file.esntlAtchFileId}-file-input`}
isUploadingByKey={isUploading}
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
onAdd={(fileList) =>
void handleEssentialFileUpload(file.esntlAtchFileId, fileList)
}
onRemove={(index) =>
void removeEssentialFile(file.esntlAtchFileId, index)
}
/>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
)}
/>
);
})
)}
</TableBody>
</Table>
</div>
<div className="px-2">
<div className="mb-4 flex items-center justify-between">
@@ -43,12 +43,13 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
) : (
<div className="space-y-2">
{files.map((file) => (
<AttachmentDownloadLink
key={file.atflId}
atflId={file.atflId}
fileName={file.orgnlFileNm || "다운로드"}
className="items-center"
/>
<div key={file.atflId}>
<AttachmentDownloadLink
atflId={file.atflId}
fileName={file.orgnlFileNm || "다운로드"}
className="items-center"
/>
</div>
))}
</div>
)}
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
@@ -31,6 +31,16 @@ interface BusinessCostFormData {
type CostFieldKey = "bscPlanAmt" | "bgtRfltAmt" | "tstmDesignAmt";
function isSameBizCstCodes(
current: BusinessCostFormData["bizCstList"],
next: BusinessCostFormData["bizCstList"],
) {
return (
current.length === next.length &&
current.every((item, index) => item.acntgSeCd === next[index]?.acntgSeCd)
);
}
function BusinessCostFormSection() {
const { control, setValue, getValues } = useFormContext<BusinessCostFormData>();
const bizCstList = useWatch({ control, name: "bizCstList" }) ?? [];
@@ -42,22 +52,22 @@ function BusinessCostFormSection() {
useEffect(() => {
if (costItems.length === 0) return;
const existingMap = new Map(getValues("bizCstList").map((item) => [item.acntgSeCd, item]));
const current = getValues("bizCstList") ?? [];
const existingMap = new Map(current.map((item) => [item.acntgSeCd, item]));
const next = costItems.map((item) => {
const existing = existingMap.get(item.code);
if (existing) return existing;
setValue(
"bizCstList",
costItems.map((item) => {
const existing = existingMap.get(item.code);
if (existing) return existing;
return {
acntgSeCd: item.code,
bscPlanAmt: 0,
bgtRfltAmt: 0,
tstmDesignAmt: 0,
};
});
return {
acntgSeCd: item.code,
bscPlanAmt: 0,
bgtRfltAmt: 0,
tstmDesignAmt: 0,
};
}),
);
if (isSameBizCstCodes(current, next)) return;
setValue("bizCstList", next);
}, [costItems, getValues, setValue]);
function handleCostChange(index: number, fieldKey: CostFieldKey, value: string) {
@@ -119,58 +129,37 @@ function BusinessCostFormSection() {
</div>
</TableHead>
<TableCell>
<Controller
name={`bizCstList.${idx}.bgtRfltAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.bgtRfltAmt ?? ""}
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bizCstList.${idx}.bscPlanAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.bscPlanAmt ?? ""}
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bizCstList.${idx}.tstmDesignAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "tstmDesignAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.tstmDesignAmt ?? ""}
onChange={(e) => handleCostChange(idx, "tstmDesignAmt", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<div className="text-right">
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
@@ -17,7 +17,7 @@ import {
TableRow,
} from "@common/components";
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
interface BusinessScaleFormData {
bzScaleList: Array<{
@@ -31,6 +31,16 @@ interface BusinessScaleFormData {
type ScaleFieldKey = "bscPl" | "budgetReflct" | "thtiDsgn";
function isSameBzScaleCodes(
current: BusinessScaleFormData["bzScaleList"],
next: BusinessScaleFormData["bzScaleList"],
) {
return (
current.length === next.length &&
current.every((item, index) => item.cnstpSeCd === next[index]?.cnstpSeCd)
);
}
function BusinessScaleFormSection() {
const { control, setValue, getValues } = useFormContext<BusinessScaleFormData>();
const bzScaleList = useWatch({ control, name: "bzScaleList" }) ?? [];
@@ -42,7 +52,8 @@ function BusinessScaleFormSection() {
useEffect(() => {
if (scaleItems.length === 0) return;
const existingMap = new Map(getValues("bzScaleList").map((item) => [item.cnstpSeCd, item]));
const current = getValues("bzScaleList") ?? [];
const existingMap = new Map(current.map((item) => [item.cnstpSeCd, item]));
const next = scaleItems.map((item) => {
const existing = existingMap.get(item.code);
@@ -57,6 +68,7 @@ function BusinessScaleFormSection() {
};
});
if (isSameBzScaleCodes(current, next)) return;
setValue("bzScaleList", recalculateBzScaleTotals(next, scaleItems));
}, [scaleItems, getValues, setValue]);
@@ -112,58 +124,37 @@ function BusinessScaleFormSection() {
</div>
</TableHead>
<TableCell>
<Controller
name={`bzScaleList.${idx}.bscPl`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleScaleChange(idx, "bscPl", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.bscPl ?? ""}
onChange={(e) => handleScaleChange(idx, "bscPl", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bzScaleList.${idx}.budgetReflct`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleScaleChange(idx, "budgetReflct", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.budgetReflct ?? ""}
onChange={(e) => handleScaleChange(idx, "budgetReflct", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bzScaleList.${idx}.thtiDsgn`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleScaleChange(idx, "thtiDsgn", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.thtiDsgn ?? ""}
onChange={(e) => handleScaleChange(idx, "thtiDsgn", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<div className="text-right">
@@ -1,18 +1,15 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel } from "@/shared/ui/field";
import { cklstMngService, type CklstTreeNode } from "@/entities/cklst";
import { useCodeList } from "@/entities/code";
import {
Button,
RadioGroup,
RadioGroupItem,
Skeleton,
Table,
TableBody,
@@ -30,7 +27,7 @@ import {
buildChecklistTabs,
normalizeTaskSeComCdArray,
syncCklstResultListFromTree,
} from "../lib/checklist-form-utils";
} from "../lib/checklist-form-utils";
type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng" | "designCnstnMng" | "sewerBscPlan";
@@ -39,7 +36,7 @@ interface CklstResultListItem {
cklstArtclSn: number;
cklstRvwSn: number;
rvwStpComCd: string;
selected: "Y" | "N" | null;
selected?: "Y" | "N" | null;
picRvwCn?: string | null;
}
@@ -125,11 +122,21 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
}
}, [checklistTabs, activeTab]);
function handleSelectChange(resultIdx: number, value: "Y" | "N") {
const currentList = getValues("cklstResultList") as CklstResultListItem[];
const row = currentList[resultIdx];
if (!row) return;
const newList = [...currentList];
newList[resultIdx] = { ...row, selected: value };
setValue("cklstResultList", newList, { shouldDirty: true });
}
function handleSelectAll(tabCode: string, value: "Y" | "N") {
const tab = checklistTabs.find((t) => t.code === tabCode);
if (!tab) return;
const currentList = getValues("cklstResultList");
const currentList = getValues("cklstResultList") as CklstResultListItem[];
const newList = [...currentList];
tab.items
@@ -141,7 +148,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
}
});
setValue("cklstResultList", newList);
setValue("cklstResultList", newList, { shouldDirty: true });
}
const hasSelectedTaskCodes = Boolean(taskSeComCdArray?.length);
@@ -234,6 +241,8 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
{tab.items.map((item) => {
const resultIdx = cklstRvwSnIndexMap.get(item.cklstRvwSn) ?? -1;
const showRadios = item.bchckUseYn === "Y";
const selected = cklstResultList[resultIdx]?.selected;
const radioName = `check-${item.cklstRvwSn}`;
return (
<TableRow key={item.cklstRvwSn}>
@@ -248,66 +257,30 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
{item.content}
</TableCell>
{showRadios && resultIdx >= 0 ? (
<Controller
name={`cklstResultList.${resultIdx}.selected`}
control={control}
render={({ field, fieldState }) => (
<>
<TableCell className="text-center">
<RadioGroup
name={field.name}
value={field.value === "Y" ? "Y" : undefined}
onValueChange={field.onChange}
className="flex justify-center"
>
<FieldLabel
htmlFor={`check-${item.cklstRvwSn}-Y`}
className="!w-auto justify-center border-none"
>
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
className="w-auto"
>
<RadioGroupItem
value="Y"
id={`check-${item.cklstRvwSn}-Y`}
aria-invalid={fieldState.invalid}
aria-label="예"
/>
</Field>
</FieldLabel>
</RadioGroup>
</TableCell>
<TableCell className="text-center">
<RadioGroup
name={field.name}
value={field.value === "N" ? "N" : undefined}
onValueChange={field.onChange}
className="flex justify-center"
>
<FieldLabel
htmlFor={`check-${item.cklstRvwSn}-N`}
className="!w-auto justify-center border-none"
>
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
className="w-auto"
>
<RadioGroupItem
value="N"
id={`check-${item.cklstRvwSn}-N`}
aria-invalid={fieldState.invalid}
aria-label="아니오"
/>
</Field>
</FieldLabel>
</RadioGroup>
</TableCell>
</>
)}
/>
<>
<TableCell className="text-center">
<input
type="radio"
name={radioName}
value="Y"
checked={selected === "Y"}
onChange={() => handleSelectChange(resultIdx, "Y")}
aria-label="예"
className="cursor-pointer"
/>
</TableCell>
<TableCell className="text-center">
<input
type="radio"
name={radioName}
value="N"
checked={selected === "N"}
onChange={() => handleSelectChange(resultIdx, "N")}
aria-label="아니오"
className="cursor-pointer"
/>
</TableCell>
</>
) : null}
</TableRow>
);
@@ -1,18 +1,17 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useDownloadAttachmentByAtflId } from "@/entities/attach";
import type { AtflMngClientResponseDto } from "@/entities/fnrs-cnsltn";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
import { useFnrsSharSeMngTree } from "@/entities/fnrs-shar-se";
import {
Button,
cn,
Input,
Skeleton,
@@ -37,6 +36,7 @@ import { calcDiffPercent } from "../lib/format-utils";
import { BizCstTotalDisplayTable } from "./biz-cst-total-display-table";
const SHAR_FILE_KEY_PREFIX = "fnrs-shar::";
const SHAR_FILE_ACCEPT = ".hwp,.hwpx";
interface FnrsSharFormData {
fnrsSharList: FnrsSharListItem[];
@@ -53,6 +53,12 @@ interface FnrsSharFormData {
type SharFieldKey = "exstnAmt" | "chgRqtAmt" | "rmrkCn";
interface SharUploadedFile {
fileKey: string;
fileName: string;
atflId?: string;
}
interface FnrsSharFormSectionProps {
initialSharTree?: FnrsSharBizCstTreeDto[];
initialSharAtchFiles?: AtflMngClientResponseDto[];
@@ -62,12 +68,30 @@ function createSharUploadKey(file: File) {
return `${SHAR_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function toInitialSharFile(file?: AtflMngClientResponseDto): SharUploadedFile | null {
if (!file?.orgnlFileNm) return null;
return {
fileKey: file.atflId ? `server::${file.atflId}` : `${SHAR_FILE_KEY_PREFIX}${file.atflCd}`,
fileName: file.orgnlFileNm,
atflId: file.atflId,
};
}
function isSameFnrsSharCodes(current: FnrsSharListItem[], next: FnrsSharListItem[]) {
return (
current.length === next.length &&
current.every((item, index) => item.fnrsSharSeCd === next[index]?.fnrsSharSeCd)
);
}
function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsSharFormSectionProps) {
const { control, setValue, getValues } = useFormContext<FnrsSharFormData>();
const fnrsSharList = useWatch({ control, name: "fnrsSharList" }) ?? [];
const [uploadedFileName, setUploadedFileName] = useState(
() => initialSharAtchFiles?.[0]?.orgnlFileNm ?? "",
const [uploadedFile, setUploadedFile] = useState<SharUploadedFile | null>(() =>
toInitialSharFile(initialSharAtchFiles?.[0]),
);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const { data: masterTree = [], isLoading: isMasterLoading } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree();
@@ -78,9 +102,13 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
const isLoading = initialSharTree?.length ? false : isMasterLoading;
const { uploadFileAsync, isUploading } = useTus({
onSuccess: (atflCd) => {
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd) return;
setValue("fnrsCnsltnMng.sharAtchFileGroupId", atflCd, { shouldDirty: true });
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
const isSharUploading = Object.values(isUploading).some(Boolean);
@@ -96,6 +124,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
: mergeFnrsSharTreeWithForm(sharTree, []);
const withTotals = recalculateFnrsSharTotals(merged, sharItems);
if (isSameFnrsSharCodes(existing, withTotals)) return;
setValue("fnrsSharList", withTotals);
}, [sharItems, sharTree, initialSharTree, getValues, setValue]);
@@ -120,16 +149,32 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
applySharList(draft);
}
async function handleSharFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
async function handleSharFileAdd(files: FileList | null) {
const file = files?.[0];
if (!file) return;
const fileKey = createSharUploadKey(file);
setUploadedFileName(file.name);
await uploadFileAsync(file, { fileKey });
event.target.value = "";
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, { fileKey });
} catch {
// onError callback handles cleanup
}
}
const sharAtchFiles = uploadedFile
? [
{
fileKey: uploadedFile.fileKey,
fileName: uploadedFile.fileName,
onDownload: uploadedFile.atflId
? () => downloadByAtflId(uploadedFile.atflId!)
: undefined,
},
]
: [];
if (isLoading) {
return (
<SectionCollapsible title="재원분담">
@@ -189,40 +234,26 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
{item.label}
</TableHead>
<TableCell>
<Controller
name={`fnrsSharList.${idx}.exstnAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value ?? ""}
onChange={(e) => handleSharChange(idx, "exstnAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.exstnAmt ?? ""}
onChange={(e) => handleSharChange(idx, "exstnAmt", e.target.value)}
readOnly={item.readonly}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`fnrsSharList.${idx}.chgRqtAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value ?? ""}
onChange={(e) => handleSharChange(idx, "chgRqtAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.chgRqtAmt ?? ""}
onChange={(e) => handleSharChange(idx, "chgRqtAmt", e.target.value)}
readOnly={item.readonly}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
</TableCell>
<TableCell>
<div className="text-right">
@@ -233,19 +264,12 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
<div className="text-right">{calcDiffPercent(diff, data.exstnAmt || 0)}</div>
</TableCell>
<TableCell>
<Controller
name={`fnrsSharList.${idx}.rmrkCn`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
value={field.value ?? ""}
onChange={(e) => handleSharChange(idx, "rmrkCn", e.target.value)}
aria-invalid={fieldState.invalid}
/>
</Field>
)}
/>
<Field>
<Input
value={data.rmrkCn ?? ""}
onChange={(e) => handleSharChange(idx, "rmrkCn", e.target.value)}
/>
</Field>
</TableCell>
</TableRow>
);
@@ -257,31 +281,20 @@ 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">
{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">
첨부파일
<input
type="file"
accept=".hwp,.hwpx"
className="hidden"
onChange={handleSharFileChange}
/>
</label>
</Button>
</div>
{!uploadedFile ? (
<p className="mb-2 px-1 text-xl text-muted-foreground">
총사업비 신청서(.hwp) 파일을 등록해주세요.
</p>
) : null}
<AttachmentFileField
files={sharAtchFiles}
inputId="fnrs-shar-atch-file-input"
isUploadingByKey={isUploading}
accept={SHAR_FILE_ACCEPT}
multiple={false}
addDisabled={isSharUploading}
onAdd={(fileList) => void handleSharFileAdd(fileList)}
/>
</div>
</SectionCollapsible>
);
@@ -5,7 +5,12 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { AttachmentDownloadLink, attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import {
AttachmentDownloadLink,
attachService,
useDownloadAttachmentByAtflId,
} from "@/entities/attach";
import { FnrsCnsltnSplmntDmndResponseDto } from "@/entities/fnrs-cnsltn";
import {
Badge,
Table,
@@ -46,6 +51,7 @@ interface SplmntDmndFormData {
}
interface SplmntDmndFormSectionProps {
initialData?: FnrsCnsltnSplmntDmndResponseDto[];
readOnly?: boolean;
}
@@ -65,7 +71,7 @@ function getRowKey(item: SplmntDmndFormItem, index: number) {
return item.splmntDmndSn != null ? String(item.splmntDmndSn) : `idx-${index}`;
}
function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) {
function SplmntDmndFormSection({ initialData, readOnly = false }: SplmntDmndFormSectionProps) {
const formId = useId();
const { control, setValue, getValues } = useFormContext<SplmntDmndFormData>();
const splmntDmndList = useWatch({ control, name: "splmntDmndList" }) ?? [];
@@ -260,6 +266,7 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
const isRowUploading = rowFiles.some((file) => isUploading[file.fileKey] ?? false);
const fileInputId = `${formId}-${rowKey}-file`;
const isRowReadOnly = readOnly || item.cmptnYn === "Y";
const splmntAtchFileInfo = initialData?.[index]?.splmntAtchFileInfo;
return (
<TableRow key={rowKey}>
@@ -282,7 +289,15 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
</TableCell>
<TableCell className="align-top">
{isRowReadOnly ? (
<span className="whitespace-pre-wrap">{item.splmntCn || "-"}</span>
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntCn || "-"}</div>
{splmntAtchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={splmntAtchFileInfo?.atflCd}
fileName={splmntAtchFileInfo?.orgnlFileNm}
/>
) : null}
</div>
) : (
<Controller
control={control}
@@ -292,7 +307,7 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
value={field.value ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="보완완료 내용을 입력하세요."
placeholder="보완 완료 내용을 입력하세요."
className="min-h-[6rem] !text-xl"
/>
)}
@@ -276,7 +276,9 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
)}
</div>
</PostAction>
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
{bbsInfo?.cmntUseYn === "Y" && (
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
)}
</>
);
}
+15 -9
View File
@@ -1,10 +1,17 @@
"use client";
import dayjs from "dayjs";
import Image from "next/image";
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMenuStore, useMutationWrapper } from "@/shared/lib";
import { AttachmentApi } from "@/entities/attachment";
import { BoardApi } from "@/entities/board";
import { PostApi } from "@/entities/post";
import {
Button,
Label,
@@ -12,14 +19,11 @@ import {
RadioGroupItem,
Skeleton,
} from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi } from "@/entities/post";
import { BoardApi } from "@/entities/board";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper, useMenuStore } from "@/shared/lib";
import { PostEvaluationApi } from "../api/post-evaluation-service";
import { PostAction } from "./post-action";
import { PostComment } from "./post-comment";
import { PostEvaluationApi } from "../api/post-evaluation-service";
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
@@ -255,7 +259,9 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
)}
</div>
</PostAction>
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
{bbsInfo?.cmntUseYn === "Y" && (
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
)}
</>
);
}
@@ -185,7 +185,9 @@ export default function DemandStatusPage() {
const userData = JSON.parse(user) as SessionUser;
const dmdInfoId = userData.dmdInfoId?.trim();
if (dmdInfoId) {
getSdmInfoStatusList(dmdInfoId);
void getSdmInfoStatusList(dmdInfoId).then((active) => {
if (!active) window.location.href = "/";
});
}
} catch (error) {
console.error("Error parsing user data:", error);
@@ -341,7 +343,9 @@ export default function DemandStatusPage() {
if (res) {
const user = JSON.parse(sessionStorage.getItem("user") || "{}");
if (user?.dmdInfoId) {
getSdmInfoStatusList(user.dmdInfoId);
void getSdmInfoStatusList(user.dmdInfoId).then((active) => {
if (!active) window.location.href = "/";
});
}
setIsEditMode(false);
}

Some files were not shown because too many files have changed in this diff Show More