Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74934d4559 | ||
|
|
027f88da6c | ||
|
|
6dc2965209 | ||
|
|
c69c1aa855 | ||
|
|
9f0e03c151 | ||
|
|
471a431af5 | ||
|
|
26d8410649 | ||
|
|
8403ad556c | ||
|
|
d26c15f857 | ||
|
|
86ed664e3e | ||
|
|
a105f395dd | ||
|
|
a5ebc12580 | ||
|
|
270f6e366f | ||
|
|
acf215c1bf | ||
|
|
8a98cd7d19 | ||
|
|
3a5b48b608 | ||
|
|
d735ad7dd7 | ||
|
|
25dd0f1504 | ||
|
|
8e14c6cef5 | ||
|
|
9f7abe4413 | ||
|
|
de730205af | ||
|
|
a873f0e0dc | ||
|
|
32e3765b52 | ||
|
|
4f62095e8e | ||
|
|
5234448ae5 | ||
|
|
f284c67988 | ||
|
|
2d77813dab | ||
|
|
d22a255143 | ||
|
|
6703bbeeec | ||
|
|
d444c591ec | ||
|
|
f7ddb6cae0 | ||
|
|
7ffc02e887 | ||
|
|
cbacb27bb1 | ||
|
|
03a08a320a | ||
|
|
45223be64a | ||
|
|
b22dfdfa4c | ||
|
|
ce219f0e40 | ||
|
|
a0f26d38b2 | ||
|
|
4efdcfe0fc | ||
|
|
806ccf950f | ||
|
|
b96b78fa0b | ||
|
|
f816bf005e | ||
|
|
791ad6ce37 | ||
|
|
4bc802cee2 | ||
|
|
4662c8c896 | ||
|
|
7058560a03 | ||
|
|
79a5999ad5 | ||
|
|
1aeded5da2 | ||
|
|
e8d1d882c2 | ||
|
|
39dca810a5 | ||
|
|
97f94688f1 | ||
|
|
0b502cbc99 | ||
|
|
a8eb8f9ab1 | ||
|
|
6471f4365a | ||
|
|
48999f5699 | ||
|
|
5ad64a596b | ||
|
|
df60bd0b40 | ||
|
|
41e6f1cf48 | ||
|
|
0802ba191a | ||
|
|
ad57ed241d | ||
|
|
f17cb420e3 | ||
|
|
5e43d69d8e | ||
|
|
9971e512c8 | ||
|
|
0c77ed9bba | ||
|
|
e759c96ade | ||
|
|
f602b75268 | ||
|
|
11d0378060 | ||
|
|
8ed4f959cb | ||
|
|
e5f7778861 | ||
|
|
2d086a05b2 | ||
|
|
6ef4603a7c |
@@ -4,7 +4,7 @@ import { Providers } from "@/app/providers";
|
|||||||
import "@/app/styles/globals.css";
|
import "@/app/styles/globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "행정지원시스템",
|
title: "유역하수도통합플랫폼 Hasuon",
|
||||||
description: "행정지원시스템",
|
description: "행정지원시스템",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "@/app/styles/globals.css";
|
import "@/app/styles/globals.css";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "디지털 행정지원 시스템",
|
title: "유역하수도통합플랫폼 Hasuon",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Providers } from "@/app/providers";
|
|||||||
import "@/app/styles/globals.css";
|
import "@/app/styles/globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "디지털 행정지원 시스템",
|
title: "유역하수도통합플랫폼 Hasuon",
|
||||||
description: "디지털 행정지원 시스템",
|
description: "디지털 행정지원 시스템",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Image from "next/image";
|
|||||||
import "@/app/styles/globals.css";
|
import "@/app/styles/globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "디지털 행정지원 시스템",
|
title: "유역하수도통합플랫폼 Hasuon",
|
||||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,10 @@
|
|||||||
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ [영역 B] 필수첨부파일 목록 │
|
│ [영역 B] 필수첨부파일 목록 │
|
||||||
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 비고 | 등록일시 | 수정일시 | 관리 │
|
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 필수파일검토내용 | 비고 | 관리 │ │
|
||||||
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │
|
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │
|
||||||
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 서명본 첨부 | 2026-04-22 10:10:00 | — | [수정][삭제] │ │
|
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 사업비 내역 검토 | 서명본 첨부 | [수정][삭제] │ │
|
||||||
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | — | 2026-04-22 11:00:00 | — | [수정][삭제] │ │
|
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | 변경 전후 비교표 | — | [수정][삭제] │ │
|
||||||
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
@@ -47,7 +47,9 @@
|
|||||||
│ 필수첨부파일아이디(자동발급) [EAF-0004] │
|
│ 필수첨부파일아이디(자동발급) [EAF-0004] │
|
||||||
│ 문서구분 [설치인가 ▼] │
|
│ 문서구분 [설치인가 ▼] │
|
||||||
│ 필수첨부파일명 [예: 사업계획서] │
|
│ 필수첨부파일명 [예: 사업계획서] │
|
||||||
|
│ 양식파일 [파일 선택] │
|
||||||
│ 필수첨부여부 [ ] 예 [ ] 아니오 │
|
│ 필수첨부여부 [ ] 예 [ ] 아니오 │
|
||||||
|
│ 필수파일검토내용 [검토내용을 입력하세요]│
|
||||||
│ 비고 [비고를 입력하세요] │
|
│ 비고 [비고를 입력하세요] │
|
||||||
├──────────────────────────────────────────────────────┤
|
├──────────────────────────────────────────────────────┤
|
||||||
│ [취소] [저장] │
|
│ [취소] [저장] │
|
||||||
@@ -139,16 +141,16 @@ END
|
|||||||
|
|
||||||
### 5-2. 그리드 컬럼
|
### 5-2. 그리드 컬럼
|
||||||
|
|
||||||
| 컬럼명 | Field | 타입 | 비고 |
|
| 컬럼명 | Field | 타입 | 비고 |
|
||||||
| ------------------ | ------------------- | ------ | ------------------------------------- |
|
| ------------------ | -------------------- | ------ | ------------------------------------- |
|
||||||
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
|
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
|
||||||
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
|
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
|
||||||
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
|
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
|
||||||
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
|
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
|
||||||
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
|
| 필수파일검토내용 | `esntlAtchFileRvwCn` | string | 없으면 `—` 표시, 최대 2줄 clamp |
|
||||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
|
||||||
| 수정일시 | `mdfcnDt` | string | 미수정 시 빈 값 또는 `—` |
|
| 양식 내려받기 | `atchFileGroupId` | string | 양식파일 다운로드 버튼 |
|
||||||
| 관리 | — | — | 행별 **[수정] · [삭제]** |
|
| 관리 | — | — | 행별 **[수정] · [삭제]** |
|
||||||
|
|
||||||
- 행 클릭 시 별도 상세 화면 없음. 편집은 **[수정]** 버튼으로 다이얼로그 C를 연다.
|
- 행 클릭 시 별도 상세 화면 없음. 편집은 **[수정]** 버튼으로 다이얼로그 C를 연다.
|
||||||
|
|
||||||
@@ -167,13 +169,15 @@ END
|
|||||||
|
|
||||||
### 6-1. 폼 필드
|
### 6-1. 폼 필드
|
||||||
|
|
||||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||||
| ------------------ | ---------- | ---- | ------ | ------------------- | -------------------------------- |
|
| ------------------ | ------------------- | ---- | ------ | -------------------- | ---------------------------------- |
|
||||||
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
|
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
|
||||||
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
|
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
|
||||||
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
|
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
|
||||||
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
|
| 양식파일 | AttachmentFileField | N | — | `atchFileGroupId` | TUS 파일 업로드 |
|
||||||
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
|
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
|
||||||
|
| 필수파일검토내용 | Textarea | N | — | `esntlAtchFileRvwCn` | placeholder: "검토내용을 입력하세요"|
|
||||||
|
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
|
||||||
|
|
||||||
### 6-2. 버튼
|
### 6-2. 버튼
|
||||||
|
|
||||||
@@ -210,28 +214,32 @@ END
|
|||||||
|
|
||||||
**응답 DTO**
|
**응답 DTO**
|
||||||
|
|
||||||
| Field | 타입 | 설명 |
|
| Field | 타입 | 설명 |
|
||||||
| ------------------- | ------- | -------------------------------- |
|
| -------------------- | -------------------------- | -------------------------------- |
|
||||||
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
|
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
|
||||||
| `docSeComCd` | string | 문서구분공통코드 |
|
| `docSeComCd` | string | 문서구분공통코드 |
|
||||||
| `esntlAtchFileNm` | string | 필수첨부파일명 |
|
| `esntlAtchFileNm` | string | 필수첨부파일명 |
|
||||||
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
|
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
|
||||||
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
|
| `esntlAtchFileRvwCn` | string? | 필수첨부파일검토내용 |
|
||||||
| `rgtr` | string? | 등록자 UUID |
|
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
|
||||||
| `rgtrNm` | string? | 등록자명 |
|
| `atchFileGroupId` | string? | 첨부파일그룹아이디 (양식파일) |
|
||||||
| `mdfr` | string? | 수정자 UUID |
|
| `atchFileList` | AtflMngClientResponseDto[]?| 첨부파일 목록 |
|
||||||
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
|
| `rgtr` | string? | 등록자 UUID |
|
||||||
| `mdfcnDt` | string? | 수정일시 |
|
| `rgtrNm` | string? | 등록자명 |
|
||||||
|
| `mdfr` | string? | 수정자 UUID |
|
||||||
|
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
|
||||||
|
| `mdfcnDt` | string? | 수정일시 |
|
||||||
|
|
||||||
**저장 Request**
|
**저장 Request**
|
||||||
|
|
||||||
| Field | 필수 | 설명 |
|
| Field | 필수 | 설명 |
|
||||||
| ------------------- | ---- | ------------------ |
|
| -------------------- | ---- | -------------------- |
|
||||||
| `esntlAtchFileId` | Y | 필수첨부파일아이디 |
|
| `docSeComCd` | Y | 문서구분 |
|
||||||
| `docSeComCd` | N | 문서구분 |
|
| `esntlAtchFileNm` | N | 필수첨부파일명 |
|
||||||
| `esntlAtchFileNm` | N | 필수첨부파일명 |
|
| `esntlAtchYn` | N | 필수첨부여부 |
|
||||||
| `esntlAtchYn` | N | 필수첨부여부 |
|
| `esntlAtchFileRvwCn` | N | 필수첨부파일검토내용 |
|
||||||
| `esntlAtchFileRmrk` | N | 비고 |
|
| `esntlAtchFileRmrk` | N | 비고 |
|
||||||
|
| `atchFileGroupId` | N | 첨부파일그룹아이디 |
|
||||||
|
|
||||||
**검색 Query**
|
**검색 Query**
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { NextConfig } from "next";
|
|||||||
const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||||
|
|
||||||
const webhwpCtrlBase = (
|
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(/\/$/, "");
|
).replace(/\/$/, "");
|
||||||
|
|
||||||
const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const;
|
const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const;
|
||||||
|
|||||||
@@ -5,18 +5,6 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
<title>웹한글 기안기 브리지</title>
|
<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>
|
<style>
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
@@ -39,27 +27,56 @@
|
|||||||
<div id="hwpctrl"></div>
|
<div id="hwpctrl"></div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function () {
|
(function () {
|
||||||
|
// baseurl: 부모(HangulGiangi)가 NEXT_PUBLIC_WEBHYP_CTRL_URL 값으로 전달.
|
||||||
|
// 없으면 same-origin 프록시 경로(/webhwpctrl/ → next.config rewrites)를 사용한다.
|
||||||
var params = new URLSearchParams(window.location.search);
|
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) !== "/") {
|
if (baseUrl.charAt(baseUrl.length - 1) !== "/") {
|
||||||
baseUrl += "/";
|
baseUrl += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
var HwpCtrl = BuildWebHwpCtrl(
|
var scripts = [
|
||||||
"hwpctrl",
|
"js/hwpctrlapp/utils/util.js",
|
||||||
baseUrl,
|
"js/hwpctrlapp/hwpCtrlApp.js",
|
||||||
function () {
|
"js/webhwpctrl.js",
|
||||||
try {
|
];
|
||||||
window.parent.postMessage({ type: "hangulgiangi-ready" }, "*");
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 부모(HangulGiangi)가 contentWindow.HwpCtrl 로 접근
|
function loadScript(index) {
|
||||||
window.HwpCtrl = HwpCtrl;
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) {
|
|||||||
authService={authService}
|
authService={authService}
|
||||||
currentPath={pathname ?? ""}
|
currentPath={pathname ?? ""}
|
||||||
handleMenuClick={handleMenuClick}
|
handleMenuClick={handleMenuClick}
|
||||||
|
sidebarTriggerPlacement="lnb"
|
||||||
>
|
>
|
||||||
<PageLayout>{children}</PageLayout>
|
<PageLayout>{children}</PageLayout>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|||||||
@@ -16,9 +16,20 @@ interface CodeSelectProps extends React.ComponentPropsWithoutRef<typeof Select>
|
|||||||
useYn?: string;
|
useYn?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 레이블(label htmlFor) 연결용 트리거 id */
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }: CodeSelectProps) {
|
function CodeSelect({
|
||||||
|
upCd,
|
||||||
|
useYn,
|
||||||
|
placeholder = "선택",
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: CodeSelectProps) {
|
||||||
const listQuery = useCodeList(useYn ? undefined : upCd);
|
const listQuery = useCodeList(useYn ? undefined : upCd);
|
||||||
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
|
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
|
||||||
|
|
||||||
@@ -27,7 +38,7 @@ function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select {...props} disabled={props.disabled || isLoading}>
|
<Select {...props} disabled={props.disabled || isLoading}>
|
||||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
<SelectTrigger id={id} title={title} className={cn("w-[12rem]", className)}>
|
||||||
<SelectValue placeholder={placeholder} />
|
<SelectValue placeholder={placeholder} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -13,12 +13,16 @@ import type {
|
|||||||
NowDmgMtncGnsvySearchDto,
|
NowDmgMtncGnsvySearchDto,
|
||||||
} from "../model/dc-interface";
|
} from "../model/dc-interface";
|
||||||
|
|
||||||
|
/** 빈 문자열 파라미터를 보내면 서버 오류가 나므로 값이 있는 항목만 전송한다. */
|
||||||
async function getNowDmgMtncGnsvy(
|
async function getNowDmgMtncGnsvy(
|
||||||
params: NowDmgMtncGnsvySearchDto,
|
params: NowDmgMtncGnsvySearchDto,
|
||||||
): Promise<NowDmgMtncGnsvyResponseDto[]> {
|
): Promise<NowDmgMtncGnsvyResponseDto[]> {
|
||||||
|
const filledParams = Object.fromEntries(
|
||||||
|
Object.entries(params).filter(([, value]) => value !== undefined && value !== ""),
|
||||||
|
);
|
||||||
const response = await client.get<NowDmgMtncGnsvyResponseDto[]>(
|
const response = await client.get<NowDmgMtncGnsvyResponseDto[]>(
|
||||||
`${ENDPOINT.DC_SERVICE}/nowDmgMtncGnsvy`,
|
`${ENDPOINT.DC_SERVICE}/nowDmgMtncGnsvy`,
|
||||||
{ params },
|
{ params: filledParams },
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function AprvrPickerDialog({ open, onOpenChange, atrzSeComCd, onSelect }: AprvrP
|
|||||||
배치된 검토인력이 없습니다.
|
배치된 검토인력이 없습니다.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Table caption="결재자 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
<Table caption="검토인력 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="25%" />
|
<col width="25%" />
|
||||||
<col width="25%" />
|
<col width="25%" />
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ function ElaprAplnPickerGrid({
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
<Table caption="결재선 선택 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
<Table caption="결재선 관리 목록으로 결재순서, 결재구분, 결재자, 관리 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="10%" />
|
<col width="10%" />
|
||||||
<col width="10%" />
|
<col width="10%" />
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ function ElaprApprovalDialog({
|
|||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<Table caption="결재 진행 현황 목록으로 결재순서, 결재구분, 결재자, 전결자 지정, 결재상태, 관리 정보 제공" className="w-full">
|
<Table caption="결재 진행 단계 목록으로 결재순서, 결재구분, 결재자, 전결자 지정, 결재상태, 관리 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="8%" />
|
<col width="8%" />
|
||||||
<col width="8%" />
|
<col width="8%" />
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function ElaprTmpltSelectTable({
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "결재서식 선택 목록",
|
caption: "전자결재서식 목록으로 결재서식명 정보 제공",
|
||||||
onRowClick: (row) => {
|
onRowClick: (row) => {
|
||||||
setRowSelection({ [row.elaprTmpltId]: true });
|
setRowSelection({ [row.elaprTmpltId]: true });
|
||||||
onSelectTmplt(row);
|
onSelectTmplt(row);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export interface EsntlAtchFileMng {
|
|||||||
esntlAtchYn: "Y" | "N";
|
esntlAtchYn: "Y" | "N";
|
||||||
/** 필수첨부파일비고 */
|
/** 필수첨부파일비고 */
|
||||||
esntlAtchFileRmrk?: string;
|
esntlAtchFileRmrk?: string;
|
||||||
|
/** 필수첨부파일검토내용 */
|
||||||
|
esntlAtchFileRvwCn?: string;
|
||||||
/** 첨부파일그룹아이디 (양식파일 그룹) */
|
/** 첨부파일그룹아이디 (양식파일 그룹) */
|
||||||
atchFileGroupId?: string;
|
atchFileGroupId?: string;
|
||||||
/** 첨부파일 목록 (양식파일) */
|
/** 첨부파일 목록 (양식파일) */
|
||||||
@@ -77,6 +79,8 @@ export interface EsntlAtchFileMngSaveRequest {
|
|||||||
esntlAtchYn?: "Y" | "N";
|
esntlAtchYn?: "Y" | "N";
|
||||||
/** 필수첨부파일비고 */
|
/** 필수첨부파일비고 */
|
||||||
esntlAtchFileRmrk?: string;
|
esntlAtchFileRmrk?: string;
|
||||||
|
/** 필수첨부파일검토내용 */
|
||||||
|
esntlAtchFileRvwCn?: string;
|
||||||
/** 첨부파일그룹아이디 (양식파일 그룹) */
|
/** 첨부파일그룹아이디 (양식파일 그룹) */
|
||||||
atchFileGroupId?: 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,
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { fnrsCnsltnRvwOpnnService } from "./api/fnrs-cnsltn-rvw-opnn-service";
|
||||||
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
||||||
export { fnrsCnsltnKeys } from "./api/queries";
|
export { fnrsCnsltnKeys } from "./api/queries";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -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,3 +1,4 @@
|
|||||||
|
export { instlSncnRvwOpnnService } from "./api/instl-sncn-rvw-opnn-service";
|
||||||
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
|
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
|
||||||
export {
|
export {
|
||||||
instlSncnKeys,
|
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,
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ export {
|
|||||||
useTechReviewSewerBscPlan,
|
useTechReviewSewerBscPlan,
|
||||||
useUpdateSewerBscPlan,
|
useUpdateSewerBscPlan,
|
||||||
} from "./api/queries";
|
} from "./api/queries";
|
||||||
|
export { sewerBscPlanRvwOpnnService } from "./api/sewer-bsc-plan-rvw-opnn-service";
|
||||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||||
export type {
|
export type {
|
||||||
CklstResultItem,
|
CklstResultItem,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ function AprvrPickerDialog({
|
|||||||
배치된 검토인력이 없습니다.
|
배치된 검토인력이 없습니다.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Table caption="검토인력 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
<Table caption="검토인력 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="25%" />
|
<col width="25%" />
|
||||||
<col width="25%" />
|
<col width="25%" />
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
<Table caption="결재선 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
<Table caption="결재선 관리 목록으로 결재순서, 결재구분, 결재자 ID, 결재자, 등록일시, 관리 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="5%" />
|
<col width="5%" />
|
||||||
<col width="10%" />
|
<col width="10%" />
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "결재서식 목록으로 결재서식명 정보 제공",
|
caption: "전자결재선 관리 목록으로 결재서식명, 문서구분, 등록일시, 관리 정보 제공",
|
||||||
onRowClick: (row) => {
|
onRowClick: (row) => {
|
||||||
setRowSelection({ [row.elaprTmpltId]: true });
|
setRowSelection({ [row.elaprTmpltId]: true });
|
||||||
onSelectTmplt(row);
|
onSelectTmplt(row);
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ function CklstArtclFormDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>사용여부</Label>
|
<Label htmlFor="cklstArtclUseYn">사용여부</Label>
|
||||||
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
|
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger id="cklstArtclUseYn" className="w-full">
|
||||||
<SelectValue placeholder="사용여부" />
|
<SelectValue placeholder="사용여부" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function CklstArtclList({ cklstSn }: CklstArtclListProps) {
|
|||||||
Success: () => (
|
Success: () => (
|
||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
caption="체크리스트 항목 목록: 항목명, 검토내용, 검토결과 정보 제공"
|
caption="점검항목 관리 목록으로 점검항목코드, 점검항목명, 사용여부, 등록일시, 관리 정보 제공"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,8 +100,9 @@ function CklstMngFormDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>문서구분</Label>
|
<Label htmlFor="cklstDocSeComCd">문서구분</Label>
|
||||||
<CodeSelect
|
<CodeSelect
|
||||||
|
id="cklstDocSeComCd"
|
||||||
upCd="ASDOCSE"
|
upCd="ASDOCSE"
|
||||||
value={docSeComCd}
|
value={docSeComCd}
|
||||||
onValueChange={(v) => setValue("docSeComCd", v)}
|
onValueChange={(v) => setValue("docSeComCd", v)}
|
||||||
@@ -110,8 +111,9 @@ function CklstMngFormDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>업무구분</Label>
|
<Label htmlFor="cklstTaskSeComCd">업무구분</Label>
|
||||||
<CodeSelect
|
<CodeSelect
|
||||||
|
id="cklstTaskSeComCd"
|
||||||
upCd={docSeComCd || "ASDOCSE"}
|
upCd={docSeComCd || "ASDOCSE"}
|
||||||
value={watch("taskSeComCd")}
|
value={watch("taskSeComCd")}
|
||||||
onValueChange={(v) => setValue("taskSeComCd", v)}
|
onValueChange={(v) => setValue("taskSeComCd", v)}
|
||||||
@@ -121,9 +123,9 @@ function CklstMngFormDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>사용여부</Label>
|
<Label htmlFor="cklstUseYn">사용여부</Label>
|
||||||
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
|
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger id="cklstUseYn" className="w-full">
|
||||||
<SelectValue placeholder="사용여부" />
|
<SelectValue placeholder="사용여부" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function CklstMngList({ selectedCklstSn, onSelectCklst }: CklstMngListProps) {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "체크리스트 목록",
|
caption: "점검표 관리 목록으로 점검표일련번호, 점검표명, 문서구분, 업무구분, 사용여부, 등록일시, 관리 정보 제공",
|
||||||
onRowClick: (row) => {
|
onRowClick: (row) => {
|
||||||
setRowSelection({ [String(row.cklstSn)]: true });
|
setRowSelection({ [String(row.cklstSn)]: true });
|
||||||
onSelectCklst(row);
|
onSelectCklst(row);
|
||||||
|
|||||||
@@ -44,65 +44,78 @@ function CklstPreviewDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-[64rem]">
|
<DialogContent className="flex max-h-[90vh] max-w-[64rem] flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>체크리스트 미리보기 — {cklstNm}</DialogTitle>
|
<DialogTitle>체크리스트 미리보기 — {cklstNm}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{displayRows.length === 0 ? (
|
{displayRows.length === 0 ? (
|
||||||
<p className="py-6 text-center text-muted-foreground">표시할 항목이 없습니다.</p>
|
<p className="py-6 text-center text-muted-foreground">표시할 항목이 없습니다.</p>
|
||||||
) : (
|
) : (
|
||||||
<Table caption={`체크리스트 ${cklstNm} 미리보기`}>
|
// 항목이 많아 창 높이를 넘으면 표 영역만 스크롤 (초점을 받아 키보드로도 스크롤 가능)
|
||||||
<colgroup>
|
<div
|
||||||
<col width="25%" />
|
tabIndex={0}
|
||||||
<col width="50%" />
|
role="region"
|
||||||
<col width="25%" />
|
aria-label="체크리스트 미리보기 목록"
|
||||||
</colgroup>
|
className="min-h-0 flex-1 overflow-y-auto rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
||||||
<TableHeader>
|
>
|
||||||
<TableRow>
|
<Table
|
||||||
<TableHead align="left">점검항목</TableHead>
|
caption={`체크리스트 미리보기 ${cklstNm} 목록으로 점검항목, 주요 검토내용, 검토 여부 정보 제공`}
|
||||||
<TableHead align="left">주요 검토내용</TableHead>
|
>
|
||||||
<TableHead>검토 여부</TableHead>
|
<colgroup>
|
||||||
</TableRow>
|
<col width="25%" />
|
||||||
</TableHeader>
|
<col width="50%" />
|
||||||
<TableBody>
|
<col width="25%" />
|
||||||
{displayRows.map(({ row, depth }) => {
|
</colgroup>
|
||||||
const branchPrefix = formatTreeBranchPrefix(depth);
|
<TableHeader>
|
||||||
const showRadios = row.bchckUseYn !== "N";
|
<TableRow>
|
||||||
|
<TableHead align="left">점검항목</TableHead>
|
||||||
|
<TableHead align="left">주요 검토내용</TableHead>
|
||||||
|
<TableHead>검토 여부</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{displayRows.map(({ row, depth }) => {
|
||||||
|
const branchPrefix = formatTreeBranchPrefix(depth);
|
||||||
|
const showRadios = row.bchckUseYn !== "N";
|
||||||
|
|
||||||
if (row.useYn === "N") return null;
|
if (row.useYn === "N") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={row.rowKey}>
|
<TableRow key={row.rowKey}>
|
||||||
<TableCell align="left">
|
<TableCell align="left">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
{branchPrefix ? (
|
{branchPrefix ? (
|
||||||
<span className="font-mono text-muted">{branchPrefix}</span>
|
<span className="font-mono text-muted">{branchPrefix}</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span>{getArtclLabel(row.cklstArtclSn)}</span>
|
<span>{getArtclLabel(row.cklstArtclSn)}</span>
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.rvwCn ?? "—"}</TableCell>
|
<TableCell>{row.rvwCn ?? "—"}</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{showRadios ? (
|
{showRadios ? (
|
||||||
<RadioGroup className="flex items-center justify-center">
|
<RadioGroup
|
||||||
<div className="flex items-center gap-1">
|
aria-label="검토 여부"
|
||||||
<RadioGroupItem value="Y" id={`${row.rowKey}-preview-y`} />
|
className="flex items-center justify-center"
|
||||||
<Label htmlFor={`${row.rowKey}-preview-y`}>예</Label>
|
>
|
||||||
</div>
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<RadioGroupItem value="Y" id={`${row.rowKey}-preview-y`} />
|
||||||
<RadioGroupItem value="N" id={`${row.rowKey}-preview-n`} />
|
<Label htmlFor={`${row.rowKey}-preview-y`}>예</Label>
|
||||||
<Label htmlFor={`${row.rowKey}-preview-n`}>아니오</Label>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-1">
|
||||||
</RadioGroup>
|
<RadioGroupItem value="N" id={`${row.rowKey}-preview-n`} />
|
||||||
) : (
|
<Label htmlFor={`${row.rowKey}-preview-n`}>아니오</Label>
|
||||||
"—"
|
</div>
|
||||||
)}
|
</RadioGroup>
|
||||||
</TableCell>
|
) : (
|
||||||
</TableRow>
|
"—"
|
||||||
);
|
)}
|
||||||
})}
|
</TableCell>
|
||||||
</TableBody>
|
</TableRow>
|
||||||
</Table>
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
|||||||
등록된 항목이 없습니다. [추가]로 1단계 항목을 등록하세요.
|
등록된 항목이 없습니다. [추가]로 1단계 항목을 등록하세요.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Table caption="체크리스트 항목 목록" className="w-full">
|
<Table caption="체크리스트 항목 목록으로 점검항목, 주요 검토내용, 사용 여부, 체크 사용, 정렬 순서, 구분, 관리 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="16%" />
|
<col width="16%" />
|
||||||
<col width="36%" />
|
<col width="36%" />
|
||||||
@@ -289,13 +289,13 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>점검항목</TableHead>
|
<TableHead scope="col">점검항목</TableHead>
|
||||||
<TableHead>주요 검토내용</TableHead>
|
<TableHead scope="col">주요 검토내용</TableHead>
|
||||||
<TableHead>사용 여부</TableHead>
|
<TableHead scope="col">사용 여부</TableHead>
|
||||||
<TableHead>체크 사용</TableHead>
|
<TableHead scope="col">체크 사용</TableHead>
|
||||||
<TableHead>정렬 순서</TableHead>
|
<TableHead scope="col">정렬 순서</TableHead>
|
||||||
<TableHead>구분</TableHead>
|
<TableHead scope="col">구분</TableHead>
|
||||||
<TableHead>관리</TableHead>
|
<TableHead scope="col">관리</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ function CklstRvwRowComponent({
|
|||||||
onValueChange={(v) => onChange({ cklstArtclSn: Number(v), isDirty: true })}
|
onValueChange={(v) => onChange({ cklstArtclSn: Number(v), isDirty: true })}
|
||||||
disabled={!row.isNew}
|
disabled={!row.isNew}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full flex-1">
|
{/* 비활성(기존 항목) 상태에서도 텍스트 명도 대비 4.5:1 이상 유지 */}
|
||||||
|
<SelectTrigger
|
||||||
|
title="점검항목 선택"
|
||||||
|
className="w-full flex-1 disabled:opacity-100"
|
||||||
|
>
|
||||||
<SelectValue placeholder="점검항목 선택" />
|
<SelectValue placeholder="점검항목 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -73,12 +77,14 @@ function CklstRvwRowComponent({
|
|||||||
value={row.rvwCn ?? ""}
|
value={row.rvwCn ?? ""}
|
||||||
onChange={(e) => onChange({ rvwCn: e.target.value, isDirty: true })}
|
onChange={(e) => onChange({ rvwCn: e.target.value, isDirty: true })}
|
||||||
placeholder="주요 검토내용"
|
placeholder="주요 검토내용"
|
||||||
|
title="주요 검토내용"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={row.useYn ?? "Y"}
|
value={row.useYn ?? "Y"}
|
||||||
onValueChange={(v) => onChange({ useYn: v as "Y" | "N", isDirty: true })}
|
onValueChange={(v) => onChange({ useYn: v as "Y" | "N", isDirty: true })}
|
||||||
|
aria-label="사용 여부"
|
||||||
className="flex justify-center gap-4"
|
className="flex justify-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -95,6 +101,7 @@ function CklstRvwRowComponent({
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={row.bchckUseYn ?? "Y"}
|
value={row.bchckUseYn ?? "Y"}
|
||||||
onValueChange={(v) => onChange({ bchckUseYn: v as "Y" | "N", isDirty: true })}
|
onValueChange={(v) => onChange({ bchckUseYn: v as "Y" | "N", isDirty: true })}
|
||||||
|
aria-label="체크 사용"
|
||||||
className="flex justify-center gap-4"
|
className="flex justify-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -111,6 +118,7 @@ function CklstRvwRowComponent({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
title="정렬 순서"
|
||||||
value={row.sortSeq ?? ""}
|
value={row.sortSeq ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({
|
onChange({
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ function AcntgCodeTabPanel() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||||
<Table caption="회계구분 코드 목록" className="w-full">
|
<Table caption="회계코드 목록으로 코드, 코드명, 설명, 사용 여부, 정렬순서, 구분, 관리 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="14%" />
|
<col width="14%" />
|
||||||
<col width="16%" />
|
<col width="16%" />
|
||||||
@@ -115,13 +115,13 @@ function AcntgCodeTabPanel() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>코드</TableHead>
|
<TableHead scope="col">코드</TableHead>
|
||||||
<TableHead>코드명</TableHead>
|
<TableHead scope="col">코드명</TableHead>
|
||||||
<TableHead>설명</TableHead>
|
<TableHead scope="col">설명</TableHead>
|
||||||
<TableHead>사용 여부</TableHead>
|
<TableHead scope="col">사용 여부</TableHead>
|
||||||
<TableHead>정렬순서</TableHead>
|
<TableHead scope="col">정렬순서</TableHead>
|
||||||
<TableHead>구분</TableHead>
|
<TableHead scope="col">구분</TableHead>
|
||||||
<TableHead>관리</TableHead>
|
<TableHead scope="col">관리</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
|
|||||||
value={row.acntgSeCd}
|
value={row.acntgSeCd}
|
||||||
onChange={(e) => onChange({ acntgSeCd: e.target.value })}
|
onChange={(e) => onChange({ acntgSeCd: e.target.value })}
|
||||||
placeholder="코드"
|
placeholder="코드"
|
||||||
|
title="코드"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
disabled={!row.isNew}
|
disabled={!row.isNew}
|
||||||
className="flex-1"
|
className="flex-1 disabled:opacity-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -51,6 +52,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
|
|||||||
value={row.acntgSeCdNm ?? ""}
|
value={row.acntgSeCdNm ?? ""}
|
||||||
onChange={(e) => onChange({ acntgSeCdNm: e.target.value })}
|
onChange={(e) => onChange({ acntgSeCdNm: e.target.value })}
|
||||||
placeholder="코드명"
|
placeholder="코드명"
|
||||||
|
title="코드명"
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -59,6 +61,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
|
|||||||
value={row.expln ?? ""}
|
value={row.expln ?? ""}
|
||||||
onChange={(e) => onChange({ expln: e.target.value })}
|
onChange={(e) => onChange({ expln: e.target.value })}
|
||||||
placeholder="설명"
|
placeholder="설명"
|
||||||
|
title="설명"
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -66,6 +69,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={row.useYn === "N" ? "N" : "Y"}
|
value={row.useYn === "N" ? "N" : "Y"}
|
||||||
onValueChange={(v) => onChange({ useYn: v })}
|
onValueChange={(v) => onChange({ useYn: v })}
|
||||||
|
aria-label="사용 여부"
|
||||||
className="flex justify-center gap-4"
|
className="flex justify-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -82,6 +86,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
title="정렬순서"
|
||||||
value={row.sortSeq ?? ""}
|
value={row.sortSeq ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
|
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ function CnstpCodeTabPanel() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||||
<Table caption="공종구분 코드 목록" className="w-full">
|
<Table caption="공사코드 목록으로 코드, 코드명, 설명, 숫자 여부, 소숫점 단위, 사용 여부, 정렬순서, 구분, 관리 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="14%" />
|
<col width="14%" />
|
||||||
<col width="16%" />
|
<col width="16%" />
|
||||||
@@ -115,15 +115,15 @@ function CnstpCodeTabPanel() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>코드</TableHead>
|
<TableHead scope="col">코드</TableHead>
|
||||||
<TableHead>코드명</TableHead>
|
<TableHead scope="col">코드명</TableHead>
|
||||||
<TableHead>설명</TableHead>
|
<TableHead scope="col">설명</TableHead>
|
||||||
<TableHead>숫자 여부</TableHead>
|
<TableHead scope="col">숫자 여부</TableHead>
|
||||||
<TableHead>소숫점 단위</TableHead>
|
<TableHead scope="col">소숫점 단위</TableHead>
|
||||||
<TableHead>사용 여부</TableHead>
|
<TableHead scope="col">사용 여부</TableHead>
|
||||||
<TableHead>정렬순서</TableHead>
|
<TableHead scope="col">정렬순서</TableHead>
|
||||||
<TableHead>구분</TableHead>
|
<TableHead scope="col">구분</TableHead>
|
||||||
<TableHead>관리</TableHead>
|
<TableHead scope="col">관리</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
|||||||
value={row.cnstpSeCd}
|
value={row.cnstpSeCd}
|
||||||
onChange={(e) => onChange({ cnstpSeCd: e.target.value })}
|
onChange={(e) => onChange({ cnstpSeCd: e.target.value })}
|
||||||
placeholder="코드"
|
placeholder="코드"
|
||||||
|
title="코드"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
disabled={!row.isNew}
|
disabled={!row.isNew}
|
||||||
className="flex-1"
|
className="flex-1 disabled:opacity-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -51,6 +52,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
|||||||
value={row.cnstpSeCdNm ?? ""}
|
value={row.cnstpSeCdNm ?? ""}
|
||||||
onChange={(e) => onChange({ cnstpSeCdNm: e.target.value })}
|
onChange={(e) => onChange({ cnstpSeCdNm: e.target.value })}
|
||||||
placeholder="코드명"
|
placeholder="코드명"
|
||||||
|
title="코드명"
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -59,6 +61,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
|||||||
value={row.expln ?? ""}
|
value={row.expln ?? ""}
|
||||||
onChange={(e) => onChange({ expln: e.target.value })}
|
onChange={(e) => onChange({ expln: e.target.value })}
|
||||||
placeholder="설명"
|
placeholder="설명"
|
||||||
|
title="설명"
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -66,15 +69,16 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={row.numYn === "N" ? "N" : "Y"}
|
value={row.numYn === "N" ? "N" : "Y"}
|
||||||
onValueChange={(v) => onChange({ numYn: v })}
|
onValueChange={(v) => onChange({ numYn: v })}
|
||||||
|
aria-label="숫자 여부"
|
||||||
className="flex justify-center gap-4"
|
className="flex justify-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem value="Y" id={`${row.rowKey}-num-y`} />
|
<RadioGroupItem value="Y" id={`${row.rowKey}-num-y`} />
|
||||||
<Label htmlFor={`${row.rowKey}-use-y`}>예</Label>
|
<Label htmlFor={`${row.rowKey}-num-y`}>예</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem value="N" id={`${row.rowKey}-num-n`} />
|
<RadioGroupItem value="N" id={`${row.rowKey}-num-n`} />
|
||||||
<Label htmlFor={`${row.rowKey}-use-n`}>아니오</Label>
|
<Label htmlFor={`${row.rowKey}-num-n`}>아니오</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -82,6 +86,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
title="소숫점 단위"
|
||||||
value={row.dcptUnitCnt ?? ""}
|
value={row.dcptUnitCnt ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({ dcptUnitCnt: e.target.value ? Number(e.target.value) : undefined })
|
onChange({ dcptUnitCnt: e.target.value ? Number(e.target.value) : undefined })
|
||||||
@@ -93,6 +98,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={row.useYn === "N" ? "N" : "Y"}
|
value={row.useYn === "N" ? "N" : "Y"}
|
||||||
onValueChange={(v) => onChange({ useYn: v })}
|
onValueChange={(v) => onChange({ useYn: v })}
|
||||||
|
aria-label="사용 여부"
|
||||||
className="flex justify-center gap-4"
|
className="flex justify-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -109,6 +115,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
title="정렬순서"
|
||||||
value={row.sortSeq ?? ""}
|
value={row.sortSeq ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
|
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ function FnrsSharCodeTabPanel() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||||
<Table caption="재원분담 코드 목록" className="w-full">
|
<Table caption="재원분담구분코드 목록으로 코드, 코드명, 설명, 사용 여부, 정렬순서, 구분, 관리 정보 제공" className="w-full">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="14%" />
|
<col width="14%" />
|
||||||
<col width="16%" />
|
<col width="16%" />
|
||||||
@@ -115,13 +115,13 @@ function FnrsSharCodeTabPanel() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>코드</TableHead>
|
<TableHead scope="col">코드</TableHead>
|
||||||
<TableHead>코드명</TableHead>
|
<TableHead scope="col">코드명</TableHead>
|
||||||
<TableHead>설명</TableHead>
|
<TableHead scope="col">설명</TableHead>
|
||||||
<TableHead>사용 여부</TableHead>
|
<TableHead scope="col">사용 여부</TableHead>
|
||||||
<TableHead>정렬순서</TableHead>
|
<TableHead scope="col">정렬순서</TableHead>
|
||||||
<TableHead>구분</TableHead>
|
<TableHead scope="col">구분</TableHead>
|
||||||
<TableHead>관리</TableHead>
|
<TableHead scope="col">관리</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
|
|||||||
value={row.fnrsSharSeCd}
|
value={row.fnrsSharSeCd}
|
||||||
onChange={(e) => onChange({ fnrsSharSeCd: e.target.value })}
|
onChange={(e) => onChange({ fnrsSharSeCd: e.target.value })}
|
||||||
placeholder="코드"
|
placeholder="코드"
|
||||||
|
title="코드"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
disabled={!row.isNew}
|
disabled={!row.isNew}
|
||||||
className="flex-1"
|
className="flex-1 disabled:opacity-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -51,6 +52,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
|
|||||||
value={row.fnrsSharSeCdNm ?? ""}
|
value={row.fnrsSharSeCdNm ?? ""}
|
||||||
onChange={(e) => onChange({ fnrsSharSeCdNm: e.target.value })}
|
onChange={(e) => onChange({ fnrsSharSeCdNm: e.target.value })}
|
||||||
placeholder="코드명"
|
placeholder="코드명"
|
||||||
|
title="코드명"
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -59,6 +61,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
|
|||||||
value={row.expln ?? ""}
|
value={row.expln ?? ""}
|
||||||
onChange={(e) => onChange({ expln: e.target.value })}
|
onChange={(e) => onChange({ expln: e.target.value })}
|
||||||
placeholder="설명"
|
placeholder="설명"
|
||||||
|
title="설명"
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -66,6 +69,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={row.useYn === "N" ? "N" : "Y"}
|
value={row.useYn === "N" ? "N" : "Y"}
|
||||||
onValueChange={(v) => onChange({ useYn: v })}
|
onValueChange={(v) => onChange({ useYn: v })}
|
||||||
|
aria-label="사용 여부"
|
||||||
className="flex justify-center gap-4"
|
className="flex justify-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -82,6 +86,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
title="정렬순서"
|
||||||
value={row.sortSeq ?? ""}
|
value={row.sortSeq ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
|
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
|
||||||
|
|||||||
@@ -56,14 +56,12 @@
|
|||||||
fill: rgba(0, 150, 255, 0.2) !important;
|
fill: rgba(0, 150, 255, 0.2) !important;
|
||||||
stroke: #0066cc;
|
stroke: #0066cc;
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.svgContainer :global(svg) :global(.cls-1) {
|
.svgContainer :global(svg) :global(.cls-1) {
|
||||||
fill: rgba(0, 150, 255, 0.2) !important;
|
fill: rgba(0, 150, 255, 0.2) !important;
|
||||||
stroke: #0066cc;
|
stroke: #0066cc;
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.svgContainer :global(svg) :global(.basin) {
|
.svgContainer :global(svg) :global(.basin) {
|
||||||
@@ -93,7 +91,6 @@
|
|||||||
fill: rgba(0, 209, 209, 0.5);
|
fill: rgba(0, 209, 209, 0.5);
|
||||||
stroke: #399ac3;
|
stroke: #399ac3;
|
||||||
stroke-width: 1.3;
|
stroke-width: 1.3;
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
@@ -160,3 +157,33 @@
|
|||||||
border-color: #96c3ff;
|
border-color: #96c3ff;
|
||||||
box-shadow: 0 4px 12px rgba(0, 107, 248, 0.3);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -247,6 +247,11 @@ export function useNationalAsMap(basinDocPrgrsList: BasinDocPrgrsResponseDto[] |
|
|||||||
svg = svgLayerRef.current.querySelector("svg");
|
svg = svgLayerRef.current.querySelector("svg");
|
||||||
if (!svg) throw new Error("SVG element not found in loaded content");
|
if (!svg) throw new Error("SVG element not found in loaded content");
|
||||||
|
|
||||||
|
// 확대·축소해도 선 두께 유지. CSS 의 vector-effect 는 W3C CSS 검사 오류로 처리되므로 SVG 속성으로 지정
|
||||||
|
svg.querySelectorAll("path, polygon, polyline").forEach((shape) => {
|
||||||
|
shape.setAttribute("vector-effect", "non-scaling-stroke");
|
||||||
|
});
|
||||||
|
|
||||||
applyViewBox(svg, stageRef.current);
|
applyViewBox(svg, stageRef.current);
|
||||||
groups = collectBasinGroups(svg);
|
groups = collectBasinGroups(svg);
|
||||||
labels = labelsLayerRef.current ? createLabels(labelsLayerRef.current, groups) : [];
|
labels = labelsLayerRef.current ? createLabels(labelsLayerRef.current, groups) : [];
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ export function DashboardNotiList({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="card-header flex items-center justify-between gap-4">
|
||||||
<h3 className="text-2xl font-semibold">
|
<h3 className="text-2xl font-semibold">
|
||||||
알림이력
|
알림이력
|
||||||
{dataUpdatedAt > 0 ? (
|
{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")})
|
(업데이트 일시: {dayjs(dataUpdatedAt).format("YYYY-MM-DD HH:mm")})
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -75,7 +75,7 @@ export function DashboardNotiList({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isError ? (
|
{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>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -110,7 +110,7 @@ interface DashboardNotiRowProps {
|
|||||||
function DashboardNotiRow({ row, isActionPending, onAction }: DashboardNotiRowProps) {
|
function DashboardNotiRow({ row, isActionPending, onAction }: DashboardNotiRowProps) {
|
||||||
return (
|
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">
|
<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") : "-"}
|
{row.regDt ? dayjs(row.regDt).format("YYYY-MM-DD") : "-"}
|
||||||
</span>
|
</span>
|
||||||
<p className="min-w-0 line-clamp-2 text-xl sm:line-clamp-1">{row.notiTtl}</p>
|
<p className="min-w-0 line-clamp-2 text-xl sm:line-clamp-1">{row.notiTtl}</p>
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ export function DashboardNoticeTable() {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "공지사항 목록",
|
caption: "공지사항 목록으로 번호, 제목, 작성자, 작성일, 조회수 정보 제공",
|
||||||
variant: "line",
|
variant: "line",
|
||||||
onRowClick: handleRowClick,
|
onRowClick: handleRowClick,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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">
|
<div className="card-header flex items-center justify-between">
|
||||||
<h3 className="text-2xl font-semibold">공지사항</h3>
|
<h3 className="text-2xl font-semibold">공지사항</h3>
|
||||||
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" onClick={handleMoreClick}>
|
<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();
|
const { data, isPending, isError, dataUpdatedAt } = useGetMainDashboard();
|
||||||
|
|
||||||
return (
|
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">
|
<div className="hidden flex-[0.5] sm:block">
|
||||||
<NationalAsMap basinDocPrgrsList={data?.basinDocPrgrsList} />
|
<NationalAsMap basinDocPrgrsList={data?.basinDocPrgrsList} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -39,7 +39,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={slot.docSeComCd}
|
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">
|
<div className="card-header">
|
||||||
<h3 className="text-2xl font-semibold">{slot.title}</h3>
|
<h3 className="text-2xl font-semibold">{slot.title}</h3>
|
||||||
@@ -90,7 +90,7 @@ function DocPrgrsSttsMetric({
|
|||||||
>
|
>
|
||||||
<Icon className="size-6" />
|
<Icon className="size-6" />
|
||||||
</span>
|
</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>
|
<span className="text-3xl font-semibold tabular-nums">{count}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -108,21 +108,22 @@ const STAT_METRICS = [
|
|||||||
field: "reqCnt",
|
field: "reqCnt",
|
||||||
label: "신청",
|
label: "신청",
|
||||||
iconClassName: "bg-[#4285F4]",
|
iconClassName: "bg-[#4285F4]",
|
||||||
bgColor: "bg-blue-100",
|
// 블랙 스킨: 아이콘 색 반투명 배경
|
||||||
|
bgColor: "bg-blue-100 [[data-theme=black]_&]:bg-[rgba(66,133,244,0.18)]",
|
||||||
Icon: FilePlus,
|
Icon: FilePlus,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "prgrsCnt",
|
field: "prgrsCnt",
|
||||||
label: "진행",
|
label: "진행",
|
||||||
iconClassName: "bg-[#34A853]",
|
iconClassName: "bg-[#34A853]",
|
||||||
bgColor: "bg-emerald-100",
|
bgColor: "bg-emerald-100 [[data-theme=black]_&]:bg-[rgba(52,168,83,0.18)]",
|
||||||
Icon: RefreshCw,
|
Icon: RefreshCw,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "cmptnCnt",
|
field: "cmptnCnt",
|
||||||
label: "완료",
|
label: "완료",
|
||||||
iconClassName: "bg-[#EA4335]",
|
iconClassName: "bg-[#EA4335]",
|
||||||
bgColor: "bg-orange-100",
|
bgColor: "bg-orange-100 [[data-theme=black]_&]:bg-[rgba(234,67,53,0.18)]",
|
||||||
Icon: CircleCheck,
|
Icon: CircleCheck,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function NationalAsMap({ basinDocPrgrsList }: NationalAsMapProps) {
|
|||||||
const popupColumnSpan = Math.max(1, popupContent.items.length);
|
const popupColumnSpan = Math.max(1, popupContent.items.length);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="card-header">
|
||||||
<h3 className="text-2xl font-semibold">전국 행정지원 지도</h3>
|
<h3 className="text-2xl font-semibold">전국 행정지원 지도</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,6 +79,23 @@ function getBadgeColor(state: BadgeState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function formatOptional(value: string | null | undefined) {
|
||||||
return value?.trim() ? value : "-";
|
return value?.trim() ? value : "-";
|
||||||
}
|
}
|
||||||
@@ -91,11 +108,21 @@ function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
|
|||||||
const badges = buildProgressBadges(prgrsSttsComCd);
|
const badges = buildProgressBadges(prgrsSttsComCd);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
<div className="flex flex-wrap items-center justify-center">
|
||||||
{badges.map((badge, index) => (
|
{badges.map((badge, index) => (
|
||||||
<Fragment key={`${badge.label}-${index}`}>
|
<Fragment key={`${badge.label}-${index}`}>
|
||||||
{index > 0 && <span className="mx-2">></span>}
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
{/* 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>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ function DesignListPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
<div className="card card-border !mt-6">
|
||||||
<section className="card card-border mx-0 bg-background">
|
<div className="card card-border">
|
||||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
||||||
@@ -145,7 +145,7 @@ function DesignListPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Button type="submit">조회</Button>
|
<Button type="submit">조회</Button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<DesignList
|
<DesignList
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
|||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "설계검토 목록",
|
caption: "설계자문 목록으로 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일 정보 제공",
|
||||||
viewPagination: true,
|
viewPagination: true,
|
||||||
onRowClick: handleRowClick,
|
onRowClick: handleRowClick,
|
||||||
},
|
},
|
||||||
@@ -78,15 +78,15 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
|||||||
|
|
||||||
return (
|
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">
|
<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 type="button" color="green" onClick={() => setIsTaskSeDialogOpen(true)}>
|
||||||
신청
|
신청
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{tableComponent}
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||||
</section>
|
</div>
|
||||||
<PbadmsTaskApplyDialog
|
<PbadmsTaskApplyDialog
|
||||||
open={isTaskSeDialogOpen}
|
open={isTaskSeDialogOpen}
|
||||||
onOpenChange={setIsTaskSeDialogOpen}
|
onOpenChange={setIsTaskSeDialogOpen}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
|
|||||||
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
|
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
|
||||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
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 { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
|
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
|
||||||
@@ -16,9 +17,9 @@ function BasicInfoFormSection() {
|
|||||||
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
|
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
|
||||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||||
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
|
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
|
||||||
const localGovLabel = [watch("designCnstnMng.sido"), watch("designCnstnMng.gugun")]
|
const sido = watch("designCnstnMng.sido");
|
||||||
.filter(Boolean)
|
const gugun = watch("designCnstnMng.gugun");
|
||||||
.join(" ");
|
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
|
||||||
const { data: taskOptions } = useCodeList("CNFM");
|
const { data: taskOptions } = useCodeList("CNFM");
|
||||||
|
|
||||||
function handleBnsSelect(data: BnsApplyData) {
|
function handleBnsSelect(data: BnsApplyData) {
|
||||||
@@ -36,7 +37,8 @@ function BasicInfoFormSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table caption="사업 기본 정보 입력" variant="secondary">
|
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||||
|
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||||
|
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
interface BasicInfoSectionProps {
|
interface BasicInfoSectionProps {
|
||||||
@@ -11,7 +12,8 @@ interface BasicInfoSectionProps {
|
|||||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table caption="사업 기본 정보" variant="secondary">
|
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
|
||||||
|
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
|||||||
return (
|
return (
|
||||||
<SectionCollapsible title="사업규모">
|
<SectionCollapsible title="사업규모">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table caption="사업규모 목록: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
<Table caption="사업규모 목록으로 구분, 기본계획(A), 예산반영(B), 금회설계시(C), 증감(C-B) 정보 제공" variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[30rem]">구분</TableHead>
|
<TableHead className="w-[30rem]">구분</TableHead>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface ContactInfoSectionProps {
|
|||||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="담당자 정보">
|
<SectionCollapsible title="담당자 정보">
|
||||||
<Table caption="사업 담당자 정보" variant="secondary">
|
<Table caption="담당자 정보로 신청자(이름, 연락처, 소속, 이메일)과 설계자(이름, 연락처, 소속, 이메일) 정보 제공" variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{det.aplcntNm && (
|
{det.aplcntNm && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,18 +2,24 @@
|
|||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { TextShimmer } from "@/shared/ui";
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
import {
|
||||||
|
designCnstnKeys,
|
||||||
|
type DesignCnstnRvwOpnnResponseDto,
|
||||||
|
designCnstnRvwOpnnService,
|
||||||
|
} from "@/entities/design-cnstn";
|
||||||
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -172,18 +178,27 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
|
return editableReviewStep === reviewStep && canWriteOpinion;
|
||||||
|
}
|
||||||
|
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
|
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
<Table caption="검토의견 입력" variant="secondary">
|
<Table
|
||||||
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
<TableHead scope="row" className="w-[12rem]">
|
||||||
|
의견내용
|
||||||
|
</TableHead>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={formData.rvwRsltCn}
|
value={formData.rvwRsltCn}
|
||||||
@@ -279,59 +294,40 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
return (
|
return (
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
{kecoRvwList.length > 0 ? (
|
||||||
|
<TechReviewOpinionTable items={kecoRvwList} />
|
||||||
|
) : null}
|
||||||
{renderOpinionForm("KECO")}
|
{renderOpinionForm("KECO")}
|
||||||
</div>
|
</div>
|
||||||
</SectionCollapsible>
|
</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) {
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<RvwOpnnListTable
|
||||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
items={items}
|
||||||
<colgroup>
|
getRowKey={(item) => `${item.designCnstnDocNo}_${item.rvwOpnnSn}`}
|
||||||
<col width="10%" />
|
currentUserUuid={currentUserUuid}
|
||||||
<col width="55%" />
|
onUpdate={async (item, body) => {
|
||||||
<col width="5%" />
|
await designCnstnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
<col width="20%" />
|
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||||
<col width="10%" />
|
}}
|
||||||
</colgroup>
|
onDelete={async (item) => {
|
||||||
<TableHeader>
|
await designCnstnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
<TableRow>
|
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,19 @@ export function createEsntlAtchFileColumns({
|
|||||||
meta: { align: "center" },
|
meta: { align: "center" },
|
||||||
cell: ({ getValue }) => formatYn(getValue()),
|
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", {
|
columnHelper.accessor("esntlAtchFileRmrk", {
|
||||||
header: "비고",
|
header: "비고",
|
||||||
meta: { align: "center" },
|
meta: { align: "center" },
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface EsntlAtchFileFormValues {
|
|||||||
docSeComCd: string;
|
docSeComCd: string;
|
||||||
esntlAtchFileNm: string;
|
esntlAtchFileNm: string;
|
||||||
esntlAtchYn: "Y" | "N";
|
esntlAtchYn: "Y" | "N";
|
||||||
|
esntlAtchFileRvwCn: string;
|
||||||
esntlAtchFileRmrk: string;
|
esntlAtchFileRmrk: string;
|
||||||
atchFileGroupId: string;
|
atchFileGroupId: string;
|
||||||
}
|
}
|
||||||
@@ -68,6 +69,7 @@ function EsntlAtchFileFormDialog({
|
|||||||
docSeComCd: "",
|
docSeComCd: "",
|
||||||
esntlAtchFileNm: "",
|
esntlAtchFileNm: "",
|
||||||
esntlAtchYn: "Y",
|
esntlAtchYn: "Y",
|
||||||
|
esntlAtchFileRvwCn: "",
|
||||||
esntlAtchFileRmrk: "",
|
esntlAtchFileRmrk: "",
|
||||||
atchFileGroupId: "",
|
atchFileGroupId: "",
|
||||||
},
|
},
|
||||||
@@ -115,6 +117,7 @@ function EsntlAtchFileFormDialog({
|
|||||||
docSeComCd: initial.docSeComCd ?? "",
|
docSeComCd: initial.docSeComCd ?? "",
|
||||||
esntlAtchFileNm: initial.esntlAtchFileNm ?? "",
|
esntlAtchFileNm: initial.esntlAtchFileNm ?? "",
|
||||||
esntlAtchYn: initial.esntlAtchYn ?? "Y",
|
esntlAtchYn: initial.esntlAtchYn ?? "Y",
|
||||||
|
esntlAtchFileRvwCn: initial.esntlAtchFileRvwCn ?? "",
|
||||||
esntlAtchFileRmrk: initial.esntlAtchFileRmrk ?? "",
|
esntlAtchFileRmrk: initial.esntlAtchFileRmrk ?? "",
|
||||||
atchFileGroupId: initial.atchFileGroupId ?? "",
|
atchFileGroupId: initial.atchFileGroupId ?? "",
|
||||||
});
|
});
|
||||||
@@ -127,6 +130,7 @@ function EsntlAtchFileFormDialog({
|
|||||||
docSeComCd: "",
|
docSeComCd: "",
|
||||||
esntlAtchFileNm: "",
|
esntlAtchFileNm: "",
|
||||||
esntlAtchYn: "Y",
|
esntlAtchYn: "Y",
|
||||||
|
esntlAtchFileRvwCn: "",
|
||||||
esntlAtchFileRmrk: "",
|
esntlAtchFileRmrk: "",
|
||||||
atchFileGroupId: "",
|
atchFileGroupId: "",
|
||||||
});
|
});
|
||||||
@@ -249,6 +253,17 @@ function EsntlAtchFileFormDialog({
|
|||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="esntlAtchFileRmrk">비고</Label>
|
<Label htmlFor="esntlAtchFileRmrk">비고</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function EsntlAtchFileList({
|
|||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "필수 첨부파일 관리 목록",
|
caption: "필수첨부파일 목록으로 필수첨부파일아이디, 문서구분, 필수첨부파일명, 필수첨부여부, 필수파일검토내용, 비고, 양식 내려받기, 관리 정보 제공",
|
||||||
viewPagination: true,
|
viewPagination: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -112,6 +112,7 @@ function EsntlAtchFileList({
|
|||||||
docSeComCd: values.docSeComCd,
|
docSeComCd: values.docSeComCd,
|
||||||
esntlAtchFileNm: values.esntlAtchFileNm,
|
esntlAtchFileNm: values.esntlAtchFileNm,
|
||||||
esntlAtchYn: values.esntlAtchYn,
|
esntlAtchYn: values.esntlAtchYn,
|
||||||
|
esntlAtchFileRvwCn: values.esntlAtchFileRvwCn || undefined,
|
||||||
esntlAtchFileRmrk: values.esntlAtchFileRmrk || undefined,
|
esntlAtchFileRmrk: values.esntlAtchFileRmrk || undefined,
|
||||||
atchFileGroupId: values.atchFileGroupId || undefined,
|
atchFileGroupId: values.atchFileGroupId || undefined,
|
||||||
};
|
};
|
||||||
@@ -136,14 +137,14 @@ function EsntlAtchFileList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<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 type="button" color="green" onClick={handleOpenCreate}>
|
||||||
등록
|
등록
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{tableComponent}
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||||
{dialogMode ? (
|
{dialogMode ? (
|
||||||
<EsntlAtchFileFormDialog
|
<EsntlAtchFileFormDialog
|
||||||
mode={dialogMode}
|
mode={dialogMode}
|
||||||
@@ -159,7 +160,7 @@ function EsntlAtchFileList({
|
|||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ function EsntlAtchFileMngPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-[1.8rem] pb-6">
|
<div className="card card-border !mt-6">
|
||||||
<section className="card card-border mx-0 bg-background">
|
<div className="card card-border">
|
||||||
<form className="flex flex-wrap items-end gap-4 p-4" onSubmit={handleSearch}>
|
<form className="flex flex-wrap items-end gap-4 p-4" onSubmit={handleSearch}>
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<Label htmlFor="docSeComCd">문서구분</Label>
|
<Label htmlFor="docSeComCd">문서구분</Label>
|
||||||
@@ -79,7 +79,7 @@ function EsntlAtchFileMngPage() {
|
|||||||
<Button type="submit">조회</Button>
|
<Button type="submit">조회</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<EsntlAtchFileList
|
<EsntlAtchFileList
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
|||||||
@@ -120,6 +120,23 @@ function getBadgeColor(state: BadgeState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function formatOptional(value: string | null | undefined) {
|
||||||
return value?.trim() ? value : "-";
|
return value?.trim() ? value : "-";
|
||||||
}
|
}
|
||||||
@@ -133,11 +150,21 @@ function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadg
|
|||||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
<div className="flex flex-wrap items-center justify-center">
|
||||||
{badges.map((badge, index) => (
|
{badges.map((badge, index) => (
|
||||||
<Fragment key={badge.label}>
|
<Fragment key={badge.label}>
|
||||||
{index > 0 && <span className="mx-2">></span>}
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
{/* 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>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,21 +53,52 @@ export function createFileVrfcSplmntColumns() {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("fileSeComCd", {
|
columnHelper.accessor("fileSeComCd", {
|
||||||
header: () => <p className="whitespace-nowrap">파일유형</p>,
|
header: "파일유형",
|
||||||
meta: { align: "left" },
|
// 고정 너비 + 자동 줄바꿈 (긴 텍스트로 표가 가로로 늘어나지 않도록)
|
||||||
|
meta: { align: "left", width: 100 },
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => (
|
||||||
<p>{formatOptional(FILE_SE_COM_CD_MAP[getValue() as keyof typeof FILE_SE_COM_CD_MAP])}</p>
|
<p className="whitespace-normal break-words">
|
||||||
|
{formatOptional(FILE_SE_COM_CD_MAP[getValue() as keyof typeof FILE_SE_COM_CD_MAP])}
|
||||||
|
</p>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("fileSeNm", {
|
columnHelper.accessor("fileSeNm", {
|
||||||
header: () => <p className="whitespace-nowrap">파일구분</p>,
|
header: "파일구분",
|
||||||
meta: { align: "left" },
|
meta: { align: "left", width: 320 },
|
||||||
cell: ({ getValue }) => <p className="whitespace-nowrap">{formatOptional(getValue())}</p>,
|
// 표 레이아웃과 무관하게 셀 내용 너비를 320px 로 고정하고 넘치는 텍스트는 자동 줄바꿈
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
width: 320,
|
||||||
|
maxWidth: 320,
|
||||||
|
// 검증내역과 글자가 붙어 보이지 않도록 오른쪽 여백
|
||||||
|
paddingRight: 12,
|
||||||
|
whiteSpace: "normal",
|
||||||
|
wordBreak: "normal",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatOptional(getValue())}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("vrfcRsltCn", {
|
columnHelper.accessor("vrfcRsltCn", {
|
||||||
header: "검증내역",
|
header: "검증내역",
|
||||||
|
// 너비 미지정: 나머지 영역을 사용, 넘치는 텍스트는 자동 줄바꿈
|
||||||
meta: { align: "left" },
|
meta: { align: "left" },
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
cell: ({ getValue }) => (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
// 파일구분과 글자가 붙어 보이지 않도록 왼쪽 여백
|
||||||
|
paddingLeft: 12,
|
||||||
|
whiteSpace: "normal",
|
||||||
|
wordBreak: "normal",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatOptional(getValue())}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function ChecklistTaskPanel({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
<Table caption={`체크리스트 목록으로 검토항목, 주요 검토내용, 검토 결과${selectedReviewStep !== "LOCAL" ? ", 기술검토 의견" : ""} 정보 제공`} variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ function FileVrfcSplmntDialog({
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "파일 검증 보완요청 목록",
|
caption: "파일보완 목록으로 선택, 파일유형, 파일구분, 검증내역 정보 제공",
|
||||||
|
// 열 너비를 고정해 긴 텍스트는 셀 안에서 줄바꿈
|
||||||
|
className: "table-fixed",
|
||||||
viewPagination: false,
|
viewPagination: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ import {
|
|||||||
interface PwrkCnsltnDocEvlDialogProps {
|
interface PwrkCnsltnDocEvlDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
pwrkCnsltnDocNo: string;
|
pwrkCnsltnDocNo: string;
|
||||||
|
/** 평가 저장 성공 시 호출 (이후 웹기안기 문서 저장이 진행된다) */
|
||||||
|
onSaved: () => void;
|
||||||
|
/** 평가 없이 닫을 때 호출 (웹기안기 문서는 저장되지 않는다) */
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +44,12 @@ const DEFAULT_VALUES: Partial<PwrkCnsltnDocEvlFormValues> = {
|
|||||||
pwrkCnsltnDocEvlCn: "",
|
pwrkCnsltnDocEvlCn: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDocEvlDialogProps) {
|
function PwrkCnsltnDocEvlDialog({
|
||||||
|
open,
|
||||||
|
pwrkCnsltnDocNo,
|
||||||
|
onSaved,
|
||||||
|
onClose,
|
||||||
|
}: PwrkCnsltnDocEvlDialogProps) {
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
const { mutateAsync: saveEvl, isPending } = useSavePwrkCnsltnDocEvl();
|
const { mutateAsync: saveEvl, isPending } = useSavePwrkCnsltnDocEvl();
|
||||||
|
|
||||||
@@ -65,8 +73,7 @@ function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDo
|
|||||||
try {
|
try {
|
||||||
const payload = buildPwrkCnsltnDocEvlSavePayload(pwrkCnsltnDocNo, values);
|
const payload = buildPwrkCnsltnDocEvlSavePayload(pwrkCnsltnDocNo, values);
|
||||||
await saveEvl(payload);
|
await saveEvl(payload);
|
||||||
alert("AI 초안 만족도 평가를 등록했습니다.");
|
onSaved();
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("AI 초안 만족도 평가 등록 실패:", error);
|
console.error("AI 초안 만족도 평가 등록 실패:", error);
|
||||||
alert("AI 초안 만족도 평가 등록에 실패했습니다.");
|
alert("AI 초안 만족도 평가 등록에 실패했습니다.");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -46,6 +46,7 @@ const HangulGiangi = dynamic(
|
|||||||
|
|
||||||
const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
|
const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
|
||||||
|
|
||||||
|
/** 현재 버전이 이 값보다 작으면(AI 초안 상태) 저장 전에 만족도 평가를 먼저 받는다. */
|
||||||
const AI_DRAFT_EVL_MAX_VER_SN = 2;
|
const AI_DRAFT_EVL_MAX_VER_SN = 2;
|
||||||
|
|
||||||
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
|
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
|
||||||
@@ -88,6 +89,8 @@ function PwrkCnsltnDocMngDialog({
|
|||||||
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
|
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
|
||||||
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
|
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
|
||||||
const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false);
|
const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false);
|
||||||
|
// 평가 모달 결과(평가 저장 여부)를 저장 흐름으로 돌려주는 resolver
|
||||||
|
const evlResolverRef = useRef<((isEvlSaved: boolean) => void) | null>(null);
|
||||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -145,6 +148,8 @@ function PwrkCnsltnDocMngDialog({
|
|||||||
setAtchFileGroupId("");
|
setAtchFileGroupId("");
|
||||||
setUploadedFiles([]);
|
setUploadedFiles([]);
|
||||||
setIsEvlDialogOpen(false);
|
setIsEvlDialogOpen(false);
|
||||||
|
evlResolverRef.current?.(false);
|
||||||
|
evlResolverRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,9 +183,32 @@ function PwrkCnsltnDocMngDialog({
|
|||||||
if (!nextOpen) onClose();
|
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) {
|
async function handleSaveDocument(file: Blob) {
|
||||||
if (!completedDraft) throw new Error("저장할 실무협의서 초안이 없습니다.");
|
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({
|
const saveDto = pwrkCnsltnDocCrtRsltSaveRequestSchema.parse({
|
||||||
crtDmndUuid: completedDraft.crtDmndUuid,
|
crtDmndUuid: completedDraft.crtDmndUuid,
|
||||||
pwrkCnsltnDocNo: completedDraft.pwrkCnsltnDocNo,
|
pwrkCnsltnDocNo: completedDraft.pwrkCnsltnDocNo,
|
||||||
@@ -201,10 +229,6 @@ function PwrkCnsltnDocMngDialog({
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`);
|
alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`);
|
||||||
|
|
||||||
if (savedDocument.verSn <= AI_DRAFT_EVL_MAX_VER_SN) {
|
|
||||||
setIsEvlDialogOpen(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownload() {
|
async function handleDownload() {
|
||||||
@@ -386,7 +410,8 @@ function PwrkCnsltnDocMngDialog({
|
|||||||
<PwrkCnsltnDocEvlDialog
|
<PwrkCnsltnDocEvlDialog
|
||||||
open={isEvlDialogOpen}
|
open={isEvlDialogOpen}
|
||||||
pwrkCnsltnDocNo={completedDraft.pwrkCnsltnDocNo}
|
pwrkCnsltnDocNo={completedDraft.pwrkCnsltnDocNo}
|
||||||
onClose={() => setIsEvlDialogOpen(false)}
|
onSaved={() => resolveEvl(true)}
|
||||||
|
onClose={() => resolveEvl(false)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function PwrkCnsltnDocPjtcoAjmtTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table caption="사업비 조정 내역: 구분, 검증결과, 검증금액 정보 제공" variant="secondary">
|
<Table caption="금액검증결과 목록으로 구분, 검증결과, 검증금액 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="30%" />
|
<col width="30%" />
|
||||||
<col width="50%" />
|
<col width="50%" />
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ function CnsltnListTable({
|
|||||||
const isSomeSelected = selectedCount > 0 && !isAllSelected;
|
const isSomeSelected = selectedCount > 0 && !isAllSelected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table caption="자문 검토결과 목록: 구분, 검토의견 정보 제공" variant="secondary">
|
<Table caption="검토 결과 조회 목록으로 선택, 구분, 검토의견 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="4%" />
|
<col width="4%" />
|
||||||
<col width="4%" />
|
<col width="4%" />
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ function FnrsListPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
<div className="card card-border !mt-6">
|
||||||
<section className="card card-border mx-0 bg-background">
|
<div className="card card-border">
|
||||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
||||||
@@ -145,7 +145,7 @@ function FnrsListPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Button type="submit">조회</Button>
|
<Button type="submit">조회</Button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<FnrsList
|
<FnrsList
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
|||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "재원협의 목록",
|
caption: "재원협의 목록으로 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일 정보 제공",
|
||||||
viewPagination: true,
|
viewPagination: true,
|
||||||
onRowClick: handleRowClick,
|
onRowClick: handleRowClick,
|
||||||
},
|
},
|
||||||
@@ -78,15 +78,15 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
|||||||
|
|
||||||
return (
|
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">
|
<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 type="button" color="green" onClick={() => setIsTaskSeDialogOpen(true)}>
|
||||||
신청
|
신청
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{tableComponent}
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||||
</section>
|
</div>
|
||||||
<PbadmsTaskApplyDialog
|
<PbadmsTaskApplyDialog
|
||||||
open={isTaskSeDialogOpen}
|
open={isTaskSeDialogOpen}
|
||||||
onOpenChange={setIsTaskSeDialogOpen}
|
onOpenChange={setIsTaskSeDialogOpen}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
|
|||||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||||
|
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
|
|||||||
const { control, watch, setValue } = useFormContext<FnrsCnsltnRegistFormData>();
|
const { control, watch, setValue } = useFormContext<FnrsCnsltnRegistFormData>();
|
||||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||||
const taskSeComCd = watch("fnrsCnsltnMng.taskSeComCd");
|
const taskSeComCd = watch("fnrsCnsltnMng.taskSeComCd");
|
||||||
const localGovLabel = [watch("fnrsCnsltnMng.sido"), watch("fnrsCnsltnMng.gugun")]
|
const sido = watch("fnrsCnsltnMng.sido");
|
||||||
.filter(Boolean)
|
const gugun = watch("fnrsCnsltnMng.gugun");
|
||||||
.join(" ");
|
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
|
||||||
const { data: taskOptions } = useCodeList("FNRS");
|
const { data: taskOptions } = useCodeList("FNRS");
|
||||||
const { data: rtlztnFcltOptions } = useCodeList("RTLZTNFCLTSE");
|
const { data: rtlztnFcltOptions } = useCodeList("RTLZTNFCLTSE");
|
||||||
const { data: rtlztnUsgOptions } = useCodeList("RTLZTNUSGSE");
|
const { data: rtlztnUsgOptions } = useCodeList("RTLZTNUSGSE");
|
||||||
@@ -49,7 +50,8 @@ function BasicInfoFormSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table caption="재원협의 기본 정보 입력" variant="secondary">
|
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||||
|
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 시설종류, 재이용용도 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
|
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
|
||||||
|
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
interface BasicInfoSectionProps {
|
interface BasicInfoSectionProps {
|
||||||
@@ -11,7 +12,8 @@ interface BasicInfoSectionProps {
|
|||||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table caption="재원협의 기본 정보" variant="secondary">
|
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
|
||||||
|
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체, 확정예산사업번호, 예산신청연도 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
|||||||
<SectionCollapsible title="총사업비">
|
<SectionCollapsible title="총사업비">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
<Table caption="총사업비 목록으로 구분, 예산반영(A), 표준사업비(B), 금회설계시(C), 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
|||||||
return (
|
return (
|
||||||
<SectionCollapsible title="사업규모">
|
<SectionCollapsible title="사업규모">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
<Table caption="사업규모 목록으로 구분, 기본계획(A), 예산반영(B), 금회설계시(C), 증감(C-B) 정보 제공" variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[30rem]">구분</TableHead>
|
<TableHead className="w-[30rem]">구분</TableHead>
|
||||||
|
|||||||
@@ -86,7 +86,13 @@ function ChecklistSection({
|
|||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{visibleReviewSteps.map((stepCode) => (
|
{visibleReviewSteps.map((stepCode) => (
|
||||||
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
|
<TabsTrigger
|
||||||
|
key={stepCode}
|
||||||
|
value={stepCode}
|
||||||
|
id={`fnrs-cklst-step-tab-${stepCode}`}
|
||||||
|
aria-controls="fnrs-cklst-task-tabs"
|
||||||
|
className="!px-6 !py-3 text-xl"
|
||||||
|
>
|
||||||
{CHECKLIST_TAB_LABELS[stepCode]}
|
{CHECKLIST_TAB_LABELS[stepCode]}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
@@ -96,6 +102,7 @@ function ChecklistSection({
|
|||||||
|
|
||||||
{taskCodes.length > 0 ? (
|
{taskCodes.length > 0 ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
id="fnrs-cklst-task-tabs"
|
||||||
value={selectedTaskCode ?? taskCodes[0]}
|
value={selectedTaskCode ?? taskCodes[0]}
|
||||||
onValueChange={setSelectedTaskCode}
|
onValueChange={setSelectedTaskCode}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -107,6 +114,8 @@ function ChecklistSection({
|
|||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={code}
|
key={code}
|
||||||
value={code}
|
value={code}
|
||||||
|
id={`fnrs-cklst-tab-${code}`}
|
||||||
|
aria-controls={`fnrs-cklst-panel-${code}`}
|
||||||
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
||||||
>
|
>
|
||||||
{TASK_CODE_LABELS[code] ?? code}
|
{TASK_CODE_LABELS[code] ?? code}
|
||||||
@@ -120,7 +129,15 @@ function ChecklistSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{taskCodes.map((code) => (
|
{taskCodes.map((code) => (
|
||||||
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
|
<TabsContent
|
||||||
|
key={code}
|
||||||
|
value={code}
|
||||||
|
id={`fnrs-cklst-panel-${code}`}
|
||||||
|
aria-labelledby={`fnrs-cklst-tab-${code}`}
|
||||||
|
className="mt-0 border-0 p-0 shadow-none ring-0"
|
||||||
|
>
|
||||||
|
{/* 선택된 탭(검토단계·업무구분)의 패널임을 알 수 있도록 숨김 제목 제공 */}
|
||||||
|
<h3 className="sr-only">{CHECKLIST_TAB_LABELS[selectedReviewStep]} {TASK_CODE_LABELS[code] ?? code} 체크리스트</h3>
|
||||||
<ChecklistTaskPanel
|
<ChecklistTaskPanel
|
||||||
selectedReviewStep={selectedReviewStep}
|
selectedReviewStep={selectedReviewStep}
|
||||||
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface ContactInfoSectionProps {
|
|||||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="담당자 정보">
|
<SectionCollapsible title="담당자 정보">
|
||||||
<Table caption="재원협의 담당자 정보" variant="secondary">
|
<Table caption="담당자 정보로 신청자(이름, 연락처, 소속, 이메일)과 설계자(이름, 연락처, 소속, 이메일) 정보 제공" variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{det.aplcntNm && (
|
{det.aplcntNm && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
|
|||||||
<SectionCollapsible title="재원분담">
|
<SectionCollapsible title="재원분담">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||||
<Table caption="재원분담 내역: 구분, 현행, 변경요구, 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
<Table caption="재원분담 목록으로 구분, 현행(A), 변경요구(B), 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[24rem]" rowSpan={2}>
|
<TableHead className="w-[24rem]" rowSpan={2}>
|
||||||
|
|||||||
@@ -2,20 +2,22 @@
|
|||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
|
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
|
||||||
import {
|
import {
|
||||||
AttachmentDownloadLink,
|
fnrsCnsltnKeys,
|
||||||
attachService,
|
type FnrsCnsltnRvwOpnnResponseDto,
|
||||||
useDownloadAttachmentByAtflId,
|
fnrsCnsltnRvwOpnnService,
|
||||||
} from "@/entities/attach";
|
} from "@/entities/fnrs-cnsltn";
|
||||||
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
import {
|
import {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -180,16 +182,24 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
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) {
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
|
||||||
|
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
<Table caption="검토의견 입력" variant="secondary">
|
<Table
|
||||||
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||||
@@ -280,7 +290,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
{hasEnvSection ? (
|
{hasEnvSection ? (
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
{envRvwList.length > 0 ? (
|
||||||
|
<TechReviewOpinionTable items={envRvwList} />
|
||||||
|
) : null}
|
||||||
{renderOpinionForm("ENV")}
|
{renderOpinionForm("ENV")}
|
||||||
</div>
|
</div>
|
||||||
</SectionCollapsible>
|
</SectionCollapsible>
|
||||||
@@ -288,7 +300,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
{hasKecoSection ? (
|
{hasKecoSection ? (
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
{kecoRvwList.length > 0 ? (
|
||||||
|
<TechReviewOpinionTable items={kecoRvwList} />
|
||||||
|
) : null}
|
||||||
{/*
|
{/*
|
||||||
재원협의에서 환경공단 검토의견은 자동 생성
|
재원협의에서 환경공단 검토의견은 자동 생성
|
||||||
{renderOpinionForm("KECO")}
|
{renderOpinionForm("KECO")}
|
||||||
@@ -300,52 +314,31 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getRvwOpnnIds(item: FnrsCnsltnRvwOpnnResponseDto) {
|
||||||
|
if (!item.fnrsCnsltnDocNo || item.rvwOpnnSn == null) {
|
||||||
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
|
}
|
||||||
|
return [item.fnrsCnsltnDocNo, item.rvwOpnnSn] as const;
|
||||||
|
}
|
||||||
|
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<RvwOpnnListTable
|
||||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
items={items}
|
||||||
<colgroup>
|
getRowKey={(item) => `${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}
|
||||||
<col width="10%" />
|
currentUserUuid={currentUserUuid}
|
||||||
<col width="55%" />
|
onUpdate={async (item, body) => {
|
||||||
<col width="5%" />
|
await fnrsCnsltnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
<col width="20%" />
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||||
<col width="10%" />
|
}}
|
||||||
</colgroup>
|
onDelete={async (item) => {
|
||||||
<TableHeader>
|
await fnrsCnsltnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
<TableRow>
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
|
|||||||
aply: "aply",
|
aply: "aply",
|
||||||
del: "del",
|
del: "del",
|
||||||
instlSncnAncmnt: "instlSncnAncmnt",
|
instlSncnAncmnt: "instlSncnAncmnt",
|
||||||
envoSbmsn: "envoSbmsn",
|
|
||||||
rjct: "rjct",
|
rjct: "rjct",
|
||||||
sncnAprv: "sncnAprv",
|
sncnAprv: "sncnAprv",
|
||||||
rvwRply: "rvwRply",
|
rvwRply: "rvwRply",
|
||||||
@@ -33,13 +32,15 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
|||||||
],
|
],
|
||||||
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
|
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
|
||||||
},
|
},
|
||||||
|
// 환경청 단계 제거: 기존 환경청(LE·TP) 기능을 광역자치단체가 수행
|
||||||
ROLE_USER04: {
|
ROLE_USER04: {
|
||||||
ST: [INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn, INSTL_SNCN_VIEW_BUTTON_ACTION.rjct],
|
ST: [
|
||||||
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
|
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply,
|
||||||
},
|
INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw,
|
||||||
ROLE_USER03: {
|
INSTL_SNCN_VIEW_BUTTON_ACTION.rjct,
|
||||||
LE: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply, INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
],
|
||||||
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
||||||
|
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,16 +55,8 @@ export const instlSncnRegistSchema = z.object({
|
|||||||
tstmDesignAmt: z.number(),
|
tstmDesignAmt: z.number(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
cklstResultList: z.array(
|
// 설치인가 화면에서 체크리스트 미사용: 숨겨진 값 때문에 신청·저장 검증이 막히지 않도록 필수값을 두지 않음
|
||||||
instlSncnCklstResultItemSchema.extend({
|
cklstResultList: z.array(instlSncnCklstResultItemSchema),
|
||||||
cklstSn: z.number(),
|
|
||||||
cklstArtclSn: z.number(),
|
|
||||||
cklstRvwSn: z.number(),
|
|
||||||
rvwStpComCd: z.string(),
|
|
||||||
selected: z.enum(["Y", "N"]).nullish(),
|
|
||||||
picRvwCn: z.string().nullish(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
esntlAtchFileList: z.array(
|
esntlAtchFileList: z.array(
|
||||||
instlSncnEsntlAtchFileItemSchema.extend({
|
instlSncnEsntlAtchFileItemSchema.extend({
|
||||||
esntlAtchFileId: z.string(),
|
esntlAtchFileId: z.string(),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function getEditableReviewStep(
|
|||||||
dtlInstCd?: string,
|
dtlInstCd?: string,
|
||||||
prgrsSttsComCd?: string,
|
prgrsSttsComCd?: string,
|
||||||
): ReviewStepCode | undefined {
|
): ReviewStepCode | undefined {
|
||||||
if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) {
|
if (dtlInstCd === "ROLE_USER04" && (prgrsSttsComCd === "ST" || prgrsSttsComCd === "TP")) {
|
||||||
return "ENV";
|
return "ENV";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,23 @@ function getBadgeColor(state: BadgeState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function formatOptional(value: string | null | undefined) {
|
||||||
return value?.trim() ? value : "-";
|
return value?.trim() ? value : "-";
|
||||||
}
|
}
|
||||||
@@ -134,11 +151,21 @@ function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadg
|
|||||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
<div className="flex flex-wrap items-center justify-center">
|
||||||
{badges.map((badge, index) => (
|
{badges.map((badge, index) => (
|
||||||
<Fragment key={badge.label}>
|
<Fragment key={badge.label}>
|
||||||
{index > 0 && <span className="mx-2">></span>}
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
{/* 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>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
|
|||||||
|
|
||||||
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
|
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
|
||||||
LOCAL: "자가체크",
|
LOCAL: "자가체크",
|
||||||
ENV: "환경청검토",
|
ENV: "광역자치단체검토",
|
||||||
KECO: "기술검토",
|
KECO: "기술검토",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
||||||
LOCAL: "지자체",
|
LOCAL: "지자체",
|
||||||
ENV: "환경청",
|
ENV: "광역자치단체",
|
||||||
KECO: "환경공단",
|
KECO: "환경공단",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export const INSTL_PROGRESS_ORDER = [
|
|||||||
|
|
||||||
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
||||||
ROLE_USER05: "LOCAL",
|
ROLE_USER05: "LOCAL",
|
||||||
ROLE_USER03: "ENV",
|
ROLE_USER04: "ENV",
|
||||||
ROLE_USER01: "KECO",
|
ROLE_USER01: "KECO",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function ChecklistTaskPanel({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
<Table caption={`체크리스트 목록으로 검토항목, 주요 검토내용, 검토 결과${selectedReviewStep !== "LOCAL" ? ", 기술검토 의견" : ""} 정보 제공`} variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import {
|
|||||||
type InstlSncnViewButtonAction,
|
type InstlSncnViewButtonAction,
|
||||||
needsRoleUser01AtrzResolve,
|
needsRoleUser01AtrzResolve,
|
||||||
} from "../../lib/button-config";
|
} from "../../lib/button-config";
|
||||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
|
||||||
import {
|
import {
|
||||||
buildSavePayload,
|
buildSavePayload,
|
||||||
buildSupplementReqPayload,
|
buildSupplementReqPayload,
|
||||||
@@ -179,11 +178,6 @@ function FloatingActionButtons({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleAplyClick() {
|
async function handleAplyClick() {
|
||||||
const { percent } = computeReviewStepChecklistProgress(
|
|
||||||
data.cklstResultList ?? [],
|
|
||||||
checklistFormState.results,
|
|
||||||
"LOCAL",
|
|
||||||
);
|
|
||||||
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
|
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
|
||||||
data,
|
data,
|
||||||
checklistFormState,
|
checklistFormState,
|
||||||
@@ -191,11 +185,6 @@ function FloatingActionButtons({
|
|||||||
"ST",
|
"ST",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (percent !== 100) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await executeAction({
|
await executeAction({
|
||||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
||||||
request: payload,
|
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() {
|
async function handleRjctClick() {
|
||||||
const payload: DocPrgrsUpdateRequestDto = {
|
const payload: DocPrgrsUpdateRequestDto = {
|
||||||
docNo,
|
docNo,
|
||||||
@@ -284,16 +257,6 @@ function FloatingActionButtons({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRvwRplyClick() {
|
async function handleRvwRplyClick() {
|
||||||
const { percent } = computeReviewStepChecklistProgress(
|
|
||||||
data.cklstResultList ?? [],
|
|
||||||
checklistFormState.results,
|
|
||||||
"ENV",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (percent !== 100) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = buildTechReviewReplyPayload(
|
const payload = buildTechReviewReplyPayload(
|
||||||
data,
|
data,
|
||||||
@@ -369,16 +332,6 @@ function FloatingActionButtons({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRvwCmptnClick() {
|
async function handleRvwCmptnClick() {
|
||||||
const { percent } = computeReviewStepChecklistProgress(
|
|
||||||
data.cklstResultList ?? [],
|
|
||||||
checklistFormState.results,
|
|
||||||
"KECO",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (percent !== 100) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...buildTechReviewReplyPayload(
|
...buildTechReviewReplyPayload(
|
||||||
@@ -447,11 +400,6 @@ function FloatingActionButtons({
|
|||||||
설치인가 고시
|
설치인가 고시
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn) && (
|
|
||||||
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
|
|
||||||
환경청제출
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rjct) && (
|
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rjct) && (
|
||||||
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
|
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
|
||||||
반려
|
반려
|
||||||
@@ -618,7 +566,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
|||||||
aply: handleSaveApply,
|
aply: handleSaveApply,
|
||||||
del: handleDel,
|
del: handleDel,
|
||||||
instlSncnAncmnt: handleChangePrgrsSttsComCd,
|
instlSncnAncmnt: handleChangePrgrsSttsComCd,
|
||||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
|
||||||
rjct: handleChangePrgrsSttsComCd,
|
rjct: handleChangePrgrsSttsComCd,
|
||||||
sncnAprv: handleChangePrgrsSttsComCd,
|
sncnAprv: handleChangePrgrsSttsComCd,
|
||||||
rvwRply: handleTechReview,
|
rvwRply: handleTechReview,
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ import {
|
|||||||
AttachmentsFormSection,
|
AttachmentsFormSection,
|
||||||
BusinessCostFormSection,
|
BusinessCostFormSection,
|
||||||
BusinessScaleFormSection,
|
BusinessScaleFormSection,
|
||||||
ChecklistFormSection,
|
|
||||||
ContactInfoFormSection,
|
ContactInfoFormSection,
|
||||||
isFormChecklistComplete,
|
|
||||||
isFormEsntlAtchComplete,
|
isFormEsntlAtchComplete,
|
||||||
PrivacyConsentSection,
|
PrivacyConsentSection,
|
||||||
ProgressStatusSection,
|
ProgressStatusSection,
|
||||||
@@ -154,11 +152,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFormChecklistComplete(data.cklstResultList)) {
|
|
||||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||||
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||||
return;
|
return;
|
||||||
@@ -228,7 +221,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
|||||||
<PrivacyConsentSection />
|
<PrivacyConsentSection />
|
||||||
<BusinessScaleFormSection />
|
<BusinessScaleFormSection />
|
||||||
<BusinessCostFormSection />
|
<BusinessCostFormSection />
|
||||||
<ChecklistFormSection docSeComCd="INSTL" fieldPrefix="instlSncnMng" />
|
|
||||||
<AttachmentsFormSection
|
<AttachmentsFormSection
|
||||||
docSeComCd="INSTL"
|
docSeComCd="INSTL"
|
||||||
fieldPrefix="instlSncnMng"
|
fieldPrefix="instlSncnMng"
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ function InstlListPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
<div className="card card-border !mt-6">
|
||||||
<section className="card card-border mx-0 bg-background">
|
<div className="card card-border">
|
||||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
||||||
@@ -145,7 +145,7 @@ function InstlListPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Button type="submit">조회</Button>
|
<Button type="submit">조회</Button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<InstlList
|
<InstlList
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
|||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "설치승인 목록",
|
caption: "설치인가 목록으로 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일 정보 제공",
|
||||||
viewPagination: true,
|
viewPagination: true,
|
||||||
onRowClick: handleRowClick,
|
onRowClick: handleRowClick,
|
||||||
},
|
},
|
||||||
@@ -82,15 +82,15 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
|||||||
|
|
||||||
return (
|
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">
|
<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 type="button" color="green" onClick={handleRegisterClick}>
|
||||||
신청
|
신청
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{tableComponent}
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||||
</section>
|
</div>
|
||||||
<PbadmsTaskApplyDialog
|
<PbadmsTaskApplyDialog
|
||||||
open={isTaskSeDialogOpen}
|
open={isTaskSeDialogOpen}
|
||||||
onOpenChange={setIsTaskSeDialogOpen}
|
onOpenChange={setIsTaskSeDialogOpen}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { FloatingActionButtons } from "./components/floating-action-buttons";
|
|||||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||||
import { BusinessCostSection } from "./sections/business-cost-section";
|
import { BusinessCostSection } from "./sections/business-cost-section";
|
||||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||||
import { ChecklistSection } from "./sections/checklist-section";
|
|
||||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||||
import {
|
import {
|
||||||
TechReviewOpinionSection,
|
TechReviewOpinionSection,
|
||||||
@@ -38,8 +37,9 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
|||||||
const userInfo = useUserStore((state) => state.userInfo);
|
const userInfo = useUserStore((state) => state.userInfo);
|
||||||
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
const { formState: checklistFormState } = useChecklistFormState(
|
||||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST,
|
||||||
|
);
|
||||||
|
|
||||||
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||||
@@ -79,7 +79,6 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
|||||||
detDto,
|
detDto,
|
||||||
bizSclList,
|
bizSclList,
|
||||||
bizCstList,
|
bizCstList,
|
||||||
cklstResultList = [],
|
|
||||||
esntlFileList,
|
esntlFileList,
|
||||||
etcFileList,
|
etcFileList,
|
||||||
rvwList = [],
|
rvwList = [],
|
||||||
@@ -109,13 +108,6 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
|||||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
||||||
<BusinessScaleSection bizSclList={bizSclList} />
|
<BusinessScaleSection bizSclList={bizSclList} />
|
||||||
<BusinessCostSection bizCstList={bizCstList} />
|
<BusinessCostSection bizCstList={bizCstList} />
|
||||||
<ChecklistSection
|
|
||||||
cklstResultList={cklstResultList}
|
|
||||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
|
||||||
userRoleCode={userInfo?.dtlInstCd}
|
|
||||||
formState={checklistFormState}
|
|
||||||
setFormState={setChecklistFormState}
|
|
||||||
/>
|
|
||||||
<AttachmentsViewSection
|
<AttachmentsViewSection
|
||||||
esntlFileList={esntlFileList ?? []}
|
esntlFileList={esntlFileList ?? []}
|
||||||
etcFileList={etcFileList ?? []}
|
etcFileList={etcFileList ?? []}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
|
|||||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||||
|
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
|
|||||||
const { control, watch, setValue } = useFormContext<InstlSncnRegistFormData>();
|
const { control, watch, setValue } = useFormContext<InstlSncnRegistFormData>();
|
||||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||||
const taskSeComCd = watch("instlSncnMng.taskSeComCd");
|
const taskSeComCd = watch("instlSncnMng.taskSeComCd");
|
||||||
const localGovLabel = [watch("instlSncnMng.sido"), watch("instlSncnMng.gugun")]
|
const sido = watch("instlSncnMng.sido");
|
||||||
.filter(Boolean)
|
const gugun = watch("instlSncnMng.gugun");
|
||||||
.join(" ");
|
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
|
||||||
|
|
||||||
function handleBnsSelect(data: BnsApplyData) {
|
function handleBnsSelect(data: BnsApplyData) {
|
||||||
setValue("instlSncnMng.bizNm", data.bizNm);
|
setValue("instlSncnMng.bizNm", data.bizNm);
|
||||||
@@ -53,7 +54,8 @@ function BasicInfoFormSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table caption="설치승인 기본 정보 입력" variant="secondary">
|
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||||
|
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 사업구분, 시설종류, 재이용용도 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
|
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
|
||||||
|
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||||
|
|
||||||
interface BasicInfoSectionProps {
|
interface BasicInfoSectionProps {
|
||||||
@@ -11,7 +12,8 @@ interface BasicInfoSectionProps {
|
|||||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본 정보">
|
<SectionCollapsible title="기본 정보">
|
||||||
<Table caption="설치승인 기본 정보" variant="secondary">
|
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
|
||||||
|
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 구분, 신청지자체, 사업구분 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
|||||||
<SectionCollapsible title="총사업비">
|
<SectionCollapsible title="총사업비">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
<Table caption="총사업비 목록으로 구분, 예산반영(A), 표준사업비(B), 금회설계시(C), 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
|||||||
return (
|
return (
|
||||||
<SectionCollapsible title="사업규모">
|
<SectionCollapsible title="사업규모">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
<Table caption="사업규모 목록으로 구분, 기본계획(A), 예산반영(B), 금회설계시(C), 증감(C-B) 정보 제공" variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[30rem]">구분</TableHead>
|
<TableHead className="w-[30rem]">구분</TableHead>
|
||||||
|
|||||||
@@ -85,7 +85,13 @@ function ChecklistSection({
|
|||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{visibleReviewSteps.map((stepCode) => (
|
{visibleReviewSteps.map((stepCode) => (
|
||||||
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
|
<TabsTrigger
|
||||||
|
key={stepCode}
|
||||||
|
value={stepCode}
|
||||||
|
id={`instl-cklst-step-tab-${stepCode}`}
|
||||||
|
aria-controls="instl-cklst-task-tabs"
|
||||||
|
className="!px-6 !py-3 text-xl"
|
||||||
|
>
|
||||||
{CHECKLIST_TAB_LABELS[stepCode]}
|
{CHECKLIST_TAB_LABELS[stepCode]}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
@@ -95,6 +101,7 @@ function ChecklistSection({
|
|||||||
|
|
||||||
{taskCodes.length > 0 ? (
|
{taskCodes.length > 0 ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
id="instl-cklst-task-tabs"
|
||||||
value={selectedTaskCode ?? taskCodes[0]}
|
value={selectedTaskCode ?? taskCodes[0]}
|
||||||
onValueChange={setSelectedTaskCode}
|
onValueChange={setSelectedTaskCode}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -106,6 +113,8 @@ function ChecklistSection({
|
|||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={code}
|
key={code}
|
||||||
value={code}
|
value={code}
|
||||||
|
id={`instl-cklst-tab-${code}`}
|
||||||
|
aria-controls={`instl-cklst-panel-${code}`}
|
||||||
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
||||||
>
|
>
|
||||||
{TASK_CODE_LABELS[code] ?? code}
|
{TASK_CODE_LABELS[code] ?? code}
|
||||||
@@ -119,7 +128,15 @@ function ChecklistSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{taskCodes.map((code) => (
|
{taskCodes.map((code) => (
|
||||||
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
|
<TabsContent
|
||||||
|
key={code}
|
||||||
|
value={code}
|
||||||
|
id={`instl-cklst-panel-${code}`}
|
||||||
|
aria-labelledby={`instl-cklst-tab-${code}`}
|
||||||
|
className="mt-0 border-0 p-0 shadow-none ring-0"
|
||||||
|
>
|
||||||
|
{/* 선택된 탭(검토단계·업무구분)의 패널임을 알 수 있도록 숨김 제목 제공 */}
|
||||||
|
<h3 className="sr-only">{CHECKLIST_TAB_LABELS[selectedReviewStep]} {TASK_CODE_LABELS[code] ?? code} 체크리스트</h3>
|
||||||
<ChecklistTaskPanel
|
<ChecklistTaskPanel
|
||||||
selectedReviewStep={selectedReviewStep}
|
selectedReviewStep={selectedReviewStep}
|
||||||
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface ContactInfoSectionProps {
|
|||||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="담당자 정보">
|
<SectionCollapsible title="담당자 정보">
|
||||||
<Table caption="설치승인 담당자 정보" variant="secondary">
|
<Table caption="담당자 정보로 신청자(이름, 연락처, 소속, 이메일)과 설계자(이름, 연락처, 소속, 이메일) 정보 제공" variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{det.aplcntNm && (
|
{det.aplcntNm && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,18 +2,25 @@
|
|||||||
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
|
||||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { TextShimmer } from "@/shared/ui";
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
import { attachService } from "@/entities/attach";
|
||||||
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
|
import {
|
||||||
|
instlSncnKeys,
|
||||||
|
type InstlSncnRvwOpnnResponseDto,
|
||||||
|
instlSncnRvwOpnnService,
|
||||||
|
type ReviewStepCode,
|
||||||
|
} from "@/entities/instl-sncn";
|
||||||
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -171,19 +178,29 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
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) {
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
|
||||||
|
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
<Table caption="검토의견 입력" variant="secondary">
|
<Table
|
||||||
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
<TableHead scope="row" className="w-[12rem]">
|
||||||
|
의견내용
|
||||||
|
</TableHead>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={formData.rvwRsltCn}
|
value={formData.rvwRsltCn}
|
||||||
@@ -281,7 +298,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
{hasEnvSection ? (
|
{hasEnvSection ? (
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
{envRvwList.length > 0 ? (
|
||||||
|
<TechReviewOpinionTable items={envRvwList} />
|
||||||
|
) : null}
|
||||||
{renderOpinionForm("ENV")}
|
{renderOpinionForm("ENV")}
|
||||||
</div>
|
</div>
|
||||||
</SectionCollapsible>
|
</SectionCollapsible>
|
||||||
@@ -289,7 +308,9 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
{hasKecoSection ? (
|
{hasKecoSection ? (
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
{kecoRvwList.length > 0 ? (
|
||||||
|
<TechReviewOpinionTable items={kecoRvwList} />
|
||||||
|
) : null}
|
||||||
{renderOpinionForm("KECO")}
|
{renderOpinionForm("KECO")}
|
||||||
</div>
|
</div>
|
||||||
</SectionCollapsible>
|
</SectionCollapsible>
|
||||||
@@ -298,52 +319,31 @@ const TechReviewOpinionSection = forwardRef<
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getRvwOpnnIds(item: InstlSncnRvwOpnnResponseDto) {
|
||||||
|
if (!item.instlSncnDocNo || item.rvwOpnnSn == null) {
|
||||||
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
|
}
|
||||||
|
return [item.instlSncnDocNo, item.rvwOpnnSn] as const;
|
||||||
|
}
|
||||||
|
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<RvwOpnnListTable
|
||||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
items={items}
|
||||||
<colgroup>
|
getRowKey={(item) => `${item.instlSncnDocNo}_${item.rvwOpnnSn}`}
|
||||||
<col width="10%" />
|
currentUserUuid={currentUserUuid}
|
||||||
<col width="55%" />
|
onUpdate={async (item, body) => {
|
||||||
<col width="5%" />
|
await instlSncnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
<col width="20%" />
|
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||||
<col width="10%" />
|
}}
|
||||||
</colgroup>
|
onDelete={async (item) => {
|
||||||
<TableHeader>
|
await instlSncnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
<TableRow>
|
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ function MyPbadmsSprtListPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
<div className="card card-border !mt-6">
|
||||||
<section className="card card-border mx-0 bg-background">
|
<div className="card card-border">
|
||||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="whitespace-nowrap">기간</Label>
|
<Label className="whitespace-nowrap">기간</Label>
|
||||||
@@ -157,7 +157,7 @@ function MyPbadmsSprtListPage() {
|
|||||||
|
|
||||||
<Button type="submit">검색</Button>
|
<Button type="submit">검색</Button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<MyPbadmsSprtList
|
<MyPbadmsSprtList
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
|||||||
@@ -70,19 +70,19 @@ function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPb
|
|||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "나의 행정지원 목록",
|
caption: "내 신청 목록으로 No, 구분, 신청일, 진행상태, 완료(예정)일, 담당자, 담당자연락처 정보 제공",
|
||||||
viewPagination: true,
|
viewPagination: true,
|
||||||
onRowClick: handleRowClick,
|
onRowClick: handleRowClick,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
</div>
|
||||||
{tableComponent}
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ function NtiHstryListPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
<div className="card card-border !mt-6">
|
||||||
<section className="card card-border mx-0 bg-background">
|
<div className="card card-border">
|
||||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="whitespace-nowrap">기간</Label>
|
<Label className="whitespace-nowrap">기간</Label>
|
||||||
@@ -114,7 +114,7 @@ function NtiHstryListPage() {
|
|||||||
|
|
||||||
<Button type="submit">검색</Button>
|
<Button type="submit">검색</Button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<NtiHstryList
|
<NtiHstryList
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
|||||||
@@ -97,18 +97,18 @@ function NtiHstryList({ searchParams, pagination, onPaginationChange }: NtiHstry
|
|||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "알림톡 발송 이력 목록",
|
caption: "알림 이력 목록으로 No, 알림일시, 알림내용, 연계업무, 상태, 처리 정보 제공",
|
||||||
viewPagination: true,
|
viewPagination: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
</div>
|
||||||
{tableComponent}
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function InPrgrsDocRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: InPrgrs
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
...rowSelectionOptions,
|
...rowSelectionOptions,
|
||||||
},
|
},
|
||||||
dataTableOptions: { caption: "진행중 문서 담당자 수신자 목록" },
|
dataTableOptions: { caption: "현재 진행중 문서 담당자(신청자) 목록으로 선택, 문서번호, 문서명, 신청자, 상태 정보 제공" },
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function RvwMnpwRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: RvwMnpwRcv
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
...rowSelectionOptions,
|
...rowSelectionOptions,
|
||||||
},
|
},
|
||||||
dataTableOptions: { caption: "검토인력 수신자 목록" },
|
dataTableOptions: { caption: "검토인력 목록으로 선택, 이름, 소속, 전화번호 정보 제공" },
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function SndngList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-h-[40rem] overflow-auto rounded-md border">
|
<div className="max-h-[40rem] overflow-auto rounded-md border">
|
||||||
<Table caption="알림톡 발송 대상 목록: 이름, 전화번호, 변수, 버튼 정보 제공" variant="line" className="text-sm">
|
<Table caption={`발송 목록으로 ${["이름", "전화번호", ...variableNames.map((variableName) => `#${variableName}`), "버튼"].join(", ")} 정보 제공`} variant="line" className="text-sm">
|
||||||
<TableHeader className="top-0">
|
<TableHeader className="top-0">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="min-w-[100px]">이름</TableHead>
|
<TableHead className="min-w-[100px]">이름</TableHead>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { useAlertStore } from "@/shared/lib";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw";
|
import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw";
|
||||||
@@ -10,6 +11,14 @@ import { Badge, Button } from "@common/components";
|
|||||||
|
|
||||||
import type { RvwMnpwBlockConfig } from "../model/rvw-mnpw-block-config";
|
import type { RvwMnpwBlockConfig } from "../model/rvw-mnpw-block-config";
|
||||||
|
|
||||||
|
/** 서버 오류 응답의 message 를 우선 표시하고, 없으면 기본 문구를 사용한다 */
|
||||||
|
function getErrorMessage(error: unknown, fallback: string) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
return error.response?.data?.message || fallback;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
interface RvwMnpwBlockProps {
|
interface RvwMnpwBlockProps {
|
||||||
block: RvwMnpwBlockConfig;
|
block: RvwMnpwBlockConfig;
|
||||||
items: RvwMnpwMngResponseDto[];
|
items: RvwMnpwMngResponseDto[];
|
||||||
@@ -33,8 +42,7 @@ function RvwMnpwBlock({ block, items, selectedPerson }: RvwMnpwBlockProps) {
|
|||||||
});
|
});
|
||||||
alert("배치되었습니다.");
|
alert("배치되었습니다.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "배치에 실패했습니다.";
|
alert(getErrorMessage(error, "배치에 실패했습니다."));
|
||||||
alert(message);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,8 +56,7 @@ function RvwMnpwBlock({ block, items, selectedPerson }: RvwMnpwBlockProps) {
|
|||||||
userUuid: item.userUuid,
|
userUuid: item.userUuid,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "제거에 실패했습니다.";
|
alert(getErrorMessage(error, "제거에 실패했습니다."));
|
||||||
alert(message);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user