[as] feat: AI 자동 진행 단계 대기 상태 텍스트 추가

This commit is contained in:
2026-09-07 17:25:40 +09:00
parent b9ac4a1c18
commit b38be38169
4 changed files with 162 additions and 121 deletions
@@ -0,0 +1,21 @@
const AI_PENDING_STATUS_CODES = ["FV", "AC", "AV"] as const;
type AiPendingStatusCode = (typeof AI_PENDING_STATUS_CODES)[number];
const AI_PENDING_MESSAGES: Record<AiPendingStatusCode, string> = {
FV: "파일 검증이 진행 중입니다",
AC: "실무협의서 초안 생성이 진행 중입니다",
AV: "금액 검증이 진행 중입니다",
};
function isAiPendingStatus(prgrsSttsComCd: string): boolean {
return AI_PENDING_STATUS_CODES.includes(prgrsSttsComCd as AiPendingStatusCode);
}
function getAiPendingMessage(prgrsSttsComCd: string): string {
if (!isAiPendingStatus(prgrsSttsComCd)) return "";
return AI_PENDING_MESSAGES[prgrsSttsComCd as AiPendingStatusCode];
}
export { AI_PENDING_STATUS_CODES, getAiPendingMessage, isAiPendingStatus };
export type { AiPendingStatusCode };
@@ -57,6 +57,7 @@ interface FloatingActionButtonsProps {
checklistFormState: ChecklistFormState;
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
opnrNm?: string;
isAiPending?: boolean;
}
type ActiveDialog = "elaprTmplt" | "rjct" | "dmndSplmnt" | "elaprApproval" | null;
@@ -72,6 +73,7 @@ function FloatingActionButtons({
checklistFormState,
techReviewOpinionRef,
opnrNm,
isAiPending = false,
}: FloatingActionButtonsProps) {
const router = useRouter();
const { confirm } = useConfirm();
@@ -402,130 +404,138 @@ function FloatingActionButtons({
<Button color="white" type="button" onClick={handleGoToList} disabled={isProcessing}>
목록
</Button>
{canEdit && (
<Button color="gray" onClick={handleEdit} disabled={isProcessing}>
수정
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg) && (
<Button onClick={handleStrgClick} disabled={isProcessing}>
저장
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply) && (
<Button color="green" onClick={handleAplyClick} disabled={isProcessing}>
신청완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.del) && (
<Button color="red" onClick={handleDelClick} disabled={isProcessing}>
삭제
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && (
<Button onClick={handleFnrsCnsltnAncmntClick} disabled={isProcessing}>
재원협의 고시
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && (
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
환경청제출
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && (
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
반려
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv) && (
<Button onClick={handleSncnAprvClick} disabled={isProcessing}>
인가승인
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && (
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
검토결과 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
<Button onClick={handleRqstTechRvwClick} disabled={isProcessing}>
기술검토 의뢰
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc) && (
<Button color="gray" onClick={() => {}} disabled={isProcessing}>
파일 검증
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && (
<Button onClick={() => setActiveDialog("elaprTmplt")} disabled={isProcessing}>
검토자지정
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) && (
<Button color="orange" onClick={() => {}} disabled={isProcessing}>
실무협의서 초안생성
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) && (
<Button color="orange" onClick={() => {}} disabled={isProcessing}>
실무협의서 관리
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && (
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
저장
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn) && (
<Button color="green" onClick={handleRvwCmptnClick} disabled={isProcessing}>
검토완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && (
<Button color="gray" onClick={handleDmndSplmntClick} disabled={isProcessing}>
보완요청
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
결재(승인)
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
결재(회신)
</Button>
{!isAiPending && (
<>
{canEdit && (
<Button color="gray" onClick={handleEdit} disabled={isProcessing}>
수정
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg) && (
<Button onClick={handleStrgClick} disabled={isProcessing}>
저장
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply) && (
<Button color="green" onClick={handleAplyClick} disabled={isProcessing}>
신청완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.del) && (
<Button color="red" onClick={handleDelClick} disabled={isProcessing}>
삭제
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && (
<Button onClick={handleFnrsCnsltnAncmntClick} disabled={isProcessing}>
재원협의 고시
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && (
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
환경청제출
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && (
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
반려
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv) && (
<Button onClick={handleSncnAprvClick} disabled={isProcessing}>
인가승인
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && (
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
검토결과 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
<Button onClick={handleRqstTechRvwClick} disabled={isProcessing}>
기술검토 의뢰
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc) && (
<Button color="gray" onClick={() => {}} disabled={isProcessing}>
파일 검증
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && (
<Button onClick={() => setActiveDialog("elaprTmplt")} disabled={isProcessing}>
검토자지정
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) && (
<Button color="orange" onClick={() => {}} disabled={isProcessing}>
실무협의서 초안생성
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) && (
<Button color="orange" onClick={() => {}} disabled={isProcessing}>
실무협의서 관리
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && (
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
저장
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn) && (
<Button color="green" onClick={handleRvwCmptnClick} disabled={isProcessing}>
검토완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && (
<Button color="gray" onClick={handleDmndSplmntClick} disabled={isProcessing}>
보완요청
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
결재(승인)
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply) && (
<Button onClick={() => setActiveDialog("elaprApproval")} disabled={isProcessing}>
결재(회신)
</Button>
)}
</>
)}
</div>
</div>
<ElaprTmpltPickerDialog
open={activeDialog === "elaprTmplt"}
onClose={() => setActiveDialog(null)}
docSeComCd={docSeComCd}
onConfirm={handleElaprTmpltConfirm}
/>
{!isAiPending && (
<>
<ElaprTmpltPickerDialog
open={activeDialog === "elaprTmplt"}
onClose={() => setActiveDialog(null)}
docSeComCd={docSeComCd}
onConfirm={handleElaprTmpltConfirm}
/>
<SplmntDmndDialog
splmntArtclUpCd="SPLMNTARTCL"
open={activeDialog === "dmndSplmnt"}
onOpenChange={() => setActiveDialog(null)}
isSubmitting={isProcessing}
onConfirm={handleDmndSplmntConfirm}
/>
<SplmntDmndDialog
splmntArtclUpCd="SPLMNTARTCL"
open={activeDialog === "dmndSplmnt"}
onOpenChange={() => setActiveDialog(null)}
isSubmitting={isProcessing}
onConfirm={handleDmndSplmntConfirm}
/>
<ElaprApprovalDialog
open={activeDialog === "elaprApproval"}
onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)}
elaprDocNo={data.detDto?.fnrsCnsltnDocNo ?? fnrsCnsltnDocNo}
currentUserUuid={userInfo?.userUnqId ?? ""}
onSuccess={() => {
queryClient.invalidateQueries({
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo),
});
}}
/>
<ElaprApprovalDialog
open={activeDialog === "elaprApproval"}
onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)}
elaprDocNo={data.detDto?.fnrsCnsltnDocNo ?? fnrsCnsltnDocNo}
currentUserUuid={userInfo?.userUnqId ?? ""}
onSuccess={() => {
queryClient.invalidateQueries({
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo),
});
}}
/>
</>
)}
</>
);
}
@@ -3,7 +3,7 @@
import { useMemo, useRef } from "react";
import { useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { SectionCollapsibleGroup, SectionCollapsibleToggle, TextShimmer } from "@/shared/ui";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
@@ -14,6 +14,7 @@ import {
} from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { getAiPendingMessage, isAiPendingStatus } from "../lib/ai-pending-utils";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
@@ -93,11 +94,19 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
userInfo?.dtlInstCd,
detDto.prgrsSttsComCd || "",
);
const isAiPending = isAiPendingStatus(detDto.prgrsSttsComCd || "");
return (
<div className="mt-4">
<SectionCollapsibleGroup>
<SectionCollapsibleToggle />
<div className="sticky top-[6rem] z-30 flex items-center gap-4 bg-container px-4 py-2">
<SectionCollapsibleToggle />
{isAiPending && (
<TextShimmer active className="text-xl">
{getAiPendingMessage(detDto.prgrsSttsComCd || "")}
</TextShimmer>
)}
</div>
<div className="space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
@@ -137,6 +146,7 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
checklistFormState={checklistFormState}
techReviewOpinionRef={techReviewOpinionRef}
opnrNm={userInfo?.userName}
isAiPending={isAiPending}
/>
</div>
</SectionCollapsibleGroup>
@@ -59,7 +59,7 @@ function SectionCollapsibleToggle() {
const { batchOpen, expandAll, collapseAll } = group;
return (
<div className="sticky top-[6rem] z-30 flex justify-start gap-2 bg-container px-4 py-2">
<div className="flex gap-2">
<Button
type="button"
size="sm"