[as fr] 병합자 조치요청 작성 버튼에 추가및 TB 단계 추가

This commit is contained in:
2026-10-28 20:04:34 +09:00
parent 33653278cd
commit 94d79908fb
6 changed files with 149 additions and 16 deletions
@@ -68,6 +68,14 @@ async function requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Pro
return response.data;
}
async function tbActnRqst(docNo: string): Promise<void> {
await client.put<void>(`${BASE}/${docNo}/tb-actn-rqst`);
}
async function tbSubmitActnResult(docNo: string, body: { isSubmit: boolean }): Promise<void> {
await client.post<void>(`${BASE}/${docNo}/tb-submit-actn-result`, body);
}
async function submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(`${BASE}/${id}/submitsupplement`, body);
return response.data;
@@ -111,6 +119,8 @@ export const fnrsCnsltnMngService = {
techReview,
requestExamine,
requestActnReview,
tbActnRqst,
tbSubmitActnResult,
submitSupplement,
submitReqSupplement,
remove,
@@ -140,6 +140,8 @@ export interface FnrsCnsltnMngService {
techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<void>;
requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string>;
requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string>;
tbActnRqst(docNo: string): Promise<void>;
tbSubmitActnResult(docNo: string, body: { isSubmit: boolean }): Promise<void>;
submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
submitReqSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
remove(fnrsCnsltnDocNo: string): Promise<void>;
@@ -22,6 +22,8 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
actnCmptn: "actnCmptn",
rqstActnRvw: "rqstActnRvw",
costVrfc: "costVrfc",
tbActnRqst: "tbActnRqst",
tbSubmitActnRslt: "tbSubmitActnRslt",
} as const;
export type FnrsCnsltnViewButtonAction =
@@ -40,6 +42,10 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
FNRS_CNSLTN_VIEW_BUTTON_ACTION.del,
],
RB: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng, FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn],
TB: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbSubmitActnRslt,
],
RA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
@@ -71,6 +77,7 @@ const ROLE_USER01_ATRZ_STATUS_CODES = [
"ER",
"SL",
"TE",
"TB",
"TP",
"RB",
"RA",
@@ -108,6 +115,7 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbActnRqst,
],
ATRZ03: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
@@ -118,6 +126,13 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
],
},
TB: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ04: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
TP: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
@@ -229,12 +244,20 @@ const ROLE_USER01_FORK_JOIN_ATRZ_STATUS_ACTIONS: Record<
ATRZ04: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbActnRqst,
],
ATRZ99: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
],
},
TB: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ04: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
TP: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
@@ -340,6 +363,7 @@ const PENDING_APPROVAL_ACTIONS = new Set<FnrsCnsltnViewButtonAction>([
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbActnRqst,
]);
function resolveRoleUser01AtrzButtonActions(
@@ -349,7 +373,10 @@ function resolveRoleUser01AtrzButtonActions(
): FnrsCnsltnViewButtonAction[] {
let effectiveStatus = prgrsSttsComCd;
if (options?.isForkJoin && options?.branchSttsCd) {
// TB는 문서 단위 상태이고 결재 브랜치는 IN_PROGRESS로 남는다. TE 맵으로 되돌리지 않는다.
if (prgrsSttsComCd === "TB") {
effectiveStatus = "TB";
} else if (options?.isForkJoin && options?.branchSttsCd) {
switch (options.branchSttsCd) {
case "SUPPLEMENT":
// 본인 브랜치가 보완요청 중이면 'SL' 맵핑 적용 (검토/승인 버튼 숨김, 실무협의서관리만 노출)
@@ -415,7 +442,12 @@ function resolveRoleUser01AtrzButtonActions(
}
if (options?.hasCompletedDraft != null) {
if (effectiveStatus === "SL" || effectiveStatus === "TE" || effectiveStatus === "RK") {
if (
effectiveStatus === "SL" ||
effectiveStatus === "TE" ||
effectiveStatus === "TB" ||
effectiveStatus === "RK"
) {
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) {
return options.hasCompletedDraft;
}
@@ -82,6 +82,7 @@ export const PBADMS_PROGRESS_ORDER = [
// KECO
"ER",
"TE",
"TB",
"EV",
"RK",
] as const;
@@ -146,10 +146,11 @@ function FloatingActionButtons({
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
const needsElaprPrgrsCheck =
needsAtrzResolve &&
(["TE", "RK"].includes(prgrsSttsComCd) || (isForkJoin && prgrsSttsComCd === "SL"));
(["TE", "TB", "RK"].includes(prgrsSttsComCd) || (isForkJoin && prgrsSttsComCd === "SL"));
const needsPwrkDraftCheck =
dtlInstCd === "ROLE_USER01" && ["SL", "TE", "RK"].includes(prgrsSttsComCd);
const needsPwrkForRb = dtlInstCd === "ROLE_USER05" && prgrsSttsComCd === "RB";
dtlInstCd === "ROLE_USER01" && ["SL", "TE", "TB", "RK"].includes(prgrsSttsComCd);
const needsPwrkForRb =
dtlInstCd === "ROLE_USER05" && (prgrsSttsComCd === "RB" || prgrsSttsComCd === "TB");
const needsPwrkForAiRetry = dtlInstCd === "ROLE_USER01" && ["TE", "EV"].includes(prgrsSttsComCd);
const needsPwrkLookup = needsPwrkDraftCheck || needsPwrkForRb || needsPwrkForAiRetry;
const needsAiAutoLatest = dtlInstCd === "ROLE_USER01" && AI_RETRY_PRGRS_STTS.has(prgrsSttsComCd);
@@ -384,14 +385,22 @@ function FloatingActionButtons({
await handler({
request,
data,
onSuccess: () => {
onSuccess: async () => {
if (successMessage) alert(successMessage);
queryClient.invalidateQueries({
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo),
});
queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH],
});
await Promise.all([
queryClient.invalidateQueries({
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo),
refetchType: "all",
}),
queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH],
refetchType: "all",
}),
queryClient.invalidateQueries({
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND],
refetchType: "all",
}),
]);
},
onError: (error) => {
let errorMessage = "";
@@ -771,6 +780,37 @@ function FloatingActionButtons({
});
}
async function handleTbActnRqstClick() {
if (
!(await confirm(
"지자체에 조치요청하시겠습니까?\n조치요청 후 7일 내 제출하지 않으면 자동으로 결재가 다음 단계로 진행됩니다.",
))
) {
return;
}
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbActnRqst,
successMessage: "조치요청이 완료되었습니다.",
});
}
async function handleTbSubmitActnRsltClick() {
if (!pwrkCnsltnDocNo) {
alert("실무협의 문서번호를 찾을 수 없습니다.");
return;
}
if (!(await confirm("조치결과를 제출하시겠습니까?"))) return;
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbSubmitActnRslt,
request: {
pwrkCnsltnDocNo,
body: { isSubmit: true } satisfies PwrkActnResultSubmitDto,
},
successMessage: "조치결과가 제출되었습니다.",
});
}
function handleFileVrfcClick() {
reTransferDocument.mutate(
{ fnrsCnsltnDocNo: docNo },
@@ -940,6 +980,17 @@ function FloatingActionButtons({
조치 완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbActnRqst) &&
prgrsSttsComCd !== "TB" && (
<Button color="orange" onClick={handleTbActnRqstClick} disabled={isBusy}>
조치요청
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.tbSubmitActnRslt) && (
<Button color="green" onClick={handleTbSubmitActnRsltClick} disabled={isBusy}>
조치결과 제출
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) &&
!isSecondApproval &&
!(
@@ -970,6 +1021,7 @@ function FloatingActionButtons({
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) &&
prgrsSttsComCd !== "TB" &&
(!isForkJoin ||
(userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ04")) && (
<Button onClick={handleApprovalOpen} disabled={isBusy}>
@@ -1223,6 +1275,33 @@ async function handleActnCmptn(
}
}
async function handleTbActnRqst(context: ActionHandlerContext<void>): Promise<void> {
try {
const docNo = context.data.detDto?.fnrsCnsltnDocNo;
if (!docNo) throw new Error("문서번호가 없습니다.");
await fnrsCnsltnMngService.tbActnRqst(docNo);
await context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleTbSubmitActnRslt(
context: ActionHandlerContext<{
pwrkCnsltnDocNo: string;
body: PwrkActnResultSubmitDto;
}>,
): Promise<void> {
try {
const docNo = context.data.detDto?.fnrsCnsltnDocNo;
if (!docNo) throw new Error("문서번호가 없습니다.");
await fnrsCnsltnMngService.tbSubmitActnResult(docNo, context.request.body);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> = {
strg: handleSaveApply,
aply: handleSaveApply,
@@ -1247,13 +1326,15 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
actnCmptn: handleActnCmptn,
rqstActnRvw: handleRequestActnReview,
costVrfc: () => Promise.resolve(),
tbActnRqst: handleTbActnRqst,
tbSubmitActnRslt: handleTbSubmitActnRslt,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface ActionHandlerContext<T = any> {
request: T;
data: FnrsCnsltnMngDetResponseDto;
onSuccess?: () => void;
onSuccess?: () => void | Promise<void>;
onError?: (error: Error) => void;
}
@@ -54,7 +54,7 @@ const AI_DRAFT_EVL_MAX_VER_SN = 2;
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["TP", "RA", "AV", "EV", "RR", "EP", "TA"]);
const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["TB", "TP", "RA", "AV", "EV", "RR", "EP", "TA"]);
interface PwrkCnsltnDocMngDialogProps {
open: boolean;
@@ -953,7 +953,9 @@ function PwrkCnsltnDocMngDialog({
]);
// 저장 버튼은 해당 부서 작성 권한이 있는 경우 표시 (메인 기안기는 항상 최신 버전 유지)
const canSaveInCurrentView = canSaveCurrentBranch;
const canSaveInCurrentView = canSaveCurrentBranch && prgrsSttsComCd !== "TB";
const hideHangulForLocalUser =
effectiveUserRole === "ROLE_USER05" && prgrsSttsComCd === "SL";
// 변경이력 콤보 박스 컴포넌트 (선택 시 해당 버전 새 창으로 열람 및 선택 항목 유지)
const versionHistorySelect = (
@@ -1167,7 +1169,12 @@ function PwrkCnsltnDocMngDialog({
생성 완료된 실무협의서 초안이 없습니다.
</p>
) : null}
{completedDraft && documentBlob ? (
{hideHangulForLocalUser ? (
<p className="py-16 text-center text-lg text-muted-foreground">
기술검토(TE) 이후 보완요청(SL) 상태에서는 지자체 사용자에게 웹기안기가 표시되지 않습니다.
</p>
) : null}
{completedDraft && documentBlob && !hideHangulForLocalUser ? (
<div className={cn("flex items-stretch gap-2", editorContainerMinHeight)}>
<div className="min-w-0 flex-1">
<HangulGiangi