Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,65 +1,66 @@
|
|||||||
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
export { fnrsCnsltnRvwOpnnService } from "./api/fnrs-cnsltn-rvw-opnn-service";
|
||||||
export { fnrsCnsltnKeys } from "./api/queries";
|
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
||||||
export {
|
export { fnrsCnsltnKeys } from "./api/queries";
|
||||||
useChangePrgrsSttsComCd,
|
export {
|
||||||
useDeleteFnrsCnsltnMng,
|
useChangePrgrsSttsComCd,
|
||||||
useFnrsCnsltnFileVrfcResults,
|
useDeleteFnrsCnsltnMng,
|
||||||
useFnrsCnsltnMng,
|
useFnrsCnsltnFileVrfcResults,
|
||||||
useFnrsCnsltnMngPage,
|
useFnrsCnsltnMng,
|
||||||
useRequestActnReviewFnrsCnsltnMng,
|
useFnrsCnsltnMngPage,
|
||||||
useRequestExamineFnrsCnsltnMng,
|
useRequestActnReviewFnrsCnsltnMng,
|
||||||
useSaveApplyFnrsCnsltnMng,
|
useRequestExamineFnrsCnsltnMng,
|
||||||
useSaveFnrsCnsltnMng,
|
useSaveApplyFnrsCnsltnMng,
|
||||||
useSubmitReqSupplementFnrsCnsltnMng,
|
useSaveFnrsCnsltnMng,
|
||||||
useSubmitSupplementFnrsCnsltnMng,
|
useSubmitReqSupplementFnrsCnsltnMng,
|
||||||
useTechReviewFnrsCnsltnMng,
|
useSubmitSupplementFnrsCnsltnMng,
|
||||||
useUpdateFnrsCnsltnMng,
|
useTechReviewFnrsCnsltnMng,
|
||||||
} from "./api/queries";
|
useUpdateFnrsCnsltnMng,
|
||||||
export type {
|
} from "./api/queries";
|
||||||
BizCstItem,
|
export type {
|
||||||
BzScaleItem,
|
BizCstItem,
|
||||||
CklstResultItem,
|
BzScaleItem,
|
||||||
EsntlAtchFileItem,
|
CklstResultItem,
|
||||||
FnrsCnsltnApplySaveRequestDto,
|
EsntlAtchFileItem,
|
||||||
FnrsCnsltnMngSaveRequestDto,
|
FnrsCnsltnApplySaveRequestDto,
|
||||||
FnrsCnsltnRvwOpnnSaveRequestDto,
|
FnrsCnsltnMngSaveRequestDto,
|
||||||
FnrsCnsltnSplmntDmndItem,
|
FnrsCnsltnRvwOpnnSaveRequestDto,
|
||||||
FnrsSharItem,
|
FnrsCnsltnSplmntDmndItem,
|
||||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
FnrsSharItem,
|
||||||
export {
|
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||||
fnrsCnsltnApplySaveSchema,
|
export {
|
||||||
fnrsCnsltnBizCstItemSchema,
|
fnrsCnsltnApplySaveSchema,
|
||||||
fnrsCnsltnBzScaleItemSchema,
|
fnrsCnsltnBizCstItemSchema,
|
||||||
fnrsCnsltnCklstResultItemSchema,
|
fnrsCnsltnBzScaleItemSchema,
|
||||||
fnrsCnsltnEsntlAtchFileItemSchema,
|
fnrsCnsltnCklstResultItemSchema,
|
||||||
fnrsCnsltnMngSaveSchema,
|
fnrsCnsltnEsntlAtchFileItemSchema,
|
||||||
fnrsCnsltnRvwOpnnSaveSchema,
|
fnrsCnsltnMngSaveSchema,
|
||||||
fnrsCnsltnSplmntDmndItemSchema,
|
fnrsCnsltnRvwOpnnSaveSchema,
|
||||||
fnrsSharItemSchema,
|
fnrsCnsltnSplmntDmndItemSchema,
|
||||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
fnrsSharItemSchema,
|
||||||
export type {
|
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||||
AtflMngClientResponseDto,
|
export type {
|
||||||
BizCstTreeDto,
|
AtflMngClientResponseDto,
|
||||||
BizSclTreeDto,
|
BizCstTreeDto,
|
||||||
CklstRvwRsltTreeDto,
|
BizSclTreeDto,
|
||||||
DocPrgrsUpdateRequestDto,
|
CklstRvwRsltTreeDto,
|
||||||
ElaprDocPrgrsClientResponseDto,
|
DocPrgrsUpdateRequestDto,
|
||||||
EsntlAtchFileMngTreeResponseDto,
|
ElaprDocPrgrsClientResponseDto,
|
||||||
FnrsCnsltnFileVrfcRsltResponseDto,
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
FnrsCnsltnMngDetResponseDto,
|
FnrsCnsltnFileVrfcRsltResponseDto,
|
||||||
FnrsCnsltnMngResponseDto,
|
FnrsCnsltnMngDetResponseDto,
|
||||||
FnrsCnsltnMngSearchDto,
|
FnrsCnsltnMngResponseDto,
|
||||||
FnrsCnsltnMngService,
|
FnrsCnsltnMngSearchDto,
|
||||||
FnrsCnsltnMngUpdateRequestDto,
|
FnrsCnsltnMngService,
|
||||||
FnrsCnsltnRvwOpnnResponseDto,
|
FnrsCnsltnMngUpdateRequestDto,
|
||||||
FnrsCnsltnSplmntDmndResponseDto,
|
FnrsCnsltnRvwOpnnResponseDto,
|
||||||
RagIndexingInfoDto,
|
FnrsCnsltnSplmntDmndResponseDto,
|
||||||
} from "./model/fnrs-cnsltn-interface";
|
RagIndexingInfoDto,
|
||||||
export type {
|
} from "./model/fnrs-cnsltn-interface";
|
||||||
EnvoActnReviewReqDto,
|
export type {
|
||||||
FnrsExamineRequestDto,
|
EnvoActnReviewReqDto,
|
||||||
} from "./model/fnrs-cnsltn-payload-schema";
|
FnrsExamineRequestDto,
|
||||||
export {
|
} from "./model/fnrs-cnsltn-payload-schema";
|
||||||
envoActnReviewReqSchema,
|
export {
|
||||||
fnrsExamineRequestSchema,
|
envoActnReviewReqSchema,
|
||||||
} from "./model/fnrs-cnsltn-payload-schema";
|
fnrsExamineRequestSchema,
|
||||||
|
} from "./model/fnrs-cnsltn-payload-schema";
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||||
|
|
||||||
|
const BASE = ENDPOINT.AS_SERVICE.INSTL_SNCN_RVW_OPNN;
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
instlSncnDocNo: string,
|
||||||
|
rvwOpnnSn: number,
|
||||||
|
body: RvwOpnnUpdateRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await client.put<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFn(instlSncnDocNo: string, rvwOpnnSn: number): Promise<void> {
|
||||||
|
await client.delete<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const instlSncnRvwOpnnService = {
|
||||||
|
update,
|
||||||
|
delete: deleteFn,
|
||||||
|
};
|
||||||
@@ -1,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,
|
||||||
|
};
|
||||||
@@ -1,64 +1,65 @@
|
|||||||
export { sewerBscPlanKeys } from "./api/queries";
|
export { sewerBscPlanKeys } from "./api/queries";
|
||||||
export {
|
export {
|
||||||
useDeleteSewerBscPlan,
|
useDeleteSewerBscPlan,
|
||||||
useSaveApplySewerBscPlan,
|
useSaveApplySewerBscPlan,
|
||||||
useSaveSewerBscPlan,
|
useSaveSewerBscPlan,
|
||||||
useSewerBscPlan,
|
useSewerBscPlan,
|
||||||
useSewerBscPlanDetails,
|
useSewerBscPlanDetails,
|
||||||
useSewerBscPlanPage,
|
useSewerBscPlanPage,
|
||||||
useSubmitReqSupplementSewerBscPlan,
|
useSubmitReqSupplementSewerBscPlan,
|
||||||
useSubmitSupplementSewerBscPlan,
|
useSubmitSupplementSewerBscPlan,
|
||||||
useTechReviewSewerBscPlan,
|
useTechReviewSewerBscPlan,
|
||||||
useUpdateSewerBscPlan,
|
useUpdateSewerBscPlan,
|
||||||
} from "./api/queries";
|
} from "./api/queries";
|
||||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
export { sewerBscPlanRvwOpnnService } from "./api/sewer-bsc-plan-rvw-opnn-service";
|
||||||
export type {
|
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||||
CklstResultItem,
|
export type {
|
||||||
EsntlAtchFileItem,
|
CklstResultItem,
|
||||||
SewerBscPlanApplySaveRequestDto,
|
EsntlAtchFileItem,
|
||||||
SewerBscPlanBizCstItem,
|
SewerBscPlanApplySaveRequestDto,
|
||||||
SewerBscPlanDtlSaveRequestDto,
|
SewerBscPlanBizCstItem,
|
||||||
SewerBscPlanFnrsCstItem,
|
SewerBscPlanDtlSaveRequestDto,
|
||||||
SewerBscPlanOtlnItem,
|
SewerBscPlanFnrsCstItem,
|
||||||
SewerBscPlanRvwOpnnItem,
|
SewerBscPlanOtlnItem,
|
||||||
SewerBscPlanSaveRequestDto,
|
SewerBscPlanRvwOpnnItem,
|
||||||
SewerBscPlanSplmntDmndItem,
|
SewerBscPlanSaveRequestDto,
|
||||||
SewerBscPlanTmplntDtlItem,
|
SewerBscPlanSplmntDmndItem,
|
||||||
SewerBscPlanTmplntItem,
|
SewerBscPlanTmplntDtlItem,
|
||||||
SewerBscPlanTmplntTmprStrgItem,
|
SewerBscPlanTmplntItem,
|
||||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
SewerBscPlanTmplntTmprStrgItem,
|
||||||
export {
|
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||||
sewerBscPlanApplySaveSchema,
|
export {
|
||||||
sewerBscPlanBizCstItemSchema,
|
sewerBscPlanApplySaveSchema,
|
||||||
sewerBscPlanCklstResultItemSchema,
|
sewerBscPlanBizCstItemSchema,
|
||||||
sewerBscPlanDtlSaveSchema,
|
sewerBscPlanCklstResultItemSchema,
|
||||||
sewerBscPlanEsntlAtchFileItemSchema,
|
sewerBscPlanDtlSaveSchema,
|
||||||
sewerBscPlanFnrsCstItemSchema,
|
sewerBscPlanEsntlAtchFileItemSchema,
|
||||||
sewerBscPlanOtlnItemSchema,
|
sewerBscPlanFnrsCstItemSchema,
|
||||||
sewerBscPlanRvwOpnnItemSchema,
|
sewerBscPlanOtlnItemSchema,
|
||||||
sewerBscPlanSaveSchema,
|
sewerBscPlanRvwOpnnItemSchema,
|
||||||
sewerBscPlanSplmntDmndItemSchema,
|
sewerBscPlanSaveSchema,
|
||||||
sewerBscPlanTmplntDtlItemSchema,
|
sewerBscPlanSplmntDmndItemSchema,
|
||||||
sewerBscPlanTmplntItemSchema,
|
sewerBscPlanTmplntDtlItemSchema,
|
||||||
sewerBscPlanTmplntTmprStrgItemSchema,
|
sewerBscPlanTmplntItemSchema,
|
||||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
sewerBscPlanTmplntTmprStrgItemSchema,
|
||||||
export type {
|
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||||
AtflMngClientResponseDto,
|
export type {
|
||||||
CklstRvwRsltTreeDto,
|
AtflMngClientResponseDto,
|
||||||
EsntlAtchFileMngTreeResponseDto,
|
CklstRvwRsltTreeDto,
|
||||||
SewerBscPlanApplySaveResponseDto,
|
EsntlAtchFileMngTreeResponseDto,
|
||||||
SewerBscPlanBizCstResponseDto,
|
SewerBscPlanApplySaveResponseDto,
|
||||||
SewerBscPlanDetResponseDto,
|
SewerBscPlanBizCstResponseDto,
|
||||||
SewerBscPlanDtlResponseDto,
|
SewerBscPlanDetResponseDto,
|
||||||
SewerBscPlanFnrsCstResponseDto,
|
SewerBscPlanDtlResponseDto,
|
||||||
SewerBscPlanOtlnResponseDto,
|
SewerBscPlanFnrsCstResponseDto,
|
||||||
SewerBscPlanResponseDto,
|
SewerBscPlanOtlnResponseDto,
|
||||||
SewerBscPlanRvwOpnnResponseDto,
|
SewerBscPlanResponseDto,
|
||||||
SewerBscPlanSearchRequestDto,
|
SewerBscPlanRvwOpnnResponseDto,
|
||||||
SewerBscPlanService,
|
SewerBscPlanSearchRequestDto,
|
||||||
SewerBscPlanSplmntDmndResponseDto,
|
SewerBscPlanService,
|
||||||
SewerBscPlanTmplntDtlResponseDto,
|
SewerBscPlanSplmntDmndResponseDto,
|
||||||
SewerBscPlanTmplntResponseDto,
|
SewerBscPlanTmplntDtlResponseDto,
|
||||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
SewerBscPlanTmplntResponseDto,
|
||||||
SewerBscPlanUpdateRequestDto,
|
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||||
} from "./model/sewer-bsc-plan-interface";
|
SewerBscPlanUpdateRequestDto,
|
||||||
|
} from "./model/sewer-bsc-plan-interface";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,156 +1,183 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
|
||||||
import { dayjs } from "@/shared/lib";
|
import { dayjs } from "@/shared/lib";
|
||||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||||
import { Badge } from "@common/components";
|
import { Badge } from "@common/components";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
|
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
|
||||||
|
|
||||||
const MAIN_STEP_LABELS = {
|
const MAIN_STEP_LABELS = {
|
||||||
apply: "설계자문 신청",
|
apply: "설계자문 신청",
|
||||||
techReview: "기술검토",
|
techReview: "기술검토",
|
||||||
reply: "검토결과 회신",
|
reply: "검토결과 회신",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PROGRESS_STATUS_COLUMNS = [
|
export const PROGRESS_STATUS_COLUMNS = [
|
||||||
{ label: "설계자문 신청 (시군)", sttsComCd: "CT" },
|
{ label: "설계자문 신청 (시군)", sttsComCd: "CT" },
|
||||||
{ label: "기술검토", sttsComCd: "CS" },
|
{ label: "기술검토", sttsComCd: "CS" },
|
||||||
{ label: "기술검토 회신", sttsComCd: "CP" },
|
{ label: "기술검토 회신", sttsComCd: "CP" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type BadgeState = "completed" | "current" | "pending";
|
type BadgeState = "completed" | "current" | "pending";
|
||||||
|
|
||||||
interface ProgressBadge {
|
interface ProgressBadge {
|
||||||
label: string;
|
label: string;
|
||||||
state: BadgeState;
|
state: BadgeState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
|
function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
|
||||||
if (prgrsSttsComCd === "CV") {
|
if (prgrsSttsComCd === "CV") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "CT" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "CT" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "CP") {
|
if (prgrsSttsComCd === "CP") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||||
if (prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "SL") {
|
||||||
return <Badge color="orange">보완요청</Badge>;
|
return <Badge color="orange">보완요청</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBadgeColor(state: BadgeState) {
|
function getBadgeColor(state: BadgeState) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "completed":
|
case "completed":
|
||||||
return "green";
|
return "green";
|
||||||
case "current":
|
case "current":
|
||||||
return "blue";
|
return "blue";
|
||||||
case "pending":
|
case "pending":
|
||||||
return "lightGray";
|
return "lightGray";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOptional(value: string | null | undefined) {
|
function getBadgeStateText(state: BadgeState) {
|
||||||
return value?.trim() ? value : "-";
|
switch (state) {
|
||||||
}
|
case "completed":
|
||||||
|
return "완료";
|
||||||
interface ProgressStepBadgeProps {
|
case "current":
|
||||||
prgrsSttsComCd: string;
|
return "진행중";
|
||||||
}
|
case "pending":
|
||||||
|
return "대기";
|
||||||
function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
|
}
|
||||||
const badges = buildProgressBadges(prgrsSttsComCd);
|
}
|
||||||
|
|
||||||
return (
|
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
function splitStepLabel(label: string) {
|
||||||
{badges.map((badge, index) => (
|
const [title = "", ...rest] = label.split(" ");
|
||||||
<Fragment key={`${badge.label}-${index}`}>
|
return { title, detail: rest.join(" ") };
|
||||||
{index > 0 && <span className="mx-2">></span>}
|
}
|
||||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
|
||||||
</Fragment>
|
function formatOptional(value: string | null | undefined) {
|
||||||
))}
|
return value?.trim() ? value : "-";
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
}
|
interface ProgressStepBadgeProps {
|
||||||
|
prgrsSttsComCd: string;
|
||||||
export function createDesignCnstnColumns() {
|
}
|
||||||
return [
|
|
||||||
columnHelper.accessor("sido", {
|
function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
|
||||||
header: "시도",
|
const badges = buildProgressBadges(prgrsSttsComCd);
|
||||||
meta: { align: "center" },
|
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
return (
|
||||||
}),
|
<div className="flex flex-wrap items-center justify-center">
|
||||||
columnHelper.accessor("gugun", {
|
{badges.map((badge, index) => (
|
||||||
header: "시군구",
|
<Fragment key={`${badge.label}-${index}`}>
|
||||||
meta: { align: "center" },
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||||
}),
|
<Badge
|
||||||
columnHelper.accessor("bizNm", {
|
color={getBadgeColor(badge.state)}
|
||||||
header: "사업명",
|
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||||
meta: { align: "left" },
|
>
|
||||||
cell: ({ row, getValue }) => (
|
<span>{splitStepLabel(badge.label).title}</span>
|
||||||
<>
|
<span>
|
||||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
{splitStepLabel(badge.label).detail}
|
||||||
</>
|
{getBadgeStateText(badge.state)}
|
||||||
),
|
</span>
|
||||||
}),
|
</Badge>
|
||||||
columnHelper.accessor("taskSeCdNm", {
|
</Fragment>
|
||||||
header: "업무구분",
|
))}
|
||||||
meta: { align: "center" },
|
</div>
|
||||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
);
|
||||||
}),
|
}
|
||||||
columnHelper.accessor("aplcntNm", {
|
|
||||||
header: "담당자",
|
export function createDesignCnstnColumns() {
|
||||||
meta: { align: "center" },
|
return [
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
columnHelper.accessor("sido", {
|
||||||
}),
|
header: "시도",
|
||||||
columnHelper.accessor("envoSeCdNm", {
|
meta: { align: "center" },
|
||||||
header: "해당기관",
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
meta: { align: "center" },
|
}),
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
columnHelper.accessor("gugun", {
|
||||||
}),
|
header: "시군구",
|
||||||
columnHelper.display({
|
meta: { align: "center" },
|
||||||
id: "progress",
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
header: "진행상황",
|
}),
|
||||||
meta: { align: "center" },
|
columnHelper.accessor("bizNm", {
|
||||||
cell: ({ row }) => <ProgressStepBadge prgrsSttsComCd={row.original.prgrsSttsComCd || ""} />,
|
header: "사업명",
|
||||||
}),
|
meta: { align: "left" },
|
||||||
columnHelper.accessor("regDt", {
|
cell: ({ row, getValue }) => (
|
||||||
header: "신청일",
|
<>
|
||||||
meta: { align: "center" },
|
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||||
cell: ({ getValue }) => {
|
</>
|
||||||
const value = getValue();
|
),
|
||||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
}),
|
||||||
},
|
columnHelper.accessor("taskSeCdNm", {
|
||||||
}),
|
header: "업무구분",
|
||||||
];
|
meta: { align: "center" },
|
||||||
}
|
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("aplcntNm", {
|
||||||
|
header: "담당자",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("envoSeCdNm", {
|
||||||
|
header: "해당기관",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "progress",
|
||||||
|
header: "진행상황",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ row }) => <ProgressStepBadge prgrsSttsComCd={row.original.prgrsSttsComCd || ""} />,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("regDt", {
|
||||||
|
header: "신청일",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue();
|
||||||
|
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ function DesignListPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,338 +1,336 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
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 { TextShimmer } from "@/shared/ui";
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import {
|
import { attachService } from "@/entities/attach";
|
||||||
Button,
|
import {
|
||||||
RadioGroup,
|
designCnstnKeys,
|
||||||
RadioGroupItem,
|
type DesignCnstnRvwOpnnResponseDto,
|
||||||
Table,
|
designCnstnRvwOpnnService,
|
||||||
TableBody,
|
} from "@/entities/design-cnstn";
|
||||||
TableCell,
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
TableHead,
|
import {
|
||||||
TableHeader,
|
Button,
|
||||||
TableRow,
|
RadioGroup,
|
||||||
Textarea,
|
RadioGroupItem,
|
||||||
} from "@common/components";
|
Table,
|
||||||
|
TableCell,
|
||||||
import type {
|
TableHead,
|
||||||
ReviewOpinionFormData,
|
TableHeader,
|
||||||
ReviewOpinionGetFormDataResult,
|
TableRow,
|
||||||
} from "../../lib/review-opinion-utils";
|
Textarea,
|
||||||
import type { ReviewStepCode } from "../../model/view-types";
|
} from "@common/components";
|
||||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
|
||||||
|
import type {
|
||||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
ReviewOpinionFormData,
|
||||||
|
ReviewOpinionGetFormDataResult,
|
||||||
interface TechReviewOpinionSectionProps {
|
} from "../../lib/review-opinion-utils";
|
||||||
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
editableReviewStep?: ReviewStepCode;
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
atrzSeComCdList?: string[];
|
|
||||||
isAtrzLoading?: boolean;
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
}
|
|
||||||
|
interface TechReviewOpinionSectionProps {
|
||||||
export interface TechReviewOpinionSectionHandle {
|
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
editableReviewStep?: ReviewStepCode;
|
||||||
resetForm: () => void;
|
atrzSeComCdList?: string[];
|
||||||
}
|
isAtrzLoading?: boolean;
|
||||||
|
}
|
||||||
interface TechReviewOpinionTableProps {
|
|
||||||
items: DesignCnstnRvwOpnnResponseDto[];
|
export interface TechReviewOpinionSectionHandle {
|
||||||
}
|
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||||
|
resetForm: () => void;
|
||||||
interface UploadedOpinionFile {
|
}
|
||||||
fileName: string;
|
|
||||||
fileKey: string;
|
interface TechReviewOpinionTableProps {
|
||||||
atflId?: string;
|
items: DesignCnstnRvwOpnnResponseDto[];
|
||||||
atflCd?: string;
|
isReviewTurn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
interface UploadedOpinionFile {
|
||||||
rvwRsltCn: "",
|
fileName: string;
|
||||||
rvwRsltAnmlyYn: "",
|
fileKey: string;
|
||||||
atchFileGroupId: "",
|
atflId?: string;
|
||||||
opnrSeComCd: "",
|
atflCd?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
function createRvwOpnnUploadKey(file: File) {
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
rvwRsltCn: "",
|
||||||
}
|
rvwRsltAnmlyYn: "",
|
||||||
|
atchFileGroupId: "",
|
||||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
opnrSeComCd: "",
|
||||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
};
|
||||||
}
|
|
||||||
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
const TechReviewOpinionSection = forwardRef<
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
TechReviewOpinionSectionHandle,
|
}
|
||||||
TechReviewOpinionSectionProps
|
|
||||||
>(function TechReviewOpinionSection(
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
ref,
|
}
|
||||||
) {
|
|
||||||
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
const TechReviewOpinionSection = forwardRef<
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
TechReviewOpinionSectionHandle,
|
||||||
...EMPTY_FORM_DATA,
|
TechReviewOpinionSectionProps
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
>(function TechReviewOpinionSection(
|
||||||
});
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
ref,
|
||||||
|
) {
|
||||||
useEffect(() => {
|
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
setFormData((prev) => ({
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
...prev,
|
...EMPTY_FORM_DATA,
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
}));
|
});
|
||||||
}, [editableReviewStep]);
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
|
|
||||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
useEffect(() => {
|
||||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
setFormData((prev) => ({
|
||||||
if (!atflCd || !atflId) return;
|
...prev,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
}));
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
}, [editableReviewStep]);
|
||||||
},
|
|
||||||
onError: (_error, _file, fileKey) => {
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
},
|
if (!atflCd || !atflId) return;
|
||||||
});
|
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
useImperativeHandle(
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
ref,
|
},
|
||||||
() => ({
|
onError: (_error, _file, fileKey) => {
|
||||||
getFormData: () => {
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
},
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
|
useImperativeHandle(
|
||||||
return {
|
ref,
|
||||||
formData: {
|
() => ({
|
||||||
...formData,
|
getFormData: () => {
|
||||||
opnrSeComCd: editableReviewStep,
|
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
||||||
},
|
return null;
|
||||||
rvwList,
|
}
|
||||||
};
|
|
||||||
},
|
return {
|
||||||
resetForm: () => {
|
formData: {
|
||||||
setFormData({
|
...formData,
|
||||||
...EMPTY_FORM_DATA,
|
opnrSeComCd: editableReviewStep,
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
},
|
||||||
});
|
rvwList,
|
||||||
setUploadedFile(null);
|
};
|
||||||
},
|
},
|
||||||
}),
|
resetForm: () => {
|
||||||
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
setFormData({
|
||||||
);
|
...EMPTY_FORM_DATA,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
});
|
||||||
const hasKecoSection =
|
setUploadedFile(null);
|
||||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
},
|
||||||
|
}),
|
||||||
if (!hasKecoSection) {
|
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
||||||
return null;
|
);
|
||||||
}
|
|
||||||
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
async function handleFileUpload(files: FileList | null) {
|
const hasKecoSection =
|
||||||
if (!files || files.length === 0) return;
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
||||||
|
|
||||||
const file = files[0];
|
if (!hasKecoSection) {
|
||||||
const fileKey = createRvwOpnnUploadKey(file);
|
return null;
|
||||||
const existingAtflCd = formData.atchFileGroupId;
|
}
|
||||||
|
|
||||||
setUploadedFile({ fileName: file.name, fileKey });
|
async function handleFileUpload(files: FileList | null) {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
try {
|
|
||||||
await uploadFileAsync(file, {
|
const file = files[0];
|
||||||
fileKey,
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
});
|
|
||||||
} catch {
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
// onError callback handles cleanup
|
|
||||||
}
|
try {
|
||||||
}
|
await uploadFileAsync(file, {
|
||||||
|
fileKey,
|
||||||
async function handleFileRemove() {
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
if (!uploadedFile) return;
|
});
|
||||||
|
} catch {
|
||||||
uploads[uploadedFile.fileKey]?.abort();
|
// onError callback handles cleanup
|
||||||
|
}
|
||||||
if (uploadedFile.atflId) {
|
}
|
||||||
try {
|
|
||||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
async function handleFileRemove() {
|
||||||
} catch (error) {
|
if (!uploadedFile) return;
|
||||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
|
||||||
}
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
}
|
|
||||||
|
if (uploadedFile.atflId) {
|
||||||
setUploadedFile(null);
|
try {
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
}
|
||||||
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
|
}
|
||||||
|
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
setUploadedFile(null);
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
return (
|
}
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
<Table caption="검토의견 입력" variant="secondary">
|
return editableReviewStep === reviewStep && canWriteOpinion;
|
||||||
<TableHeader>
|
}
|
||||||
<TableRow>
|
|
||||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
<TableCell>
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
<Textarea
|
|
||||||
value={formData.rvwRsltCn}
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
onChange={(event) =>
|
|
||||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
return (
|
||||||
}
|
<div className="space-y-2">
|
||||||
placeholder="의견내용을 입력하세요"
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
rows={4}
|
<Table
|
||||||
className="min-h-[8rem] !text-xl"
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
/>
|
variant="secondary"
|
||||||
</TableCell>
|
>
|
||||||
</TableRow>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead scope="row">검토결과</TableHead>
|
<TableHead scope="row" className="w-[12rem]">
|
||||||
<TableCell>
|
의견내용
|
||||||
<FieldSet>
|
</TableHead>
|
||||||
<RadioGroup
|
<TableCell>
|
||||||
value={formData.rvwRsltAnmlyYn}
|
<Textarea
|
||||||
onValueChange={(value) =>
|
value={formData.rvwRsltCn}
|
||||||
setFormData((prev) => ({
|
onChange={(event) =>
|
||||||
...prev,
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
}
|
||||||
}))
|
placeholder="의견내용을 입력하세요"
|
||||||
}
|
rows={4}
|
||||||
>
|
className="min-h-[8rem] !text-xl"
|
||||||
<div className="flex flex-wrap gap-4">
|
/>
|
||||||
<FieldLabel
|
</TableCell>
|
||||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
</TableRow>
|
||||||
className="!w-auto border-none"
|
<TableRow>
|
||||||
>
|
<TableHead scope="row">검토결과</TableHead>
|
||||||
<Field orientation="horizontal" className="w-auto">
|
<TableCell>
|
||||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
<FieldSet>
|
||||||
<span className="font-normal">정상</span>
|
<RadioGroup
|
||||||
</Field>
|
value={formData.rvwRsltAnmlyYn}
|
||||||
</FieldLabel>
|
onValueChange={(value) =>
|
||||||
<FieldLabel
|
setFormData((prev) => ({
|
||||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
...prev,
|
||||||
className="!w-auto border-none"
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
>
|
}))
|
||||||
<Field orientation="horizontal" className="w-auto">
|
}
|
||||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
>
|
||||||
<span className="font-normal">이상</span>
|
<div className="flex flex-wrap gap-4">
|
||||||
</Field>
|
<FieldLabel
|
||||||
</FieldLabel>
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
</div>
|
className="!w-auto border-none"
|
||||||
</RadioGroup>
|
>
|
||||||
</FieldSet>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
</TableCell>
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
</TableRow>
|
<span className="font-normal">정상</span>
|
||||||
<TableRow>
|
</Field>
|
||||||
<TableHead scope="row">첨부파일</TableHead>
|
</FieldLabel>
|
||||||
<TableCell>
|
<FieldLabel
|
||||||
{uploadedFile ? (
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
<div className="flex items-center gap-2">
|
className="!w-auto border-none"
|
||||||
<TextShimmer className="text-xl" active={uploading}>
|
>
|
||||||
{uploadedFile.fileName}
|
<Field orientation="horizontal" className="w-auto">
|
||||||
</TextShimmer>
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
<Button
|
<span className="font-normal">이상</span>
|
||||||
size="icon"
|
</Field>
|
||||||
onClick={(e) => {
|
</FieldLabel>
|
||||||
e.preventDefault();
|
</div>
|
||||||
void handleFileRemove();
|
</RadioGroup>
|
||||||
}}
|
</FieldSet>
|
||||||
disabled={uploading}
|
</TableCell>
|
||||||
className="size-8 !bg-destructive"
|
</TableRow>
|
||||||
>
|
<TableRow>
|
||||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
<TableHead scope="row">첨부파일</TableHead>
|
||||||
</Button>
|
<TableCell>
|
||||||
</div>
|
{uploadedFile ? (
|
||||||
) : (
|
<div className="flex items-center gap-2">
|
||||||
<label>
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
<input
|
{uploadedFile.fileName}
|
||||||
type="file"
|
</TextShimmer>
|
||||||
className="hidden"
|
<Button
|
||||||
onChange={(event) => {
|
size="icon"
|
||||||
void handleFileUpload(event.target.files);
|
onClick={(e) => {
|
||||||
event.target.value = "";
|
e.preventDefault();
|
||||||
}}
|
void handleFileRemove();
|
||||||
/>
|
}}
|
||||||
<Button size="sm" variant="outline" type="button" asChild>
|
disabled={uploading}
|
||||||
<span>파일 선택</span>
|
className="size-8 !bg-destructive"
|
||||||
</Button>
|
>
|
||||||
</label>
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
)}
|
</Button>
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
) : (
|
||||||
</TableHeader>
|
<label>
|
||||||
</Table>
|
<input
|
||||||
</div>
|
type="file"
|
||||||
);
|
className="hidden"
|
||||||
}
|
onChange={(event) => {
|
||||||
|
void handleFileUpload(event.target.files);
|
||||||
return (
|
event.target.value = "";
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
}}
|
||||||
<div className="space-y-6">
|
/>
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
<Button size="sm" variant="outline" type="button" asChild>
|
||||||
{renderOpinionForm("KECO")}
|
<span>파일 선택</span>
|
||||||
</div>
|
</Button>
|
||||||
</SectionCollapsible>
|
</label>
|
||||||
);
|
)}
|
||||||
});
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
</TableHeader>
|
||||||
return (
|
</Table>
|
||||||
<div className="overflow-x-auto">
|
</div>
|
||||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
);
|
||||||
<colgroup>
|
}
|
||||||
<col width="10%" />
|
|
||||||
<col width="55%" />
|
return (
|
||||||
<col width="5%" />
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
<col width="20%" />
|
<div className="space-y-6">
|
||||||
<col width="10%" />
|
{kecoRvwList.length > 0 ? (
|
||||||
</colgroup>
|
<TechReviewOpinionTable items={kecoRvwList} isReviewTurn={isReviewTurn("KECO")} />
|
||||||
<TableHeader>
|
) : null}
|
||||||
<TableRow>
|
{renderOpinionForm("KECO")}
|
||||||
<TableHead>의견자명</TableHead>
|
</div>
|
||||||
<TableHead>의견내용</TableHead>
|
</SectionCollapsible>
|
||||||
<TableHead>검토결과</TableHead>
|
);
|
||||||
<TableHead>첨부파일</TableHead>
|
});
|
||||||
<TableHead>등록일시</TableHead>
|
|
||||||
</TableRow>
|
function getRvwOpnnIds(item: DesignCnstnRvwOpnnResponseDto) {
|
||||||
</TableHeader>
|
if (!item.designCnstnDocNo || item.rvwOpnnSn == null) {
|
||||||
<TableBody>
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
{items.map((item) => (
|
}
|
||||||
<TableRow key={`${item.designCnstnDocNo}_${item.rvwOpnnSn}`}>
|
return [item.designCnstnDocNo, item.rvwOpnnSn] as const;
|
||||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
}
|
||||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
|
||||||
<TableCell align="center">
|
function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableProps) {
|
||||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
const queryClient = useQueryClient();
|
||||||
</TableCell>
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
<TableCell align="center">
|
|
||||||
{item.atchFileInfo?.atflCd ? (
|
return (
|
||||||
<AttachmentDownloadLink
|
<RvwOpnnListTable
|
||||||
atflCd={item.atchFileInfo.atflCd}
|
items={items}
|
||||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
getRowKey={(item) => `${item.designCnstnDocNo}_${item.rvwOpnnSn}`}
|
||||||
/>
|
currentUserUuid={currentUserUuid}
|
||||||
) : (
|
isReviewTurn={isReviewTurn}
|
||||||
"-"
|
onUpdate={async (item, body) => {
|
||||||
)}
|
await designCnstnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
</TableCell>
|
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||||
<TableCell align="center">
|
}}
|
||||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
onDelete={async (item) => {
|
||||||
</TableCell>
|
await designCnstnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
</TableRow>
|
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||||
))}
|
}}
|
||||||
</TableBody>
|
/>
|
||||||
</Table>
|
);
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
}
|
export { TechReviewOpinionSection };
|
||||||
|
|
||||||
export { TechReviewOpinionSection };
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function EsntlAtchFileList({
|
|||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
dataTableOptions: {
|
||||||
caption: "필수 첨부파일 관리 목록",
|
caption: "필수첨부파일 목록으로 필수첨부파일아이디, 문서구분, 필수첨부파일명, 필수첨부여부, 비고, 양식 내려받기, 관리 정보 제공",
|
||||||
viewPagination: true,
|
viewPagination: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -136,14 +136,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 +159,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}
|
||||||
|
|||||||
@@ -1,203 +1,230 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
|
||||||
import { dayjs } from "@/shared/lib";
|
import { dayjs } from "@/shared/lib";
|
||||||
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
|
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
|
||||||
import { Badge } from "@common/components";
|
import { Badge } from "@common/components";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<FnrsCnsltnMngResponseDto>();
|
const columnHelper = createColumnHelper<FnrsCnsltnMngResponseDto>();
|
||||||
|
|
||||||
const MAIN_STEP_LABELS = {
|
const MAIN_STEP_LABELS = {
|
||||||
apply: "재원협의 신청",
|
apply: "재원협의 신청",
|
||||||
envoReview: "재원협의 검토",
|
envoReview: "재원협의 검토",
|
||||||
techReview: "기술검토",
|
techReview: "기술검토",
|
||||||
reply: "검토결과 회신",
|
reply: "검토결과 회신",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PROGRESS_STATUS_COLUMNS = [
|
export const PROGRESS_STATUS_COLUMNS = [
|
||||||
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
|
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
|
||||||
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
|
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
|
||||||
{ label: "재원협의 검토", sttsComCd: "ER" },
|
{ label: "재원협의 검토", sttsComCd: "ER" },
|
||||||
{ label: "기술검토", sttsComCd: "TE" },
|
{ label: "기술검토", sttsComCd: "TE" },
|
||||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||||
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
|
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type BadgeState = "completed" | "current" | "pending";
|
type BadgeState = "completed" | "current" | "pending";
|
||||||
|
|
||||||
interface ProgressBadge {
|
interface ProgressBadge {
|
||||||
label: string;
|
label: string;
|
||||||
state: BadgeState;
|
state: BadgeState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||||
if (prgrsSttsComCd === "SV") {
|
if (prgrsSttsComCd === "SV") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "ST") {
|
if (prgrsSttsComCd === "ST") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "LE") {
|
if (prgrsSttsComCd === "LE") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["FV", "ER", "AC", "TE", "RB", "RA", "AV", "EV", "RK", "RR", "SL"].includes(prgrsSttsComCd)) {
|
if (["FV", "ER", "AC", "TE", "RB", "RA", "AV", "EV", "RK", "RR", "SL"].includes(prgrsSttsComCd)) {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "TP") {
|
if (prgrsSttsComCd === "TP") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{
|
{
|
||||||
label: MAIN_STEP_LABELS.techReview,
|
label: MAIN_STEP_LABELS.techReview,
|
||||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||||
},
|
},
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "EP" || prgrsSttsComCd === "TA") {
|
if (prgrsSttsComCd === "EP" || prgrsSttsComCd === "TA") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{
|
{
|
||||||
label: MAIN_STEP_LABELS.techReview,
|
label: MAIN_STEP_LABELS.techReview,
|
||||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||||
},
|
},
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||||
if (prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "SL") {
|
||||||
return <Badge color="orange">보완요청</Badge>;
|
return <Badge color="orange">보완요청</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBadgeColor(state: BadgeState) {
|
function getBadgeColor(state: BadgeState) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "completed":
|
case "completed":
|
||||||
return "green";
|
return "green";
|
||||||
case "current":
|
case "current":
|
||||||
return "blue";
|
return "blue";
|
||||||
case "pending":
|
case "pending":
|
||||||
return "lightGray";
|
return "lightGray";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOptional(value: string | null | undefined) {
|
function getBadgeStateText(state: BadgeState) {
|
||||||
return value?.trim() ? value : "-";
|
switch (state) {
|
||||||
}
|
case "completed":
|
||||||
|
return "완료";
|
||||||
interface ProgressStepBadgeProps {
|
case "current":
|
||||||
prgrsSttsComCd: string;
|
return "진행중";
|
||||||
techRplyHstryYn: "Y" | "N";
|
case "pending":
|
||||||
}
|
return "대기";
|
||||||
|
}
|
||||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
}
|
||||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
|
||||||
|
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||||
return (
|
function splitStepLabel(label: string) {
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
const [title = "", ...rest] = label.split(" ");
|
||||||
{badges.map((badge, index) => (
|
return { title, detail: rest.join(" ") };
|
||||||
<Fragment key={badge.label}>
|
}
|
||||||
{index > 0 && <span className="mx-2">></span>}
|
|
||||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
function formatOptional(value: string | null | undefined) {
|
||||||
</Fragment>
|
return value?.trim() ? value : "-";
|
||||||
))}
|
}
|
||||||
</div>
|
|
||||||
);
|
interface ProgressStepBadgeProps {
|
||||||
}
|
prgrsSttsComCd: string;
|
||||||
|
techRplyHstryYn: "Y" | "N";
|
||||||
export function createFnrsCnsltnColumns() {
|
}
|
||||||
return [
|
|
||||||
columnHelper.accessor("sido", {
|
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||||
header: "시도",
|
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||||
meta: { align: "center" },
|
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
return (
|
||||||
}),
|
<div className="flex flex-wrap items-center justify-center">
|
||||||
columnHelper.accessor("gugun", {
|
{badges.map((badge, index) => (
|
||||||
header: "시군구",
|
<Fragment key={badge.label}>
|
||||||
meta: { align: "center" },
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||||
}),
|
<Badge
|
||||||
columnHelper.accessor("bizNm", {
|
color={getBadgeColor(badge.state)}
|
||||||
header: "사업명",
|
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||||
meta: { align: "left" },
|
>
|
||||||
cell: ({ row, getValue }) => (
|
<span>{splitStepLabel(badge.label).title}</span>
|
||||||
<>
|
<span>
|
||||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
{splitStepLabel(badge.label).detail}
|
||||||
</>
|
{getBadgeStateText(badge.state)}
|
||||||
),
|
</span>
|
||||||
}),
|
</Badge>
|
||||||
columnHelper.accessor("taskSeCdNm", {
|
</Fragment>
|
||||||
header: "업무구분",
|
))}
|
||||||
meta: { align: "center" },
|
</div>
|
||||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
);
|
||||||
}),
|
}
|
||||||
columnHelper.accessor("aplcntNm", {
|
|
||||||
header: "담당자",
|
export function createFnrsCnsltnColumns() {
|
||||||
meta: { align: "center" },
|
return [
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
columnHelper.accessor("sido", {
|
||||||
}),
|
header: "시도",
|
||||||
columnHelper.accessor("envoSeCdNm", {
|
meta: { align: "center" },
|
||||||
header: "해당기관",
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
meta: { align: "center" },
|
}),
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
columnHelper.accessor("gugun", {
|
||||||
}),
|
header: "시군구",
|
||||||
columnHelper.display({
|
meta: { align: "center" },
|
||||||
id: "progress",
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
header: "진행상황",
|
}),
|
||||||
meta: { align: "center" },
|
columnHelper.accessor("bizNm", {
|
||||||
cell: ({ row }) => (
|
header: "사업명",
|
||||||
<ProgressStepBadge
|
meta: { align: "left" },
|
||||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
cell: ({ row, getValue }) => (
|
||||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
<>
|
||||||
/>
|
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||||
),
|
</>
|
||||||
}),
|
),
|
||||||
columnHelper.accessor("regDt", {
|
}),
|
||||||
header: "신청일",
|
columnHelper.accessor("taskSeCdNm", {
|
||||||
meta: { align: "center" },
|
header: "업무구분",
|
||||||
cell: ({ getValue }) => {
|
meta: { align: "center" },
|
||||||
const value = getValue();
|
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
}),
|
||||||
},
|
columnHelper.accessor("aplcntNm", {
|
||||||
}),
|
header: "담당자",
|
||||||
];
|
meta: { align: "center" },
|
||||||
}
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("envoSeCdNm", {
|
||||||
|
header: "해당기관",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "progress",
|
||||||
|
header: "진행상황",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ProgressStepBadge
|
||||||
|
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||||
|
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("regDt", {
|
||||||
|
header: "신청일",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue();
|
||||||
|
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -1,352 +1,347 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
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 { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import {
|
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
||||||
AttachmentDownloadLink,
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
attachService,
|
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
|
||||||
useDownloadAttachmentByAtflId,
|
import {
|
||||||
} from "@/entities/attach";
|
fnrsCnsltnKeys,
|
||||||
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
|
type FnrsCnsltnRvwOpnnResponseDto,
|
||||||
import {
|
fnrsCnsltnRvwOpnnService,
|
||||||
RadioGroup,
|
} from "@/entities/fnrs-cnsltn";
|
||||||
RadioGroupItem,
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
Table,
|
import {
|
||||||
TableBody,
|
RadioGroup,
|
||||||
TableCell,
|
RadioGroupItem,
|
||||||
TableHead,
|
Table,
|
||||||
TableHeader,
|
TableCell,
|
||||||
TableRow,
|
TableHead,
|
||||||
Textarea,
|
TableHeader,
|
||||||
} from "@common/components";
|
TableRow,
|
||||||
|
Textarea,
|
||||||
import type {
|
} from "@common/components";
|
||||||
ReviewOpinionFormData,
|
|
||||||
ReviewOpinionGetFormDataResult,
|
import type {
|
||||||
} from "../../lib/review-opinion-utils";
|
ReviewOpinionFormData,
|
||||||
import type { ReviewStepCode } from "../../model/view-types";
|
ReviewOpinionGetFormDataResult,
|
||||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
} from "../../lib/review-opinion-utils";
|
||||||
|
import type { ReviewStepCode } from "../../model/view-types";
|
||||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
|
|
||||||
interface TechReviewOpinionSectionProps {
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
|
||||||
editableReviewStep?: ReviewStepCode;
|
interface TechReviewOpinionSectionProps {
|
||||||
atrzSeComCdList?: string[];
|
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
||||||
isAtrzLoading?: boolean;
|
editableReviewStep?: ReviewStepCode;
|
||||||
}
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
export interface TechReviewOpinionSectionHandle {
|
}
|
||||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
|
||||||
resetForm: () => void;
|
export interface TechReviewOpinionSectionHandle {
|
||||||
}
|
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||||
|
resetForm: () => void;
|
||||||
interface TechReviewOpinionTableProps {
|
}
|
||||||
items: FnrsCnsltnRvwOpnnResponseDto[];
|
|
||||||
}
|
interface TechReviewOpinionTableProps {
|
||||||
|
items: FnrsCnsltnRvwOpnnResponseDto[];
|
||||||
interface UploadedOpinionFile {
|
isReviewTurn: boolean;
|
||||||
fileName: string;
|
}
|
||||||
fileKey: string;
|
|
||||||
atflId?: string;
|
interface UploadedOpinionFile {
|
||||||
atflCd?: string;
|
fileName: string;
|
||||||
}
|
fileKey: string;
|
||||||
|
atflId?: string;
|
||||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
atflCd?: string;
|
||||||
rvwRsltCn: "",
|
}
|
||||||
rvwRsltAnmlyYn: "",
|
|
||||||
atchFileGroupId: "",
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
opnrSeComCd: "",
|
rvwRsltCn: "",
|
||||||
};
|
rvwRsltAnmlyYn: "",
|
||||||
|
atchFileGroupId: "",
|
||||||
function createRvwOpnnUploadKey(file: File) {
|
opnrSeComCd: "",
|
||||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
};
|
||||||
}
|
|
||||||
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
}
|
||||||
}
|
|
||||||
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
const TechReviewOpinionSection = forwardRef<
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
TechReviewOpinionSectionHandle,
|
}
|
||||||
TechReviewOpinionSectionProps
|
|
||||||
>(function TechReviewOpinionSection(
|
const TechReviewOpinionSection = forwardRef<
|
||||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
TechReviewOpinionSectionHandle,
|
||||||
ref,
|
TechReviewOpinionSectionProps
|
||||||
) {
|
>(function TechReviewOpinionSection(
|
||||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
ref,
|
||||||
...EMPTY_FORM_DATA,
|
) {
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
});
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
...EMPTY_FORM_DATA,
|
||||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
|
});
|
||||||
useEffect(() => {
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
setFormData((prev) => ({
|
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||||
...prev,
|
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
useEffect(() => {
|
||||||
}));
|
setFormData((prev) => ({
|
||||||
}, [editableReviewStep]);
|
...prev,
|
||||||
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
}));
|
||||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
}, [editableReviewStep]);
|
||||||
if (!atflCd || !atflId) return;
|
|
||||||
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
if (!atflCd || !atflId) return;
|
||||||
},
|
|
||||||
onError: (_error, _file, fileKey) => {
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
},
|
},
|
||||||
});
|
onError: (_error, _file, fileKey) => {
|
||||||
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
useImperativeHandle(
|
},
|
||||||
ref,
|
});
|
||||||
() => ({
|
|
||||||
getFormData: () => {
|
useImperativeHandle(
|
||||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
ref,
|
||||||
return null;
|
() => ({
|
||||||
}
|
getFormData: () => {
|
||||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
return {
|
return null;
|
||||||
formData: {
|
}
|
||||||
...formData,
|
|
||||||
opnrSeComCd: editableReviewStep,
|
return {
|
||||||
},
|
formData: {
|
||||||
rvwList,
|
...formData,
|
||||||
};
|
opnrSeComCd: editableReviewStep,
|
||||||
},
|
},
|
||||||
resetForm: () => {
|
rvwList,
|
||||||
setFormData({
|
};
|
||||||
...EMPTY_FORM_DATA,
|
},
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
resetForm: () => {
|
||||||
});
|
setFormData({
|
||||||
setUploadedFile(null);
|
...EMPTY_FORM_DATA,
|
||||||
},
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
}),
|
});
|
||||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
setUploadedFile(null);
|
||||||
);
|
},
|
||||||
|
}),
|
||||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
);
|
||||||
|
|
||||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
|
|
||||||
if (!hasEnvSection && !hasKecoSection) {
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
return null;
|
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
||||||
}
|
|
||||||
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
async function handleFileUpload(files: FileList | null) {
|
return null;
|
||||||
if (!files || files.length === 0) return;
|
}
|
||||||
|
|
||||||
const file = files[0];
|
async function handleFileUpload(files: FileList | null) {
|
||||||
const fileKey = createRvwOpnnUploadKey(file);
|
if (!files || files.length === 0) return;
|
||||||
const existingAtflCd = formData.atchFileGroupId;
|
|
||||||
|
const file = files[0];
|
||||||
setUploadedFile({ fileName: file.name, fileKey });
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
try {
|
|
||||||
await uploadFileAsync(file, {
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
fileKey,
|
|
||||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
try {
|
||||||
});
|
await uploadFileAsync(file, {
|
||||||
} catch {
|
fileKey,
|
||||||
// onError callback handles cleanup
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
}
|
});
|
||||||
}
|
} catch {
|
||||||
|
// onError callback handles cleanup
|
||||||
async function handleFileRemove() {
|
}
|
||||||
if (!uploadedFile) return;
|
}
|
||||||
|
|
||||||
uploads[uploadedFile.fileKey]?.abort();
|
async function handleFileRemove() {
|
||||||
|
if (!uploadedFile) return;
|
||||||
if (uploadedFile.atflId) {
|
|
||||||
try {
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
|
||||||
} catch (error) {
|
if (uploadedFile.atflId) {
|
||||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
try {
|
||||||
}
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
setUploadedFile(null);
|
}
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
}
|
||||||
}
|
|
||||||
|
setUploadedFile(null);
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
}
|
||||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
|
||||||
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
if (editableReviewStep !== reviewStep) return false;
|
||||||
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||||
return (
|
return true;
|
||||||
<div className="space-y-2">
|
}
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
|
||||||
<Table caption="검토의견 입력" variant="secondary">
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
<TableHeader>
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
<TableCell>
|
|
||||||
<Textarea
|
return (
|
||||||
value={formData.rvwRsltCn}
|
<div className="space-y-2">
|
||||||
onChange={(event) =>
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
<Table
|
||||||
}
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
placeholder="의견내용을 입력하세요"
|
variant="secondary"
|
||||||
rows={4}
|
>
|
||||||
className="min-h-[8rem] !text-xl"
|
<TableHeader>
|
||||||
/>
|
<TableRow>
|
||||||
</TableCell>
|
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||||
</TableRow>
|
<TableCell>
|
||||||
<TableRow>
|
<Textarea
|
||||||
<TableHead>검토결과</TableHead>
|
value={formData.rvwRsltCn}
|
||||||
<TableCell>
|
onChange={(event) =>
|
||||||
<FieldSet>
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
<RadioGroup
|
}
|
||||||
value={formData.rvwRsltAnmlyYn}
|
placeholder="의견내용을 입력하세요"
|
||||||
onValueChange={(value) =>
|
rows={4}
|
||||||
setFormData((prev) => ({
|
className="min-h-[8rem] !text-xl"
|
||||||
...prev,
|
/>
|
||||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
</TableCell>
|
||||||
}))
|
</TableRow>
|
||||||
}
|
<TableRow>
|
||||||
>
|
<TableHead>검토결과</TableHead>
|
||||||
<div className="flex flex-wrap gap-4">
|
<TableCell>
|
||||||
<FieldLabel
|
<FieldSet>
|
||||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
<RadioGroup
|
||||||
className="!w-auto border-none"
|
value={formData.rvwRsltAnmlyYn}
|
||||||
>
|
onValueChange={(value) =>
|
||||||
<Field orientation="horizontal" className="w-auto">
|
setFormData((prev) => ({
|
||||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
...prev,
|
||||||
<span className="font-normal">정상</span>
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
</Field>
|
}))
|
||||||
</FieldLabel>
|
}
|
||||||
<FieldLabel
|
>
|
||||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
<div className="flex flex-wrap gap-4">
|
||||||
className="!w-auto border-none"
|
<FieldLabel
|
||||||
>
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
<Field orientation="horizontal" className="w-auto">
|
className="!w-auto border-none"
|
||||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
>
|
||||||
<span className="font-normal">이상</span>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
</Field>
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
</FieldLabel>
|
<span className="font-normal">정상</span>
|
||||||
</div>
|
</Field>
|
||||||
</RadioGroup>
|
</FieldLabel>
|
||||||
</FieldSet>
|
<FieldLabel
|
||||||
</TableCell>
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
</TableRow>
|
className="!w-auto border-none"
|
||||||
<TableRow>
|
>
|
||||||
<TableHead>첨부파일</TableHead>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
<TableCell>
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
<AttachmentFileField
|
<span className="font-normal">이상</span>
|
||||||
files={
|
</Field>
|
||||||
uploadedFile
|
</FieldLabel>
|
||||||
? [
|
</div>
|
||||||
{
|
</RadioGroup>
|
||||||
fileKey: uploadedFile.fileKey,
|
</FieldSet>
|
||||||
fileName: uploadedFile.fileName,
|
</TableCell>
|
||||||
onDownload: uploadedFile.atflId
|
</TableRow>
|
||||||
? () => downloadByAtflId(uploadedFile.atflId!)
|
<TableRow>
|
||||||
: undefined,
|
<TableHead>첨부파일</TableHead>
|
||||||
},
|
<TableCell>
|
||||||
]
|
<AttachmentFileField
|
||||||
: []
|
files={
|
||||||
}
|
uploadedFile
|
||||||
inputId={`rvw-opnn-${reviewStep}-file-input`}
|
? [
|
||||||
isUploadingByKey={isUploading}
|
{
|
||||||
multiple={false}
|
fileKey: uploadedFile.fileKey,
|
||||||
showAddButton={!uploadedFile}
|
fileName: uploadedFile.fileName,
|
||||||
addDisabled={uploading}
|
onDownload: uploadedFile.atflId
|
||||||
onAdd={(fileList) => void handleFileUpload(fileList)}
|
? () => downloadByAtflId(uploadedFile.atflId!)
|
||||||
onRemove={() => void handleFileRemove()}
|
: undefined,
|
||||||
/>
|
},
|
||||||
</TableCell>
|
]
|
||||||
</TableRow>
|
: []
|
||||||
</TableHeader>
|
}
|
||||||
</Table>
|
inputId={`rvw-opnn-${reviewStep}-file-input`}
|
||||||
</div>
|
isUploadingByKey={isUploading}
|
||||||
);
|
multiple={false}
|
||||||
}
|
showAddButton={!uploadedFile}
|
||||||
|
addDisabled={uploading}
|
||||||
return (
|
onAdd={(fileList) => void handleFileUpload(fileList)}
|
||||||
<>
|
onRemove={() => void handleFileRemove()}
|
||||||
{hasEnvSection ? (
|
/>
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
</TableCell>
|
||||||
<div className="space-y-6">
|
</TableRow>
|
||||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
</TableHeader>
|
||||||
{renderOpinionForm("ENV")}
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</SectionCollapsible>
|
);
|
||||||
) : null}
|
}
|
||||||
{hasKecoSection ? (
|
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
return (
|
||||||
<div className="space-y-6">
|
<>
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
{hasEnvSection ? (
|
||||||
{/*
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
재원협의에서 환경공단 검토의견은 자동 생성
|
<div className="space-y-6">
|
||||||
{renderOpinionForm("KECO")}
|
{envRvwList.length > 0 ? (
|
||||||
*/}
|
<TechReviewOpinionTable items={envRvwList} isReviewTurn={isReviewTurn("ENV")} />
|
||||||
</div>
|
) : null}
|
||||||
</SectionCollapsible>
|
{renderOpinionForm("ENV")}
|
||||||
) : null}
|
</div>
|
||||||
</>
|
</SectionCollapsible>
|
||||||
);
|
) : null}
|
||||||
});
|
{hasKecoSection ? (
|
||||||
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
<div className="space-y-6">
|
||||||
return (
|
{kecoRvwList.length > 0 ? (
|
||||||
<div className="overflow-x-auto">
|
<TechReviewOpinionTable items={kecoRvwList} isReviewTurn={isReviewTurn("KECO")} />
|
||||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
) : null}
|
||||||
<colgroup>
|
{/*
|
||||||
<col width="10%" />
|
재원협의에서 환경공단 검토의견은 자동 생성
|
||||||
<col width="55%" />
|
{renderOpinionForm("KECO")}
|
||||||
<col width="5%" />
|
*/}
|
||||||
<col width="20%" />
|
</div>
|
||||||
<col width="10%" />
|
</SectionCollapsible>
|
||||||
</colgroup>
|
) : null}
|
||||||
<TableHeader>
|
</>
|
||||||
<TableRow>
|
);
|
||||||
<TableHead>의견자명</TableHead>
|
});
|
||||||
<TableHead>의견내용</TableHead>
|
|
||||||
<TableHead>검토결과</TableHead>
|
function getRvwOpnnIds(item: FnrsCnsltnRvwOpnnResponseDto) {
|
||||||
<TableHead>첨부파일</TableHead>
|
if (!item.fnrsCnsltnDocNo || item.rvwOpnnSn == null) {
|
||||||
<TableHead>등록일시</TableHead>
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
</TableRow>
|
}
|
||||||
</TableHeader>
|
return [item.fnrsCnsltnDocNo, item.rvwOpnnSn] as const;
|
||||||
<TableBody>
|
}
|
||||||
{items.map((item) => (
|
|
||||||
<TableRow key={`${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}>
|
function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableProps) {
|
||||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
const queryClient = useQueryClient();
|
||||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
<TableCell align="center">
|
|
||||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
return (
|
||||||
</TableCell>
|
<RvwOpnnListTable
|
||||||
<TableCell align="center">
|
items={items}
|
||||||
{item.atchFileInfo?.atflCd ? (
|
getRowKey={(item) => `${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}
|
||||||
<AttachmentDownloadLink
|
currentUserUuid={currentUserUuid}
|
||||||
atflCd={item.atchFileInfo.atflCd}
|
isReviewTurn={isReviewTurn}
|
||||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
onUpdate={async (item, body) => {
|
||||||
/>
|
await fnrsCnsltnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
) : (
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||||
"-"
|
}}
|
||||||
)}
|
onDelete={async (item) => {
|
||||||
</TableCell>
|
await fnrsCnsltnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
<TableCell align="center">
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
}}
|
||||||
</TableCell>
|
/>
|
||||||
</TableRow>
|
);
|
||||||
))}
|
}
|
||||||
</TableBody>
|
|
||||||
</Table>
|
export { TechReviewOpinionSection };
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { TechReviewOpinionSection };
|
|
||||||
|
|||||||
@@ -1,204 +1,231 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
|
||||||
import { dayjs } from "@/shared/lib";
|
import { dayjs } from "@/shared/lib";
|
||||||
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
|
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
|
||||||
import { Badge } from "@common/components";
|
import { Badge } from "@common/components";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<InstlSncnMngResponseDto>();
|
const columnHelper = createColumnHelper<InstlSncnMngResponseDto>();
|
||||||
|
|
||||||
const MAIN_STEP_LABELS = {
|
const MAIN_STEP_LABELS = {
|
||||||
apply: "설치인가 신청",
|
apply: "설치인가 신청",
|
||||||
envoReview: "설치인가 검토",
|
envoReview: "설치인가 검토",
|
||||||
techReview: "기술검토",
|
techReview: "기술검토",
|
||||||
reply: "검토결과 회신",
|
reply: "검토결과 회신",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PROGRESS_STATUS_COLUMNS = [
|
export const PROGRESS_STATUS_COLUMNS = [
|
||||||
{ label: "설치인가 신청 (시군)", sttsComCd: "ST" },
|
{ label: "설치인가 신청 (시군)", sttsComCd: "ST" },
|
||||||
{ label: "설치인가 신청 (시도)", sttsComCd: "LE" },
|
{ label: "설치인가 신청 (시도)", sttsComCd: "LE" },
|
||||||
{ label: "설치인가 검토", sttsComCd: "ER" },
|
{ label: "설치인가 검토", sttsComCd: "ER" },
|
||||||
{ label: "기술검토", sttsComCd: "TE" },
|
{ label: "기술검토", sttsComCd: "TE" },
|
||||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||||
{ label: "검토결과 회신", sttsComCd: "EP" },
|
{ label: "검토결과 회신", sttsComCd: "EP" },
|
||||||
{ label: "설치인가 승인", sttsComCd: "TA" },
|
{ label: "설치인가 승인", sttsComCd: "TA" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type BadgeState = "completed" | "current" | "pending";
|
type BadgeState = "completed" | "current" | "pending";
|
||||||
|
|
||||||
interface ProgressBadge {
|
interface ProgressBadge {
|
||||||
label: string;
|
label: string;
|
||||||
state: BadgeState;
|
state: BadgeState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||||
if (prgrsSttsComCd === "SV") {
|
if (prgrsSttsComCd === "SV") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "ST") {
|
if (prgrsSttsComCd === "ST") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "LE") {
|
if (prgrsSttsComCd === "LE") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "TP" || prgrsSttsComCd === "EP") {
|
if (prgrsSttsComCd === "TP" || prgrsSttsComCd === "EP") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{
|
{
|
||||||
label: MAIN_STEP_LABELS.techReview,
|
label: MAIN_STEP_LABELS.techReview,
|
||||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||||
},
|
},
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
|
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{
|
{
|
||||||
label: MAIN_STEP_LABELS.techReview,
|
label: MAIN_STEP_LABELS.techReview,
|
||||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||||
},
|
},
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||||
if (prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "SL") {
|
||||||
return <Badge color="orange">보완요청</Badge>;
|
return <Badge color="orange">보완요청</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBadgeColor(state: BadgeState) {
|
function getBadgeColor(state: BadgeState) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "completed":
|
case "completed":
|
||||||
return "green";
|
return "green";
|
||||||
case "current":
|
case "current":
|
||||||
return "blue";
|
return "blue";
|
||||||
case "pending":
|
case "pending":
|
||||||
return "lightGray";
|
return "lightGray";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOptional(value: string | null | undefined) {
|
function getBadgeStateText(state: BadgeState) {
|
||||||
return value?.trim() ? value : "-";
|
switch (state) {
|
||||||
}
|
case "completed":
|
||||||
|
return "완료";
|
||||||
interface ProgressStepBadgeProps {
|
case "current":
|
||||||
prgrsSttsComCd: string;
|
return "진행중";
|
||||||
techRplyHstryYn: "Y" | "N";
|
case "pending":
|
||||||
}
|
return "대기";
|
||||||
|
}
|
||||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
}
|
||||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
|
||||||
|
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||||
return (
|
function splitStepLabel(label: string) {
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
const [title = "", ...rest] = label.split(" ");
|
||||||
{badges.map((badge, index) => (
|
return { title, detail: rest.join(" ") };
|
||||||
<Fragment key={badge.label}>
|
}
|
||||||
{index > 0 && <span className="mx-2">></span>}
|
|
||||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
function formatOptional(value: string | null | undefined) {
|
||||||
</Fragment>
|
return value?.trim() ? value : "-";
|
||||||
))}
|
}
|
||||||
</div>
|
|
||||||
);
|
interface ProgressStepBadgeProps {
|
||||||
}
|
prgrsSttsComCd: string;
|
||||||
|
techRplyHstryYn: "Y" | "N";
|
||||||
export function createInstlSncnColumns() {
|
}
|
||||||
return [
|
|
||||||
columnHelper.accessor("sido", {
|
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||||
header: "시도",
|
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||||
meta: { align: "center" },
|
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
return (
|
||||||
}),
|
<div className="flex flex-wrap items-center justify-center">
|
||||||
columnHelper.accessor("gugun", {
|
{badges.map((badge, index) => (
|
||||||
header: "시군구",
|
<Fragment key={badge.label}>
|
||||||
meta: { align: "center" },
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||||
}),
|
<Badge
|
||||||
columnHelper.accessor("bizNm", {
|
color={getBadgeColor(badge.state)}
|
||||||
header: "사업명",
|
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||||
meta: { align: "left" },
|
>
|
||||||
cell: ({ row, getValue }) => (
|
<span>{splitStepLabel(badge.label).title}</span>
|
||||||
<>
|
<span>
|
||||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
{splitStepLabel(badge.label).detail}
|
||||||
</>
|
{getBadgeStateText(badge.state)}
|
||||||
),
|
</span>
|
||||||
}),
|
</Badge>
|
||||||
columnHelper.accessor("taskSeCdNm", {
|
</Fragment>
|
||||||
header: "업무구분",
|
))}
|
||||||
meta: { align: "center" },
|
</div>
|
||||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
);
|
||||||
}),
|
}
|
||||||
columnHelper.accessor("aplcntNm", {
|
|
||||||
header: "담당자",
|
export function createInstlSncnColumns() {
|
||||||
meta: { align: "center" },
|
return [
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
columnHelper.accessor("sido", {
|
||||||
}),
|
header: "시도",
|
||||||
columnHelper.accessor("envoSeCdNm", {
|
meta: { align: "center" },
|
||||||
header: "해당기관",
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
meta: { align: "center" },
|
}),
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
columnHelper.accessor("gugun", {
|
||||||
}),
|
header: "시군구",
|
||||||
columnHelper.display({
|
meta: { align: "center" },
|
||||||
id: "progress",
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
header: "진행상황",
|
}),
|
||||||
meta: { align: "center" },
|
columnHelper.accessor("bizNm", {
|
||||||
cell: ({ row }) => (
|
header: "사업명",
|
||||||
<ProgressStepBadge
|
meta: { align: "left" },
|
||||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
cell: ({ row, getValue }) => (
|
||||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
<>
|
||||||
/>
|
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||||
),
|
</>
|
||||||
}),
|
),
|
||||||
columnHelper.accessor("regDt", {
|
}),
|
||||||
header: "신청일",
|
columnHelper.accessor("taskSeCdNm", {
|
||||||
meta: { align: "center" },
|
header: "업무구분",
|
||||||
cell: ({ getValue }) => {
|
meta: { align: "center" },
|
||||||
const value = getValue();
|
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
}),
|
||||||
},
|
columnHelper.accessor("aplcntNm", {
|
||||||
}),
|
header: "담당자",
|
||||||
];
|
meta: { align: "center" },
|
||||||
}
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("envoSeCdNm", {
|
||||||
|
header: "해당기관",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "progress",
|
||||||
|
header: "진행상황",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ProgressStepBadge
|
||||||
|
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||||
|
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("regDt", {
|
||||||
|
header: "신청일",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue();
|
||||||
|
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,350 +1,352 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
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 { TextShimmer } from "@/shared/ui";
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
import { TextShimmer } from "@/shared/ui";
|
||||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
|
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||||
import {
|
import { attachService } from "@/entities/attach";
|
||||||
Button,
|
import {
|
||||||
RadioGroup,
|
instlSncnKeys,
|
||||||
RadioGroupItem,
|
type InstlSncnRvwOpnnResponseDto,
|
||||||
Table,
|
instlSncnRvwOpnnService,
|
||||||
TableBody,
|
type ReviewStepCode,
|
||||||
TableCell,
|
} from "@/entities/instl-sncn";
|
||||||
TableHead,
|
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||||
TableHeader,
|
import {
|
||||||
TableRow,
|
Button,
|
||||||
Textarea,
|
RadioGroup,
|
||||||
} from "@common/components";
|
RadioGroupItem,
|
||||||
|
Table,
|
||||||
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
|
TableCell,
|
||||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
TableRow,
|
||||||
|
Textarea,
|
||||||
interface TechReviewOpinionSectionProps {
|
} from "@common/components";
|
||||||
rvwList: InstlSncnRvwOpnnResponseDto[];
|
|
||||||
editableReviewStep?: ReviewStepCode;
|
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
|
||||||
atrzSeComCdList?: string[];
|
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||||
isAtrzLoading?: boolean;
|
|
||||||
}
|
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||||
|
|
||||||
export interface TechReviewOpinionSectionHandle {
|
interface TechReviewOpinionSectionProps {
|
||||||
getFormData: () => ReviewOpinionFormData | null;
|
rvwList: InstlSncnRvwOpnnResponseDto[];
|
||||||
resetForm: () => void;
|
editableReviewStep?: ReviewStepCode;
|
||||||
}
|
atrzSeComCdList?: string[];
|
||||||
|
isAtrzLoading?: boolean;
|
||||||
interface TechReviewOpinionTableProps {
|
}
|
||||||
items: InstlSncnRvwOpnnResponseDto[];
|
|
||||||
}
|
export interface TechReviewOpinionSectionHandle {
|
||||||
|
getFormData: () => ReviewOpinionFormData | null;
|
||||||
interface UploadedOpinionFile {
|
resetForm: () => void;
|
||||||
fileName: string;
|
}
|
||||||
fileKey: string;
|
|
||||||
atflId?: string;
|
interface TechReviewOpinionTableProps {
|
||||||
atflCd?: string;
|
items: InstlSncnRvwOpnnResponseDto[];
|
||||||
}
|
isReviewTurn: boolean;
|
||||||
|
}
|
||||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
|
||||||
rvwRsltCn: "",
|
interface UploadedOpinionFile {
|
||||||
rvwRsltAnmlyYn: "",
|
fileName: string;
|
||||||
atchFileGroupId: "",
|
fileKey: string;
|
||||||
opnrSeComCd: "",
|
atflId?: string;
|
||||||
};
|
atflCd?: string;
|
||||||
|
}
|
||||||
function createRvwOpnnUploadKey(file: File) {
|
|
||||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||||
}
|
rvwRsltCn: "",
|
||||||
|
rvwRsltAnmlyYn: "",
|
||||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
atchFileGroupId: "",
|
||||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
opnrSeComCd: "",
|
||||||
}
|
};
|
||||||
|
|
||||||
const TechReviewOpinionSection = forwardRef<
|
function createRvwOpnnUploadKey(file: File) {
|
||||||
TechReviewOpinionSectionHandle,
|
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||||
TechReviewOpinionSectionProps
|
}
|
||||||
>(function TechReviewOpinionSection(
|
|
||||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||||
ref,
|
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||||
) {
|
}
|
||||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
|
||||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
const TechReviewOpinionSection = forwardRef<
|
||||||
...EMPTY_FORM_DATA,
|
TechReviewOpinionSectionHandle,
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
TechReviewOpinionSectionProps
|
||||||
});
|
>(function TechReviewOpinionSection(
|
||||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||||
|
ref,
|
||||||
useEffect(() => {
|
) {
|
||||||
setFormData((prev) => ({
|
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||||
...prev,
|
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
...EMPTY_FORM_DATA,
|
||||||
}));
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
}, [editableReviewStep]);
|
});
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
|
||||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
useEffect(() => {
|
||||||
if (!atflCd || !atflId) return;
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
}));
|
||||||
},
|
}, [editableReviewStep]);
|
||||||
onError: (_error, _file, fileKey) => {
|
|
||||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||||
},
|
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||||
});
|
if (!atflCd || !atflId) return;
|
||||||
|
|
||||||
useImperativeHandle(
|
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||||
ref,
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||||
() => ({
|
},
|
||||||
getFormData: () => {
|
onError: (_error, _file, fileKey) => {
|
||||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||||
return null;
|
},
|
||||||
}
|
});
|
||||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
|
||||||
return null;
|
useImperativeHandle(
|
||||||
}
|
ref,
|
||||||
|
() => ({
|
||||||
return {
|
getFormData: () => {
|
||||||
...formData,
|
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||||
opnrSeComCd: editableReviewStep,
|
return null;
|
||||||
};
|
}
|
||||||
},
|
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||||
resetForm: () => {
|
return null;
|
||||||
setFormData({
|
}
|
||||||
...EMPTY_FORM_DATA,
|
|
||||||
opnrSeComCd: editableReviewStep ?? "",
|
return {
|
||||||
});
|
...formData,
|
||||||
setUploadedFile(null);
|
opnrSeComCd: editableReviewStep,
|
||||||
},
|
};
|
||||||
}),
|
},
|
||||||
[canWriteKecoOpinion, editableReviewStep, formData],
|
resetForm: () => {
|
||||||
);
|
setFormData({
|
||||||
|
...EMPTY_FORM_DATA,
|
||||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
opnrSeComCd: editableReviewStep ?? "",
|
||||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
});
|
||||||
|
setUploadedFile(null);
|
||||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
},
|
||||||
const hasKecoSection =
|
}),
|
||||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
[canWriteKecoOpinion, editableReviewStep, formData],
|
||||||
|
);
|
||||||
if (!hasEnvSection && !hasKecoSection) {
|
|
||||||
return null;
|
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||||
}
|
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||||
|
|
||||||
async function handleFileUpload(files: FileList | null) {
|
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||||
if (!files || files.length === 0) return;
|
const hasKecoSection =
|
||||||
|
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||||
const file = files[0];
|
|
||||||
const fileKey = createRvwOpnnUploadKey(file);
|
if (!hasEnvSection && !hasKecoSection) {
|
||||||
const existingAtflCd = formData.atchFileGroupId;
|
return null;
|
||||||
|
}
|
||||||
setUploadedFile({ fileName: file.name, fileKey });
|
|
||||||
|
async function handleFileUpload(files: FileList | null) {
|
||||||
try {
|
if (!files || files.length === 0) return;
|
||||||
await uploadFileAsync(file, {
|
|
||||||
fileKey,
|
const file = files[0];
|
||||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
const fileKey = createRvwOpnnUploadKey(file);
|
||||||
});
|
const existingAtflCd = formData.atchFileGroupId;
|
||||||
} catch {
|
|
||||||
// onError callback handles cleanup
|
setUploadedFile({ fileName: file.name, fileKey });
|
||||||
}
|
|
||||||
}
|
try {
|
||||||
|
await uploadFileAsync(file, {
|
||||||
async function handleFileRemove() {
|
fileKey,
|
||||||
if (!uploadedFile) return;
|
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||||
|
});
|
||||||
uploads[uploadedFile.fileKey]?.abort();
|
} catch {
|
||||||
|
// onError callback handles cleanup
|
||||||
if (uploadedFile.atflId) {
|
}
|
||||||
try {
|
}
|
||||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
|
||||||
} catch (error) {
|
async function handleFileRemove() {
|
||||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
if (!uploadedFile) return;
|
||||||
}
|
|
||||||
}
|
uploads[uploadedFile.fileKey]?.abort();
|
||||||
|
|
||||||
setUploadedFile(null);
|
if (uploadedFile.atflId) {
|
||||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
try {
|
||||||
}
|
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||||
|
} catch (error) {
|
||||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||||
if (editableReviewStep !== reviewStep) return null;
|
}
|
||||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
}
|
||||||
|
|
||||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
setUploadedFile(null);
|
||||||
|
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||||
return (
|
}
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||||
<Table caption="검토의견 입력" variant="secondary">
|
if (editableReviewStep !== reviewStep) return false;
|
||||||
<TableHeader>
|
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||||
<TableRow>
|
return true;
|
||||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
}
|
||||||
<TableCell>
|
|
||||||
<Textarea
|
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||||
value={formData.rvwRsltCn}
|
if (!isReviewTurn(reviewStep)) return null;
|
||||||
onChange={(event) =>
|
|
||||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||||
}
|
|
||||||
placeholder="의견내용을 입력하세요"
|
return (
|
||||||
rows={4}
|
<div className="space-y-2">
|
||||||
className="min-h-[8rem] !text-xl"
|
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||||
/>
|
<Table
|
||||||
</TableCell>
|
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||||
</TableRow>
|
variant="secondary"
|
||||||
<TableRow>
|
>
|
||||||
<TableHead scope="row">검토결과</TableHead>
|
<TableHeader>
|
||||||
<TableCell>
|
<TableRow>
|
||||||
<FieldSet>
|
<TableHead scope="row" className="w-[12rem]">
|
||||||
<RadioGroup
|
의견내용
|
||||||
value={formData.rvwRsltAnmlyYn}
|
</TableHead>
|
||||||
onValueChange={(value) =>
|
<TableCell>
|
||||||
setFormData((prev) => ({
|
<Textarea
|
||||||
...prev,
|
value={formData.rvwRsltCn}
|
||||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
onChange={(event) =>
|
||||||
}))
|
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||||
}
|
}
|
||||||
>
|
placeholder="의견내용을 입력하세요"
|
||||||
<div className="flex flex-wrap gap-4">
|
rows={4}
|
||||||
<FieldLabel
|
className="min-h-[8rem] !text-xl"
|
||||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
/>
|
||||||
className="!w-auto border-none"
|
</TableCell>
|
||||||
>
|
</TableRow>
|
||||||
<Field orientation="horizontal" className="w-auto">
|
<TableRow>
|
||||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
<TableHead scope="row">검토결과</TableHead>
|
||||||
<span className="font-normal">정상</span>
|
<TableCell>
|
||||||
</Field>
|
<FieldSet>
|
||||||
</FieldLabel>
|
<RadioGroup
|
||||||
<FieldLabel
|
value={formData.rvwRsltAnmlyYn}
|
||||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
onValueChange={(value) =>
|
||||||
className="!w-auto border-none"
|
setFormData((prev) => ({
|
||||||
>
|
...prev,
|
||||||
<Field orientation="horizontal" className="w-auto">
|
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
}))
|
||||||
<span className="font-normal">이상</span>
|
}
|
||||||
</Field>
|
>
|
||||||
</FieldLabel>
|
<div className="flex flex-wrap gap-4">
|
||||||
</div>
|
<FieldLabel
|
||||||
</RadioGroup>
|
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||||
</FieldSet>
|
className="!w-auto border-none"
|
||||||
</TableCell>
|
>
|
||||||
</TableRow>
|
<Field orientation="horizontal" className="w-auto">
|
||||||
<TableRow>
|
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||||
<TableHead scope="row">첨부파일</TableHead>
|
<span className="font-normal">정상</span>
|
||||||
<TableCell>
|
</Field>
|
||||||
{uploadedFile ? (
|
</FieldLabel>
|
||||||
<div className="flex items-center gap-2">
|
<FieldLabel
|
||||||
<TextShimmer className="text-xl" active={uploading}>
|
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||||
{uploadedFile.fileName}
|
className="!w-auto border-none"
|
||||||
</TextShimmer>
|
>
|
||||||
<Button
|
<Field orientation="horizontal" className="w-auto">
|
||||||
size="icon"
|
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||||
onClick={(e) => {
|
<span className="font-normal">이상</span>
|
||||||
e.preventDefault();
|
</Field>
|
||||||
void handleFileRemove();
|
</FieldLabel>
|
||||||
}}
|
</div>
|
||||||
disabled={uploading}
|
</RadioGroup>
|
||||||
className="size-8 !bg-destructive"
|
</FieldSet>
|
||||||
>
|
</TableCell>
|
||||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
</TableRow>
|
||||||
</Button>
|
<TableRow>
|
||||||
</div>
|
<TableHead scope="row">첨부파일</TableHead>
|
||||||
) : (
|
<TableCell>
|
||||||
<label>
|
{uploadedFile ? (
|
||||||
<input
|
<div className="flex items-center gap-2">
|
||||||
type="file"
|
<TextShimmer className="text-xl" active={uploading}>
|
||||||
className="hidden"
|
{uploadedFile.fileName}
|
||||||
onChange={(event) => {
|
</TextShimmer>
|
||||||
void handleFileUpload(event.target.files);
|
<Button
|
||||||
event.target.value = "";
|
size="icon"
|
||||||
}}
|
onClick={(e) => {
|
||||||
/>
|
e.preventDefault();
|
||||||
<Button size="sm" variant="outline" type="button" asChild>
|
void handleFileRemove();
|
||||||
<span>파일 선택</span>
|
}}
|
||||||
</Button>
|
disabled={uploading}
|
||||||
</label>
|
className="size-8 !bg-destructive"
|
||||||
)}
|
>
|
||||||
</TableCell>
|
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||||
</TableRow>
|
</Button>
|
||||||
</TableHeader>
|
</div>
|
||||||
</Table>
|
) : (
|
||||||
</div>
|
<label>
|
||||||
);
|
<input
|
||||||
}
|
type="file"
|
||||||
|
className="hidden"
|
||||||
return (
|
onChange={(event) => {
|
||||||
<>
|
void handleFileUpload(event.target.files);
|
||||||
{hasEnvSection ? (
|
event.target.value = "";
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
}}
|
||||||
<div className="space-y-6">
|
/>
|
||||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
<Button size="sm" variant="outline" type="button" asChild>
|
||||||
{renderOpinionForm("ENV")}
|
<span>파일 선택</span>
|
||||||
</div>
|
</Button>
|
||||||
</SectionCollapsible>
|
</label>
|
||||||
) : null}
|
)}
|
||||||
{hasKecoSection ? (
|
</TableCell>
|
||||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
</TableRow>
|
||||||
<div className="space-y-6">
|
</TableHeader>
|
||||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
</Table>
|
||||||
{renderOpinionForm("KECO")}
|
</div>
|
||||||
</div>
|
);
|
||||||
</SectionCollapsible>
|
}
|
||||||
) : null}
|
|
||||||
</>
|
return (
|
||||||
);
|
<>
|
||||||
});
|
{hasEnvSection ? (
|
||||||
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
<div className="space-y-6">
|
||||||
return (
|
{envRvwList.length > 0 ? (
|
||||||
<div className="overflow-x-auto">
|
<TechReviewOpinionTable items={envRvwList} isReviewTurn={isReviewTurn("ENV")} />
|
||||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
) : null}
|
||||||
<colgroup>
|
{renderOpinionForm("ENV")}
|
||||||
<col width="10%" />
|
</div>
|
||||||
<col width="55%" />
|
</SectionCollapsible>
|
||||||
<col width="5%" />
|
) : null}
|
||||||
<col width="20%" />
|
{hasKecoSection ? (
|
||||||
<col width="10%" />
|
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||||
</colgroup>
|
<div className="space-y-6">
|
||||||
<TableHeader>
|
{kecoRvwList.length > 0 ? (
|
||||||
<TableRow>
|
<TechReviewOpinionTable items={kecoRvwList} isReviewTurn={isReviewTurn("KECO")} />
|
||||||
<TableHead>의견자명</TableHead>
|
) : null}
|
||||||
<TableHead>의견내용</TableHead>
|
{renderOpinionForm("KECO")}
|
||||||
<TableHead>검토결과</TableHead>
|
</div>
|
||||||
<TableHead>첨부파일</TableHead>
|
</SectionCollapsible>
|
||||||
<TableHead>등록일시</TableHead>
|
) : null}
|
||||||
</TableRow>
|
</>
|
||||||
</TableHeader>
|
);
|
||||||
<TableBody>
|
});
|
||||||
{items.map((item) => (
|
|
||||||
<TableRow key={`${item.instlSncnDocNo}_${item.rvwOpnnSn}`}>
|
function getRvwOpnnIds(item: InstlSncnRvwOpnnResponseDto) {
|
||||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
if (!item.instlSncnDocNo || item.rvwOpnnSn == null) {
|
||||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
throw new Error("검토의견 식별값이 없습니다.");
|
||||||
<TableCell align="center">
|
}
|
||||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
return [item.instlSncnDocNo, item.rvwOpnnSn] as const;
|
||||||
</TableCell>
|
}
|
||||||
<TableCell align="center">
|
|
||||||
{item.atchFileInfo?.atflCd ? (
|
function TechReviewOpinionTable({ items, isReviewTurn }: TechReviewOpinionTableProps) {
|
||||||
<AttachmentDownloadLink
|
const queryClient = useQueryClient();
|
||||||
atflCd={item.atchFileInfo.atflCd}
|
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
|
||||||
/>
|
return (
|
||||||
) : (
|
<RvwOpnnListTable
|
||||||
"-"
|
items={items}
|
||||||
)}
|
getRowKey={(item) => `${item.instlSncnDocNo}_${item.rvwOpnnSn}`}
|
||||||
</TableCell>
|
currentUserUuid={currentUserUuid}
|
||||||
<TableCell align="center">
|
isReviewTurn={isReviewTurn}
|
||||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
onUpdate={async (item, body) => {
|
||||||
</TableCell>
|
await instlSncnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||||
</TableRow>
|
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||||
))}
|
}}
|
||||||
</TableBody>
|
onDelete={async (item) => {
|
||||||
</Table>
|
await instlSncnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||||
</div>
|
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||||
);
|
}}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
export { TechReviewOpinionSection };
|
}
|
||||||
|
|
||||||
|
export { TechReviewOpinionSection };
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,208 +1,235 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
|
||||||
import { dayjs } from "@/shared/lib";
|
import { dayjs } from "@/shared/lib";
|
||||||
import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan";
|
import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan";
|
||||||
import { Badge } from "@common/components";
|
import { Badge } from "@common/components";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
|
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
|
||||||
|
|
||||||
const MAIN_STEP_LABELS = {
|
const MAIN_STEP_LABELS = {
|
||||||
apply: "기본계획 신청",
|
apply: "기본계획 신청",
|
||||||
envoReview: "기본계획 검토",
|
envoReview: "기본계획 검토",
|
||||||
techReview: "기술검토",
|
techReview: "기술검토",
|
||||||
reply: "검토결과 회신",
|
reply: "검토결과 회신",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PROGRESS_STATUS_COLUMNS = [
|
export const PROGRESS_STATUS_COLUMNS = [
|
||||||
{ label: "기본계획 신청 (시군)", sttsComCd: "LE" },
|
{ label: "기본계획 신청 (시군)", sttsComCd: "LE" },
|
||||||
{ label: "기본계획 검토", sttsComCd: "ER" },
|
{ label: "기본계획 검토", sttsComCd: "ER" },
|
||||||
{ label: "기술검토", sttsComCd: "TE" },
|
{ label: "기술검토", sttsComCd: "TE" },
|
||||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||||
{ label: "횐경청 검토 결과 회신", sttsComCd: "EP" },
|
{ label: "횐경청 검토 결과 회신", sttsComCd: "EP" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type BadgeState = "completed" | "current" | "pending";
|
type BadgeState = "completed" | "current" | "pending";
|
||||||
|
|
||||||
interface ProgressBadge {
|
interface ProgressBadge {
|
||||||
label: string;
|
label: string;
|
||||||
state: BadgeState;
|
state: BadgeState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||||
if (prgrsSttsComCd === "SV") {
|
if (prgrsSttsComCd === "SV") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "ST") {
|
if (prgrsSttsComCd === "ST") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "LE") {
|
if (prgrsSttsComCd === "LE") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "TP") {
|
if (prgrsSttsComCd === "TP") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{
|
{
|
||||||
label: MAIN_STEP_LABELS.techReview,
|
label: MAIN_STEP_LABELS.techReview,
|
||||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||||
},
|
},
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prgrsSttsComCd === "EP") {
|
if (prgrsSttsComCd === "EP") {
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||||
{
|
{
|
||||||
label: MAIN_STEP_LABELS.techReview,
|
label: MAIN_STEP_LABELS.techReview,
|
||||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||||
},
|
},
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||||
if (prgrsSttsComCd === "SL") {
|
if (prgrsSttsComCd === "SL") {
|
||||||
return <Badge color="orange">보완요청</Badge>;
|
return <Badge color="orange">보완요청</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBadgeColor(state: BadgeState) {
|
function getBadgeColor(state: BadgeState) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "completed":
|
case "completed":
|
||||||
return "green";
|
return "green";
|
||||||
case "current":
|
case "current":
|
||||||
return "blue";
|
return "blue";
|
||||||
case "pending":
|
case "pending":
|
||||||
return "lightGray";
|
return "lightGray";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOptional(value: string | undefined) {
|
function getBadgeStateText(state: BadgeState) {
|
||||||
return value?.trim() ? value : "-";
|
switch (state) {
|
||||||
}
|
case "completed":
|
||||||
|
return "완료";
|
||||||
interface ProgressStepBadgeProps {
|
case "current":
|
||||||
prgrsSttsComCd: string;
|
return "진행중";
|
||||||
techRplyHstryYn: "Y" | "N";
|
case "pending":
|
||||||
}
|
return "대기";
|
||||||
|
}
|
||||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
}
|
||||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
|
||||||
|
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||||
return (
|
function splitStepLabel(label: string) {
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
const [title = "", ...rest] = label.split(" ");
|
||||||
{badges.map((badge, index) => (
|
return { title, detail: rest.join(" ") };
|
||||||
<Fragment key={`${badge.label}-${index}`}>
|
}
|
||||||
{index > 0 && <span className="mx-2">></span>}
|
|
||||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
function formatOptional(value: string | undefined) {
|
||||||
</Fragment>
|
return value?.trim() ? value : "-";
|
||||||
))}
|
}
|
||||||
</div>
|
|
||||||
);
|
interface ProgressStepBadgeProps {
|
||||||
}
|
prgrsSttsComCd: string;
|
||||||
|
techRplyHstryYn: "Y" | "N";
|
||||||
export function createSewerBscPlanColumns() {
|
}
|
||||||
return [
|
|
||||||
columnHelper.accessor("sido", {
|
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||||
header: "시도",
|
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||||
meta: { align: "center" },
|
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
return (
|
||||||
}),
|
<div className="flex flex-wrap items-center justify-center">
|
||||||
columnHelper.accessor("gugun", {
|
{badges.map((badge, index) => (
|
||||||
header: "시군구",
|
<Fragment key={`${badge.label}-${index}`}>
|
||||||
meta: { align: "center" },
|
{index > 0 && <span className="mx-2">></span>}
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||||
}),
|
<Badge
|
||||||
columnHelper.accessor("sewerBscPlanNm", {
|
color={getBadgeColor(badge.state)}
|
||||||
header: "기본계획명",
|
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||||
meta: { align: "left" },
|
>
|
||||||
cell: ({ row, getValue }) => (
|
<span>{splitStepLabel(badge.label).title}</span>
|
||||||
<>
|
<span>
|
||||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
{splitStepLabel(badge.label).detail}
|
||||||
</>
|
{getBadgeStateText(badge.state)}
|
||||||
),
|
</span>
|
||||||
}),
|
</Badge>
|
||||||
columnHelper.accessor("sewerBscPlanSeCdNm", {
|
</Fragment>
|
||||||
header: "계획구분",
|
))}
|
||||||
meta: { align: "center" },
|
</div>
|
||||||
cell: ({ row }) =>
|
);
|
||||||
formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd),
|
}
|
||||||
}),
|
|
||||||
columnHelper.accessor("taskSeCdNm", {
|
export function createSewerBscPlanColumns() {
|
||||||
header: "업무구분",
|
return [
|
||||||
meta: { align: "center" },
|
columnHelper.accessor("sido", {
|
||||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
header: "시도",
|
||||||
}),
|
meta: { align: "center" },
|
||||||
columnHelper.accessor("rgtrNm", {
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
header: "담당자",
|
}),
|
||||||
meta: { align: "center" },
|
columnHelper.accessor("gugun", {
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
header: "시군구",
|
||||||
}),
|
meta: { align: "center" },
|
||||||
columnHelper.accessor("envoSeCdNm", {
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
header: "해당기관",
|
}),
|
||||||
meta: { align: "center" },
|
columnHelper.accessor("sewerBscPlanNm", {
|
||||||
cell: ({ getValue }) => formatOptional(getValue()),
|
header: "기본계획명",
|
||||||
}),
|
meta: { align: "left" },
|
||||||
columnHelper.display({
|
cell: ({ row, getValue }) => (
|
||||||
id: "progress",
|
<>
|
||||||
header: "진행상황",
|
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||||
meta: { align: "center" },
|
</>
|
||||||
cell: ({ row }) => (
|
),
|
||||||
<ProgressStepBadge
|
}),
|
||||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
columnHelper.accessor("sewerBscPlanSeCdNm", {
|
||||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
header: "계획구분",
|
||||||
/>
|
meta: { align: "center" },
|
||||||
),
|
cell: ({ row }) =>
|
||||||
}),
|
formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd),
|
||||||
columnHelper.accessor("regDt", {
|
}),
|
||||||
header: "등록일",
|
columnHelper.accessor("taskSeCdNm", {
|
||||||
meta: { align: "center" },
|
header: "업무구분",
|
||||||
cell: ({ getValue }) => {
|
meta: { align: "center" },
|
||||||
const value = getValue();
|
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
}),
|
||||||
},
|
columnHelper.accessor("rgtrNm", {
|
||||||
}),
|
header: "담당자",
|
||||||
];
|
meta: { align: "center" },
|
||||||
}
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("envoSeCdNm", {
|
||||||
|
header: "해당기관",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => formatOptional(getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "progress",
|
||||||
|
header: "진행상황",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ProgressStepBadge
|
||||||
|
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||||
|
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("regDt", {
|
||||||
|
header: "등록일",
|
||||||
|
meta: { align: "center" },
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue();
|
||||||
|
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function BasicInfoFormSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본계획 신청">
|
<SectionCollapsible title="기본계획 신청">
|
||||||
<Table caption="기본계획 정보 입력" variant="secondary">
|
<Table caption="기본계획 신청 입력으로 기본계획명, 계획년도, 기준년도, 1단계 목표연도, 2단계 목표연도, 3단계 목표연도, 4단계 목표연도, 계획개요, 구분, 신청 지자체 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function BasicInfoSection({ det, dtl }: BasicInfoSectionProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="기본계획 신청">
|
<SectionCollapsible title="기본계획 신청">
|
||||||
<Table caption="기본계획 정보" variant="secondary">
|
<Table caption="기본계획 신청으로 기본계획명, 계획년도, 기준년도, 1단계 목표연도, 2단계 목표연도, 3단계 목표연도, 4단계 목표연도, 계획개요, 구분, 신청 지자체 정보 제공" variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="15%" />
|
<col width="15%" />
|
||||||
<col width="35%" />
|
<col width="35%" />
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function BizCstMatrixFormSection() {
|
|||||||
<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="총사업비 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="12%" />
|
<col width="12%" />
|
||||||
<col width="22%" />
|
<col width="22%" />
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function BizCstMatrixSection({ bizCstList, dtl }: BizCstMatrixSectionProps) {
|
|||||||
<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="총사업비 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="12%" />
|
<col width="12%" />
|
||||||
<col width="22%" />
|
<col width="22%" />
|
||||||
|
|||||||
@@ -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={`sewer-cklst-step-tab-${stepCode}`}
|
||||||
|
aria-controls="sewer-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="sewer-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={`sewer-cklst-tab-${code}`}
|
||||||
|
aria-controls={`sewer-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={`sewer-cklst-panel-${code}`}
|
||||||
|
aria-labelledby={`sewer-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)}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function ContactInfoSection({ dtl }: ContactInfoSectionProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="담당자 정보">
|
<SectionCollapsible title="담당자 정보">
|
||||||
<Table caption="기본계획 담당자 정보" variant="secondary">
|
<Table caption="담당자 정보로 신청자(이름, 연락처, 소속, 이메일)과 설계자(이름, 연락처, 소속, 이메일) 정보 제공" variant="secondary">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{dtl.aplcntNm ? (
|
{dtl.aplcntNm ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function FnrsCstFormSection() {
|
|||||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||||
</div>
|
</div>
|
||||||
<Table caption="총사업비 대비 재원분담 합계 비교" variant="secondary" className="table-fixed">
|
<Table caption="세부별 총사업비(①) - 재원분담 총사업비(②) 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="12%" />
|
<col width="12%" />
|
||||||
<col width="22%" />
|
<col width="22%" />
|
||||||
@@ -125,7 +125,7 @@ function FnrsCstFormSection() {
|
|||||||
<h3 className="text-xl font-semibold">재원분담</h3>
|
<h3 className="text-xl font-semibold">재원분담</h3>
|
||||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||||
</div>
|
</div>
|
||||||
<Table caption="단계별 재원분담 입력" variant="secondary" className="table-fixed">
|
<Table caption="재원분담 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="12%" />
|
<col width="12%" />
|
||||||
<col width="22%" />
|
<col width="22%" />
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function FnrsCstSection({ fnrsCstList, dtl }: FnrsCstSectionProps) {
|
|||||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||||
</div>
|
</div>
|
||||||
<Table caption="단계별 재원분담 내역" variant="secondary" className="table-fixed">
|
<Table caption="재원분담 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="12%" />
|
<col width="12%" />
|
||||||
<col width="22%" />
|
<col width="22%" />
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
|
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import { Field } from "@/shared/ui/field";
|
import { Field } from "@/shared/ui/field";
|
||||||
import { truncateDecimals } from "@/widgets/pbadms-sprt";
|
import { truncateDecimals } from "@/widgets/pbadms-sprt";
|
||||||
|
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Input,
|
Input,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -21,14 +19,6 @@ import {
|
|||||||
|
|
||||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||||
|
|
||||||
const PreviousPlanLookupDialog = dynamic(
|
|
||||||
() =>
|
|
||||||
import("../previous-plan-lookup-dialog").then((mod) => ({
|
|
||||||
default: mod.PreviousPlanLookupDialog,
|
|
||||||
})),
|
|
||||||
{ ssr: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const OTLN_ROWS = [
|
const OTLN_ROWS = [
|
||||||
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구", decimals: 0 },
|
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구", decimals: 0 },
|
||||||
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구", decimals: 0 },
|
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구", decimals: 0 },
|
||||||
@@ -37,7 +27,6 @@ const OTLN_ROWS = [
|
|||||||
|
|
||||||
function OutlineFormSection() {
|
function OutlineFormSection() {
|
||||||
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||||
const [isLookupOpen, setIsLookupOpen] = useState(false);
|
|
||||||
|
|
||||||
const otlnList = useWatch({ control, name: "sewerBscPlanOtlnList" }) ?? [];
|
const otlnList = useWatch({ control, name: "sewerBscPlanOtlnList" }) ?? [];
|
||||||
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
||||||
@@ -74,12 +63,8 @@ function OutlineFormSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCollapsible title="사업개요">
|
<SectionCollapsible title="사업개요">
|
||||||
<div className="mb-2 flex justify-end px-2">
|
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||||
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
|
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
|
||||||
이전기본계획조회
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Table caption="사업개요 단계별 입력" variant="secondary">
|
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="12%" />
|
<col width="12%" />
|
||||||
<col width="22%" />
|
<col width="22%" />
|
||||||
@@ -126,13 +111,6 @@ function OutlineFormSection() {
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<PreviousPlanLookupDialog
|
|
||||||
open={isLookupOpen}
|
|
||||||
onOpenChange={setIsLookupOpen}
|
|
||||||
initialSidoNm={sido}
|
|
||||||
initialGugunNm={gugun}
|
|
||||||
/>
|
|
||||||
</SectionCollapsible>
|
</SectionCollapsible>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user