From 6fb610d9aa31fe7570b88cd04846201ef15e48bb Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 30 Jun 2026 13:51:22 +0900 Subject: [PATCH 01/83] =?UTF-8?q?[as]=20feat:=20fnrs-cnsltn-mng=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fnrs-cnsltn/api/fnrs-cnsltn-service.ts | 86 ++++++ .../src/entities/fnrs-cnsltn/api/queries.ts | 131 +++++++++ apps/as/src/entities/fnrs-cnsltn/index.ts | 37 +++ .../model/fnrs-cnsltn-interface.ts | 277 ++++++++++++++++++ .../ui/components/progress-step-indicator.tsx | 111 ------- apps/as/src/shared/config/api.ts | 1 + 6 files changed, 532 insertions(+), 111 deletions(-) create mode 100644 apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts create mode 100644 apps/as/src/entities/fnrs-cnsltn/api/queries.ts create mode 100644 apps/as/src/entities/fnrs-cnsltn/index.ts create mode 100644 apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts delete mode 100644 apps/as/src/pages/instl-sncn/ui/components/progress-step-indicator.tsx diff --git a/apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts b/apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts new file mode 100644 index 000000000..e67a9a6c7 --- /dev/null +++ b/apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts @@ -0,0 +1,86 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + +import type { + DocPrgrsUpdateRequestDto, + FnrsCnsltnApplySaveRequestDto, + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnMngResponseDto, + FnrsCnsltnMngSaveRequestDto, + FnrsCnsltnMngSearchDto, + FnrsCnsltnMngService, + FnrsCnsltnMngUpdateRequestDto, +} from "../model/fnrs-cnsltn-interface"; + +const BASE = ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG; + +async function findPage( + searchDto: FnrsCnsltnMngSearchDto, +): Promise> { + const response = await client.get>(BASE, { + params: searchDto, + }); + return response.data; +} + +async function findById(fnrsCnsltnDocNo: string): Promise { + const response = await client.get(`${BASE}/${fnrsCnsltnDocNo}`); + return response.data; +} + +async function save(body: FnrsCnsltnMngSaveRequestDto): Promise { + const response = await client.post(BASE, body); + return response.data; +} + +async function saveApply(body: FnrsCnsltnApplySaveRequestDto): Promise { + const response = await client.post(`${BASE}/save-apply`, body); + return response.data; +} + +async function update( + fnrsCnsltnDocNo: string, + body: FnrsCnsltnMngUpdateRequestDto, +): Promise { + const response = await client.put(`${BASE}/${fnrsCnsltnDocNo}`, body); + return response.data; +} + +async function techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise { + await client.post(`${BASE}/${id}/tech-review`, body); +} + +async function submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise { + const response = await client.post(`${BASE}/${id}/submitsupplement`, body); + return response.data; +} + +async function submitReqSupplement( + id: string, + body: FnrsCnsltnApplySaveRequestDto, +): Promise { + const response = await client.post(`${BASE}/${id}/reqsupplement`, body); + return response.data; +} + +async function remove(fnrsCnsltnDocNo: string): Promise { + await client.delete(`${BASE}/${fnrsCnsltnDocNo}`); +} + +async function changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise { + await client.post(`${BASE}/doc/updatePrgrsStts`, body); +} + +export const fnrsCnsltnMngService = { + findPage, + findById, + save, + saveApply, + update, + techReview, + submitSupplement, + submitReqSupplement, + remove, + changePrgrsSttsComCd, +} satisfies FnrsCnsltnMngService; diff --git a/apps/as/src/entities/fnrs-cnsltn/api/queries.ts b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts new file mode 100644 index 000000000..ac564f300 --- /dev/null +++ b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts @@ -0,0 +1,131 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { + DocPrgrsUpdateRequestDto, + FnrsCnsltnApplySaveRequestDto, + FnrsCnsltnMngSaveRequestDto, + FnrsCnsltnMngSearchDto, + FnrsCnsltnMngUpdateRequestDto, +} from "../model/fnrs-cnsltn-interface"; + +import { fnrsCnsltnMngService } from "./fnrs-cnsltn-service"; + +const fnrsCnsltnMngBaseKey = [ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG] as const; + +export function useFnrsCnsltnMngPage(params: FnrsCnsltnMngSearchDto) { + return useQuery({ + queryKey: [...fnrsCnsltnMngBaseKey, "findPage", params], + queryFn: () => fnrsCnsltnMngService.findPage(params), + }); +} + +export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) { + return useQuery({ + queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo], + queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!), + enabled: Boolean(fnrsCnsltnDocNo), + }); +} + +export function useSaveFnrsCnsltnMng() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (body: FnrsCnsltnMngSaveRequestDto) => fnrsCnsltnMngService.save(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + }, + }); +} + +export function useSaveApplyFnrsCnsltnMng() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (body: FnrsCnsltnApplySaveRequestDto) => fnrsCnsltnMngService.saveApply(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + }, + }); +} + +export function useUpdateFnrsCnsltnMng() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ + fnrsCnsltnDocNo, + body, + }: { + fnrsCnsltnDocNo: string; + body: FnrsCnsltnMngUpdateRequestDto; + }) => fnrsCnsltnMngService.update(fnrsCnsltnDocNo, body), + onSuccess: (_data, { fnrsCnsltnDocNo }) => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo], + }); + }, + }); +} + +export function useTechReviewFnrsCnsltnMng() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) => + fnrsCnsltnMngService.techReview(id, body), + onSuccess: (_data, { id }) => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...fnrsCnsltnMngBaseKey, "findById", id], + }); + }, + }); +} + +export function useSubmitSupplementFnrsCnsltnMng() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) => + fnrsCnsltnMngService.submitSupplement(id, body), + onSuccess: (_data, { id }) => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...fnrsCnsltnMngBaseKey, "findById", id], + }); + }, + }); +} + +export function useSubmitReqSupplementFnrsCnsltnMng() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) => + fnrsCnsltnMngService.submitReqSupplement(id, body), + onSuccess: (_data, { id }) => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...fnrsCnsltnMngBaseKey, "findById", id], + }); + }, + }); +} + +export function useDeleteFnrsCnsltnMng() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (fnrsCnsltnDocNo: string) => fnrsCnsltnMngService.remove(fnrsCnsltnDocNo), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + }, + }); +} + +export function useChangePrgrsSttsComCd() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (body: DocPrgrsUpdateRequestDto) => fnrsCnsltnMngService.changePrgrsSttsComCd(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); + }, + }); +} diff --git a/apps/as/src/entities/fnrs-cnsltn/index.ts b/apps/as/src/entities/fnrs-cnsltn/index.ts new file mode 100644 index 000000000..ca54cbfe6 --- /dev/null +++ b/apps/as/src/entities/fnrs-cnsltn/index.ts @@ -0,0 +1,37 @@ +export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service"; +export { + useChangePrgrsSttsComCd, + useDeleteFnrsCnsltnMng, + useFnrsCnsltnMng, + useFnrsCnsltnMngPage, + useSaveApplyFnrsCnsltnMng, + useSaveFnrsCnsltnMng, + useSubmitReqSupplementFnrsCnsltnMng, + useSubmitSupplementFnrsCnsltnMng, + useTechReviewFnrsCnsltnMng, + useUpdateFnrsCnsltnMng, +} from "./api/queries"; +export type { + AtflMngClientResponseDto, + BizCstItem, + BizCstTreeDto, + BizSclTreeDto, + BzScaleItem, + CklstResultItem, + CklstRvwRsltTreeDto, + DocPrgrsUpdateRequestDto, + ElaprDocPrgrsClientResponseDto, + EsntlAtchFileItem, + EsntlAtchFileMngTreeResponseDto, + FnrsCnsltnApplySaveRequestDto, + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnMngResponseDto, + FnrsCnsltnMngSaveRequestDto, + FnrsCnsltnMngSearchDto, + FnrsCnsltnMngService, + FnrsCnsltnMngUpdateRequestDto, + FnrsCnsltnRvwOpnnResponseDto, + FnrsCnsltnRvwOpnnSaveRequestDto, + FnrsCnsltnSplmntDmndItem, + FnrsCnsltnSplmntDmndResponseDto, +} from "./model/fnrs-cnsltn-interface"; diff --git a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts new file mode 100644 index 000000000..6dc5da4f4 --- /dev/null +++ b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts @@ -0,0 +1,277 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 FnrsCnsltnMngResponseDto */ +export interface FnrsCnsltnMngResponseDto { + fnrsCnsltnDocNo: string; + admdstCd?: string; + envoSeComCd?: string; + envoSeCdNm?: string; + taskSeComCd?: string; + taskSeCdNm?: 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; + prgrsSttsComCd?: string; + prgrsSttsCdNm?: string; + regDt?: string; + rgtr?: string; + rgtrNm?: string; + mdfcnDt?: string; + sido?: string; + gugun?: string; +} + +/** 백엔드 FnrsCnsltnMngSearchDto + Pageable */ +export interface FnrsCnsltnMngSearchDto extends PageableParams { + 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; + prgrsSttsComCd?: string; +} + +/** 백엔드 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; + prgrsSttsComCd?: string; +} + +/** 백엔드 FnrsCnsltnMngUpdateRequestDto */ +export interface FnrsCnsltnMngUpdateRequestDto { + 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; + prgrsSttsComCd?: string; +} + +/** 백엔드 FnrsCnsltnMngDetResponseDto */ +export interface FnrsCnsltnMngDetResponseDto { + detDto?: FnrsCnsltnMngResponseDto; + bizSclList?: BizSclTreeDto[]; + bizCstList?: BizCstTreeDto[]; + cklstResultList?: CklstRvwRsltTreeDto[]; + esntlFileList?: EsntlAtchFileMngTreeResponseDto[]; + etcFileList?: AtflMngClientResponseDto[]; + rvwList?: FnrsCnsltnRvwOpnnResponseDto[]; + splmntDmndList?: FnrsCnsltnSplmntDmndResponseDto[]; + elaprDocPrgrsList?: ElaprDocPrgrsClientResponseDto[]; +} + +/** 백엔드 FnrsCnsltnApplySaveRequestDto */ +export interface FnrsCnsltnApplySaveRequestDto { + techReViewSaveCd?: string; + rvwStpCd?: string; + fnrsCnsltnMng: FnrsCnsltnMngSaveRequestDto; + bzScaleList?: BzScaleItem[]; + bizCstList?: BizCstItem[]; + cklstResultList?: CklstResultItem[]; + cklstResultMap?: Record; + esntlAtchFileList?: EsntlAtchFileItem[]; + rvwOpnnList?: FnrsCnsltnRvwOpnnSaveRequestDto[]; + splmntDmndList?: FnrsCnsltnSplmntDmndItem[]; +} + +/** 백엔드 DocPrgrsUpdateRequestDto */ +export interface DocPrgrsUpdateRequestDto { + docNo?: string; + prgrsSttsComCd?: string; +} + +export interface BzScaleItem { + cnstpSeCd?: string; + bscPl?: number; + budgetReflct?: number; + thtiDsgn?: number; +} + +export interface BizCstItem { + acntgSeCd?: string; + bscPlanAmt?: number; + bgtRfltAmt?: number; + tstmDesignAmt?: number; +} + +export interface CklstResultItem { + cklstRvwSn?: number; + cklstArtclSn?: number; + cklstSn?: number; + rvwStpComCd: string; + selected?: string; + picRvwCn?: string; +} + +export interface EsntlAtchFileItem { + esntlAtchFileId?: string; + atchFileGroupId?: string; +} + +export interface FnrsCnsltnSplmntDmndItem { + splmntDmndSn?: number; + splmntArtclComCd?: string; + splmntDmndCn?: string; + splmntDmndDt?: string; + splmntDmndAtchFileGroupId?: string; + splmntCn?: string; + splmntPicNm?: string; + splmntCmptnDt?: string; + splmntAtchFileGroupId?: string; + cmptnYn?: string; +} + +export interface BizSclTreeDto { + cnstpSeCd?: string; + cnstpSeNm?: string; + bscPl?: number; + budgetReflct?: number; + thtiDsgn?: number; +} + +export interface BizCstTreeDto { + acntgSeCd?: string; + acntgSeNm?: string; + bscPlanAmt?: number; + bgtRfltAmt?: number; + tstmDesignAmt?: number; +} + +export interface CklstRvwRsltTreeDto { + cklstRvwSn?: number; + cklstArtclSn?: number; + cklstSn?: number; + rvwStpComCd?: string; + selected?: string; + picRvwCn?: string; + cklstNm?: string; + cklstArtclNm?: string; +} + +export interface EsntlAtchFileMngTreeResponseDto { + esntlAtchFileId?: string; + atchFileGroupId?: string; + esntlAtchFileNm?: string; + esntlAtchYn?: string; +} + +export interface AtflMngClientResponseDto { + atflId?: string; + orgnlFileNm?: string; + atflFlieSz?: number; + atflType?: string; + dwnldNocs?: number; + regDt?: string; +} + +export interface FnrsCnsltnRvwOpnnResponseDto { + fnrsCnsltnRvwOpnnSn?: number; + fnrsCnsltnDocNo?: string; + rvwStpComCd?: string; + rvwOpnn?: string; + rvwOpnnDt?: string; + rgtr?: string; + rgtrNm?: string; +} + +export interface FnrsCnsltnRvwOpnnSaveRequestDto { + fnrsCnsltnDocNo?: string; + rvwStpComCd?: string; + rvwOpnn?: string; +} + +export interface FnrsCnsltnSplmntDmndResponseDto { + splmntDmndSn?: number; + fnrsCnsltnDocNo?: string; + splmntArtclComCd?: string; + splmntDmndCn?: string; + splmntDmndDt?: string; + splmntDmndAtchFileGroupId?: string; + splmntCn?: string; + splmntPicNm?: string; + splmntCmptnDt?: string; + splmntAtchFileGroupId?: string; + cmptnYn?: string; +} + +export interface ElaprDocPrgrsClientResponseDto { + elaprDmndNo?: string; + elaprAplnNo?: string; + docNo?: string; + docPrgrsSn?: number; + prgrsSttsComCd?: string; + prgrsSttsCdNm?: string; + aprvrNm?: string; + aprvDt?: string; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface FnrsCnsltnMngService { + findPage(searchDto: FnrsCnsltnMngSearchDto): Promise>; + findById(fnrsCnsltnDocNo: string): Promise; + save(body: FnrsCnsltnMngSaveRequestDto): Promise; + saveApply(body: FnrsCnsltnApplySaveRequestDto): Promise; + update(fnrsCnsltnDocNo: string, body: FnrsCnsltnMngUpdateRequestDto): Promise; + techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise; + submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise; + submitReqSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise; + remove(fnrsCnsltnDocNo: string): Promise; + changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise; +} diff --git a/apps/as/src/pages/instl-sncn/ui/components/progress-step-indicator.tsx b/apps/as/src/pages/instl-sncn/ui/components/progress-step-indicator.tsx deleted file mode 100644 index e3cff5d35..000000000 --- a/apps/as/src/pages/instl-sncn/ui/components/progress-step-indicator.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import { Fragment } from "react"; - -import { Badge } from "@common/components"; - -interface ProgressStepIndicatorProps { - prgrsSttsComCd: string; - prgrsSttsCdNm: string; -} - -const MAIN_STEP_LABELS = { - save: "저장", - apply: "신청완료", - techReview: "기술검토", - reply: "검토결과 회신", -} as const; - -const TECH_REVIEW_STATUS_CODES = new Set(["LE", "ER", "EE", "TE", "TS", "TR", "TP"]); - -type BadgeState = "completed" | "current" | "pending" | "action"; - -interface ProgressBadge { - label: string; - state: BadgeState; -} - -function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] { - const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview; - const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply; - - if (prgrsSttsComCd === "SV") { - return [ - { label: MAIN_STEP_LABELS.save, state: "current" }, - { label: MAIN_STEP_LABELS.apply, state: "pending" }, - { label: MAIN_STEP_LABELS.techReview, state: "pending" }, - { label: MAIN_STEP_LABELS.reply, state: "pending" }, - ]; - } - - if (prgrsSttsComCd === "ST") { - return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "current" }, - { label: MAIN_STEP_LABELS.techReview, state: "pending" }, - { label: MAIN_STEP_LABELS.reply, state: "pending" }, - ]; - } - - if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) { - return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: techReviewLabel, state: "action" }, - { label: MAIN_STEP_LABELS.reply, state: "pending" }, - ]; - } - - if (prgrsSttsComCd === "EP") { - return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.techReview, state: "completed" }, - { label: replyLabel, state: "action" }, - ]; - } - - if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") { - return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.techReview, state: "completed" }, - { label: MAIN_STEP_LABELS.reply, state: "completed" }, - ]; - } - - return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }]; -} - -function getBadgeColor(state: BadgeState) { - switch (state) { - case "completed": - return "green"; - case "current": - return "blue"; - case "pending": - return "lightGray"; - case "action": - return "orange"; - } -} - -function ProgressStepIndicator({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepIndicatorProps) { - const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm); - - return ( -
-

진행 단계

-
- {badges.map((badge, index) => ( - - {index > 0 && >} - {badge.label} - - ))} -
-
- ); -} - -export { ProgressStepIndicator }; diff --git a/apps/as/src/shared/config/api.ts b/apps/as/src/shared/config/api.ts index 784af3f15..a0545215d 100644 --- a/apps/as/src/shared/config/api.ts +++ b/apps/as/src/shared/config/api.ts @@ -15,6 +15,7 @@ export const ENDPOINT = { CKLST_ARTCL: "/as-service/api/v1/cklst-artcl", CKLST_RVW: "/as-service/api/v1/cklst-rvw", ADSTDSTR: "/as-service/api/v1/adstdstr", + FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng", }, USER_SERVICE: { LOGIN: "/user-service/login", From e81210ec042d7a9c476fe41c731381cd15527927 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 1 Jul 2026 17:19:48 +0900 Subject: [PATCH 02/83] =?UTF-8?q?[as]=20feat:=20=EC=9E=AC=EC=9B=90?= =?UTF-8?q?=ED=98=91=EC=9D=98=20=EC=8B=A0=EC=B2=AD=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9E=91=EC=97=85=20=EB=B0=8F=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=8B=A0=EC=B2=AD=20=ED=8F=BC=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fnrs-cnsltn/[id]/edit/page.tsx | 10 + .../fnrs-cnsltn/[id]/page.tsx | 10 + .../(with-base-layout)/fnrs-cnsltn/page.tsx | 3 + .../fnrs-cnsltn/regist/page.tsx | 12 + .../src/entities/fnrs-cnsltn/api/queries.ts | 2 +- apps/as/src/entities/fnrs-cnsltn/index.ts | 1 + .../model/fnrs-cnsltn-interface.ts | 308 +++------ .../instl-sncn/model/instl-sncn-interface.ts | 277 ++------ apps/as/src/pages/fnrs-cnsltn/index.ts | 5 + .../pages/fnrs-cnsltn/lib/approval-utils.ts | 28 + .../fnrs-cnsltn/lib/business-cost-utils.ts | 35 + .../pages/fnrs-cnsltn/lib/button-config.ts | 133 ++++ .../pages/fnrs-cnsltn/lib/checklist-utils.ts | 178 +++++ .../lib/fnrs-cnsltn-regist-schema.ts | 87 +++ .../pages/fnrs-cnsltn/lib/form-data-mapper.ts | 130 ++++ .../fnrs-cnsltn/lib/review-opinion-utils.ts | 45 ++ .../fnrs-cnsltn/lib/review-step-utils.ts | 44 ++ .../fnrs-cnsltn/lib/save-payload-utils.ts | 148 ++++ .../lib/use-checklist-form-state.ts | 61 ++ .../src/pages/fnrs-cnsltn/model/columns.tsx | 170 +++++ .../src/pages/fnrs-cnsltn/model/view-types.ts | 86 +++ .../ui/components/checklist-task-panel.tsx | 164 +++++ .../ui/components/elapr-approval-dialog.tsx | 223 ++++++ .../components/elapr-tmplt-picker-dialog.tsx | 137 ++++ .../ui/components/floating-action-buttons.tsx | 640 ++++++++++++++++++ .../pages/fnrs-cnsltn/ui/fnrs-edit-page.tsx | 25 + .../pages/fnrs-cnsltn/ui/fnrs-form-page.tsx | 137 ++++ .../pages/fnrs-cnsltn/ui/fnrs-list-page.tsx | 152 +++++ .../as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx | 86 +++ .../pages/fnrs-cnsltn/ui/fnrs-regist-page.tsx | 11 + .../pages/fnrs-cnsltn/ui/fnrs-view-page.tsx | 107 +++ .../ui/sections/attachments-section.tsx | 118 ++++ .../ui/sections/basic-info-form-section.tsx | 247 +++++++ .../ui/sections/basic-info-section.tsx | 50 ++ .../ui/sections/business-cost-section.tsx | 93 +++ .../ui/sections/business-scale-section.tsx | 71 ++ .../ui/sections/checklist-section.tsx | 137 ++++ .../ui/sections/contact-info-section.tsx | 56 ++ .../ui/sections/supplement-demand-section.tsx | 57 ++ .../sections/tech-review-opinion-section.tsx | 338 +++++++++ .../lib/business-cost-form-utils.ts | 158 ----- .../pages/instl-sncn/lib/form-data-mapper.ts | 8 +- ...-schema.ts => instl-sncn-regist-schema.ts} | 18 +- .../ui/components/floating-action-buttons.tsx | 2 +- .../pages/instl-sncn/ui/instl-form-page.tsx | 36 +- .../as/src/pages/instl-sncn/ui/instl-list.tsx | 9 +- .../ui/sections/attachments-section.tsx | 3 +- .../ui/sections/basic-info-form-section.tsx | 27 +- .../ui/sections/basic-info-section.tsx | 3 +- .../ui/sections/business-cost-section.tsx | 4 +- .../ui/sections/business-scale-section.tsx | 5 +- .../ui/sections/checklist-section.tsx | 2 +- .../ui/sections/contact-info-section.tsx | 3 +- .../ui/sections/history-info-section.tsx | 3 +- .../sections/tech-review-opinion-section.tsx | 2 +- apps/as/src/shared/model/index.ts | 19 + apps/as/src/shared/model/pbadms-sprt.ts | 245 +++++++ apps/as/src/widgets/bns-picker/index.ts | 2 + .../lib/to-bns-list-page-response.ts | 0 .../bns-picker}/model/bns-picker-columns.tsx | 0 .../bns-picker/ui}/bns-picker-dialog.tsx | 7 +- apps/as/src/widgets/cnfm/ui/index.ts | 6 +- apps/as/src/widgets/pbadms-sprt/index.ts | 23 + .../pbadms-sprt/lib/attachments-form-utils.ts | 21 + .../lib/business-cost-form-utils.ts | 89 +++ .../lib/business-scale-form-utils.ts | 98 +++ .../pbadms-sprt}/lib/checklist-form-utils.ts | 43 ++ .../pbadms-sprt}/lib/format-utils.ts | 0 .../ui}/attachments-form-section.tsx | 69 +- .../ui}/business-cost-form-section.tsx | 65 +- .../ui}/business-scale-form-section.tsx | 112 +-- .../ui}/checklist-form-section.tsx | 78 +-- .../ui}/contact-info-form-section.tsx | 20 +- .../ui}/privacy-consent-section.tsx | 9 +- .../pbadms-sprt/ui}/section-collapsible.tsx | 0 .../pbadms-sprt/ui}/task-se-picker-dialog.tsx | 14 +- .../pbadms-sprt/ui}/text-input-dialog.tsx | 0 77 files changed, 4953 insertions(+), 872 deletions(-) create mode 100644 apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/edit/page.tsx create mode 100644 apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx create mode 100644 apps/as/app/(with-base-layout)/fnrs-cnsltn/page.tsx create mode 100644 apps/as/app/(with-base-layout)/fnrs-cnsltn/regist/page.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/index.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/business-cost-utils.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/checklist-utils.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/review-opinion-utils.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/use-checklist-form-state.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/model/columns.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/model/view-types.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-approval-dialog.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-tmplt-picker-dialog.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/fnrs-edit-page.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list-page.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/fnrs-regist-page.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/attachments-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/checklist-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/contact-info-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/supplement-demand-section.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx delete mode 100644 apps/as/src/pages/instl-sncn/lib/business-cost-form-utils.ts rename apps/as/src/pages/instl-sncn/lib/{cnfm-regist-schema.ts => instl-sncn-regist-schema.ts} (83%) create mode 100644 apps/as/src/shared/model/pbadms-sprt.ts create mode 100644 apps/as/src/widgets/bns-picker/index.ts rename apps/as/src/{pages/instl-sncn => widgets/bns-picker}/lib/to-bns-list-page-response.ts (100%) rename apps/as/src/{pages/instl-sncn => widgets/bns-picker}/model/bns-picker-columns.tsx (100%) rename apps/as/src/{pages/instl-sncn/ui/components => widgets/bns-picker/ui}/bns-picker-dialog.tsx (95%) create mode 100644 apps/as/src/widgets/pbadms-sprt/index.ts create mode 100644 apps/as/src/widgets/pbadms-sprt/lib/attachments-form-utils.ts create mode 100644 apps/as/src/widgets/pbadms-sprt/lib/business-cost-form-utils.ts create mode 100644 apps/as/src/widgets/pbadms-sprt/lib/business-scale-form-utils.ts rename apps/as/src/{pages/instl-sncn => widgets/pbadms-sprt}/lib/checklist-form-utils.ts (72%) rename apps/as/src/{pages/instl-sncn => widgets/pbadms-sprt}/lib/format-utils.ts (100%) rename apps/as/src/{pages/instl-sncn/ui/sections => widgets/pbadms-sprt/ui}/attachments-form-section.tsx (89%) rename apps/as/src/{pages/instl-sncn/ui/sections => widgets/pbadms-sprt/ui}/business-cost-form-section.tsx (77%) rename apps/as/src/{pages/instl-sncn/ui/sections => widgets/pbadms-sprt/ui}/business-scale-form-section.tsx (57%) rename apps/as/src/{pages/instl-sncn/ui/sections => widgets/pbadms-sprt/ui}/checklist-form-section.tsx (81%) rename apps/as/src/{pages/instl-sncn/ui/sections => widgets/pbadms-sprt/ui}/contact-info-form-section.tsx (86%) rename apps/as/src/{pages/instl-sncn/ui/sections => widgets/pbadms-sprt/ui}/privacy-consent-section.tsx (94%) rename apps/as/src/{pages/instl-sncn/ui/components => widgets/pbadms-sprt/ui}/section-collapsible.tsx (100%) rename apps/as/src/{pages/instl-sncn/ui/components => widgets/pbadms-sprt/ui}/task-se-picker-dialog.tsx (88%) rename apps/as/src/{pages/instl-sncn/ui/components => widgets/pbadms-sprt/ui}/text-input-dialog.tsx (100%) diff --git a/apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/edit/page.tsx b/apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/edit/page.tsx new file mode 100644 index 000000000..f1371598e --- /dev/null +++ b/apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/edit/page.tsx @@ -0,0 +1,10 @@ +import { FnrsEditPage } from "@/pages/fnrs-cnsltn"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function Page({ params }: PageProps) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx b/apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx new file mode 100644 index 000000000..ec9a3911e --- /dev/null +++ b/apps/as/app/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx @@ -0,0 +1,10 @@ +import { FnrsViewPage } from "@/pages/fnrs-cnsltn"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function Page({ params }: PageProps) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(with-base-layout)/fnrs-cnsltn/page.tsx b/apps/as/app/(with-base-layout)/fnrs-cnsltn/page.tsx new file mode 100644 index 000000000..734a0afd9 --- /dev/null +++ b/apps/as/app/(with-base-layout)/fnrs-cnsltn/page.tsx @@ -0,0 +1,3 @@ +import { FnrsListPage } from "@/pages/fnrs-cnsltn"; + +export default FnrsListPage; diff --git a/apps/as/app/(with-base-layout)/fnrs-cnsltn/regist/page.tsx b/apps/as/app/(with-base-layout)/fnrs-cnsltn/regist/page.tsx new file mode 100644 index 000000000..4360a6979 --- /dev/null +++ b/apps/as/app/(with-base-layout)/fnrs-cnsltn/regist/page.tsx @@ -0,0 +1,12 @@ +import { FnrsRegistPage } from "@/pages/fnrs-cnsltn"; +import { parseTaskSeComCdFromApi } from "@/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema"; + +interface PageProps { + searchParams: Promise<{ taskSeComCd?: string }>; +} + +export default async function Page({ searchParams }: PageProps) { + const params = await searchParams; + const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd); + return ; +} diff --git a/apps/as/src/entities/fnrs-cnsltn/api/queries.ts b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts index ac564f300..127e87e08 100644 --- a/apps/as/src/entities/fnrs-cnsltn/api/queries.ts +++ b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts @@ -12,7 +12,7 @@ import type { import { fnrsCnsltnMngService } from "./fnrs-cnsltn-service"; -const fnrsCnsltnMngBaseKey = [ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG] as const; +export const fnrsCnsltnMngBaseKey = [ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG] as const; export function useFnrsCnsltnMngPage(params: FnrsCnsltnMngSearchDto) { return useQuery({ diff --git a/apps/as/src/entities/fnrs-cnsltn/index.ts b/apps/as/src/entities/fnrs-cnsltn/index.ts index ca54cbfe6..274b38c23 100644 --- a/apps/as/src/entities/fnrs-cnsltn/index.ts +++ b/apps/as/src/entities/fnrs-cnsltn/index.ts @@ -1,4 +1,5 @@ export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service"; +export { fnrsCnsltnMngBaseKey } from "./api/queries"; export { useChangePrgrsSttsComCd, useDeleteFnrsCnsltnMng, diff --git a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts index 6dc5da4f4..0b1975ccd 100644 --- a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts +++ b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts @@ -1,61 +1,28 @@ -import type { PageableParams, PageResponse } from "@/shared/model"; +import type { + BizCstSaveBase, + BzScaleSaveBase, + CklstResultSaveBase, + DocPrgrsUpdateRequestDto, + EsntlAtchFileSaveBase, + PageableParams, + PageResponse, + PbadmsApplySaveHeader, + PbadmsMngDetResponseBase, + RvwOpnnResponseExtension, + RvwOpnnSaveBase, + SplmntDmndResponseExtension, + SplmntDmndSaveBase, +} from "@/shared/model"; -/** 백엔드 FnrsCnsltnMngResponseDto */ -export interface FnrsCnsltnMngResponseDto { - fnrsCnsltnDocNo: string; - admdstCd?: string; - envoSeComCd?: string; - envoSeCdNm?: string; - taskSeComCd?: string; - taskSeCdNm?: 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; - prgrsSttsComCd?: string; - prgrsSttsCdNm?: string; - regDt?: string; - rgtr?: string; - rgtrNm?: string; - mdfcnDt?: string; - sido?: string; - gugun?: string; -} - -/** 백엔드 FnrsCnsltnMngSearchDto + Pageable */ -export interface FnrsCnsltnMngSearchDto extends PageableParams { - 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; - prgrsSttsComCd?: string; -} +export type { + AtflMngClientResponseDto, + BizCstTreeDto, + BizSclTreeDto, + CklstRvwRsltTreeDto, + DocPrgrsUpdateRequestDto, + ElaprDocPrgrsClientResponseDto, + EsntlAtchFileMngTreeResponseDto, +} from "@/shared/model"; /** 백엔드 FnrsCnsltnMngSaveRequestDto */ export interface FnrsCnsltnMngSaveRequestDto { @@ -79,48 +46,76 @@ export interface FnrsCnsltnMngSaveRequestDto { dsgnrEmlAddr?: string; atchFileGroupId?: string; prgrsSttsComCd?: string; + rtlztnFcltSeComCd?: string; + rtlztnUsgSeComCd?: string; } /** 백엔드 FnrsCnsltnMngUpdateRequestDto */ -export interface FnrsCnsltnMngUpdateRequestDto { - 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; - prgrsSttsComCd?: string; +export type FnrsCnsltnMngUpdateRequestDto = Omit< + FnrsCnsltnMngSaveRequestDto, + "fnrsCnsltnDocNo" +>; + +/** 백엔드 FnrsCnsltnMngSearchDto + Pageable */ +export type FnrsCnsltnMngSearchDto = PageableParams & Partial; + +interface FnrsCnsltnMngResponseExtension { + fnrsCnsltnDocNo: string; + envoSeCdNm?: string; + taskSeCdNm?: string; + prgrsSttsCdNm?: string; + regDt?: string; + rgtr?: string; + rgtrNm?: string; + mdfcnDt?: string; + sido?: string; + gugun?: string; } +/** 백엔드 FnrsCnsltnMngResponseDto */ +export type FnrsCnsltnMngResponseDto = Omit & + FnrsCnsltnMngResponseExtension; + +export type BzScaleItem = BzScaleSaveBase; + +export type BizCstItem = BizCstSaveBase; + +export interface CklstResultItem extends CklstResultSaveBase { + selected?: string; +} + +export type EsntlAtchFileItem = EsntlAtchFileSaveBase; + +export type FnrsCnsltnSplmntDmndItem = SplmntDmndSaveBase; + +/** 백엔드 FnrsCnsltnRvwOpnnSaveRequestDto */ +export interface FnrsCnsltnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase { + fnrsCnsltnDocNo: string; + rvwOpnnSn: number; +} + +/** 백엔드 FnrsCnsltnRvwOpnnResponseDto */ +export type FnrsCnsltnRvwOpnnResponseDto = Partial< + Pick +> & + Omit & + RvwOpnnResponseExtension; + +/** 백엔드 FnrsCnsltnSplmntDmndResponseDto */ +export type FnrsCnsltnSplmntDmndResponseDto = FnrsCnsltnSplmntDmndItem & + SplmntDmndResponseExtension & { + fnrsCnsltnDocNo?: string; + }; + /** 백엔드 FnrsCnsltnMngDetResponseDto */ -export interface FnrsCnsltnMngDetResponseDto { - detDto?: FnrsCnsltnMngResponseDto; - bizSclList?: BizSclTreeDto[]; - bizCstList?: BizCstTreeDto[]; - cklstResultList?: CklstRvwRsltTreeDto[]; - esntlFileList?: EsntlAtchFileMngTreeResponseDto[]; - etcFileList?: AtflMngClientResponseDto[]; - rvwList?: FnrsCnsltnRvwOpnnResponseDto[]; - splmntDmndList?: FnrsCnsltnSplmntDmndResponseDto[]; - elaprDocPrgrsList?: ElaprDocPrgrsClientResponseDto[]; -} +export type FnrsCnsltnMngDetResponseDto = PbadmsMngDetResponseBase< + FnrsCnsltnMngResponseDto, + FnrsCnsltnRvwOpnnResponseDto, + FnrsCnsltnSplmntDmndResponseDto +>; /** 백엔드 FnrsCnsltnApplySaveRequestDto */ -export interface FnrsCnsltnApplySaveRequestDto { - techReViewSaveCd?: string; - rvwStpCd?: string; +export interface FnrsCnsltnApplySaveRequestDto extends PbadmsApplySaveHeader { fnrsCnsltnMng: FnrsCnsltnMngSaveRequestDto; bzScaleList?: BzScaleItem[]; bizCstList?: BizCstItem[]; @@ -131,137 +126,6 @@ export interface FnrsCnsltnApplySaveRequestDto { splmntDmndList?: FnrsCnsltnSplmntDmndItem[]; } -/** 백엔드 DocPrgrsUpdateRequestDto */ -export interface DocPrgrsUpdateRequestDto { - docNo?: string; - prgrsSttsComCd?: string; -} - -export interface BzScaleItem { - cnstpSeCd?: string; - bscPl?: number; - budgetReflct?: number; - thtiDsgn?: number; -} - -export interface BizCstItem { - acntgSeCd?: string; - bscPlanAmt?: number; - bgtRfltAmt?: number; - tstmDesignAmt?: number; -} - -export interface CklstResultItem { - cklstRvwSn?: number; - cklstArtclSn?: number; - cklstSn?: number; - rvwStpComCd: string; - selected?: string; - picRvwCn?: string; -} - -export interface EsntlAtchFileItem { - esntlAtchFileId?: string; - atchFileGroupId?: string; -} - -export interface FnrsCnsltnSplmntDmndItem { - splmntDmndSn?: number; - splmntArtclComCd?: string; - splmntDmndCn?: string; - splmntDmndDt?: string; - splmntDmndAtchFileGroupId?: string; - splmntCn?: string; - splmntPicNm?: string; - splmntCmptnDt?: string; - splmntAtchFileGroupId?: string; - cmptnYn?: string; -} - -export interface BizSclTreeDto { - cnstpSeCd?: string; - cnstpSeNm?: string; - bscPl?: number; - budgetReflct?: number; - thtiDsgn?: number; -} - -export interface BizCstTreeDto { - acntgSeCd?: string; - acntgSeNm?: string; - bscPlanAmt?: number; - bgtRfltAmt?: number; - tstmDesignAmt?: number; -} - -export interface CklstRvwRsltTreeDto { - cklstRvwSn?: number; - cklstArtclSn?: number; - cklstSn?: number; - rvwStpComCd?: string; - selected?: string; - picRvwCn?: string; - cklstNm?: string; - cklstArtclNm?: string; -} - -export interface EsntlAtchFileMngTreeResponseDto { - esntlAtchFileId?: string; - atchFileGroupId?: string; - esntlAtchFileNm?: string; - esntlAtchYn?: string; -} - -export interface AtflMngClientResponseDto { - atflId?: string; - orgnlFileNm?: string; - atflFlieSz?: number; - atflType?: string; - dwnldNocs?: number; - regDt?: string; -} - -export interface FnrsCnsltnRvwOpnnResponseDto { - fnrsCnsltnRvwOpnnSn?: number; - fnrsCnsltnDocNo?: string; - rvwStpComCd?: string; - rvwOpnn?: string; - rvwOpnnDt?: string; - rgtr?: string; - rgtrNm?: string; -} - -export interface FnrsCnsltnRvwOpnnSaveRequestDto { - fnrsCnsltnDocNo?: string; - rvwStpComCd?: string; - rvwOpnn?: string; -} - -export interface FnrsCnsltnSplmntDmndResponseDto { - splmntDmndSn?: number; - fnrsCnsltnDocNo?: string; - splmntArtclComCd?: string; - splmntDmndCn?: string; - splmntDmndDt?: string; - splmntDmndAtchFileGroupId?: string; - splmntCn?: string; - splmntPicNm?: string; - splmntCmptnDt?: string; - splmntAtchFileGroupId?: string; - cmptnYn?: string; -} - -export interface ElaprDocPrgrsClientResponseDto { - elaprDmndNo?: string; - elaprAplnNo?: string; - docNo?: string; - docPrgrsSn?: number; - prgrsSttsComCd?: string; - prgrsSttsCdNm?: string; - aprvrNm?: string; - aprvDt?: string; -} - /** Controller public 메서드명·시그니처와 동일 */ export interface FnrsCnsltnMngService { findPage(searchDto: FnrsCnsltnMngSearchDto): Promise>; diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts index 378d1e427..500cfb842 100644 --- a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts +++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts @@ -1,4 +1,32 @@ -import type { PageableParams, PageResponse } from "@/shared/model"; +import type { + AtflMngClientResponseDto, + BizCstSaveBase, + BizCstTreeDto, + BizSclTreeDto, + BzScaleSaveBase, + CklstResultSaveBase, + DocPrgrsUpdateRequestDto, + ElaprDocPrgrsClientResponseDto, + EsntlAtchFileMngTreeResponseDto, + EsntlAtchFileSaveBase, + PageableParams, + PageResponse, + PbadmsApplySaveHeader, + PbadmsMngDetResponseBase, + RvwOpnnResponseExtension, + RvwOpnnSaveBase, + SplmntDmndSaveBase, +} from "@/shared/model"; + +export type { + AtflMngClientResponseDto, + BizCstTreeDto, + BizSclTreeDto, + CklstRvwRsltTreeDto, + DocPrgrsUpdateRequestDto, + ElaprDocPrgrsClientResponseDto, + EsntlAtchFileMngTreeResponseDto, +} from "@/shared/model"; /** 행정구역(시도·시군구) 콤보 응답 DTO */ export interface AdstZoneResponseDto { @@ -70,16 +98,6 @@ export interface InstlSncnMngSaveRequestDto { /** 설치인가관리 수정 요청 DTO */ export type InstlSncnMngUpdateRequestDto = InstlSncnMngSaveRequestDto; -/** 문서 진행상태 수정 요청 DTO */ -export interface DocPrgrsUpdateRequestDto { - /** 행정지원문서번호 */ - docNo: string; - /** 문서구분코드 */ - docSeComCd: string; - /** 진행상태공통코드 */ - prgrsSttsComCd: string; -} - /** 설치인가관리 응답 DTO */ export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto { /** 설치인가문서번호 (필수로 재정의) */ @@ -111,7 +129,7 @@ export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto { } /** 설치인가 신청 - 사업규모 저장 DTO(배열 원소) */ -export interface InstlSncnBzScaleSaveDto { +export interface InstlSncnBzScaleSaveDto extends BzScaleSaveBase { /** 공종구분코드 */ cnstpSeCd: string; /** 기본계획수량 */ @@ -125,7 +143,7 @@ export interface InstlSncnBzScaleSaveDto { } /** 설치인가 신청 - 총사업비 저장 DTO(배열 원소) */ -export interface InstlSncnBizCstSaveRequestDto { +export interface InstlSncnBizCstSaveRequestDto extends BizCstSaveBase { /** 회계구분코드 */ acntgSeCd: string; /** 기본계획금액 */ @@ -137,15 +155,13 @@ export interface InstlSncnBizCstSaveRequestDto { } /** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */ -export interface InstlSncnCklstResultSaveDto { +export interface InstlSncnCklstResultSaveDto extends CklstResultSaveBase { /** 점검표일련번호 */ cklstSn: number; /** 점검표항목일련번호 */ cklstArtclSn: number; /** 점검표검토일련번호 */ cklstRvwSn: number; - /** 검토단계공통코드 */ - rvwStpComCd: string; /** 선택값(Y/N/null) */ selected: "Y" | "N" | null; /** 담당자검토내용(기술검토의견) */ @@ -153,7 +169,7 @@ export interface InstlSncnCklstResultSaveDto { } /** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */ -export interface InstlSncnEsntlAtchFileSaveDto { +export interface InstlSncnEsntlAtchFileSaveDto extends EsntlAtchFileSaveBase { /** 필수첨부파일아이디 */ esntlAtchFileId: string; /** 첨부파일그룹아이디 */ @@ -161,53 +177,16 @@ export interface InstlSncnEsntlAtchFileSaveDto { } /** 설치인가검토의견 저장 요청 DTO */ -export interface InstlSncnRvwOpnnSaveRequestDto { +export interface InstlSncnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase { /** 설치인가문서번호 */ instlSncnDocNo: string; - /** 검토의견일련번호 */ - rvwOpnnSn?: number; - /** 의견자구분공통코드 */ - opnrSeComCd?: string; - /** 의견자명 */ - opnrNm?: string; - /** 검토결과내용 */ - rvwRsltCn?: string; - /** 검토결과이상여부 */ - rvwRsltAnmlyYn?: string; - /** 첨부파일그룹아이디 */ - atchFileGroupId?: string; } /** 설치인가 보완요구 저장 DTO(배열 원소) */ -export interface InstlSncnSplmntDmndSaveDto { - /** 보완요구 일련번호 */ - splmntDmndSn?: number; - /** 보완항목 공통코드 */ - splmntArtclComCd?: string; - /** 보완요구 내용 */ - splmntDmndCn?: string; - /** 보완요구 일시 */ - splmntDmndDt?: string; - /** 보완요구 첨부파일 그룹 ID */ - splmntDmndAtchFileGroupId?: string; - /** 보완 내용 */ - splmntCn?: string; - /** 보완 담당자 명 */ - splmntPicNm?: string; - /** 보완완료 일시 */ - splmntCmptnDt?: string; - /** 보완 첨부파일 그룹 ID */ - splmntAtchFileGroupId?: string; - /** 완료 여부 */ - cmptnYn?: string; -} +export type InstlSncnSplmntDmndSaveDto = SplmntDmndSaveBase; /** 설치인가 신청 - 통합 저장 요청 DTO */ -export interface InstlSncnApplySaveRequestDto { - /** 기술검토저장구분(SV.저장, CP.검토완료) */ - techReViewSaveCd?: string; - /** 검토단계공통코드(ENV.환경청.EP, KECO.환경공단.TE) */ - rvwStpCd?: string; +export interface InstlSncnApplySaveRequestDto extends PbadmsApplySaveHeader { /** 설치인가관리 정보 */ instlSncnMng: InstlSncnMngSaveRequestDto; /** 사업규모 목록 */ @@ -228,184 +207,28 @@ export interface InstlSncnApplySaveRequestDto { export type ReviewStepCode = "LOCAL" | "ENV" | "KECO"; -/** 공종 트리와 사업규모(수량) 조합 DTO */ -export interface BizSclTreeDto { - /** 들여쓰기 적용된 공종명(시각적) */ - indCnstpSeCdNm: string; - /** 공종 코드 */ - cnstpSeCd: string; - /** 공종 코드명 */ - cnstpSeCdNm: string; - /** 사용 여부(Y/N) */ - useYn: string; - /** 트리 레벨(깊이) */ - treeLevel: number; - /** 트리 경로 표시용 문자열 */ - treePath: string; - /** 기본계획수량 */ - bscPlanQty: number; - /** 예산반영수량 */ - bgtRfltQty: number; - /** 금회설계수량 */ - tstmDesignQty: number; -} - -/** 회계구분 트리와 사업비용 조합 DTO */ -export interface BizCstTreeDto { - /** 들여쓰기 적용된 회계구분명(시각적) */ - indAcntgSeCdNm: string; - /** 회계구분 코드 */ - acntgSeCd: string; - /** 회계구분 코드명 */ - acntgSeCdNm: string; - /** 사용 여부(Y/N) */ - useYn: string; - /** 트리 레벨(깊이) */ - treeLevel: number; - /** 트리 경로 표시용 문자열 */ - treePath: string; - /** 기본계획금액 */ - bscPlanAmt: number; - /** 예산반영금액 */ - bgtRfltAmt: number; - /** 금회설계금액 */ - tstmDesignAmt: number; -} - -/** 점검표 검토결과 트리 DTO */ -export interface CklstRvwRsltTreeDto { - /** 점검표일련번호 */ - cklstSn: number; - /** 점검표명 */ - cklstNm: string; - /** 점검표항목일련번호 */ - cklstArtclSn: number; - /** 점검표항목명 */ - cklstArtclNm: string; - /** 검토일련번호 */ - cklstRvwSn: number; - /** 상위검토일련번호 */ - upCklstRvwSn: number | null; - /** 트리 레벨 */ - treeLevel: number; - /** 검토내용 */ - rvwCn: string; - /** 점검 사용 여부 */ - bchckUseYn: string; - /** 정렬순서 */ - sortSeq: number; - /** 업무구분코드 */ - taskSeComCd: string; - /** 검토단계공통코드 */ - rvwStpComCd: string; - /** 검토결과이상여부 */ - rvwRsltAnmlyYn: string; - /** 검토담당자 UUID */ - rvwPicUuid: string | null; - /** 담당자검토내용 */ - picRvwCn: string | null; - /** 검토일시 */ - rvwDt: string | null; -} - -/** 필수첨부파일관리 트리 조회 응답 DTO */ -export interface EsntlAtchFileMngTreeResponseDto { - /** 필수첨부파일아이디 */ - esntlAtchFileId: string; - /** 문서구분공통코드 */ - docSeComCd: string; - /** 필수첨부파일명 */ - esntlAtchFileNm: string; - /** 필수첨부여부 */ - esntlAtchYn: string; - /** 필수첨부파일비고 */ - esntlAtchFileRmrk: string | null; - /** 첨부파일 코드 */ - atflCd?: string; - /** 첨부파일 일련번호 */ - atflSn?: number; - /** 첨부파일 ID */ - atflId?: string; - /** 원본 파일명 */ - orgnlFileNm?: string; - /** 첨부파일 크기 */ - atflFlieSz?: number; - /** 첨부파일 타입 */ - atflType?: string; - /** 다운로드 횟수 */ - dwnldNocs?: number; - /** 등록일시 */ - regDt?: string; -} - -/** 첨부파일 응답 DTO (attach-service client) */ -export interface AtflMngClientResponseDto { - atflCd: string; - atflSn: number; - atflId: string; - physFileNm: string; - orgnlFileNm: string; - atflFlieSz: number; - atflType: string; - dwnldNocs: number; - delYn: string; - lnkgDmnId: string | null; - lnkgDmnNm: string | null; - regDt: string; -} - /** 설치인가검토의견 응답 DTO */ -export interface InstlSncnRvwOpnnResponseDto { - /** 설치인가문서번호 */ - instlSncnDocNo: string; - /** 검토의견일련번호 */ - rvwOpnnSn: number; - /** 의견자구분공통코드 */ - opnrSeComCd: string; - /** 의견자구분공통코드명 */ - opnrSeCdNm: string | null; - /** 의견자명 */ - opnrNm: string; - /** 검토결과내용 */ - rvwRsltCn: string; - /** 검토결과이상여부 */ - rvwRsltAnmlyYn: string; - /** 첨부파일그룹아이디 */ - atchFileGroupId: string | null; - /** 첨부파일 상세 정보 */ - atchFileInfo?: AtflMngClientResponseDto; - /** 등록일시 */ - regDt: string; -} - -/** 전자결재 진행상태 (클라이언트 응답용) */ -export interface ElaprDocPrgrsClientResponseDto { - elaprDocNo?: string; - elaprDmndId?: string; - elaprPrgrsId?: string; - atrzSeqNo?: number; - atrzSeComCd?: string; - atrzSeComNm?: string; - aprvrUuid?: string; - aprvrUuidNm?: string; - atrzPrgrsSttsComCd?: string; - atrzPrgrsSttsComNm?: string; - rjctRsn?: string | null; - atrzDt?: string | null; - jbpsSeNm?: string; - jbgdSeNm?: string; - ognzNm?: string; -} +export type InstlSncnRvwOpnnResponseDto = InstlSncnRvwOpnnSaveRequestDto & + RvwOpnnResponseExtension & { + instlSncnDocNo: string; + rvwOpnnSn: number; + opnrSeComCd: string; + opnrNm: string; + rvwRsltCn: string; + rvwRsltAnmlyYn: string; + atchFileGroupId: string | null; + opnrSeCdNm: string | null; + regDt: string; + }; /** 설치인가관리 상세 응답 DTO */ -export interface InstlSncnMngDetResponse { +export interface InstlSncnMngDetResponse + extends PbadmsMngDetResponseBase { detDto: InstlSncnMngResponseDto; bizSclList: BizSclTreeDto[]; bizCstList: BizCstTreeDto[]; - cklstResultList?: CklstRvwRsltTreeDto[]; esntlFileList: EsntlAtchFileMngTreeResponseDto[]; etcFileList: AtflMngClientResponseDto[]; - rvwList?: InstlSncnRvwOpnnResponseDto[]; elaprDocPrgrsList?: ElaprDocPrgrsClientResponseDto[] | null; } diff --git a/apps/as/src/pages/fnrs-cnsltn/index.ts b/apps/as/src/pages/fnrs-cnsltn/index.ts new file mode 100644 index 000000000..0193bd7c8 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/index.ts @@ -0,0 +1,5 @@ +export { FnrsEditPage } from "./ui/fnrs-edit-page"; +export { FnrsFormPage } from "./ui/fnrs-form-page"; +export { FnrsListPage } from "./ui/fnrs-list-page"; +export { FnrsRegistPage } from "./ui/fnrs-regist-page"; +export { FnrsViewPage } from "./ui/fnrs-view-page"; diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts new file mode 100644 index 000000000..d60d20b1c --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts @@ -0,0 +1,28 @@ +import type { ElaprDocPrgrsClientResponseDto } from "@/entities/fnrs-cnsltn"; + +export interface ApproverStatusConfig { + label: string; + badgeClass: string; +} + +export const APPROVER_STATUS_MAP: Record = { + ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" }, + ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-800" }, + ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800" }, + ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800" }, +}; + +export function sortByAtrzSeqNo( + list: ElaprDocPrgrsClientResponseDto[], +): ElaprDocPrgrsClientResponseDto[] { + return list.slice().sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0)); +} + +export function getApproverStatus(status: string | undefined): ApproverStatusConfig { + return ( + APPROVER_STATUS_MAP[status ?? ""] ?? { + label: status || "-", + badgeClass: "bg-gray-100 text-gray-700", + } + ); +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/business-cost-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/business-cost-utils.ts new file mode 100644 index 000000000..6f3c14851 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/business-cost-utils.ts @@ -0,0 +1,35 @@ +import type { BizCstTreeDto } from "@/entities/fnrs-cnsltn"; + +const ACNTG_TREE_PATH_SEPARATOR = "|"; + +/** treePath를 회계구분 코드 배열로 파싱 */ +export function parseAcntgTreePath(treePath: string): string[] { + return treePath.split(ACNTG_TREE_PATH_SEPARATOR).filter(Boolean); +} + +/** treePath에서 직계 부모 acntgSeCd 반환 (루트면 null) */ +export function getParentAcntgSeCd(node: Pick): string | null { + const segments = parseAcntgTreePath(node.treePath); + if (segments.length <= 1) return null; + return segments[segments.length - 2]; +} + +/** treePath 기준으로 bizCstList 정렬 (원본 불변) */ +export function sortBizCstListByTreePath(list: BizCstTreeDto[]): BizCstTreeDto[] { + return [...list].sort((a, b) => { + const byPath = a.treePath.localeCompare(b.treePath); + if (byPath !== 0) return byPath; + return a.acntgSeCd.localeCompare(b.acntgSeCd); + }); +} + +/** 자식 노드가 없는 leaf 여부 (treePath | 구분자 기준) */ +export function isBizCstLeafNode( + node: Pick, + allNodes: Pick[], +): boolean { + const childPrefix = `${node.treePath}${ACNTG_TREE_PATH_SEPARATOR}`; + return !allNodes.some( + (other) => other.acntgSeCd !== node.acntgSeCd && other.treePath.startsWith(childPrefix), + ); +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts new file mode 100644 index 000000000..3632b2a18 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -0,0 +1,133 @@ +export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = { + strg: "strg", + aply: "aply", + del: "del", + splmntCmptn: "splmntCmptn", + fnrsCnsltnAncmnt: "fnrsCnsltnAncmnt", + envoSbmsn: "envoSbmsn", + rjct: "rjct", + sncnAprv: "sncnAprv", + rvwRply: "rvwRply", + rqstTechRvw: "rqstTechRvw", + dsgntRvwr: "dsgntRvwr", + rvwStrg: "rvwStrg", + rvwCmptn: "rvwCmptn", + dmndSplmnt: "dmndSplmnt", + atrzAprv: "atrzAprv", + atrzRply: "atrzRply", + grndsChck: "grndsChck", +} as const; + +export type FnrsCnsltnViewButtonAction = + (typeof FNRS_CNSLTN_VIEW_BUTTON_ACTION)[keyof typeof FNRS_CNSLTN_VIEW_BUTTON_ACTION]; + +const ROLE_STATUS_BUTTON_ACTIONS: Record> = { + ROLE_USER05: { + "": [ + FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.del, + ], + SV: [ + FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.del, + ], + SL: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn], + TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt], + }, + ROLE_USER04: { + ST: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct], + EP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv], + }, + ROLE_USER03: { + LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw], + TP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply], + }, + ROLE_USER01: { + TE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.grndsChck], + SL: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn], + }, +}; + +const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const; + +const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record< + string, + Record +> = { + ER: { + ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr], + }, + TE: { + ATRZ02: [ + FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt, + ], + ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv], + ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply], + }, +}; + +function resolveButtonConfigs(actions: FnrsCnsltnViewButtonAction[]): FnrsCnsltnViewButtonAction[] { + return actions.map((action) => FNRS_CNSLTN_VIEW_BUTTON_ACTION[action]); +} + +function resolveRoleUser01AtrzButtonActions( + prgrsSttsComCd: string, + atrzSeComCdList: string[], +): FnrsCnsltnViewButtonAction[] { + const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd]; + if (!atrzStatusMap || atrzSeComCdList.length === 0) return []; + + const seen = new Set(); + const actions: FnrsCnsltnViewButtonAction[] = []; + + for (const atrzSeComCd of atrzSeComCdList) { + const mappedActions = atrzStatusMap[atrzSeComCd]; + if (!mappedActions) continue; + + for (const action of mappedActions) { + if (seen.has(action)) continue; + seen.add(action); + actions.push(action); + } + } + + return actions; +} + +export function needsRoleUser01AtrzResolve( + dtlInstCd: string | undefined, + prgrsSttsComCd: string, +): boolean { + return ( + dtlInstCd === "ROLE_USER01" && + ROLE_USER01_ATRZ_STATUS_CODES.includes( + prgrsSttsComCd as (typeof ROLE_USER01_ATRZ_STATUS_CODES)[number], + ) + ); +} + +export function getButtonConfigs( + dtlInstCd: string | undefined, + prgrsSttsComCd: string, + atrzSeComCdList?: string[], +): FnrsCnsltnViewButtonAction[] { + if (!dtlInstCd) return []; + + if (needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd)) { + return resolveButtonConfigs( + resolveRoleUser01AtrzButtonActions(prgrsSttsComCd, atrzSeComCdList ?? []), + ); + } + + const statusMap = ROLE_STATUS_BUTTON_ACTIONS[dtlInstCd]; + if (!statusMap) return []; + + const actions = statusMap[prgrsSttsComCd]; + if (!actions) return []; + + return resolveButtonConfigs(actions); +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/checklist-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/checklist-utils.ts new file mode 100644 index 000000000..c7ef294b9 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/checklist-utils.ts @@ -0,0 +1,178 @@ +import type { CklstRvwRsltTreeDto, CklstResultItem } from "@/entities/fnrs-cnsltn"; + +import { + type ChecklistFormState, + type ChecklistProgress, + type ChecklistRowKey, + type ChecklistStepMap, + type ReviewStepCode, + REVIEW_STEP_CODES, +} from "../model/view-types"; + +export function buildChecklistRowKey( + row: CklstRvwRsltTreeDto, + reviewStep?: ReviewStepCode, +): ChecklistRowKey { + const step = reviewStep ?? (row.rvwStpComCd as ReviewStepCode) ?? "LOCAL"; + return `${row.taskSeComCd}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${step}`; +} + +export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap { + const base: ChecklistStepMap = { LOCAL: [], ENV: [], KECO: [] }; + rows.forEach((row) => { + const step = (row.rvwStpComCd ?? "LOCAL") as ReviewStepCode; + if (REVIEW_STEP_CODES.includes(step)) base[step].push(row); + }); + return base; +} + +export function extractTaskCodes(rows: CklstRvwRsltTreeDto[]): string[] { + return Array.from(new Set(rows.map((row) => row.taskSeComCd).filter(Boolean))); +} + +export function filterChecklistRows( + rows: CklstRvwRsltTreeDto[], + taskCode: string | null, +): CklstRvwRsltTreeDto[] { + return rows.filter((row) => (taskCode ? row.taskSeComCd === taskCode : true)).slice(); +} + +export function getChecklistDisplayRows( + rows: CklstRvwRsltTreeDto[], + taskCode?: string | null, +): CklstRvwRsltTreeDto[] { + const filtered = filterChecklistRows(rows, taskCode ?? null); + const seen = new Set(); + const deduped: CklstRvwRsltTreeDto[] = []; + + for (const row of filtered) { + const dedupeKey = `${row.taskSeComCd}_${row.cklstRvwSn}`; + if (seen.has(dedupeKey)) continue; + seen.add(dedupeKey); + deduped.push(row); + } + + return deduped; +} + +export function computeChecklistProgress( + rows: CklstRvwRsltTreeDto[], + results: ChecklistFormState["results"], +): ChecklistProgress { + const activeRows = rows.filter((row) => row.bchckUseYn === "Y"); + const total = activeRows.length; + const completed = activeRows.reduce((acc, row) => { + const key = buildChecklistRowKey(row); + const val = results[key] ?? row.rvwRsltAnmlyYn ?? ""; + return acc + (val ? 1 : 0); + }, 0); + const percent = total > 0 ? Math.round((completed / total) * 100) : 0; + return { total, completed, percent }; +} + +export function filterChecklistRowsByReviewStep( + rows: CklstRvwRsltTreeDto[], + reviewStep: ReviewStepCode, +): CklstRvwRsltTreeDto[] { + return rows.filter((row) => row.rvwStpComCd === reviewStep); +} + +export function computeReviewStepChecklistProgress( + rows: CklstRvwRsltTreeDto[], + results: ChecklistFormState["results"], + reviewStep: ReviewStepCode, +): ChecklistProgress { + const activeRows = getChecklistDisplayRows(rows).filter((row) => row.bchckUseYn === "Y"); + const total = activeRows.length; + const completed = activeRows.reduce((acc, row) => { + const key = buildChecklistRowKey(row, reviewStep); + const val = results[key]; + return acc + (val === "Y" || val === "N" ? 1 : 0); + }, 0); + const percent = total > 0 ? Math.round((completed / total) * 100) : 0; + return { total, completed, percent }; +} + +export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistFormState { + const results: ChecklistFormState["results"] = {}; + const comments: ChecklistFormState["comments"] = {}; + rows.forEach((row) => { + const key = buildChecklistRowKey(row); + if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") { + results[key] = row.rvwRsltAnmlyYn; + } + if (row.picRvwCn) comments[key] = row.picRvwCn; + }); + return { + results, + comments, + progress: computeChecklistProgress(rows, results), + }; +} + +export function mergeChecklistFormStateFromServer( + prev: ChecklistFormState, + allRows: CklstRvwRsltTreeDto[], +): ChecklistFormState { + const results = { ...prev.results }; + const comments = { ...prev.comments }; + + for (const row of allRows) { + const step = row.rvwStpComCd as ReviewStepCode | undefined; + if (!step || !REVIEW_STEP_CODES.includes(step)) continue; + + const key = buildChecklistRowKey(row); + if ((row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") && results[key] === undefined) { + results[key] = row.rvwRsltAnmlyYn; + } + if (row.picRvwCn && !comments[key]) { + comments[key] = row.picRvwCn; + } + } + + return { + results, + comments, + progress: computeChecklistProgress(getChecklistDisplayRows(allRows), results), + }; +} + +export function patchChecklistFormState( + prev: ChecklistFormState, + patch: Partial>, + allRows: CklstRvwRsltTreeDto[], + reviewStep?: ReviewStepCode, +): ChecklistFormState { + const results = patch.results ?? prev.results; + const comments = patch.comments ?? prev.comments; + const progress = reviewStep + ? computeReviewStepChecklistProgress(allRows, results, reviewStep) + : computeChecklistProgress(getChecklistDisplayRows(allRows), results); + + return { + results, + comments, + progress, + }; +} + +export function toCklstSaveDto( + rows: CklstRvwRsltTreeDto[], + formState: ChecklistFormState, + reviewStep: ReviewStepCode, +): CklstResultItem[] { + return getChecklistDisplayRows(rows) + .filter((row) => row.bchckUseYn === "Y") + .map((row) => { + const key = buildChecklistRowKey(row, reviewStep); + const selected = formState.results[key] ?? null; + return { + cklstSn: row.cklstSn, + cklstArtclSn: row.cklstArtclSn, + cklstRvwSn: row.cklstRvwSn, + rvwStpComCd: reviewStep, + selected, + picRvwCn: formState.comments[key] ?? null, + }; + }); +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts new file mode 100644 index 000000000..789a56993 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts @@ -0,0 +1,87 @@ +import { z } from "zod"; + +export const fnrsCnsltnRegistSchema = z.object({ + fnrsCnsltnMng: z.object({ + 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().min(1, "설계자 전화번호는 필수입니다"), + dsgnrOgdpNm: z.string().min(1, "설계자 소속은 필수입니다"), + dsgnrEmlAddr: z + .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(), + 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(), + }), + bzScaleList: z.array( + z.object({ + cnstpSeCd: z.string(), + bscPl: z.number(), + budgetReflct: z.number(), + thtiDsgn: z.number(), + diff: z.number().optional(), + }), + ), + bizCstList: z.array( + z.object({ + acntgSeCd: z.string(), + bscPlanAmt: z.number(), + bgtRfltAmt: z.number(), + tstmDesignAmt: z.number(), + }), + ), + cklstResultList: z.array( + z.object({ + cklstSn: z.number(), + cklstArtclSn: z.number(), + cklstRvwSn: z.number(), + rvwStpComCd: z.string(), + selected: z.enum(["Y", "N"]).nullable(), + picRvwCn: z.string().nullable().optional(), + }), + ), + esntlAtchFileList: z.array( + z.object({ + esntlAtchFileId: z.string(), + atchFileGroupId: z.string(), + }), + ), + privacyConsent: z.enum(["Y", "N"], { + required_error: "개인정보 수집/이용 동의는 필수입니다", + }), +}); + +export type FnrsCnsltnRegistFormData = z.infer; + +export function formatTaskSeComCdForApi(taskSeComCd: string[]): string { + return taskSeComCd.join(","); +} + +export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] { + if (!taskSeComCd) return []; + return taskSeComCd.split(",").filter(Boolean); +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts b/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts new file mode 100644 index 000000000..e23da5444 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts @@ -0,0 +1,130 @@ +import type { IUserInfo } from "@/shared/store/user-store"; +import type { FnrsCnsltnMngDetResponseDto } from "@/entities/fnrs-cnsltn"; + +import type { FnrsCnsltnRegistFormData } from "./fnrs-cnsltn-regist-schema"; +import { parseTaskSeComCdFromApi } from "./fnrs-cnsltn-regist-schema"; + +function mapSelectedValue(value?: string | null): "Y" | "N" | null { + if (value === "Y" || value === "N") return value; + return null; +} + +function mapEsntlAtchFileList(esntlFileList: FnrsCnsltnMngDetResponseDto["esntlFileList"]) { + const esntlMap = new Map(); + + for (const item of esntlFileList ?? []) { + if (!item.esntlAtchFileId || !item.atflCd) continue; + esntlMap.set(item.esntlAtchFileId, item.atflCd); + } + + return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ + esntlAtchFileId, + atchFileGroupId, + })); +} + +export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsltnRegistFormData { + const detDto = data.detDto; + if (!detDto) { + throw new Error("상세 정보(detDto)가 없습니다."); + } + + const bizSclList = data.bizSclList ?? []; + const bizCstList = data.bizCstList ?? []; + const cklstResultList = data.cklstResultList; + const esntlFileList = data.esntlFileList; + + return { + fnrsCnsltnMng: { + fnrsCnsltnDocNo: detDto.fnrsCnsltnDocNo, + bizNm: detDto.bizNm ?? "", + bizBgngYr: detDto.bizBgngYr ?? "", + bizEndYr: detDto.bizEndYr ?? "", + bizCn: detDto.bizCn ?? "", + taskSeComCd: parseTaskSeComCdFromApi(detDto.taskSeComCd), + cfmtnBgtBizNo: detDto.cfmtnBgtBizNo, + bgtAplyYr: detDto.bgtAplyYr, + dsgnrNm: detDto.dsgnrNm ?? "", + dsgnrTelno: detDto.dsgnrTelno ?? "", + dsgnrOgdpNm: detDto.dsgnrOgdpNm ?? "", + dsgnrEmlAddr: detDto.dsgnrEmlAddr ?? "", + aplcntNm: detDto.aplcntNm ?? "", + aplcntMblTelno: detDto.aplcntMblTelno ?? "", + aplcntOgdpNm: detDto.aplcntOgdpNm ?? "", + aplcntEmlAddr: detDto.aplcntEmlAddr ?? "", + admdstCd: detDto.admdstCd, + envoSeComCd: detDto.envoSeComCd, + rtlztnFcltSeComCd: detDto.rtlztnFcltSeComCd ?? "", + rtlztnUsgSeComCd: detDto.rtlztnUsgSeComCd ?? "", + 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, + bscPl: item.bscPlanQty, + budgetReflct: item.bgtRfltQty, + thtiDsgn: item.tstmDesignQty, + diff: item.tstmDesignQty - item.bgtRfltQty, + })), + bizCstList: bizCstList.map((item) => ({ + acntgSeCd: item.acntgSeCd, + bscPlanAmt: item.bscPlanAmt, + bgtRfltAmt: item.bgtRfltAmt, + tstmDesignAmt: item.tstmDesignAmt, + })), + cklstResultList: + cklstResultList?.map((item) => ({ + cklstSn: item.cklstSn, + cklstArtclSn: item.cklstArtclSn, + cklstRvwSn: item.cklstRvwSn, + rvwStpComCd: item.rvwStpComCd, + selected: mapSelectedValue(item.rvwRsltAnmlyYn), + picRvwCn: item.picRvwCn, + })) ?? [], + esntlAtchFileList: mapEsntlAtchFileList(esntlFileList), + privacyConsent: "Y", + }; +} + +export function getCreateDefaultValues( + userInfo: IUserInfo | null, + taskSeComCd?: string[], +): FnrsCnsltnRegistFormData { + return { + fnrsCnsltnMng: { + bizNm: "", + bizBgngYr: "", + bizEndYr: "", + bizCn: "", + taskSeComCd: taskSeComCd ?? [], + cfmtnBgtBizNo: "", + bgtAplyYr: "", + dsgnrNm: "", + dsgnrTelno: "", + dsgnrOgdpNm: "", + dsgnrEmlAddr: "", + aplcntNm: userInfo?.userName ?? "", + aplcntMblTelno: userInfo?.mphn ?? "", + aplcntOgdpNm: userInfo?.ogdpSeNm ?? "", + aplcntEmlAddr: userInfo?.email ?? "", + prgrsSttsComCd: "SV", + atchFileGroupId: "", + rtlztnFcltSeComCd: "", + rtlztnUsgSeComCd: "", + }, + bzScaleList: [], + bizCstList: [], + cklstResultList: [], + esntlAtchFileList: [], + privacyConsent: "N", + }; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/review-opinion-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/review-opinion-utils.ts new file mode 100644 index 000000000..d1d2e2352 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/review-opinion-utils.ts @@ -0,0 +1,45 @@ +import type { + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnRvwOpnnSaveRequestDto, +} from "@/entities/fnrs-cnsltn"; + +export interface ReviewOpinionFormData { + rvwRsltCn: string; + rvwRsltAnmlyYn: "Y" | "N" | ""; + atchFileGroupId: string; + opnrSeComCd: string; + rvwOpnnSn?: number; +} + +export function toRvwOpnnSaveDto( + data: FnrsCnsltnMngDetResponseDto, + reviewOpinionFormData: ReviewOpinionFormData | null | undefined, + opnrNm?: string, +): FnrsCnsltnRvwOpnnSaveRequestDto[] | undefined { + if (!reviewOpinionFormData || !data.detDto) return undefined; + + const hasContent = + reviewOpinionFormData.rvwRsltCn.trim() || + reviewOpinionFormData.rvwRsltAnmlyYn || + reviewOpinionFormData.atchFileGroupId; + + if (!hasContent) return undefined; + + const existingSn = + reviewOpinionFormData.rvwOpnnSn ?? + data.rvwList?.find((item) => item.opnrSeComCd === reviewOpinionFormData.opnrSeComCd) + ?.rvwOpnnSn ?? + 1; + + return [ + { + fnrsCnsltnDocNo: data.detDto.fnrsCnsltnDocNo, + rvwOpnnSn: existingSn, + opnrSeComCd: reviewOpinionFormData.opnrSeComCd, + opnrNm: opnrNm ?? "", + rvwRsltCn: reviewOpinionFormData.rvwRsltCn, + rvwRsltAnmlyYn: reviewOpinionFormData.rvwRsltAnmlyYn || undefined, + atchFileGroupId: reviewOpinionFormData.atchFileGroupId || undefined, + }, + ]; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts new file mode 100644 index 000000000..bce365857 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts @@ -0,0 +1,44 @@ +import { + PBADMS_PROGRESS_ORDER, + REVIEW_STEP_CODES, + REVIEW_STEP_ROLE_RANK, + type ReviewStepCode, + ROLE_TO_REVIEW_STEP, +} from "../model/view-types"; + +export function getProgressStepCount(prgrsSttsComCd?: string): number { + const idx = PBADMS_PROGRESS_ORDER.indexOf( + prgrsSttsComCd as (typeof PBADMS_PROGRESS_ORDER)[number], + ); + if (idx <= PBADMS_PROGRESS_ORDER.indexOf("SV")) return 1; + if (idx <= PBADMS_PROGRESS_ORDER.indexOf("LE")) return 2; + return 3; +} + +export function getRoleStepCount(dtlInstCd?: string): number { + const mapped = dtlInstCd ? ROLE_TO_REVIEW_STEP[dtlInstCd] : undefined; + return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0; +} + +export function getVisibleReviewSteps( + prgrsSttsComCd: string, + userRoleCode?: string, +): ReviewStepCode[] { + const count = Math.min(getProgressStepCount(prgrsSttsComCd), getRoleStepCount(userRoleCode)); + return REVIEW_STEP_CODES.slice(0, count); +} + +export function getEditableReviewStep( + dtlInstCd?: string, + prgrsSttsComCd?: string, +): ReviewStepCode | undefined { + if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) { + return "ENV"; + } + + if (dtlInstCd === "ROLE_USER01" && prgrsSttsComCd === "TE") { + return "KECO"; + } + + return undefined; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts new file mode 100644 index 000000000..43775d839 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts @@ -0,0 +1,148 @@ +import type { + BizCstItem, + BizCstTreeDto, + BizSclTreeDto, + BzScaleItem, + CklstResultItem, + EsntlAtchFileItem, + EsntlAtchFileMngTreeResponseDto, + FnrsCnsltnApplySaveRequestDto, + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnMngResponseDto, + FnrsCnsltnSplmntDmndItem, +} from "@/entities/fnrs-cnsltn"; + +import type { ChecklistFormState, ReviewStepCode } from "../model/view-types"; + +import { toCklstSaveDto } from "./checklist-utils"; +import { type ReviewOpinionFormData, toRvwOpnnSaveDto } from "./review-opinion-utils"; + +export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] { + return bizSclList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + cnstpSeCd: node.cnstpSeCd, + bscPl: node.bscPlanQty, + budgetReflct: node.bgtRfltQty, + thtiDsgn: node.tstmDesignQty, + })); +} + +export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] { + return bizCstList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + acntgSeCd: node.acntgSeCd, + bscPlanAmt: node.bscPlanAmt, + bgtRfltAmt: node.bgtRfltAmt, + tstmDesignAmt: node.tstmDesignAmt, + })); +} + +export function toEsntlAtchFileSaveDto( + esntlFileList: EsntlAtchFileMngTreeResponseDto[], +): EsntlAtchFileItem[] { + const esntlMap = new Map(); + + for (const item of esntlFileList) { + if (!item.esntlAtchFileId || !item.atflCd) continue; + esntlMap.set(item.esntlAtchFileId, item.atflCd); + } + + return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ + esntlAtchFileId, + atchFileGroupId, + })); +} + +export function buildSavePayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, +): FnrsCnsltnApplySaveRequestDto { + const detDto = data.detDto; + if (!detDto) { + throw new Error("상세 정보(detDto)가 없습니다."); + } + + const bizSclList = data.bizSclList ?? []; + const bizCstList = data.bizCstList ?? []; + const cklstResultList = data.cklstResultList ?? []; + const esntlFileList = data.esntlFileList ?? []; + + return { + fnrsCnsltnMng: { ...detDto, prgrsSttsComCd }, + bzScaleList: toBzScaleSaveDto(bizSclList), + bizCstList: toBizCstSaveDto(bizCstList), + cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), + esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), + }; +} + +export function buildTechReviewReplyPayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, + techReViewSaveCd: string, + reviewOpinionFormData?: ReviewOpinionFormData | null, + opnrNm?: string, +): FnrsCnsltnApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), + techReViewSaveCd, + rvwStpCd: reviewStep, + rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), + }; +} + +export function buildSupplementReqPayload( + data: FnrsCnsltnMngDetResponseDto, + splmntDmndCn: string, +): FnrsCnsltnApplySaveRequestDto { + const detDto = data.detDto; + if (!detDto) { + throw new Error("상세 정보(detDto)가 없습니다."); + } + + const nextSn = + (data.splmntDmndList?.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) ?? 0) + 1; + + const splmntDmndList: FnrsCnsltnSplmntDmndItem[] = [ + ...(data.splmntDmndList ?? []), + { + splmntDmndSn: nextSn, + splmntDmndCn, + cmptnYn: "N", + }, + ]; + + return { + fnrsCnsltnMng: detDto, + splmntDmndList, + }; +} + +export function buildSupplementSubmitPayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: data.splmntDmndList, + }; +} + +export function buildTechReviewPayload( + det: FnrsCnsltnMngResponseDto, + cklstResultList: CklstResultItem[], +): FnrsCnsltnApplySaveRequestDto { + return { + fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" }, + bzScaleList: [], + bizCstList: [], + cklstResultList, + esntlAtchFileList: [], + }; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/use-checklist-form-state.ts b/apps/as/src/pages/fnrs-cnsltn/lib/use-checklist-form-state.ts new file mode 100644 index 000000000..6adca3b74 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/use-checklist-form-state.ts @@ -0,0 +1,61 @@ +import { useCallback, useEffect, useState } from "react"; + +import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn"; + +import { + type ChecklistFormState, + DEFAULT_CHECKLIST_PROGRESS, + type ReviewStepCode, +} from "../model/view-types"; + +import { mergeChecklistFormStateFromServer, patchChecklistFormState } from "./checklist-utils"; + +const EMPTY_CKLST_RESULT_LIST: CklstRvwRsltTreeDto[] = []; + +function isSameChecklistFormState(a: ChecklistFormState, b: ChecklistFormState): boolean { + if ( + a.progress.total !== b.progress.total || + a.progress.completed !== b.progress.completed || + a.progress.percent !== b.progress.percent + ) { + return false; + } + + const aResultKeys = Object.keys(a.results); + if (aResultKeys.length !== Object.keys(b.results).length) return false; + if (!aResultKeys.every((key) => a.results[key] === b.results[key])) return false; + + const aCommentKeys = Object.keys(a.comments); + if (aCommentKeys.length !== Object.keys(b.comments).length) return false; + + return aCommentKeys.every((key) => a.comments[key] === b.comments[key]); +} + +function useChecklistFormState(cklstResultList: CklstRvwRsltTreeDto[] = EMPTY_CKLST_RESULT_LIST) { + const [formState, setFormState] = useState({ + results: {}, + comments: {}, + progress: DEFAULT_CHECKLIST_PROGRESS, + }); + + useEffect(() => { + setFormState((prev) => { + const next = mergeChecklistFormStateFromServer(prev, cklstResultList); + return isSameChecklistFormState(prev, next) ? prev : next; + }); + }, [cklstResultList]); + + const patchFormState = useCallback( + ( + patch: Partial>, + reviewStep?: ReviewStepCode, + ) => { + setFormState((prev) => patchChecklistFormState(prev, patch, cklstResultList, reviewStep)); + }, + [cklstResultList], + ); + + return { formState, setFormState, patchFormState }; +} + +export { EMPTY_CKLST_RESULT_LIST, useChecklistFormState }; diff --git a/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx b/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx new file mode 100644 index 000000000..ba660c342 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { Fragment } from "react"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import { dayjs } from "@/shared/lib"; +import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn"; +import { Badge } from "@common/components"; + +const columnHelper = createColumnHelper(); + +const MAIN_STEP_LABELS = { + save: "저장", + apply: "신청완료", + techReview: "기술검토", + reply: "검토결과 회신", +} as const; + +const TECH_REVIEW_STATUS_CODES = new Set(["LE", "ER", "EE", "TE", "TS", "TR", "TP"]); + +type BadgeState = "completed" | "current" | "pending" | "action"; + +interface ProgressBadge { + label: string; + state: BadgeState; +} + +function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] { + const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview; + const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply; + + if (prgrsSttsComCd === "SV") { + return [ + { label: MAIN_STEP_LABELS.save, state: "current" }, + { label: MAIN_STEP_LABELS.apply, state: "pending" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; + } + + if (prgrsSttsComCd === "ST") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "current" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; + } + + if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: techReviewLabel, state: "action" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; + } + + if (prgrsSttsComCd === "EP") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.techReview, state: "completed" }, + { label: replyLabel, state: "action" }, + ]; + } + + if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.techReview, state: "completed" }, + { label: MAIN_STEP_LABELS.reply, state: "completed" }, + ]; + } + + return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }]; +} + +function getBadgeColor(state: BadgeState) { + switch (state) { + case "completed": + return "green"; + case "current": + return "blue"; + case "pending": + return "lightGray"; + case "action": + return "orange"; + } +} + +function formatOptional(value: string | undefined) { + return value?.trim() ? value : "-"; +} + +interface ProgressStepBadgeProps { + prgrsSttsComCd: string; + prgrsSttsCdNm: string; +} + +function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) { + const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm); + + return ( +
+ {badges.map((badge, index) => ( + + {index > 0 && >} + {badge.label} + + ))} +
+ ); +} + +export function createFnrsCnsltnColumns() { + return [ + columnHelper.accessor("sido", { + header: "시도", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("gugun", { + header: "시군구", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("bizNm", { + header: "사업명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("taskSeCdNm", { + header: "업무구분", + meta: { align: "center" }, + cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd), + }), + columnHelper.accessor("aplcntNm", { + header: "담당자", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("envoSeCdNm", { + header: "해당기관", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.display({ + id: "progress", + header: "진행상황", + meta: { align: "center" }, + cell: ({ row }) => ( + + ), + }), + columnHelper.accessor("regDt", { + header: "신청일", + meta: { align: "center" }, + cell: ({ getValue }) => { + const value = getValue(); + return value ? dayjs(value).format("YYYY-MM-DD") : "-"; + }, + }), + ]; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/model/view-types.ts b/apps/as/src/pages/fnrs-cnsltn/model/view-types.ts new file mode 100644 index 000000000..db60bd696 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/model/view-types.ts @@ -0,0 +1,86 @@ +import type { + AtflMngClientResponseDto, + BizCstTreeDto, + BizSclTreeDto, + CklstRvwRsltTreeDto, + ElaprDocPrgrsClientResponseDto, + EsntlAtchFileMngTreeResponseDto, + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnMngResponseDto, + FnrsCnsltnRvwOpnnResponseDto, +} from "@/entities/fnrs-cnsltn"; + +export type ReviewStepCode = "LOCAL" | "ENV" | "KECO"; + +export type { + AtflMngClientResponseDto, + BizCstTreeDto, + BizSclTreeDto, + CklstRvwRsltTreeDto, + ElaprDocPrgrsClientResponseDto, + EsntlAtchFileMngTreeResponseDto, + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnMngResponseDto, + FnrsCnsltnRvwOpnnResponseDto, +}; + +export type ChecklistRowKey = string; + +export interface ChecklistProgress { + total: number; + completed: number; + percent: number; +} + +export const DEFAULT_CHECKLIST_PROGRESS: ChecklistProgress = { + total: 0, + completed: 0, + percent: 0, +}; + +export interface ChecklistFormState { + results: Record; + comments: Record; + progress: ChecklistProgress; +} + +export type ChecklistStepMap = Record; + +export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"]; + +export const REVIEW_STEP_LABELS: Record = { + LOCAL: "지자체", + ENV: "환경청", + KECO: "환경공단", +}; + +export const TASK_CODE_LABELS: Record = { + RE: "재이용수", + SP: "하수관로", + SF: "하수처리시설", +}; + +export const PBADMS_PROGRESS_ORDER = [ + "SV", + "SL", + "TA", + "ST", + "EP", + "LE", + "TP", + "ER", + "TE", + "TB", +] as const; + +export const ROLE_TO_REVIEW_STEP: Record = { + ROLE_USER05: "LOCAL", + ROLE_USER03: "ENV", + ROLE_USER01: "KECO", +}; + +export const REVIEW_STEP_ROLE_RANK: Record = { + LOCAL: 1, + ENV: 2, + KECO: 3, +}; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx new file mode 100644 index 000000000..4e0100089 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx @@ -0,0 +1,164 @@ +import type { Dispatch, SetStateAction } from "react"; + +import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn"; +import { + cn, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Textarea, +} from "@common/components"; + +import { buildChecklistRowKey, patchChecklistFormState } from "../../lib/checklist-utils"; +import type { ReviewStepCode } from "../../model/view-types"; +import { type ChecklistFormState, ROLE_TO_REVIEW_STEP } from "../../model/view-types"; + +interface ChecklistTaskPanelProps { + selectedReviewStep: ReviewStepCode; + selectedRows: CklstRvwRsltTreeDto[]; + allRows: CklstRvwRsltTreeDto[]; + formState: ChecklistFormState; + setFormState: Dispatch>; + userRoleCode?: string; +} + +function ChecklistTaskPanel({ + selectedReviewStep, + selectedRows, + allRows, + formState, + setFormState, + userRoleCode, +}: ChecklistTaskPanelProps) { + const currentUserReviewStep = userRoleCode ? ROLE_TO_REVIEW_STEP[userRoleCode] : undefined; + const isStepEditable = currentUserReviewStep === selectedReviewStep; + + return ( + <> +
+ + + + + 검토항목 + + 주요 검토내용 + 검토 결과 + {selectedReviewStep !== "LOCAL" ? ( + + 기술검토 의견 + + ) : null} + + + 예 + 아니오 + + + + {selectedRows.length === 0 ? ( + + + 조회된 체크리스트 항목이 없습니다. + + + ) : ( + selectedRows.map((row) => { + const key = buildChecklistRowKey(row, selectedReviewStep); + const isFieldVisible = row.bchckUseYn === "Y"; + const isLocalReviewStep = selectedReviewStep === "LOCAL"; + + return ( + + {row.cklstArtclNm} + + {row.rvwCn} + + {isFieldVisible && ( + <> + + { + if (!isStepEditable) return; + setFormState((prev) => + patchChecklistFormState( + prev, + { results: { ...prev.results, [key]: "Y" } }, + allRows, + selectedReviewStep, + ), + ); + }} + className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")} + /> + + + { + if (!isStepEditable) return; + setFormState((prev) => + patchChecklistFormState( + prev, + { results: { ...prev.results, [key]: "N" } }, + allRows, + selectedReviewStep, + ), + ); + }} + className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")} + /> + + + )} + {!isLocalReviewStep && isFieldVisible && ( + +