[as elapr] 재원업무 협의 2차 결재시 자동 검토자 지정 로직 추가 3
This commit is contained in:
+13
-3
@@ -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);
|
||||
|
||||
+13
-4
@@ -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,
|
||||
|
||||
+1
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user