Compare commits

...
41 Commits
Author SHA1 Message Date
jcchoi 74934d4559 수정
Deploy Frontend / deploy (push) Successful in 4m0s
2026-10-22 14:28:31 +09:00
jcchoi 027f88da6c 대시보드 ㅁ이상치리포트 명칭 변경 2026-10-22 14:24:21 +09:00
changwng 6dc2965209 [as fr] 필수첨부 파일에 필수파일검토내용에 CRUD 처리 및 AI 요청시 같이 전송처리
Deploy Frontend / deploy (push) Successful in 4m5s
2026-10-21 16:14:39 +09:00
jcchoi c69c1aa855 Merge branch 'iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 9m0s
2026-10-21 13:15:44 +09:00
jcchoi 9f0e03c151 이상치 선별보고서 2026-10-21 11:31:50 +09:00
jcchoi 471a431af5 대시보드 진행상태 변경 2026-10-21 10:37:32 +09:00
jcchoi 26d8410649 Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 5m0s
2026-10-20 21:52:31 +09:00
jcchoi 8403ad556c 설치인가 버튼 이벤트 수정 2026-10-20 21:50:41 +09:00
rsone001 d26c15f857 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-10-20 17:49:42 +09:00
rsone001 86ed664e3e [input] - 오류 로직 수정 2026-10-20 17:49:09 +09:00
jcchoi a105f395dd 설치인가 프로세스 변경 2026-10-20 15:50:05 +09:00
jcchoi a5ebc12580 검토의견 수정 오류 처리 2026-10-20 13:48:39 +09:00
jcchoi 270f6e366f Merge branch 'iipbs/st/dev' 2026-10-20 10:53:41 +09:00
jcchoi acf215c1bf 대시보드 이상치선별보고서 수정 2026-10-20 10:45:29 +09:00
jcchoi 8a98cd7d19 행정지원 검토의견 수정 2026-10-20 10:04:42 +09:00
jcchoi 3a5b48b608 통계 도움말 오류 수정 2026-10-20 10:04:26 +09:00
jcchoi d735ad7dd7 통계 파라미터 변경
Deploy Frontend / deploy (push) Successful in 1m57s
2026-10-19 21:28:01 +09:00
jcchoi 25dd0f1504 getVerifyResultList 파라미터 변경
Deploy Frontend / deploy (push) Successful in 2m2s
2026-10-19 19:17:01 +09:00
jcchoi 8e14c6cef5 Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-10-19 19:07:14 +09:00
jcchoi 9f7abe4413 필수첨부파일 위치 업데이트 2026-10-19 18:41:58 +09:00
rsone001 de730205af [ST] - 일반 검증 등록 및 수정 다이얼로그 css 수정 2026-10-19 17:50:49 +09:00
rsone001 a873f0e0dc [ST] - api 표준화 누락 수정 2026-10-19 15:14:58 +09:00
jcchoi 32e3765b52 초안 평가 단계 수정
Deploy Frontend / deploy (push) Successful in 1m53s
2026-10-15 19:56:54 +09:00
rsone001 4f62095e8e [ST] - fix: input 2-3 검증 cell 스타일 로직 수정 2026-10-15 16:03:40 +09:00
jcchoi 5234448ae5 기본계획 조회 전체 적용
Deploy Frontend / deploy (push) Successful in 1m40s
2026-10-15 15:53:38 +09:00
jcchoi f284c67988 hwp 서버 도메인 연결 2026-10-15 10:43:15 +09:00
jcchoi 2d77813dab Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 4m31s
2026-10-14 17:36:08 +09:00
rsone001 d22a255143 [ST] - fix: type 오류 수정 2026-10-14 17:35:32 +09:00
jcchoi 6703bbeeec Merge remote-tracking branch 'origin/iipbs/st/dev' 2026-10-14 17:25:16 +09:00
rsone001 d444c591ec [ST] - flx: input 표준화에 맞춰 소스 수정 2026-10-14 17:05:45 +09:00
rsone001 f7ddb6cae0 Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-10-14 15:39:21 +09:00
yangsein 7ffc02e887 refactor: 대시보드 통계처리현황 표준화 수정 2026-10-14 15:17:25 +09:00
yangsein cbacb27bb1 refactor: 행정동 갱신 표준화 수정 2026-10-14 15:16:40 +09:00
100JM 03a08a320a [ST] - fix: DB 표준화에 따른 소스 수정 2026-10-14 14:24:03 +09:00
jcchoi 45223be64a 포탈 수정
Deploy Frontend / deploy (push) Successful in 3m45s
2026-10-13 11:44:05 +09:00
jcchoi b22dfdfa4c 웹기안기 도메인 적용 2026-10-13 11:22:08 +09:00
jcchoi ce219f0e40 https 적용
Deploy Frontend / deploy (push) Successful in 4m5s
2026-10-13 10:53:39 +09:00
jcchoi a0f26d38b2 행정지원 리스트 디자인 cs 와 통일
Deploy Frontend / deploy (push) Successful in 3m42s
2026-10-12 19:19:13 +09:00
jcchoi 4efdcfe0fc 공통 테마 적용
Deploy Frontend / deploy (push) Successful in 7m55s
2026-10-12 18:47:26 +09:00
jcchoi 806ccf950f 행정지원 대시보드 테마 적용 2026-10-12 18:38:00 +09:00
jcchoi b96b78fa0b skin 및 레이아웃 작업 2026-10-12 17:22:37 +09:00
290 changed files with 9218 additions and 8797 deletions
@@ -33,10 +33,10 @@
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ [영역 B] 필수첨부파일 목록 │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 비고 | 등록일시 | 수정일시 | 관리 │
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 필수파일검토내용 | 비고 | 관리 │ │
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 서명본 첨부 | 2026-04-22 10:10:00 | — | [수정][삭제] │ │
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | — | 2026-04-22 11:00:00 | — | [수정][삭제] │ │
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 사업비 내역 검토 | 서명본 첨부 | [수정][삭제] │ │
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | 변경 전후 비교표 | — | [수정][삭제] │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
@@ -47,7 +47,9 @@
│ 필수첨부파일아이디(자동발급) [EAF-0004] │
│ 문서구분 [설치인가 ▼] │
│ 필수첨부파일명 [예: 사업계획서] │
│ 양식파일 [파일 선택] │
│ 필수첨부여부 [ ] 예 [ ] 아니오 │
│ 필수파일검토내용 [검토내용을 입력하세요]│
│ 비고 [비고를 입력하세요] │
├──────────────────────────────────────────────────────┤
│ [취소] [저장] │
@@ -139,16 +141,16 @@ END
### 5-2. 그리드 컬럼
| 컬럼명 | Field | 타입 | 비고 |
| ------------------ | ------------------- | ------ | ------------------------------------- |
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
| 수정일시 | `mdfcnDt` | string | 미수정 시 빈 값 또는 `—` |
| 관리 | — | — | 행별 **[수정] · [삭제]** |
| 컬럼명 | Field | 타입 | 비고 |
| ------------------ | -------------------- | ------ | ------------------------------------- |
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
| 필수파일검토내용 | `esntlAtchFileRvwCn` | string | 없으면 `—` 표시, 최대 2줄 clamp |
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
| 양식 내려받기 | `atchFileGroupId` | string | 양식파일 다운로드 버튼 |
| 관리 | — | — | 행별 **[수정] · [삭제]** |
- 행 클릭 시 별도 상세 화면 없음. 편집은 **[수정]** 버튼으로 다이얼로그 C를 연다.
@@ -167,13 +169,15 @@ END
### 6-1. 폼 필드
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| ------------------ | ---------- | ---- | ------ | ------------------- | -------------------------------- |
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
| ------------------ | ------------------- | ---- | ------ | -------------------- | ---------------------------------- |
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
| 양식파일 | AttachmentFileField | N | — | `atchFileGroupId` | TUS 파일 업로드 |
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
| 필수파일검토내용 | Textarea | N | — | `esntlAtchFileRvwCn` | placeholder: "검토내용을 입력하세요"|
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
### 6-2. 버튼
@@ -210,28 +214,32 @@ END
**응답 DTO**
| Field | 타입 | 설명 |
| ------------------- | ------- | -------------------------------- |
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
| `docSeComCd` | string | 문서구분공통코드 |
| `esntlAtchFileNm` | string | 필수첨부파일명 |
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
| `rgtr` | string? | 등록자 UUID |
| `rgtrNm` | string? | 등록자명 |
| `mdfr` | string? | 수정자 UUID |
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
| `mdfcnDt` | string? | 수정일시 |
| Field | 타입 | 설명 |
| -------------------- | -------------------------- | -------------------------------- |
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
| `docSeComCd` | string | 문서구분공통코드 |
| `esntlAtchFileNm` | string | 필수첨부파일명 |
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
| `esntlAtchFileRvwCn` | string? | 필수첨부파일검토내용 |
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
| `atchFileGroupId` | string? | 첨부파일그룹아이디 (양식파일) |
| `atchFileList` | AtflMngClientResponseDto[]?| 첨부파일 목록 |
| `rgtr` | string? | 등록자 UUID |
| `rgtrNm` | string? | 등록자명 |
| `mdfr` | string? | 수정자 UUID |
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
| `mdfcnDt` | string? | 수정일시 |
**저장 Request**
| Field | 필수 | 설명 |
| ------------------- | ---- | ------------------ |
| `esntlAtchFileId` | Y | 필수첨부파일아이디 |
| `docSeComCd` | N | 문서구분 |
| `esntlAtchFileNm` | N | 필수첨부파일명 |
| `esntlAtchYn` | N | 필수첨부여부 |
| `esntlAtchFileRmrk` | N | 비고 |
| Field | 필수 | 설명 |
| -------------------- | ---- | -------------------- |
| `docSeComCd` | Y | 문서구분 |
| `esntlAtchFileNm` | N | 필수첨부파일명 |
| `esntlAtchYn` | N | 필수첨부여부 |
| `esntlAtchFileRvwCn` | N | 필수첨부파일검토내용 |
| `esntlAtchFileRmrk` | N | 비고 |
| `atchFileGroupId` | N | 첨부파일그룹아이디 |
**검색 Query**
+1 -1
View File
@@ -3,7 +3,7 @@ import type { NextConfig } from "next";
const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
const webhwpCtrlBase = (
process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "http://hwp.onasys.co.kr/webhwpctrl"
process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "https://hwp.onasys.co.kr/webhwpctrl"
).replace(/\/$/, "");
const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const;
+44 -27
View File
@@ -5,18 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>웹한글 기안기 브리지</title>
<script
type="text/javascript"
src="http://hwp.onasys.co.kr/webhwpctrl/js/hwpctrlapp/utils/util.js"
></script>
<script
type="text/javascript"
src="http://hwp.onasys.co.kr/webhwpctrl/js/hwpctrlapp/hwpCtrlApp.js"
></script>
<script
type="text/javascript"
src="http://hwp.onasys.co.kr/webhwpctrl/js/webhwpctrl.js"
></script>
<style>
html,
body,
@@ -39,27 +27,56 @@
<div id="hwpctrl"></div>
<script type="text/javascript">
(function () {
// baseurl: 부모(HangulGiangi)가 NEXT_PUBLIC_WEBHYP_CTRL_URL 값으로 전달.
// 없으면 same-origin 프록시 경로(/webhwpctrl/ → next.config rewrites)를 사용한다.
var params = new URLSearchParams(window.location.search);
var baseUrl = params.get("baseurl") || "http://hwp.onasys.co.kr/webhwpctrl";
var baseUrl = params.get("baseurl") || new URL("./", window.location.href).href;
if (baseUrl.charAt(baseUrl.length - 1) !== "/") {
baseUrl += "/";
}
var HwpCtrl = BuildWebHwpCtrl(
"hwpctrl",
baseUrl,
function () {
try {
window.parent.postMessage({ type: "hangulgiangi-ready" }, "*");
} catch (e) {
console.warn(e);
}
},
2,
);
var scripts = [
"js/hwpctrlapp/utils/util.js",
"js/hwpctrlapp/hwpCtrlApp.js",
"js/webhwpctrl.js",
];
// 부모(HangulGiangi)가 contentWindow.HwpCtrl 로 접근
window.HwpCtrl = HwpCtrl;
function loadScript(index) {
if (index >= scripts.length) {
init();
return;
}
var script = document.createElement("script");
script.type = "text/javascript";
script.src = baseUrl + scripts[index];
script.onload = function () {
loadScript(index + 1);
};
script.onerror = function () {
console.error("웹한글 기안기 스크립트 로드 실패: " + script.src);
};
document.head.appendChild(script);
}
function init() {
var HwpCtrl = BuildWebHwpCtrl(
"hwpctrl",
baseUrl,
function () {
try {
window.parent.postMessage({ type: "hangulgiangi-ready" }, "*");
} catch (e) {
console.warn(e);
}
},
2,
);
// 부모(HangulGiangi)가 contentWindow.HwpCtrl 로 접근
window.HwpCtrl = HwpCtrl;
}
loadScript(0);
})();
</script>
</body>
+5 -1
View File
@@ -13,12 +13,16 @@ import type {
NowDmgMtncGnsvySearchDto,
} from "../model/dc-interface";
/** 빈 문자열 파라미터를 보내면 서버 오류가 나므로 값이 있는 항목만 전송한다. */
async function getNowDmgMtncGnsvy(
params: NowDmgMtncGnsvySearchDto,
): Promise<NowDmgMtncGnsvyResponseDto[]> {
const filledParams = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== undefined && value !== ""),
);
const response = await client.get<NowDmgMtncGnsvyResponseDto[]>(
`${ENDPOINT.DC_SERVICE}/nowDmgMtncGnsvy`,
{ params },
{ params: filledParams },
);
return response.data;
}
@@ -19,6 +19,8 @@ export interface EsntlAtchFileMng {
esntlAtchYn: "Y" | "N";
/** 필수첨부파일비고 */
esntlAtchFileRmrk?: string;
/** 필수첨부파일검토내용 */
esntlAtchFileRvwCn?: string;
/** 첨부파일그룹아이디 (양식파일 그룹) */
atchFileGroupId?: string;
/** 첨부파일 목록 (양식파일) */
@@ -77,6 +79,8 @@ export interface EsntlAtchFileMngSaveRequest {
esntlAtchYn?: "Y" | "N";
/** 필수첨부파일비고 */
esntlAtchFileRmrk?: string;
/** 필수첨부파일검토내용 */
esntlAtchFileRvwCn?: string;
/** 첨부파일그룹아이디 (양식파일 그룹) */
atchFileGroupId?: string;
}
@@ -0,0 +1,22 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
const BASE = ENDPOINT.AS_SERVICE.FNRS_CNSLTN_RVW_OPNN;
async function update(
fnrsCnsltnDocNo: string,
rvwOpnnSn: number,
body: RvwOpnnUpdateRequestDto,
): Promise<void> {
await client.put<void>(`${BASE}/${fnrsCnsltnDocNo}/${rvwOpnnSn}`, body);
}
async function deleteFn(fnrsCnsltnDocNo: string, rvwOpnnSn: number): Promise<void> {
await client.delete<void>(`${BASE}/${fnrsCnsltnDocNo}/${rvwOpnnSn}`);
}
export const fnrsCnsltnRvwOpnnService = {
update,
delete: deleteFn,
};
+66 -65
View File
@@ -1,65 +1,66 @@
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
export { fnrsCnsltnKeys } from "./api/queries";
export {
useChangePrgrsSttsComCd,
useDeleteFnrsCnsltnMng,
useFnrsCnsltnFileVrfcResults,
useFnrsCnsltnMng,
useFnrsCnsltnMngPage,
useRequestActnReviewFnrsCnsltnMng,
useRequestExamineFnrsCnsltnMng,
useSaveApplyFnrsCnsltnMng,
useSaveFnrsCnsltnMng,
useSubmitReqSupplementFnrsCnsltnMng,
useSubmitSupplementFnrsCnsltnMng,
useTechReviewFnrsCnsltnMng,
useUpdateFnrsCnsltnMng,
} from "./api/queries";
export type {
BizCstItem,
BzScaleItem,
CklstResultItem,
EsntlAtchFileItem,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnRvwOpnnSaveRequestDto,
FnrsCnsltnSplmntDmndItem,
FnrsSharItem,
} from "./model/fnrs-cnsltn-apply-payload-schema";
export {
fnrsCnsltnApplySaveSchema,
fnrsCnsltnBizCstItemSchema,
fnrsCnsltnBzScaleItemSchema,
fnrsCnsltnCklstResultItemSchema,
fnrsCnsltnEsntlAtchFileItemSchema,
fnrsCnsltnMngSaveSchema,
fnrsCnsltnRvwOpnnSaveSchema,
fnrsCnsltnSplmntDmndItemSchema,
fnrsSharItemSchema,
} from "./model/fnrs-cnsltn-apply-payload-schema";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
BizSclTreeDto,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
FnrsCnsltnRvwOpnnResponseDto,
FnrsCnsltnSplmntDmndResponseDto,
RagIndexingInfoDto,
} from "./model/fnrs-cnsltn-interface";
export type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
} from "./model/fnrs-cnsltn-payload-schema";
export {
envoActnReviewReqSchema,
fnrsExamineRequestSchema,
} from "./model/fnrs-cnsltn-payload-schema";
export { fnrsCnsltnRvwOpnnService } from "./api/fnrs-cnsltn-rvw-opnn-service";
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
export { fnrsCnsltnKeys } from "./api/queries";
export {
useChangePrgrsSttsComCd,
useDeleteFnrsCnsltnMng,
useFnrsCnsltnFileVrfcResults,
useFnrsCnsltnMng,
useFnrsCnsltnMngPage,
useRequestActnReviewFnrsCnsltnMng,
useRequestExamineFnrsCnsltnMng,
useSaveApplyFnrsCnsltnMng,
useSaveFnrsCnsltnMng,
useSubmitReqSupplementFnrsCnsltnMng,
useSubmitSupplementFnrsCnsltnMng,
useTechReviewFnrsCnsltnMng,
useUpdateFnrsCnsltnMng,
} from "./api/queries";
export type {
BizCstItem,
BzScaleItem,
CklstResultItem,
EsntlAtchFileItem,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnRvwOpnnSaveRequestDto,
FnrsCnsltnSplmntDmndItem,
FnrsSharItem,
} from "./model/fnrs-cnsltn-apply-payload-schema";
export {
fnrsCnsltnApplySaveSchema,
fnrsCnsltnBizCstItemSchema,
fnrsCnsltnBzScaleItemSchema,
fnrsCnsltnCklstResultItemSchema,
fnrsCnsltnEsntlAtchFileItemSchema,
fnrsCnsltnMngSaveSchema,
fnrsCnsltnRvwOpnnSaveSchema,
fnrsCnsltnSplmntDmndItemSchema,
fnrsSharItemSchema,
} from "./model/fnrs-cnsltn-apply-payload-schema";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
BizSclTreeDto,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
FnrsCnsltnRvwOpnnResponseDto,
FnrsCnsltnSplmntDmndResponseDto,
RagIndexingInfoDto,
} from "./model/fnrs-cnsltn-interface";
export type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
} from "./model/fnrs-cnsltn-payload-schema";
export {
envoActnReviewReqSchema,
fnrsExamineRequestSchema,
} from "./model/fnrs-cnsltn-payload-schema";
@@ -0,0 +1,22 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
const BASE = ENDPOINT.AS_SERVICE.INSTL_SNCN_RVW_OPNN;
async function update(
instlSncnDocNo: string,
rvwOpnnSn: number,
body: RvwOpnnUpdateRequestDto,
): Promise<void> {
await client.put<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`, body);
}
async function deleteFn(instlSncnDocNo: string, rvwOpnnSn: number): Promise<void> {
await client.delete<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`);
}
export const instlSncnRvwOpnnService = {
update,
delete: deleteFn,
};
+1
View File
@@ -1,3 +1,4 @@
export { instlSncnRvwOpnnService } from "./api/instl-sncn-rvw-opnn-service";
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
export {
instlSncnKeys,
@@ -0,0 +1,23 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
const BASE = ENDPOINT.AS_SERVICE.SEWER_BSC_PLAN_RVW_OPNNS;
async function update(
sewerBscPlanDocNo: string,
chgCycl: number,
rvwOpnnSn: number,
body: RvwOpnnUpdateRequestDto,
): Promise<void> {
await client.put<void>(`${BASE}/${sewerBscPlanDocNo}/${chgCycl}/${rvwOpnnSn}`, body);
}
async function deleteFn(sewerBscPlanDocNo: string, chgCycl: number, rvwOpnnSn: number): Promise<void> {
await client.delete<void>(`${BASE}/${sewerBscPlanDocNo}/${chgCycl}/${rvwOpnnSn}`);
}
export const sewerBscPlanRvwOpnnService = {
update,
delete: deleteFn,
};
+65 -64
View File
@@ -1,64 +1,65 @@
export { sewerBscPlanKeys } from "./api/queries";
export {
useDeleteSewerBscPlan,
useSaveApplySewerBscPlan,
useSaveSewerBscPlan,
useSewerBscPlan,
useSewerBscPlanDetails,
useSewerBscPlanPage,
useSubmitReqSupplementSewerBscPlan,
useSubmitSupplementSewerBscPlan,
useTechReviewSewerBscPlan,
useUpdateSewerBscPlan,
} from "./api/queries";
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
export type {
CklstResultItem,
EsntlAtchFileItem,
SewerBscPlanApplySaveRequestDto,
SewerBscPlanBizCstItem,
SewerBscPlanDtlSaveRequestDto,
SewerBscPlanFnrsCstItem,
SewerBscPlanOtlnItem,
SewerBscPlanRvwOpnnItem,
SewerBscPlanSaveRequestDto,
SewerBscPlanSplmntDmndItem,
SewerBscPlanTmplntDtlItem,
SewerBscPlanTmplntItem,
SewerBscPlanTmplntTmprStrgItem,
} from "./model/sewer-bsc-plan-apply-payload-schema";
export {
sewerBscPlanApplySaveSchema,
sewerBscPlanBizCstItemSchema,
sewerBscPlanCklstResultItemSchema,
sewerBscPlanDtlSaveSchema,
sewerBscPlanEsntlAtchFileItemSchema,
sewerBscPlanFnrsCstItemSchema,
sewerBscPlanOtlnItemSchema,
sewerBscPlanRvwOpnnItemSchema,
sewerBscPlanSaveSchema,
sewerBscPlanSplmntDmndItemSchema,
sewerBscPlanTmplntDtlItemSchema,
sewerBscPlanTmplntItemSchema,
sewerBscPlanTmplntTmprStrgItemSchema,
} from "./model/sewer-bsc-plan-apply-payload-schema";
export type {
AtflMngClientResponseDto,
CklstRvwRsltTreeDto,
EsntlAtchFileMngTreeResponseDto,
SewerBscPlanApplySaveResponseDto,
SewerBscPlanBizCstResponseDto,
SewerBscPlanDetResponseDto,
SewerBscPlanDtlResponseDto,
SewerBscPlanFnrsCstResponseDto,
SewerBscPlanOtlnResponseDto,
SewerBscPlanResponseDto,
SewerBscPlanRvwOpnnResponseDto,
SewerBscPlanSearchRequestDto,
SewerBscPlanService,
SewerBscPlanSplmntDmndResponseDto,
SewerBscPlanTmplntDtlResponseDto,
SewerBscPlanTmplntResponseDto,
SewerBscPlanTmplntTmprStrgResponseDto,
SewerBscPlanUpdateRequestDto,
} from "./model/sewer-bsc-plan-interface";
export { sewerBscPlanKeys } from "./api/queries";
export {
useDeleteSewerBscPlan,
useSaveApplySewerBscPlan,
useSaveSewerBscPlan,
useSewerBscPlan,
useSewerBscPlanDetails,
useSewerBscPlanPage,
useSubmitReqSupplementSewerBscPlan,
useSubmitSupplementSewerBscPlan,
useTechReviewSewerBscPlan,
useUpdateSewerBscPlan,
} from "./api/queries";
export { sewerBscPlanRvwOpnnService } from "./api/sewer-bsc-plan-rvw-opnn-service";
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
export type {
CklstResultItem,
EsntlAtchFileItem,
SewerBscPlanApplySaveRequestDto,
SewerBscPlanBizCstItem,
SewerBscPlanDtlSaveRequestDto,
SewerBscPlanFnrsCstItem,
SewerBscPlanOtlnItem,
SewerBscPlanRvwOpnnItem,
SewerBscPlanSaveRequestDto,
SewerBscPlanSplmntDmndItem,
SewerBscPlanTmplntDtlItem,
SewerBscPlanTmplntItem,
SewerBscPlanTmplntTmprStrgItem,
} from "./model/sewer-bsc-plan-apply-payload-schema";
export {
sewerBscPlanApplySaveSchema,
sewerBscPlanBizCstItemSchema,
sewerBscPlanCklstResultItemSchema,
sewerBscPlanDtlSaveSchema,
sewerBscPlanEsntlAtchFileItemSchema,
sewerBscPlanFnrsCstItemSchema,
sewerBscPlanOtlnItemSchema,
sewerBscPlanRvwOpnnItemSchema,
sewerBscPlanSaveSchema,
sewerBscPlanSplmntDmndItemSchema,
sewerBscPlanTmplntDtlItemSchema,
sewerBscPlanTmplntItemSchema,
sewerBscPlanTmplntTmprStrgItemSchema,
} from "./model/sewer-bsc-plan-apply-payload-schema";
export type {
AtflMngClientResponseDto,
CklstRvwRsltTreeDto,
EsntlAtchFileMngTreeResponseDto,
SewerBscPlanApplySaveResponseDto,
SewerBscPlanBizCstResponseDto,
SewerBscPlanDetResponseDto,
SewerBscPlanDtlResponseDto,
SewerBscPlanFnrsCstResponseDto,
SewerBscPlanOtlnResponseDto,
SewerBscPlanResponseDto,
SewerBscPlanRvwOpnnResponseDto,
SewerBscPlanSearchRequestDto,
SewerBscPlanService,
SewerBscPlanSplmntDmndResponseDto,
SewerBscPlanTmplntDtlResponseDto,
SewerBscPlanTmplntResponseDto,
SewerBscPlanTmplntTmprStrgResponseDto,
SewerBscPlanUpdateRequestDto,
} from "./model/sewer-bsc-plan-interface";
@@ -157,3 +157,33 @@
border-color: #96c3ff;
box-shadow: 0 4px 12px rgba(0, 107, 248, 0.3);
}
/* 블랙 스킨 (onasys 디자인 design/c 색상 기준) */
:global([data-theme="black"]) .basinLabel {
background: #404244ee;
border-color: #5e6165;
color: #f1f1f1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
:global([data-theme="black"]) .basinLabel.active {
background: #0056b3;
border-color: #96c3ff;
color: #fff;
}
:global([data-theme="black"]) .popup {
background: #404244;
border-color: #4e5154;
color: #f1f1f1;
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.5);
}
:global([data-theme="black"]) .popup > div {
border-color: #4e5154;
}
:global([data-theme="black"]) .popup > div:first-child {
background: #4e5154;
color: #fff;
}
@@ -40,12 +40,12 @@ export function DashboardNotiList({
);
return (
<section className="card card-border m-0 bg-[#FAFBFC]">
<section className="card card-border m-0">
<div className="card-header flex items-center justify-between gap-4">
<h3 className="text-2xl font-semibold">
알림이력
{dataUpdatedAt > 0 ? (
<span className="pl-2 text-lg font-medium text-[#4A4A4A]">
<span className="pl-2 text-lg font-medium text-label">
(업데이트 일시: {dayjs(dataUpdatedAt).format("YYYY-MM-DD HH:mm")})
</span>
) : null}
@@ -75,7 +75,7 @@ export function DashboardNotiList({
) : null}
{isError ? (
<p className="flex min-h-[12rem] items-center justify-center text-[#4A4A4A]">
<p className="flex min-h-[12rem] items-center justify-center text-label">
알림이력을 불러오지 못했습니다.
</p>
) : null}
@@ -110,7 +110,7 @@ interface DashboardNotiRowProps {
function DashboardNotiRow({ row, isActionPending, onAction }: DashboardNotiRowProps) {
return (
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-4 border-b px-2 py-4 last:border-b-0">
<span className="text-lg text-[#4A4A4A]">
<span className="text-lg text-label">
{row.regDt ? dayjs(row.regDt).format("YYYY-MM-DD") : "-"}
</span>
<p className="min-w-0 line-clamp-2 text-xl sm:line-clamp-1">{row.notiTtl}</p>
@@ -46,7 +46,7 @@ export function DashboardNoticeTable() {
});
return (
<section className="card card-border m-0 bg-[#FAFBFC]">
<section className="card card-border m-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">공지사항</h3>
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" onClick={handleMoreClick}>
@@ -11,7 +11,8 @@ export function DashboardPage() {
const { data, isPending, isError, dataUpdatedAt } = useGetMainDashboard();
return (
<div className="flex min-h-full gap-4 bg-[#E9EDF2] px-[1.8rem] pb-6 pt-4 text-[#1E1E1E]">
// 배경·글자색은 테마 변수를 따름 (배경 --container, 글자 --foreground)
<div className="flex min-h-full gap-4 bg-container px-[1.8rem] pb-6 pt-4 text-foreground">
<div className="hidden flex-[0.5] sm:block">
<NationalAsMap basinDocPrgrsList={data?.basinDocPrgrsList} />
</div>
@@ -24,7 +24,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
if (isError) {
return (
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center bg-[#FAFBFC] text-[#4A4A4A]">
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center text-label">
진행상황을 불러오지 못했습니다.
</div>
);
@@ -39,7 +39,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
return (
<div
key={slot.docSeComCd}
className="card card-border hover:bg-muted/40 m-0 block bg-[#FAFBFC] transition-colors"
className="card card-border hover:bg-muted/40 m-0 block transition-colors"
>
<div className="card-header">
<h3 className="text-2xl font-semibold">{slot.title}</h3>
@@ -90,7 +90,7 @@ function DocPrgrsSttsMetric({
>
<Icon className="size-6" />
</span>
<span className="text-xl text-[#4A4A4A]">{label}</span>
<span className="text-xl text-label">{label}</span>
<span className="text-3xl font-semibold tabular-nums">{count}</span>
</div>
);
@@ -108,21 +108,22 @@ const STAT_METRICS = [
field: "reqCnt",
label: "신청",
iconClassName: "bg-[#4285F4]",
bgColor: "bg-blue-100",
// 블랙 스킨: 아이콘 색 반투명 배경
bgColor: "bg-blue-100 [[data-theme=black]_&]:bg-[rgba(66,133,244,0.18)]",
Icon: FilePlus,
},
{
field: "prgrsCnt",
label: "진행",
iconClassName: "bg-[#34A853]",
bgColor: "bg-emerald-100",
bgColor: "bg-emerald-100 [[data-theme=black]_&]:bg-[rgba(52,168,83,0.18)]",
Icon: RefreshCw,
},
{
field: "cmptnCnt",
label: "완료",
iconClassName: "bg-[#EA4335]",
bgColor: "bg-orange-100",
bgColor: "bg-orange-100 [[data-theme=black]_&]:bg-[rgba(234,67,53,0.18)]",
Icon: CircleCheck,
},
] as const;
@@ -18,7 +18,7 @@ export function NationalAsMap({ basinDocPrgrsList }: NationalAsMapProps) {
const popupColumnSpan = Math.max(1, popupContent.items.length);
return (
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col bg-[#FAFBFC]">
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col">
<div className="card-header">
<h3 className="text-2xl font-semibold">전국 행정지원 지도</h3>
</div>
+183 -169
View File
@@ -1,169 +1,183 @@
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
const MAIN_STEP_LABELS = {
apply: "설계자문 신청",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "설계자문 신청 (시군)", sttsComCd: "CT" },
{ label: "기술검토", sttsComCd: "CS" },
{ label: "기술검토 회신", sttsComCd: "CP" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
if (prgrsSttsComCd === "CV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "CT" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "CP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
}
function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd);
return (
<div className="flex flex-wrap items-center justify-start">
{badges.map((badge, index) => (
<Fragment key={`${badge.label}-${index}`}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
);
}
export function createDesignCnstnColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("bizNm", {
header: "사업명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("aplcntNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => <ProgressStepBadge prgrsSttsComCd={row.original.prgrsSttsComCd || ""} />,
}),
columnHelper.accessor("regDt", {
header: "신청일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
const MAIN_STEP_LABELS = {
apply: "설계자문 신청",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "설계자문 신청 (시군)", sttsComCd: "CT" },
{ label: "기술검토", sttsComCd: "CS" },
{ label: "기술검토 회신", sttsComCd: "CP" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
if (prgrsSttsComCd === "CV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "CT" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "CP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
function splitStepLabel(label: string) {
const [title = "", ...rest] = label.split(" ");
return { title, detail: rest.join(" ") };
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
}
function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd);
return (
<div className="flex flex-wrap items-center justify-center">
{badges.map((badge, index) => (
<Fragment key={`${badge.label}-${index}`}>
{index > 0 && <span className="mx-2">&gt;</span>}
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
<Badge
color={getBadgeColor(badge.state)}
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
>
<span>{splitStepLabel(badge.label).title}</span>
<span>
{splitStepLabel(badge.label).detail}
{getBadgeStateText(badge.state)}
</span>
</Badge>
</Fragment>
))}
</div>
);
}
export function createDesignCnstnColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("bizNm", {
header: "사업명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("aplcntNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => <ProgressStepBadge prgrsSttsComCd={row.original.prgrsSttsComCd || ""} />,
}),
columnHelper.accessor("regDt", {
header: "신청일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
@@ -77,8 +77,8 @@ function DesignListPage() {
};
return (
<div className="space-y-4 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background">
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
<div className="flex items-center gap-2">
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
@@ -145,7 +145,7 @@ function DesignListPage() {
</div>
<Button type="submit">조회</Button>
</form>
</section>
</div>
<DesignList
searchParams={searchParams}
@@ -78,15 +78,15 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
return (
<>
<section className="card card-border mx-0 bg-background pb-8 pt-0">
<div className="card card-border pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">설계자문 목록</h3>
<h3 className="text-2xl font-medium text-label">설계자문 목록</h3>
<Button type="button" color="green" onClick={() => setIsTaskSeDialogOpen(true)}>
신청
</Button>
</div>
{tableComponent}
</section>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
<PbadmsTaskApplyDialog
open={isTaskSeDialogOpen}
onOpenChange={setIsTaskSeDialogOpen}
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
@@ -16,9 +17,9 @@ function BasicInfoFormSection() {
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
const localGovLabel = [watch("designCnstnMng.sido"), watch("designCnstnMng.gugun")]
.filter(Boolean)
.join(" ");
const sido = watch("designCnstnMng.sido");
const gugun = watch("designCnstnMng.gugun");
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
const { data: taskOptions } = useCodeList("CNFM");
function handleBnsSelect(data: BnsApplyData) {
@@ -36,6 +37,7 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -2,6 +2,7 @@
import { SectionCollapsible } from "@/shared/ui";
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
function BasicInfoSection({ det }: BasicInfoSectionProps) {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -1,338 +1,334 @@
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type {
ReviewOpinionFormData,
ReviewOpinionGetFormDataResult,
} from "../../lib/review-opinion-utils";
import type { ReviewStepCode } from "../../model/view-types";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: DesignCnstnRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionGetFormDataResult | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: DesignCnstnRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
return null;
}
return {
formData: {
...formData,
opnrSeComCd: editableReviewStep,
},
rvwList,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteOpinion, editableReviewStep, formData, rvwList],
);
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasKecoSection =
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
if (!hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<TableHeader>
<TableRow>
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">첨부파일</TableHead>
<TableCell>
{uploadedFile ? (
<div className="flex items-center gap-2">
<TextShimmer className="text-xl" active={uploading}>
{uploadedFile.fileName}
</TextShimmer>
<Button
size="icon"
onClick={(e) => {
e.preventDefault();
void handleFileRemove();
}}
disabled={uploading}
className="size-8 !bg-destructive"
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
</div>
) : (
<label>
<input
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(event.target.files);
event.target.value = "";
}}
/>
<Button size="sm" variant="outline" type="button" asChild>
<span>파일 선택</span>
</Button>
</label>
)}
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
);
});
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.designCnstnDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
</TableCell>
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
export { TechReviewOpinionSection };
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import {
designCnstnKeys,
type DesignCnstnRvwOpnnResponseDto,
designCnstnRvwOpnnService,
} from "@/entities/design-cnstn";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type {
ReviewOpinionFormData,
ReviewOpinionGetFormDataResult,
} from "../../lib/review-opinion-utils";
import type { ReviewStepCode } from "../../model/view-types";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: DesignCnstnRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionGetFormDataResult | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: DesignCnstnRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
return null;
}
return {
formData: {
...formData,
opnrSeComCd: editableReviewStep,
},
rvwList,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteOpinion, editableReviewStep, formData, rvwList],
);
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasKecoSection =
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
if (!hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
return editableReviewStep === reviewStep && canWriteOpinion;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead scope="row" className="w-[12rem]">
의견내용
</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">첨부파일</TableHead>
<TableCell>
{uploadedFile ? (
<div className="flex items-center gap-2">
<TextShimmer className="text-xl" active={uploading}>
{uploadedFile.fileName}
</TextShimmer>
<Button
size="icon"
onClick={(e) => {
e.preventDefault();
void handleFileRemove();
}}
disabled={uploading}
className="size-8 !bg-destructive"
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
</div>
) : (
<label>
<input
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(event.target.files);
event.target.value = "";
}}
/>
<Button size="sm" variant="outline" type="button" asChild>
<span>파일 선택</span>
</Button>
</label>
)}
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
);
});
function getRvwOpnnIds(item: DesignCnstnRvwOpnnResponseDto) {
if (!item.designCnstnDocNo || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.designCnstnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.designCnstnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await designCnstnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
}}
onDelete={async (item) => {
await designCnstnRvwOpnnService.delete(...getRvwOpnnIds(item));
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
}}
/>
);
}
export { TechReviewOpinionSection };
@@ -51,6 +51,19 @@ export function createEsntlAtchFileColumns({
meta: { align: "center" },
cell: ({ getValue }) => formatYn(getValue()),
}),
columnHelper.accessor("esntlAtchFileRvwCn", {
header: "필수파일검토내용",
meta: { align: "left" },
cell: ({ getValue }) => {
const val = getValue();
if (!val?.trim()) return "-";
return (
<span className="line-clamp-2 max-w-[20rem] text-left" title={val}>
{val}
</span>
);
},
}),
columnHelper.accessor("esntlAtchFileRmrk", {
header: "비고",
meta: { align: "center" },
@@ -26,6 +26,7 @@ export interface EsntlAtchFileFormValues {
docSeComCd: string;
esntlAtchFileNm: string;
esntlAtchYn: "Y" | "N";
esntlAtchFileRvwCn: string;
esntlAtchFileRmrk: string;
atchFileGroupId: string;
}
@@ -68,6 +69,7 @@ function EsntlAtchFileFormDialog({
docSeComCd: "",
esntlAtchFileNm: "",
esntlAtchYn: "Y",
esntlAtchFileRvwCn: "",
esntlAtchFileRmrk: "",
atchFileGroupId: "",
},
@@ -115,6 +117,7 @@ function EsntlAtchFileFormDialog({
docSeComCd: initial.docSeComCd ?? "",
esntlAtchFileNm: initial.esntlAtchFileNm ?? "",
esntlAtchYn: initial.esntlAtchYn ?? "Y",
esntlAtchFileRvwCn: initial.esntlAtchFileRvwCn ?? "",
esntlAtchFileRmrk: initial.esntlAtchFileRmrk ?? "",
atchFileGroupId: initial.atchFileGroupId ?? "",
});
@@ -127,6 +130,7 @@ function EsntlAtchFileFormDialog({
docSeComCd: "",
esntlAtchFileNm: "",
esntlAtchYn: "Y",
esntlAtchFileRvwCn: "",
esntlAtchFileRmrk: "",
atchFileGroupId: "",
});
@@ -249,6 +253,17 @@ function EsntlAtchFileFormDialog({
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label htmlFor="esntlAtchFileRvwCn">필수파일검토내용</Label>
<Textarea
id="esntlAtchFileRvwCn"
rows={3}
placeholder="검토내용을 입력하세요"
maxLength={4000}
{...register("esntlAtchFileRvwCn")}
className="!text-xl"
/>
</div>
<div className="space-y-2">
<Label htmlFor="esntlAtchFileRmrk">비고</Label>
<Textarea
@@ -100,7 +100,7 @@ function EsntlAtchFileList({
manualPagination: true,
},
dataTableOptions: {
caption: "필수첨부파일 목록으로 필수첨부파일아이디, 문서구분, 필수첨부파일명, 필수첨부여부, 비고, 양식 내려받기, 관리 정보 제공",
caption: "필수첨부파일 목록으로 필수첨부파일아이디, 문서구분, 필수첨부파일명, 필수첨부여부, 필수파일검토내용, 비고, 양식 내려받기, 관리 정보 제공",
viewPagination: true,
},
});
@@ -112,6 +112,7 @@ function EsntlAtchFileList({
docSeComCd: values.docSeComCd,
esntlAtchFileNm: values.esntlAtchFileNm,
esntlAtchYn: values.esntlAtchYn,
esntlAtchFileRvwCn: values.esntlAtchFileRvwCn || undefined,
esntlAtchFileRmrk: values.esntlAtchFileRmrk || undefined,
atchFileGroupId: values.atchFileGroupId || undefined,
};
@@ -136,14 +137,14 @@ function EsntlAtchFileList({
};
return (
<section className="card card-border mx-0 bg-background pt-0">
<div className="card card-border pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">필수첨부파일 목록</h3>
<h3 className="text-2xl font-medium text-label">필수첨부파일 목록</h3>
<Button type="button" color="green" onClick={handleOpenCreate}>
등록
</Button>
</div>
{tableComponent}
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
{dialogMode ? (
<EsntlAtchFileFormDialog
mode={dialogMode}
@@ -159,7 +160,7 @@ function EsntlAtchFileList({
onSubmit={handleFormSubmit}
/>
) : null}
</section>
</div>
);
}
@@ -52,8 +52,8 @@ function EsntlAtchFileMngPage() {
};
return (
<div className="space-y-6 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background">
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex flex-wrap items-end gap-4 p-4" onSubmit={handleSearch}>
<div className="flex flex-wrap items-center gap-3">
<Label htmlFor="docSeComCd">문서구분</Label>
@@ -79,7 +79,7 @@ function EsntlAtchFileMngPage() {
<Button type="submit">조회</Button>
</div>
</form>
</section>
</div>
<EsntlAtchFileList
searchParams={searchParams}
+230 -216
View File
@@ -1,216 +1,230 @@
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<FnrsCnsltnMngResponseDto>();
const MAIN_STEP_LABELS = {
apply: "재원협의 신청",
envoReview: "재원협의 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
{ label: "재원협의 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (["FV", "ER", "AC", "TE", "RB", "RA", "AV", "EV", "RK", "RR", "SL"].includes(prgrsSttsComCd)) {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "EP" || prgrsSttsComCd === "TA") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-start">
{badges.map((badge, index) => (
<Fragment key={badge.label}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
);
}
export function createFnrsCnsltnColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("bizNm", {
header: "사업명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("aplcntNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
columnHelper.accessor("regDt", {
header: "신청일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<FnrsCnsltnMngResponseDto>();
const MAIN_STEP_LABELS = {
apply: "재원협의 신청",
envoReview: "재원협의 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
{ label: "재원협의 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (["FV", "ER", "AC", "TE", "RB", "RA", "AV", "EV", "RK", "RR", "SL"].includes(prgrsSttsComCd)) {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "EP" || prgrsSttsComCd === "TA") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
function splitStepLabel(label: string) {
const [title = "", ...rest] = label.split(" ");
return { title, detail: rest.join(" ") };
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-center">
{badges.map((badge, index) => (
<Fragment key={badge.label}>
{index > 0 && <span className="mx-2">&gt;</span>}
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
<Badge
color={getBadgeColor(badge.state)}
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
>
<span>{splitStepLabel(badge.label).title}</span>
<span>
{splitStepLabel(badge.label).detail}
{getBadgeStateText(badge.state)}
</span>
</Badge>
</Fragment>
))}
</div>
);
}
export function createFnrsCnsltnColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("bizNm", {
header: "사업명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("aplcntNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
columnHelper.accessor("regDt", {
header: "신청일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
@@ -33,6 +33,9 @@ import {
interface PwrkCnsltnDocEvlDialogProps {
open: boolean;
pwrkCnsltnDocNo: string;
/** 평가 저장 성공 시 호출 (이후 웹기안기 문서 저장이 진행된다) */
onSaved: () => void;
/** 평가 없이 닫을 때 호출 (웹기안기 문서는 저장되지 않는다) */
onClose: () => void;
}
@@ -41,7 +44,12 @@ const DEFAULT_VALUES: Partial<PwrkCnsltnDocEvlFormValues> = {
pwrkCnsltnDocEvlCn: "",
};
function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDocEvlDialogProps) {
function PwrkCnsltnDocEvlDialog({
open,
pwrkCnsltnDocNo,
onSaved,
onClose,
}: PwrkCnsltnDocEvlDialogProps) {
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: saveEvl, isPending } = useSavePwrkCnsltnDocEvl();
@@ -65,8 +73,7 @@ function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDo
try {
const payload = buildPwrkCnsltnDocEvlSavePayload(pwrkCnsltnDocNo, values);
await saveEvl(payload);
alert("AI 초안 만족도 평가를 등록했습니다.");
onClose();
onSaved();
} catch (error) {
console.error("AI 초안 만족도 평가 등록 실패:", error);
alert("AI 초안 만족도 평가 등록에 실패했습니다.");
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { useQuery, useQueryClient } from "@tanstack/react-query";
@@ -46,6 +46,7 @@ const HangulGiangi = dynamic(
const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
/** 현재 버전이 이 값보다 작으면(AI 초안 상태) 저장 전에 만족도 평가를 먼저 받는다. */
const AI_DRAFT_EVL_MAX_VER_SN = 2;
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
@@ -88,6 +89,8 @@ function PwrkCnsltnDocMngDialog({
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false);
// 평가 모달 결과(평가 저장 여부)를 저장 흐름으로 돌려주는 resolver
const evlResolverRef = useRef<((isEvlSaved: boolean) => void) | null>(null);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const {
@@ -145,6 +148,8 @@ function PwrkCnsltnDocMngDialog({
setAtchFileGroupId("");
setUploadedFiles([]);
setIsEvlDialogOpen(false);
evlResolverRef.current?.(false);
evlResolverRef.current = null;
return;
}
@@ -178,9 +183,32 @@ function PwrkCnsltnDocMngDialog({
if (!nextOpen) onClose();
}
function requestEvl() {
return new Promise<boolean>((resolve) => {
evlResolverRef.current = resolve;
setIsEvlDialogOpen(true);
});
}
function resolveEvl(isEvlSaved: boolean) {
setIsEvlDialogOpen(false);
evlResolverRef.current?.(isEvlSaved);
evlResolverRef.current = null;
}
async function handleSaveDocument(file: Blob) {
if (!completedDraft) throw new Error("저장할 실무협의서 초안이 없습니다.");
// AI 초안(버전 < 2) 저장 시에는 만족도 평가를 먼저 저장해야 웹기안기 문서가 저장된다.
const currentVerSn = completedDraft.crtRslt?.verSn ?? 0;
if (currentVerSn < AI_DRAFT_EVL_MAX_VER_SN) {
const isEvlSaved = await requestEvl();
if (!isEvlSaved) {
alert("AI 초안 만족도 평가를 등록해야 실무협의서가 저장됩니다.");
return;
}
}
const saveDto = pwrkCnsltnDocCrtRsltSaveRequestSchema.parse({
crtDmndUuid: completedDraft.crtDmndUuid,
pwrkCnsltnDocNo: completedDraft.pwrkCnsltnDocNo,
@@ -201,10 +229,6 @@ function PwrkCnsltnDocMngDialog({
}),
]);
alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`);
if (savedDocument.verSn <= AI_DRAFT_EVL_MAX_VER_SN) {
setIsEvlDialogOpen(true);
}
}
async function handleDownload() {
@@ -386,7 +410,8 @@ function PwrkCnsltnDocMngDialog({
<PwrkCnsltnDocEvlDialog
open={isEvlDialogOpen}
pwrkCnsltnDocNo={completedDraft.pwrkCnsltnDocNo}
onClose={() => setIsEvlDialogOpen(false)}
onSaved={() => resolveEvl(true)}
onClose={() => resolveEvl(false)}
/>
) : null}
</>
@@ -77,8 +77,8 @@ function FnrsListPage() {
};
return (
<div className="space-y-4 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background">
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
<div className="flex items-center gap-2">
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
@@ -145,7 +145,7 @@ function FnrsListPage() {
</div>
<Button type="submit">조회</Button>
</form>
</section>
</div>
<FnrsList
searchParams={searchParams}
@@ -78,15 +78,15 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
return (
<>
<section className="card card-border mx-0 bg-background pb-8 pt-0">
<div className="card card-border pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">재원협의 목록</h3>
<h3 className="text-2xl font-medium text-label">재원협의 목록</h3>
<Button type="button" color="green" onClick={() => setIsTaskSeDialogOpen(true)}>
신청
</Button>
</div>
{tableComponent}
</section>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
<PbadmsTaskApplyDialog
open={isTaskSeDialogOpen}
onOpenChange={setIsTaskSeDialogOpen}
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import {
Button,
Checkbox,
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
const { control, watch, setValue } = useFormContext<FnrsCnsltnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("fnrsCnsltnMng.taskSeComCd");
const localGovLabel = [watch("fnrsCnsltnMng.sido"), watch("fnrsCnsltnMng.gugun")]
.filter(Boolean)
.join(" ");
const sido = watch("fnrsCnsltnMng.sido");
const gugun = watch("fnrsCnsltnMng.gugun");
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
const { data: taskOptions } = useCodeList("FNRS");
const { data: rtlztnFcltOptions } = useCodeList("RTLZTNFCLTSE");
const { data: rtlztnUsgOptions } = useCodeList("RTLZTNUSGSE");
@@ -49,6 +50,7 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 시설종류, 재이용용도 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -2,6 +2,7 @@
import { SectionCollapsible } from "@/shared/ui";
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
function BasicInfoSection({ det }: BasicInfoSectionProps) {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체, 확정예산사업번호, 예산신청연도 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -1,352 +1,345 @@
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import {
AttachmentDownloadLink,
attachService,
useDownloadAttachmentByAtflId,
} from "@/entities/attach";
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
import {
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type {
ReviewOpinionFormData,
ReviewOpinionGetFormDataResult,
} from "../../lib/review-opinion-utils";
import type { ReviewStepCode } from "../../model/view-types";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionGetFormDataResult | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: FnrsCnsltnRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!editableReviewStep || !hasFormContent(formData)) {
return null;
}
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
return null;
}
return {
formData: {
...formData,
opnrSeComCd: editableReviewStep,
},
rvwList,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
);
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
if (!hasEnvSection && !hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return null;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<TableHeader>
<TableRow>
<TableHead className="w-[12rem]">의견내용</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell>
<AttachmentFileField
files={
uploadedFile
? [
{
fileKey: uploadedFile.fileKey,
fileName: uploadedFile.fileName,
onDownload: uploadedFile.atflId
? () => downloadByAtflId(uploadedFile.atflId!)
: undefined,
},
]
: []
}
inputId={`rvw-opnn-${reviewStep}-file-input`}
isUploadingByKey={isUploading}
multiple={false}
showAddButton={!uploadedFile}
addDisabled={uploading}
onAdd={(fileList) => void handleFileUpload(fileList)}
onRemove={() => void handleFileRemove()}
/>
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<>
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
) : null}
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{/*
재원협의에서 환경공단 검토의견은 자동 생성
{renderOpinionForm("KECO")}
*/}
</div>
</SectionCollapsible>
) : null}
</>
);
});
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
</TableCell>
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
export { TechReviewOpinionSection };
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import {
fnrsCnsltnKeys,
type FnrsCnsltnRvwOpnnResponseDto,
fnrsCnsltnRvwOpnnService,
} from "@/entities/fnrs-cnsltn";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
RadioGroup,
RadioGroupItem,
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type {
ReviewOpinionFormData,
ReviewOpinionGetFormDataResult,
} from "../../lib/review-opinion-utils";
import type { ReviewStepCode } from "../../model/view-types";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionGetFormDataResult | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: FnrsCnsltnRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!editableReviewStep || !hasFormContent(formData)) {
return null;
}
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
return null;
}
return {
formData: {
...formData,
opnrSeComCd: editableReviewStep,
},
rvwList,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
);
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
if (!hasEnvSection && !hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return false;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
return true;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead className="w-[12rem]">의견내용</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell>
<AttachmentFileField
files={
uploadedFile
? [
{
fileKey: uploadedFile.fileKey,
fileName: uploadedFile.fileName,
onDownload: uploadedFile.atflId
? () => downloadByAtflId(uploadedFile.atflId!)
: undefined,
},
]
: []
}
inputId={`rvw-opnn-${reviewStep}-file-input`}
isUploadingByKey={isUploading}
multiple={false}
showAddButton={!uploadedFile}
addDisabled={uploading}
onAdd={(fileList) => void handleFileUpload(fileList)}
onRemove={() => void handleFileRemove()}
/>
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<>
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
) : null}
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{/*
재원협의에서 환경공단 검토의견은 자동 생성
{renderOpinionForm("KECO")}
*/}
</div>
</SectionCollapsible>
) : null}
</>
);
});
function getRvwOpnnIds(item: FnrsCnsltnRvwOpnnResponseDto) {
if (!item.fnrsCnsltnDocNo || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.fnrsCnsltnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await fnrsCnsltnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
}}
onDelete={async (item) => {
await fnrsCnsltnRvwOpnnService.delete(...getRvwOpnnIds(item));
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
}}
/>
);
}
export { TechReviewOpinionSection };
@@ -3,7 +3,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
aply: "aply",
del: "del",
instlSncnAncmnt: "instlSncnAncmnt",
envoSbmsn: "envoSbmsn",
rjct: "rjct",
sncnAprv: "sncnAprv",
rvwRply: "rvwRply",
@@ -33,14 +32,16 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
],
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
},
ROLE_USER04: {
ST: [INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn, INSTL_SNCN_VIEW_BUTTON_ACTION.rjct],
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
},
ROLE_USER03: {
LE: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply, INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw],
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
},
// 환경청 단계 제거: 기존 환경청(LE·TP) 기능을 광역자치단체가 수행
ROLE_USER04: {
ST: [
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply,
INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw,
INSTL_SNCN_VIEW_BUTTON_ACTION.rjct,
],
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
},
};
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
@@ -55,16 +55,8 @@ export const instlSncnRegistSchema = z.object({
tstmDesignAmt: z.number(),
}),
),
cklstResultList: z.array(
instlSncnCklstResultItemSchema.extend({
cklstSn: z.number(),
cklstArtclSn: z.number(),
cklstRvwSn: z.number(),
rvwStpComCd: z.string(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
}),
),
// 설치인가 화면에서 체크리스트 미사용: 숨겨진 값 때문에 신청·저장 검증이 막히지 않도록 필수값을 두지 않음
cklstResultList: z.array(instlSncnCklstResultItemSchema),
esntlAtchFileList: z.array(
instlSncnEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(),
@@ -50,7 +50,7 @@ export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
): ReviewStepCode | undefined {
if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) {
if (dtlInstCd === "ROLE_USER04" && (prgrsSttsComCd === "ST" || prgrsSttsComCd === "TP")) {
return "ENV";
}
+231 -217
View File
@@ -1,217 +1,231 @@
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<InstlSncnMngResponseDto>();
const MAIN_STEP_LABELS = {
apply: "설치인가 신청",
envoReview: "설치인가 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "설치인가 신청 (시군)", sttsComCd: "ST" },
{ label: "설치인가 신청 (시도)", sttsComCd: "LE" },
{ label: "설치인가 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "검토결과 회신", sttsComCd: "EP" },
{ label: "설치인가 승인", sttsComCd: "TA" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP" || prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-start">
{badges.map((badge, index) => (
<Fragment key={badge.label}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
);
}
export function createInstlSncnColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("bizNm", {
header: "사업명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("aplcntNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
columnHelper.accessor("regDt", {
header: "신청일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<InstlSncnMngResponseDto>();
const MAIN_STEP_LABELS = {
apply: "설치인가 신청",
envoReview: "설치인가 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "설치인가 신청 (시군)", sttsComCd: "ST" },
{ label: "설치인가 신청 (시도)", sttsComCd: "LE" },
{ label: "설치인가 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "검토결과 회신", sttsComCd: "EP" },
{ label: "설치인가 승인", sttsComCd: "TA" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP" || prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
function splitStepLabel(label: string) {
const [title = "", ...rest] = label.split(" ");
return { title, detail: rest.join(" ") };
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-center">
{badges.map((badge, index) => (
<Fragment key={badge.label}>
{index > 0 && <span className="mx-2">&gt;</span>}
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
<Badge
color={getBadgeColor(badge.state)}
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
>
<span>{splitStepLabel(badge.label).title}</span>
<span>
{splitStepLabel(badge.label).detail}
{getBadgeStateText(badge.state)}
</span>
</Badge>
</Fragment>
))}
</div>
);
}
export function createInstlSncnColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("bizNm", {
header: "사업명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("aplcntNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
columnHelper.accessor("regDt", {
header: "신청일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
@@ -50,13 +50,13 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "환경청검토",
ENV: "광역자치단체검토",
KECO: "기술검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
ENV: "광역자치단체",
KECO: "환경공단",
};
@@ -89,7 +89,7 @@ export const INSTL_PROGRESS_ORDER = [
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
ROLE_USER05: "LOCAL",
ROLE_USER03: "ENV",
ROLE_USER04: "ENV",
ROLE_USER01: "KECO",
};
@@ -35,7 +35,6 @@ import {
type InstlSncnViewButtonAction,
needsRoleUser01AtrzResolve,
} from "../../lib/button-config";
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
import {
buildSavePayload,
buildSupplementReqPayload,
@@ -179,11 +178,6 @@ function FloatingActionButtons({
}
async function handleAplyClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"LOCAL",
);
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
data,
checklistFormState,
@@ -191,11 +185,6 @@ function FloatingActionButtons({
"ST",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
}
await executeAction({
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
request: payload,
@@ -231,22 +220,6 @@ function FloatingActionButtons({
});
}
async function handleEnvoSbmsnClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
docSeComCd,
prgrsSttsComCd: "LE",
};
await executeAction({
action: INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn,
request: payload,
successMessage: "제출되었습니다.",
});
queryClient.invalidateQueries({
queryKey: instlSncnKeys.list(),
});
}
async function handleRjctClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
@@ -284,16 +257,6 @@ function FloatingActionButtons({
}
async function handleRvwRplyClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"ENV",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
return;
}
const payload = buildTechReviewReplyPayload(
data,
@@ -369,16 +332,6 @@ function FloatingActionButtons({
}
async function handleRvwCmptnClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"KECO",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
return;
}
const payload = {
...buildTechReviewReplyPayload(
@@ -447,11 +400,6 @@ function FloatingActionButtons({
설치인가 고시
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn) && (
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
환경청제출
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rjct) && (
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
반려
@@ -618,7 +566,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
aply: handleSaveApply,
del: handleDel,
instlSncnAncmnt: handleChangePrgrsSttsComCd,
envoSbmsn: handleChangePrgrsSttsComCd,
rjct: handleChangePrgrsSttsComCd,
sncnAprv: handleChangePrgrsSttsComCd,
rvwRply: handleTechReview,
@@ -20,9 +20,7 @@ import {
AttachmentsFormSection,
BusinessCostFormSection,
BusinessScaleFormSection,
ChecklistFormSection,
ContactInfoFormSection,
isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection,
ProgressStatusSection,
@@ -154,11 +152,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
return;
}
if (!isFormChecklistComplete(data.cklstResultList)) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
}
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
return;
@@ -228,7 +221,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
<PrivacyConsentSection />
<BusinessScaleFormSection />
<BusinessCostFormSection />
<ChecklistFormSection docSeComCd="INSTL" fieldPrefix="instlSncnMng" />
<AttachmentsFormSection
docSeComCd="INSTL"
fieldPrefix="instlSncnMng"
@@ -77,8 +77,8 @@ function InstlListPage() {
};
return (
<div className="space-y-4 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background">
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
<div className="flex items-center gap-2">
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
@@ -145,7 +145,7 @@ function InstlListPage() {
</div>
<Button type="submit">조회</Button>
</form>
</section>
</div>
<InstlList
searchParams={searchParams}
@@ -82,15 +82,15 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
return (
<>
<section className="card card-border mx-0 bg-background pb-8 pt-0">
<div className="card card-border pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">설치인가 목록</h3>
<h3 className="text-2xl font-medium text-label">설치인가 목록</h3>
<Button type="button" color="green" onClick={handleRegisterClick}>
신청
</Button>
</div>
{tableComponent}
</section>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
<PbadmsTaskApplyDialog
open={isTaskSeDialogOpen}
onOpenChange={setIsTaskSeDialogOpen}
@@ -1,149 +1,141 @@
"use client";
import { useMemo, useRef } from "react";
import { useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useInstlSncnDetail } from "@/entities/instl-sncn";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import {
AttachmentsViewSection,
ProgressStatusSection,
SupplementDemandSection,
} from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { FloatingActionButtons } from "./components/floating-action-buttons";
import { BasicInfoSection } from "./sections/basic-info-section";
import { BusinessCostSection } from "./sections/business-cost-section";
import { BusinessScaleSection } from "./sections/business-scale-section";
import { ChecklistSection } from "./sections/checklist-section";
import { ContactInfoSection } from "./sections/contact-info-section";
import {
TechReviewOpinionSection,
type TechReviewOpinionSectionHandle,
} from "./sections/tech-review-opinion-section";
interface InstlViewPageProps {
id: string;
}
function InstlViewPage({ id }: InstlViewPageProps) {
const userInfo = useUserStore((state) => state.userInfo);
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
const { data, isLoading, isError } = useInstlSncnDetail(id);
const { formState: checklistFormState, setFormState: setChecklistFormState } =
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
{},
{ enabled: needsAtrzResolve },
);
const atrzSeComCdList = useMemo(
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
[rvwLoginUserData],
);
if (isLoading) {
return (
<div className="mt-6 flex flex-col gap-4 px-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[32rem] rounded-xl" />
</div>
</div>
);
}
if (isError || !data) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const {
detDto,
bizSclList,
bizCstList,
cklstResultList = [],
esntlFileList,
etcFileList,
rvwList = [],
splmntDmndList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
userInfo?.dtlInstCd,
detDto.prgrsSttsComCd || "",
);
return (
<div className="mt-4">
<SectionCollapsibleGroup>
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
<SectionCollapsibleToggle />
</div>
<div className="space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.instlSncnDocNo}
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
<BusinessScaleSection bizSclList={bizSclList} />
<BusinessCostSection bizCstList={bizCstList} />
<ChecklistSection
cklstResultList={cklstResultList}
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
userRoleCode={userInfo?.dtlInstCd}
formState={checklistFormState}
setFormState={setChecklistFormState}
/>
<AttachmentsViewSection
esntlFileList={esntlFileList ?? []}
etcFileList={etcFileList ?? []}
/>
<TechReviewOpinionSection
ref={techReviewOpinionRef}
rvwList={rvwList}
editableReviewStep={editableReviewStep}
atrzSeComCdList={atrzSeComCdList}
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
/>
<SupplementDemandSection splmntDmndList={splmntDmndList} />
<FloatingActionButtons
data={data}
docSeComCd="INSTL"
instlSncnDocNo={detDto.instlSncnDocNo}
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
dtlInstCd={userInfo?.dtlInstCd}
atrzSeComCdList={atrzSeComCdList}
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
checklistFormState={checklistFormState}
techReviewOpinionRef={techReviewOpinionRef}
opnrNm={userInfo?.userName}
/>
</div>
</SectionCollapsibleGroup>
</div>
);
}
export { InstlViewPage };
"use client";
import { useMemo, useRef } from "react";
import { useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useInstlSncnDetail } from "@/entities/instl-sncn";
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
import {
AttachmentsViewSection,
ProgressStatusSection,
SupplementDemandSection,
} from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { FloatingActionButtons } from "./components/floating-action-buttons";
import { BasicInfoSection } from "./sections/basic-info-section";
import { BusinessCostSection } from "./sections/business-cost-section";
import { BusinessScaleSection } from "./sections/business-scale-section";
import { ContactInfoSection } from "./sections/contact-info-section";
import {
TechReviewOpinionSection,
type TechReviewOpinionSectionHandle,
} from "./sections/tech-review-opinion-section";
interface InstlViewPageProps {
id: string;
}
function InstlViewPage({ id }: InstlViewPageProps) {
const userInfo = useUserStore((state) => state.userInfo);
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
const { data, isLoading, isError } = useInstlSncnDetail(id);
const { formState: checklistFormState } = useChecklistFormState(
data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST,
);
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
{},
{ enabled: needsAtrzResolve },
);
const atrzSeComCdList = useMemo(
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
[rvwLoginUserData],
);
if (isLoading) {
return (
<div className="mt-6 flex flex-col gap-4 px-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[16rem] rounded-xl" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-[4rem] rounded-xl" />
<Skeleton className="h-[32rem] rounded-xl" />
</div>
</div>
);
}
if (isError || !data) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const {
detDto,
bizSclList,
bizCstList,
esntlFileList,
etcFileList,
rvwList = [],
splmntDmndList = [],
sttsChgHstryList = [],
} = data;
const editableReviewStep = getEditableReviewStep(
userInfo?.dtlInstCd,
detDto.prgrsSttsComCd || "",
);
return (
<div className="mt-4">
<SectionCollapsibleGroup>
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
<SectionCollapsibleToggle />
</div>
<div className="space-y-4">
<ProgressStatusSection
columns={PROGRESS_STATUS_COLUMNS}
sttsChgHstryList={sttsChgHstryList}
elaprDocNo={detDto.instlSncnDocNo}
/>
<BasicInfoSection det={detDto} />
<ContactInfoSection det={detDto} />
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
<BusinessScaleSection bizSclList={bizSclList} />
<BusinessCostSection bizCstList={bizCstList} />
<AttachmentsViewSection
esntlFileList={esntlFileList ?? []}
etcFileList={etcFileList ?? []}
/>
<TechReviewOpinionSection
ref={techReviewOpinionRef}
rvwList={rvwList}
editableReviewStep={editableReviewStep}
atrzSeComCdList={atrzSeComCdList}
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
/>
<SupplementDemandSection splmntDmndList={splmntDmndList} />
<FloatingActionButtons
data={data}
docSeComCd="INSTL"
instlSncnDocNo={detDto.instlSncnDocNo}
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
dtlInstCd={userInfo?.dtlInstCd}
atrzSeComCdList={atrzSeComCdList}
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
checklistFormState={checklistFormState}
techReviewOpinionRef={techReviewOpinionRef}
opnrNm={userInfo?.userName}
/>
</div>
</SectionCollapsibleGroup>
</div>
);
}
export { InstlViewPage };
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import {
Button,
Checkbox,
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
const { control, watch, setValue } = useFormContext<InstlSncnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("instlSncnMng.taskSeComCd");
const localGovLabel = [watch("instlSncnMng.sido"), watch("instlSncnMng.gugun")]
.filter(Boolean)
.join(" ");
const sido = watch("instlSncnMng.sido");
const gugun = watch("instlSncnMng.gugun");
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
function handleBnsSelect(data: BnsApplyData) {
setValue("instlSncnMng.bizNm", data.bizNm);
@@ -53,6 +54,7 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 사업구분, 시설종류, 재이용용도 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -2,6 +2,7 @@
import { SectionCollapsible } from "@/shared/ui";
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
function BasicInfoSection({ det }: BasicInfoSectionProps) {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 구분, 신청지자체, 사업구분 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -1,350 +1,350 @@
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: InstlSncnRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionFormData | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: InstlSncnRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!editableReviewStep || !hasFormContent(formData)) {
return null;
}
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
return null;
}
return {
...formData,
opnrSeComCd: editableReviewStep,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteKecoOpinion, editableReviewStep, formData],
);
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
const hasKecoSection =
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
if (!hasEnvSection && !hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return null;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<TableHeader>
<TableRow>
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">첨부파일</TableHead>
<TableCell>
{uploadedFile ? (
<div className="flex items-center gap-2">
<TextShimmer className="text-xl" active={uploading}>
{uploadedFile.fileName}
</TextShimmer>
<Button
size="icon"
onClick={(e) => {
e.preventDefault();
void handleFileRemove();
}}
disabled={uploading}
className="size-8 !bg-destructive"
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
</div>
) : (
<label>
<input
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(event.target.files);
event.target.value = "";
}}
/>
<Button size="sm" variant="outline" type="button" asChild>
<span>파일 선택</span>
</Button>
</label>
)}
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<>
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
) : null}
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
) : null}
</>
);
});
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.instlSncnDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
</TableCell>
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
export { TechReviewOpinionSection };
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import {
instlSncnKeys,
type InstlSncnRvwOpnnResponseDto,
instlSncnRvwOpnnService,
type ReviewStepCode,
} from "@/entities/instl-sncn";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: InstlSncnRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionFormData | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: InstlSncnRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!editableReviewStep || !hasFormContent(formData)) {
return null;
}
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
return null;
}
return {
...formData,
opnrSeComCd: editableReviewStep,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteKecoOpinion, editableReviewStep, formData],
);
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
const hasKecoSection =
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
if (!hasEnvSection && !hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return false;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
return true;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead scope="row" className="w-[12rem]">
의견내용
</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row">첨부파일</TableHead>
<TableCell>
{uploadedFile ? (
<div className="flex items-center gap-2">
<TextShimmer className="text-xl" active={uploading}>
{uploadedFile.fileName}
</TextShimmer>
<Button
size="icon"
onClick={(e) => {
e.preventDefault();
void handleFileRemove();
}}
disabled={uploading}
className="size-8 !bg-destructive"
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
</div>
) : (
<label>
<input
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(event.target.files);
event.target.value = "";
}}
/>
<Button size="sm" variant="outline" type="button" asChild>
<span>파일 선택</span>
</Button>
</label>
)}
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<>
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
) : null}
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
) : null}
</>
);
});
function getRvwOpnnIds(item: InstlSncnRvwOpnnResponseDto) {
if (!item.instlSncnDocNo || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.instlSncnDocNo, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.instlSncnDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await instlSncnRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
}}
onDelete={async (item) => {
await instlSncnRvwOpnnService.delete(...getRvwOpnnIds(item));
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
}}
/>
);
}
export { TechReviewOpinionSection };
@@ -89,8 +89,8 @@ function MyPbadmsSprtListPage() {
};
return (
<div className="space-y-4 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background">
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap">기간</Label>
@@ -157,7 +157,7 @@ function MyPbadmsSprtListPage() {
<Button type="submit">검색</Button>
</form>
</section>
</div>
<MyPbadmsSprtList
searchParams={searchParams}
@@ -77,12 +77,12 @@ function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPb
});
return (
<section className="card card-border mx-0 bg-background pb-8 pt-0">
<div className="card card-border pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">내 신청 목록</h3>
<h3 className="text-2xl font-medium text-label">내 신청 목록</h3>
</div>
{tableComponent}
</section>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
);
}
@@ -71,8 +71,8 @@ function NtiHstryListPage() {
};
return (
<div className="space-y-4 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background">
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap">기간</Label>
@@ -114,7 +114,7 @@ function NtiHstryListPage() {
<Button type="submit">검색</Button>
</form>
</section>
</div>
<NtiHstryList
searchParams={searchParams}
@@ -103,12 +103,12 @@ function NtiHstryList({ searchParams, pagination, onPaginationChange }: NtiHstry
});
return (
<section className="card card-border mx-0 bg-background pb-8 pt-0">
<div className="card card-border pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">알림 이력 목록</h3>
<h3 className="text-2xl font-medium text-label">알림 이력 목록</h3>
</div>
{tableComponent}
</section>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
);
}
+235 -221
View File
@@ -1,221 +1,235 @@
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
const MAIN_STEP_LABELS = {
apply: "기본계획 신청",
envoReview: "기본계획 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "기본계획 신청 (시군)", sttsComCd: "LE" },
{ label: "기본계획 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "횐경청 검토 결과 회신", sttsComCd: "EP" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-start">
{badges.map((badge, index) => (
<Fragment key={`${badge.label}-${index}`}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
);
}
export function createSewerBscPlanColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("sewerBscPlanNm", {
header: "기본계획명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("sewerBscPlanSeCdNm", {
header: "계획구분",
meta: { align: "center" },
cell: ({ row }) =>
formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("rgtrNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
columnHelper.accessor("regDt", {
header: "등록일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
"use client";
import { Fragment } from "react";
import { createColumnHelper } from "@tanstack/react-table";
import { dayjs } from "@/shared/lib";
import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan";
import { Badge } from "@common/components";
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
const MAIN_STEP_LABELS = {
apply: "기본계획 신청",
envoReview: "기본계획 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "기본계획 신청 (시군)", sttsComCd: "LE" },
{ label: "기본계획 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "횐경청 검토 결과 회신", sttsComCd: "EP" },
];
type BadgeState = "completed" | "current" | "pending";
interface ProgressBadge {
label: string;
state: BadgeState;
}
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
if (prgrsSttsComCd === "SV") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ST") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "current" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "LE") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
if (prgrsSttsComCd === "TP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "current" },
];
}
if (prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{
label: MAIN_STEP_LABELS.techReview,
state: techRplyHstryYn === "Y" ? "completed" : "pending",
},
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
];
}
return [
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
];
}
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
if (prgrsSttsComCd === "SL") {
return <Badge color="orange">보완요청</Badge>;
}
return null;
}
function getBadgeColor(state: BadgeState) {
switch (state) {
case "completed":
return "green";
case "current":
return "blue";
case "pending":
return "lightGray";
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
function splitStepLabel(label: string) {
const [title = "", ...rest] = label.split(" ");
return { title, detail: rest.join(" ") };
}
function formatOptional(value: string | undefined) {
return value?.trim() ? value : "-";
}
interface ProgressStepBadgeProps {
prgrsSttsComCd: string;
techRplyHstryYn: "Y" | "N";
}
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
return (
<div className="flex flex-wrap items-center justify-center">
{badges.map((badge, index) => (
<Fragment key={`${badge.label}-${index}`}>
{index > 0 && <span className="mx-2">&gt;</span>}
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
<Badge
color={getBadgeColor(badge.state)}
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
>
<span>{splitStepLabel(badge.label).title}</span>
<span>
{splitStepLabel(badge.label).detail}
{getBadgeStateText(badge.state)}
</span>
</Badge>
</Fragment>
))}
</div>
);
}
export function createSewerBscPlanColumns() {
return [
columnHelper.accessor("sido", {
header: "시도",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("gugun", {
header: "시군구",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("sewerBscPlanNm", {
header: "기본계획명",
meta: { align: "left" },
cell: ({ row, getValue }) => (
<>
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
</>
),
}),
columnHelper.accessor("sewerBscPlanSeCdNm", {
header: "계획구분",
meta: { align: "center" },
cell: ({ row }) =>
formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd),
}),
columnHelper.accessor("taskSeCdNm", {
header: "업무구분",
meta: { align: "center" },
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
}),
columnHelper.accessor("rgtrNm", {
header: "담당자",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.accessor("envoSeCdNm", {
header: "해당기관",
meta: { align: "center" },
cell: ({ getValue }) => formatOptional(getValue()),
}),
columnHelper.display({
id: "progress",
header: "진행상황",
meta: { align: "center" },
cell: ({ row }) => (
<ProgressStepBadge
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
/>
),
}),
columnHelper.accessor("regDt", {
header: "등록일",
meta: { align: "center" },
cell: ({ getValue }) => {
const value = getValue();
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
},
}),
];
}
@@ -1,15 +1,13 @@
"use client";
import { useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import dynamic from "next/dynamic";
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { truncateDecimals } from "@/widgets/pbadms-sprt";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import {
Button,
Input,
Table,
TableBody,
@@ -21,14 +19,6 @@ import {
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
const PreviousPlanLookupDialog = dynamic(
() =>
import("../previous-plan-lookup-dialog").then((mod) => ({
default: mod.PreviousPlanLookupDialog,
})),
{ ssr: false },
);
const OTLN_ROWS = [
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구", decimals: 0 },
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구", decimals: 0 },
@@ -37,7 +27,6 @@ const OTLN_ROWS = [
function OutlineFormSection() {
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
const [isLookupOpen, setIsLookupOpen] = useState(false);
const otlnList = useWatch({ control, name: "sewerBscPlanOtlnList" }) ?? [];
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
@@ -74,11 +63,7 @@ function OutlineFormSection() {
return (
<SectionCollapsible title="사업개요">
<div className="mb-2 flex justify-end px-2">
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
이전기본계획조회
</Button>
</div>
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
<colgroup>
<col width="12%" />
@@ -126,13 +111,6 @@ function OutlineFormSection() {
))}
</TableBody>
</Table>
<PreviousPlanLookupDialog
open={isLookupOpen}
onOpenChange={setIsLookupOpen}
initialSidoNm={sido}
initialGugunNm={gugun}
/>
</SectionCollapsible>
);
}
@@ -6,11 +6,14 @@ import type {
SewerBscPlanOtlnResponseDto,
} from "@/entities/sewer-bsc-plan";
import { fmtNumber } from "@/widgets/pbadms-sprt";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface OutlineSectionProps {
otlnList: SewerBscPlanOtlnResponseDto[];
dtl?: SewerBscPlanDtlResponseDto;
sido?: string | null;
gugun?: string | null;
}
const OTLN_ROWS = [
@@ -19,7 +22,7 @@ const OTLN_ROWS = [
{ key: "sewerPsplyRt" as const, label: "하수도보급율" },
];
function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
function OutlineSection({ otlnList, dtl, sido, gugun }: OutlineSectionProps) {
const stepHeaders = [
{ planStpNo: 1, label: "1단계", year: dtl?.frlvlGoalYr },
{ planStpNo: 2, label: "2단계", year: dtl?.sclvlGoalYr },
@@ -29,6 +32,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
return (
<SectionCollapsible title="사업개요">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
<colgroup>
<col width="12%" />
@@ -1,357 +1,354 @@
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { SewerBscPlanRvwOpnnResponseDto } from "@/entities/sewer-bsc-plan";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type {
ReviewOpinionFormData,
ReviewOpinionGetFormDataResult,
} from "../../lib/review-opinion-utils";
import type { ReviewStepCode } from "../../model/view-types";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: SewerBscPlanRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionGetFormDataResult | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: SewerBscPlanRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!editableReviewStep || !hasFormContent(formData)) {
return null;
}
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
return null;
}
return {
formData: {
...formData,
opnrSeComCd: editableReviewStep,
},
rvwList,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
);
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
const hasKecoSection =
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
if (!hasEnvSection && !hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return null;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
<TableHeader>
<TableRow>
<TableHead className="w-[12rem]">의견내용</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell>
{uploadedFile ? (
<div className="flex items-center gap-2">
<TextShimmer className="text-xl" active={uploading}>
{uploadedFile.fileName}
</TextShimmer>
<Button
size="icon"
onClick={(e) => {
e.preventDefault();
void handleFileRemove();
}}
disabled={uploading}
className="size-8 !bg-destructive"
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
</div>
) : (
<label>
<input
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(event.target.files);
event.target.value = "";
}}
/>
<Button size="sm" variant="outline" type="button" asChild>
<span>파일 선택</span>
</Button>
</label>
)}
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<>
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
) : null}
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
) : null}
</>
);
});
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
return (
<div className="overflow-x-auto">
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
<colgroup>
<col width="10%" />
<col width="55%" />
<col width="5%" />
<col width="20%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={`${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}>
<TableCell align="center">{item.opnrNm}</TableCell>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
</TableCell>
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
export { TechReviewOpinionSection };
"use client";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import {
sewerBscPlanKeys,
type SewerBscPlanRvwOpnnResponseDto,
sewerBscPlanRvwOpnnService,
} from "@/entities/sewer-bsc-plan";
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
import type {
ReviewOpinionFormData,
ReviewOpinionGetFormDataResult,
} from "../../lib/review-opinion-utils";
import type { ReviewStepCode } from "../../model/view-types";
import { REVIEW_STEP_LABELS } from "../../model/view-types";
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
interface TechReviewOpinionSectionProps {
rvwList: SewerBscPlanRvwOpnnResponseDto[];
editableReviewStep?: ReviewStepCode;
atrzSeComCdList?: string[];
isAtrzLoading?: boolean;
}
export interface TechReviewOpinionSectionHandle {
getFormData: () => ReviewOpinionGetFormDataResult | null;
resetForm: () => void;
}
interface TechReviewOpinionTableProps {
items: SewerBscPlanRvwOpnnResponseDto[];
}
interface UploadedOpinionFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
}
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
rvwRsltCn: "",
rvwRsltAnmlyYn: "",
atchFileGroupId: "",
opnrSeComCd: "",
};
function createRvwOpnnUploadKey(file: File) {
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function hasFormContent(formData: ReviewOpinionFormData) {
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
}
const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps
>(function TechReviewOpinionSection(
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
ref,
) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
useEffect(() => {
setFormData((prev) => ({
...prev,
opnrSeComCd: editableReviewStep ?? "",
}));
}, [editableReviewStep]);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
useImperativeHandle(
ref,
() => ({
getFormData: () => {
if (!editableReviewStep || !hasFormContent(formData)) {
return null;
}
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
return null;
}
return {
formData: {
...formData,
opnrSeComCd: editableReviewStep,
},
rvwList,
};
},
resetForm: () => {
setFormData({
...EMPTY_FORM_DATA,
opnrSeComCd: editableReviewStep ?? "",
});
setUploadedFile(null);
},
}),
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
);
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
const hasKecoSection =
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
if (!hasEnvSection && !hasKecoSection) {
return null;
}
async function handleFileUpload(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createRvwOpnnUploadKey(file);
const existingAtflCd = formData.atchFileGroupId;
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
}
}
async function handleFileRemove() {
if (!uploadedFile) return;
uploads[uploadedFile.fileKey]?.abort();
if (uploadedFile.atflId) {
try {
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
setUploadedFile(null);
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
}
function isReviewTurn(reviewStep: ReviewStepCode) {
if (editableReviewStep !== reviewStep) return false;
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
return true;
}
function renderOpinionForm(reviewStep: ReviewStepCode) {
if (!isReviewTurn(reviewStep)) return null;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
return (
<div className="space-y-2">
<h4 className="text-xl font-semibold">검토의견 작성</h4>
<Table
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
variant="secondary"
>
<TableHeader>
<TableRow>
<TableHead className="w-[12rem]">의견내용</TableHead>
<TableCell>
<Textarea
value={formData.rvwRsltCn}
onChange={(event) =>
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>검토결과</TableHead>
<TableCell>
<FieldSet>
<RadioGroup
value={formData.rvwRsltAnmlyYn}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
}))
}
>
<div className="flex flex-wrap gap-4">
<FieldLabel
htmlFor={`rvw-opnn-normal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell>
{uploadedFile ? (
<div className="flex items-center gap-2">
<TextShimmer className="text-xl" active={uploading}>
{uploadedFile.fileName}
</TextShimmer>
<Button
size="icon"
onClick={(e) => {
e.preventDefault();
void handleFileRemove();
}}
disabled={uploading}
className="size-8 !bg-destructive"
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
</div>
) : (
<label>
<input
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(event.target.files);
event.target.value = "";
}}
/>
<Button size="sm" variant="outline" type="button" asChild>
<span>파일 선택</span>
</Button>
</label>
)}
</TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
);
}
return (
<>
{hasEnvSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
<div className="space-y-6">
{envRvwList.length > 0 ? (
<TechReviewOpinionTable items={envRvwList} />
) : null}
{renderOpinionForm("ENV")}
</div>
</SectionCollapsible>
) : null}
{hasKecoSection ? (
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
<div className="space-y-6">
{kecoRvwList.length > 0 ? (
<TechReviewOpinionTable items={kecoRvwList} />
) : null}
{renderOpinionForm("KECO")}
</div>
</SectionCollapsible>
) : null}
</>
);
});
function getRvwOpnnIds(item: SewerBscPlanRvwOpnnResponseDto) {
if (!item.sewerBscPlanDocNo || item.chgCycl == null || item.rvwOpnnSn == null) {
throw new Error("검토의견 식별값이 없습니다.");
}
return [item.sewerBscPlanDocNo, item.chgCycl, item.rvwOpnnSn] as const;
}
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
const queryClient = useQueryClient();
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
return (
<RvwOpnnListTable
items={items}
getRowKey={(item) => `${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}
currentUserUuid={currentUserUuid}
onUpdate={async (item, body) => {
await sewerBscPlanRvwOpnnService.update(...getRvwOpnnIds(item), body);
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
}}
onDelete={async (item) => {
await sewerBscPlanRvwOpnnService.delete(...getRvwOpnnIds(item));
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
}}
/>
);
}
export { TechReviewOpinionSection };
@@ -81,8 +81,8 @@ function SewerBscPlanListPage() {
};
return (
<div className="space-y-4 px-[1.8rem] pb-6">
<section className="card card-border mx-0 bg-background">
<div className="card card-border !mt-6">
<div className="card card-border">
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
<div className="flex items-center gap-2">
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
@@ -165,7 +165,7 @@ function SewerBscPlanListPage() {
</div>
<Button type="submit">조회</Button>
</form>
</section>
</div>
<SewerBscPlanList
searchParams={searchParams}
@@ -85,15 +85,15 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
return (
<>
<section className="card card-border mx-0 bg-background pb-8 pt-0">
<div className="card card-border pt-0">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-semibold">하수도기본계획 목록</h3>
<h3 className="text-2xl font-medium text-label">하수도기본계획 목록</h3>
<Button type="button" color="green" onClick={() => setIsApplyDialogOpen(true)}>
신청
</Button>
</div>
{tableComponent}
</section>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
<SewerBscPlanApplyDialog
open={isApplyDialogOpen}
onOpenChange={setIsApplyDialogOpen}
@@ -114,7 +114,12 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
<BasicInfoSection det={detDto} dtl={activeDtlDto} />
<ContactInfoSection dtl={activeDtlDto} />
<ElaprDocPrgrsStatus elaprDocNo={detDto.sewerBscPlanDocNo} />
<OutlineSection otlnList={otlnList} dtl={activeDtlDto} />
<OutlineSection
otlnList={otlnList}
dtl={activeDtlDto}
sido={detDto.sido}
gugun={detDto.gugun}
/>
<BizCstMatrixSection bizCstList={bizCstList} dtl={activeDtlDto} />
<FnrsCstSection fnrsCstList={fnrsCstList} dtl={activeDtlDto} />
<TmplntSection tmplntList={tmplntList} det={detDto} dtl={activeDtlDto} />
+3
View File
@@ -12,11 +12,13 @@ export const ENDPOINT = {
RVW_MNPW: "/as-service/api/v1/rvw-mnpw",
BNS_IF: "/as-service/api/v1/if",
INSTL_SNCN: "/as-service/api/v1/instl-sncn",
INSTL_SNCN_RVW_OPNN: "/as-service/api/v1/instl-sncn-rvw-opnn",
CKLST: "/as-service/api/v1/cklst",
CKLST_ARTCL: "/as-service/api/v1/cklst-artcl",
CKLST_RVW: "/as-service/api/v1/cklst-rvw",
ADSTDSTR: "/as-service/api/v1/adstdstr",
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
FNRS_CNSLTN_RVW_OPNN: "/as-service/api/v1/fnrs-cnsltn-rvw-opnn",
FNRS_SHAR_SE: "/as-service/api/v1/fnrs-shar-se-mng",
DESIGN_CNSTN: "/as-service/api/v1/design-cnstn",
DESIGN_CNSTN_BIZ_CST: "/as-service/api/v1/design-cnstn-biz-cst",
@@ -24,6 +26,7 @@ export const ENDPOINT = {
DESIGN_CNSTN_ESNTL_ATCH_FILE: "/as-service/api/v1/design-cnstn-esntl-atch-file",
DESIGN_CNSTN_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
SEWER_BSC_PLAN: "/as-service/api/v1/sewer-bsc-plans",
SEWER_BSC_PLAN_RVW_OPNNS: "/as-service/api/v1/sewer-bsc-plan-rvw-opnns",
DASHBOARD: "/as-service/api/v1/dashboard",
DCRAG_TO: "/as-service/api/v1/dcrag-to",
AI_AUTO_PRCS_MNG: "/as-service/api/v1/ai-auto-prcs-mng",
+1 -1
View File
@@ -13,4 +13,4 @@ export const AS_DASHBOARD_BBS_ID = process.env.NEXT_PUBLIC_AS_DASHBOARD_BBS_ID;
/** 웹한글 기안기(WebHWP) 컨트롤 URL */
export const WEBHYP_CTRL_URL =
process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "http://hwp.onasys.co.kr/webhwpctrl";
process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "https://hwp.onasys.co.kr/webhwpctrl";
+1
View File
@@ -46,6 +46,7 @@ export type {
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
RvwOpnnSaveBase,
RvwOpnnUpdateRequestDto,
SplmntDmndResponseExtension,
SplmntDmndSaveBase,
} from "./pbadms-sprt";
+11
View File
@@ -231,11 +231,22 @@ export interface PbadmsApplySaveHeader {
rvwStpCd?: string;
}
/** 검토의견 수정 공통 요청 DTO */
export interface RvwOpnnUpdateRequestDto {
opnrSeComCd?: string;
opnrNm?: string;
rvwRsltCn?: string;
rvwRsltAnmlyYn?: string;
atchFileGroupId?: string;
}
/** 검토의견 응답 공통 확장 필드 */
export interface RvwOpnnResponseExtension {
opnrSeCdNm?: string;
atchFileInfo?: AtflMngClientResponseDto;
regDt?: string;
rgtr?: string;
rgtrNm?: string;
}
/** 보완요구 응답 공통 확장 필드 */
+1
View File
@@ -51,6 +51,7 @@ export {
} from "./ui/pbadms-task-apply-dialog";
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
export { ProgressStatusSection } from "./ui/progress-status-section";
export { RvwOpnnListTable, type RvwOpnnListTableProps } from "./ui/rvw-opnn-list-table";
export {
SplmntDmndDialog,
type SplmntDmndDialogConfirmItem,
@@ -404,11 +404,17 @@ function AttachmentsFormSection({
<TableHead
scope="row"
className="group break-keep font-medium"
data-required={file.esntlAtchYn === "Y"}
align="left"
>
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
{file.esntlAtchFileNm}
<span>
{file.esntlAtchFileNm}
{file.esntlAtchYn === "Y" && (
<span className="whitespace-nowrap text-red-600" title="필수 항목">
{" "}(*)
</span>
)}
</span>
{atflCd ? (
<Button
size="sm"
@@ -0,0 +1,403 @@
"use client";
import { useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import type { AtflMngClientResponseDto, RvwOpnnUpdateRequestDto } from "@/shared/model";
import { AttachmentFileField } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import {
AttachmentDownloadLink,
attachService,
useDownloadAttachmentByAtflId,
} from "@/entities/attach";
import {
Button,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Textarea,
} from "@common/components";
const RVW_OPNN_EDIT_FILE_KEY_PREFIX = "rvw-opnn-edit::";
/** 업무별 검토의견 응답 DTO 공통 필드 (업무마다 null 허용 여부가 달라 느슨하게 정의) */
interface RvwOpnnListItem {
opnrSeComCd?: string | null;
opnrNm?: string | null;
rvwRsltCn?: string | null;
rvwRsltAnmlyYn?: string | null;
atchFileGroupId?: string | null;
atchFileInfo?: AtflMngClientResponseDto | null;
regDt?: string | null;
rgtr?: string | null;
}
interface RvwOpnnListTableProps<T extends RvwOpnnListItem> {
items: T[];
getRowKey: (item: T) => string;
/** 로그인 사용자 UUID — 검토의견 등록자(rgtr)와 같을 때만 수정·삭제 버튼 노출 */
currentUserUuid?: string;
onUpdate: (item: T, body: RvwOpnnUpdateRequestDto) => Promise<void>;
onDelete: (item: T) => Promise<void>;
}
interface EditDraft {
rvwRsltCn: string;
rvwRsltAnmlyYn: string;
atchFileGroupId: string;
}
interface EditFile {
fileName: string;
fileKey: string;
atflId?: string;
atflCd?: string;
/** 이번 수정 중 새로 업로드한 파일 여부 (취소 시 정리 대상) */
isNew: boolean;
}
function formatAnmlyYn(value?: string | null) {
if (value === "Y") return "이상";
if (value === "N") return "정상";
return "-";
}
function RvwOpnnListTable<T extends RvwOpnnListItem>({
items,
getRowKey,
currentUserUuid,
onUpdate,
onDelete,
}: RvwOpnnListTableProps<T>) {
const { confirm } = useConfirm();
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const [editingKey, setEditingKey] = useState<string | null>(null);
const [draft, setDraft] = useState<EditDraft | null>(null);
const [editFile, setEditFile] = useState<EditFile | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd || !atflId) return;
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: atflCd } : prev));
setEditFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setEditFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
const editFileUploading = editFile ? (isUploading[editFile.fileKey] ?? false) : false;
function canModify(item: T) {
return Boolean(currentUserUuid) && item.rgtr === currentUserUuid;
}
async function discardNewFile(file: EditFile | null) {
if (!file?.isNew) return;
uploads[file.fileKey]?.abort();
if (file.atflId) {
try {
await attachService.deleteAttachmentByAtflId(file.atflId);
} catch (error) {
console.error("검토의견 첨부파일 삭제 실패:", error);
}
}
}
function resetEdit() {
setEditingKey(null);
setDraft(null);
setEditFile(null);
}
function startEdit(item: T) {
void discardNewFile(editFile);
setEditingKey(getRowKey(item));
setDraft({
rvwRsltCn: item.rvwRsltCn ?? "",
rvwRsltAnmlyYn: item.rvwRsltAnmlyYn ?? "",
atchFileGroupId: item.atchFileGroupId ?? "",
});
setEditFile(
item.atchFileInfo?.atflCd
? {
fileName: item.atchFileInfo.orgnlFileNm || "첨부파일",
fileKey: `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${item.atchFileInfo.atflCd}`,
atflId: item.atchFileInfo.atflId,
atflCd: item.atchFileInfo.atflCd,
isNew: false,
}
: null,
);
}
function cancelEdit() {
void discardNewFile(editFile);
resetEdit();
}
async function handleFileAdd(files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
setEditFile({ fileName: file.name, fileKey, isNew: true });
try {
await uploadFileAsync(file, { fileKey });
} catch {
// onError callback handles cleanup
}
}
function handleFileRemove() {
// 기존 파일은 저장 시점에 연결만 해제하고, 새로 올린 파일은 즉시 정리
void discardNewFile(editFile);
setEditFile(null);
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: "" } : prev));
}
async function handleSave(item: T) {
if (!draft) return;
if (!draft.rvwRsltCn.trim()) {
alert("의견내용을 입력하세요.");
return;
}
if (!draft.rvwRsltAnmlyYn) {
alert("검토결과를 선택하세요.");
return;
}
if (!(await confirm("검토의견을 수정하시겠습니까?"))) return;
setIsSubmitting(true);
try {
await onUpdate(item, {
opnrSeComCd: item.opnrSeComCd ?? undefined,
opnrNm: item.opnrNm ?? undefined,
rvwRsltCn: draft.rvwRsltCn,
rvwRsltAnmlyYn: draft.rvwRsltAnmlyYn,
atchFileGroupId: draft.atchFileGroupId,
});
alert("수정되었습니다.");
resetEdit();
} catch (error) {
console.error("검토의견 수정 실패:", error);
alert("검토의견 수정 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
}
async function handleDelete(item: T) {
if (!(await confirm("검토의견을 삭제하시겠습니까?"))) return;
setIsSubmitting(true);
try {
await onDelete(item);
alert("삭제되었습니다.");
if (editingKey === getRowKey(item)) resetEdit();
} catch (error) {
console.error("검토의견 삭제 실패:", error);
alert("검토의견 삭제 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
}
function renderViewRow(item: T) {
return (
<>
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
<TableCell align="center">{formatAnmlyYn(item.rvwRsltAnmlyYn)}</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
</TableCell>
</>
);
}
function renderEditRow(rowKey: string, editDraft: EditDraft) {
return (
<>
<TableCell align="left">
<Textarea
value={editDraft.rvwRsltCn}
onChange={(event) =>
setDraft((prev) => (prev ? { ...prev, rvwRsltCn: event.target.value } : prev))
}
placeholder="의견내용을 입력하세요"
rows={4}
className="min-h-[8rem] !text-xl"
/>
</TableCell>
<TableCell align="center">
<FieldSet>
<RadioGroup
value={editDraft.rvwRsltAnmlyYn}
onValueChange={(value) =>
setDraft((prev) => (prev ? { ...prev, rvwRsltAnmlyYn: value } : prev))
}
>
<div className="flex flex-col gap-2">
<FieldLabel
htmlFor={`rvw-opnn-edit-normal-${rowKey}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`rvw-opnn-edit-normal-${rowKey}`} />
<span className="font-normal">정상</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`rvw-opnn-edit-abnormal-${rowKey}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`rvw-opnn-edit-abnormal-${rowKey}`} />
<span className="font-normal">이상</span>
</Field>
</FieldLabel>
</div>
</RadioGroup>
</FieldSet>
</TableCell>
<TableCell align="center">
<AttachmentFileField
files={
editFile
? [
{
fileKey: editFile.fileKey,
fileName: editFile.fileName,
onDownload: editFile.atflId
? () => downloadByAtflId(editFile.atflId!)
: undefined,
},
]
: []
}
inputId={`rvw-opnn-edit-${rowKey}-file-input`}
isUploadingByKey={isUploading}
multiple={false}
showAddButton={!editFile}
addDisabled={editFileUploading}
onAdd={(fileList) => void handleFileAdd(fileList)}
onRemove={handleFileRemove}
/>
</TableCell>
</>
);
}
return (
<div className="overflow-x-auto">
<Table
caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시, 관리 정보 제공"
variant="secondary"
>
<colgroup>
<col width="10%" />
<col width="47%" />
<col width="6%" />
<col width="17%" />
<col width="10%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>의견자명</TableHead>
<TableHead>의견내용</TableHead>
<TableHead>검토결과</TableHead>
<TableHead>첨부파일</TableHead>
<TableHead>등록일시</TableHead>
<TableHead>관리</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => {
const rowKey = getRowKey(item);
const isEditing = editingKey === rowKey && draft !== null;
return (
<TableRow key={rowKey}>
<TableCell align="center">{item.opnrNm}</TableCell>
{isEditing ? renderEditRow(rowKey, draft) : renderViewRow(item)}
<TableCell align="center">
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
</TableCell>
<TableCell align="center">
{!canModify(item) ? (
"-"
) : isEditing ? (
<div className="flex flex-wrap justify-center gap-1">
<Button
type="button"
size="sm"
onClick={() => void handleSave(item)}
disabled={isSubmitting || editFileUploading}
>
저장
</Button>
<Button
type="button"
size="sm"
color="white"
onClick={cancelEdit}
disabled={isSubmitting}
>
취소
</Button>
</div>
) : (
<div className="flex flex-wrap justify-center gap-1">
<Button
type="button"
size="sm"
color="white"
onClick={() => startEdit(item)}
disabled={isSubmitting}
>
수정
</Button>
<Button
type="button"
size="sm"
color="gray"
onClick={() => void handleDelete(item)}
disabled={isSubmitting}
>
삭제
</Button>
</div>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}
export { RvwOpnnListTable, type RvwOpnnListTableProps };
@@ -0,0 +1,4 @@
export {
PreviousPlanLookupButton,
type PreviousPlanLookupButtonProps,
} from "./ui/previous-plan-lookup-button";
@@ -0,0 +1,46 @@
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
import { Button } from "@common/components";
const PreviousPlanLookupDialog = dynamic(
() =>
import("./previous-plan-lookup-dialog").then((mod) => ({
default: mod.PreviousPlanLookupDialog,
})),
{ ssr: false },
);
interface PreviousPlanLookupButtonProps {
/** 조회 다이얼로그 시도 초기값(표시명) */
sidoNm?: string | null;
/** 조회 다이얼로그 시군구 초기값(표시명) */
gugunNm?: string | null;
}
/** 섹션 상단 우측에 [이전기본계획조회] 버튼과 조회 다이얼로그를 배치한다. */
function PreviousPlanLookupButton({ sidoNm, gugunNm }: PreviousPlanLookupButtonProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<div className="mb-2 flex justify-end px-2">
<Button type="button" color="white" onClick={() => setIsOpen(true)}>
이전기본계획조회
</Button>
</div>
<PreviousPlanLookupDialog
open={isOpen}
onOpenChange={setIsOpen}
initialSidoNm={sidoNm}
initialGugunNm={gugunNm}
/>
</>
);
}
export { PreviousPlanLookupButton };
export type { PreviousPlanLookupButtonProps };
@@ -51,7 +51,7 @@ function PreviousPlanLookupDialog({
const [gnsvyYr, setGnsvyYr] = useState("");
const [swtrmtFcltCd, setSwtrmtFcltCd] = useState("");
const [hasPrefillApplied, setHasPrefillApplied] = useState(false);
const [searchedClsfSystmNm, setSearchedClsfSystmNm] = useState("");
const [hasGugunPrefillApplied, setHasGugunPrefillApplied] = useState(false);
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
@@ -62,15 +62,19 @@ function PreviousPlanLookupDialog({
const ctpvNm = sidoOptions.find((option) => option.adstDstrCd === sidoComCd)?.adstDstrNm ?? "";
const sggNm = gugunOptions.find((option) => option.adstDstrCd === sggComCd)?.adstDstrNm ?? "";
// 창 오픈 시 시도/시군구로 조회하고, 콤보 선택에 따라 파라미터가 추가되며 재조회된다.
// 빈 값은 서비스에서 제외되며, 시도가 없으면 조회하지 않는다.
const searchParams = {
swtrmtFcltCd,
ctpvNm,
sggNm,
targetYear: gnsvyYr,
swtrmtFcltCd,
};
const isSearchEnabled = open && hasGugunPrefillApplied && Boolean(ctpvNm);
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, false);
const planData = gnsvyYr && swtrmtFcltCd ? (data?.[0] ?? null) : null;
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, isSearchEnabled);
const planData = isSearchEnabled ? (data?.[0] ?? null) : null;
const clsfSystmNm = planData ? sggNm || ctpvNm : "";
const { data: yearTree, isPending: isYearPending } = useLvNowDmgMtncGnsvy({
level: "2",
@@ -97,8 +101,8 @@ function PreviousPlanLookupDialog({
setSggComCd("");
setGnsvyYr("");
setSwtrmtFcltCd("");
setSearchedClsfSystmNm("");
setHasPrefillApplied(false);
setHasGugunPrefillApplied(false);
return;
}
}, [open]);
@@ -115,12 +119,27 @@ function PreviousPlanLookupDialog({
setHasPrefillApplied(true);
}, [open, hasPrefillApplied, isSidoPending, sidoOptions, initialSidoNm]);
// 시군구 초기값 적용이 끝난 뒤에 최초 조회가 나가도록 완료 여부를 표시한다.
useEffect(() => {
if (!open || !sidoComCd || isGugunPending || !initialGugunNm) return;
if (sggComCd) return;
if (!open || !hasPrefillApplied || hasGugunPrefillApplied) return;
if (!sidoComCd || !initialGugunNm) {
setHasGugunPrefillApplied(true);
return;
}
if (isGugunPending) return;
const matchedGugun = gugunOptions.find((option) => option.adstDstrNm === initialGugunNm);
if (matchedGugun) setSggComCd(matchedGugun.adstDstrCd);
}, [open, sidoComCd, isGugunPending, gugunOptions, initialGugunNm, sggComCd]);
setHasGugunPrefillApplied(true);
}, [
open,
hasPrefillApplied,
hasGugunPrefillApplied,
sidoComCd,
isGugunPending,
gugunOptions,
initialGugunNm,
]);
function handleSidoChange(value: string) {
setSidoComCd(value);
@@ -145,18 +164,18 @@ function PreviousPlanLookupDialog({
}
async function handleSearch() {
if (!ctpvNm) {
alert("시도를 선택하세요.");
return;
}
const { data: list, error } = await refetch();
if (error) {
alert("데이터 조회 중 오류가 발생했습니다.");
setSearchedClsfSystmNm("");
return;
}
if (!list?.length) {
alert("조회된 데이터가 없습니다.");
setSearchedClsfSystmNm("");
return;
}
setSearchedClsfSystmNm(sggNm || ctpvNm);
}
return (
@@ -198,8 +217,8 @@ function PreviousPlanLookupDialog({
<SectionCollapsible title="기본계획파일" defaultOpen>
<PreviousPlanFileSection
key={searchedClsfSystmNm || "empty"}
clsfSystmNm={searchedClsfSystmNm || undefined}
key={clsfSystmNm || "empty"}
clsfSystmNm={clsfSystmNm || undefined}
/>
</SectionCollapsible>
+7 -145
View File
@@ -1,151 +1,13 @@
.card {
position: relative;
margin: 0.6rem;
box-sizing: border-box;
min-height: 2rem;
border-radius: 0.5rem;
padding: 0.6rem;
font-size: 1.3rem;
color: #666;
}
.card_width10 {
width: calc(10% - 12px);
}
.card_width20 {
width: calc(20% - 12px);
}
.card_width25 {
width: calc(25% - 12px);
}
.card_width30 {
width: calc(30% - 12px);
}
.card_width33 {
width: calc(33.333% - 12px);
}
.card_width40 {
width: calc(40% - 12px);
}
.card_width45 {
width: calc(45% - 12px);
}
.card_width50 {
width: calc(50% - 12px);
}
.card_width60 {
width: calc(60% - 12px);
}
.card_width66 {
width: calc(66.666% - 12px);
}
.card_width70 {
width: calc(70% - 12px);
}
.card_width75 {
width: calc(75% - 12px);
}
.card_width80 {
width: calc(80% - 12px);
}
.card_width90 {
width: calc(90% - 12px);
}
.card_width100 {
width: calc(100% - 12px);
}
.card_nopadding {
padding: 0 !important;
}
.card_paddingnone {
padding: 0 !important;
}
.card_nomargin {
width: 100%;
margin: 0 !important;
padding: 0 !important;
}
.card_title {
padding: 1rem;
margin-bottom: 1rem;
}
.card_title .leftArea {
display: flex;
align-items: center;
}
.card_title h4 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.topline {
border-top: 1px solid #ccc;
}
.bottomline {
border-bottom: 1px solid #e6eef3;
background: #fff;
}
.card-border {
border: 1px solid #dbe2e6;
background-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
/*
* 카드 관련 클래스(.card, .card-border, .card-header, .card-footer, .card_width*, .card_nopadding,
* .card_paddingnone, .card_nomargin, .card_title, .topline, .bottomline)와 테마 색상은
* 공통 @common/components/index.css(layout/common.css) + @common/assets base.css 변수에서 관리.
* 이 파일에는 앱 전용 레이아웃 차이와 페이지 전용 스타일만 둔다.
*/
/* 앱 레이아웃 차이: 카드 헤더를 카드 상단 끝까지 붙임 (공통 .card-header 에 추가) */
.card-header {
margin-top: -0.6rem;
margin-left: -0.6rem;
margin-right: -0.6rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e1e7eb;
padding: 1rem;
}
.card-footer {
margin-left: -0.6rem;
margin-right: -0.6rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #e1e7eb;
padding-top: 1rem;
padding-bottom: 1rem;
}
/* 블랙 테마 */
[data-theme="black"] {
.card-border {
background-color: rgba(255, 255, 255, 0.065);
border-color: #38383a;
}
.card-header {
border-bottom-color: #3f4042;
}
}
/* 자료현황 스타일 */
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { isAxiosError } from "axios";
import Image from "next/image";
import { authService, tokenManager } from "@/entities/auth";
import { client } from "@/shared/api";
@@ -107,10 +108,12 @@ function DashboardPage() {
}
} catch (error) {
console.error("Error fetching user info:", error);
if (error instanceof Error && error.message.includes("401")) {
// 401은 로그인 페이지로 이동하지 않고 오류 처리만 한다
if (
(isAxiosError(error) && error.response?.status === 401) ||
(error instanceof Error && error.message.includes("401"))
) {
setError("인증이 만료되었습니다. 다시 로그인해주세요.");
tokenManager.clearAuthToken();
router.push("/auth/login");
} else {
setError("네트워크 오류가 발생했습니다.");
}
+1 -1
View File
@@ -90,7 +90,7 @@ export const ENDPOINT = {
REUSE_MAP: "/operation-service/api/v1/dashBoard/main/reuse/map",
},
IC_SERVICE: {
GROUPING_STATISTICS: "/ic-service/api/v1/support-status-summary",
GROUPING_STATISTICS: "/ic-service/api/v1/support-status-summary/public",
},
DC_SERVICE: {
DATA_STATUS: "/dc-service/api/v1/dataStatus",
+7 -125
View File
@@ -1,129 +1,11 @@
.card {
position: relative;
margin: 0.6rem;
box-sizing: border-box;
min-height: 2rem;
border-radius: 0.5rem;
padding: 0.6rem;
font-size: 1.3rem;
color: #666;
}
.card_width10 {
width: calc(10% - 12px);
}
.card_width20 {
width: calc(20% - 12px);
}
.card_width25 {
width: calc(25% - 12px);
}
.card_width30 {
width: calc(30% - 12px);
}
.card_width33 {
width: calc(33.333% - 12px);
}
.card_width40 {
width: calc(40% - 12px);
}
.card_width45 {
width: calc(45% - 12px);
}
.card_width50 {
width: calc(50% - 12px);
}
.card_width60 {
width: calc(60% - 12px);
}
.card_width66 {
width: calc(66.666% - 12px);
}
.card_width70 {
width: calc(70% - 12px);
}
.card_width75 {
width: calc(75% - 12px);
}
.card_width80 {
width: calc(80% - 12px);
}
.card_width90 {
width: calc(90% - 12px);
}
.card_width100 {
width: calc(100% - 12px);
}
.card_nopadding {
padding: 0 !important;
}
.card_nomargin {
width: 100%;
margin: 0 !important;
padding: 0 !important;
}
.topline {
border-top: 1px solid #ccc;
}
.bottomline {
border-bottom: 1px solid #e6eef3;
background: #fff;
}
.card-border {
border: 1px solid #dbe2e6;
background-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
/*
* 카드 관련 클래스(.card, .card-border, .card-header, .card-footer, .card_width*, .card_nopadding,
* .card_paddingnone, .card_nomargin, .card_title, .topline, .bottomline)와 테마 색상은
* 공통 @common/components/index.css(layout/common.css) + @common/assets base.css 변수에서 관리.
* 이 파일에는 앱 전용 레이아웃 차이와 페이지 전용 스타일만 둔다.
*/
/* 앱 레이아웃 차이: 카드 헤더를 카드 상단 끝까지 붙임 (공통 .card-header 에 추가) */
.card-header {
margin-top: -0.6rem;
margin-left: -0.6rem;
margin-right: -0.6rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e1e7eb;
padding: 1rem;
}
.card-footer {
margin-left: -0.6rem;
margin-right: -0.6rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #e1e7eb;
padding-top: 1rem;
padding-bottom: 1rem;
}
/* 블랙 테마 */
[data-theme="black"] {
.card-border {
background-color: rgba(255, 255, 255, 0.065);
border-color: #38383a;
}
.card-header {
border-bottom-color: #3f4042;
}
}
+16 -52
View File
@@ -1992,42 +1992,6 @@ useEffect(() => {
footerVerificationResult,
]);
/** footerVerificationResult만 바뀐 경우에도 총계 스타일 즉시 재적용/초기화 */
useEffect(() => {
const gridView = gridRef.current?.gridView as
| {
columnByName?: (name: string) => {
footer?: { styleName?: string } | Array<{ styleName?: string }>;
} | null;
refresh?: () => void;
}
| undefined;
if (!gridView?.columnByName) return;
columns.forEach((col) => {
if (!col.footer) return;
try {
const column = gridView.columnByName?.(col.name);
if (!column?.footer) return;
const styleName =
resolveFooterVerificationStyleName(
col,
footerVerificationResult,
) ?? "";
if (Array.isArray(column.footer)) {
column.footer.forEach((item) => {
item.styleName = styleName;
});
} else {
column.footer.styleName = styleName;
}
} catch {
console.error('총계 스타일 재적용/초기화 error');
}
});
gridView.refresh?.();
}, [footerVerificationResult, columns]);
// 행·셀 클릭 이벤트 (검증 오류 툴팁은 hover만 — onShowTooltip / 빈 셀 오버레이)
useEffect(() => {
const gridView = gridRef.current?.gridView;
@@ -2403,17 +2367,17 @@ useEffect(() => {
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0" ? (
{statusInfo.lclgvSbmsnSttsCd === null ||
statusInfo.lclgvSbmsnSttsCd === "0" ? (
"미작성"
) : statusInfo.laeSbmsnSttsCd === "1" ? (
) : statusInfo.lclgvSbmsnSttsCd === "1" ? (
<span className="animate-pulse">작성중</span>
) : statusInfo.laeSbmsnSttsCd === "2" ? (
) : statusInfo.lclgvSbmsnSttsCd === "2" ? (
<span className="!text-[#2094fa]">제출</span>
) : statusInfo.laeSbmsnSttsCd === "3" ? (
<span className="!text-[#179d5b]">승인</span>
) : statusInfo.laeSbmsnSttsCd === "4" ? (
<span className="relative inline-flex pr-3 !text-[#e53e35]">
) : statusInfo.lclgvSbmsnSttsCd === "3" ? (
<span className="!text-[#1fd078]">승인</span>
) : statusInfo.lclgvSbmsnSttsCd === "4" ? (
<span className="relative inline-flex pr-3 !text-[#ff6c64]">
반려
<span className="absolute -right-0.5 -top-0.5 inline-flex size-[0.6125rem]">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#e53e35] opacity-75" />
@@ -2432,21 +2396,21 @@ useEffect(() => {
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
{statusInfo.lclgvSbmsnYmd
? statusInfo.lclgvSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.lclgvSbmsnYmd)
? `${statusInfo.lclgvSbmsnYmd.slice(0, 4)}-${statusInfo.lclgvSbmsnYmd.slice(4, 6)}-${statusInfo.lclgvSbmsnYmd.slice(6, 8)}`
: statusInfo.lclgvSbmsnYmd
: "미제출"}
</span>
<span>
담당자:{" "}
{statusInfo.laePicNm ? statusInfo.laePicNm : "-"}
{statusInfo.lclgvPicNm ? statusInfo.lclgvPicNm : "-"}
</span>
<span>
연락처:{" "}
{statusInfo.laePicTelNo
? statusInfo.laePicTelNo
{statusInfo.lclgvPicTelnoCn
? statusInfo.lclgvPicTelnoCn
: "-"}
</span>
</div>
@@ -63,14 +63,14 @@ const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] =
const TITLE = "예외처리 내역 조회";
/** 예외처리 건만 */
const PRCS_YN_LIST = "2";
const VRFC_STTS_CD_LIST = "2";
export type ExceptionRejectSubmitItem = {
vrfcSn: number;
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
mdfrUuid: string;
rjctOpnnCn?: string;
};
@@ -84,8 +84,8 @@ function prcsYnCodeToNumber(raw: string | number | undefined): number | undefine
}
function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined {
if (fieldName !== "prcsYnNm") return undefined;
const code = prcsYnCodeToNumber(row.prcsYn);
if (fieldName !== "vrfcSttsNm") return undefined;
const code = prcsYnCodeToNumber(row.vrfcSttsCd);
if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad";
return "rg-cell-vrf-prcs-ok";
}
@@ -96,10 +96,10 @@ const exceptionHistoryColumnInfo: ColumnInfo = {
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "nowCn", fieldName: "nowCn", header: { text: "입력값" }, editable: false },
{ name: "cmprCn", fieldName: "cmprCn", header: { text: "기준값" }, editable: false },
{ name: "vrfcSttsNm", fieldName: "vrfcSttsNm", header: { text: "처리여부" }, editable: false },
{ name: "statsVrfcRjtComNm", fieldName: "statsVrfcRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
@@ -112,7 +112,7 @@ const exceptionHistoryColumnInfo: ColumnInfo = {
width: 104,
styleName: "rg-cell-reject-mini-btn",
},
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
{ name: "vrfcSttsCd", fieldName: "vrfcSttsCd", header: { text: "검증결과상태코드" }, editable: false, visible: false },
{ name: "vrfcSn", fieldName: "vrfcSn", header: { text: "vrfcSn" }, editable: false, visible: false },
],
columnLayout: [
@@ -120,16 +120,16 @@ const exceptionHistoryColumnInfo: ColumnInfo = {
"admdstNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYnNm",
"stVrfRjtComNm",
"nowCn",
"cmprCn",
"vrfcSttsNm",
"statsVrfcRjtComNm",
"prcsOpnnExpln",
"rjctOpnnCn",
"regDt",
"mdfcnDt",
"individualReject",
"prcsYn",
"vrfcSttsCd",
"vrfcSn",
],
};
@@ -196,8 +196,7 @@ export function ExceptionProcessingHistoryDialog({
statsYr: queryContext.statsYr,
statsCd: queryContext.statsCd,
vrfcMthdCd,
prcsYnList: PRCS_YN_LIST,
rejectCodeList: "0,1,2",
vrfcSttsCdList: VRFC_STTS_CD_LIST,
page: pageIndex + 1,
size: 30,
};
@@ -316,13 +315,13 @@ export function ExceptionProcessingHistoryDialog({
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYnNm: item.prcsYnNm ?? "-",
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
nowCn: item.nowCn ?? "-",
cmprCn: item.cmprCn ?? "-",
vrfcSttsNm: item.vrfcSttsNm ?? "-",
statsVrfcRjtComNm: item.statsVrfcRjtComNm ?? "-",
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
rjctOpnnCn: item.rjctOpnnCn ?? "-",
prcsYn: item.prcsYn ?? "",
vrfcSttsCd: item.vrfcSttsCd ?? "",
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
individualReject: "개별 반려",
@@ -401,7 +400,7 @@ export function ExceptionProcessingHistoryDialog({
alert("사용자 정보를 불러오지 못했습니다.");
return;
}
const mdfrId = userInfo.userUnqId;
const mdfrUuid = userInfo.userUnqId;
const trimmedReason = rejectReasonText.trim();
const items: ReviewInputSaveRequest[] = pendingRejectVrfcSns.map((vrfcSn) => ({
@@ -409,7 +408,7 @@ export function ExceptionProcessingHistoryDialog({
statsYr: queryContext.statsYr,
sewerBzmnCd: queryContext.sewerBzmnCd,
statsCd: queryContext.statsCd,
mdfrId,
mdfrUuid,
vrfcSn: String(vrfcSn),
...(trimmedReason ? { rjctOpnnCn: trimmedReason } : {}),
}));
+5 -2
View File
@@ -145,6 +145,9 @@ const SearchForm = (props: SearchFormProps = {}) => {
const currentYear = new Date().getFullYear();
const defaultYear = currentYear - 1;
const isInput = Boolean(props.isInput);
const isDetailInputId = props.tabKey
? `${props.tabKey}-isDetail`
: "isDetail";
const sidoListKind = isInput ? "input" : "default";
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
@@ -479,12 +482,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
<div className="flex items-center gap-2">
<Label
className="cursor-pointer text-label"
htmlFor="isDetail"
htmlFor={isDetailInputId}
>
세부 데이터 포함
</Label>
<Checkbox
id="isDetail"
id={isDetailInputId}
checked={values.isDetail}
onCheckedChange={(value) =>
setValues({
@@ -399,7 +399,7 @@ const SidoSggFacilityNameSearchForm = ({
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList,
deleteList,
};
@@ -101,9 +101,9 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false },
{ name: "nowCn", fieldName: "nowCn", header: { text: "입력값" }, editable: false },
{ name: "cmprCn", fieldName: "cmprCn", header: { text: "기준값" }, editable: false },
{ name: "vrfcSttsNm", fieldName: "vrfcSttsNm", header: { text: "처리여부" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{
@@ -120,9 +120,9 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
"fcltNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYn",
"nowCn",
"cmprCn",
"vrfcSttsNm",
"regDt",
"mdfcnDt",
"prcsOpnnExpln",
@@ -211,9 +211,9 @@ export function VerificationResultDialog({
fcltNm: item.fcltNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYn: item.prcsYnNm ?? PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
nowCn: item.nowCn ?? "-",
cmprCn: item.cmprCn ?? "-",
vrfcSttsNm: item.vrfcSttsNm ?? PRCS_YN_LABEL[item.vrfcSttsCd] ?? item.vrfcSttsCd,
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
prcsOpnnExpln: item.prcsOpnnExpln ?? "",
@@ -232,8 +232,7 @@ export function VerificationResultDialog({
statsYr: stableQueryContext.statsYr,
statsCd: stableQueryContext.statsCd,
vrfcMthdCd,
prcsYnList: "0",
rejectCodeList: "0,1,2",
vrfcSttsCdList: "0",
page: page + 1,
size: 10000,
};
@@ -351,7 +350,7 @@ export function VerificationResultDialog({
sewerBzmnCd: stableQueryContext.sewerBzmnCd,
statsYr: stableQueryContext.statsYr,
statsCd: stableQueryContext.statsCd,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
exceptionList: payload,
});
if (response.status === "success") {
@@ -113,7 +113,7 @@ function syncCheckedMapFromGrid(
if (!entry) continue;
const sn = Number(entry.vrfcSn);
if (!Number.isFinite(sn)) continue;
next.set(sn, String(entry.prcsYn ?? ""));
next.set(sn, String(entry.vrfcSttsCd ?? ""));
}
return next;
}
@@ -124,15 +124,15 @@ function selectionHasNonExceptionResetAllowed(
): boolean {
for (const [vrfcSn, storedPrcsYn] of map) {
const onPage = currentEntries.find((e) => Number(e.vrfcSn) === vrfcSn);
const prcsYn = onPage ? onPage.prcsYn : storedPrcsYn;
const prcsYn = onPage ? onPage.vrfcSttsCd : storedPrcsYn;
if (!canExceptionReset(prcsYn)) return true;
}
return false;
}
function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined {
if (fieldName !== "prcsYnNm") return undefined;
const code = prcsYnCodeToNumber(row.prcsYn);
if (fieldName !== "vrfcSttsNm") return undefined;
const code = prcsYnCodeToNumber(row.vrfcSttsCd);
if (code === 0 || code === 3) return "rg-cell-vrf-prcs-bad";
return "rg-cell-vrf-prcs-ok";
}
@@ -144,15 +144,15 @@ const verificationResultColumnInfo: ColumnInfo = {
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "nowCn", fieldName: "nowCn", header: { text: "입력값" }, editable: false },
{ name: "cmprCn", fieldName: "cmprCn", header: { text: "기준값" }, editable: false },
{ name: "vrfcSttsNm", fieldName: "vrfcSttsNm", header: { text: "처리여부" }, editable: false },
{ name: "statsVrfcRjtComNm", fieldName: "statsVrfcRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
{ name: "vrfcSttsCd", fieldName: "vrfcSttsCd", header: { text: "검증결과상태코드" }, editable: false, visible: false },
{ name: "vrfcSn", fieldName: "vrfcSn", header: { text: "검증일련번호" }, editable: false, visible: false },
],
columnLayout: [
@@ -161,15 +161,15 @@ const verificationResultColumnInfo: ColumnInfo = {
"fcltNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYnNm",
"stVrfRjtComNm",
"nowCn",
"cmprCn",
"vrfcSttsNm",
"statsVrfcRjtComNm",
"prcsOpnnExpln",
"rjctOpnnCn",
"regDt",
"mdfcnDt",
"prcsYn",
"vrfcSttsCd",
"vrfcSn",
],
};
@@ -262,8 +262,7 @@ export function VerificationResultHistoryDialog({
statsYr: stableQueryContext.statsYr,
statsCd: stableQueryContext.statsCd,
vrfcMthdCd,
prcsYnList: "0,2",
rejectCodeList: "0,1,2",
vrfcSttsCdList: "0,2",
page: pageIndex + 1,
size: PAGE_SIZE,
};
@@ -385,13 +384,13 @@ export function VerificationResultHistoryDialog({
fcltNm: item.fcltNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYnNm: item.prcsYnNm ?? "-",
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
nowCn: item.nowCn ?? "-",
cmprCn: item.cmprCn ?? "-",
vrfcSttsNm: item.vrfcSttsNm ?? "-",
statsVrfcRjtComNm: item.statsVrfcRjtComNm ?? "-",
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
rjctOpnnCn: item.rjctOpnnCn ?? "-",
prcsYn: item.prcsYn ?? "",
vrfcSttsCd: item.vrfcSttsCd ?? "",
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
};
@@ -434,10 +433,10 @@ export function VerificationResultHistoryDialog({
}
setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: syncedMap }));
const vrfcSnList = Array.from(syncedMap.keys());
let mdfrId: string;
let mdfrUuid: string;
try {
const userInfo = await getUserInfo();
mdfrId = userInfo.userUnqId;
mdfrUuid = userInfo.userUnqId;
} catch {
alert("사용자 정보를 불러오지 못했습니다.");
return;
@@ -449,7 +448,7 @@ export function VerificationResultHistoryDialog({
statsYr: stableQueryContext.statsYr,
sewerBzmnCd: stableQueryContext.sewerBzmnCd,
statsCd: stableQueryContext.statsCd,
mdfrId,
mdfrUuid,
vrfcSnList,
});
if (response.status !== "success") {
@@ -512,13 +511,13 @@ export function VerificationResultHistoryDialog({
if (!entry) return;
const vrfcSn = Number(entry.vrfcSn);
if (!Number.isFinite(vrfcSn)) return;
if (checked && !canExceptionReset(entry.prcsYn)) {
if (checked && !canExceptionReset(entry.vrfcSttsCd)) {
alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE);
uncheckGridRow(gridRef, itemIndex);
return;
}
updateCheckedVrfcForTab(tab, (map) => {
if (checked) map.set(vrfcSn, String(entry.prcsYn ?? ""));
if (checked) map.set(vrfcSn, String(entry.vrfcSttsCd ?? ""));
else map.delete(vrfcSn);
});
}}
@@ -531,14 +530,14 @@ export function VerificationResultHistoryDialog({
map.delete(vrfcSn);
return;
}
if (canExceptionReset(entry.prcsYn)) {
map.set(vrfcSn, String(entry.prcsYn ?? ""));
if (canExceptionReset(entry.vrfcSttsCd)) {
map.set(vrfcSn, String(entry.vrfcSttsCd ?? ""));
} else {
uncheckGridRow(gridRef, itemIndex);
}
});
});
if (checked && entries.some((e) => !canExceptionReset(e.prcsYn))) {
if (checked && entries.some((e) => !canExceptionReset(e.vrfcSttsCd))) {
alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE);
}
}}
@@ -22,7 +22,7 @@ function responseHasVerificationResults(response: VerificationResultListResponse
}
/**
* 제출 직후 `VerificationResultDialog`에 표시할 미통과(prcsYn=0) 검증 결과가
* 제출 직후 `VerificationResultDialog`에 표시할 미통과(vrfcSttsCd=0) 검증 결과가
* 일반검증(NV)·I-MR(IMR)·HB 중 하나라도 있는지 조회합니다.
*/
export async function hasAnyPostSubmitVerificationResult(
@@ -34,8 +34,7 @@ export async function hasAnyPostSubmitVerificationResult(
sggNm: ctx.sggNm,
statsYr: ctx.statsYr,
statsCd: ctx.statsCd,
prcsYnList: "0",
rejectCodeList: "0,1,2",
vrfcSttsCdList: "0",
page: 1,
size: 1,
};
@@ -379,8 +379,8 @@ const VerificationSelectionReport = ({
<div className="flex justify-between text-lg text-muted-foreground">
<span>{formatReportItemRegion(item)}</span>
<span>
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
{item.compVlCn ?? "-"}
현재: {item.nowCn ?? "-"} / 비교:{" "}
{item.cmprCn ?? "-"}
</span>
</div>
</div>
@@ -93,8 +93,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -130,7 +130,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
@@ -257,35 +257,35 @@ function buildExcretaTreatmentFacilitySaveItem(
fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: nullableTrimmedString(row.fcltNm),
fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
telNo: nullableTrimmedString(row.telNo),
telnoCn: nullableTrimmedString(row.telnoCn),
cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd),
oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd),
wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt),
prcsFcltCpct: toGridNumber(row.prcsFcltCpct),
cpct: toGridNumber(row.cpct),
oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
prcsMthdExpln: nullableTrimmedString(row.prcsMthdExpln),
fcltCpct: toGridNumber(row.fcltCpct),
excrFcltPrcqty: toGridNumber(row.excrFcltPrcqty),
fcltOprtngDayCnt: toGridNumber(row.fcltOprtngDayCnt),
prcsExpln: nullableTrimmedString(row.prcsExpln),
linkTmplntCn: nullableTrimmedString(row.linkTmplntCn),
linkPrcsQnt: toGridNumber(row.linkPrcsQnt),
inflwBodWqVl: toGridNumber(row.inflwBodWqVl),
inflwTocWqVl: toGridNumber(row.inflwTocWqVl),
inflwSsWqVl: toGridNumber(row.inflwSsWqVl),
inflwTnWqVl: toGridNumber(row.inflwTnWqVl),
inflwTpWqVl: toGridNumber(row.inflwTpWqVl),
inflwClfgCnt: toGridNumber(row.inflwClfgCnt),
dschgBodWqVl: toGridNumber(row.dschgBodWqVl),
dschgTocWqVl: toGridNumber(row.dschgTocWqVl),
dschgSsWqVl: toGridNumber(row.dschgSsWqVl),
dschgTnWqVl: toGridNumber(row.dschgTnWqVl),
dschgTpWqVl: toGridNumber(row.dschgTpWqVl),
dschgClfgCnt: toGridNumber(row.dschgClfgCnt),
wtsysNm: nullableTrimmedString(row.wtsysNm),
trbtrNm: nullableTrimmedString(row.trbtrNm),
inflwWqBod: toGridNumber(row.inflwWqBod),
inflwWqTorgcb: toGridNumber(row.inflwWqTorgcb),
inflwWqSpsld: toGridNumber(row.inflwWqSpsld),
inflwWqDtn: toGridNumber(row.inflwWqDtn),
inflwWqTph: toGridNumber(row.inflwWqTph),
fcltInflwClfgCnt: toGridNumber(row.fcltInflwClfgCnt),
dschgWqBod: toGridNumber(row.dschgWqBod),
dschgWqTorgcb: toGridNumber(row.dschgWqTorgcb),
dschgWqSpsld: toGridNumber(row.dschgWqSpsld),
dschgWqDtn: toGridNumber(row.dschgWqDtn),
dschgWqTph: toGridNumber(row.dschgWqTph),
fcltDschgClfgCnt: toGridNumber(row.fcltDschgClfgCnt),
fcltWtsysNm: nullableTrimmedString(row.fcltWtsysNm),
fcltTrbtrNm: nullableTrimmedString(row.fcltTrbtrNm),
msirbCn: nullableTrimmedString(row.msirbCn),
ssirbCn: nullableTrimmedString(row.ssirbCn),
operMthdCdCn: nullableTrimmedString(row.operMthdCdCn),
operCn: nullableTrimmedString(row.operCn),
wholNope: toGridNumber(row.wholNope),
sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt),
sftyAcdntNocs: toGridNumber(row.sftyAcdntNocs),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -615,7 +615,7 @@ const ExcretaTreatmentFacilityInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD,
};
@@ -674,7 +674,7 @@ const ExcretaTreatmentFacilityInp = () => {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable,
saveList,
@@ -775,16 +775,15 @@ const ExcretaTreatmentFacilityInp = () => {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: userInfo.userUnqId,
laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
userUuid: userInfo.userUnqId,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
noApplicable,
submitList,
deleteList: deleteListForSubmit,
@@ -808,7 +807,7 @@ const ExcretaTreatmentFacilityInp = () => {
stats_cd: statsCdForSubmit,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd:
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
statusInfo?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -962,7 +961,7 @@ const ExcretaTreatmentFacilityInp = () => {
? "일반검증 - "
: "모집단 검증 -";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1044,7 +1043,7 @@ const ExcretaTreatmentFacilityInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{statusInfo?.laeSbmsnSttsCd === "2" && submitOk && (
{statusInfo?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -249,7 +249,7 @@ export function MainFacilityMappingSaveDialog({
setIsSubmitting(true);
await onConfirm?.({
prxyBzentyNmCn: prxyBzentyNmCn.trim(),
cnsgnCstVl,
cnsgnCstVl: cnsgnCstVl,
wholNope,
prxyCtrtCn: prxyCtrtCnParam,
});
@@ -119,8 +119,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -148,7 +148,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
@@ -210,20 +210,20 @@ function buildSewageTreatFactStatusSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
step1StaaPrcsQnt: formatNumber(d.step1StaaPrcsQnt, "step1StaaPrcsQnt"),
step2StaaPrcsQnt: formatNumber(d.step2StaaPrcsQnt, "step2StaaPrcsQnt"),
step3StaaPrcsQnt: formatNumber(d.step3StaaPrcsQnt, "step3StaaPrcsQnt"),
step4StaaPrcsQnt: formatNumber(d.step4StaaPrcsQnt, "step4StaaPrcsQnt"),
step5StaaPrcsQnt: formatNumber(d.step5StaaPrcsQnt, "step5StaaPrcsQnt"),
step6StaaPrcsQnt: formatNumber(d.step6StaaPrcsQnt, "step6StaaPrcsQnt"),
step7StaaPrcsQnt: formatNumber(d.step7StaaPrcsQnt, "step7StaaPrcsQnt"),
step1StaaExcpPrcsQnt: formatNumber(d.step1StaaExcpPrcsQnt, "step1StaaExcpPrcsQnt"),
step2StaaExcpPrcsQnt: formatNumber(d.step2StaaExcpPrcsQnt, "step2StaaExcpPrcsQnt"),
step3StaaExcpPrcsQnt: formatNumber(d.step3StaaExcpPrcsQnt, "step3StaaExcpPrcsQnt"),
step4StaaExcpPrcsQnt: formatNumber(d.step4StaaExcpPrcsQnt, "step4StaaExcpPrcsQnt"),
step5StaaExcpPrcsQnt: formatNumber(d.step5StaaExcpPrcsQnt, "step5StaaExcpPrcsQnt"),
step6StaaExcpPrcsQnt: formatNumber(d.step6StaaExcpPrcsQnt, "step6StaaExcpPrcsQnt"),
step7StaaExcpPrcsQnt: formatNumber(d.step7StaaExcpPrcsQnt, "step7StaaExcpPrcsQnt"),
frlvlStaaPrcqty: formatNumber(d.frlvlStaaPrcqty, "frlvlStaaPrcqty"),
sclvlStaaPrcqty: formatNumber(d.sclvlStaaPrcqty, "sclvlStaaPrcqty"),
thlvlStaaPrcqty: formatNumber(d.thlvlStaaPrcqty, "thlvlStaaPrcqty"),
folvlStaaPrcqty: formatNumber(d.folvlStaaPrcqty, "folvlStaaPrcqty"),
filvlStaaPrcqty: formatNumber(d.filvlStaaPrcqty, "filvlStaaPrcqty"),
sxlvlStaaPrcqty: formatNumber(d.sxlvlStaaPrcqty, "sxlvlStaaPrcqty"),
svlvlStaaPrcqty: formatNumber(d.svlvlStaaPrcqty, "svlvlStaaPrcqty"),
frlvlStaaOtsdPrcqty: formatNumber(d.frlvlStaaOtsdPrcqty, "frlvlStaaOtsdPrcqty"),
sclvlStaaOtsdPrcqty: formatNumber(d.sclvlStaaOtsdPrcqty, "sclvlStaaOtsdPrcqty"),
thlvlStaaOtsdPrcqty: formatNumber(d.thlvlStaaOtsdPrcqty, "thlvlStaaOtsdPrcqty"),
folvlStaaOtsdPrcqty: formatNumber(d.folvlStaaOtsdPrcqty, "folvlStaaOtsdPrcqty"),
filvlStaaOtsdPrcqty: formatNumber(d.filvlStaaOtsdPrcqty, "filvlStaaOtsdPrcqty"),
sxlvlStaaOtsdPrcqty: formatNumber(d.sxlvlStaaOtsdPrcqty, "sxlvlStaaOtsdPrcqty"),
svlvlStaaOtsdPrcqty: formatNumber(d.svlvlStaaOtsdPrcqty, "svlvlStaaOtsdPrcqty"),
rmrk: nullableTrimmedString(d.rmrk),
}));
}
@@ -236,34 +236,34 @@ function buildSepticTankStatusSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
staaInsdSttkNprsflCnt: formatNumber(d.staaInsdSttkNprsflCnt, "staaInsdSttkNprsflCnt"),
step1StaaInsdSttkFrcCnt: formatNumber(d.step1StaaInsdSttkFrcCnt, "step1StaaInsdSttkFrcCnt"),
staaInsdSprktpFltrtbCnt: formatNumber(d.staaInsdSprktpFltrtbCnt, "staaInsdSprktpFltrtbCnt"),
staaInsdAertMthdCnt: formatNumber(d.staaInsdAertMthdCnt, "staaInsdAertMthdCnt"),
staaInsdCntcAertCnt: formatNumber(d.staaInsdCntcAertCnt, "staaInsdCntcAertCnt"),
staaInsdSpctnkNatrCnt: formatNumber(d.staaInsdSpctnkNatrCnt, "staaInsdSpctnkNatrCnt"),
staaInsdSpctnkFrcCnt: formatNumber(d.staaInsdSpctnkFrcCnt, "staaInsdSpctnkFrcCnt"),
staaInsdSprklFiltrCnt: formatNumber(d.staaInsdSprklFiltrCnt, "staaInsdSprklFiltrCnt"),
staaInsdAirCnt: formatNumber(d.staaInsdAirCnt, "staaInsdAirCnt"),
staaInsdCntcAirCnt: formatNumber(d.staaInsdCntcAirCnt, "staaInsdCntcAirCnt"),
staaInsdEtcCnt: formatNumber(d.staaInsdEtcCnt, "staaInsdEtcCnt"),
step1StaaInsdNope: formatNumber(d.step1StaaInsdNope, "step1StaaInsdNope"),
step2StaaInsdNope: formatNumber(d.step2StaaInsdNope, "step2StaaInsdNope"),
step3StaaInsdNope: formatNumber(d.step3StaaInsdNope, "step3StaaInsdNope"),
step4StaaInsdNope: formatNumber(d.step4StaaInsdNope, "step4StaaInsdNope"),
step5StaaInsdNope: formatNumber(d.step5StaaInsdNope, "step5StaaInsdNope"),
step6StaaInsdNope: formatNumber(d.step6StaaInsdNope, "step6StaaInsdNope"),
step7StaaInsdNope: formatNumber(d.step7StaaInsdNope, "step7StaaInsdNope"),
step8StaaInsdNope: formatNumber(d.step8StaaInsdNope, "step8StaaInsdNope"),
staaExcpSttkNprsflCnt: formatNumber(d.staaExcpSttkNprsflCnt, "staaExcpSttkNprsflCnt"),
step1StaaExcpSttkFrcCnt: formatNumber(d.step1StaaExcpSttkFrcCnt, "step1StaaExcpSttkFrcCnt"),
staaExcpSprktpFltrtbCnt: formatNumber(d.staaExcpSprktpFltrtbCnt, "staaExcpSprktpFltrtbCnt"),
staaExcpAertMthdCnt: formatNumber(d.staaExcpAertMthdCnt, "staaExcpAertMthdCnt"),
staaExcpCntcAertCnt: formatNumber(d.staaExcpCntcAertCnt, "staaExcpCntcAertCnt"),
staaExcpEtcCnt: formatNumber(d.staaExcpEtcCnt, "staaExcpEtcCnt"),
step1StaaExcpNope: formatNumber(d.step1StaaExcpNope, "step1StaaExcpNope"),
step2StaaExcpNope: formatNumber(d.step2StaaExcpNope, "step2StaaExcpNope"),
step3StaaExcpNope: formatNumber(d.step3StaaExcpNope, "step3StaaExcpNope"),
step4StaaExcpNope: formatNumber(d.step4StaaExcpNope, "step4StaaExcpNope"),
step5StaaExcpNope: formatNumber(d.step5StaaExcpNope, "step5StaaExcpNope"),
step6StaaExcpNope: formatNumber(d.step6StaaExcpNope, "step6StaaExcpNope"),
step7StaaExcpNope: formatNumber(d.step7StaaExcpNope, "step7StaaExcpNope"),
step8StaaExcpNope: formatNumber(d.step8StaaExcpNope, "step8StaaExcpNope"),
frlvlStaaInsdNope: formatNumber(d.frlvlStaaInsdNope, "frlvlStaaInsdNope"),
sclvlStaaInsdNope: formatNumber(d.sclvlStaaInsdNope, "sclvlStaaInsdNope"),
thlvlStaaInsdNope: formatNumber(d.thlvlStaaInsdNope, "thlvlStaaInsdNope"),
folvlStaaInsdNope: formatNumber(d.folvlStaaInsdNope, "folvlStaaInsdNope"),
filvlStaaInsdNope: formatNumber(d.filvlStaaInsdNope, "filvlStaaInsdNope"),
sxlvlStaaInsdNope: formatNumber(d.sxlvlStaaInsdNope, "sxlvlStaaInsdNope"),
svlvlStaaInsdNope: formatNumber(d.svlvlStaaInsdNope, "svlvlStaaInsdNope"),
eilvlStaaInsdNope: formatNumber(d.eilvlStaaInsdNope, "eilvlStaaInsdNope"),
staaOtsdSpctnkNatrCnt: formatNumber(d.staaOtsdSpctnkNatrCnt, "staaOtsdSpctnkNatrCnt"),
staaOtsdSpctnkFrcCnt: formatNumber(d.staaOtsdSpctnkFrcCnt, "staaOtsdSpctnkFrcCnt"),
staaOtsdSprklFiltrCnt: formatNumber(d.staaOtsdSprklFiltrCnt, "staaOtsdSprklFiltrCnt"),
staaOtsdAirCnt: formatNumber(d.staaOtsdAirCnt, "staaOtsdAirCnt"),
staaOtsdCntcAirCnt: formatNumber(d.staaOtsdCntcAirCnt, "staaOtsdCntcAirCnt"),
staaOtsdEtcCnt: formatNumber(d.staaOtsdEtcCnt, "staaOtsdEtcCnt"),
frlvlStaaOtsdNope: formatNumber(d.frlvlStaaOtsdNope, "frlvlStaaOtsdNope"),
sclvlStaaOtsdNope: formatNumber(d.sclvlStaaOtsdNope, "sclvlStaaOtsdNope"),
thlvlStaaOtsdNope: formatNumber(d.thlvlStaaOtsdNope, "thlvlStaaOtsdNope"),
folvlStaaOtsdNope: formatNumber(d.folvlStaaOtsdNope, "folvlStaaOtsdNope"),
filvlStaaOtsdNope: formatNumber(d.filvlStaaOtsdNope, "filvlStaaOtsdNope"),
sxlvlStaaOtsdNope: formatNumber(d.sxlvlStaaOtsdNope, "sxlvlStaaOtsdNope"),
svlvlStaaOtsdNope: formatNumber(d.svlvlStaaOtsdNope, "svlvlStaaOtsdNope"),
eilvlStaaOtsdNope: formatNumber(d.eilvlStaaOtsdNope, "eilvlStaaOtsdNope"),
rmrk: nullableTrimmedString(d.rmrk),
}));
}
@@ -278,13 +278,13 @@ function buildPrivateSewageDesigStatusSaveList(
admdstNm: nullableTrimmedString(d.admdstNm),
indvSwagePbancRgnCnt: formatNumber(d.indvSwagePbancRgnCnt, "indvSwagePbancRgnCnt"),
indvSwageDsgnScpLen: formatNumber(d.indvSwageDsgnScpLen, "indvSwageDsgnScpLen"),
spctnkGrssFcltCnt: formatNumber(d.spctnkGrssFcltCnt, "spctnkGrssFcltCnt"),
spctnkQnt: formatNumber(d.spctnkQnt, "spctnkQnt"),
drtwPrcsFcltGrssFcltCnt: formatNumber(
d.drtwPrcsFcltGrssFcltCnt,
"drtwPrcsFcltGrssFcltCnt",
spctnkWholFcltCnt: formatNumber(d.spctnkWholFcltCnt, "spctnkWholFcltCnt"),
spctnkPrcqty: formatNumber(d.spctnkPrcqty, "spctnkPrcqty"),
swgdplFcltWholFcltCnt: formatNumber(
d.swgdplFcltWholFcltCnt,
"swgdplFcltWholFcltCnt",
),
drtwPrcsFcltQnt: formatNumber(d.drtwPrcsFcltQnt, "drtwPrcsFcltQnt"),
swgdplFcltPrcqty: formatNumber(d.swgdplFcltPrcqty, "swgdplFcltPrcqty"),
rmrk: nullableTrimmedString(d.rmrk),
}));
}
@@ -721,7 +721,7 @@ const PrivateSewageFacilityInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -797,20 +797,19 @@ const PrivateSewageFacilityInp = () => {
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
};
const verificationQuery = {
@@ -840,9 +839,8 @@ const PrivateSewageFacilityInp = () => {
if (isSewageTreatTab) {
const param: SewageTreatFactStatusSubmitRequest = {
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildSewageTreatFactStatusSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus(
@@ -851,18 +849,16 @@ const PrivateSewageFacilityInp = () => {
} else if (isSepticTankTab) {
const param: SepticTankStatusSubmitRequest = {
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildSepticTankStatusSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param);
} else {
const param: PrivateSewageDesigStatusSubmitRequest = {
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildPrivateSewageDesigStatusSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus(
@@ -886,7 +882,7 @@ const PrivateSewageFacilityInp = () => {
sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
lae_sbmsn_stts_cd: activeTabStatus?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -982,7 +978,7 @@ const PrivateSewageFacilityInp = () => {
if (isSewageTreatTab) {
const param: SewageTreatFactStatusSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildSewageTreatFactStatusSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
@@ -1008,7 +1004,7 @@ const PrivateSewageFacilityInp = () => {
if (isSepticTankTab) {
const param: SepticTankStatusSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildSepticTankStatusSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
@@ -1031,7 +1027,7 @@ const PrivateSewageFacilityInp = () => {
const param: PrivateSewageDesigStatusSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildPrivateSewageDesigStatusSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
@@ -1156,7 +1152,7 @@ const PrivateSewageFacilityInp = () => {
: e.vrfcCd === "NV"
? "일반검증 - "
: "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1202,7 +1198,7 @@ const PrivateSewageFacilityInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1,4 +1,4 @@
import { type Dispatch, type SetStateAction,useEffect, useRef, useState } from 'react';
import { type Dispatch, type SetStateAction,useEffect, useRef, useState } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
@@ -89,8 +89,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (['2', '3'].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -108,7 +108,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2';
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === '2';
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
@@ -397,16 +397,16 @@ const PublicSewageFacilityInp = () => {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
noApplicable,
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
submitList,
};
@@ -432,7 +432,7 @@ const PublicSewageFacilityInp = () => {
sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? '',
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? '',
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2',
lae_sbmsn_stts_cd: activeTabStatus?.lclgvSbmsnSttsCd === '4' ? '4' : '2',
gen_verify_stts_cd: nvpassed ? '1' : '2',
hb_pass_yn: response.data?.isHbPassed,
};
@@ -657,7 +657,7 @@ const PublicSewageFacilityInp = () => {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: 'superAdmin' } : {}),
noApplicable,
saveList,
@@ -902,7 +902,7 @@ const PublicSewageFacilityInp = () => {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
};
const response =
@@ -949,7 +949,7 @@ const PublicSewageFacilityInp = () => {
admdstNm: draft.admdstNm,
mainFcltSn: draft.mainFcltSn,
mainFcltNm: draft.mainFcltNm,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: draft.saveList,
deleteList: draft.deleteList,
};
@@ -1128,7 +1128,7 @@ const PublicSewageFacilityInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues?.sido ?? '',
statsYr: lastSearchedValues?.year ?? '',
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForCancel,
};
@@ -1203,15 +1203,17 @@ const PublicSewageFacilityInp = () => {
alert('해당 시설은 중심처리시설의 위탁시설로 연결되어있습니다.');
}
const mappedTargetRows = applyExclusiveSelfMappingChecks(
rawList.map((item) => ({
...item,
__checked: item.mappedYn === 'Y' ? 'Y' : '',
})),
toGridRows(
rawList.map((item) => ({
...item,
__checked: item.mappedYn === 'Y' ? 'Y' : '',
})),
),
fcltSn,
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('MainFacilityTarget', toGridRows(mappedTargetRows));
next.set('MainFacilityTarget', mappedTargetRows);
return next;
});
} catch {
@@ -1321,7 +1323,7 @@ const PublicSewageFacilityInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{!isMainFacilityTab && activeTabStatus?.laeSbmsnSttsCd === '2' && submitOk && (
{!isMainFacilityTab && activeTabStatus?.lclgvSbmsnSttsCd === '2' && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1577,8 +1579,8 @@ const PublicSewageFacilityInp = () => {
.map((e) => {
const prefix =
e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? "";
const comp = e.cmprCn ?? "";
const now = e.nowCn ?? "";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${comp} / 현재 입력값: ${now})`;
})
@@ -93,8 +93,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -130,7 +130,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
@@ -252,34 +252,34 @@ function buildPumpingStationSaveItem(
pumpSeNm: nullableTrimmedString(row.pumpSeNm),
fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planMltvArea: toGridNumber(row.planMltvArea),
sndyArea: toGridNumber(row.sndyArea),
planDrnar: toGridNumber(row.planDrnar),
icodwArea: toGridNumber(row.icodwArea),
rainArea: toGridNumber(row.rainArea),
drtwPumpN1DmtrArea: toGridNumber(row.drtwPumpN1DmtrArea),
drtwPumpN1Dmtr: toGridNumber(row.drtwPumpN1Dmtr),
drtwPumpN1Ppqty: toGridNumber(row.drtwPumpN1Ppqty),
drtwPumpN1Cnt: toGridNumber(row.drtwPumpN1Cnt),
drtwPumpN1RsrvCnt: toGridNumber(row.drtwPumpN1RsrvCnt),
drtwPumpN2DmtrArea: toGridNumber(row.drtwPumpN2DmtrArea),
drtwPumpN2Dmtr: toGridNumber(row.drtwPumpN2Dmtr),
drtwPumpN2Ppqty: toGridNumber(row.drtwPumpN2Ppqty),
drtwPumpN2Cnt: toGridNumber(row.drtwPumpN2Cnt),
drtwPumpN2RsrvCnt: toGridNumber(row.drtwPumpN2RsrvCnt),
drtwPumpN3DmtrArea: toGridNumber(row.drtwPumpN3DmtrArea),
drtwPumpN3Dmtr: toGridNumber(row.drtwPumpN3Dmtr),
drtwPumpN3Ppqty: toGridNumber(row.drtwPumpN3Ppqty),
drtwPumpN3Cnt: toGridNumber(row.drtwPumpN3Cnt),
drtwPumpN3RsrvCnt: toGridNumber(row.drtwPumpN3RsrvCnt),
exlnPumpN1DmtrArea: toGridNumber(row.exlnPumpN1DmtrArea),
exlnPumpN1Dmtr: toGridNumber(row.exlnPumpN1Dmtr),
exlnPumpN1Ppqty: toGridNumber(row.exlnPumpN1Ppqty),
exlnPumpN1Cnt: toGridNumber(row.exlnPumpN1Cnt),
exlnPumpN1RsrvCnt: toGridNumber(row.exlnPumpN1RsrvCnt),
exlnPumpN2DmtrArea: toGridNumber(row.exlnPumpN2DmtrArea),
exlnPumpN2Dmtr: toGridNumber(row.exlnPumpN2Dmtr),
exlnPumpN2Ppqty: toGridNumber(row.exlnPumpN2Ppqty),
exlnPumpN2Cnt: toGridNumber(row.exlnPumpN2Cnt),
exlnPumpN2RsrvCnt: toGridNumber(row.exlnPumpN2RsrvCnt),
exlnPumpN3DmtrArea: toGridNumber(row.exlnPumpN3DmtrArea),
exlnPumpN3Dmtr: toGridNumber(row.exlnPumpN3Dmtr),
exlnPumpN3Ppqty: toGridNumber(row.exlnPumpN3Ppqty),
exlnPumpN3Cnt: toGridNumber(row.exlnPumpN3Cnt),
exlnPumpN3RsrvCnt: toGridNumber(row.exlnPumpN3RsrvCnt),
anlElpwrUseQnt: toGridNumber(row.anlElpwrUseQnt),
anlElpwrUsqty: toGridNumber(row.anlElpwrUsqty),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -606,7 +606,7 @@ const PumpingStationInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCd || PUMPING_STATION_STATS_CD,
};
@@ -691,7 +691,7 @@ const PumpingStationInp = () => {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable,
saveList,
@@ -759,16 +759,15 @@ const PumpingStationInp = () => {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: userInfo.userUnqId,
laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
userUuid: userInfo.userUnqId,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
noApplicable,
submitList,
deleteList,
@@ -791,7 +790,7 @@ const PumpingStationInp = () => {
stats_cd: statsCdForSubmit,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd:
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
statusInfo?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -945,7 +944,7 @@ const PumpingStationInp = () => {
? "일반검증 - "
: "모집단 검증 -";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1027,7 +1026,7 @@ const PumpingStationInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{statusInfo?.laeSbmsnSttsCd === "2" && submitOk && (
{statusInfo?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -16,6 +16,7 @@ import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ExcretaRelatedIndustrySaveItem,
PrivateSewageTreatRelatedIndustrySaveItem,
PublicInstitutionExcretaIndustrySaveItem,
PublicInstitutionSewerIndustrySaveItem,
SewageExcretaRelatedRequestParams,
SewageExcretaRelatedVerificationResult,
@@ -157,8 +158,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -194,7 +195,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
@@ -238,10 +239,20 @@ function isCheckedRow(row: Record<string, string | number>): boolean {
type ArticleOgnzSnSet = Set<string>;
function extractOgnzSnSet(rows: Record<string, string | number>[]): ArticleOgnzSnSet {
const PUBLIC_INSTITUTION_OGNZ_SN_FIELD = "statsOgnzSn" as const;
function getPublicInstitutionOgnzSn(
row: Record<string, string | number>,
): string {
return String(row[PUBLIC_INSTITUTION_OGNZ_SN_FIELD] ?? "").trim();
}
function extractOgnzSnSet(
rows: Record<string, string | number>[],
): ArticleOgnzSnSet {
const set = new Set<string>();
for (const row of rows) {
const ognzSn = String(row.ognzSn ?? "").trim();
const ognzSn = getPublicInstitutionOgnzSn(row);
if (ognzSn) set.add(ognzSn);
}
return set;
@@ -277,7 +288,7 @@ function isPersistedPublicInstitutionArticleRow(
): boolean {
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) return false;
if (isNewArticleRow(row)) return false;
const ognzSn = String(row.ognzSn ?? "").trim();
const ognzSn = getPublicInstitutionOgnzSn(row);
if (!ognzSn) return false;
if ((preYearRef.current.get(tabKey) ?? new Set()).has(ognzSn)) return false;
return (persistedRef.current.get(tabKey) ?? new Set()).has(ognzSn);
@@ -402,18 +413,18 @@ function toGridNumber(value: string | number | null | undefined): number {
function buildPublicInstitutionIndustrySaveItem(
row: Record<string, string | number>,
sggOptions: Sgg[],
): PublicInstitutionSewerIndustrySaveItem {
): PublicInstitutionSewerIndustrySaveItem | PublicInstitutionExcretaIndustrySaveItem {
const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(row, sggOptions);
return {
admdstCd,
admdstNm,
ognzSn: nullableTrimmedString(row.ognzSn),
statsOgnzSn: nullableTrimmedString(row.statsOgnzSn),
ognzNm: nullableTrimmedString(row.ognzNm),
pbadmsNope: toGridNumber(row.pbadmsNope),
engrkCnt: toGridNumber(row.engrkCnt),
mchnCnt: toGridNumber(row.mchnCnt),
prvtmCnt: toGridNumber(row.prvtmCnt),
chmengCnt: toGridNumber(row.chmengCnt),
chemCnt: toGridNumber(row.chemCnt),
envCnt: toGridNumber(row.envCnt),
enjbEtcCnt: toGridNumber(row.enjbEtcCnt),
etcCnt: toGridNumber(row.etcCnt),
@@ -430,9 +441,9 @@ function buildExcretaRelatedIndustrySaveItem(
admdstNm: nullableTrimmedString(row.admdstNm),
bzentyCnt: toGridNumber(row.bzentyCnt),
empNope: toGridNumber(row.empNope),
n3TonUdrCnt: toGridNumber(row.n3TonUdrCnt),
n4TonUdrCnt: toGridNumber(row.n4TonUdrCnt),
n8TonUdrCnt: toGridNumber(row.n8TonUdrCnt),
frlvlTonUdrCnt: toGridNumber(row.frlvlTonUdrCnt),
sclvlTonUdrCnt: toGridNumber(row.sclvlTonUdrCnt),
thlvlTonUdrCnt: toGridNumber(row.thlvlTonUdrCnt),
etcCnt: toGridNumber(row.etcCnt),
clctCnvncPrfmncQnt: toGridNumber(row.clctCnvncPrfmncQnt),
rmrk: nullableTrimmedString(row.rmrk),
@@ -550,7 +561,7 @@ function verificationCellTooltip({
? "일반검증 - "
: "모집단 검증 - ";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1179,7 +1190,7 @@ const SewageExcretaRelatedInp = () => {
...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido ?? "",
statsYr: ctx.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -1317,9 +1328,12 @@ const SewageExcretaRelatedInp = () => {
let submitList:
| PublicInstitutionSewerIndustrySaveItem[]
| PublicInstitutionExcretaIndustrySaveItem[]
| ExcretaRelatedIndustrySaveItem[]
| PrivateSewageTreatRelatedIndustrySaveItem[];
let deleteList: PublicInstitutionSewerIndustrySaveItem[] = [];
let deleteList:
| PublicInstitutionSewerIndustrySaveItem[]
| PublicInstitutionExcretaIndustrySaveItem[] = [];
let sggList: Sgg[] = [];
if (isDistrictRowTab) {
@@ -1351,7 +1365,7 @@ const SewageExcretaRelatedInp = () => {
sggList = list;
const rowsForSubmit = mergeGridRowsWithMapRows(currentData, mapRows, [
"ognzSn",
PUBLIC_INSTITUTION_OGNZ_SN_FIELD,
"admdstCd",
"admdstNm",
"__isNewArticle",
@@ -1387,16 +1401,15 @@ const SewageExcretaRelatedInp = () => {
const submitRequestBase = {
...searchContextBase,
laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
...(isPublicInstitutionTab ? { noApplicable } : {}),
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
};
const verificationQuery = {
@@ -1437,14 +1450,14 @@ const SewageExcretaRelatedInp = () => {
tabKey === ExcretaRelatedIndustryInpColumn.tabKey
? await statisticsInputSewageExcretaRelatedApi.submitExcretaRelatedIndustry({
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: submitList as ExcretaRelatedIndustrySaveItem[],
})
: tabKey === PrivateSewageTreatRelatedIndustryInpColumn.tabKey
? await statisticsInputSewageExcretaRelatedApi.submitPrivateSewageTreatRelatedIndustry(
{
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList:
submitList as PrivateSewageTreatRelatedIndustrySaveItem[],
},
@@ -1453,17 +1466,17 @@ const SewageExcretaRelatedInp = () => {
? await statisticsInputSewageExcretaRelatedApi.submitPublicInstitutionSewerIndustry(
{
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: submitList as PublicInstitutionSewerIndustrySaveItem[],
deleteList,
deleteList: deleteList as PublicInstitutionSewerIndustrySaveItem[],
},
)
: await statisticsInputSewageExcretaRelatedApi.submitPublicInstitutionExcretaIndustry(
{
...submitRequestBase,
userId: userInfo.userUnqId,
submitList: submitList as PublicInstitutionSewerIndustrySaveItem[],
deleteList,
userUuid: userInfo.userUnqId,
submitList: submitList as PublicInstitutionExcretaIndustrySaveItem[],
deleteList: deleteList as PublicInstitutionExcretaIndustrySaveItem[],
},
);
@@ -1484,7 +1497,7 @@ const SewageExcretaRelatedInp = () => {
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
activeTabStatus?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -1577,7 +1590,7 @@ const SewageExcretaRelatedInp = () => {
setIsLoading(true);
const userInfo = await getUserInfo();
const userFields = {
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
};
@@ -1628,7 +1641,7 @@ const SewageExcretaRelatedInp = () => {
sggList = list;
const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [
"ognzSn",
PUBLIC_INSTITUTION_OGNZ_SN_FIELD,
"admdstCd",
"admdstNm",
"__isNewArticle",
@@ -1661,16 +1674,16 @@ const SewageExcretaRelatedInp = () => {
{
...baseSaveRequest,
...userFields,
saveList,
deleteList,
saveList: saveList as PublicInstitutionSewerIndustrySaveItem[],
deleteList: deleteList as PublicInstitutionSewerIndustrySaveItem[],
},
)
: await statisticsInputSewageExcretaRelatedApi.savePublicInstitutionExcretaIndustry(
{
...baseSaveRequest,
...userFields,
saveList,
deleteList,
saveList: saveList as PublicInstitutionExcretaIndustrySaveItem[],
deleteList: deleteList as PublicInstitutionExcretaIndustrySaveItem[],
},
);
} else {
@@ -2011,7 +2024,7 @@ const SewageExcretaRelatedInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -157,7 +157,7 @@ function verificationCellTooltip({
? "일반검증 - "
: "모집단 검증 - ";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -343,8 +343,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -380,7 +380,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
@@ -504,39 +504,39 @@ function buildSlurgeProcessingFacStatusSaveItem(
otsdInflwQnt: toGridNumber(row.otsdInflwQnt),
transfTmplntNm: nullableTrimmedString(row.transfTmplntNm),
transfPrcsQnt: toGridNumber(row.transfPrcsQnt),
srpFncRt: toGridNumber(row.srpFncRt),
itslfTs1RclmQnt: toGridNumber(row.itslfTs1RclmQnt),
itslfTs1IncnrQnt: toGridNumber(row.itslfTs1IncnrQnt),
itslfTs1AltdQnt: toGridNumber(row.itslfTs1AltdQnt),
itslfTs1DryQnt: toGridNumber(row.itslfTs1DryQnt),
itslfTs1FuelQnt: toGridNumber(row.itslfTs1FuelQnt),
itslfTs1CmpstgQnt: toGridNumber(row.itslfTs1CmpstgQnt),
sludgeFncRt: toGridNumber(row.sludgeFncRt),
itslfFrlvlRclmQnt: toGridNumber(row.itslfFrlvlRclmQnt),
itslfFrlvlIncnrQnt: toGridNumber(row.itslfFrlvlIncnrQnt),
itslfFrlvlSldQnt: toGridNumber(row.itslfFrlvlSldQnt),
itslfFrlvlDryQnt: toGridNumber(row.itslfFrlvlDryQnt),
itslfFrlvlCrbnQnt: toGridNumber(row.itslfFrlvlCrbnQnt),
itslfFrlvlCmpstgQnt: toGridNumber(row.itslfFrlvlCmpstgQnt),
cmpsPrcsFcltBzentyNm: nullableTrimmedString(row.cmpsPrcsFcltBzentyNm),
incnrPrcsLfvlm: toGridNumber(row.incnrPrcsLfvlm),
incnrPrcsPrdctRmtrlQnt: toGridNumber(row.incnrPrcsPrdctRmtrlQnt),
incnrPrcsFmlyrQnt: toGridNumber(row.incnrPrcsFmlyrQnt),
incnrPrcsCwthQnt: toGridNumber(row.incnrPrcsCwthQnt),
incnrPrcsFcltNm: nullableTrimmedString(row.incnrPrcsFcltNm),
stbznPrcsLfvlm: toGridNumber(row.stbznPrcsLfvlm),
stbznPrcsFmlyrQnt: toGridNumber(row.stbznPrcsFmlyrQnt),
stbznPrcsFcltNm: nullableTrimmedString(row.stbznPrcsFcltNm),
sldPrcsLfvlm: toGridNumber(row.sldPrcsLfvlm),
sldPrcsCwthQnt: toGridNumber(row.sldPrcsCwthQnt),
sldPrcsFcltNm: nullableTrimmedString(row.sldPrcsFcltNm),
dryPrcsLfvlm: toGridNumber(row.dryPrcsLfvlm),
dryPrcsIncnrQnt: toGridNumber(row.dryPrcsIncnrQnt),
dryPrcsFuelQnt: toGridNumber(row.dryPrcsFuelQnt),
dryPrcsPrdctRmtrlQnt: toGridNumber(row.dryPrcsPrdctRmtrlQnt),
dryPrcsFmlyrQnt: toGridNumber(row.dryPrcsFmlyrQnt),
dryPrcsCwthQnt: toGridNumber(row.dryPrcsCwthQnt),
dryPrcsCmpstgQnt: toGridNumber(row.dryPrcsCmpstgQnt),
dryPrcsFcltNm: nullableTrimmedString(row.dryPrcsFcltNm),
fuelPrcsIncnrQnt: toGridNumber(row.fuelPrcsIncnrQnt),
fuelPrcsFuelQnt: toGridNumber(row.fuelPrcsFuelQnt),
fuelPrcsFcltNm: nullableTrimmedString(row.fuelPrcsFcltNm),
crbnPrcsIncnrQnt: toGridNumber(row.crbnPrcsIncnrQnt),
crbnPrcsFuelQnt: toGridNumber(row.crbnPrcsFuelQnt),
crbnPrcsFcltNm: nullableTrimmedString(row.crbnPrcsFcltNm),
otsdCnsgnRclmQnt: toGridNumber(row.otsdCnsgnRclmQnt),
otsdCnsgnIncnrQnt: toGridNumber(row.otsdCnsgnIncnrQnt),
otsdCnsgnFuelPwplQnt: toGridNumber(row.otsdCnsgnFuelPwplQnt),
otsdCnsgnPwplQnt: toGridNumber(row.otsdCnsgnPwplQnt),
otsdCnsgnPrdctRmtrlQnt: toGridNumber(row.otsdCnsgnPrdctRmtrlQnt),
otsdCnsgnFmlyrQnt: toGridNumber(row.otsdCnsgnFmlyrQnt),
otsdCnsgnCwthQnt: toGridNumber(row.otsdCnsgnCwthQnt),
otsdCnsgnCmpstgQnt: toGridNumber(row.otsdCnsgnCmpstgQnt),
otsdCnsgnFcltNm: nullableTrimmedString(row.otsdCnsgnFcltNm),
cparTs1Whol: toGridNumber(row.cparTs1Whol),
cparFrlvlWholQnt: toGridNumber(row.cparFrlvlWholQnt),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -553,23 +553,23 @@ function buildDigestorOperatingStatusSaveItem(
swtrmtFcltCd: nullableTrimmedString(row.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm),
fcltSn: nullableTrimmedString(row.fcltSn),
digstnEnYn: nullableTrimmedString(row.digstnEnYn),
gasPrcsEnYn: nullableTrimmedString(row.gasPrcsEnYn),
};
if (isDigestorOperatingRowDisabled(row)) {
return {
...base,
digstnEnYn: "N",
digstnCpct: 0,
gasPrcsEnYn: "N",
gasPrcsCpct: 0,
actlDrvnCpct: 0,
digstnGasInputQnt: 0,
digstnGasInputSldDnst: 0,
digstnTnkOprtngDayCnt: 0,
prcsGasInputQnt: 0,
prcsGasInputSldDnst: 0,
prcsRepoOprtngDayCnt: 0,
ocrnGasQnt: 0,
digstnGasUtztnNtslQnt: 0,
digstnGasUtztnEtgtQnt: 0,
digstnGasUtztnItslfQnt: 0,
digstnGasUtztnEtcQnt: 0,
prcsGasUtztnNtslQnt: 0,
prcsGasUtztnEtgtQnt: 0,
prcsGasUtztnItslfQnt: 0,
prcsGasUtztnEtcQnt: 0,
surplsGasQnt: 0,
rmrk: null,
};
@@ -577,16 +577,16 @@ function buildDigestorOperatingStatusSaveItem(
return {
...base,
digstnCpct: toGridNumber(row.digstnCpct),
gasPrcsCpct: toGridNumber(row.gasPrcsCpct),
actlDrvnCpct: toGridNumber(row.actlDrvnCpct),
digstnGasInputQnt: toGridNumber(row.digstnGasInputQnt),
digstnGasInputSldDnst: toGridNumber(row.digstnGasInputSldDnst),
digstnTnkOprtngDayCnt: toGridNumber(row.digstnTnkOprtngDayCnt),
prcsGasInputQnt: toGridNumber(row.prcsGasInputQnt),
prcsGasInputSldDnst: toGridNumber(row.prcsGasInputSldDnst),
prcsRepoOprtngDayCnt: toGridNumber(row.prcsRepoOprtngDayCnt),
ocrnGasQnt: toGridNumber(row.ocrnGasQnt),
digstnGasUtztnNtslQnt: toGridNumber(row.digstnGasUtztnNtslQnt),
digstnGasUtztnEtgtQnt: toGridNumber(row.digstnGasUtztnEtgtQnt),
digstnGasUtztnItslfQnt: toGridNumber(row.digstnGasUtztnItslfQnt),
digstnGasUtztnEtcQnt: toGridNumber(row.digstnGasUtztnEtcQnt),
prcsGasUtztnNtslQnt: toGridNumber(row.prcsGasUtztnNtslQnt),
prcsGasUtztnEtgtQnt: toGridNumber(row.prcsGasUtztnEtgtQnt),
prcsGasUtztnItslfQnt: toGridNumber(row.prcsGasUtztnItslfQnt),
prcsGasUtztnEtcQnt: toGridNumber(row.prcsGasUtztnEtcQnt),
surplsGasQnt: toGridNumber(row.surplsGasQnt),
rmrk: nullableTrimmedString(row.rmrk),
};
@@ -604,10 +604,10 @@ function buildSewageSlurgeProcessingFacStatusSaveItem(
fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: nullableTrimmedString(row.fcltNm),
prcsMthExpln: nullableTrimmedString(row.prcsMthExpln),
fcltCpct: toGridNumber(row.fcltCpct),
sludgeFcltCpct: toGridNumber(row.sludgeFcltCpct),
anlPrcsQnt: toGridNumber(row.anlPrcsQnt),
dayAvgOprtngHr: toGridNumber(row.dayAvgOprtngHr),
anlOprtngDayCnt: toGridNumber(row.anlOprtngDayCnt),
dyrgOprtngHr: toGridNumber(row.dyrgOprtngHr),
fcltAnlOprtngDayCnt: toGridNumber(row.fcltAnlOprtngDayCnt),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -630,7 +630,7 @@ function buildExcretaSlurgeProcessingStatusSaveItem(
oceanInputQnt: toGridNumber(row.oceanInputQnt),
fuelQnt: toGridNumber(row.fuelQnt),
etcQnt: toGridNumber(row.etcQnt),
nondspsQnt: toGridNumber(row.nondspsQnt),
unprcsQnt: toGridNumber(row.unprcsQnt),
swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm),
rmrk: nullableTrimmedString(row.rmrk),
};
@@ -1357,11 +1357,6 @@ const SewageExcretaSlurgeInp = () => {
: rowsForSubmit.map((row) =>
buildExcretaSlurgeProcessingStatusSaveItem(row, list),
);
const slurgeDeleteList = isSlurgeFacTab
? (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list))
: [];
const sewageSlurgeFacDeleteList = isSewageSlurgeFacTab
? (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
@@ -1423,16 +1418,15 @@ const SewageExcretaSlurgeInp = () => {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
userId: "",
laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
userUuid: "",
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
noApplicable,
};
@@ -1443,21 +1437,20 @@ const SewageExcretaSlurgeInp = () => {
const response = isSlurgeFacTab
? await statisticsInputSewageExcretaSlurgeApi.submitSlurgeProcessingFacStatus({
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: slurgeSubmitList,
deleteList: slurgeDeleteList,
})
: isDigestorTab
? await statisticsInputSewageExcretaSlurgeApi.submitDigestorOperatingStatus({
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: digestorSubmitList,
})
: isSewageSlurgeFacTab
? await statisticsInputSewageExcretaSlurgeApi.submitSewageSlurgeProcessingFacStatus(
{
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: sewageSlurgeFacSubmitList,
deleteList: sewageSlurgeFacDeleteList,
},
@@ -1465,7 +1458,7 @@ const SewageExcretaSlurgeInp = () => {
: await statisticsInputSewageExcretaSlurgeApi.submitExcretaSlurgeProcessingStatus(
{
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: excretaSlurgeSubmitList,
},
);
@@ -1487,7 +1480,7 @@ const SewageExcretaSlurgeInp = () => {
stats_cd: statsCdForSubmit,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
activeTabStatus?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -1564,7 +1557,7 @@ const SewageExcretaSlurgeInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -1636,7 +1629,7 @@ const SewageExcretaSlurgeInp = () => {
setIsLoading(true);
const userInfo = await getUserInfo();
const userFields = {
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
};
@@ -1655,9 +1648,6 @@ const SewageExcretaSlurgeInp = () => {
: rowsForSave.map((row) =>
buildSlurgeProcessingFacStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list));
if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요.");
@@ -1670,7 +1660,6 @@ const SewageExcretaSlurgeInp = () => {
...baseSaveRequest,
...userFields,
saveList,
deleteList,
},
);
} else if (tabKey === DigestorOperatingStatusInpColumn.tabKey) {
@@ -2109,7 +2098,7 @@ const SewageExcretaSlurgeInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -81,7 +81,10 @@ import {
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import {
getVerificationErrorsForField,
type VerificationResultLikeMap,
} from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -91,8 +94,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -219,18 +222,33 @@ const applyPipeTypeStatusEditableColumns = (
});
};
/** 2-3 관종별: verificationResult.prlgWhol → 총계 계(pipeByPrstWhol) */
const PIPE_TYPE_TOTAL_WHOL_ALIASES = ["pipeWholPrlg", "prlgWhol"] as const;
/** 2-3 관종별: 총계 검증 키(pipeWholPrlg/prlgWhol) → 총계 계(pipeByPrstWhol) */
function remapPipeTypePrlgWholVerification(
result?: SeweragePipeInputVerificationResult | null,
): SeweragePipeInputVerificationResult | null | undefined {
if (!result) return result;
const fromAlias = result.prlgWhol;
if (!Array.isArray(fromAlias) || fromAlias.length === 0) return result;
const existing = result.pipeByPrstWhol;
if (Array.isArray(existing) && existing.length > 0) return result;
const fromAlias = result.pipeWholPrlg ?? result.prlgWhol;
if (!Array.isArray(fromAlias) || fromAlias.length === 0) return result;
return { ...result, pipeByPrstWhol: fromAlias };
}
function getSewagePipeRowVerificationErrors(
verification: SeweragePipeInputVerificationResult | undefined,
fieldName: string,
tabKey: string,
) {
if (!verification) return undefined;
const aliases =
tabKey === PIPE_TYPE_STATUS_TAB_KEY && fieldName === "pipeByPrstWhol"
? PIPE_TYPE_TOTAL_WHOL_ALIASES
: undefined;
return getVerificationErrorsForField(verification, fieldName, aliases);
}
/** 관종별 신 응답(yearlyStatList) -> 그리드 y{year}* 필드로 정규화 */
function normalizePipeTypeStatusRow(
row: SeweragePipeInputPipeTypeStatusItem,
@@ -242,6 +260,7 @@ function normalizePipeTypeStatusRow(
lyngPrlgWhol,
dmltnPrlgWhol,
verificationResult,
rmrk,
...rest
} = row;
const yearFields: Record<string, number> = {};
@@ -256,7 +275,7 @@ function normalizePipeTypeStatusRow(
if (!Number.isFinite(year)) continue;
const targetYear = Math.min(lastDataYear, Math.max(1995, year));
yearFields[`y${targetYear}PrlgWhol`] =
(yearFields[`y${targetYear}PrlgWhol`] ?? 0) + Number(y.prlgWhol ?? 0);
(yearFields[`y${targetYear}PrlgWhol`] ?? 0) + Number(y.pipeWholPrlg ?? 0);
yearFields[`y${targetYear}LyngPrlg`] =
(yearFields[`y${targetYear}LyngPrlg`] ?? 0) + Number(y.lyngPrlg ?? 0);
yearFields[`y${targetYear}DmltnPrlg`] =
@@ -266,8 +285,9 @@ function normalizePipeTypeStatusRow(
return {
ctpvNm,
...rest,
lyngPrlg: Number(row.lyngPrlg ?? lyngPrlgWhol ?? 0),
dmltnPrlg: Number(row.dmltnPrlg ?? dmltnPrlgWhol ?? 0),
rmrk: rmrk ?? "",
lyngPrlg: Number(lyngPrlgWhol ?? 0),
dmltnPrlg: Number(dmltnPrlgWhol ?? 0),
...yearFields,
verificationResult: remapPipeTypePrlgWholVerification(verificationResult) ?? undefined,
};
@@ -330,7 +350,6 @@ function buildRegionalFactSubmitListFromGrid(
};
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: nullableTrimmedString(d.goalYr),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
@@ -341,29 +360,29 @@ function buildRegionalFactSubmitListFromGrid(
cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"),
cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"),
cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"),
cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"),
cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"),
cbsysShpPrlg: formatNumber(d.cbsysShpPrlg, "cbsysShpPrlg"),
cbsysElpsdPrlg: formatNumber(d.cbsysElpsdPrlg, "cbsysElpsdPrlg"),
cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"),
cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"),
cbsysLaditPrlg: formatNumber(d.cbsysLaditPrlg, "cbsysLaditPrlg"),
drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"),
drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"),
drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"),
drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"),
drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"),
drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"),
drtwPplShpPrlg: formatNumber(d.drtwPplShpPrlg, "drtwPplShpPrlg"),
drtwPplElpsdPrlg: formatNumber(d.drtwPplElpsdPrlg, "drtwPplElpsdPrlg"),
exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"),
exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"),
exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"),
exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"),
exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"),
exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"),
exlnPplShpPrlg: formatNumber(d.exlnPplShpPrlg, "exlnPplShpPrlg"),
exlnPplElpsdPrlg: formatNumber(d.exlnPplElpsdPrlg, "exlnPplElpsdPrlg"),
exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"),
exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"),
exlnPplLaditPrlg: formatNumber(d.exlnPplLaditPrlg, "exlnPplLaditPrlg"),
cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"),
sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"),
sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"),
drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"),
exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"),
drtwDrgCnt: formatNumber(d.drtwDrgCnt, "drtwDrgCnt"),
exlnDrgCnt: formatNumber(d.exlnDrgCnt, "exlnDrgCnt"),
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
@@ -419,7 +438,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
@@ -1109,7 +1128,7 @@ const SewagePipeInp = () => {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? "",
statsYr: searchValues?.year ?? "",
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -1191,15 +1210,15 @@ const SewagePipeInp = () => {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
userUuid: userInfo.userUnqId,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
};
let response: SeweragePipeInputSubmitResponse | null = null;
@@ -1244,20 +1263,19 @@ const SewagePipeInp = () => {
};
const submitList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
cbsysTrtznArea: formatNumber(d.cbsysTrtznArea, "cbsysTrtznArea"),
sprsysTrtznArea: formatNumber(d.sprsysTrtznArea, "sprsysTrtznArea"),
sprsysVartnRt: formatNumber(d.sprsysVartnRt, "sprsysVartnRt"),
mltvEqpmCnt: formatNumber(d.mltvEqpmCnt, "mltvEqpmCnt"),
cbsysMltvEqpmCnt: formatNumber(d.cbsysMltvEqpmCnt, "cbsysMltvEqpmCnt"),
sprsysMltvEqpmCnt: formatNumber(d.sprsysMltvEqpmCnt, "sprsysMltvEqpmCnt"),
sprsysMltvLnkgCnt: formatNumber(d.sprsysMltvLnkgCnt, "sprsysMltvLnkgCnt"),
sprsysMltvYetLnkgCnt: formatNumber(d.sprsysMltvYetLnkgCnt, "sprsysMltvYetLnkgCnt"),
wholDrgCnt: formatNumber(d.wholDrgCnt, "wholDrgCnt"),
cbsysDrgCnt: formatNumber(d.cbsysDrgCnt, "cbsysDrgCnt"),
sprsysDrgCnt: formatNumber(d.sprsysDrgCnt, "sprsysDrgCnt"),
sprsysDrgLnkgCnt: formatNumber(d.sprsysDrgLnkgCnt, "sprsysDrgLnkgCnt"),
sprsysDrgNoinsLnkgCnt: formatNumber(d.sprsysDrgNoinsLnkgCnt, "sprsysDrgNoinsLnkgCnt"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholIcptgAtlfcsPrlg: formatNumber(d.wholIcptgAtlfcsPrlg, "wholIcptgAtlfcsPrlg"),
icptgPrlg: formatNumber(d.icptgPrlg, "icptgPrlg"),
@@ -1265,10 +1283,10 @@ const SewagePipeInp = () => {
cbsysStofcbrCnt: formatNumber(d.cbsysStofcbrCnt, "cbsysStofcbrCnt"),
sprsysStofcbrCnt: formatNumber(d.sprsysStofcbrCnt, "sprsysStofcbrCnt"),
sprsysExlnOtflCnt: formatNumber(d.sprsysExlnOtflCnt, "sprsysExlnOtflCnt"),
wholBrchlnPrlg: formatNumber(d.wholBrchlnPrlg, "wholBrchlnPrlg"),
cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"),
exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"),
drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"),
wholPplPrlg: formatNumber(d.wholPplPrlg, "wholPplPrlg"),
cbsysPplPrlg: formatNumber(d.cbsysPplPrlg, "cbsysPplPrlg"),
exlnPplPrlg: formatNumber(d.exlnPplPrlg, "exlnPplPrlg"),
drtwPplPrlg: formatNumber(d.drtwPplPrlg, "drtwPplPrlg"),
rmrk: nullableTrimmedString(d.rmrk),
}));
const requestData: SeweragePipeInputFactInstallStatusSubmitRequest = {
@@ -1309,7 +1327,7 @@ const SewagePipeInp = () => {
lyngYr: String(year),
lyngPrlg: formatNumber(d[lyngKey] as string | number | undefined, lyngKey),
dmltnPrlg: formatNumber(d[dmltnKey] as string | number | undefined, dmltnKey),
prlgWhol: formatNumber(d[wholKey] as string | number | undefined, wholKey),
pipeWholPrlg: formatNumber(d[wholKey] as string | number | undefined, wholKey),
});
}
return {
@@ -1352,15 +1370,15 @@ const SewagePipeInp = () => {
};
const submitList = currentData.map((d) => ({
pipeSeCd: nullableTrimmedString(d.pipeSeCd),
step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"),
step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"),
step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"),
step4PdmtPrlg: formatNumber(d.step4PdmtPrlg, "step4PdmtPrlg"),
step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"),
sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"),
frlvlPdmtPrlg: formatNumber(d.frlvlPdmtPrlg, "frlvlPdmtPrlg"),
sclvlPdmtPrlg: formatNumber(d.sclvlPdmtPrlg, "sclvlPdmtPrlg"),
thlvlPdmtPrlg: formatNumber(d.thlvlPdmtPrlg, "thlvlPdmtPrlg"),
folvlPdmtPrlg: formatNumber(d.folvlPdmtPrlg, "folvlPdmtPrlg"),
filvlPdmtPrlg: formatNumber(d.filvlPdmtPrlg, "filvlPdmtPrlg"),
shpPdmtPrlg: formatNumber(d.shpPdmtPrlg, "shpPdmtPrlg"),
etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"),
rmrk: nullableTrimmedString(d.rmrk),
pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"),
pdmtSeWholPrlg: formatNumber(d.pdmtSeWholPrlg, "pdmtSeWholPrlg"),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
}));
@@ -1431,7 +1449,7 @@ const SewagePipeInp = () => {
sewer_bzmn_cd: values?.bsnmCd ?? '',
stats_cd: statsCdForTab,
stats_yr: values.year ?? "",
lae_sbmsn_stts_cd: statusForTab?.laeSbmsnSttsCd === "4" ? "4" : "2",
lae_sbmsn_stts_cd: statusForTab?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -1533,7 +1551,7 @@ const SewagePipeInp = () => {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '',
statsYr: searchValues?.year ?? '',
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
};
@@ -1617,7 +1635,6 @@ const SewagePipeInp = () => {
const saveList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: nullableTrimmedString(d.goalYr),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
@@ -1628,29 +1645,29 @@ const SewagePipeInp = () => {
cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"),
cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"),
cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"),
cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"),
cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"),
cbsysShpPrlg: formatNumber(d.cbsysShpPrlg, "cbsysShpPrlg"),
cbsysElpsdPrlg: formatNumber(d.cbsysElpsdPrlg, "cbsysElpsdPrlg"),
cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"),
cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"),
cbsysLaditPrlg: formatNumber(d.cbsysLaditPrlg, "cbsysLaditPrlg"),
drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"),
drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"),
drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"),
drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"),
drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"),
drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"),
drtwPplShpPrlg: formatNumber(d.drtwPplShpPrlg, "drtwPplShpPrlg"),
drtwPplElpsdPrlg: formatNumber(d.drtwPplElpsdPrlg, "drtwPplElpsdPrlg"),
exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"),
exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"),
exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"),
exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"),
exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"),
exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"),
exlnPplShpPrlg: formatNumber(d.exlnPplShpPrlg, "exlnPplShpPrlg"),
exlnPplElpsdPrlg: formatNumber(d.exlnPplElpsdPrlg, "exlnPplElpsdPrlg"),
exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"),
exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"),
exlnPplLaditPrlg: formatNumber(d.exlnPplLaditPrlg, "exlnPplLaditPrlg"),
cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"),
sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"),
sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"),
drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"),
exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"),
drtwDrgCnt: formatNumber(d.drtwDrgCnt, "drtwDrgCnt"),
exlnDrgCnt: formatNumber(d.exlnDrgCnt, "exlnDrgCnt"),
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
@@ -1662,7 +1679,7 @@ const SewagePipeInp = () => {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
};
@@ -1708,20 +1725,19 @@ const SewagePipeInp = () => {
const saveList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
cbsysTrtznArea: formatNumber(d.cbsysTrtznArea, "cbsysTrtznArea"),
sprsysTrtznArea: formatNumber(d.sprsysTrtznArea, "sprsysTrtznArea"),
sprsysVartnRt: formatNumber(d.sprsysVartnRt, "sprsysVartnRt"),
mltvEqpmCnt: formatNumber(d.mltvEqpmCnt, "mltvEqpmCnt"),
cbsysMltvEqpmCnt: formatNumber(d.cbsysMltvEqpmCnt, "cbsysMltvEqpmCnt"),
sprsysMltvEqpmCnt: formatNumber(d.sprsysMltvEqpmCnt, "sprsysMltvEqpmCnt"),
sprsysMltvLnkgCnt: formatNumber(d.sprsysMltvLnkgCnt, "sprsysMltvLnkgCnt"),
sprsysMltvYetLnkgCnt: formatNumber(d.sprsysMltvYetLnkgCnt, "sprsysMltvYetLnkgCnt"),
wholDrgCnt: formatNumber(d.wholDrgCnt, "wholDrgCnt"),
cbsysDrgCnt: formatNumber(d.cbsysDrgCnt, "cbsysDrgCnt"),
sprsysDrgCnt: formatNumber(d.sprsysDrgCnt, "sprsysDrgCnt"),
sprsysDrgLnkgCnt: formatNumber(d.sprsysDrgLnkgCnt, "sprsysDrgLnkgCnt"),
sprsysDrgNoinsLnkgCnt: formatNumber(d.sprsysDrgNoinsLnkgCnt, "sprsysDrgNoinsLnkgCnt"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholIcptgAtlfcsPrlg: formatNumber(d.wholIcptgAtlfcsPrlg, "wholIcptgAtlfcsPrlg"),
icptgPrlg: formatNumber(d.icptgPrlg, "icptgPrlg"),
@@ -1729,10 +1745,10 @@ const SewagePipeInp = () => {
cbsysStofcbrCnt: formatNumber(d.cbsysStofcbrCnt, "cbsysStofcbrCnt"),
sprsysStofcbrCnt: formatNumber(d.sprsysStofcbrCnt, "sprsysStofcbrCnt"),
sprsysExlnOtflCnt: formatNumber(d.sprsysExlnOtflCnt, "sprsysExlnOtflCnt"),
wholBrchlnPrlg: formatNumber(d.wholBrchlnPrlg, "wholBrchlnPrlg"),
cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"),
exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"),
drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"),
wholPplPrlg: formatNumber(d.wholPplPrlg, "wholPplPrlg"),
cbsysPplPrlg: formatNumber(d.cbsysPplPrlg, "cbsysPplPrlg"),
exlnPplPrlg: formatNumber(d.exlnPplPrlg, "exlnPplPrlg"),
drtwPplPrlg: formatNumber(d.drtwPplPrlg, "drtwPplPrlg"),
rmrk: nullableTrimmedString(d.rmrk),
}));
@@ -1740,7 +1756,7 @@ const SewagePipeInp = () => {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
};
@@ -1800,7 +1816,7 @@ const SewagePipeInp = () => {
d[dmltnKey] as string | number | undefined,
dmltnKey,
),
prlgWhol: formatNumber(
pipeWholPrlg: formatNumber(
d[wholKey] as string | number | undefined,
wholKey,
),
@@ -1824,7 +1840,7 @@ const SewagePipeInp = () => {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
};
@@ -1867,15 +1883,15 @@ const SewagePipeInp = () => {
const saveList = currentData.map((d) => ({
pipeSeCd: nullableTrimmedString(d.pipeSeCd),
step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"),
step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"),
step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"),
step4PdmtPrlg: formatNumber(d.step4PdmtPrlg, "step4PdmtPrlg"),
step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"),
sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"),
frlvlPdmtPrlg: formatNumber(d.frlvlPdmtPrlg, "frlvlPdmtPrlg"),
sclvlPdmtPrlg: formatNumber(d.sclvlPdmtPrlg, "sclvlPdmtPrlg"),
thlvlPdmtPrlg: formatNumber(d.thlvlPdmtPrlg, "thlvlPdmtPrlg"),
folvlPdmtPrlg: formatNumber(d.folvlPdmtPrlg, "folvlPdmtPrlg"),
filvlPdmtPrlg: formatNumber(d.filvlPdmtPrlg, "filvlPdmtPrlg"),
shpPdmtPrlg: formatNumber(d.shpPdmtPrlg, "shpPdmtPrlg"),
etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"),
rmrk: nullableTrimmedString(d.rmrk),
pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"),
pdmtSeWholPrlg: formatNumber(d.pdmtSeWholPrlg, "pdmtSeWholPrlg"),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
}));
@@ -1886,7 +1902,7 @@ const SewagePipeInp = () => {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
};
@@ -1944,7 +1960,7 @@ const SewagePipeInp = () => {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
};
@@ -2025,7 +2041,7 @@ const SewagePipeInp = () => {
{submitOk && <SubmitButton submitFunc={() => void submitFunc(false)} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -2200,8 +2216,11 @@ const SewagePipeInp = () => {
}
cellStyleCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
const errors = getSewagePipeRowVerificationErrors(
verification,
fieldName,
tabKey,
);
if (Array.isArray(errors) && errors.length > 0) {
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
@@ -2212,15 +2231,18 @@ const SewagePipeInp = () => {
}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
const errors = getSewagePipeRowVerificationErrors(
verification,
fieldName,
tabKey,
);
if (Array.isArray(errors) && errors.length > 0) {
return errors
.map((e) => {
const prefix =
e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? "";
const comp = e.cmprCn ?? "";
const now = e.nowCn ?? "";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${comp} / 현재 입력값: ${now})`;
})
@@ -113,8 +113,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -142,7 +142,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
@@ -218,20 +218,20 @@ function buildRepairRowList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
mendngWhol: formatNumber(d.mendngWhol, "mendngWhol"),
cbsysMendngPrlg: formatNumber(d.cbsysMendngPrlg, "cbsysMendngPrlg"),
dfrpWholPrlg: formatNumber(d.dfrpWholPrlg, "dfrpWholPrlg"),
cbsysDfrpPrlg: formatNumber(d.cbsysDfrpPrlg, "cbsysDfrpPrlg"),
sprsysExlnPrlg: formatNumber(d.sprsysExlnPrlg, "sprsysExlnPrlg"),
sprsysDrtwPrlg: formatNumber(d.sprsysDrtwPrlg, "sprsysDrtwPrlg"),
cbsysPartMendngPrlg: formatNumber(d.cbsysPartMendngPrlg, "cbsysPartMendngPrlg"),
sprsysExlnMendngPrlg: formatNumber(d.sprsysExlnMendngPrlg, "sprsysExlnMendngPrlg"),
sprsysDrtwMendngPrlg: formatNumber(d.sprsysDrtwMendngPrlg, "sprsysDrtwMendngPrlg"),
partDfrpWhol: formatNumber(d.partDfrpWhol, "partDfrpWhol"),
cbsysPartDfrpPrlg: formatNumber(d.cbsysPartDfrpPrlg, "cbsysPartDfrpPrlg"),
sprsysExlnDfrpPrlg: formatNumber(d.sprsysExlnDfrpPrlg, "sprsysExlnDfrpPrlg"),
sprsysDrtwDfrpPrlg: formatNumber(d.sprsysDrtwDfrpPrlg, "sprsysDrtwDfrpPrlg"),
cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"),
sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"),
sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"),
drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"),
exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"),
drtwDrgCnt: formatNumber(d.drtwDrgCnt, "drtwDrgCnt"),
exlnDrgCnt: formatNumber(d.exlnDrgCnt, "exlnDrgCnt"),
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
otflCnt: formatNumber(d.otflCnt, "otflCnt"),
cctvExmnPrlg: formatNumber(d.cctvExmnPrlg, "cctvExmnPrlg"),
@@ -246,7 +246,6 @@ function buildDredgingRowList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"),
@@ -254,14 +253,14 @@ function buildDredgingRowList(
sprsysExlnPrlg: formatNumber(d.sprsysExlnPrlg, "sprsysExlnPrlg"),
sprsysDrtwPrlg: formatNumber(d.sprsysDrtwPrlg, "sprsysDrtwPrlg"),
mnhlCnt: formatNumber(d.mnhlCnt, "mnhlCnt"),
exlnDrtwRcvprpCnt: formatNumber(d.exlnDrtwRcvprpCnt, "exlnDrtwRcvprpCnt"),
exlnDrtwDrgCnt: formatNumber(d.exlnDrtwDrgCnt, "exlnDrtwDrgCnt"),
stofcbrOtflCnt: formatNumber(d.stofcbrOtflCnt, "stofcbrOtflCnt"),
drdgnPlanPrlg: formatNumber(d.drdgnPlanPrlg, "drdgnPlanPrlg"),
drdgnTrgtCtrsDrdgnRt: formatNumber(d.drdgnTrgtCtrsDrdgnRt, "drdgnTrgtCtrsDrdgnRt"),
wholPrlgCtrsDrdgnRt: formatNumber(d.wholPrlgCtrsDrdgnRt, "wholPrlgCtrsDrdgnRt"),
drdgnAmt: formatNumber(d.drdgnAmt, "drdgnAmt"),
drdgnQnt: formatNumber(d.drdgnQnt, "drdgnQnt"),
drdgnUntprcAmt: formatNumber(d.drdgnUntprcAmt, "drdgnUntprcAmt"),
pplDrdqty: formatNumber(d.pplDrdqty, "pplDrdqty"),
drdgnSoilPrcsAmt: formatNumber(d.drdgnSoilPrcsAmt, "drdgnSoilPrcsAmt"),
wholDrdgnPrcsQnt: formatNumber(d.wholDrdgnPrcsQnt, "wholDrdgnPrcsQnt"),
rclmQnt: formatNumber(d.rclmQnt, "rclmQnt"),
oceanEmsnQnt: formatNumber(d.oceanEmsnQnt, "oceanEmsnQnt"),
@@ -613,14 +612,14 @@ const SewagePipeMaintainInp = () => {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
};
const verificationQuery = {
@@ -648,12 +647,12 @@ const SewagePipeMaintainInp = () => {
const response = isRepairTab
? await sewagePipeMaintainApiService.submitSewagePipeMaintainRepair({
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildRepairRowList(currentData),
} satisfies SewagePipeMaintainRepairSubmitRequest)
: await sewagePipeMaintainApiService.submitSewagePipeMaintainDredging({
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildDredgingRowList(currentData),
} satisfies SewagePipeMaintainDredgingSubmitRequest);
@@ -673,7 +672,7 @@ const SewagePipeMaintainInp = () => {
sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
lae_sbmsn_stts_cd: activeTabStatus?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -767,7 +766,7 @@ const SewagePipeMaintainInp = () => {
const param: SewagePipeMaintainRepairSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildRepairRowList(currentData),
};
if (isAdminInfo) {
@@ -804,7 +803,7 @@ const SewagePipeMaintainInp = () => {
const param: SewagePipeMaintainDredgingSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildDredgingRowList(currentData),
};
if (isAdminInfo) {
@@ -856,7 +855,7 @@ const SewagePipeMaintainInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -1108,7 +1107,7 @@ const SewagePipeMaintainInp = () => {
.map((e) => {
const prefix =
e.vrfcCd === "IMR" ? "시계열 검증 - " : e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1143,7 +1142,7 @@ const SewagePipeMaintainInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -125,8 +125,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -162,7 +162,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
@@ -274,7 +274,7 @@ function mapSewageReservoirListToGridRows(
ctpvNm,
oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
fldngPrvtnYn: normalizeOxYnValue(item.fldngPrvtnYn),
wqPlltnMtgtYn: normalizeOxYnValue(item.wqPlltnMtgtYn),
wtpltMtgtYn: normalizeOxYnValue(item.wtpltMtgtYn),
rtlztnUsgYn: normalizeOxYnValue(item.rtlztnUsgYn),
},
])[0] as GridRowWithVerification;
@@ -332,37 +332,37 @@ function buildSewageReservoirSaveItem(
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
fldngPrvtnYn: nullableTrimmedString(normalizeOxYnValue(row.fldngPrvtnYn)),
wqPlltnMtgtYn: nullableTrimmedString(normalizeOxYnValue(row.wqPlltnMtgtYn)),
wtpltMtgtYn: nullableTrimmedString(normalizeOxYnValue(row.wtpltMtgtYn)),
rtlztnUsgYn: nullableTrimmedString(normalizeOxYnValue(row.rtlztnUsgYn)),
fcltStrctCn: nullableTrimmedString(row.fcltStrctCn),
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
impdFcltCpct: toGridNumber(row.impdFcltCpct),
grssPpqty: toGridNumber(row.grssPpqty),
wholPpqty: toGridNumber(row.wholPpqty),
pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln),
pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty),
pumpFcltN1Ppqty: toGridNumber(row.pumpFcltN1Ppqty),
pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt),
pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt),
pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln),
pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty),
pumpFcltN2Ppqty: toGridNumber(row.pumpFcltN2Ppqty),
pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt),
pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt),
pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln),
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
pumpFcltN3Ppqty: toGridNumber(row.pumpFcltN3Ppqty),
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
prcsgFcltYn: row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
ddrztnEqpmYn: row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
odorEqpmYn: row.odorEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
oslfEtgtYn: row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
mltvMthCn: nullableTrimmedString(row.mltvMthCn),
drgptnCn: nullableTrimmedString(row.drgptnCn),
impdMthCn: nullableTrimmedString(row.impdMthCn),
impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn),
impdPrcsCn: nullableTrimmedString(row.impdPrcsCn),
dschgWtrsExpln: nullableTrimmedString(row.dschgWtrsExpln),
prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt),
prpwLastDspsQnt: toGridNumber(row.prpwLastDspsQnt),
ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt),
prcptnLastDspsMthdExpln: nullableTrimmedString(row.prcptnLastDspsMthdExpln),
ctmnLastDspsMthdExpln: nullableTrimmedString(row.ctmnLastDspsMthdExpln),
prpwLastDspsExpln: nullableTrimmedString(row.prpwLastDspsExpln),
ctmnLastDspsExpln: nullableTrimmedString(row.ctmnLastDspsExpln),
oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
grssMltvQnt: toGridNumber(row.grssMltvQnt),
wholQnt: toGridNumber(row.wholQnt),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -386,32 +386,32 @@ function buildRainfallPumpStationSaveItem(
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
rsvrYn: row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : null,
rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt),
runwtrYn: row.runwtrYn ? (row.runwtrYn === "무" ? "N" : "Y") : null,
runwtrStrqty: toGridNumber(row.runwtrStrqty),
abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt),
grssPpqty: toGridNumber(row.grssPpqty),
wholPpqty: toGridNumber(row.wholPpqty),
pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln),
pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty),
pumpFcltN1UnitPpqty: toGridNumber(row.pumpFcltN1UnitPpqty),
pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt),
pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt),
pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln),
pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty),
pumpFcltN2UnitPpqty: toGridNumber(row.pumpFcltN2UnitPpqty),
pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt),
pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt),
pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln),
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
pumpFcltN3UnitPpqty: toGridNumber(row.pumpFcltN3UnitPpqty),
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
prcsgFcltYn: row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
ddrztnEqpmYn: row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
odorEqpmYn: row.odorEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
oslfEtgtYn: row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
dschgWtrsExpln: nullableTrimmedString(row.dschgWtrsExpln),
prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt),
prpwLastDspsQnt: toGridNumber(row.prpwLastDspsQnt),
ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt),
prcptnLastDspsMthdExpln: nullableTrimmedString(row.prcptnLastDspsMthdExpln),
ctmnLastDspsMthdExpln: nullableTrimmedString(row.ctmnLastDspsMthdExpln),
prpwLastDspsExpln: nullableTrimmedString(row.prpwLastDspsExpln),
ctmnLastDspsExpln: nullableTrimmedString(row.ctmnLastDspsExpln),
oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
grssMltvQnt: toGridNumber(row.grssMltvQnt),
wholQnt: toGridNumber(row.wholQnt),
rmrk: nullableTrimmedString(row.rmrk),
};
}
@@ -427,9 +427,9 @@ function mapRainfallPumpStationListToGridRows(
...item,
ctpvNm,
oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
rsvrYn: normalizeOxYnValue(item.rsvrYn),
runwtrYn: normalizeOxYnValue(item.runwtrYn),
prcsgFcltYn: normalizeOxYnValue(item.prcsgFcltYn),
ddrztnEqpmYn: normalizeOxYnValue(item.ddrztnEqpmYn),
odorEqpmYn: normalizeOxYnValue(item.odorEqpmYn),
oslfEtgtYn: normalizeOxYnValue(item.oslfEtgtYn),
},
])[0] as GridRowWithVerification;
@@ -531,7 +531,7 @@ const SewageReservoirPumpInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -720,13 +720,13 @@ const SewageReservoirPumpInp = () => {
const response = isReservoirTab
? await statisticsInputSewageReservoirPumpApi.saveSewageReservoir({
...requestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: saveList as SewageReservoirSaveItem[],
deleteList: deleteList as SewageReservoirSaveItem[],
})
: await statisticsInputSewageReservoirPumpApi.saveRainfallPumpStation({
...requestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: saveList as RainfallPumpStationSaveItem[],
deleteList: deleteList as RainfallPumpStationSaveItem[],
});
@@ -813,14 +813,14 @@ const SewageReservoirPumpInp = () => {
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
...(noApplicable ? { noApplicable: true } : {}),
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
};
try {
@@ -830,13 +830,13 @@ const SewageReservoirPumpInp = () => {
const response = isReservoirTab
? await statisticsInputSewageReservoirPumpApi.submitSewageReservoir({
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: submitList as SewageReservoirSaveItem[],
deleteList: deleteList as SewageReservoirSaveItem[],
})
: await statisticsInputSewageReservoirPumpApi.submitRainfallPumpStation({
...submitRequestBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: submitList as RainfallPumpStationSaveItem[],
deleteList: deleteList as RainfallPumpStationSaveItem[],
});
@@ -858,7 +858,7 @@ const SewageReservoirPumpInp = () => {
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
activeTabStatus?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -1525,7 +1525,7 @@ const SewageReservoirPumpInp = () => {
? "일반검증 - "
: "모집단 검증 -";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1584,7 +1584,7 @@ const SewageReservoirPumpInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -118,8 +118,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -147,7 +147,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
@@ -215,34 +215,33 @@ function buildSewerageFinanceSettleSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
cnterGovSprtAmt: formatNumber(d.cnterGovSprtAmt, "cnterGovSprtAmt"),
soaGovSprtAmt: formatNumber(d.soaGovSprtAmt, "soaGovSprtAmt"),
mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"),
wareaSggLclAmt: formatNumber(d.wareaSggLclAmt, "wareaSggLclAmt"),
sewerUseAmt: formatNumber(d.sewerUseAmt, "sewerUseAmt"),
orgprBrdnAmt: formatNumber(d.orgprBrdnAmt, "orgprBrdnAmt"),
fnnOrgprBrdnAmt: formatNumber(d.fnnOrgprBrdnAmt, "fnnOrgprBrdnAmt"),
etcIncmAmt: formatNumber(d.etcIncmAmt, "etcIncmAmt"),
crfwAmt: formatNumber(d.crfwAmt, "crfwAmt"),
lbltAmt: formatNumber(d.lbltAmt, "lbltAmt"),
soaCrfwAmt: formatNumber(d.soaCrfwAmt, "soaCrfwAmt"),
soaLbltAmt: formatNumber(d.soaLbltAmt, "soaLbltAmt"),
tmplntFcltAmt: formatNumber(d.tmplntFcltAmt, "tmplntFcltAmt"),
tmplntMendngAmt: formatNumber(d.tmplntMendngAmt, "tmplntMendngAmt"),
tmplntAmt: formatNumber(d.tmplntAmt, "tmplntAmt"),
tmplntOperKeepAmt: formatNumber(d.tmplntOperKeepAmt, "tmplntOperKeepAmt"),
tmplntEtcAmt: formatNumber(d.tmplntEtcAmt, "tmplntEtcAmt"),
swagePplFcltAmt: formatNumber(d.swagePplFcltAmt, "swagePplFcltAmt"),
swagePplMendngAmt: formatNumber(d.swagePplMendngAmt, "swagePplMendngAmt"),
swagePplAmt: formatNumber(d.swagePplAmt, "swagePplAmt"),
swagePplOperKeepAmt: formatNumber(d.swagePplOperKeepAmt, "swagePplOperKeepAmt"),
swagePplEtcAmt: formatNumber(d.swagePplEtcAmt, "swagePplEtcAmt"),
swageImpdFcltAmt: formatNumber(d.swageImpdFcltAmt, "swageImpdFcltAmt"),
swageImpdMendngAmt: formatNumber(d.swageImpdMendngAmt, "swageImpdMendngAmt"),
swageImpdAmt: formatNumber(d.swageImpdAmt, "swageImpdAmt"),
swageImpdOperKeepAmt: formatNumber(d.swageImpdOperKeepAmt, "swageImpdOperKeepAmt"),
swageImpdFcltEtcAmt: formatNumber(d.swageImpdFcltEtcAmt, "swageImpdFcltEtcAmt"),
etcAcdntCrfwAmt: formatNumber(d.etcAcdntCrfwAmt, "etcAcdntCrfwAmt"),
etcNrcpAmt: formatNumber(d.etcNrcpAmt, "etcNrcpAmt"),
etcAmt: formatNumber(d.etcAmt, "etcAmt"),
fnnEtcAmt: formatNumber(d.fnnEtcAmt, "fnnEtcAmt"),
rmrk: nullableTrimmedString(d.rmrk),
fnnByWholPjtcoAmt: formatNumber(d.fnnByWholPjtcoAmt, "fnnByWholPjtcoAmt"),
usgByWholPjtcoAmt: formatNumber(d.usgByWholPjtcoAmt, "usgByWholPjtcoAmt"),
fnnSeWholPjtcoAmt: formatNumber(d.fnnSeWholPjtcoAmt, "fnnSeWholPjtcoAmt"),
usgSeWholPjtcoAmt: formatNumber(d.usgSeWholPjtcoAmt, "usgSeWholPjtcoAmt"),
tmplntWholAmt: formatNumber(d.tmplntWholAmt, "tmplntWholAmt"),
wholSwagePplAmt: formatNumber(d.wholSwagePplAmt, "wholSwagePplAmt"),
wholSwageImpdFcltAmt: formatNumber(d.wholSwageImpdFcltAmt, "wholSwageImpdFcltAmt"),
@@ -257,15 +256,14 @@ function buildSewerageChargeSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"),
levyAmt: formatNumber(d.levyAmt, "levyAmt"),
grssPrmcoAmt: formatNumber(d.grssPrmcoAmt, "grssPrmcoAmt"),
sewerLevyAmt: formatNumber(d.sewerLevyAmt, "sewerLevyAmt"),
wholPrmcoAmt: formatNumber(d.wholPrmcoAmt, "wholPrmcoAmt"),
rmrk: nullableTrimmedString(d.rmrk),
avgUntprcAmt: formatNumber(d.avgUntprcAmt, "avgUntprcAmt"),
grssUnitPrmcoAmt: formatNumber(d.grssUnitPrmcoAmt, "grssUnitPrmcoAmt"),
rlztnRt: formatNumber(d.rlztnRt, "rlztnRt"),
wholUnitPrmcoAmt: formatNumber(d.wholUnitPrmcoAmt, "wholUnitPrmcoAmt"),
sewerCrgRlztnRt: formatNumber(d.sewerCrgRlztnRt, "sewerCrgRlztnRt"),
}));
}
@@ -276,32 +274,31 @@ function buildSewerageUsageFeeSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"),
hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"),
hshusUntprcAmt: formatNumber(d.hshusUntprcAmt, "hshusUntprcAmt"),
taskFrLevyAmt: formatNumber(d.taskFrLevyAmt, "taskFrLevyAmt"),
taskFrLevyQnt: formatNumber(d.taskFrLevyQnt, "taskFrLevyQnt"),
taskFrUntprcAmt: formatNumber(d.taskFrUntprcAmt, "taskFrUntprcAmt"),
ofuseLevyAmt: formatNumber(d.ofuseLevyAmt, "ofuseLevyAmt"),
ofuseLevyQnt: formatNumber(d.ofuseLevyQnt, "ofuseLevyQnt"),
ofuseUntprcAmt: formatNumber(d.ofuseUntprcAmt, "ofuseUntprcAmt"),
fbuLevyAmt: formatNumber(d.fbuLevyAmt, "fbuLevyAmt"),
fbuLevyQnt: formatNumber(d.fbuLevyQnt, "fbuLevyQnt"),
fbuUntprcAmt: formatNumber(d.fbuUntprcAmt, "fbuUntprcAmt"),
bthhsFrSpcs1LevyAmt: formatNumber(d.bthhsFrSpcs1LevyAmt, "bthhsFrSpcs1LevyAmt"),
bthhsFrSpcs1LevyQnt: formatNumber(d.bthhsFrSpcs1LevyQnt, "bthhsFrSpcs1LevyQnt"),
bthhsFrSpcs1UntprcAmt: formatNumber(d.bthhsFrSpcs1UntprcAmt, "bthhsFrSpcs1UntprcAmt"),
bthhsFrSpcs2LevyAmt: formatNumber(d.bthhsFrSpcs2LevyAmt, "bthhsFrSpcs2LevyAmt"),
bthhsFrSpcs2LevyQnt: formatNumber(d.bthhsFrSpcs2LevyQnt, "bthhsFrSpcs2LevyQnt"),
bthhsFrSpcs2UntprcAmt: formatNumber(d.bthhsFrSpcs2UntprcAmt, "bthhsFrSpcs2UntprcAmt"),
induLevyAmt: formatNumber(d.induLevyAmt, "induLevyAmt"),
induLevyQnt: formatNumber(d.induLevyQnt, "induLevyQnt"),
induUntprcAmt: formatNumber(d.induUntprcAmt, "induUntprcAmt"),
btht1LevyAmt: formatNumber(d.btht1LevyAmt, "btht1LevyAmt"),
btht1LevyQnt: formatNumber(d.btht1LevyQnt, "btht1LevyQnt"),
btht1UntprcAmt: formatNumber(d.btht1UntprcAmt, "btht1UntprcAmt"),
btht2LevyAmt: formatNumber(d.btht2LevyAmt, "btht2LevyAmt"),
btht2LevyQnt: formatNumber(d.btht2LevyQnt, "btht2LevyQnt"),
btht2UntprcAmt: formatNumber(d.btht2UntprcAmt, "btht2UntprcAmt"),
indstLevyAmt: formatNumber(d.indstLevyAmt, "indstLevyAmt"),
indstLevyQnt: formatNumber(d.indstLevyQnt, "indstLevyQnt"),
indstUntprcAmt: formatNumber(d.indstUntprcAmt, "indstUntprcAmt"),
etcLevyAmt: formatNumber(d.etcLevyAmt, "etcLevyAmt"),
etcLevyQnt: formatNumber(d.etcLevyQnt, "etcLevyQnt"),
etcLevyUntprcAmt: formatNumber(d.etcLevyUntprcAmt, "etcLevyUntprcAmt"),
rmrk: nullableTrimmedString(d.rmrk),
levyAmt: formatNumber(d.levyAmt, "levyAmt"),
levyQnt: formatNumber(d.levyQnt, "levyQnt"),
sewerLevyAmt: formatNumber(d.sewerLevyAmt, "sewerLevyAmt"),
sewerLevyQnt: formatNumber(d.sewerLevyQnt, "sewerLevyQnt"),
}));
}
@@ -312,16 +309,15 @@ function buildCauserBurdenSaveList(
return currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
levyEvntcsCnt: formatNumber(d.levyEvntcsCnt, "levyEvntcsCnt"),
levyAmt: formatNumber(d.levyAmt, "levyAmt"),
mltvEqpmEvntcsCnt: formatNumber(d.mltvEqpmEvntcsCnt, "mltvEqpmEvntcsCnt"),
mltvEqpmAmt: formatNumber(d.mltvEqpmAmt, "mltvEqpmAmt"),
brdnAmtLevyNocs: formatNumber(d.brdnAmtLevyNocs, "brdnAmtLevyNocs"),
brdnAmtLevyAmt: formatNumber(d.brdnAmtLevyAmt, "brdnAmtLevyAmt"),
drgNocs: formatNumber(d.drgNocs, "drgNocs"),
drgAmt: formatNumber(d.drgAmt, "drgAmt"),
othrCstrnOthrActrCnt: formatNumber(d.othrCstrnOthrActrCnt, "othrCstrnOthrActrCnt"),
othrCstrnOthrActrAmt: formatNumber(d.othrCstrnOthrActrAmt, "othrCstrnOthrActrAmt"),
rmrk: nullableTrimmedString(d.rmrk),
trgtByWholClctnAmt: formatNumber(d.trgtByWholClctnAmt, "trgtByWholClctnAmt"),
trgtSeWholClctnAmt: formatNumber(d.trgtSeWholClctnAmt, "trgtSeWholClctnAmt"),
}));
}
@@ -773,7 +769,7 @@ const SewerageFinanceInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -849,20 +845,19 @@ const SewerageFinanceInp = () => {
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
};
const verificationQuery = {
@@ -892,9 +887,8 @@ const SewerageFinanceInp = () => {
if (isSettleTab) {
const param: SewerageFinanceSettleSubmitRequest = {
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildSewerageFinanceSettleSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputSewerageFinanceApi.submitSewerageFinanceSettle(
@@ -903,26 +897,23 @@ const SewerageFinanceInp = () => {
} else if (isChargeTab) {
const param: SewerageChargeSubmitRequest = {
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildSewerageChargeSaveList(currentData),
...adminOpt,
};
response = await statisticsInputSewerageFinanceApi.submitSewerageCharge(param);
} else if (isUsageFeeTab) {
const param: SewerageUsageFeeSubmitRequest = {
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildSewerageUsageFeeSaveList(currentData),
...adminOpt,
};
response =
await statisticsInputSewerageFinanceApi.submitSewerageUsageFee(param);
} else {
const param: CauserBurdenSubmitRequest = {
...submitInfoBase,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: buildCauserBurdenSaveList(currentData),
...adminOpt,
};
response = await statisticsInputSewerageFinanceApi.submitCauserBurden(param);
}
@@ -943,7 +934,7 @@ const SewerageFinanceInp = () => {
sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
lae_sbmsn_stts_cd: activeTabStatus?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -1039,7 +1030,7 @@ const SewerageFinanceInp = () => {
if (isSettleTab) {
const param: SewerageFinanceSettleSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildSewerageFinanceSettleSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
@@ -1063,7 +1054,7 @@ const SewerageFinanceInp = () => {
if (isChargeTab) {
const param: SewerageChargeSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildSewerageChargeSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
@@ -1087,7 +1078,7 @@ const SewerageFinanceInp = () => {
if (isUsageFeeTab) {
const param: SewerageUsageFeeSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildSewerageUsageFeeSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
@@ -1110,7 +1101,7 @@ const SewerageFinanceInp = () => {
const param: CauserBurdenSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
saveList: buildCauserBurdenSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
@@ -1232,7 +1223,7 @@ const SewerageFinanceInp = () => {
: e.vrfcCd === "NV"
? "일반검증 - "
: "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1278,7 +1269,7 @@ const SewerageFinanceInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -325,8 +325,8 @@ const SeweragePenetrationRateInp = () => {
);
setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
if (statusInfo && statusInfo.laeSbmsnSttsCd) {
if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo && statusInfo.lclgvSbmsnSttsCd) {
if (['2', '3'].includes(statusInfo.lclgvSbmsnSttsCd)) {
setIsEnterable(false);
} else {
setIsEnterable(isEnterable);
@@ -395,14 +395,10 @@ const SeweragePenetrationRateInp = () => {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '',
statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
statsCd: statsCd,
};
if (isAdminInfo) {
params.superAdmin = 'superAdmin';
}
try {
setIsLoading(true);
@@ -498,33 +494,32 @@ const SeweragePenetrationRateInp = () => {
};
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
sggNm: nullableTrimmedString(d.sggNm),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
trbtrNm: nullableTrimmedString(d.trbtrNm),
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm),
spcCtpnRgnCn:
d.spcCtpnRgnCn === '해당없음' ? null : nullableTrimmedString(d.spcCtpnRgnCn),
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'),
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'),
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'),
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'),
staaArea: formatNumber(d.staaArea, 'staaArea'),
physSwtrmtPpltnCnt: formatNumber(d.physSwtrmtPpltnCnt, 'physSwtrmtPpltnCnt'),
crtureSwtrmtPpltnCnt: formatNumber(d.crtureSwtrmtPpltnCnt, 'crtureSwtrmtPpltnCnt'),
altdSwtrmtPpltnCnt: formatNumber(d.altdSwtrmtPpltnCnt, 'altdSwtrmtPpltnCnt'),
physWswtPpltnCnt: formatNumber(d.physWswtPpltnCnt, 'physWswtPpltnCnt'),
crtureWswtPpltnCnt: formatNumber(d.crtureWswtPpltnCnt, 'crtureWswtPpltnCnt'),
altdWswtPpltnCnt: formatNumber(d.altdWswtPpltnCnt, 'altdWswtPpltnCnt'),
urftPpltnCnt: formatNumber(d.urftPpltnCnt, 'urftPpltnCnt'),
swtrmtZoneIntnar: formatNumber(d.swtrmtZoneIntnar, 'swtrmtZoneIntnar'),
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'),
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'),
swgdplPpltnCnt: formatNumber(d.swgdplPpltnCnt, 'swgdplPpltnCnt'),
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'),
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'),
swtrmtZoneExtnar: formatNumber(d.swtrmtZoneExtnar, 'swtrmtZoneExtnar'),
rmrk: nullableTrimmedString(d.rmrk),
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'),
wholArea: formatNumber(d.wholArea, 'wholArea'),
whlar: formatNumber(d.whlar, 'whlar'),
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'),
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'),
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'),
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'),
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'),
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'),
wholWswtPpltnCnt: formatNumber(d.wholWswtPpltnCnt, 'wholWswtPpltnCnt'),
wholStaaOtsdPpltnCnt: formatNumber(d.wholStaaOtsdPpltnCnt, 'wholStaaOtsdPpltnCnt'),
swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, 'swagePplPsplyRt'),
awtrPsplyRt: formatNumber(d.awtrPsplyRt, 'awtrPsplyRt'),
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'),
}));
@@ -532,17 +527,17 @@ const SeweragePenetrationRateInp = () => {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '',
statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
submitList: convertedData,
sewerBzmnArea: footerFromApi?.sewerBzmnArea,
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
};
setGlobalProgressLabel('일반 검증 진행 중...');
@@ -557,7 +552,7 @@ const SeweragePenetrationRateInp = () => {
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
stats_cd: statsCd,
stats_yr: searchValues?.year ?? '',
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
lae_sbmsn_stts_cd: (statusInfo?.lclgvSbmsnSttsCd === '4' ? '4' : '2'),
gen_verify_stts_cd: response.data.nvpassed ? '1' : '2',
hb_pass_yn: response.data.isHbPassed,
};
@@ -677,41 +672,39 @@ const SeweragePenetrationRateInp = () => {
};
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
// 행정구역
sggNm: nullableTrimmedString(d.sggNm), // 시군구
admdstCd: nullableTrimmedString(d.admdstCd), // 행정코드
admdstNm: nullableTrimmedString(d.admdstNm), // 행정구역명
trbtrNm: nullableTrimmedString(d.trbtrNm), // 지류
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm), // 지류
spcCtpnRgnCn:
d.spcCtpnRgnCn === '해당없음'
? null
: nullableTrimmedString(d.spcCtpnRgnCn), // 특별대책지역
// 공공하수처리인구(명) - 물리적/생물학적/고도처리
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), // 고도처리
physSwtrmtPpltnCnt: formatNumber(d.physSwtrmtPpltnCnt, 'physSwtrmtPpltnCnt'), // 물리적처리
crtureSwtrmtPpltnCnt: formatNumber(d.crtureSwtrmtPpltnCnt, 'crtureSwtrmtPpltnCnt'), // 생물학적처리
altdSwtrmtPpltnCnt: formatNumber(d.altdSwtrmtPpltnCnt, 'altdSwtrmtPpltnCnt'), // 고도처리
// 폐수처리인구(명) - 물리적/생물학적/고도처리
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'), // 미접속인구
staaArea: formatNumber(d.staaArea, 'staaArea'), // 하수처리구역 내 면적(㎢)
physWswtPpltnCnt: formatNumber(d.physWswtPpltnCnt, 'physWswtPpltnCnt'),
crtureWswtPpltnCnt: formatNumber(d.crtureWswtPpltnCnt, 'crtureWswtPpltnCnt'),
altdWswtPpltnCnt: formatNumber(d.altdWswtPpltnCnt, 'altdWswtPpltnCnt'),
urftPpltnCnt: formatNumber(d.urftPpltnCnt, 'urftPpltnCnt'), // 미접속인구
swtrmtZoneIntnar: formatNumber(d.swtrmtZoneIntnar, 'swtrmtZoneIntnar'), // 하수처리구역 내 면적(㎢)
// 하수처리구역 외
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'), // 미처리인구
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구
swgdplPpltnCnt: formatNumber(d.swgdplPpltnCnt, 'swgdplPpltnCnt'), // 오수처리인구
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢)
swtrmtZoneExtnar: formatNumber(d.swtrmtZoneExtnar, 'swtrmtZoneExtnar'), // 하수처리구역 외 면적(㎢)
rmrk: nullableTrimmedString(d.rmrk), // 비고
// 합계·소계(계산필드 저장용)
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명)
wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢)
whlar: formatNumber(d.whlar, 'whlar'), // 총면적(㎢)
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), // 하수처리구역 내 합계
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'), // 공공하수처리인구 소계
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'), // 폐수처리인구 소계
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'), // 하수처리구역 외 합계
wholWswtPpltnCnt: formatNumber(d.wholWswtPpltnCnt, 'wholWswtPpltnCnt'), // 폐수처리인구 소계
wholStaaOtsdPpltnCnt: formatNumber(d.wholStaaOtsdPpltnCnt, 'wholStaaOtsdPpltnCnt'), // 하수처리구역 외 합계
// 보급률(%)
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'), // 공공하수처리구역 인구보급률
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'), // 고도처리 인구보급률
swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, 'swagePplPsplyRt'), // 공공하수처리구역 인구보급률
awtrPsplyRt: formatNumber(d.awtrPsplyRt, 'awtrPsplyRt'), // 고도처리 인구보급률
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'), // 하수도 설치율
}));
@@ -719,7 +712,7 @@ const SeweragePenetrationRateInp = () => {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '', // 시도
statsYr: searchValues?.year ?? '', // 통계 연도
userId: userInfo.userUnqId, // 사용자
userUuid: userInfo.userUnqId, // 사용자
saveList: convertedData, // 저장 데이터
sewerBzmnArea: footerFromApi?.sewerBzmnArea, // 행정참고자료면적
};
@@ -763,7 +756,7 @@ const SeweragePenetrationRateInp = () => {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '',
statsYr: searchValues?.year ?? '',
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCd,
};
@@ -794,7 +787,7 @@ const SeweragePenetrationRateInp = () => {
const submitOk = isSubmitOkForToolbar(sidoList, toolbarSearchContext);
const inputPeriodCurrent = isInputPeriodCurrent(periodCheckList);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2';
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === '2';
const baseShowSaveInToolbar =
(isEnterable || isAdminInfo) && inputPeriodCurrent && gugunOkOrNotDoList;
const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
@@ -834,7 +827,7 @@ const SeweragePenetrationRateInp = () => {
{submitOk && <SubmitButton submitFunc={() => submitFunc(false)} />}
</>
)}
{statusInfo?.laeSbmsnSttsCd === '2' && submitOk && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
{statusInfo?.lclgvSbmsnSttsCd === '2' && submitOk && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div>
</div>
@@ -878,7 +871,7 @@ const SeweragePenetrationRateInp = () => {
.map((e) => {
const prefix = e.vrfcCd === 'IMR' ? '시계열 검증 - ' : (e.vrfcCd === 'NV' ? '일반검증 - ' : '모집단 검증 -');
return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join('\n');
@@ -119,8 +119,8 @@ const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
if (statusInfo?.lclgvSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.lclgvSbmsnSttsCd)) {
return false;
}
return periodEnterable;
@@ -156,7 +156,7 @@ function computeTabToolbarSnapshot(
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const isSubmitted = statusInfo?.lclgvSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
@@ -223,18 +223,18 @@ function buildComplaintSaveList(
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
swtrmtPpltnCnt: formatNumber(d.swtrmtPpltnCnt, "swtrmtPpltnCnt"),
dschgWqCnt: formatNumber(d.dschgWqCnt, "dschgWqCnt"),
odorCnt: formatNumber(d.odorCnt, "odorCnt"),
ldsCnt: formatNumber(d.ldsCnt, "ldsCnt"),
plltnAcdntCnt: formatNumber(d.plltnAcdntCnt, "plltnAcdntCnt"),
swagePplCnt: formatNumber(d.swagePplCnt, "swagePplCnt"),
sewerCrgAmt: formatNumber(d.sewerCrgAmt, "sewerCrgAmt"),
etcCnt: formatNumber(d.etcCnt, "etcCnt"),
cvlcptDschgWq: formatNumber(d.cvlcptDschgWq, "cvlcptDschgWq"),
cvlcptOdorCnt: formatNumber(d.cvlcptOdorCnt, "cvlcptOdorCnt"),
cvlcptDrgCnt: formatNumber(d.cvlcptDrgCnt, "cvlcptDrgCnt"),
cvlcptMnhlCnt: formatNumber(d.cvlcptMnhlCnt, "cvlcptMnhlCnt"),
cvlcptSpctnkCnt: formatNumber(d.cvlcptSpctnkCnt, "cvlcptSpctnkCnt"),
cvlcptSewerCrg: formatNumber(d.cvlcptSewerCrg, "cvlcptSewerCrg"),
cvlcptEtcCnt: formatNumber(d.cvlcptEtcCnt, "cvlcptEtcCnt"),
rmrk: nullableTrimmedString(d.rmrk),
whol: formatNumber(d.whol, "whol"),
crtrPpltnCvlcptEvntcsCnt: formatNumber(
d.crtrPpltnCvlcptEvntcsCnt,
"crtrPpltnCvlcptEvntcsCnt",
cvlcptWholCnt: formatNumber(d.cvlcptWholCnt, "cvlcptWholCnt"),
crtrPpltnCvlcptCnt: formatNumber(
d.crtrPpltnCvlcptCnt,
"crtrPpltnCvlcptCnt",
),
}));
}
@@ -250,15 +250,15 @@ function buildResidentFriendlyFactSaveList(
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
fcltSn: nullableTrimmedString(d.fcltSn),
tmplntArea: formatNumber(d.tmplntArea, "tmplntArea"),
brkFcltArea: formatNumber(d.brkFcltArea, "brkFcltArea"),
stpar: formatNumber(d.stpar, "stpar"),
brkFcar: formatNumber(d.brkFcar, "brkFcar"),
childFrshpSpacArea: formatNumber(d.childFrshpSpacArea, "childFrshpSpacArea"),
bdySportsFcltArea: formatNumber(d.bdySportsFcltArea, "bdySportsFcltArea"),
sportsFcltArea: formatNumber(d.sportsFcltArea, "sportsFcltArea"),
pkltArea: formatNumber(d.pkltArea, "pkltArea"),
eduMultiFcltArea: formatNumber(d.eduMultiFcltArea, "eduMultiFcltArea"),
bdySportsFcar: formatNumber(d.bdySportsFcar, "bdySportsFcar"),
sportsFcar: formatNumber(d.sportsFcar, "sportsFcar"),
pkltar: formatNumber(d.pkltar, "pkltar"),
eduMultiFcar: formatNumber(d.eduMultiFcar, "eduMultiFcar"),
wtlndMakeArea: formatNumber(d.wtlndMakeArea, "wtlndMakeArea"),
etcCnt: formatNumber(d.etcCnt, "etcCnt"),
othrar: formatNumber(d.othrar, "othrar"),
rmrk: nullableTrimmedString(d.rmrk),
}));
}
@@ -772,14 +772,14 @@ const SewerageServiceLevelInp = () => {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
lclgvPicNm: submitInfo.lclgvPicNm,
lclgvPicTelnoCn: submitInfo.lclgvPicTelnoCn,
lclgvKbrdrDeptNm: submitInfo.lclgvKbrdrDeptNm,
lclgvKbrdrJbgdNm: submitInfo.lclgvKbrdrJbgdNm,
lclgvIdfrNm: submitInfo.lclgvIdfrNm,
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
};
const verificationQuery = {
@@ -807,14 +807,12 @@ const SewerageServiceLevelInp = () => {
const response = isComplaintTab
? await statisticsInputSewerageServiceLevelApi.submitComplaintList({
...submitInfoBase,
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
userUuid: userInfo.userUnqId,
submitList: complaintSubmitList,
})
: await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({
...submitInfoBase,
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
userUuid: userInfo.userUnqId,
noApplicable,
submitList: residentSubmitList,
});
@@ -835,7 +833,7 @@ const SewerageServiceLevelInp = () => {
sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
lae_sbmsn_stts_cd: activeTabStatus?.lclgvSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
};
@@ -941,7 +939,7 @@ const SewerageServiceLevelInp = () => {
const response =
await statisticsInputSewerageServiceLevelApi.saveComplaintList({
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
});
@@ -967,7 +965,7 @@ const SewerageServiceLevelInp = () => {
await statisticsInputSewerageServiceLevelApi.saveResidentFriendlyFactList(
{
...baseRequest,
userId: userInfo.userUnqId,
userUuid: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable,
saveList,
@@ -1021,7 +1019,7 @@ const SewerageServiceLevelInp = () => {
...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "",
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
mdfrUuid: userInfo.userUnqId,
statsCd: statsCdForTab,
};
@@ -1156,7 +1154,7 @@ const SewerageServiceLevelInp = () => {
: e.vrfcCd === "NV"
? "일반검증 - "
: "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
})
.filter(Boolean)
.join("\n");
@@ -1202,7 +1200,7 @@ const SewerageServiceLevelInp = () => {
{submitOk && <SubmitButton submitFunc={submitFunc} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
{activeTabStatus?.lclgvSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -161,7 +161,7 @@ const StatsInputStatusInp = () => {
}
const hasInvalidStatus = rowData.some((row) =>
["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
["0", "1", "4"].includes(String(row.lclgvSbmsnSttsCd ?? "")),
);
if (hasInvalidStatus) {
@@ -176,8 +176,8 @@ const StatsInputStatusInp = () => {
setIsApproveLoading(true);
try {
const userInfo = await getUserInfo();
const mdfrId = (userInfo.userUnqId ?? "").trim();
if (!mdfrId) {
const mdfrUuid = (userInfo.userUnqId ?? "").trim();
if (!mdfrUuid) {
alert("수정자 ID를 확인할 수 없습니다.");
return;
}
@@ -189,7 +189,7 @@ const StatsInputStatusInp = () => {
.map((row) => {
const statsCd = String(row.statsCd ?? "").trim();
const sewerBzmnCd = String(row.sewerBzmnCd ?? sewerBzmnCdDefault).trim();
return { statsYr, sewerBzmnCd, statsCd, mdfrId };
return { statsYr, sewerBzmnCd, statsCd, mdfrUuid };
})
.filter((item) => item.statsCd.length > 0);
@@ -260,7 +260,7 @@ const StatsInputStatusInp = () => {
const allRowsLaeSbmsnApproved = useMemo(
() =>
rowData.length > 0 &&
rowData.every((row) => String(row.laeSbmsnSttsCd ?? "") === "3"),
rowData.every((row) => String(row.lclgvSbmsnSttsCd ?? "") === "3"),
[rowData],
);
@@ -301,11 +301,11 @@ const StatsInputStatusInp = () => {
getGridRef={gridRef}
onDataCellClick={handleExceptionSubmitHistoryCellClick}
cellStyleCallback={({ row, fieldName }) => {
if (fieldName !== "laeSbmsnSttsNm") {
if (fieldName !== "lclgvSbmsnSttsNm") {
return undefined;
}
const statusCd = String(row.laeSbmsnSttsCd ?? "");
const statusCd = String(row.lclgvSbmsnSttsCd ?? "");
if (statusCd === "0" || statusCd === "1") {
return "rg-cell-stats-status-pending";
}

Some files were not shown because too many files have changed in this diff Show More