feat: 재원협의&실무협의서 로직 사이클

This commit is contained in:
2026-09-17 10:00:00 +09:00
parent e8a5701e73
commit b6b58eaeba
5 changed files with 82 additions and 68 deletions
@@ -71,8 +71,8 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
| 34 | 상세 | USER01 | 승인자 | `RK` | 결재(승인) | `RK` | 다음 결재자 | `process` |
| 35 | 상세 | USER01 | 최종승인자 | `RK` | 결재(회신) | `RR` | 환경청 | `process` 최종 |
| 36 | 상세 | USER03 | - | `RR` | 실무협의서 관리(조회), 검토결과 회신 | `EP` | 광역 | `tech-review` |
| 37 | 작성/상세 | USER04 | - | `EP` | 결과회신 | `TA` | 기초 | `updatePrgrsStts` |
| 38 | 상세 | USER05 | - | `TA` | 결과회신 | — | — | **API 없음. 조회** |
| 37 | 작성/상세 | USER04 | - | `EP` | 결과회신, 실무협의서 관리(조회) | `TA` | 기초 | `updatePrgrsStts` |
| 38 | 상세 | USER05 | - | `TA` | 결과회신, 실무협의서 관리(조회) | — | — | **API 없음. 조회** |
`SL` 20~22행: 버튼은 직전 상태에 따라 **하나만** 노출. 파일 루프면 [파일보완완료], 그 외 [보완완료].
@@ -109,13 +109,13 @@ ROLE_USER05
"" | SV → 저장, 신청완료, 삭제
SL → (직전 ER) 파일보완완료 | (직전 TE|RK) 보완완료
RB → 실무협의서 관리, 조치완료
TA → 결과회신 (조회)
TA → 결과회신 (조회), 실무협의서 관리(조회)
ROLE_USER04
ST → 환경청제출 // 현행 반려(rjct→SV)는 SoT 38행에 없음. 유지 시 별도 합의
RM → 지자체회신
RC → 조치결과서제출
EP → 결과회신
EP → 결과회신, 실무협의서 관리(조회)
ROLE_USER03
LE → 결과회신(자체종결), 기술검토 의뢰
@@ -39,12 +39,15 @@ 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],
TA: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt,
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],
EP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv, FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
ROLE_USER03: {
LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw],
@@ -61,16 +61,25 @@ export const TASK_CODE_LABELS: Record<string, string> = {
};
export const PBADMS_PROGRESS_ORDER = [
// LOCAL
"SV",
"SL",
"TA",
"ST",
"SL",
"RB",
"EP",
"TA",
// ENV
"LE",
"TP",
"RA",
"RR",
// KECO
"ER",
"TE",
"TB",
"EV",
"RK",
] as const;
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
@@ -262,18 +262,18 @@ 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 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 = {
@@ -323,7 +323,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv,
request: payload,
successMessage: "인가승인되었습니다.",
successMessage: "결과 회신이 완료되었습니다.",
});
}
@@ -570,11 +570,11 @@ function FloatingActionButtons({
삭제
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && (
{/* {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}>
환경청 제출
@@ -592,12 +592,12 @@ function FloatingActionButtons({
)}
{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}>
{prgrsSttsComCd === "RR" ? "검토결과 회신" : "재원협의 결과 회신"}
{prgrsSttsComCd === "RR" ? "검토 결과 회신" : "재원협의 결과 회신"}
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
@@ -722,13 +722,8 @@ function FloatingActionButtons({
open={activeDialog === "pwrkCnsltnDocMng"}
fnrsCnsltnDocNo={docNo}
atrzSeComCdList={atrzSeComCdList}
canSave={
prgrsSttsComCd === "RA" ||
prgrsSttsComCd === "RR" ||
(needsElaprPrgrsCheck && !hasPendingApprovalRequest)
? false
: undefined
}
prgrsSttsComCd={prgrsSttsComCd}
canSave={needsElaprPrgrsCheck && !hasPendingApprovalRequest ? false : undefined}
onClose={() => setActiveDialog(null)}
/>
@@ -43,10 +43,13 @@ const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["RA", "RR", "EP", "TA"]);
interface PwrkCnsltnDocMngDialogProps {
open: boolean;
fnrsCnsltnDocNo: string;
atrzSeComCdList: string[];
prgrsSttsComCd: string;
canSave?: boolean;
onClose: () => void;
}
@@ -66,6 +69,7 @@ function PwrkCnsltnDocMngDialog({
open,
fnrsCnsltnDocNo,
atrzSeComCdList,
prgrsSttsComCd,
canSave: canSaveProp,
onClose,
}: PwrkCnsltnDocMngDialogProps) {
@@ -73,7 +77,7 @@ function PwrkCnsltnDocMngDialog({
const alert = useAlertStore((state) => state.setMessage);
const [atchFileGroupId, setAtchFileGroupId] = useState("");
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(true);
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
const {
data: pwrkCnsltnData,
@@ -253,8 +257,9 @@ 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 canSave = canSaveProp ?? canSaveByAtrz;
const canSave = !viewOnly && (canSaveProp ?? canSaveByAtrz);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
@@ -289,39 +294,41 @@ function PwrkCnsltnDocMngDialog({
onDownload={handleDownload}
/>
</div>
{isPjtcoAjmtTableOpen ? (
<section className="flex h-[calc(64vh+4rem)] w-0 flex-[0.4] flex-col overflow-hidden rounded-md border border-border">
<header className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
<h3 className="text-xl font-semibold">금액검증결과</h3>
<button
type="button"
className="rounded-sm border border-border p-2 hover:opacity-90"
onClick={() => setIsPjtcoAjmtTableOpen(false)}
aria-label="접기"
>
<PanelRightClose className="size-6" />
</button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto">
<PwrkCnsltnDocPjtcoAjmtTable
rows={pjtcoAjmtPage?.content ?? []}
isLoading={isPjtcoAjmtLoading}
isError={isPjtcoAjmtError}
/>
</div>
</section>
) : (
<button
type="button"
className="flex items-center gap-1 self-start rounded-sm border border-border px-1 py-2 hover:opacity-90"
style={{ writingMode: "vertical-rl" }}
onClick={() => setIsPjtcoAjmtTableOpen(true)}
aria-label="펼치기"
>
<PanelRightOpen className="size-5" />
<span className="text-xl tracking-wide">금액검증결과</span>
</button>
)}
{pjtcoAjmtPage?.content?.length ? (
isPjtcoAjmtTableOpen ? (
<section className="flex h-[calc(64vh+4rem)] w-0 flex-[0.4] flex-col overflow-hidden rounded-md border border-border">
<header className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
<h3 className="text-xl font-semibold">금액검증결과</h3>
<button
type="button"
className="rounded-sm border border-border p-2 hover:opacity-90"
onClick={() => setIsPjtcoAjmtTableOpen(false)}
aria-label="접기"
>
<PanelRightClose className="size-6" />
</button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto">
<PwrkCnsltnDocPjtcoAjmtTable
rows={pjtcoAjmtPage?.content ?? []}
isLoading={isPjtcoAjmtLoading}
isError={isPjtcoAjmtError}
/>
</div>
</section>
) : (
<button
type="button"
className="flex items-center gap-1 self-start rounded-sm border border-border px-1 py-2 hover:opacity-90"
style={{ writingMode: "vertical-rl" }}
onClick={() => setIsPjtcoAjmtTableOpen(true)}
aria-label="펼치기"
>
<PanelRightOpen className="size-5" />
<span className="text-xl tracking-wide">금액검증결과</span>
</button>
)
) : null}
</div>
) : null}