[as elapr] 재원업무 협의 2차 결재시 자동 검토자 지정 로직 추가 3

This commit is contained in:
2026-10-15 17:02:26 +09:00
parent dd4ee1dcb5
commit e697881581
8 changed files with 214 additions and 18 deletions
@@ -11,27 +11,34 @@ const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_DOC_CRT_RSLT;
async function findLatestByCrtDmndUuid(
crtDmndUuid: string,
branchSeCd?: string,
): Promise<PwrkCnsltnDocCrtRsltResponseDto> {
const response = await client.get<PwrkCnsltnDocCrtRsltResponseDto>(
`${BASE}/${crtDmndUuid}/latest`,
{
params: branchSeCd ? { branchSeCd } : undefined,
},
);
return response.data;
}
async function openWebHwpLatest(crtDmndUuid: string) {
async function openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string) {
return client.get<Blob>(`${BASE}/${crtDmndUuid}/webhwp/open`, {
params: branchSeCd ? { branchSeCd } : undefined,
responseType: "blob",
});
}
async function download(crtDmndUuid: string, verSn: number) {
async function download(crtDmndUuid: string, verSn: number, branchSeCd?: string) {
return client.get<Blob>(`${BASE}/${crtDmndUuid}/${verSn}/download`, {
params: branchSeCd ? { branchSeCd } : undefined,
responseType: "blob",
});
}
async function downloadLatest(crtDmndUuid: string) {
async function downloadLatest(crtDmndUuid: string, branchSeCd?: string) {
return client.get<Blob>(`${BASE}/${crtDmndUuid}/download`, {
params: branchSeCd ? { branchSeCd } : undefined,
responseType: "blob",
});
}
@@ -47,6 +54,9 @@ async function saveMultipart(
if (saveDto.atchFileGroupId) {
formData.append("atchFileGroupId", saveDto.atchFileGroupId);
}
if (saveDto.branchSeCd) {
formData.append("branchSeCd", saveDto.branchSeCd);
}
if (file) {
const resolvedFileName = fileName ?? (file instanceof File ? file.name : "document.hwpx");
formData.append("file", file, resolvedFileName);
@@ -5,6 +5,7 @@ import type { PwrkCnsltnDocCrtRsltSaveRequestDto } from "./pwrk-cnsltn-doc-crt-r
export interface PwrkCnsltnDocCrtRsltResponseDto {
crtDmndUuid: string;
verSn: number;
branchSeCd?: string;
rgtrUuid?: string;
rgtrNm?: string;
orgnlOtptDocCn: null;
@@ -14,15 +15,23 @@ export interface PwrkCnsltnDocCrtRsltResponseDto {
/** 백엔드 PwrkCnsltnDocCrtRsltUpdateRequestDto (모두 optional) */
export interface PwrkCnsltnDocCrtRsltUpdateRequestDto {
rgtrUuid?: string;
branchSeCd?: string;
orgnlOtptDocCn?: string;
crtCmptnDt?: string;
}
export interface PwrkCnsltnDocCrtRsltService {
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
openWebHwpLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
findLatestByCrtDmndUuid(
crtDmndUuid: string,
branchSeCd?: string,
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string): Promise<AxiosResponse<Blob>>;
download(
crtDmndUuid: string,
verSn: number,
branchSeCd?: string,
): Promise<AxiosResponse<Blob>>;
downloadLatest(crtDmndUuid: string, branchSeCd?: string): Promise<AxiosResponse<Blob>>;
saveMultipart(
saveDto: PwrkCnsltnDocCrtRsltSaveRequestDto,
file?: Blob | File,
@@ -5,6 +5,7 @@ export const pwrkCnsltnDocCrtRsltSaveRequestSchema = z.object({
crtDmndUuid: z.string().uuid(),
pwrkCnsltnDocNo: z.string().min(1).max(100),
atchFileGroupId: z.string().max(30).nullish(),
branchSeCd: z.string().max(20).nullish(),
});
export type PwrkCnsltnDocCrtRsltSaveRequestDto = z.infer<
@@ -932,6 +932,9 @@ function FloatingActionButtons({
fnrsCnsltnDocNo={docNo}
atrzSeComCdList={effectiveAtrzSeComCdList}
prgrsSttsComCd={prgrsSttsComCd}
branchSeCd={userPendingBranchSeCd}
isForkJoin={isForkJoin}
userRole={dtlInstCd}
canSave={
(needsElaprPrgrsCheck && !hasPendingApprovalRequest) || viewOnlyPwrkCnsltnDoc
? false
@@ -60,6 +60,10 @@ interface PwrkCnsltnDocMngDialogProps {
canSave?: boolean | null;
onDmndSplmnt?: () => void;
onClose: () => void;
branchSeCd?: string;
isForkJoin?: boolean;
userRole?: string;
supplementDmndBranch?: string;
}
interface UploadedAtchFile {
@@ -73,6 +77,31 @@ function createPwrkAtchUploadKey(file: File) {
return `${PWRK_ATCH_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function resolveDefaultBranchSeCd({
branchSeCd,
isForkJoin,
userRole,
prgrsSttsComCd,
supplementDmndBranch,
}: {
branchSeCd?: string;
isForkJoin?: boolean;
userRole?: string;
prgrsSttsComCd?: string;
supplementDmndBranch?: string;
}): string {
if (userRole === "ROLE_USER05" && prgrsSttsComCd === "SL") {
return supplementDmndBranch || branchSeCd || "SP";
}
if (branchSeCd === "TOTAL_AGG") {
return "MAIN";
}
if (branchSeCd && ["SP", "SF", "MAIN"].includes(branchSeCd)) {
return branchSeCd;
}
return isForkJoin ? "SP" : "MAIN";
}
function PwrkCnsltnDocMngDialog({
open,
fnrsCnsltnDocNo,
@@ -81,6 +110,10 @@ function PwrkCnsltnDocMngDialog({
canSave: canSaveProp,
onDmndSplmnt,
onClose,
branchSeCd,
isForkJoin,
userRole,
supplementDmndBranch,
}: PwrkCnsltnDocMngDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
@@ -88,6 +121,30 @@ function PwrkCnsltnDocMngDialog({
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false);
const [currentBranchSeCd, setCurrentBranchSeCd] = useState<string>(() =>
resolveDefaultBranchSeCd({
branchSeCd,
isForkJoin,
userRole,
prgrsSttsComCd,
supplementDmndBranch,
}),
);
useEffect(() => {
if (open) {
setCurrentBranchSeCd(
resolveDefaultBranchSeCd({
branchSeCd,
isForkJoin,
userRole,
prgrsSttsComCd,
supplementDmndBranch,
}),
);
}
}, [open, branchSeCd, isForkJoin, userRole, prgrsSttsComCd, supplementDmndBranch]);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const {
@@ -103,13 +160,20 @@ function PwrkCnsltnDocMngDialog({
isLoading: isDocumentLoading,
isError: isDocumentError,
} = useQuery({
queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", crtDmndUuid],
queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!),
queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", crtDmndUuid, currentBranchSeCd],
queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, currentBranchSeCd),
select: (response) => response.data,
enabled: open && Boolean(crtDmndUuid),
});
const documentFileName = `실무협의서_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`;
const branchLabel = isForkJoin
? currentBranchSeCd === "SP"
? "_관로"
: currentBranchSeCd === "SF"
? "_시설"
: "_종합"
: "";
const documentFileName = `실무협의서${branchLabel}_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`;
const isLoading = isPwrkCnsltnLoading || isDocumentLoading;
const isError = isPwrkCnsltnError || isDocumentError;
const pjtcoAjmtSearchParams = useMemo(
@@ -181,10 +245,18 @@ function PwrkCnsltnDocMngDialog({
async function handleSaveDocument(file: Blob) {
if (!completedDraft) throw new Error("저장할 실무협의서 초안이 없습니다.");
const saveBranch =
branchSeCd === "TOTAL_AGG" || currentBranchSeCd === "MAIN"
? "MAIN"
: isForkJoin
? currentBranchSeCd
: undefined;
const saveDto = pwrkCnsltnDocCrtRsltSaveRequestSchema.parse({
crtDmndUuid: completedDraft.crtDmndUuid,
pwrkCnsltnDocNo: completedDraft.pwrkCnsltnDocNo,
atchFileGroupId: atchFileGroupId || undefined,
branchSeCd: saveBranch,
});
const savedDocument = await pwrkCnsltnDocCrtRsltService.saveMultipart(
@@ -210,7 +282,10 @@ function PwrkCnsltnDocMngDialog({
async function handleDownload() {
if (!completedDraft) throw new Error("다운로드할 실무협의서 초안이 없습니다.");
const response = await pwrkCnsltnDocCrtRsltService.downloadLatest(completedDraft.crtDmndUuid);
const response = await pwrkCnsltnDocCrtRsltService.downloadLatest(
completedDraft.crtDmndUuid,
currentBranchSeCd,
);
const objectUrl = window.URL.createObjectURL(response.data);
const anchor = document.createElement("a");
anchor.href = objectUrl;
@@ -277,7 +352,24 @@ function PwrkCnsltnDocMngDialog({
const canSaveByAtrz = atrzSeComCdList
? !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code))
: true;
const canSave = !viewOnly && (canSaveProp ?? canSaveByAtrz);
const baseCanSave = !viewOnly && (canSaveProp ?? canSaveByAtrz);
const canSaveCurrentBranch = useMemo(() => {
if (!baseCanSave) return false;
if (userRole === "ROLE_USER05") return false;
if (isForkJoin) {
if (branchSeCd === "TOTAL_AGG") {
return currentBranchSeCd === "MAIN";
}
if (branchSeCd === "SP") {
return currentBranchSeCd === "SP";
}
if (branchSeCd === "SF") {
return currentBranchSeCd === "SF";
}
}
return baseCanSave;
}, [baseCanSave, userRole, isForkJoin, branchSeCd, currentBranchSeCd]);
return (
<>
@@ -290,6 +382,53 @@ function PwrkCnsltnDocMngDialog({
<DialogTitle>실무협의서 관리</DialogTitle>
</DialogHeader>
{isForkJoin ? (
<div className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-border bg-muted/40 px-3 py-2">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-muted-foreground">부서별 문서 탭:</span>
<Button
type="button"
size="sm"
color={currentBranchSeCd === "SP" ? "primary" : "white"}
onClick={() => setCurrentBranchSeCd("SP")}
>
관로 초안 (SP)
</Button>
<Button
type="button"
size="sm"
color={currentBranchSeCd === "SF" ? "primary" : "white"}
onClick={() => setCurrentBranchSeCd("SF")}
>
시설 초안 (SF)
</Button>
<Button
type="button"
size="sm"
color={currentBranchSeCd === "MAIN" ? "primary" : "white"}
onClick={() => setCurrentBranchSeCd("MAIN")}
>
종합 실무협의서 (MAIN)
</Button>
</div>
<span className="text-xs text-muted-foreground">
{branchSeCd === "TOTAL_AGG"
? "종합병합자: SP/SF를 참조하여 MAIN 종합본을 수동 병합 저장합니다."
: branchSeCd === "SP"
? currentBranchSeCd === "SP"
? "관로 검토자: 관로 초안(SP) 편집 모드"
: "관로 검토자: 타 문서 열람 모드 (읽기 전용)"
: branchSeCd === "SF"
? currentBranchSeCd === "SF"
? "시설 검토자: 시설 초안(SF) 편집 모드"
: "시설 검토자: 타 문서 열람 모드 (읽기 전용)"
: userRole === "ROLE_USER05"
? "지자체 사용자: 보완요청 부서 문서 열람 전용"
: "문서 열람 모드"}
</span>
</div>
) : null}
{isLoading ? <Skeleton className="h-[70vh] min-h-[40rem] w-full" /> : null}
{isError ? (
<p className="py-16 text-center text-lg text-muted-foreground">
@@ -306,9 +445,9 @@ function PwrkCnsltnDocMngDialog({
<div className="min-w-0 flex-1">
<HangulGiangi
documentBlob={documentBlob}
documentKey={completedDraft.crtDmndUuid}
documentKey={`${completedDraft.crtDmndUuid}_${currentBranchSeCd}`}
documentFileName={documentFileName}
canSave={canSave}
canSave={canSaveCurrentBranch}
onSaveDocument={handleSaveDocument}
onDownload={handleDownload}
onDmndSplmnt={onDmndSplmnt}
@@ -366,10 +505,10 @@ function PwrkCnsltnDocMngDialog({
}))}
inputId="pwrk-cnsltn-doc-atch-file-input"
isUploadingByKey={isUploading}
addDisabled={!canSave || isAnyAtchUploading}
showAddButton={canSave}
addDisabled={!canSaveCurrentBranch || isAnyAtchUploading}
showAddButton={canSaveCurrentBranch}
onAdd={(files) => void handleAtchFileUpload(files)}
onRemove={canSave ? (index) => void handleAtchFileRemove(index) : undefined}
onRemove={canSaveCurrentBranch ? (index) => void handleAtchFileRemove(index) : undefined}
/>
</section>
) : null}
@@ -39,6 +39,7 @@ import { Button } from "@common/components";
import {
type FnrsCnsltnRegistFormData,
fnrsCnsltnRegistSchema,
isForkJoinTask,
} from "../lib/fnrs-cnsltn-regist-schema";
import {
buildFormPayload,
@@ -349,7 +350,10 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
<PwrkCnsltnDocMngDialog
open={isPwrkCnsltnDocMngOpen}
fnrsCnsltnDocNo={fnrsCnsltnDocNo}
canSave
isForkJoin={isForkJoinTask(initialData?.detDto?.taskSeComCd)}
userRole={userInfo?.dtlInstCd}
prgrsSttsComCd={initialData?.detDto?.prgrsSttsComCd}
canSave={!isSplmntPrgrsStts}
onClose={() => setIsPwrkCnsltnDocMngOpen(false)}
/>
) : null}
+1
View File
@@ -16,6 +16,7 @@ export { buildCnstpScaleItems, recalculateBzScaleTotals } from "./lib/business-s
export { canEditPbadmsDoc, type CanEditPbadmsDocParams } from "./lib/can-edit-pbadms-doc";
export {
buildChecklistTabs,
isBranchChecklistComplete,
isFormChecklistComplete,
normalizeTaskSeComCdArray,
syncCklstResultListFromTree,
@@ -138,3 +138,32 @@ export function isFormChecklistComplete(cklstResultList: CklstResultListItem[]):
if (cklstResultList.length === 0) return true;
return cklstResultList.every((item) => item.selected === "Y" || item.selected === "N");
}
/**
* 결재자 브랜치별 체크리스트 완료 여부 검증 (병렬 결재 지원)
* - branchSeCd가 주어지면 해당 브랜치의 항목만 필수 검증 (SP 또는 SF)
* - branchSeCd가 없거나 TOTAL_AGG, MAIN 등인 경우 전체 항목 검증
*/
export function isBranchChecklistComplete(
cklstResultList: CklstResultListItem[],
cklstTree: CklstTreeNode[],
branchSeCd?: string | null,
): boolean {
if (cklstResultList.length === 0) return true;
if (!branchSeCd || branchSeCd === "TOTAL_AGG" || branchSeCd === "MAIN") {
return isFormChecklistComplete(cklstResultList);
}
const targetRvwSns = new Set(
cklstTree
.filter((node) => node.taskSeComCd === branchSeCd && node.bchckUseYn === "Y")
.map((node) => node.cklstRvwSn),
);
if (targetRvwSns.size === 0) return true;
return cklstResultList
.filter((item) => targetRvwSns.has(item.cklstRvwSn))
.every((item) => item.selected === "Y" || item.selected === "N");
}