[as] chore: 재원협의 코드 수정

This commit is contained in:
2026-09-17 16:48:13 +09:00
parent 41643563f8
commit 086d5a18e8
12 changed files with 81 additions and 94 deletions
+2 -1
View File
@@ -187,13 +187,14 @@ export function useElaprDmnd(elaprDmndId: string | null | undefined) {
export function useElaprDocPrgrsByDocNo(
elaprDocNo: string | null | undefined,
options?: { elaprCycl?: number; enabled?: boolean },
options?: { elaprCycl?: number; enabled?: boolean; structuralSharing?: boolean },
) {
const elaprCycl = options?.elaprCycl;
return useQuery({
queryKey: [...elaprDmndBaseKey, "findDocPrgrsByDocNo", elaprDocNo, elaprCycl],
queryFn: () => elaprDmndService.findDocPrgrsByDocNo(elaprDocNo!, elaprCycl),
enabled: Boolean(elaprDocNo) && (options?.enabled ?? true),
structuralSharing: options?.structuralSharing ?? true,
});
}
@@ -71,6 +71,7 @@ function ElaprApprovalDialog({
const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null, {
elaprCycl,
structuralSharing: false,
});
const cyclDocPrgrsList = useMemo(() => {
if (elaprCycl == null) return elaprDocPrgrsList;
@@ -100,10 +101,7 @@ function ElaprApprovalDialog({
const savePrgrsSttsMutation = useSaveElaprPrgrsStts();
const sensors = useDndSensors();
const currentUserPrgrs = findCurrentUserPendingApprovalRequest(
cyclDocPrgrsList,
currentUserUuid,
);
const currentUserPrgrs = findCurrentUserPendingApprovalRequest(cyclDocPrgrsList, currentUserUuid);
const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId);
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
@@ -2,11 +2,9 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
strg: "strg",
aply: "aply",
del: "del",
fnrsCnsltnAncmnt: "fnrsCnsltnAncmnt",
envoSbmsn: "envoSbmsn",
rjct: "rjct",
sncnAprv: "sncnAprv",
lclRply: "lclRply",
rvwRply: "rvwRply",
rqstTechRvw: "rqstTechRvw",
fileSplmnt: "fileSplmnt",
@@ -39,14 +37,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],
TA: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
],
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
ROLE_USER04: {
ST: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct],
RM: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply],
EP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv, FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
ROLE_USER03: {
@@ -56,10 +50,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw,
],
RR: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
],
RR: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply],
},
};
@@ -117,6 +108,8 @@ interface GetButtonConfigsOptions {
}
const PENDING_APPROVAL_ACTIONS = new Set<FnrsCnsltnViewButtonAction>([
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
@@ -262,19 +262,6 @@ function FloatingActionButtons({
router.push("/fnrs-cnsltn");
}
// async function handleFnrsCnsltnAncmntClick() {
// const payload: DocPrgrsUpdateRequestDto = {
// docNo,
// docSeComCd,
// prgrsSttsComCd: "TB",
// };
// await executeAction({
// action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt,
// request: payload,
// successMessage: "고시되었습니다.",
// });
// }
async function handleEnvoSbmsnClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
@@ -288,19 +275,6 @@ function FloatingActionButtons({
});
}
async function handleLclRplyClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
docSeComCd,
prgrsSttsComCd: "RB",
};
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply,
request: payload,
successMessage: "지자체 회신이 완료되었습니다.",
});
}
async function handleRjctClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
@@ -339,7 +313,7 @@ function FloatingActionButtons({
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
alert("모든 체크리스트 항목을 체크해야 검토 결과 회신이 가능합니다.");
return;
}
@@ -348,7 +322,7 @@ function FloatingActionButtons({
data,
checklistFormState,
"ENV",
isResultReply ? "EP" : "RM",
isResultReply ? "EP" : "RB",
"CP",
getReviewOpinionFormData(),
opnrNm,
@@ -357,21 +331,32 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
request: payload,
successMessage: isResultReply
? "검토결과 회신이 완료되었습니다."
: "재원협의 결과 회신이 완료되었습니다.",
successMessage: "검토 결과 회신이 완료되었습니다.",
});
techReviewOpinionRef?.current?.resetForm();
}
async function handleRqstTechRvwClick() {
if (prgrsSttsComCd === "LE") {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"ENV",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 기술 검토 의뢰가 가능합니다.");
return;
}
}
const payload: FnrsExamineRequestDto = {
prgrsSttsComCd: "FV",
};
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw,
request: payload,
successMessage: "기술검토 의뢰가 완료되었습니다.",
successMessage: "기술 검토 의뢰가 완료되었습니다.",
});
}
@@ -570,21 +555,11 @@ function FloatingActionButtons({
삭제
</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.lclRply) && (
<Button onClick={handleLclRplyClick} disabled={isProcessing}>
지자체 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && (
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
반려
@@ -597,7 +572,7 @@ function FloatingActionButtons({
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && (
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
{prgrsSttsComCd === "RR" ? "검토 결과 회신" : "재원협의 결과 회신"}
검토 결과 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
@@ -858,11 +833,9 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
strg: handleSaveApply,
aply: handleSaveApply,
del: handleDel,
fnrsCnsltnAncmnt: handleChangePrgrsSttsComCd,
envoSbmsn: handleChangePrgrsSttsComCd,
rjct: handleChangePrgrsSttsComCd,
sncnAprv: handleChangePrgrsSttsComCd,
lclRply: handleChangePrgrsSttsComCd,
rvwRply: handleTechReview,
rqstTechRvw: handleRequestExamine,
fileSplmnt: handleDmndSplmnt,
@@ -48,8 +48,8 @@ const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["RA", "RR", "EP", "TA"]);
interface PwrkCnsltnDocMngDialogProps {
open: boolean;
fnrsCnsltnDocNo: string;
atrzSeComCdList: string[];
prgrsSttsComCd: string;
atrzSeComCdList?: string[];
prgrsSttsComCd?: string;
canSave?: boolean;
onClose: () => void;
}
@@ -257,8 +257,10 @@ function PwrkCnsltnDocMngDialog({
}
const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]);
const viewOnly = VIEW_ONLY_PRGRS_STTS_COM_CDS.has(prgrsSttsComCd);
const canSaveByAtrz = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code));
const viewOnly = prgrsSttsComCd ? VIEW_ONLY_PRGRS_STTS_COM_CDS.has(prgrsSttsComCd) : false;
const canSaveByAtrz = atrzSeComCdList
? !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code))
: true;
const canSave = !viewOnly && (canSaveProp ?? canSaveByAtrz);
return (
@@ -96,7 +96,7 @@ function PwrkCnsltnRvwRsltInqDialog({
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>검토결과조회</DialogTitle>
<DialogTitle>검토 결과 조회</DialogTitle>
</DialogHeader>
{isLoading ? (
@@ -159,9 +159,9 @@ function CnsltnListTable({
return (
<Table variant="secondary">
<colgroup>
<col width="8%" />
<col width="42%" />
<col width="50%" />
<col width="4%" />
<col width="4%" />
<col width="92%" />
</colgroup>
<TableHeader className="sticky top-0">
<TableRow>
@@ -172,7 +172,7 @@ function CnsltnListTable({
aria-label="전체 선택"
/>
</TableHead>
<TableHead>분류</TableHead>
<TableHead>구분</TableHead>
<TableHead>검토의견</TableHead>
</TableRow>
</TableHeader>
@@ -194,8 +194,8 @@ function CnsltnListTable({
aria-label="행 선택"
/>
</TableCell>
<TableCell>
<p className="whitespace-pre-wrap">{formatOptional(row.rmrk)}</p>
<TableCell align="center">
<p className="whitespace-pre-wrap">{formatOptional(row.rvwOpnnSn)}</p>
</TableCell>
<TableCell>
<p className="whitespace-pre-wrap">{formatOptional(row.rvwOpnn)}</p>
@@ -47,6 +47,7 @@ import {
import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { PwrkCnsltnDocMngDialog } from "./components/pwrk-cnsltn-doc-mng-dialog";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
interface FnrsFormPageProps {
mode: "create" | "edit";
@@ -66,8 +67,10 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isPwrkCnsltnDocMngOpen, setIsPwrkCnsltnDocMngOpen] = useState(false);
const isEditMode = mode === "edit";
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
const fnrsCnsltnDocNo = initialData?.detDto?.fnrsCnsltnDocNo;
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree();
@@ -156,7 +159,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
(item) => !item.splmntCn || !item.splmntCn.trim(),
);
if (hasEmptySplmntCn) {
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
alert("보완요청내용이 있습니다. 보완 완료내용을 입력해 주세요.");
return;
}
@@ -171,12 +174,12 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
const result = await submitSupplementForm(data);
if (!result) return;
alert("보완완료 처리되었습니다.");
alert("보완 완료 처리되었습니다.");
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
router.push(`/fnrs-cnsltn/${docNo}`);
} catch (error) {
console.error("보완완료 실패:", error);
alert("보완완료 처리 중 오류가 발생했습니다.");
console.error("보완 완료 실패:", error);
alert("보완 완료 처리 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
@@ -334,14 +337,32 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
onClick={form.handleSubmit(onSplmntCmptn)}
disabled={isSubmitting}
>
보완완료
보완 완료
</Button>
)}
{isSplmntPrgrsStts && fnrsCnsltnDocNo ? (
<Button
color="orange"
type="button"
onClick={() => setIsPwrkCnsltnDocMngOpen(true)}
disabled={isSubmitting}
>
실무협의서 관리
</Button>
) : null}
</div>
</div>
</div>
</SectionCollapsibleGroup>
</form>
{fnrsCnsltnDocNo ? (
<PwrkCnsltnDocMngDialog
open={isPwrkCnsltnDocMngOpen}
fnrsCnsltnDocNo={fnrsCnsltnDocNo}
canSave
onClose={() => setIsPwrkCnsltnDocMngOpen(false)}
/>
) : null}
</FormProvider>
);
}
@@ -136,8 +136,7 @@ const TechReviewOpinionSection = forwardRef<
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
const hasKecoSection =
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
if (!hasEnvSection && !hasKecoSection) {
return null;
+2 -2
View File
@@ -54,9 +54,9 @@ export interface BizCstTreeDto {
bscPlanAmt: number;
/** 금회설계금액(C) */
tstmDesignAmt: number;
/** 증감금액(금회설계금액 - 표준사업비, C-B) */
/** 증감금액(금회설계금액 - 예산반영금액, C-A) */
chgAmt: number;
/** 증감율 % ((증감금액 / 표준사업비) * 100) */
/** 증감율 % ((증감금액 / 예산반영금액(A)) * 100) */
chgRt: number;
}
@@ -122,7 +122,7 @@ function findBizCstSyncItem(costItems: CostItemDef[]): CostItemDef | undefined {
}
export interface BizCstTotalAmounts {
bscPlanAmt: number;
bgtRfltAmt: number;
tstmDesignAmt: number;
}
@@ -136,13 +136,13 @@ export function getBizCstTotalRow(
costItems: CostItemDef[],
): BizCstTotalAmounts {
const syncCostItem = findBizCstSyncItem(costItems);
if (!syncCostItem) return { bscPlanAmt: 0, tstmDesignAmt: 0 };
if (!syncCostItem) return { bgtRfltAmt: 0, tstmDesignAmt: 0 };
const costRow = bizCstList.find((item) => item.acntgSeCd === syncCostItem.code);
if (!costRow) return { bscPlanAmt: 0, tstmDesignAmt: 0 };
if (!costRow) return { bgtRfltAmt: 0, tstmDesignAmt: 0 };
return {
bscPlanAmt: costRow.bscPlanAmt || 0,
bgtRfltAmt: costRow.bgtRfltAmt || 0,
tstmDesignAmt: costRow.tstmDesignAmt || 0,
};
}
@@ -178,7 +178,7 @@ export function calcBizCstSharRemaining(
const leafSum = sumFnrsSharLeafAmounts(sharList, sharItems);
return {
exstnRemaining: roundAmount(bizCstTotal.bscPlanAmt - leafSum.exstnAmt),
exstnRemaining: roundAmount(bizCstTotal.bgtRfltAmt - leafSum.exstnAmt),
chgRqtRemaining: roundAmount(bizCstTotal.tstmDesignAmt - leafSum.chgRqtAmt),
};
}
@@ -104,7 +104,7 @@ function BusinessCostFormSection() {
const data = bizCstList[idx];
if (!data) return null;
const diff = (data.tstmDesignAmt || 0) - (data.bscPlanAmt || 0);
const diff = (data.tstmDesignAmt || 0) - (data.bgtRfltAmt || 0);
const paddingLeft = `${item.level * 1.6}rem`;
const showSubIcon = item.level >= 2;
@@ -120,14 +120,14 @@ function BusinessCostFormSection() {
</TableHead>
<TableCell>
<Controller
name={`bizCstList.${idx}.bscPlanAmt`}
name={`bizCstList.${idx}.bgtRfltAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
@@ -138,14 +138,14 @@ function BusinessCostFormSection() {
</TableCell>
<TableCell>
<Controller
name={`bizCstList.${idx}.bgtRfltAmt`}
name={`bizCstList.${idx}.bscPlanAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
@@ -199,7 +199,7 @@ function SplmntDmndDialog({
try {
await attachService.deleteAttachmentByAtflId(atflId);
} catch (error) {
console.error("보완요청 첨부파일 삭제 실패:", error);
console.error("보완 요청 첨부파일 삭제 실패:", error);
}
}
@@ -226,7 +226,7 @@ function SplmntDmndDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[100rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>보완요청</DialogTitle>
<DialogTitle>보완 요청</DialogTitle>
</DialogHeader>
<div className="space-y-3">
@@ -322,7 +322,7 @@ function SplmntDmndDialog({
취소
</Button>
<Button type="button" onClick={handleConfirm} disabled={!canSubmit}>
보완요청
보완 요청
</Button>
</DialogFooter>
</DialogContent>