From f662250e1afcf23ba4e607e0e1f162769ea0e6af Mon Sep 17 00:00:00 2001 From: jangwoong Date: Mon, 26 Oct 2026 16:39:18 +0900 Subject: [PATCH] =?UTF-8?q?[as=20fr]=20=EC=84=A4=EC=B9=98=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=EC=97=90=20=EB=B3=91=EB=A0=AC=EA=B2=B0=EC=9E=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=98=95=ED=83=9C=EB=A1=9C=20frontend=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elapr/ui/elapr-tmplt-picker-dialog.tsx | 4 +- .../src/pages/instl-sncn/lib/button-config.ts | 309 +++++++++++------- .../lib/instl-sncn-regist-schema.ts | 194 ++++++----- .../ui/components/floating-action-buttons.tsx | 115 ++++++- .../pages/instl-sncn/ui/instl-view-page.tsx | 6 +- 5 files changed, 401 insertions(+), 227 deletions(-) diff --git a/apps/as/src/entities/elapr/ui/elapr-tmplt-picker-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-tmplt-picker-dialog.tsx index cbfd53e2d..3dd80de68 100644 --- a/apps/as/src/entities/elapr/ui/elapr-tmplt-picker-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-tmplt-picker-dialog.tsx @@ -56,7 +56,7 @@ function ElaprTmpltPickerDialog({ const [aplnRows, setAplnRows] = useState([]); const [isEditingLocked, setIsEditingLocked] = useState(false); - const isForkJoinMode = docSeComCd === "FNRS" && taskSeComCd === "SF_SP"; + const isForkJoinMode = taskSeComCd === "SF_SP"; const tmpltQuery = useElaprTmpltPage({ page: 0, @@ -174,7 +174,7 @@ function ElaprTmpltPickerDialog({ const defaultTmplt: ElaprTmplt = selectedTmplt ?? { elaprTmpltId: "TMPLT_SF_SP_FORKJOIN", elaprTmpltNm: "[복합] 시설+관로 병렬 결재선 (6인)", - docSeComCd: "FNRS", + docSeComCd: docSeComCd || "FNRS", }; const payload: ElaprTmpltPickerConfirmPayload = { diff --git a/apps/as/src/pages/instl-sncn/lib/button-config.ts b/apps/as/src/pages/instl-sncn/lib/button-config.ts index b19609a06..826fa498a 100644 --- a/apps/as/src/pages/instl-sncn/lib/button-config.ts +++ b/apps/as/src/pages/instl-sncn/lib/button-config.ts @@ -1,37 +1,37 @@ -export const INSTL_SNCN_VIEW_BUTTON_ACTION = { - strg: "strg", - aply: "aply", - del: "del", - instlSncnAncmnt: "instlSncnAncmnt", - rjct: "rjct", - sncnAprv: "sncnAprv", - rvwRply: "rvwRply", - rqstTechRvw: "rqstTechRvw", - dsgntRvwr: "dsgntRvwr", - rvwStrg: "rvwStrg", - rvwCmptn: "rvwCmptn", - dmndSplmnt: "dmndSplmnt", - atrzAprv: "atrzAprv", - atrzRply: "atrzRply", -} as const; - -export type InstlSncnViewButtonAction = - (typeof INSTL_SNCN_VIEW_BUTTON_ACTION)[keyof typeof INSTL_SNCN_VIEW_BUTTON_ACTION]; - -const ROLE_STATUS_BUTTON_ACTIONS: Record> = { - ROLE_USER05: { - "": [ - INSTL_SNCN_VIEW_BUTTON_ACTION.strg, - INSTL_SNCN_VIEW_BUTTON_ACTION.aply, - INSTL_SNCN_VIEW_BUTTON_ACTION.del, - ], - SV: [ - INSTL_SNCN_VIEW_BUTTON_ACTION.strg, - INSTL_SNCN_VIEW_BUTTON_ACTION.aply, - INSTL_SNCN_VIEW_BUTTON_ACTION.del, - ], - TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt], - }, +export const INSTL_SNCN_VIEW_BUTTON_ACTION = { + strg: "strg", + aply: "aply", + del: "del", + instlSncnAncmnt: "instlSncnAncmnt", + rjct: "rjct", + sncnAprv: "sncnAprv", + rvwRply: "rvwRply", + rqstTechRvw: "rqstTechRvw", + dsgntRvwr: "dsgntRvwr", + rvwStrg: "rvwStrg", + rvwCmptn: "rvwCmptn", + dmndSplmnt: "dmndSplmnt", + atrzAprv: "atrzAprv", + atrzRply: "atrzRply", +} as const; + +export type InstlSncnViewButtonAction = + (typeof INSTL_SNCN_VIEW_BUTTON_ACTION)[keyof typeof INSTL_SNCN_VIEW_BUTTON_ACTION]; + +const ROLE_STATUS_BUTTON_ACTIONS: Record> = { + ROLE_USER05: { + "": [ + INSTL_SNCN_VIEW_BUTTON_ACTION.strg, + INSTL_SNCN_VIEW_BUTTON_ACTION.aply, + INSTL_SNCN_VIEW_BUTTON_ACTION.del, + ], + SV: [ + INSTL_SNCN_VIEW_BUTTON_ACTION.strg, + INSTL_SNCN_VIEW_BUTTON_ACTION.aply, + INSTL_SNCN_VIEW_BUTTON_ACTION.del, + ], + TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt], + }, // 환경청 단계 제거: 기존 환경청(LE·TP) 기능을 광역자치단체가 수행 ROLE_USER04: { ST: [ @@ -42,96 +42,151 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record -> = { - ER: { - ATRZ01: [INSTL_SNCN_VIEW_BUTTON_ACTION.dsgntRvwr], - }, - TE: { - ATRZ02: [ - INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg, - INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn, - INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt, - ], - ATRZ03: [INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv], - ATRZ99: [INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply], - }, -}; - -function resolveButtonConfigs(actions: InstlSncnViewButtonAction[]): InstlSncnViewButtonAction[] { - return actions.map((action) => INSTL_SNCN_VIEW_BUTTON_ACTION[action]); -} - -interface GetButtonConfigsOptions { - hasPendingApprovalRequest?: boolean; -} - -function resolveRoleUser01AtrzButtonActions( - prgrsSttsComCd: string, - atrzSeComCdList: string[], - options?: GetButtonConfigsOptions, -): InstlSncnViewButtonAction[] { - if (prgrsSttsComCd === "TE" && options?.hasPendingApprovalRequest === false) { - return []; - } - - const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd]; - if (!atrzStatusMap || atrzSeComCdList.length === 0) return []; - - const seen = new Set(); - const actions: InstlSncnViewButtonAction[] = []; - - for (const atrzSeComCd of atrzSeComCdList) { - const mappedActions = atrzStatusMap[atrzSeComCd]; - if (!mappedActions) continue; - - for (const action of mappedActions) { - if (seen.has(action)) continue; - seen.add(action); - actions.push(action); - } - } - - return actions; -} - -export function needsRoleUser01AtrzResolve( - dtlInstCd: string | undefined, - prgrsSttsComCd: string, -): boolean { - return ( - dtlInstCd === "ROLE_USER01" && - ROLE_USER01_ATRZ_STATUS_CODES.includes( - prgrsSttsComCd as (typeof ROLE_USER01_ATRZ_STATUS_CODES)[number], - ) - ); -} - -export function getButtonConfigs( - dtlInstCd: string | undefined, - prgrsSttsComCd: string, - atrzSeComCdList?: string[], - options?: GetButtonConfigsOptions, -): InstlSncnViewButtonAction[] { - if (!dtlInstCd) return []; - - if (needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd)) { - return resolveButtonConfigs( - resolveRoleUser01AtrzButtonActions(prgrsSttsComCd, atrzSeComCdList ?? [], options), - ); - } - - const statusMap = ROLE_STATUS_BUTTON_ACTIONS[dtlInstCd]; - if (!statusMap) return []; - - const actions = statusMap[prgrsSttsComCd]; - if (!actions) return []; - - return resolveButtonConfigs(actions); -} +}; + +const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const; + +const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record< + string, + Record +> = { + ER: { + ATRZ01: [INSTL_SNCN_VIEW_BUTTON_ACTION.dsgntRvwr], + }, + TE: { + ATRZ02: [ + INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg, + INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn, + INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt, + ], + ATRZ03: [INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv], + ATRZ99: [INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply], + }, +}; + +/** + * Fork-Join (병렬 결재 6인 체제: 관로2 + 시설2 + 정책병합2) 전용 상태별 버튼 액션 매핑 + * - Stage 2: 관로/시설 실무검토자(ATRZ02), 부서승인자(ATRZ03) + * - Stage 3: 정책총괄 종합병합자(ATRZ04), 최종회신자(ATRZ99) + * - 일반 순차 결재 라인과 분리하여 독립적으로 관리 + */ +const ROLE_USER01_FORK_JOIN_ATRZ_STATUS_ACTIONS: Record< + string, + Record +> = { + ER: { + ATRZ01: [INSTL_SNCN_VIEW_BUTTON_ACTION.dsgntRvwr], + }, + TE: { + ATRZ02: [ + INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg, + INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn, + INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt, + ], + ATRZ03: [INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply], + ATRZ04: [INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv], + ATRZ99: [INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply], + }, + TP: {}, +}; + +function resolveButtonConfigs(actions: InstlSncnViewButtonAction[]): InstlSncnViewButtonAction[] { + return actions.map((action) => INSTL_SNCN_VIEW_BUTTON_ACTION[action]); +} + +const PENDING_APPROVAL_ACTIONS = new Set([ + INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn, + INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv, + INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply, +]); + +export interface GetButtonConfigsOptions { + hasPendingApprovalRequest?: boolean; + isForkJoin?: boolean; + branchSeCd?: string; + branchSttsCd?: string; +} + +function resolveRoleUser01AtrzButtonActions( + prgrsSttsComCd: string, + atrzSeComCdList: string[], + options?: GetButtonConfigsOptions, +): InstlSncnViewButtonAction[] { + let effectiveStatus = prgrsSttsComCd; + + if (options?.isForkJoin && options?.branchSttsCd) { + switch (options.branchSttsCd) { + case "SUPPLEMENT": + // 본인 브랜치가 보완요청 중이면 검토/승인 버튼 숨김 + return []; + case "COMPLETED": + // 본인 브랜치 승인 완료 후 타 브랜치 대기 중이면 검토/승인 버튼 숨김 + return []; + case "WAITING": + return []; + case "IN_PROGRESS": + effectiveStatus = "TE"; + break; + } + } + + const atrzStatusMap = options?.isForkJoin + ? ROLE_USER01_FORK_JOIN_ATRZ_STATUS_ACTIONS[effectiveStatus] + : ROLE_USER01_ATRZ_STATUS_ACTIONS[effectiveStatus]; + if (!atrzStatusMap || atrzSeComCdList.length === 0) return []; + + const seen = new Set(); + const actions: InstlSncnViewButtonAction[] = []; + + 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.filter((action) => { + const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action); + if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false; + return true; + }); +} + +export function needsRoleUser01AtrzResolve( + dtlInstCd: string | undefined, + prgrsSttsComCd: string, +): boolean { + return ( + dtlInstCd === "ROLE_USER01" && + ROLE_USER01_ATRZ_STATUS_CODES.includes( + prgrsSttsComCd as (typeof ROLE_USER01_ATRZ_STATUS_CODES)[number], + ) + ); +} + +export function getButtonConfigs( + dtlInstCd: string | undefined, + prgrsSttsComCd: string, + atrzSeComCdList?: string[], + options?: GetButtonConfigsOptions, +): InstlSncnViewButtonAction[] { + if (!dtlInstCd) return []; + + if (needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd)) { + return resolveButtonConfigs( + resolveRoleUser01AtrzButtonActions(prgrsSttsComCd, atrzSeComCdList ?? [], options), + ); + } + + const statusMap = ROLE_STATUS_BUTTON_ACTIONS[dtlInstCd]; + if (!statusMap) return []; + + const actions = statusMap[prgrsSttsComCd]; + if (!actions) return []; + + return resolveButtonConfigs(actions); +} diff --git a/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts b/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts index 0315e61a5..ff0792ca9 100644 --- a/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts +++ b/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts @@ -1,86 +1,110 @@ -import { z } from "zod"; - -import { - instlSncnBizCstItemSchema, - instlSncnBzScaleItemSchema, - instlSncnCklstResultItemSchema, - instlSncnEsntlAtchFileItemSchema, - instlSncnMngSaveSchema, - instlSncnSplmntDmndItemSchema, -} from "@/entities/instl-sncn"; - -export const instlSncnRegistSchema = z.object({ - instlSncnMng: instlSncnMngSaveSchema.extend({ - bizNm: z.string().min(1, "사업명은 필수입니다"), - bizBgngYr: z - .string() - .min(1, "사업시작연도는 필수입니다") - .regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"), - bizEndYr: z - .string() - .min(1, "사업종료연도는 필수입니다") - .regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"), - bizCn: z.string().min(1, "사업내용은 필수입니다"), - taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"), - sncnSeComCd: z.string().min(1, "사업구분은 필수입니다"), - dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"), - dsgnrTelno: z - .string() - .min(1, "설계자 전화번호는 필수입니다") - .regex(/^[0-9-]+$/, "숫자와 하이픈(-)만 입력 가능합니다"), - dsgnrOgdpNm: z.string().min(1, "설계자 소속은 필수입니다"), - dsgnrEmlAddr: z - .string() - .min(1, "설계자 이메일은 필수입니다") - .email("올바른 이메일 형식이 아닙니다"), - instlSncnDocNo: z.string().nullish(), - prgrsSttsComCd: z.string().nullish(), - sido: z.string().nullish(), - gugun: z.string().nullish(), - }), - bzScaleList: z.array( - instlSncnBzScaleItemSchema.extend({ - cnstpSeCd: z.string(), - bscPl: z.number(), - budgetReflct: z.number(), - thtiDsgn: z.number(), - diff: z.number(), - }), - ), - bizCstList: z.array( - instlSncnBizCstItemSchema.extend({ - acntgSeCd: z.string(), - bscPlanAmt: z.number(), - bgtRfltAmt: z.number(), - tstmDesignAmt: z.number(), - }), - ), +import { z } from "zod"; + +import { + instlSncnBizCstItemSchema, + instlSncnBzScaleItemSchema, + instlSncnCklstResultItemSchema, + instlSncnEsntlAtchFileItemSchema, + instlSncnMngSaveSchema, + instlSncnSplmntDmndItemSchema, +} from "@/entities/instl-sncn"; + +export const instlSncnRegistSchema = z.object({ + instlSncnMng: instlSncnMngSaveSchema.extend({ + bizNm: z.string().min(1, "사업명은 필수입니다"), + bizBgngYr: z + .string() + .min(1, "사업시작연도는 필수입니다") + .regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"), + bizEndYr: z + .string() + .min(1, "사업종료연도는 필수입니다") + .regex(/^\d{4}$/, "연도는 4자리 숫자여야 합니다"), + bizCn: z.string().min(1, "사업내용은 필수입니다"), + taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"), + sncnSeComCd: z.string().min(1, "사업구분은 필수입니다"), + dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"), + dsgnrTelno: z + .string() + .min(1, "설계자 전화번호는 필수입니다") + .regex(/^[0-9-]+$/, "숫자와 하이픈(-)만 입력 가능합니다"), + dsgnrOgdpNm: z.string().min(1, "설계자 소속은 필수입니다"), + dsgnrEmlAddr: z + .string() + .min(1, "설계자 이메일은 필수입니다") + .email("올바른 이메일 형식이 아닙니다"), + instlSncnDocNo: z.string().nullish(), + prgrsSttsComCd: z.string().nullish(), + sido: z.string().nullish(), + gugun: z.string().nullish(), + }), + bzScaleList: z.array( + instlSncnBzScaleItemSchema.extend({ + cnstpSeCd: z.string(), + bscPl: z.number(), + budgetReflct: z.number(), + thtiDsgn: z.number(), + diff: z.number(), + }), + ), + bizCstList: z.array( + instlSncnBizCstItemSchema.extend({ + acntgSeCd: z.string(), + bscPlanAmt: z.number(), + bgtRfltAmt: z.number(), + tstmDesignAmt: z.number(), + }), + ), // 설치인가 화면에서 체크리스트 미사용: 숨겨진 값 때문에 신청·저장 검증이 막히지 않도록 필수값을 두지 않음 - cklstResultList: z.array(instlSncnCklstResultItemSchema), - esntlAtchFileList: z.array( - instlSncnEsntlAtchFileItemSchema.extend({ - esntlAtchFileId: z.string(), - atchFileGroupId: z.string(), - }), - ), - splmntDmndList: z.array( - instlSncnSplmntDmndItemSchema.extend({ - splmntArtclCdNm: z.string().nullish(), - splmntDmndAtchFileNm: z.string().nullish(), - }), - ), - privacyConsent: z.enum(["Y", "N"], { - required_error: "개인정보 수집/이용 동의는 필수입니다", - }), -}); - -export type InstlSncnRegistFormData = z.infer; - -export function formatTaskSeComCdForApi(taskSeComCd: string[]): string { - return taskSeComCd.join(","); -} - -export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] { - if (!taskSeComCd) return []; - return taskSeComCd.split(",").filter(Boolean); -} + cklstResultList: z.array(instlSncnCklstResultItemSchema), + esntlAtchFileList: z.array( + instlSncnEsntlAtchFileItemSchema.extend({ + esntlAtchFileId: z.string(), + atchFileGroupId: z.string(), + }), + ), + splmntDmndList: z.array( + instlSncnSplmntDmndItemSchema.extend({ + splmntArtclCdNm: z.string().nullish(), + splmntDmndAtchFileNm: z.string().nullish(), + }), + ), + privacyConsent: z.enum(["Y", "N"], { + required_error: "개인정보 수집/이용 동의는 필수입니다", + }), +}); + +export type InstlSncnRegistFormData = z.infer; + +export function formatTaskSeComCdForApi(taskSeComCd: string[]): string { + return taskSeComCd.join(","); +} + +export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] { + if (!taskSeComCd) return []; + return taskSeComCd.split(",").filter(Boolean); +} + +/** + * 설치인가 저장 시 코드가 "SF,SP", "SP,SF" 등의 복수 코드로 분리 저장된 경우, + * 전자결재 복합병렬결재(Fork-Join) 화면 및 프로세스 규격에 맞춰 "SF_SP"로 정규화합니다. + */ +export function normalizeTaskSeComCdForElapr(taskSeComCd?: string | null): string { + if (!taskSeComCd) return ""; + const trimmed = taskSeComCd.trim(); + if (trimmed === "SF_SP") return "SF_SP"; + + const codes = trimmed.split(",").map((c) => c.trim()).filter(Boolean); + if (codes.includes("SF") && codes.includes("SP")) { + return "SF_SP"; + } + return trimmed; +} + +/** + * 전자결재 복합병렬결재(Fork-Join) 대상 업무 여부 판단 + */ +export function isForkJoinTask(taskSeComCd?: string | null): boolean { + return normalizeTaskSeComCdForElapr(taskSeComCd) === "SF_SP"; +} + diff --git a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx index 3fe70d66c..cf3c94584 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx @@ -17,6 +17,7 @@ import { type ElaprTmpltPickerConfirmPayload, ElaprTmpltPickerDialog, hasCurrentUserPendingApprovalRequest, + useElaprBranchesByDocNo, useElaprDocPrgrsByDocNo, } from "@/entities/elapr"; import { @@ -41,6 +42,10 @@ import { buildTechReviewReplyPayload, toSplmntDmndSaveItem, } from "../../lib/save-payload-utils"; +import { + isForkJoinTask, + normalizeTaskSeComCdForElapr, +} from "../../lib/instl-sncn-regist-schema"; import { type ChecklistFormState } from "../../model/view-types"; import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section"; @@ -82,31 +87,94 @@ function FloatingActionButtons({ const docNo = data.detDto.instlSncnDocNo; const isRegister = data.detDto.rgtr === userInfo?.userUnqId; + const isForkJoin = isForkJoinTask(data.detDto.taskSeComCd); + const normalizedTaskSeComCd = normalizeTaskSeComCdForElapr(data.detDto.taskSeComCd); + const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE"; const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } = useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null); + const { data: elaprBranchList = [], isLoading: isElaprBranchLoading } = useElaprBranchesByDocNo( + isForkJoin && needsTeApprovalCheck ? docNo : null, + ); + const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest( elaprDocPrgrsList, userInfo?.userUnqId, ); + const currentUserPrgrsRow = elaprDocPrgrsList.find( + (row) => row.aprvrUuid?.toLowerCase() === userInfo?.userUnqId?.toLowerCase(), + ); + const userPendingPrgrs = elaprDocPrgrsList.find( + (row) => + row.aprvrUuid?.toLowerCase() === userInfo?.userUnqId?.toLowerCase() && + row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_002", + ); + const userBranchId = userPendingPrgrs?.branchId ?? currentUserPrgrsRow?.branchId; + const userBranchSeCd = (userPendingPrgrs?.branchSeCd ?? currentUserPrgrsRow?.branchSeCd) as + | "SP" + | "SF" + | "TOTAL_AGG" + | "MAIN" + | undefined; + const userPendingBranchSeCd = userPendingPrgrs?.branchSeCd ?? userBranchSeCd; + const userPendingAtrzSeComCd = userPendingPrgrs?.atrzSeComCd; + + const effectiveAtrzSeComCdList = useMemo(() => { + if (isForkJoin && userPendingAtrzSeComCd) { + return [userPendingAtrzSeComCd]; + } + if (isForkJoin && currentUserPrgrsRow?.atrzSeComCd) { + return [currentUserPrgrsRow.atrzSeComCd]; + } + return atrzSeComCdList; + }, [atrzSeComCdList, currentUserPrgrsRow?.atrzSeComCd, isForkJoin, userPendingAtrzSeComCd]); + + const userBranchSttsCd = useMemo(() => { + if (!isForkJoin || !userBranchSeCd) return undefined; + + const matchedBranch = userBranchId + ? elaprBranchList.find((b) => b.branchId === userBranchId) + : elaprBranchList.findLast((b) => b.branchSeCd === userBranchSeCd); + if (matchedBranch?.branchSttsCd) { + return matchedBranch.branchSttsCd; + } + + if (prgrsSttsComCd === "TE") { + return "IN_PROGRESS"; + } + if (prgrsSttsComCd === "TP") { + return "COMPLETED"; + } + + return undefined; + }, [elaprBranchList, isForkJoin, prgrsSttsComCd, userBranchId, userBranchSeCd]); + const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return []; - return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, { + if (isForkJoin && isElaprBranchLoading) return []; + return getButtonConfigs(dtlInstCd, prgrsSttsComCd, effectiveAtrzSeComCdList, { hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined, + isForkJoin, + branchSeCd: userBranchSeCd, + branchSttsCd: userBranchSttsCd, }); }, [ - atrzSeComCdList, dtlInstCd, + effectiveAtrzSeComCdList, hasPendingApprovalRequest, + isElaprBranchLoading, isElaprDocPrgrsLoading, + isForkJoin, isRvwLoginUserLoading, needsAtrzResolve, needsTeApprovalCheck, prgrsSttsComCd, + userBranchSeCd, + userBranchSttsCd, ]); const canEdit = @@ -300,6 +368,9 @@ function FloatingActionButtons({ elaprDocNo: docNo, dmndTtl: data.detDto.bizNm ?? "", dmndCn: data.detDto.bizCn ?? "", + taskSeComCd: normalizedTaskSeComCd || undefined, + forkJoinSeCd: isForkJoin ? "FORK_JOIN" : "NONE", + crntStageNo: isForkJoin ? 2 : 1, elaprCycl: 1, docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows), @@ -443,16 +514,33 @@ function FloatingActionButtons({ 보완요청 )} - {shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv) && ( - - )} - {shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply) && ( - - )} + {shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv) && + (!isForkJoin || + (userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ04")) && ( + + )} + {shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply) && + (!isForkJoin || + (userPendingBranchSeCd !== "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ03") || + (userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ99")) && ( + + )} @@ -460,6 +548,7 @@ function FloatingActionButtons({ open={activeDialog === "elaprTmplt"} onClose={() => setActiveDialog(null)} docSeComCd={docSeComCd} + taskSeComCd={normalizedTaskSeComCd || undefined} onConfirm={handleElaprTmpltConfirm} /> @@ -476,6 +565,8 @@ function FloatingActionButtons({ onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)} elaprDocNo={data.detDto.instlSncnDocNo} currentUserUuid={userInfo?.userUnqId ?? ""} + elaprCycl={1} + taskSeComCd={normalizedTaskSeComCd} onSuccess={() => { queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(instlSncnDocNo), diff --git a/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx b/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx index 727c9036d..2f61916f5 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx @@ -17,6 +17,7 @@ import { import { Button, cn, Skeleton } from "@common/components"; import { needsRoleUser01AtrzResolve } from "../lib/button-config"; +import { normalizeTaskSeComCdForElapr } from "../lib/instl-sncn-regist-schema"; import { getEditableReviewStep } from "../lib/review-step-utils"; import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state"; import { PROGRESS_STATUS_COLUMNS } from "../model/columns"; @@ -119,7 +120,10 @@ function InstlViewPage({ id }: InstlViewPageProps) { /> - +