diff --git a/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts index 8b2e67fa2..368600110 100644 --- a/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts +++ b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts @@ -1,17 +1,67 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; import type { - ElaprDmndResponse, - ElaprDmndSaveRequest, + ElaprDmndResponseDto, + ElaprDmndSaveRequestDto, + ElaprDmndSearchDto, ElaprDmndService, + ElaprDocPrgrsResponseDto, } from "../model/elapr-dmnd-interface"; -async function create(body: ElaprDmndSaveRequest): Promise { - const response = await client.post(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, body); +const BASE = ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND; + +async function findPage(params: ElaprDmndSearchDto): Promise> { + const response = await client.get>(BASE, { params }); return response.data; } -export const ElaprDmndApi: ElaprDmndService = { - create, -}; +async function findById(elaprDmndId: string): Promise { + const response = await client.get(`${BASE}/${elaprDmndId}`); + return response.data; +} + +async function save(body: ElaprDmndSaveRequestDto): Promise { + const response = await client.post(BASE, body); + return response.data; +} + +async function update( + elaprDmndId: string, + body: ElaprDmndSaveRequestDto, +): Promise { + const response = await client.put(`${BASE}/${elaprDmndId}`, body); + return response.data; +} + +async function updateStatus( + elaprDmndId: string, + atrzSttsComCd: string, +): Promise { + const response = await client.put(`${BASE}/${elaprDmndId}/status`, null, { + params: { atrzSttsComCd }, + }); + return response.data; +} + +async function remove(elaprDmndId: string): Promise { + await client.delete(`${BASE}/${elaprDmndId}`); +} + +async function findDocPrgrsByDocNo(elaprDocNo: string): Promise { + const response = await client.get( + `${BASE}/sanc/docprgrs/${elaprDocNo}`, + ); + return response.data; +} + +export const elaprDmndService = { + findPage, + findById, + save, + update, + updateStatus, + remove, + findDocPrgrsByDocNo, +} satisfies ElaprDmndService; diff --git a/apps/as/src/entities/elapr/api/queries.ts b/apps/as/src/entities/elapr/api/queries.ts index 59a6bb91b..fae6a0d03 100644 --- a/apps/as/src/entities/elapr/api/queries.ts +++ b/apps/as/src/entities/elapr/api/queries.ts @@ -8,6 +8,7 @@ import type { ElaprAplnSaveRequest, ElaprAplnUpdateRequest, } from "../model/elapr-apln-interface"; +import type { ElaprDmndSaveRequestDto, ElaprDmndSearchDto } from "../model/elapr-dmnd-interface"; import type { ElaprTmpltSaveRequest, ElaprTmpltSearchParams, @@ -15,10 +16,12 @@ import type { } from "../model/elapr-tmplt-interface"; import { elaprAplnService } from "./elapr-apln-service"; +import { elaprDmndService } from "./elapr-dmnd-service"; import { elaprTmpltService } from "./elapr-tmplt-service"; const elaprTmpltBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT] as const; const elaprAplnBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN] as const; +const elaprDmndBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND] as const; export function useElaprTmpltPage(params: ElaprTmpltSearchParams) { return useQuery({ @@ -50,13 +53,8 @@ export function useUpdateElaprTmplt() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - elaprTmpltId, - body, - }: { - elaprTmpltId: string; - body: ElaprTmpltUpdateRequest; - }) => elaprTmpltService.update(elaprTmpltId, body), + mutationFn: ({ elaprTmpltId, body }: { elaprTmpltId: string; body: ElaprTmpltUpdateRequest }) => + elaprTmpltService.update(elaprTmpltId, body), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprTmpltBaseKey }); queryClient.invalidateQueries({ @@ -99,12 +97,8 @@ export function useSaveMultiElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - body, - }: { - body: ElaprAplnSaveRequest[]; - elaprTmpltId: string; - }) => elaprAplnService.saveMulti(body), + mutationFn: ({ body }: { body: ElaprAplnSaveRequest[]; elaprTmpltId: string }) => + elaprAplnService.saveMulti(body), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -118,13 +112,8 @@ export function useUpdateElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - aplnId, - body, - }: { - aplnId: string; - body: ElaprAplnUpdateRequest; - }) => elaprAplnService.update(aplnId, body), + mutationFn: ({ aplnId, body }: { aplnId: string; body: ElaprAplnUpdateRequest }) => + elaprAplnService.update(aplnId, body), onSuccess: (_data, { body, aplnId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -141,12 +130,8 @@ export function useDeleteElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - aplnId, - }: { - aplnId: string; - elaprTmpltId: string; - }) => elaprAplnService.remove(aplnId), + mutationFn: ({ aplnId }: { aplnId: string; elaprTmpltId: string }) => + elaprAplnService.remove(aplnId), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -160,12 +145,8 @@ export function useReorderElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - body, - }: { - body: ElaprAplnReorderRequest; - elaprTmpltId: string; - }) => elaprAplnService.reorder(body), + mutationFn: ({ body }: { body: ElaprAplnReorderRequest; elaprTmpltId: string }) => + elaprAplnService.reorder(body), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -174,3 +155,78 @@ export function useReorderElaprApln() { }, }); } + +export function useElaprDmndPage(params: ElaprDmndSearchDto) { + return useQuery({ + queryKey: [...elaprDmndBaseKey, "findPage", params], + queryFn: () => elaprDmndService.findPage(params), + }); +} + +export function useElaprDmnd(elaprDmndId: string | null | undefined) { + return useQuery({ + queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId], + queryFn: () => elaprDmndService.findById(elaprDmndId!), + enabled: Boolean(elaprDmndId), + }); +} + +export function useElaprDocPrgrsByDocNo(elaprDocNo: string | null | undefined) { + return useQuery({ + queryKey: [...elaprDmndBaseKey, "findDocPrgrsByDocNo", elaprDocNo], + queryFn: () => elaprDmndService.findDocPrgrsByDocNo(elaprDocNo!), + enabled: Boolean(elaprDocNo), + }); +} + +export function useSaveElaprDmnd() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: ElaprDmndSaveRequestDto) => elaprDmndService.save(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + }, + }); +} + +export function useUpdateElaprDmnd() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ elaprDmndId, body }: { elaprDmndId: string; body: ElaprDmndSaveRequestDto }) => + elaprDmndService.update(elaprDmndId, body), + onSuccess: (_data, { elaprDmndId }) => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId], + }); + }, + }); +} + +export function useUpdateElaprDmndStatus() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ elaprDmndId, atrzSttsComCd }: { elaprDmndId: string; atrzSttsComCd: string }) => + elaprDmndService.updateStatus(elaprDmndId, atrzSttsComCd), + onSuccess: (_data, { elaprDmndId }) => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId], + }); + }, + }); +} + +export function useRemoveElaprDmnd() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (elaprDmndId: string) => elaprDmndService.remove(elaprDmndId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + }, + }); +} diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index ad35ae13b..4b14ea0d9 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -1,5 +1,5 @@ export { elaprAplnService } from "./api/elapr-apln-service"; -export { ElaprDmndApi } from "./api/elapr-dmnd-service"; +export { elaprDmndService } from "./api/elapr-dmnd-service"; export { elaprTmpltService } from "./api/elapr-tmplt-service"; export { useCreateElaprTmplt, @@ -7,11 +7,18 @@ export { useDeleteElaprTmplt, useElaprApln, useElaprAplnPage, + useElaprDmnd, + useElaprDmndPage, + useElaprDocPrgrsByDocNo, useElaprTmplt, useElaprTmpltPage, + useRemoveElaprDmnd, useReorderElaprApln, + useSaveElaprDmnd, useSaveMultiElaprApln, useUpdateElaprApln, + useUpdateElaprDmnd, + useUpdateElaprDmndStatus, useUpdateElaprTmplt, } from "./api/queries"; export { @@ -25,7 +32,13 @@ export { type ElaprAplnService, type ElaprAplnUpdateRequest, } from "./model/elapr-apln-interface"; -export type { ElaprDmndSaveRequest } from "./model/elapr-dmnd-interface"; +export type { + ElaprDmndResponseDto, + ElaprDmndSaveRequestDto, + ElaprDmndSearchDto, + ElaprDmndService, + ElaprDocPrgrsResponseDto, +} from "./model/elapr-dmnd-interface"; export type { ElaprTmplt, ElaprTmpltSaveRequest, diff --git a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts index 249d98bf9..f653249f2 100644 --- a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts @@ -1,22 +1,62 @@ -export interface ElaprDmndSaveRequest { +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 ElaprDmndSaveRequestDto */ +export interface ElaprDmndSaveRequestDto { elaprDocNo: string; elaprTmpltId: string; dmndTtl: string; dmndCn: string; } -export interface ElaprDmndResponse { +/** 백엔드 ElaprDmndResponseDto */ +export interface ElaprDmndResponseDto { elaprDmndId: string; + rqstrUuid?: string; elaprDocNo: string; elaprTmpltId: string; dmndTtl: string; dmndCn: string; atrzSttsComCd: string; - rqstrUuid: string; - regDt: string; - updtDt: string; + atrzSttsComNm?: string; + dmndDt?: string; + updtDt?: string; } -export interface ElaprDmndService { - create(body: ElaprDmndSaveRequest): Promise; +/** 백엔드 ElaprDmndSearchDto + Pageable */ +export interface ElaprDmndSearchDto extends PageableParams { + rqstrUuid?: string; + atrzSttsComCd?: string; + dmndTtl?: string; + dmndDtStart?: string; + dmndDtEnd?: string; +} + +/** 백엔드 ElaprDocPrgrsResponseDto */ +export interface ElaprDocPrgrsResponseDto { + elaprDocNo: string; + elaprDmndId: string; + elaprPrgrsId: string; + atrzSeqNo?: number; + atrzSeComCd: string; + atrzSeComNm?: string; + aprvrUuid?: string; + aprvrUuidNm?: string; + atrzPrgrsSttsComCd: string; + atrzPrgrsSttsComNm?: string; + rjctRsn?: string; + atrzDt?: string; + jbpsSeNm?: string; + jbgdSeNm?: string; + ognzNm?: string; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface ElaprDmndService { + findPage(params: ElaprDmndSearchDto): Promise>; + findById(elaprDmndId: string): Promise; + save(body: ElaprDmndSaveRequestDto): Promise; + update(elaprDmndId: string, body: ElaprDmndSaveRequestDto): Promise; + updateStatus(elaprDmndId: string, atrzSttsComCd: string): Promise; + remove(elaprDmndId: string): Promise; + findDocPrgrsByDocNo(elaprDocNo: string): Promise; } diff --git a/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx b/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx index 1a54fb444..ce7468c32 100644 --- a/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx +++ b/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx @@ -12,7 +12,7 @@ import { PROXY_HOST } from "@/shared/config"; import { formatFileSize } from "@/shared/lib/file-utils"; import axios from "axios"; import { processElaprPrgrs } from "@/entities/elapr/api/elapr-prgrs-service"; -import { ElaprDmndApi } from "@/entities/elapr/api/elapr-dmnd-service"; +import { elaprDmndService } from "@/entities/elapr"; import { dayjs, useUserStore, useAlertStore } from "@/shared/lib"; import { instlSncnService, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn"; import { client } from "@/shared/api"; @@ -206,8 +206,7 @@ function CnfmViewPage({ id }: { id: string }) { dmndTtl: det.bizNm /* "구례 하수도 처리시설 2026", */, dmndCn: det.bizCn /*"구례 발주 하수도 처리시설 사업 2026년도 진행분 결제 요청 입니다."*/, }; - // ElaprDmndApi 서비스를 통해 백엔드 연동 - await ElaprDmndApi.create(payload); + await elaprDmndService.save(payload); alert("결재자 지정(결재요청 등록)이 완료되었습니다."); try { await query.refetch(); diff --git a/apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx b/apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx new file mode 100644 index 000000000..31fed4ccc --- /dev/null +++ b/apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx @@ -0,0 +1,223 @@ +"use client"; + +import { useState } from "react"; + +import { useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib"; +import { useElaprDocPrgrsByDocNo } from "@/entities/elapr"; +import { processElaprPrgrs } from "@/entities/elapr/api/elapr-prgrs-service"; +import { + Button, + cn, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Skeleton, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Textarea, +} from "@common/components"; + +import { getApproverStatus, sortByAtrzSeqNo } from "../../lib/approval-utils"; + +interface ElaprApprovalDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + elaprDocNo: string; + currentUserUuid: string; + onSuccess: () => void; +} + +function ElaprApprovalDialog({ + open, + onOpenChange, + elaprDocNo, + currentUserUuid, + onSuccess, +}: ElaprApprovalDialogProps) { + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + const [rjctRsn, setRjctRsn] = useState(""); + const [isProcessing, setIsProcessing] = useState(false); + + const { data: elaprDocPrgrsList = [], isLoading } = useElaprDocPrgrsByDocNo( + open ? elaprDocNo : null, + ); + + const orderedApprovers = sortByAtrzSeqNo(elaprDocPrgrsList); + + const currentUserPrgrs = elaprDocPrgrsList.find( + (row) => row.aprvrUuid === currentUserUuid && row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_002", + ); + const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId); + + async function handleApprove() { + if (!currentUserPrgrs?.elaprPrgrsId) return; + + setIsProcessing(true); + try { + await processElaprPrgrs(currentUserPrgrs.elaprPrgrsId, "ATRZ_PRGRS_003"); + alert("결재가 완료되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo], + }); + onSuccess(); + onOpenChange(false); + } catch (error: unknown) { + let errorMessage = ""; + if (error instanceof AxiosError) { + errorMessage = error.response?.data?.message ?? ""; + } else { + errorMessage = "결재 처리 중 오류가 발생했습니다."; + } + alert(errorMessage); + } finally { + setIsProcessing(false); + } + } + + async function handleReject() { + if (!currentUserPrgrs?.elaprPrgrsId) return; + if (!rjctRsn.trim()) { + alert("반려 사유를 입력해주세요."); + return; + } + + setIsProcessing(true); + try { + await processElaprPrgrs(currentUserPrgrs.elaprPrgrsId, "ATRZ_PRGRS_004"); + alert("반려 처리가 완료되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo], + }); + onSuccess(); + onOpenChange(false); + } catch (error: unknown) { + let errorMessage = ""; + if (error instanceof AxiosError) { + errorMessage = error.response?.data?.message ?? ""; + } else { + errorMessage = "반려 처리 중 오류가 발생했습니다."; + } + alert(errorMessage); + } finally { + setIsProcessing(false); + } + } + + function handleClose() { + setRjctRsn(""); + onOpenChange(false); + } + + return ( + + + + 전자결재 + + +
+
+

결재 진행 단계

+ {isLoading ? ( +
+ + +
+ ) : orderedApprovers.length === 0 ? ( +
결재 정보가 없습니다.
+ ) : ( +
+ + + + {orderedApprovers.map((row) => ( + + {row.atrzSeComNm ?? row.atrzSeComCd} + + ))} + + + + + {orderedApprovers.map((row) => { + const status = row.atrzPrgrsSttsComCd ?? ""; + const st = getApproverStatus(status); + + return ( + +
+ + {st.label} + + {row.aprvrUuidNm ?? "-"} +
+
+ {row.ognzNm ?? ""} + {row.jbpsSeNm || row.jbgdSeNm + ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` + : ""} +
+
+ ); + })} +
+
+
+
+ )} +
+ + {canApprove && ( +
+

반려 의견

+