[as fr] 설치인가에 병렬결재 추가 형태로 frontend 추가

This commit is contained in:
2026-10-26 16:39:18 +09:00
parent c3711c2609
commit f662250e1a
5 changed files with 401 additions and 227 deletions
@@ -56,7 +56,7 @@ function ElaprTmpltPickerDialog({
const [aplnRows, setAplnRows] = useState<ElaprAplnRow[]>([]);
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 = {
+182 -127
View File
@@ -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<string, Record<string, InstlSncnViewButtonAction[]>> = {
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<string, Record<string, InstlSncnViewButtonAction[]>> = {
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<string, Record<string, InstlSncnViewBut
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
},
};
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
string,
Record<string, InstlSncnViewButtonAction[]>
> = {
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<InstlSncnViewButtonAction>();
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<string, InstlSncnViewButtonAction[]>
> = {
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<string, InstlSncnViewButtonAction[]>
> = {
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<InstlSncnViewButtonAction>([
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<InstlSncnViewButtonAction>();
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);
}
@@ -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<typeof instlSncnRegistSchema>;
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<typeof instlSncnRegistSchema>;
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";
}
@@ -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<string | undefined>(() => {
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({
보완요청
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
결재(승인)
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
결재(회신)
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv) &&
(!isForkJoin ||
(userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ04")) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
{isForkJoin ? "결재 (병합)" : "결재(승인)"}
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply) &&
(!isForkJoin ||
(userPendingBranchSeCd !== "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ03") ||
(userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ99")) && (
<Button
color={
isForkJoin && userPendingBranchSeCd !== "TOTAL_AGG"
? "green"
: undefined
}
onClick={() => setActiveDialog("elaprApproval")}
disabled={isProcessing}
>
{isForkJoin
? userPendingBranchSeCd === "TOTAL_AGG"
? "결재 (환경청에 회신)"
: "결재 (병합에 회신)"
: "결재(회신)"}
</Button>
)}
</div>
</div>
@@ -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),
@@ -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) {
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
<ElaprDocPrgrsStatus
elaprDocNo={detDto.instlSncnDocNo}
taskSeComCd={normalizeTaskSeComCdForElapr(detDto.taskSeComCd)}
/>
<BusinessScaleSection bizSclList={bizSclList} />
<BusinessCostSection bizCstList={bizCstList} />
<AttachmentsViewSection