Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cf3617513 | ||
|
|
b611538180 | ||
|
|
1cffe06cf9 | ||
|
|
9a24e4aa23 | ||
|
|
3732c9380b | ||
|
|
9e15b23eda | ||
|
|
6f300918cd | ||
|
|
619878bc03 | ||
|
|
9f0e03c151 | ||
|
|
471a431af5 | ||
|
|
26d8410649 | ||
|
|
8403ad556c | ||
|
|
d26c15f857 | ||
|
|
86ed664e3e | ||
|
|
a105f395dd | ||
|
|
a5ebc12580 | ||
|
|
270f6e366f | ||
|
|
acf215c1bf | ||
|
|
8a98cd7d19 | ||
|
|
3a5b48b608 | ||
|
|
d735ad7dd7 | ||
|
|
25dd0f1504 | ||
|
|
8e14c6cef5 | ||
|
|
9f7abe4413 | ||
|
|
de730205af | ||
|
|
a873f0e0dc | ||
|
|
32e3765b52 | ||
|
|
4f62095e8e | ||
|
|
5234448ae5 | ||
|
|
f284c67988 |
@@ -14,15 +14,15 @@
|
||||
type="text/javascript"
|
||||
data-type="css/app"
|
||||
data-framework="aori|lab"
|
||||
src="https://hwp.onasys.co.kr/webhwpctrl/js/loadVersion.js"
|
||||
src="js/loadVersion.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
data-type="css/app"
|
||||
data-framework="aori|lab"
|
||||
src="https://hwp.onasys.co.kr/webhwpctrl/js/loadHead.js"
|
||||
src="js/loadHead.js"
|
||||
></script>
|
||||
<script data-main="js/main" data-bundle="dist/main_bundle" src="https://hwp.onasys.co.kr/webhwpctrl/js/loadMain.js"></script>
|
||||
<script data-main="js/main" data-bundle="dist/main_bundle" src="js/loadMain.js"></script>
|
||||
<style>
|
||||
#hcwoViewScroll::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
@@ -57,6 +57,6 @@
|
||||
</div>
|
||||
|
||||
<iframe id="download_iframe" style="width: 0px; height: 0px; border: 0; display: none"></iframe>
|
||||
<script type="text/javascript" src="https://hwp.onasys.co.kr/webhwpctrl/hwpsdk/loadHwpsdk.js"></script>
|
||||
<script type="text/javascript" src="hwpsdk/loadHwpsdk.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,18 +5,6 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>웹한글 기안기 브리지</title>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://hwp.onasys.co.kr/webhwpctrl/js/hwpctrlapp/utils/util.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://hwp.onasys.co.kr/webhwpctrl/js/hwpctrlapp/hwpCtrlApp.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://hwp.onasys.co.kr/webhwpctrl/js/webhwpctrl.js"
|
||||
></script>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
@@ -39,27 +27,56 @@
|
||||
<div id="hwpctrl"></div>
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
// baseurl: 부모(HangulGiangi)가 NEXT_PUBLIC_WEBHYP_CTRL_URL 값으로 전달.
|
||||
// 없으면 same-origin 프록시 경로(/webhwpctrl/ → next.config rewrites)를 사용한다.
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var baseUrl = params.get("baseurl") || "https://hwp.onasys.co.kr/webhwpctrl";
|
||||
var baseUrl = params.get("baseurl") || new URL("./", window.location.href).href;
|
||||
if (baseUrl.charAt(baseUrl.length - 1) !== "/") {
|
||||
baseUrl += "/";
|
||||
}
|
||||
|
||||
var HwpCtrl = BuildWebHwpCtrl(
|
||||
"hwpctrl",
|
||||
baseUrl,
|
||||
function () {
|
||||
try {
|
||||
window.parent.postMessage({ type: "hangulgiangi-ready" }, "*");
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
2,
|
||||
);
|
||||
var scripts = [
|
||||
"js/hwpctrlapp/utils/util.js",
|
||||
"js/hwpctrlapp/hwpCtrlApp.js",
|
||||
"js/webhwpctrl.js",
|
||||
];
|
||||
|
||||
// 부모(HangulGiangi)가 contentWindow.HwpCtrl 로 접근
|
||||
window.HwpCtrl = HwpCtrl;
|
||||
function loadScript(index) {
|
||||
if (index >= scripts.length) {
|
||||
init();
|
||||
return;
|
||||
}
|
||||
var script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = baseUrl + scripts[index];
|
||||
script.onload = function () {
|
||||
loadScript(index + 1);
|
||||
};
|
||||
script.onerror = function () {
|
||||
console.error("웹한글 기안기 스크립트 로드 실패: " + script.src);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function init() {
|
||||
var HwpCtrl = BuildWebHwpCtrl(
|
||||
"hwpctrl",
|
||||
baseUrl,
|
||||
function () {
|
||||
try {
|
||||
window.parent.postMessage({ type: "hangulgiangi-ready" }, "*");
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
2,
|
||||
);
|
||||
|
||||
// 부모(HangulGiangi)가 contentWindow.HwpCtrl 로 접근
|
||||
window.HwpCtrl = HwpCtrl;
|
||||
}
|
||||
|
||||
loadScript(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -13,12 +13,16 @@ import type {
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "../model/dc-interface";
|
||||
|
||||
/** 빈 문자열 파라미터를 보내면 서버 오류가 나므로 값이 있는 항목만 전송한다. */
|
||||
async function getNowDmgMtncGnsvy(
|
||||
params: NowDmgMtncGnsvySearchDto,
|
||||
): Promise<NowDmgMtncGnsvyResponseDto[]> {
|
||||
const filledParams = Object.fromEntries(
|
||||
Object.entries(params).filter(([, value]) => value !== undefined && value !== ""),
|
||||
);
|
||||
const response = await client.get<NowDmgMtncGnsvyResponseDto[]>(
|
||||
`${ENDPOINT.DC_SERVICE}/nowDmgMtncGnsvy`,
|
||||
{ params },
|
||||
{ params: filledParams },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -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 { fnrsCnsltnKeys } from "./api/queries";
|
||||
export {
|
||||
useChangePrgrsSttsComCd,
|
||||
useDeleteFnrsCnsltnMng,
|
||||
useFnrsCnsltnFileVrfcResults,
|
||||
useFnrsCnsltnMng,
|
||||
useFnrsCnsltnMngPage,
|
||||
useRequestActnReviewFnrsCnsltnMng,
|
||||
useRequestExamineFnrsCnsltnMng,
|
||||
useSaveApplyFnrsCnsltnMng,
|
||||
useSaveFnrsCnsltnMng,
|
||||
useSubmitReqSupplementFnrsCnsltnMng,
|
||||
useSubmitSupplementFnrsCnsltnMng,
|
||||
useTechReviewFnrsCnsltnMng,
|
||||
useUpdateFnrsCnsltnMng,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
BizCstItem,
|
||||
BzScaleItem,
|
||||
CklstResultItem,
|
||||
EsntlAtchFileItem,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnMngSaveRequestDto,
|
||||
FnrsCnsltnRvwOpnnSaveRequestDto,
|
||||
FnrsCnsltnSplmntDmndItem,
|
||||
FnrsSharItem,
|
||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||
export {
|
||||
fnrsCnsltnApplySaveSchema,
|
||||
fnrsCnsltnBizCstItemSchema,
|
||||
fnrsCnsltnBzScaleItemSchema,
|
||||
fnrsCnsltnCklstResultItemSchema,
|
||||
fnrsCnsltnEsntlAtchFileItemSchema,
|
||||
fnrsCnsltnMngSaveSchema,
|
||||
fnrsCnsltnRvwOpnnSaveSchema,
|
||||
fnrsCnsltnSplmntDmndItemSchema,
|
||||
fnrsSharItemSchema,
|
||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
DocPrgrsUpdateRequestDto,
|
||||
ElaprDocPrgrsClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
FnrsCnsltnFileVrfcRsltResponseDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnMngSearchDto,
|
||||
FnrsCnsltnMngService,
|
||||
FnrsCnsltnMngUpdateRequestDto,
|
||||
FnrsCnsltnRvwOpnnResponseDto,
|
||||
FnrsCnsltnSplmntDmndResponseDto,
|
||||
RagIndexingInfoDto,
|
||||
} from "./model/fnrs-cnsltn-interface";
|
||||
export type {
|
||||
EnvoActnReviewReqDto,
|
||||
FnrsExamineRequestDto,
|
||||
} from "./model/fnrs-cnsltn-payload-schema";
|
||||
export {
|
||||
envoActnReviewReqSchema,
|
||||
fnrsExamineRequestSchema,
|
||||
} from "./model/fnrs-cnsltn-payload-schema";
|
||||
export { fnrsCnsltnRvwOpnnService } from "./api/fnrs-cnsltn-rvw-opnn-service";
|
||||
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
||||
export { fnrsCnsltnKeys } from "./api/queries";
|
||||
export {
|
||||
useChangePrgrsSttsComCd,
|
||||
useDeleteFnrsCnsltnMng,
|
||||
useFnrsCnsltnFileVrfcResults,
|
||||
useFnrsCnsltnMng,
|
||||
useFnrsCnsltnMngPage,
|
||||
useRequestActnReviewFnrsCnsltnMng,
|
||||
useRequestExamineFnrsCnsltnMng,
|
||||
useSaveApplyFnrsCnsltnMng,
|
||||
useSaveFnrsCnsltnMng,
|
||||
useSubmitReqSupplementFnrsCnsltnMng,
|
||||
useSubmitSupplementFnrsCnsltnMng,
|
||||
useTechReviewFnrsCnsltnMng,
|
||||
useUpdateFnrsCnsltnMng,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
BizCstItem,
|
||||
BzScaleItem,
|
||||
CklstResultItem,
|
||||
EsntlAtchFileItem,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnMngSaveRequestDto,
|
||||
FnrsCnsltnRvwOpnnSaveRequestDto,
|
||||
FnrsCnsltnSplmntDmndItem,
|
||||
FnrsSharItem,
|
||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||
export {
|
||||
fnrsCnsltnApplySaveSchema,
|
||||
fnrsCnsltnBizCstItemSchema,
|
||||
fnrsCnsltnBzScaleItemSchema,
|
||||
fnrsCnsltnCklstResultItemSchema,
|
||||
fnrsCnsltnEsntlAtchFileItemSchema,
|
||||
fnrsCnsltnMngSaveSchema,
|
||||
fnrsCnsltnRvwOpnnSaveSchema,
|
||||
fnrsCnsltnSplmntDmndItemSchema,
|
||||
fnrsSharItemSchema,
|
||||
} from "./model/fnrs-cnsltn-apply-payload-schema";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
DocPrgrsUpdateRequestDto,
|
||||
ElaprDocPrgrsClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
FnrsCnsltnFileVrfcRsltResponseDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnMngSearchDto,
|
||||
FnrsCnsltnMngService,
|
||||
FnrsCnsltnMngUpdateRequestDto,
|
||||
FnrsCnsltnRvwOpnnResponseDto,
|
||||
FnrsCnsltnSplmntDmndResponseDto,
|
||||
RagIndexingInfoDto,
|
||||
} from "./model/fnrs-cnsltn-interface";
|
||||
export type {
|
||||
EnvoActnReviewReqDto,
|
||||
FnrsExamineRequestDto,
|
||||
} from "./model/fnrs-cnsltn-payload-schema";
|
||||
export {
|
||||
envoActnReviewReqSchema,
|
||||
fnrsExamineRequestSchema,
|
||||
} from "./model/fnrs-cnsltn-payload-schema";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.INSTL_SNCN_RVW_OPNN;
|
||||
|
||||
async function update(
|
||||
instlSncnDocNo: string,
|
||||
rvwOpnnSn: number,
|
||||
body: RvwOpnnUpdateRequestDto,
|
||||
): Promise<void> {
|
||||
await client.put<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`, body);
|
||||
}
|
||||
|
||||
async function deleteFn(instlSncnDocNo: string, rvwOpnnSn: number): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${instlSncnDocNo}/${rvwOpnnSn}`);
|
||||
}
|
||||
|
||||
export const instlSncnRvwOpnnService = {
|
||||
update,
|
||||
delete: deleteFn,
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export { instlSncnRvwOpnnService } from "./api/instl-sncn-rvw-opnn-service";
|
||||
export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service";
|
||||
export {
|
||||
instlSncnKeys,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.SEWER_BSC_PLAN_RVW_OPNNS;
|
||||
|
||||
async function update(
|
||||
sewerBscPlanDocNo: string,
|
||||
chgCycl: number,
|
||||
rvwOpnnSn: number,
|
||||
body: RvwOpnnUpdateRequestDto,
|
||||
): Promise<void> {
|
||||
await client.put<void>(`${BASE}/${sewerBscPlanDocNo}/${chgCycl}/${rvwOpnnSn}`, body);
|
||||
}
|
||||
|
||||
async function deleteFn(sewerBscPlanDocNo: string, chgCycl: number, rvwOpnnSn: number): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${sewerBscPlanDocNo}/${chgCycl}/${rvwOpnnSn}`);
|
||||
}
|
||||
|
||||
export const sewerBscPlanRvwOpnnService = {
|
||||
update,
|
||||
delete: deleteFn,
|
||||
};
|
||||
@@ -1,64 +1,65 @@
|
||||
export { sewerBscPlanKeys } from "./api/queries";
|
||||
export {
|
||||
useDeleteSewerBscPlan,
|
||||
useSaveApplySewerBscPlan,
|
||||
useSaveSewerBscPlan,
|
||||
useSewerBscPlan,
|
||||
useSewerBscPlanDetails,
|
||||
useSewerBscPlanPage,
|
||||
useSubmitReqSupplementSewerBscPlan,
|
||||
useSubmitSupplementSewerBscPlan,
|
||||
useTechReviewSewerBscPlan,
|
||||
useUpdateSewerBscPlan,
|
||||
} from "./api/queries";
|
||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||
export type {
|
||||
CklstResultItem,
|
||||
EsntlAtchFileItem,
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanBizCstItem,
|
||||
SewerBscPlanDtlSaveRequestDto,
|
||||
SewerBscPlanFnrsCstItem,
|
||||
SewerBscPlanOtlnItem,
|
||||
SewerBscPlanRvwOpnnItem,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSplmntDmndItem,
|
||||
SewerBscPlanTmplntDtlItem,
|
||||
SewerBscPlanTmplntItem,
|
||||
SewerBscPlanTmplntTmprStrgItem,
|
||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||
export {
|
||||
sewerBscPlanApplySaveSchema,
|
||||
sewerBscPlanBizCstItemSchema,
|
||||
sewerBscPlanCklstResultItemSchema,
|
||||
sewerBscPlanDtlSaveSchema,
|
||||
sewerBscPlanEsntlAtchFileItemSchema,
|
||||
sewerBscPlanFnrsCstItemSchema,
|
||||
sewerBscPlanOtlnItemSchema,
|
||||
sewerBscPlanRvwOpnnItemSchema,
|
||||
sewerBscPlanSaveSchema,
|
||||
sewerBscPlanSplmntDmndItemSchema,
|
||||
sewerBscPlanTmplntDtlItemSchema,
|
||||
sewerBscPlanTmplntItemSchema,
|
||||
sewerBscPlanTmplntTmprStrgItemSchema,
|
||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanBizCstResponseDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanDtlResponseDto,
|
||||
SewerBscPlanFnrsCstResponseDto,
|
||||
SewerBscPlanOtlnResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanRvwOpnnResponseDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanSplmntDmndResponseDto,
|
||||
SewerBscPlanTmplntDtlResponseDto,
|
||||
SewerBscPlanTmplntResponseDto,
|
||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "./model/sewer-bsc-plan-interface";
|
||||
export { sewerBscPlanKeys } from "./api/queries";
|
||||
export {
|
||||
useDeleteSewerBscPlan,
|
||||
useSaveApplySewerBscPlan,
|
||||
useSaveSewerBscPlan,
|
||||
useSewerBscPlan,
|
||||
useSewerBscPlanDetails,
|
||||
useSewerBscPlanPage,
|
||||
useSubmitReqSupplementSewerBscPlan,
|
||||
useSubmitSupplementSewerBscPlan,
|
||||
useTechReviewSewerBscPlan,
|
||||
useUpdateSewerBscPlan,
|
||||
} from "./api/queries";
|
||||
export { sewerBscPlanRvwOpnnService } from "./api/sewer-bsc-plan-rvw-opnn-service";
|
||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||
export type {
|
||||
CklstResultItem,
|
||||
EsntlAtchFileItem,
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanBizCstItem,
|
||||
SewerBscPlanDtlSaveRequestDto,
|
||||
SewerBscPlanFnrsCstItem,
|
||||
SewerBscPlanOtlnItem,
|
||||
SewerBscPlanRvwOpnnItem,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSplmntDmndItem,
|
||||
SewerBscPlanTmplntDtlItem,
|
||||
SewerBscPlanTmplntItem,
|
||||
SewerBscPlanTmplntTmprStrgItem,
|
||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||
export {
|
||||
sewerBscPlanApplySaveSchema,
|
||||
sewerBscPlanBizCstItemSchema,
|
||||
sewerBscPlanCklstResultItemSchema,
|
||||
sewerBscPlanDtlSaveSchema,
|
||||
sewerBscPlanEsntlAtchFileItemSchema,
|
||||
sewerBscPlanFnrsCstItemSchema,
|
||||
sewerBscPlanOtlnItemSchema,
|
||||
sewerBscPlanRvwOpnnItemSchema,
|
||||
sewerBscPlanSaveSchema,
|
||||
sewerBscPlanSplmntDmndItemSchema,
|
||||
sewerBscPlanTmplntDtlItemSchema,
|
||||
sewerBscPlanTmplntItemSchema,
|
||||
sewerBscPlanTmplntTmprStrgItemSchema,
|
||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanBizCstResponseDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanDtlResponseDto,
|
||||
SewerBscPlanFnrsCstResponseDto,
|
||||
SewerBscPlanOtlnResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanRvwOpnnResponseDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanSplmntDmndResponseDto,
|
||||
SewerBscPlanTmplntDtlResponseDto,
|
||||
SewerBscPlanTmplntResponseDto,
|
||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "./model/sewer-bsc-plan-interface";
|
||||
|
||||
@@ -1,183 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "설계자문 신청",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "설계자문 신청 (시군)", sttsComCd: "CT" },
|
||||
{ label: "기술검토", sttsComCd: "CS" },
|
||||
{ label: "기술검토 회신", sttsComCd: "CP" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "CV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "CT" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "CP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | null | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={`${badge.label}-${index}`}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="flex-col justify-center !px-[20px] py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createDesignCnstnColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("aplcntNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => <ProgressStepBadge prgrsSttsComCd={row.original.prgrsSttsComCd || ""} />,
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "신청일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<DesignCnstnMngResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "설계자문 신청",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "설계자문 신청 (시군)", sttsComCd: "CT" },
|
||||
{ label: "기술검토", sttsComCd: "CS" },
|
||||
{ label: "기술검토 회신", sttsComCd: "CP" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "CV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "CT" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "CP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | null | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={`${badge.label}-${index}`}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createDesignCnstnColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("aplcntNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => <ProgressStepBadge prgrsSttsComCd={row.original.prgrsSttsComCd || ""} />,
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "신청일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
|
||||
@@ -16,9 +17,9 @@ function BasicInfoFormSection() {
|
||||
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
|
||||
const localGovLabel = [watch("designCnstnMng.sido"), watch("designCnstnMng.gugun")]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const sido = watch("designCnstnMng.sido");
|
||||
const gugun = watch("designCnstnMng.gugun");
|
||||
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
|
||||
const { data: taskOptions } = useCodeList("CNFM");
|
||||
|
||||
function handleBnsSelect(data: BnsApplyData) {
|
||||
@@ -36,6 +37,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface BasicInfoSectionProps {
|
||||
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
|
||||
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
|
||||
@@ -1,338 +1,334 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
||||
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type {
|
||||
ReviewOpinionFormData,
|
||||
ReviewOpinionGetFormDataResult,
|
||||
} from "../../lib/review-opinion-utils";
|
||||
import type { ReviewStepCode } from "../../model/view-types";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: DesignCnstnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
},
|
||||
rvwList,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
||||
|
||||
if (!hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep || !canWriteOpinion) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleFileRemove();
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" type="button" asChild>
|
||||
<span>파일 선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
});
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
<col width="5%" />
|
||||
<col width="20%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>의견자명</TableHead>
|
||||
<TableHead>의견내용</TableHead>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead>등록일시</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={`${item.designCnstnDocNo}_${item.rvwOpnnSn}`}>
|
||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<AttachmentDownloadLink
|
||||
atflCd={item.atchFileInfo.atflCd}
|
||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import {
|
||||
designCnstnKeys,
|
||||
type DesignCnstnRvwOpnnResponseDto,
|
||||
designCnstnRvwOpnnService,
|
||||
} from "@/entities/design-cnstn";
|
||||
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type {
|
||||
ReviewOpinionFormData,
|
||||
ReviewOpinionGetFormDataResult,
|
||||
} from "../../lib/review-opinion-utils";
|
||||
import type { ReviewStepCode } from "../../model/view-types";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: DesignCnstnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: DesignCnstnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!canWriteOpinion || !editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
},
|
||||
rvwList,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteOpinion);
|
||||
|
||||
if (!hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||
return editableReviewStep === reviewStep && canWriteOpinion;
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (!isReviewTurn(reviewStep)) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table
|
||||
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||
variant="secondary"
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row" className="w-[12rem]">
|
||||
의견내용
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleFileRemove();
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" type="button" asChild>
|
||||
<span>파일 선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? (
|
||||
<TechReviewOpinionTable items={kecoRvwList} />
|
||||
) : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
});
|
||||
|
||||
function getRvwOpnnIds(item: DesignCnstnRvwOpnnResponseDto) {
|
||||
if (!item.designCnstnDocNo || item.rvwOpnnSn == null) {
|
||||
throw new Error("검토의견 식별값이 없습니다.");
|
||||
}
|
||||
return [item.designCnstnDocNo, item.rvwOpnnSn] as const;
|
||||
}
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||
|
||||
return (
|
||||
<RvwOpnnListTable
|
||||
items={items}
|
||||
getRowKey={(item) => `${item.designCnstnDocNo}_${item.rvwOpnnSn}`}
|
||||
currentUserUuid={currentUserUuid}
|
||||
onUpdate={async (item, body) => {
|
||||
await designCnstnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
}}
|
||||
onDelete={async (item) => {
|
||||
await designCnstnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||
await queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
|
||||
@@ -1,230 +1,230 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<FnrsCnsltnMngResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "재원협의 신청",
|
||||
envoReview: "재원협의 검토",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
|
||||
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
|
||||
{ label: "재원협의 검토", sttsComCd: "ER" },
|
||||
{ label: "기술검토", sttsComCd: "TE" },
|
||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "SV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (["FV", "ER", "AC", "TE", "RB", "RA", "AV", "EV", "RK", "RR", "SL"].includes(prgrsSttsComCd)) {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "EP" || prgrsSttsComCd === "TA") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | null | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
techRplyHstryYn: "Y" | "N";
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={badge.label}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="flex-col justify-center !px-[20px] py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createFnrsCnsltnColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("aplcntNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "신청일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<FnrsCnsltnMngResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "재원협의 신청",
|
||||
envoReview: "재원협의 검토",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
|
||||
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
|
||||
{ label: "재원협의 검토", sttsComCd: "ER" },
|
||||
{ label: "기술검토", sttsComCd: "TE" },
|
||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "SV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (["FV", "ER", "AC", "TE", "RB", "RA", "AV", "EV", "RK", "RR", "SL"].includes(prgrsSttsComCd)) {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "EP" || prgrsSttsComCd === "TA") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | null | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
techRplyHstryYn: "Y" | "N";
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={badge.label}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createFnrsCnsltnColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("aplcntNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "신청일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ import {
|
||||
interface PwrkCnsltnDocEvlDialogProps {
|
||||
open: boolean;
|
||||
pwrkCnsltnDocNo: string;
|
||||
/** 평가 저장 성공 시 호출 (이후 웹기안기 문서 저장이 진행된다) */
|
||||
onSaved: () => void;
|
||||
/** 평가 없이 닫을 때 호출 (웹기안기 문서는 저장되지 않는다) */
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -41,7 +44,12 @@ const DEFAULT_VALUES: Partial<PwrkCnsltnDocEvlFormValues> = {
|
||||
pwrkCnsltnDocEvlCn: "",
|
||||
};
|
||||
|
||||
function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDocEvlDialogProps) {
|
||||
function PwrkCnsltnDocEvlDialog({
|
||||
open,
|
||||
pwrkCnsltnDocNo,
|
||||
onSaved,
|
||||
onClose,
|
||||
}: PwrkCnsltnDocEvlDialogProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { mutateAsync: saveEvl, isPending } = useSavePwrkCnsltnDocEvl();
|
||||
|
||||
@@ -65,8 +73,7 @@ function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDo
|
||||
try {
|
||||
const payload = buildPwrkCnsltnDocEvlSavePayload(pwrkCnsltnDocNo, values);
|
||||
await saveEvl(payload);
|
||||
alert("AI 초안 만족도 평가를 등록했습니다.");
|
||||
onClose();
|
||||
onSaved();
|
||||
} catch (error) {
|
||||
console.error("AI 초안 만족도 평가 등록 실패:", error);
|
||||
alert("AI 초안 만족도 평가 등록에 실패했습니다.");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -46,6 +46,7 @@ const HangulGiangi = dynamic(
|
||||
|
||||
const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
|
||||
|
||||
/** 현재 버전이 이 값보다 작으면(AI 초안 상태) 저장 전에 만족도 평가를 먼저 받는다. */
|
||||
const AI_DRAFT_EVL_MAX_VER_SN = 2;
|
||||
|
||||
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
|
||||
@@ -88,6 +89,8 @@ function PwrkCnsltnDocMngDialog({
|
||||
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
|
||||
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
|
||||
const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false);
|
||||
// 평가 모달 결과(평가 저장 여부)를 저장 흐름으로 돌려주는 resolver
|
||||
const evlResolverRef = useRef<((isEvlSaved: boolean) => void) | null>(null);
|
||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||
|
||||
const {
|
||||
@@ -145,6 +148,8 @@ function PwrkCnsltnDocMngDialog({
|
||||
setAtchFileGroupId("");
|
||||
setUploadedFiles([]);
|
||||
setIsEvlDialogOpen(false);
|
||||
evlResolverRef.current?.(false);
|
||||
evlResolverRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,9 +183,32 @@ function PwrkCnsltnDocMngDialog({
|
||||
if (!nextOpen) onClose();
|
||||
}
|
||||
|
||||
function requestEvl() {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
evlResolverRef.current = resolve;
|
||||
setIsEvlDialogOpen(true);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveEvl(isEvlSaved: boolean) {
|
||||
setIsEvlDialogOpen(false);
|
||||
evlResolverRef.current?.(isEvlSaved);
|
||||
evlResolverRef.current = null;
|
||||
}
|
||||
|
||||
async function handleSaveDocument(file: Blob) {
|
||||
if (!completedDraft) throw new Error("저장할 실무협의서 초안이 없습니다.");
|
||||
|
||||
// AI 초안(버전 < 2) 저장 시에는 만족도 평가를 먼저 저장해야 웹기안기 문서가 저장된다.
|
||||
const currentVerSn = completedDraft.crtRslt?.verSn ?? 0;
|
||||
if (currentVerSn < AI_DRAFT_EVL_MAX_VER_SN) {
|
||||
const isEvlSaved = await requestEvl();
|
||||
if (!isEvlSaved) {
|
||||
alert("AI 초안 만족도 평가를 등록해야 실무협의서가 저장됩니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const saveDto = pwrkCnsltnDocCrtRsltSaveRequestSchema.parse({
|
||||
crtDmndUuid: completedDraft.crtDmndUuid,
|
||||
pwrkCnsltnDocNo: completedDraft.pwrkCnsltnDocNo,
|
||||
@@ -201,10 +229,6 @@ function PwrkCnsltnDocMngDialog({
|
||||
}),
|
||||
]);
|
||||
alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`);
|
||||
|
||||
if (savedDocument.verSn <= AI_DRAFT_EVL_MAX_VER_SN) {
|
||||
setIsEvlDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownload() {
|
||||
@@ -386,7 +410,8 @@ function PwrkCnsltnDocMngDialog({
|
||||
<PwrkCnsltnDocEvlDialog
|
||||
open={isEvlDialogOpen}
|
||||
pwrkCnsltnDocNo={completedDraft.pwrkCnsltnDocNo}
|
||||
onClose={() => setIsEvlDialogOpen(false)}
|
||||
onSaved={() => resolveEvl(true)}
|
||||
onClose={() => resolveEvl(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
|
||||
const { control, watch, setValue } = useFormContext<FnrsCnsltnRegistFormData>();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const taskSeComCd = watch("fnrsCnsltnMng.taskSeComCd");
|
||||
const localGovLabel = [watch("fnrsCnsltnMng.sido"), watch("fnrsCnsltnMng.gugun")]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const sido = watch("fnrsCnsltnMng.sido");
|
||||
const gugun = watch("fnrsCnsltnMng.gugun");
|
||||
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
|
||||
const { data: taskOptions } = useCodeList("FNRS");
|
||||
const { data: rtlztnFcltOptions } = useCodeList("RTLZTNFCLTSE");
|
||||
const { data: rtlztnUsgOptions } = useCodeList("RTLZTNUSGSE");
|
||||
@@ -49,6 +50,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 시설종류, 재이용용도 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface BasicInfoSectionProps {
|
||||
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
|
||||
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체, 확정예산사업번호, 예산신청연도 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
|
||||
@@ -1,352 +1,345 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import {
|
||||
AttachmentDownloadLink,
|
||||
attachService,
|
||||
useDownloadAttachmentByAtflId,
|
||||
} from "@/entities/attach";
|
||||
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type {
|
||||
ReviewOpinionFormData,
|
||||
ReviewOpinionGetFormDataResult,
|
||||
} from "../../lib/review-opinion-utils";
|
||||
import type { ReviewStepCode } from "../../model/view-types";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: FnrsCnsltnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
},
|
||||
rvwList,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return null;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell>
|
||||
<AttachmentFileField
|
||||
files={
|
||||
uploadedFile
|
||||
? [
|
||||
{
|
||||
fileKey: uploadedFile.fileKey,
|
||||
fileName: uploadedFile.fileName,
|
||||
onDownload: uploadedFile.atflId
|
||||
? () => downloadByAtflId(uploadedFile.atflId!)
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
inputId={`rvw-opnn-${reviewStep}-file-input`}
|
||||
isUploadingByKey={isUploading}
|
||||
multiple={false}
|
||||
showAddButton={!uploadedFile}
|
||||
addDisabled={uploading}
|
||||
onAdd={(fileList) => void handleFileUpload(fileList)}
|
||||
onRemove={() => void handleFileRemove()}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasEnvSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
{hasKecoSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||
{/*
|
||||
재원협의에서 환경공단 검토의견은 자동 생성
|
||||
{renderOpinionForm("KECO")}
|
||||
*/}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
<col width="5%" />
|
||||
<col width="20%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>의견자명</TableHead>
|
||||
<TableHead>의견내용</TableHead>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead>등록일시</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={`${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}>
|
||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<AttachmentDownloadLink
|
||||
atflCd={item.atchFileInfo.atflCd}
|
||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
|
||||
import {
|
||||
fnrsCnsltnKeys,
|
||||
type FnrsCnsltnRvwOpnnResponseDto,
|
||||
fnrsCnsltnRvwOpnnService,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type {
|
||||
ReviewOpinionFormData,
|
||||
ReviewOpinionGetFormDataResult,
|
||||
} from "../../lib/review-opinion-utils";
|
||||
import type { ReviewStepCode } from "../../model/view-types";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: FnrsCnsltnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: FnrsCnsltnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
},
|
||||
rvwList,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO";
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return false;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (!isReviewTurn(reviewStep)) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table
|
||||
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||
variant="secondary"
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell>
|
||||
<AttachmentFileField
|
||||
files={
|
||||
uploadedFile
|
||||
? [
|
||||
{
|
||||
fileKey: uploadedFile.fileKey,
|
||||
fileName: uploadedFile.fileName,
|
||||
onDownload: uploadedFile.atflId
|
||||
? () => downloadByAtflId(uploadedFile.atflId!)
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
inputId={`rvw-opnn-${reviewStep}-file-input`}
|
||||
isUploadingByKey={isUploading}
|
||||
multiple={false}
|
||||
showAddButton={!uploadedFile}
|
||||
addDisabled={uploading}
|
||||
onAdd={(fileList) => void handleFileUpload(fileList)}
|
||||
onRemove={() => void handleFileRemove()}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasEnvSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{envRvwList.length > 0 ? (
|
||||
<TechReviewOpinionTable items={envRvwList} />
|
||||
) : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
{hasKecoSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? (
|
||||
<TechReviewOpinionTable items={kecoRvwList} />
|
||||
) : null}
|
||||
{/*
|
||||
재원협의에서 환경공단 검토의견은 자동 생성
|
||||
{renderOpinionForm("KECO")}
|
||||
*/}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function getRvwOpnnIds(item: FnrsCnsltnRvwOpnnResponseDto) {
|
||||
if (!item.fnrsCnsltnDocNo || item.rvwOpnnSn == null) {
|
||||
throw new Error("검토의견 식별값이 없습니다.");
|
||||
}
|
||||
return [item.fnrsCnsltnDocNo, item.rvwOpnnSn] as const;
|
||||
}
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||
|
||||
return (
|
||||
<RvwOpnnListTable
|
||||
items={items}
|
||||
getRowKey={(item) => `${item.fnrsCnsltnDocNo}_${item.rvwOpnnSn}`}
|
||||
currentUserUuid={currentUserUuid}
|
||||
onUpdate={async (item, body) => {
|
||||
await fnrsCnsltnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
}}
|
||||
onDelete={async (item) => {
|
||||
await fnrsCnsltnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
|
||||
@@ -3,7 +3,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
|
||||
aply: "aply",
|
||||
del: "del",
|
||||
instlSncnAncmnt: "instlSncnAncmnt",
|
||||
envoSbmsn: "envoSbmsn",
|
||||
rjct: "rjct",
|
||||
sncnAprv: "sncnAprv",
|
||||
rvwRply: "rvwRply",
|
||||
@@ -33,14 +32,16 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
||||
],
|
||||
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
|
||||
},
|
||||
ROLE_USER04: {
|
||||
ST: [INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn, INSTL_SNCN_VIEW_BUTTON_ACTION.rjct],
|
||||
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
|
||||
},
|
||||
ROLE_USER03: {
|
||||
LE: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply, INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
||||
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
||||
},
|
||||
// 환경청 단계 제거: 기존 환경청(LE·TP) 기능을 광역자치단체가 수행
|
||||
ROLE_USER04: {
|
||||
ST: [
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply,
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw,
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.rjct,
|
||||
],
|
||||
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
||||
EP: [INSTL_SNCN_VIEW_BUTTON_ACTION.sncnAprv],
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||
|
||||
@@ -55,16 +55,8 @@ export const instlSncnRegistSchema = z.object({
|
||||
tstmDesignAmt: z.number(),
|
||||
}),
|
||||
),
|
||||
cklstResultList: z.array(
|
||||
instlSncnCklstResultItemSchema.extend({
|
||||
cklstSn: z.number(),
|
||||
cklstArtclSn: z.number(),
|
||||
cklstRvwSn: z.number(),
|
||||
rvwStpComCd: z.string(),
|
||||
selected: z.enum(["Y", "N"]).nullish(),
|
||||
picRvwCn: z.string().nullish(),
|
||||
}),
|
||||
),
|
||||
// 설치인가 화면에서 체크리스트 미사용: 숨겨진 값 때문에 신청·저장 검증이 막히지 않도록 필수값을 두지 않음
|
||||
cklstResultList: z.array(instlSncnCklstResultItemSchema),
|
||||
esntlAtchFileList: z.array(
|
||||
instlSncnEsntlAtchFileItemSchema.extend({
|
||||
esntlAtchFileId: z.string(),
|
||||
|
||||
@@ -50,7 +50,7 @@ export function getEditableReviewStep(
|
||||
dtlInstCd?: string,
|
||||
prgrsSttsComCd?: string,
|
||||
): ReviewStepCode | undefined {
|
||||
if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) {
|
||||
if (dtlInstCd === "ROLE_USER04" && (prgrsSttsComCd === "ST" || prgrsSttsComCd === "TP")) {
|
||||
return "ENV";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,231 +1,231 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<InstlSncnMngResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "설치인가 신청",
|
||||
envoReview: "설치인가 검토",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "설치인가 신청 (시군)", sttsComCd: "ST" },
|
||||
{ label: "설치인가 신청 (시도)", sttsComCd: "LE" },
|
||||
{ label: "설치인가 검토", sttsComCd: "ER" },
|
||||
{ label: "기술검토", sttsComCd: "TE" },
|
||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||
{ label: "검토결과 회신", sttsComCd: "EP" },
|
||||
{ label: "설치인가 승인", sttsComCd: "TA" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "SV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TP" || prgrsSttsComCd === "EP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | null | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
techRplyHstryYn: "Y" | "N";
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={badge.label}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="flex-col justify-center !px-[20px] py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createInstlSncnColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("aplcntNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "신청일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<InstlSncnMngResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "설치인가 신청",
|
||||
envoReview: "설치인가 검토",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "설치인가 신청 (시군)", sttsComCd: "ST" },
|
||||
{ label: "설치인가 신청 (시도)", sttsComCd: "LE" },
|
||||
{ label: "설치인가 검토", sttsComCd: "ER" },
|
||||
{ label: "기술검토", sttsComCd: "TE" },
|
||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||
{ label: "검토결과 회신", sttsComCd: "EP" },
|
||||
{ label: "설치인가 승인", sttsComCd: "TA" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "SV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TP" || prgrsSttsComCd === "EP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | null | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
techRplyHstryYn: "Y" | "N";
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={badge.label}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createInstlSncnColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("aplcntNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "신청일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -50,13 +50,13 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
|
||||
|
||||
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
|
||||
LOCAL: "자가체크",
|
||||
ENV: "환경청검토",
|
||||
ENV: "광역자치단체검토",
|
||||
KECO: "기술검토",
|
||||
};
|
||||
|
||||
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
|
||||
LOCAL: "지자체",
|
||||
ENV: "환경청",
|
||||
ENV: "광역자치단체",
|
||||
KECO: "환경공단",
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ export const INSTL_PROGRESS_ORDER = [
|
||||
|
||||
export const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
||||
ROLE_USER05: "LOCAL",
|
||||
ROLE_USER03: "ENV",
|
||||
ROLE_USER04: "ENV",
|
||||
ROLE_USER01: "KECO",
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
type InstlSncnViewButtonAction,
|
||||
needsRoleUser01AtrzResolve,
|
||||
} from "../../lib/button-config";
|
||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import {
|
||||
buildSavePayload,
|
||||
buildSupplementReqPayload,
|
||||
@@ -179,11 +178,6 @@ function FloatingActionButtons({
|
||||
}
|
||||
|
||||
async function handleAplyClick() {
|
||||
const { percent } = computeReviewStepChecklistProgress(
|
||||
data.cklstResultList ?? [],
|
||||
checklistFormState.results,
|
||||
"LOCAL",
|
||||
);
|
||||
const payload: InstlSncnApplySaveRequestDto = buildSavePayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
@@ -191,11 +185,6 @@ function FloatingActionButtons({
|
||||
"ST",
|
||||
);
|
||||
|
||||
if (percent !== 100) {
|
||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
||||
request: payload,
|
||||
@@ -231,22 +220,6 @@ function FloatingActionButtons({
|
||||
});
|
||||
}
|
||||
|
||||
async function handleEnvoSbmsnClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
docSeComCd,
|
||||
prgrsSttsComCd: "LE",
|
||||
};
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn,
|
||||
request: payload,
|
||||
successMessage: "제출되었습니다.",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.list(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRjctClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
@@ -284,16 +257,6 @@ function FloatingActionButtons({
|
||||
}
|
||||
|
||||
async function handleRvwRplyClick() {
|
||||
const { percent } = computeReviewStepChecklistProgress(
|
||||
data.cklstResultList ?? [],
|
||||
checklistFormState.results,
|
||||
"ENV",
|
||||
);
|
||||
|
||||
if (percent !== 100) {
|
||||
alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildTechReviewReplyPayload(
|
||||
data,
|
||||
@@ -369,16 +332,6 @@ function FloatingActionButtons({
|
||||
}
|
||||
|
||||
async function handleRvwCmptnClick() {
|
||||
const { percent } = computeReviewStepChecklistProgress(
|
||||
data.cklstResultList ?? [],
|
||||
checklistFormState.results,
|
||||
"KECO",
|
||||
);
|
||||
|
||||
if (percent !== 100) {
|
||||
alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...buildTechReviewReplyPayload(
|
||||
@@ -447,11 +400,6 @@ function FloatingActionButtons({
|
||||
설치인가 고시
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.envoSbmsn) && (
|
||||
<Button onClick={handleEnvoSbmsnClick} disabled={isProcessing}>
|
||||
환경청제출
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.rjct) && (
|
||||
<Button color="red" onClick={handleRjctClick} disabled={isProcessing}>
|
||||
반려
|
||||
@@ -618,7 +566,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
||||
aply: handleSaveApply,
|
||||
del: handleDel,
|
||||
instlSncnAncmnt: handleChangePrgrsSttsComCd,
|
||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
||||
rjct: handleChangePrgrsSttsComCd,
|
||||
sncnAprv: handleChangePrgrsSttsComCd,
|
||||
rvwRply: handleTechReview,
|
||||
|
||||
@@ -20,9 +20,7 @@ import {
|
||||
AttachmentsFormSection,
|
||||
BusinessCostFormSection,
|
||||
BusinessScaleFormSection,
|
||||
ChecklistFormSection,
|
||||
ContactInfoFormSection,
|
||||
isFormChecklistComplete,
|
||||
isFormEsntlAtchComplete,
|
||||
PrivacyConsentSection,
|
||||
ProgressStatusSection,
|
||||
@@ -154,11 +152,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFormChecklistComplete(data.cklstResultList)) {
|
||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
||||
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
||||
return;
|
||||
@@ -228,7 +221,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
<PrivacyConsentSection />
|
||||
<BusinessScaleFormSection />
|
||||
<BusinessCostFormSection />
|
||||
<ChecklistFormSection docSeComCd="INSTL" fieldPrefix="instlSncnMng" />
|
||||
<AttachmentsFormSection
|
||||
docSeComCd="INSTL"
|
||||
fieldPrefix="instlSncnMng"
|
||||
|
||||
@@ -1,149 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useRef } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import {
|
||||
AttachmentsViewSection,
|
||||
ProgressStatusSection,
|
||||
SupplementDemandSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
||||
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
import { BusinessCostSection } from "./sections/business-cost-section";
|
||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
import { ChecklistSection } from "./sections/checklist-section";
|
||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||
import {
|
||||
TechReviewOpinionSection,
|
||||
type TechReviewOpinionSectionHandle,
|
||||
} from "./sections/tech-review-opinion-section";
|
||||
|
||||
interface InstlViewPageProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
||||
|
||||
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||
{},
|
||||
{ enabled: needsAtrzResolve },
|
||||
);
|
||||
const atrzSeComCdList = useMemo(
|
||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||
[rvwLoginUserData],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[32rem] rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
const {
|
||||
detDto,
|
||||
bizSclList,
|
||||
bizCstList,
|
||||
cklstResultList = [],
|
||||
esntlFileList,
|
||||
etcFileList,
|
||||
rvwList = [],
|
||||
splmntDmndList = [],
|
||||
sttsChgHstryList = [],
|
||||
} = data;
|
||||
|
||||
const editableReviewStep = getEditableReviewStep(
|
||||
userInfo?.dtlInstCd,
|
||||
detDto.prgrsSttsComCd || "",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<ProgressStatusSection
|
||||
columns={PROGRESS_STATUS_COLUMNS}
|
||||
sttsChgHstryList={sttsChgHstryList}
|
||||
elaprDocNo={detDto.instlSncnDocNo}
|
||||
/>
|
||||
<BasicInfoSection det={detDto} />
|
||||
<ContactInfoSection det={detDto} />
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<BusinessCostSection bizCstList={bizCstList} />
|
||||
<ChecklistSection
|
||||
cklstResultList={cklstResultList}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
userRoleCode={userInfo?.dtlInstCd}
|
||||
formState={checklistFormState}
|
||||
setFormState={setChecklistFormState}
|
||||
/>
|
||||
<AttachmentsViewSection
|
||||
esntlFileList={esntlFileList ?? []}
|
||||
etcFileList={etcFileList ?? []}
|
||||
/>
|
||||
<TechReviewOpinionSection
|
||||
ref={techReviewOpinionRef}
|
||||
rvwList={rvwList}
|
||||
editableReviewStep={editableReviewStep}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
/>
|
||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||
<FloatingActionButtons
|
||||
data={data}
|
||||
docSeComCd="INSTL"
|
||||
instlSncnDocNo={detDto.instlSncnDocNo}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
dtlInstCd={userInfo?.dtlInstCd}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
checklistFormState={checklistFormState}
|
||||
techReviewOpinionRef={techReviewOpinionRef}
|
||||
opnrNm={userInfo?.userName}
|
||||
/>
|
||||
</div>
|
||||
</SectionCollapsibleGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { InstlViewPage };
|
||||
"use client";
|
||||
|
||||
import { useMemo, useRef } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import {
|
||||
AttachmentsViewSection,
|
||||
ProgressStatusSection,
|
||||
SupplementDemandSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
|
||||
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
import { BusinessCostSection } from "./sections/business-cost-section";
|
||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||
import {
|
||||
TechReviewOpinionSection,
|
||||
type TechReviewOpinionSectionHandle,
|
||||
} from "./sections/tech-review-opinion-section";
|
||||
|
||||
interface InstlViewPageProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||
const { formState: checklistFormState } = useChecklistFormState(
|
||||
data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST,
|
||||
);
|
||||
|
||||
const prgrsSttsComCd = data?.detDto?.prgrsSttsComCd || "";
|
||||
const needsAtrzResolve = needsRoleUser01AtrzResolve(userInfo?.dtlInstCd, prgrsSttsComCd);
|
||||
const { data: rvwLoginUserData, isLoading: isRvwLoginUserLoading } = useRvwMnpwMngLoginUserList(
|
||||
{},
|
||||
{ enabled: needsAtrzResolve },
|
||||
);
|
||||
const atrzSeComCdList = useMemo(
|
||||
() => rvwLoginUserData?.content?.map((item) => item.atrzSeComCd) ?? [],
|
||||
[rvwLoginUserData],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[32rem] rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
const {
|
||||
detDto,
|
||||
bizSclList,
|
||||
bizCstList,
|
||||
esntlFileList,
|
||||
etcFileList,
|
||||
rvwList = [],
|
||||
splmntDmndList = [],
|
||||
sttsChgHstryList = [],
|
||||
} = data;
|
||||
|
||||
const editableReviewStep = getEditableReviewStep(
|
||||
userInfo?.dtlInstCd,
|
||||
detDto.prgrsSttsComCd || "",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<ProgressStatusSection
|
||||
columns={PROGRESS_STATUS_COLUMNS}
|
||||
sttsChgHstryList={sttsChgHstryList}
|
||||
elaprDocNo={detDto.instlSncnDocNo}
|
||||
/>
|
||||
<BasicInfoSection det={detDto} />
|
||||
<ContactInfoSection det={detDto} />
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<BusinessCostSection bizCstList={bizCstList} />
|
||||
<AttachmentsViewSection
|
||||
esntlFileList={esntlFileList ?? []}
|
||||
etcFileList={etcFileList ?? []}
|
||||
/>
|
||||
<TechReviewOpinionSection
|
||||
ref={techReviewOpinionRef}
|
||||
rvwList={rvwList}
|
||||
editableReviewStep={editableReviewStep}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isAtrzLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
/>
|
||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||
<FloatingActionButtons
|
||||
data={data}
|
||||
docSeComCd="INSTL"
|
||||
instlSncnDocNo={detDto.instlSncnDocNo}
|
||||
prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
|
||||
dtlInstCd={userInfo?.dtlInstCd}
|
||||
atrzSeComCdList={atrzSeComCdList}
|
||||
isRvwLoginUserLoading={needsAtrzResolve && isRvwLoginUserLoading}
|
||||
checklistFormState={checklistFormState}
|
||||
techReviewOpinionRef={techReviewOpinionRef}
|
||||
opnrNm={userInfo?.userName}
|
||||
/>
|
||||
</div>
|
||||
</SectionCollapsibleGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { InstlViewPage };
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
|
||||
const { control, watch, setValue } = useFormContext<InstlSncnRegistFormData>();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const taskSeComCd = watch("instlSncnMng.taskSeComCd");
|
||||
const localGovLabel = [watch("instlSncnMng.sido"), watch("instlSncnMng.gugun")]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const sido = watch("instlSncnMng.sido");
|
||||
const gugun = watch("instlSncnMng.gugun");
|
||||
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
|
||||
|
||||
function handleBnsSelect(data: BnsApplyData) {
|
||||
setValue("instlSncnMng.bizNm", data.bizNm);
|
||||
@@ -53,6 +54,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 사업구분, 시설종류, 재이용용도 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface BasicInfoSectionProps {
|
||||
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
|
||||
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 구분, 신청지자체, 사업구분 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
|
||||
@@ -1,350 +1,350 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
||||
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: InstlSncnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionFormData | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: InstlSncnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteKecoOpinion, editableReviewStep, formData],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return null;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleFileRemove();
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" type="button" asChild>
|
||||
<span>파일 선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasEnvSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
{hasKecoSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
<col width="5%" />
|
||||
<col width="20%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>의견자명</TableHead>
|
||||
<TableHead>의견내용</TableHead>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead>등록일시</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={`${item.instlSncnDocNo}_${item.rvwOpnnSn}`}>
|
||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<AttachmentDownloadLink
|
||||
atflCd={item.atchFileInfo.atflCd}
|
||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import {
|
||||
instlSncnKeys,
|
||||
type InstlSncnRvwOpnnResponseDto,
|
||||
instlSncnRvwOpnnService,
|
||||
type ReviewStepCode,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type { ReviewOpinionFormData } from "../../lib/review-opinion-utils";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: InstlSncnRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionFormData | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: InstlSncnRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteKecoOpinion, editableReviewStep, formData],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return false;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (!isReviewTurn(reviewStep)) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table
|
||||
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||
variant="secondary"
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row" className="w-[12rem]">
|
||||
의견내용
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleFileRemove();
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" type="button" asChild>
|
||||
<span>파일 선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasEnvSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{envRvwList.length > 0 ? (
|
||||
<TechReviewOpinionTable items={envRvwList} />
|
||||
) : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
{hasKecoSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? (
|
||||
<TechReviewOpinionTable items={kecoRvwList} />
|
||||
) : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function getRvwOpnnIds(item: InstlSncnRvwOpnnResponseDto) {
|
||||
if (!item.instlSncnDocNo || item.rvwOpnnSn == null) {
|
||||
throw new Error("검토의견 식별값이 없습니다.");
|
||||
}
|
||||
return [item.instlSncnDocNo, item.rvwOpnnSn] as const;
|
||||
}
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||
|
||||
return (
|
||||
<RvwOpnnListTable
|
||||
items={items}
|
||||
getRowKey={(item) => `${item.instlSncnDocNo}_${item.rvwOpnnSn}`}
|
||||
currentUserUuid={currentUserUuid}
|
||||
onUpdate={async (item, body) => {
|
||||
await instlSncnRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
}}
|
||||
onDelete={async (item) => {
|
||||
await instlSncnRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
|
||||
@@ -1,235 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "기본계획 신청",
|
||||
envoReview: "기본계획 검토",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "기본계획 신청 (시군)", sttsComCd: "LE" },
|
||||
{ label: "기본계획 검토", sttsComCd: "ER" },
|
||||
{ label: "기술검토", sttsComCd: "TE" },
|
||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||
{ label: "횐경청 검토 결과 회신", sttsComCd: "EP" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "SV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "EP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
techRplyHstryYn: "Y" | "N";
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={`${badge.label}-${index}`}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="flex-col justify-center !px-[20px] py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createSewerBscPlanColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("sewerBscPlanNm", {
|
||||
header: "기본계획명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("sewerBscPlanSeCdNm", {
|
||||
header: "계획구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) =>
|
||||
formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("rgtrNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "등록일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
apply: "기본계획 신청",
|
||||
envoReview: "기본계획 검토",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
} as const;
|
||||
|
||||
export const PROGRESS_STATUS_COLUMNS = [
|
||||
{ label: "기본계획 신청 (시군)", sttsComCd: "LE" },
|
||||
{ label: "기본계획 검토", sttsComCd: "ER" },
|
||||
{ label: "기술검토", sttsComCd: "TE" },
|
||||
{ label: "기술검토 회신", sttsComCd: "TP" },
|
||||
{ label: "횐경청 검토 결과 회신", sttsComCd: "EP" },
|
||||
];
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] {
|
||||
if (prgrsSttsComCd === "SV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "current" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "EP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{
|
||||
label: MAIN_STEP_LABELS.techReview,
|
||||
state: techRplyHstryYn === "Y" ? "completed" : "pending",
|
||||
},
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
}
|
||||
}
|
||||
|
||||
function getBadgeStateText(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "완료";
|
||||
case "current":
|
||||
return "진행중";
|
||||
case "pending":
|
||||
return "대기";
|
||||
}
|
||||
}
|
||||
|
||||
/** 단계명을 2줄로 분리: "설치인가 신청" → ["설치인가", "신청"], "기술검토" → ["기술검토", ""] */
|
||||
function splitStepLabel(label: string) {
|
||||
const [title = "", ...rest] = label.split(" ");
|
||||
return { title, detail: rest.join(" ") };
|
||||
}
|
||||
|
||||
function formatOptional(value: string | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
techRplyHstryYn: "Y" | "N";
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={`${badge.label}-${index}`}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
{/* 1줄: 단계명 / 2줄: 세부단계 + 상태 (예: 설치인가 / 신청완료, 기술검토 / 완료) */}
|
||||
<Badge
|
||||
color={getBadgeColor(badge.state)}
|
||||
className="w-[100px] flex-col justify-center !px-0 py-1 text-center leading-tight"
|
||||
>
|
||||
<span>{splitStepLabel(badge.label).title}</span>
|
||||
<span>
|
||||
{splitStepLabel(badge.label).detail}
|
||||
{getBadgeStateText(badge.state)}
|
||||
</span>
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createSewerBscPlanColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("sewerBscPlanNm", {
|
||||
header: "기본계획명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("sewerBscPlanSeCdNm", {
|
||||
header: "계획구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) =>
|
||||
formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("rgtrNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
techRplyHstryYn={row.original.techRplyHstryYn || "N"}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "등록일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { truncateDecimals } from "@/widgets/pbadms-sprt";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -21,14 +19,6 @@ import {
|
||||
|
||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||
|
||||
const PreviousPlanLookupDialog = dynamic(
|
||||
() =>
|
||||
import("../previous-plan-lookup-dialog").then((mod) => ({
|
||||
default: mod.PreviousPlanLookupDialog,
|
||||
})),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const OTLN_ROWS = [
|
||||
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구", decimals: 0 },
|
||||
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구", decimals: 0 },
|
||||
@@ -37,7 +27,6 @@ const OTLN_ROWS = [
|
||||
|
||||
function OutlineFormSection() {
|
||||
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const [isLookupOpen, setIsLookupOpen] = useState(false);
|
||||
|
||||
const otlnList = useWatch({ control, name: "sewerBscPlanOtlnList" }) ?? [];
|
||||
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
||||
@@ -74,11 +63,7 @@ function OutlineFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<div className="mb-2 flex justify-end px-2">
|
||||
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
@@ -126,13 +111,6 @@ function OutlineFormSection() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<PreviousPlanLookupDialog
|
||||
open={isLookupOpen}
|
||||
onOpenChange={setIsLookupOpen}
|
||||
initialSidoNm={sido}
|
||||
initialGugunNm={gugun}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import type {
|
||||
SewerBscPlanOtlnResponseDto,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import { fmtNumber } from "@/widgets/pbadms-sprt";
|
||||
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface OutlineSectionProps {
|
||||
otlnList: SewerBscPlanOtlnResponseDto[];
|
||||
dtl?: SewerBscPlanDtlResponseDto;
|
||||
sido?: string | null;
|
||||
gugun?: string | null;
|
||||
}
|
||||
|
||||
const OTLN_ROWS = [
|
||||
@@ -19,7 +22,7 @@ const OTLN_ROWS = [
|
||||
{ key: "sewerPsplyRt" as const, label: "하수도보급율" },
|
||||
];
|
||||
|
||||
function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||
function OutlineSection({ otlnList, dtl, sido, gugun }: OutlineSectionProps) {
|
||||
const stepHeaders = [
|
||||
{ planStpNo: 1, label: "1단계", year: dtl?.frlvlGoalYr },
|
||||
{ planStpNo: 2, label: "2단계", year: dtl?.sclvlGoalYr },
|
||||
@@ -29,6 +32,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
|
||||
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
|
||||
@@ -1,357 +1,354 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
|
||||
import type { SewerBscPlanRvwOpnnResponseDto } from "@/entities/sewer-bsc-plan";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type {
|
||||
ReviewOpinionFormData,
|
||||
ReviewOpinionGetFormDataResult,
|
||||
} from "../../lib/review-opinion-utils";
|
||||
import type { ReviewStepCode } from "../../model/view-types";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: SewerBscPlanRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: SewerBscPlanRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
},
|
||||
rvwList,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return null;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleFileRemove();
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" type="button" asChild>
|
||||
<span>파일 선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasEnvSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{envRvwList.length > 0 ? <TechReviewOpinionTable items={envRvwList} /> : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
{hasKecoSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? <TechReviewOpinionTable items={kecoRvwList} /> : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
<col width="5%" />
|
||||
<col width="20%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>의견자명</TableHead>
|
||||
<TableHead>의견내용</TableHead>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead>등록일시</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={`${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}>
|
||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.rvwRsltAnmlyYn === "Y" ? "이상" : item.rvwRsltAnmlyYn === "N" ? "정상" : "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<AttachmentDownloadLink
|
||||
atflCd={item.atchFileInfo.atflCd}
|
||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getDefaultFileKey, useTus, useUserStore } from "@/shared/lib";
|
||||
import { TextShimmer } from "@/shared/ui";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import {
|
||||
sewerBscPlanKeys,
|
||||
type SewerBscPlanRvwOpnnResponseDto,
|
||||
sewerBscPlanRvwOpnnService,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import { RvwOpnnListTable } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type {
|
||||
ReviewOpinionFormData,
|
||||
ReviewOpinionGetFormDataResult,
|
||||
} from "../../lib/review-opinion-utils";
|
||||
import type { ReviewStepCode } from "../../model/view-types";
|
||||
import { REVIEW_STEP_LABELS } from "../../model/view-types";
|
||||
|
||||
const RVW_OPNN_FILE_KEY_PREFIX = "rvw-opnn::";
|
||||
|
||||
interface TechReviewOpinionSectionProps {
|
||||
rvwList: SewerBscPlanRvwOpnnResponseDto[];
|
||||
editableReviewStep?: ReviewStepCode;
|
||||
atrzSeComCdList?: string[];
|
||||
isAtrzLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface TechReviewOpinionSectionHandle {
|
||||
getFormData: () => ReviewOpinionGetFormDataResult | null;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
interface TechReviewOpinionTableProps {
|
||||
items: SewerBscPlanRvwOpnnResponseDto[];
|
||||
}
|
||||
|
||||
interface UploadedOpinionFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM_DATA: ReviewOpinionFormData = {
|
||||
rvwRsltCn: "",
|
||||
rvwRsltAnmlyYn: "",
|
||||
atchFileGroupId: "",
|
||||
opnrSeComCd: "",
|
||||
};
|
||||
|
||||
function createRvwOpnnUploadKey(file: File) {
|
||||
return `${RVW_OPNN_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function hasFormContent(formData: ReviewOpinionFormData) {
|
||||
return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId);
|
||||
}
|
||||
|
||||
const TechReviewOpinionSection = forwardRef<
|
||||
TechReviewOpinionSectionHandle,
|
||||
TechReviewOpinionSectionProps
|
||||
>(function TechReviewOpinionSection(
|
||||
{ rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
|
||||
ref,
|
||||
) {
|
||||
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
|
||||
const [formData, setFormData] = useState<ReviewOpinionFormData>({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
}));
|
||||
}, [editableReviewStep]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: atflCd }));
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFormData: () => {
|
||||
if (!editableReviewStep || !hasFormContent(formData)) {
|
||||
return null;
|
||||
}
|
||||
if (editableReviewStep === "KECO" && !canWriteKecoOpinion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
...formData,
|
||||
opnrSeComCd: editableReviewStep,
|
||||
},
|
||||
rvwList,
|
||||
};
|
||||
},
|
||||
resetForm: () => {
|
||||
setFormData({
|
||||
...EMPTY_FORM_DATA,
|
||||
opnrSeComCd: editableReviewStep ?? "",
|
||||
});
|
||||
setUploadedFile(null);
|
||||
},
|
||||
}),
|
||||
[canWriteKecoOpinion, editableReviewStep, formData, rvwList],
|
||||
);
|
||||
|
||||
const envRvwList = rvwList.filter((item) => item.opnrSeComCd === "ENV");
|
||||
const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO");
|
||||
|
||||
const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV";
|
||||
const hasKecoSection =
|
||||
kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion);
|
||||
|
||||
if (!hasEnvSection && !hasKecoSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createRvwOpnnUploadKey(file);
|
||||
const existingAtflCd = formData.atchFileGroupId;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setFormData((prev) => ({ ...prev, atchFileGroupId: "" }));
|
||||
}
|
||||
|
||||
function isReviewTurn(reviewStep: ReviewStepCode) {
|
||||
if (editableReviewStep !== reviewStep) return false;
|
||||
if (reviewStep === "KECO" && !canWriteKecoOpinion) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderOpinionForm(reviewStep: ReviewStepCode) {
|
||||
if (!isReviewTurn(reviewStep)) return null;
|
||||
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table
|
||||
caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공"
|
||||
variant="secondary"
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setFormData((prev) => ({ ...prev, rvwRsltCn: event.target.value }))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={formData.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
rvwRsltAnmlyYn: value as ReviewOpinionFormData["rvwRsltAnmlyYn"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-normal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-normal-${reviewStep}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-abnormal-${reviewStep}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-abnormal-${reviewStep}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{uploadedFile.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleFileRemove();
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" type="button" asChild>
|
||||
<span>파일 선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasEnvSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.ENV} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{envRvwList.length > 0 ? (
|
||||
<TechReviewOpinionTable items={envRvwList} />
|
||||
) : null}
|
||||
{renderOpinionForm("ENV")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
{hasKecoSection ? (
|
||||
<SectionCollapsible title={`${REVIEW_STEP_LABELS.KECO} 검토의견`}>
|
||||
<div className="space-y-6">
|
||||
{kecoRvwList.length > 0 ? (
|
||||
<TechReviewOpinionTable items={kecoRvwList} />
|
||||
) : null}
|
||||
{renderOpinionForm("KECO")}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function getRvwOpnnIds(item: SewerBscPlanRvwOpnnResponseDto) {
|
||||
if (!item.sewerBscPlanDocNo || item.chgCycl == null || item.rvwOpnnSn == null) {
|
||||
throw new Error("검토의견 식별값이 없습니다.");
|
||||
}
|
||||
return [item.sewerBscPlanDocNo, item.chgCycl, item.rvwOpnnSn] as const;
|
||||
}
|
||||
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const currentUserUuid = useUserStore((state) => state.userInfo?.userUnqId);
|
||||
|
||||
return (
|
||||
<RvwOpnnListTable
|
||||
items={items}
|
||||
getRowKey={(item) => `${item.sewerBscPlanDocNo}_${item.rvwOpnnSn}`}
|
||||
currentUserUuid={currentUserUuid}
|
||||
onUpdate={async (item, body) => {
|
||||
await sewerBscPlanRvwOpnnService.update(...getRvwOpnnIds(item), body);
|
||||
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
}}
|
||||
onDelete={async (item) => {
|
||||
await sewerBscPlanRvwOpnnService.delete(...getRvwOpnnIds(item));
|
||||
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { TechReviewOpinionSection };
|
||||
|
||||
@@ -114,7 +114,12 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
|
||||
<BasicInfoSection det={detDto} dtl={activeDtlDto} />
|
||||
<ContactInfoSection dtl={activeDtlDto} />
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.sewerBscPlanDocNo} />
|
||||
<OutlineSection otlnList={otlnList} dtl={activeDtlDto} />
|
||||
<OutlineSection
|
||||
otlnList={otlnList}
|
||||
dtl={activeDtlDto}
|
||||
sido={detDto.sido}
|
||||
gugun={detDto.gugun}
|
||||
/>
|
||||
<BizCstMatrixSection bizCstList={bizCstList} dtl={activeDtlDto} />
|
||||
<FnrsCstSection fnrsCstList={fnrsCstList} dtl={activeDtlDto} />
|
||||
<TmplntSection tmplntList={tmplntList} det={detDto} dtl={activeDtlDto} />
|
||||
|
||||
@@ -12,11 +12,13 @@ export const ENDPOINT = {
|
||||
RVW_MNPW: "/as-service/api/v1/rvw-mnpw",
|
||||
BNS_IF: "/as-service/api/v1/if",
|
||||
INSTL_SNCN: "/as-service/api/v1/instl-sncn",
|
||||
INSTL_SNCN_RVW_OPNN: "/as-service/api/v1/instl-sncn-rvw-opnn",
|
||||
CKLST: "/as-service/api/v1/cklst",
|
||||
CKLST_ARTCL: "/as-service/api/v1/cklst-artcl",
|
||||
CKLST_RVW: "/as-service/api/v1/cklst-rvw",
|
||||
ADSTDSTR: "/as-service/api/v1/adstdstr",
|
||||
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
||||
FNRS_CNSLTN_RVW_OPNN: "/as-service/api/v1/fnrs-cnsltn-rvw-opnn",
|
||||
FNRS_SHAR_SE: "/as-service/api/v1/fnrs-shar-se-mng",
|
||||
DESIGN_CNSTN: "/as-service/api/v1/design-cnstn",
|
||||
DESIGN_CNSTN_BIZ_CST: "/as-service/api/v1/design-cnstn-biz-cst",
|
||||
@@ -24,6 +26,7 @@ export const ENDPOINT = {
|
||||
DESIGN_CNSTN_ESNTL_ATCH_FILE: "/as-service/api/v1/design-cnstn-esntl-atch-file",
|
||||
DESIGN_CNSTN_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
|
||||
SEWER_BSC_PLAN: "/as-service/api/v1/sewer-bsc-plans",
|
||||
SEWER_BSC_PLAN_RVW_OPNNS: "/as-service/api/v1/sewer-bsc-plan-rvw-opnns",
|
||||
DASHBOARD: "/as-service/api/v1/dashboard",
|
||||
DCRAG_TO: "/as-service/api/v1/dcrag-to",
|
||||
AI_AUTO_PRCS_MNG: "/as-service/api/v1/ai-auto-prcs-mng",
|
||||
|
||||
@@ -46,6 +46,7 @@ export type {
|
||||
PbadmsMngDetResponseBase,
|
||||
RvwOpnnResponseExtension,
|
||||
RvwOpnnSaveBase,
|
||||
RvwOpnnUpdateRequestDto,
|
||||
SplmntDmndResponseExtension,
|
||||
SplmntDmndSaveBase,
|
||||
} from "./pbadms-sprt";
|
||||
|
||||
@@ -231,11 +231,22 @@ export interface PbadmsApplySaveHeader {
|
||||
rvwStpCd?: string;
|
||||
}
|
||||
|
||||
/** 검토의견 수정 공통 요청 DTO */
|
||||
export interface RvwOpnnUpdateRequestDto {
|
||||
opnrSeComCd?: string;
|
||||
opnrNm?: string;
|
||||
rvwRsltCn?: string;
|
||||
rvwRsltAnmlyYn?: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 검토의견 응답 공통 확장 필드 */
|
||||
export interface RvwOpnnResponseExtension {
|
||||
opnrSeCdNm?: string;
|
||||
atchFileInfo?: AtflMngClientResponseDto;
|
||||
regDt?: string;
|
||||
rgtr?: string;
|
||||
rgtrNm?: string;
|
||||
}
|
||||
|
||||
/** 보완요구 응답 공통 확장 필드 */
|
||||
|
||||
@@ -51,6 +51,7 @@ export {
|
||||
} from "./ui/pbadms-task-apply-dialog";
|
||||
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
|
||||
export { ProgressStatusSection } from "./ui/progress-status-section";
|
||||
export { RvwOpnnListTable, type RvwOpnnListTableProps } from "./ui/rvw-opnn-list-table";
|
||||
export {
|
||||
SplmntDmndDialog,
|
||||
type SplmntDmndDialogConfirmItem,
|
||||
|
||||
@@ -404,11 +404,17 @@ function AttachmentsFormSection({
|
||||
<TableHead
|
||||
scope="row"
|
||||
className="group break-keep font-medium"
|
||||
data-required={file.esntlAtchYn === "Y"}
|
||||
align="left"
|
||||
>
|
||||
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
|
||||
{file.esntlAtchFileNm}
|
||||
<span>
|
||||
{file.esntlAtchFileNm}
|
||||
{file.esntlAtchYn === "Y" && (
|
||||
<span className="whitespace-nowrap text-red-600" title="필수 항목">
|
||||
{" "}(*)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{atflCd ? (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import type { AtflMngClientResponseDto, RvwOpnnUpdateRequestDto } from "@/shared/model";
|
||||
import { AttachmentFileField } from "@/shared/ui";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import {
|
||||
AttachmentDownloadLink,
|
||||
attachService,
|
||||
useDownloadAttachmentByAtflId,
|
||||
} from "@/entities/attach";
|
||||
import {
|
||||
Button,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
const RVW_OPNN_EDIT_FILE_KEY_PREFIX = "rvw-opnn-edit::";
|
||||
|
||||
/** 업무별 검토의견 응답 DTO 공통 필드 (업무마다 null 허용 여부가 달라 느슨하게 정의) */
|
||||
interface RvwOpnnListItem {
|
||||
opnrSeComCd?: string | null;
|
||||
opnrNm?: string | null;
|
||||
rvwRsltCn?: string | null;
|
||||
rvwRsltAnmlyYn?: string | null;
|
||||
atchFileGroupId?: string | null;
|
||||
atchFileInfo?: AtflMngClientResponseDto | null;
|
||||
regDt?: string | null;
|
||||
rgtr?: string | null;
|
||||
}
|
||||
|
||||
interface RvwOpnnListTableProps<T extends RvwOpnnListItem> {
|
||||
items: T[];
|
||||
getRowKey: (item: T) => string;
|
||||
/** 로그인 사용자 UUID — 검토의견 등록자(rgtr)와 같을 때만 수정·삭제 버튼 노출 */
|
||||
currentUserUuid?: string;
|
||||
onUpdate: (item: T, body: RvwOpnnUpdateRequestDto) => Promise<void>;
|
||||
onDelete: (item: T) => Promise<void>;
|
||||
}
|
||||
|
||||
interface EditDraft {
|
||||
rvwRsltCn: string;
|
||||
rvwRsltAnmlyYn: string;
|
||||
atchFileGroupId: string;
|
||||
}
|
||||
|
||||
interface EditFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
/** 이번 수정 중 새로 업로드한 파일 여부 (취소 시 정리 대상) */
|
||||
isNew: boolean;
|
||||
}
|
||||
|
||||
function formatAnmlyYn(value?: string | null) {
|
||||
if (value === "Y") return "이상";
|
||||
if (value === "N") return "정상";
|
||||
return "-";
|
||||
}
|
||||
|
||||
function RvwOpnnListTable<T extends RvwOpnnListItem>({
|
||||
items,
|
||||
getRowKey,
|
||||
currentUserUuid,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: RvwOpnnListTableProps<T>) {
|
||||
const { confirm } = useConfirm();
|
||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<EditDraft | null>(null);
|
||||
const [editFile, setEditFile] = useState<EditFile | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
|
||||
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: atflCd } : prev));
|
||||
setEditFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setEditFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
const editFileUploading = editFile ? (isUploading[editFile.fileKey] ?? false) : false;
|
||||
|
||||
function canModify(item: T) {
|
||||
return Boolean(currentUserUuid) && item.rgtr === currentUserUuid;
|
||||
}
|
||||
|
||||
async function discardNewFile(file: EditFile | null) {
|
||||
if (!file?.isNew) return;
|
||||
|
||||
uploads[file.fileKey]?.abort();
|
||||
if (file.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(file.atflId);
|
||||
} catch (error) {
|
||||
console.error("검토의견 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetEdit() {
|
||||
setEditingKey(null);
|
||||
setDraft(null);
|
||||
setEditFile(null);
|
||||
}
|
||||
|
||||
function startEdit(item: T) {
|
||||
void discardNewFile(editFile);
|
||||
|
||||
setEditingKey(getRowKey(item));
|
||||
setDraft({
|
||||
rvwRsltCn: item.rvwRsltCn ?? "",
|
||||
rvwRsltAnmlyYn: item.rvwRsltAnmlyYn ?? "",
|
||||
atchFileGroupId: item.atchFileGroupId ?? "",
|
||||
});
|
||||
setEditFile(
|
||||
item.atchFileInfo?.atflCd
|
||||
? {
|
||||
fileName: item.atchFileInfo.orgnlFileNm || "첨부파일",
|
||||
fileKey: `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${item.atchFileInfo.atflCd}`,
|
||||
atflId: item.atchFileInfo.atflId,
|
||||
atflCd: item.atchFileInfo.atflCd,
|
||||
isNew: false,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
void discardNewFile(editFile);
|
||||
resetEdit();
|
||||
}
|
||||
|
||||
async function handleFileAdd(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = `${RVW_OPNN_EDIT_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
|
||||
|
||||
setEditFile({ fileName: file.name, fileKey, isNew: true });
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, { fileKey });
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileRemove() {
|
||||
// 기존 파일은 저장 시점에 연결만 해제하고, 새로 올린 파일은 즉시 정리
|
||||
void discardNewFile(editFile);
|
||||
setEditFile(null);
|
||||
setDraft((prev) => (prev ? { ...prev, atchFileGroupId: "" } : prev));
|
||||
}
|
||||
|
||||
async function handleSave(item: T) {
|
||||
if (!draft) return;
|
||||
if (!draft.rvwRsltCn.trim()) {
|
||||
alert("의견내용을 입력하세요.");
|
||||
return;
|
||||
}
|
||||
if (!draft.rvwRsltAnmlyYn) {
|
||||
alert("검토결과를 선택하세요.");
|
||||
return;
|
||||
}
|
||||
if (!(await confirm("검토의견을 수정하시겠습니까?"))) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onUpdate(item, {
|
||||
opnrSeComCd: item.opnrSeComCd ?? undefined,
|
||||
opnrNm: item.opnrNm ?? undefined,
|
||||
rvwRsltCn: draft.rvwRsltCn,
|
||||
rvwRsltAnmlyYn: draft.rvwRsltAnmlyYn,
|
||||
atchFileGroupId: draft.atchFileGroupId,
|
||||
});
|
||||
alert("수정되었습니다.");
|
||||
resetEdit();
|
||||
} catch (error) {
|
||||
console.error("검토의견 수정 실패:", error);
|
||||
alert("검토의견 수정 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item: T) {
|
||||
if (!(await confirm("검토의견을 삭제하시겠습니까?"))) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onDelete(item);
|
||||
alert("삭제되었습니다.");
|
||||
if (editingKey === getRowKey(item)) resetEdit();
|
||||
} catch (error) {
|
||||
console.error("검토의견 삭제 실패:", error);
|
||||
alert("검토의견 삭제 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderViewRow(item: T) {
|
||||
return (
|
||||
<>
|
||||
<TableCell align="left">{item.rvwRsltCn || "-"}</TableCell>
|
||||
<TableCell align="center">{formatAnmlyYn(item.rvwRsltAnmlyYn)}</TableCell>
|
||||
<TableCell align="center">
|
||||
{item.atchFileInfo?.atflCd ? (
|
||||
<AttachmentDownloadLink
|
||||
atflCd={item.atchFileInfo.atflCd}
|
||||
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderEditRow(rowKey: string, editDraft: EditDraft) {
|
||||
return (
|
||||
<>
|
||||
<TableCell align="left">
|
||||
<Textarea
|
||||
value={editDraft.rvwRsltCn}
|
||||
onChange={(event) =>
|
||||
setDraft((prev) => (prev ? { ...prev, rvwRsltCn: event.target.value } : prev))
|
||||
}
|
||||
placeholder="의견내용을 입력하세요"
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={editDraft.rvwRsltAnmlyYn}
|
||||
onValueChange={(value) =>
|
||||
setDraft((prev) => (prev ? { ...prev, rvwRsltAnmlyYn: value } : prev))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-edit-normal-${rowKey}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`rvw-opnn-edit-normal-${rowKey}`} />
|
||||
<span className="font-normal">정상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`rvw-opnn-edit-abnormal-${rowKey}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`rvw-opnn-edit-abnormal-${rowKey}`} />
|
||||
<span className="font-normal">이상</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<AttachmentFileField
|
||||
files={
|
||||
editFile
|
||||
? [
|
||||
{
|
||||
fileKey: editFile.fileKey,
|
||||
fileName: editFile.fileName,
|
||||
onDownload: editFile.atflId
|
||||
? () => downloadByAtflId(editFile.atflId!)
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
inputId={`rvw-opnn-edit-${rowKey}-file-input`}
|
||||
isUploadingByKey={isUploading}
|
||||
multiple={false}
|
||||
showAddButton={!editFile}
|
||||
addDisabled={editFileUploading}
|
||||
onAdd={(fileList) => void handleFileAdd(fileList)}
|
||||
onRemove={handleFileRemove}
|
||||
/>
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시, 관리 정보 제공"
|
||||
variant="secondary"
|
||||
>
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="47%" />
|
||||
<col width="6%" />
|
||||
<col width="17%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>의견자명</TableHead>
|
||||
<TableHead>의견내용</TableHead>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead>등록일시</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
const rowKey = getRowKey(item);
|
||||
const isEditing = editingKey === rowKey && draft !== null;
|
||||
|
||||
return (
|
||||
<TableRow key={rowKey}>
|
||||
<TableCell align="center">{item.opnrNm}</TableCell>
|
||||
{isEditing ? renderEditRow(rowKey, draft) : renderViewRow(item)}
|
||||
<TableCell align="center">
|
||||
{item.regDt ? dayjs(item.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{!canModify(item) ? (
|
||||
"-"
|
||||
) : isEditing ? (
|
||||
<div className="flex flex-wrap justify-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => void handleSave(item)}
|
||||
disabled={isSubmitting || editFileUploading}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={cancelEdit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap justify-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => startEdit(item)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
onClick={() => void handleDelete(item)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { RvwOpnnListTable, type RvwOpnnListTableProps };
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
PreviousPlanLookupButton,
|
||||
type PreviousPlanLookupButtonProps,
|
||||
} from "./ui/previous-plan-lookup-button";
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { Button } from "@common/components";
|
||||
|
||||
const PreviousPlanLookupDialog = dynamic(
|
||||
() =>
|
||||
import("./previous-plan-lookup-dialog").then((mod) => ({
|
||||
default: mod.PreviousPlanLookupDialog,
|
||||
})),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface PreviousPlanLookupButtonProps {
|
||||
/** 조회 다이얼로그 시도 초기값(표시명) */
|
||||
sidoNm?: string | null;
|
||||
/** 조회 다이얼로그 시군구 초기값(표시명) */
|
||||
gugunNm?: string | null;
|
||||
}
|
||||
|
||||
/** 섹션 상단 우측에 [이전기본계획조회] 버튼과 조회 다이얼로그를 배치한다. */
|
||||
function PreviousPlanLookupButton({ sidoNm, gugunNm }: PreviousPlanLookupButtonProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 flex justify-end px-2">
|
||||
<Button type="button" color="white" onClick={() => setIsOpen(true)}>
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PreviousPlanLookupDialog
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
initialSidoNm={sidoNm}
|
||||
initialGugunNm={gugunNm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanLookupButton };
|
||||
export type { PreviousPlanLookupButtonProps };
|
||||
+33
-14
@@ -51,7 +51,7 @@ function PreviousPlanLookupDialog({
|
||||
const [gnsvyYr, setGnsvyYr] = useState("");
|
||||
const [swtrmtFcltCd, setSwtrmtFcltCd] = useState("");
|
||||
const [hasPrefillApplied, setHasPrefillApplied] = useState(false);
|
||||
const [searchedClsfSystmNm, setSearchedClsfSystmNm] = useState("");
|
||||
const [hasGugunPrefillApplied, setHasGugunPrefillApplied] = useState(false);
|
||||
|
||||
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
|
||||
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
|
||||
@@ -62,15 +62,19 @@ function PreviousPlanLookupDialog({
|
||||
const ctpvNm = sidoOptions.find((option) => option.adstDstrCd === sidoComCd)?.adstDstrNm ?? "";
|
||||
const sggNm = gugunOptions.find((option) => option.adstDstrCd === sggComCd)?.adstDstrNm ?? "";
|
||||
|
||||
// 창 오픈 시 시도/시군구로 조회하고, 콤보 선택에 따라 파라미터가 추가되며 재조회된다.
|
||||
// 빈 값은 서비스에서 제외되며, 시도가 없으면 조회하지 않는다.
|
||||
const searchParams = {
|
||||
swtrmtFcltCd,
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
targetYear: gnsvyYr,
|
||||
swtrmtFcltCd,
|
||||
};
|
||||
const isSearchEnabled = open && hasGugunPrefillApplied && Boolean(ctpvNm);
|
||||
|
||||
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, false);
|
||||
const planData = gnsvyYr && swtrmtFcltCd ? (data?.[0] ?? null) : null;
|
||||
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, isSearchEnabled);
|
||||
const planData = isSearchEnabled ? (data?.[0] ?? null) : null;
|
||||
const clsfSystmNm = planData ? sggNm || ctpvNm : "";
|
||||
|
||||
const { data: yearTree, isPending: isYearPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "2",
|
||||
@@ -97,8 +101,8 @@ function PreviousPlanLookupDialog({
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setSearchedClsfSystmNm("");
|
||||
setHasPrefillApplied(false);
|
||||
setHasGugunPrefillApplied(false);
|
||||
return;
|
||||
}
|
||||
}, [open]);
|
||||
@@ -115,12 +119,27 @@ function PreviousPlanLookupDialog({
|
||||
setHasPrefillApplied(true);
|
||||
}, [open, hasPrefillApplied, isSidoPending, sidoOptions, initialSidoNm]);
|
||||
|
||||
// 시군구 초기값 적용이 끝난 뒤에 최초 조회가 나가도록 완료 여부를 표시한다.
|
||||
useEffect(() => {
|
||||
if (!open || !sidoComCd || isGugunPending || !initialGugunNm) return;
|
||||
if (sggComCd) return;
|
||||
if (!open || !hasPrefillApplied || hasGugunPrefillApplied) return;
|
||||
if (!sidoComCd || !initialGugunNm) {
|
||||
setHasGugunPrefillApplied(true);
|
||||
return;
|
||||
}
|
||||
if (isGugunPending) return;
|
||||
|
||||
const matchedGugun = gugunOptions.find((option) => option.adstDstrNm === initialGugunNm);
|
||||
if (matchedGugun) setSggComCd(matchedGugun.adstDstrCd);
|
||||
}, [open, sidoComCd, isGugunPending, gugunOptions, initialGugunNm, sggComCd]);
|
||||
setHasGugunPrefillApplied(true);
|
||||
}, [
|
||||
open,
|
||||
hasPrefillApplied,
|
||||
hasGugunPrefillApplied,
|
||||
sidoComCd,
|
||||
isGugunPending,
|
||||
gugunOptions,
|
||||
initialGugunNm,
|
||||
]);
|
||||
|
||||
function handleSidoChange(value: string) {
|
||||
setSidoComCd(value);
|
||||
@@ -145,18 +164,18 @@ function PreviousPlanLookupDialog({
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
if (!ctpvNm) {
|
||||
alert("시도를 선택하세요.");
|
||||
return;
|
||||
}
|
||||
const { data: list, error } = await refetch();
|
||||
if (error) {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
setSearchedClsfSystmNm("");
|
||||
return;
|
||||
}
|
||||
if (!list?.length) {
|
||||
alert("조회된 데이터가 없습니다.");
|
||||
setSearchedClsfSystmNm("");
|
||||
return;
|
||||
}
|
||||
setSearchedClsfSystmNm(sggNm || ctpvNm);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -198,8 +217,8 @@ function PreviousPlanLookupDialog({
|
||||
|
||||
<SectionCollapsible title="기본계획파일" defaultOpen>
|
||||
<PreviousPlanFileSection
|
||||
key={searchedClsfSystmNm || "empty"}
|
||||
clsfSystmNm={searchedClsfSystmNm || undefined}
|
||||
key={clsfSystmNm || "empty"}
|
||||
clsfSystmNm={clsfSystmNm || undefined}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
|
||||
@@ -289,14 +289,28 @@ type GridViewWithSetColumnProperty = {
|
||||
};
|
||||
|
||||
type GridViewWithEditOptions = {
|
||||
setEditOptions?: (options: { commitWhenLeave?: boolean }) => void;
|
||||
setEditOptions?: (options: {
|
||||
commitWhenLeave?: boolean;
|
||||
appendable?: boolean;
|
||||
insertable?: boolean;
|
||||
}) => void;
|
||||
setPasteOptions?: (options: { enableAppend?: boolean }) => void;
|
||||
};
|
||||
|
||||
/** 포커스가 그리드 밖으로 나갈 때 편집 중인 값을 자동 commit */
|
||||
function applyDefaultGridEditOptions(
|
||||
gridView: GridViewWithEditOptions | null | undefined,
|
||||
options?: { preventRowAppend?: boolean },
|
||||
) {
|
||||
gridView?.setEditOptions?.({ commitWhenLeave: true });
|
||||
gridView?.setEditOptions?.({
|
||||
commitWhenLeave: true,
|
||||
...(options?.preventRowAppend
|
||||
? { appendable: false, insertable: false }
|
||||
: {}),
|
||||
});
|
||||
if (options?.preventRowAppend) {
|
||||
gridView?.setPasteOptions?.({ enableAppend: false });
|
||||
}
|
||||
}
|
||||
|
||||
function isDateEditorColumn(column: {
|
||||
@@ -853,6 +867,7 @@ const DataGrid = ({
|
||||
gridHeight = 55, // vh 단위
|
||||
headerHeight = 60,
|
||||
fitStyle = GridFitStyle.EVEN,
|
||||
preventRowAppend = true,
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const fixedColumnNameSet = useMemo(
|
||||
@@ -1467,7 +1482,9 @@ useEffect(() => {
|
||||
const scheduleFooterTask = (task: () => void, delayMs: number) => {
|
||||
footerTimerIds.push(setTimeout(task, delayMs));
|
||||
};
|
||||
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions);
|
||||
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions, {
|
||||
preventRowAppend,
|
||||
});
|
||||
|
||||
const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001");
|
||||
if (columnsFieldsSig !== columnsFieldsSigRef.current) {
|
||||
@@ -1990,44 +2007,9 @@ useEffect(() => {
|
||||
fitStyle,
|
||||
enableSort,
|
||||
footerVerificationResult,
|
||||
preventRowAppend,
|
||||
]);
|
||||
|
||||
/** footerVerificationResult만 바뀐 경우에도 총계 스타일 즉시 재적용/초기화 */
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView as
|
||||
| {
|
||||
columnByName?: (name: string) => {
|
||||
footer?: { styleName?: string } | Array<{ styleName?: string }>;
|
||||
} | null;
|
||||
refresh?: () => void;
|
||||
}
|
||||
| undefined;
|
||||
if (!gridView?.columnByName) return;
|
||||
|
||||
columns.forEach((col) => {
|
||||
if (!col.footer) return;
|
||||
try {
|
||||
const column = gridView.columnByName?.(col.name);
|
||||
if (!column?.footer) return;
|
||||
const styleName =
|
||||
resolveFooterVerificationStyleName(
|
||||
col,
|
||||
footerVerificationResult,
|
||||
) ?? "";
|
||||
if (Array.isArray(column.footer)) {
|
||||
column.footer.forEach((item) => {
|
||||
item.styleName = styleName;
|
||||
});
|
||||
} else {
|
||||
column.footer.styleName = styleName;
|
||||
}
|
||||
} catch {
|
||||
console.error('총계 스타일 재적용/초기화 error');
|
||||
}
|
||||
});
|
||||
gridView.refresh?.();
|
||||
}, [footerVerificationResult, columns]);
|
||||
|
||||
// 행·셀 클릭 이벤트 (검증 오류 툴팁은 hover만 — onShowTooltip / 빈 셀 오버레이)
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
@@ -2563,6 +2545,7 @@ useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView ?? grid;
|
||||
applyDefaultGridEditOptions(
|
||||
gridView as GridViewWithEditOptions,
|
||||
{ preventRowAppend },
|
||||
);
|
||||
if (!gridView?.columnByName) return;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -63,7 +63,7 @@ const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] =
|
||||
|
||||
const TITLE = "예외처리 내역 조회";
|
||||
/** 예외처리 건만 */
|
||||
const PRCS_YN_LIST = "2";
|
||||
const VRFC_STTS_CD_LIST = "2";
|
||||
|
||||
export type ExceptionRejectSubmitItem = {
|
||||
vrfcSn: number;
|
||||
@@ -196,8 +196,7 @@ export function ExceptionProcessingHistoryDialog({
|
||||
statsYr: queryContext.statsYr,
|
||||
statsCd: queryContext.statsCd,
|
||||
vrfcMthdCd,
|
||||
prcsYnList: PRCS_YN_LIST,
|
||||
rejectCodeList: "0,1,2",
|
||||
vrfcSttsCdList: VRFC_STTS_CD_LIST,
|
||||
page: pageIndex + 1,
|
||||
size: 30,
|
||||
};
|
||||
|
||||
@@ -232,8 +232,7 @@ export function VerificationResultDialog({
|
||||
statsYr: stableQueryContext.statsYr,
|
||||
statsCd: stableQueryContext.statsCd,
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0",
|
||||
rejectCodeList: "0,1,2",
|
||||
vrfcSttsCdList: "0",
|
||||
page: page + 1,
|
||||
size: 10000,
|
||||
};
|
||||
|
||||
@@ -262,8 +262,7 @@ export function VerificationResultHistoryDialog({
|
||||
statsYr: stableQueryContext.statsYr,
|
||||
statsCd: stableQueryContext.statsCd,
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0,2",
|
||||
rejectCodeList: "0,1,2",
|
||||
vrfcSttsCdList: "0,2",
|
||||
page: pageIndex + 1,
|
||||
size: PAGE_SIZE,
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ function responseHasVerificationResults(response: VerificationResultListResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* 제출 직후 `VerificationResultDialog`에 표시할 미통과(prcsYn=0) 검증 결과가
|
||||
* 제출 직후 `VerificationResultDialog`에 표시할 미통과(vrfcSttsCd=0) 검증 결과가
|
||||
* 일반검증(NV)·I-MR(IMR)·HB 중 하나라도 있는지 조회합니다.
|
||||
*/
|
||||
export async function hasAnyPostSubmitVerificationResult(
|
||||
@@ -34,8 +34,7 @@ export async function hasAnyPostSubmitVerificationResult(
|
||||
sggNm: ctx.sggNm,
|
||||
statsYr: ctx.statsYr,
|
||||
statsCd: ctx.statsCd,
|
||||
prcsYnList: "0",
|
||||
rejectCodeList: "0,1,2",
|
||||
vrfcSttsCdList: "0",
|
||||
page: 1,
|
||||
size: 1,
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import {
|
||||
EXCRETA_TREATMENT_FACILITY_STATS_CD,
|
||||
type ExcretaTreatmentFacilityItem,
|
||||
@@ -75,7 +76,11 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
|
||||
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
|
||||
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
@@ -207,7 +212,7 @@ function mapExcretaTreatmentFacilityListToGridRows(
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
row.verificationResult = normalizeVerificationResultMap(item.verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
@@ -502,7 +507,9 @@ const ExcretaTreatmentFacilityInp = () => {
|
||||
setStatusInfo(nextStatusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
|
||||
setFooterVerificationResult(commonInfo.verificationResult ?? null);
|
||||
setFooterVerificationResult(
|
||||
normalizeVerificationResultMap(commonInfo.verificationResult) ?? null,
|
||||
);
|
||||
if (overrides?.mode !== "PRE") {
|
||||
setIsNotApplicable(gridRows.length === 0);
|
||||
} else if (gridRows.length > 0) {
|
||||
@@ -784,7 +791,6 @@ const ExcretaTreatmentFacilityInp = () => {
|
||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||
noApplicable,
|
||||
submitList,
|
||||
deleteList: deleteListForSubmit,
|
||||
@@ -814,7 +820,13 @@ const ExcretaTreatmentFacilityInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForSubmit,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: statusInfo?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -953,19 +965,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 -";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { statisticsInputPrivateSewageFacilityApi } from "@/entities/statistics/api/input/statistics-input-private-sewage-facility-service";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import type {
|
||||
PrivateSewageDesigStatusItem,
|
||||
PrivateSewageDesigStatusSaveItem,
|
||||
@@ -68,7 +69,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
@@ -175,7 +180,9 @@ function mapRegionalListToGridRows<
|
||||
return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
|
||||
const { verificationResult, ...rest } = item;
|
||||
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
if (verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@@ -797,7 +804,6 @@ const PrivateSewageFacilityInp = () => {
|
||||
|
||||
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
||||
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
|
||||
|
||||
const submitInfoBase = {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
@@ -842,7 +848,6 @@ const PrivateSewageFacilityInp = () => {
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: buildSewageTreatFactStatusSaveList(currentData),
|
||||
...adminOpt,
|
||||
};
|
||||
response =
|
||||
await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus(
|
||||
@@ -853,7 +858,6 @@ const PrivateSewageFacilityInp = () => {
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: buildSepticTankStatusSaveList(currentData),
|
||||
...adminOpt,
|
||||
};
|
||||
response =
|
||||
await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param);
|
||||
@@ -862,7 +866,6 @@ const PrivateSewageFacilityInp = () => {
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: buildPrivateSewageDesigStatusSaveList(currentData),
|
||||
...adminOpt,
|
||||
};
|
||||
response =
|
||||
await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus(
|
||||
@@ -892,7 +895,13 @@ const PrivateSewageFacilityInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -1148,18 +1157,7 @@ const PrivateSewageFacilityInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 - ";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useStatisticsInputPendingNavigation } from '@/shared/lib/use-statistics
|
||||
import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service';
|
||||
import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service';
|
||||
import { statisticsApi } from '@/entities/statistics/api/statistics-service';
|
||||
import { callImrServiceWithRollbackOnFailure } from '@/components/util/input/call-imr-with-rollback';
|
||||
import type {
|
||||
PublicSewageFacilityRequestParams,
|
||||
PublicSewageFacilitySaveItem,
|
||||
@@ -52,7 +53,11 @@ import { fetchColumnGuidesRecord } from '@/components/util/grid/columnGuides';
|
||||
import { toGridRows } from '@/components/util/grid/gridFunction';
|
||||
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
|
||||
import { patchFooterVerificationByTab,patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag';
|
||||
import type { VerificationResultLikeMap } from '@/components/util/input/verification-result-display';
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
withNormalizedVerificationResult,
|
||||
type VerificationResultLikeMap,
|
||||
} from '@/components/util/input/verification-result-display';
|
||||
import { nullableTrimmedString } from '@/components/util/input/nullableString';
|
||||
import { useAdminStore } from '@/store/admin-store';
|
||||
import { useTabStore } from '@/store/tab-store';
|
||||
@@ -438,7 +443,13 @@ const PublicSewageFacilityInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? '',
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? '',
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? '',
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -789,11 +800,13 @@ const PublicSewageFacilityInp = () => {
|
||||
sidoListRef.current,
|
||||
searchContext,
|
||||
);
|
||||
const convertedData = list.map((item) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...item,
|
||||
rmrk: item.rmrk ?? null,
|
||||
}));
|
||||
const convertedData = list.map((item) =>
|
||||
withNormalizedVerificationResult({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...item,
|
||||
rmrk: item.rmrk ?? null,
|
||||
}),
|
||||
);
|
||||
|
||||
if (list.length === 0) {
|
||||
setShowIsNotApplicable(true);
|
||||
@@ -1575,17 +1588,7 @@ const PublicSewageFacilityInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
|
||||
const comp = e.cmprCn ?? "";
|
||||
const now = e.nowCn ?? "";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${comp} / 현재 입력값: ${now})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { statisticsInputPumpingStationApi } from "@/entities/statistics/api/input/statistics-input-pumping-station-service";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import {
|
||||
PUMPING_STATION_STATS_CD,
|
||||
type PumpingStationItem,
|
||||
@@ -75,7 +76,11 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
|
||||
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
|
||||
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
@@ -206,7 +211,7 @@ function mapPumpingStationListToGridRows(
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
row.verificationResult = normalizeVerificationResultMap(item.verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
@@ -493,7 +498,9 @@ const PumpingStationInp = () => {
|
||||
setStatusInfo(nextStatusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
|
||||
setFooterVerificationResult(commonInfo.verificationResult ?? null);
|
||||
setFooterVerificationResult(
|
||||
normalizeVerificationResultMap(commonInfo.verificationResult) ?? null,
|
||||
);
|
||||
if (overrides?.mode !== "PRE") {
|
||||
setIsNotApplicable(gridRows.length === 0);
|
||||
} else if (gridRows.length > 0) {
|
||||
@@ -768,7 +775,6 @@ const PumpingStationInp = () => {
|
||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||
noApplicable,
|
||||
submitList,
|
||||
deleteList,
|
||||
@@ -797,7 +803,13 @@ const PumpingStationInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForSubmit,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: statusInfo?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -936,19 +948,7 @@ const PumpingStationInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 -";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import type {
|
||||
ExcretaRelatedIndustrySaveItem,
|
||||
PrivateSewageTreatRelatedIndustrySaveItem,
|
||||
@@ -84,7 +85,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
@@ -552,19 +557,7 @@ function verificationCellTooltip({
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 - ";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +581,7 @@ function mapListToGridRows<T extends { verificationResult?: SewageExcretaRelated
|
||||
return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
|
||||
const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification;
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
row.verificationResult = normalizeVerificationResultMap(item.verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
@@ -1410,7 +1403,6 @@ const SewageExcretaRelatedInp = () => {
|
||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||
...(isPublicInstitutionTab ? { noApplicable } : {}),
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
|
||||
};
|
||||
|
||||
const verificationQuery = {
|
||||
@@ -1504,7 +1496,13 @@ const SewageExcretaRelatedInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import type {
|
||||
DigestorOperatingStatusSaveItem,
|
||||
ExcretaSlurgeProcessingStatusSaveItem,
|
||||
@@ -90,7 +91,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
@@ -148,19 +153,7 @@ function verificationCellTooltip({
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 - ";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +178,7 @@ function mapListToGridRows<T extends { verificationResult?: SewageExcretaSlurgeV
|
||||
return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
|
||||
const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification;
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
row.verificationResult = normalizeVerificationResultMap(item.verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
@@ -1357,11 +1350,6 @@ const SewageExcretaSlurgeInp = () => {
|
||||
: rowsForSubmit.map((row) =>
|
||||
buildExcretaSlurgeProcessingStatusSaveItem(row, list),
|
||||
);
|
||||
const slurgeDeleteList = isSlurgeFacTab
|
||||
? (deleteListByTab.get(tabKey) ?? [])
|
||||
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
|
||||
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list))
|
||||
: [];
|
||||
const sewageSlurgeFacDeleteList = isSewageSlurgeFacTab
|
||||
? (deleteListByTab.get(tabKey) ?? [])
|
||||
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
|
||||
@@ -1432,7 +1420,6 @@ const SewageExcretaSlurgeInp = () => {
|
||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
|
||||
noApplicable,
|
||||
};
|
||||
|
||||
@@ -1445,7 +1432,6 @@ const SewageExcretaSlurgeInp = () => {
|
||||
...submitRequestBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: slurgeSubmitList,
|
||||
deleteList: slurgeDeleteList,
|
||||
})
|
||||
: isDigestorTab
|
||||
? await statisticsInputSewageExcretaSlurgeApi.submitDigestorOperatingStatus({
|
||||
@@ -1493,7 +1479,13 @@ const SewageExcretaSlurgeInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForSubmit,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -1655,9 +1647,6 @@ const SewageExcretaSlurgeInp = () => {
|
||||
: rowsForSave.map((row) =>
|
||||
buildSlurgeProcessingFacStatusSaveItem(row, list),
|
||||
);
|
||||
const deleteList = (deleteListByTab.get(tabKey) ?? [])
|
||||
.filter((row) => isPersistedArticleRowForTab(tabKey, row))
|
||||
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list));
|
||||
|
||||
if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
@@ -1670,7 +1659,6 @@ const SewageExcretaSlurgeInp = () => {
|
||||
...baseSaveRequest,
|
||||
...userFields,
|
||||
saveList,
|
||||
deleteList,
|
||||
},
|
||||
);
|
||||
} else if (tabKey === DigestorOperatingStatusInpColumn.tabKey) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import type {
|
||||
SeweragePipeInputFactInstallStatusSaveRequest,
|
||||
SeweragePipeInputFactInstallStatusSubmitRequest,
|
||||
@@ -81,7 +82,13 @@ import {
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
getVerificationErrorsForField,
|
||||
normalizeVerificationResultMap,
|
||||
withNormalizedVerificationResult,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
@@ -219,18 +226,33 @@ const applyPipeTypeStatusEditableColumns = (
|
||||
});
|
||||
};
|
||||
|
||||
/** 2-3 관종별: verificationResult.prlgWhol → 총계 계(pipeByPrstWhol) */
|
||||
const PIPE_TYPE_TOTAL_WHOL_ALIASES = ["pipeWholPrlg", "prlgWhol"] as const;
|
||||
|
||||
/** 2-3 관종별: 총계 검증 키(pipeWholPrlg/prlgWhol) → 총계 계(pipeByPrstWhol) */
|
||||
function remapPipeTypePrlgWholVerification(
|
||||
result?: SeweragePipeInputVerificationResult | null,
|
||||
): SeweragePipeInputVerificationResult | null | undefined {
|
||||
if (!result) return result;
|
||||
const fromAlias = result.prlgWhol;
|
||||
if (!Array.isArray(fromAlias) || fromAlias.length === 0) return result;
|
||||
const existing = result.pipeByPrstWhol;
|
||||
if (Array.isArray(existing) && existing.length > 0) return result;
|
||||
const fromAlias = result.pipeWholPrlg ?? result.prlgWhol;
|
||||
if (!Array.isArray(fromAlias) || fromAlias.length === 0) return result;
|
||||
return { ...result, pipeByPrstWhol: fromAlias };
|
||||
}
|
||||
|
||||
function getSewagePipeRowVerificationErrors(
|
||||
verification: SeweragePipeInputVerificationResult | undefined,
|
||||
fieldName: string,
|
||||
tabKey: string,
|
||||
) {
|
||||
if (!verification) return undefined;
|
||||
const aliases =
|
||||
tabKey === PIPE_TYPE_STATUS_TAB_KEY && fieldName === "pipeByPrstWhol"
|
||||
? PIPE_TYPE_TOTAL_WHOL_ALIASES
|
||||
: undefined;
|
||||
return getVerificationErrorsForField(verification, fieldName, aliases);
|
||||
}
|
||||
|
||||
/** 관종별 신 응답(yearlyStatList) -> 그리드 y{year}* 필드로 정규화 */
|
||||
function normalizePipeTypeStatusRow(
|
||||
row: SeweragePipeInputPipeTypeStatusItem,
|
||||
@@ -271,7 +293,10 @@ function normalizePipeTypeStatusRow(
|
||||
lyngPrlg: Number(lyngPrlgWhol ?? 0),
|
||||
dmltnPrlg: Number(dmltnPrlgWhol ?? 0),
|
||||
...yearFields,
|
||||
verificationResult: remapPipeTypePrlgWholVerification(verificationResult) ?? undefined,
|
||||
verificationResult:
|
||||
normalizeVerificationResultMap(
|
||||
remapPipeTypePrlgWholVerification(verificationResult),
|
||||
) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -332,7 +357,6 @@ function buildRegionalFactSubmitListFromGrid(
|
||||
};
|
||||
return currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
goalYr: nullableTrimmedString(d.goalYr),
|
||||
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
||||
@@ -702,10 +726,12 @@ const SewagePipeInp = () => {
|
||||
const response = await statisticsInputSewPipeApiService.getRegionalFactStatus(searchParam);
|
||||
if (response.status === 'success') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = filterFetched(response.data.regionalFactStatusList).map((d) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}));
|
||||
const convertedData = filterFetched(response.data.regionalFactStatusList).map((d) =>
|
||||
withNormalizedVerificationResult({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}),
|
||||
);
|
||||
applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData)));
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
@@ -732,10 +758,12 @@ const SewagePipeInp = () => {
|
||||
const response = await statisticsInputSewPipeApiService.getFactInstallStatus(searchParam);
|
||||
if (response.status === 'success') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = filterFetched(response.data.factInstallStatusList).map((d) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}));
|
||||
const convertedData = filterFetched(response.data.factInstallStatusList).map((d) =>
|
||||
withNormalizedVerificationResult({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}),
|
||||
);
|
||||
if (convertedData.length === 0) {
|
||||
const alreadyHandledForSameSearch =
|
||||
factInstallNoDataHandledKeyRef.current === searchConditionKey;
|
||||
@@ -812,10 +840,12 @@ const SewagePipeInp = () => {
|
||||
const response = await statisticsInputSewPipeApiService.getPipeDiameterStatus(searchParam);
|
||||
if (response.status === 'success') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = filterFetched(response.data.pipeDiameterStatusList).map((d) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}));
|
||||
const convertedData = filterFetched(response.data.pipeDiameterStatusList).map((d) =>
|
||||
withNormalizedVerificationResult({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}),
|
||||
);
|
||||
applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData)));
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
@@ -842,10 +872,12 @@ const SewagePipeInp = () => {
|
||||
const response = await statisticsInputSewPipeApiService.getManholeStatus(searchParam);
|
||||
if (response.status === 'success') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = filterFetched(response.data.manholeStatusList).map((d) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}));
|
||||
const convertedData = filterFetched(response.data.manholeStatusList).map((d) =>
|
||||
withNormalizedVerificationResult({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}),
|
||||
);
|
||||
applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData)));
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
@@ -872,13 +904,15 @@ const SewagePipeInp = () => {
|
||||
const response = await statisticsInputSewPipeApiService.getConnectionStatus(searchParam);
|
||||
if (response.status === 'success') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const facilityConvertedData = filterFetched(response.data.facilityConnectionList).map((d) => ({
|
||||
...d,
|
||||
}));
|
||||
const adminConvertedData = filterFetched(response.data.adminAreaConnectionList).map((d) => ({
|
||||
...d,
|
||||
ctpvNm: d.ctpvNm || commonInfo.ctpvNm,
|
||||
}));
|
||||
const facilityConvertedData = filterFetched(response.data.facilityConnectionList).map((d) =>
|
||||
withNormalizedVerificationResult({ ...d }),
|
||||
);
|
||||
const adminConvertedData = filterFetched(response.data.adminAreaConnectionList).map((d) =>
|
||||
withNormalizedVerificationResult({
|
||||
...d,
|
||||
ctpvNm: d.ctpvNm || commonInfo.ctpvNm,
|
||||
}),
|
||||
);
|
||||
applyRowMap((m) => {
|
||||
m.set('treatmentPlantConnectionInp', toGridRows(facilityConvertedData));
|
||||
m.set('adminAreaConnectionInp', toGridRows(adminConvertedData));
|
||||
@@ -1246,7 +1280,6 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
const submitList = currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
fcltSn: nullableTrimmedString(d.fcltSn),
|
||||
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
|
||||
@@ -1322,7 +1355,6 @@ const SewagePipeInp = () => {
|
||||
rmrk: nullableTrimmedString(d.rmrk),
|
||||
lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"),
|
||||
dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"),
|
||||
pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"),
|
||||
};
|
||||
});
|
||||
const requestData: SeweragePipeInputPipeTypeStatusSubmitRequest = {
|
||||
@@ -1439,7 +1471,13 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: values.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: values?.bsnmCd ?? "",
|
||||
rollbackStatusCode: statusForTab?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -1619,7 +1657,6 @@ const SewagePipeInp = () => {
|
||||
|
||||
const saveList = currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
goalYr: nullableTrimmedString(d.goalYr),
|
||||
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
||||
@@ -1710,7 +1747,6 @@ const SewagePipeInp = () => {
|
||||
|
||||
const saveList = currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
fcltSn: nullableTrimmedString(d.fcltSn),
|
||||
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
|
||||
@@ -1816,7 +1852,6 @@ const SewagePipeInp = () => {
|
||||
rmrk: nullableTrimmedString(d.rmrk),
|
||||
lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"),
|
||||
dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"),
|
||||
pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2202,8 +2237,11 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
const errors = getSewagePipeRowVerificationErrors(
|
||||
verification,
|
||||
fieldName,
|
||||
tabKey,
|
||||
);
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
|
||||
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
|
||||
@@ -2214,20 +2252,13 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
cellTooltipCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
const errors = getSewagePipeRowVerificationErrors(
|
||||
verification,
|
||||
fieldName,
|
||||
tabKey,
|
||||
);
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
|
||||
const comp = e.cmprCn ?? "";
|
||||
const now = e.nowCn ?? "";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${comp} / 현재 입력값: ${now})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { sewagePipeMaintainApiService } from "@/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import type {
|
||||
SewagePipeMaintainDredgingSaveItem,
|
||||
SewagePipeMaintainDredgingSaveRequest,
|
||||
@@ -69,7 +70,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
withNormalizedVerificationResult,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
@@ -218,7 +223,6 @@ function buildRepairRowList(
|
||||
|
||||
return currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
dfrpWholPrlg: formatNumber(d.dfrpWholPrlg, "dfrpWholPrlg"),
|
||||
cbsysDfrpPrlg: formatNumber(d.cbsysDfrpPrlg, "cbsysDfrpPrlg"),
|
||||
@@ -247,7 +251,6 @@ function buildDredgingRowList(
|
||||
|
||||
return currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
|
||||
wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"),
|
||||
@@ -446,10 +449,12 @@ const SewagePipeMaintainInp = () => {
|
||||
repairStatusList ?? [],
|
||||
sidoListRef.current,
|
||||
searchContext,
|
||||
).map((d) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}));
|
||||
).map((d) =>
|
||||
withNormalizedVerificationResult({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}),
|
||||
);
|
||||
const gridRows = toGridRows(convertedData);
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
@@ -489,10 +494,12 @@ const SewagePipeMaintainInp = () => {
|
||||
pipeDredgingStatusList ?? [],
|
||||
sidoListRef.current,
|
||||
searchContext,
|
||||
).map((d) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}));
|
||||
).map((d) =>
|
||||
withNormalizedVerificationResult({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}),
|
||||
);
|
||||
const gridRows = toGridRows(convertedData);
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
@@ -680,7 +687,13 @@ const SewagePipeMaintainInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -1105,14 +1118,7 @@ const SewagePipeMaintainInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR" ? "시계열 검증 - " : e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import {
|
||||
type RainfallPumpStationItem,
|
||||
type RainfallPumpStationSaveItem,
|
||||
@@ -89,7 +90,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
@@ -279,7 +284,7 @@ function mapSewageReservoirListToGridRows(
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
row.verificationResult = normalizeVerificationResultMap(item.verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
@@ -435,7 +440,7 @@ function mapRainfallPumpStationListToGridRows(
|
||||
])[0] as GridRowWithVerification;
|
||||
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
row.verificationResult = normalizeVerificationResultMap(item.verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
@@ -864,7 +869,13 @@ const SewageReservoirPumpInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -1516,19 +1527,7 @@ const SewageReservoirPumpInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 -";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/input/statistics-input-sewerage-finance-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import type {
|
||||
CauserBurdenItem,
|
||||
CauserBurdenSaveItem,
|
||||
@@ -73,7 +74,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
@@ -174,7 +179,9 @@ function mapRegionalListToGridRows<
|
||||
return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
|
||||
const { verificationResult, ...rest } = item;
|
||||
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
if (verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@@ -215,7 +222,6 @@ function buildSewerageFinanceSettleSaveList(
|
||||
|
||||
return currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
soaGovSprtAmt: formatNumber(d.soaGovSprtAmt, "soaGovSprtAmt"),
|
||||
mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"),
|
||||
@@ -257,7 +263,6 @@ function buildSewerageChargeSaveList(
|
||||
|
||||
return currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"),
|
||||
sewerLevyAmt: formatNumber(d.sewerLevyAmt, "sewerLevyAmt"),
|
||||
@@ -276,7 +281,6 @@ function buildSewerageUsageFeeSaveList(
|
||||
|
||||
return currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"),
|
||||
hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"),
|
||||
@@ -312,7 +316,6 @@ function buildCauserBurdenSaveList(
|
||||
|
||||
return currentData.map((d) => ({
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
brdnAmtLevyNocs: formatNumber(d.brdnAmtLevyNocs, "brdnAmtLevyNocs"),
|
||||
brdnAmtLevyAmt: formatNumber(d.brdnAmtLevyAmt, "brdnAmtLevyAmt"),
|
||||
@@ -849,7 +852,6 @@ const SewerageFinanceInp = () => {
|
||||
|
||||
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
||||
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
|
||||
|
||||
const submitInfoBase = {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
@@ -894,7 +896,6 @@ const SewerageFinanceInp = () => {
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: buildSewerageFinanceSettleSaveList(currentData),
|
||||
...adminOpt,
|
||||
};
|
||||
response =
|
||||
await statisticsInputSewerageFinanceApi.submitSewerageFinanceSettle(
|
||||
@@ -905,7 +906,6 @@ const SewerageFinanceInp = () => {
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: buildSewerageChargeSaveList(currentData),
|
||||
...adminOpt,
|
||||
};
|
||||
response = await statisticsInputSewerageFinanceApi.submitSewerageCharge(param);
|
||||
} else if (isUsageFeeTab) {
|
||||
@@ -913,7 +913,6 @@ const SewerageFinanceInp = () => {
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: buildSewerageUsageFeeSaveList(currentData),
|
||||
...adminOpt,
|
||||
};
|
||||
response =
|
||||
await statisticsInputSewerageFinanceApi.submitSewerageUsageFee(param);
|
||||
@@ -922,7 +921,6 @@ const SewerageFinanceInp = () => {
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
submitList: buildCauserBurdenSaveList(currentData),
|
||||
...adminOpt,
|
||||
};
|
||||
response = await statisticsInputSewerageFinanceApi.submitCauserBurden(param);
|
||||
}
|
||||
@@ -949,7 +947,13 @@ const SewerageFinanceInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -1224,18 +1228,7 @@ const SewerageFinanceInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 - ";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import {
|
||||
type CheckInputPeriodItem,
|
||||
getVerificationPeriodActionBlockedMessage,
|
||||
@@ -58,7 +59,11 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
|
||||
import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
@@ -87,7 +92,7 @@ const remapCommonInfoVerificationToGridFields = (
|
||||
// API 키로도 조회 가능하도록 유지
|
||||
if (gridField !== key) remapped[key] = errors;
|
||||
}
|
||||
return remapped;
|
||||
return normalizeVerificationResultMap(remapped) ?? remapped;
|
||||
};
|
||||
|
||||
/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */
|
||||
@@ -302,7 +307,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
setRowData(
|
||||
convertedData.map((d) => {
|
||||
const row = toGridRows([d])[0] as GridRowWithVerification;
|
||||
if (d.verificationResult) row.verificationResult = d.verificationResult;
|
||||
if (d.verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(
|
||||
d.verificationResult,
|
||||
);
|
||||
}
|
||||
return row;
|
||||
})
|
||||
);
|
||||
@@ -399,10 +408,6 @@ const SeweragePenetrationRateInp = () => {
|
||||
statsCd: statsCd,
|
||||
};
|
||||
|
||||
if (isAdminInfo) {
|
||||
params.superAdmin = 'superAdmin';
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -498,7 +503,6 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||
sggNm: nullableTrimmedString(d.sggNm),
|
||||
admdstCd: nullableTrimmedString(d.admdstCd),
|
||||
admdstNm: nullableTrimmedString(d.admdstNm),
|
||||
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm),
|
||||
@@ -563,7 +567,13 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: searchValues?.year ?? "",
|
||||
statsCd,
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: statusInfo?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(imrResponse.job_id, (id) =>
|
||||
statisticsApi.getImrStatus(id),
|
||||
@@ -677,8 +687,6 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||
// 행정구역
|
||||
sggNm: nullableTrimmedString(d.sggNm), // 시군구
|
||||
admdstCd: nullableTrimmedString(d.admdstCd), // 행정코드
|
||||
admdstNm: nullableTrimmedString(d.admdstNm), // 행정구역명
|
||||
statsTrbtrNm: nullableTrimmedString(d.statsTrbtrNm), // 지류
|
||||
@@ -874,14 +882,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix = e.vrfcCd === 'IMR' ? '시계열 검증 - ' : (e.vrfcCd === 'NV' ? '일반검증 - ' : '모집단 검증 -');
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputSewerageServiceLevelApi } from "@/entities/statistics/api/input/statistics-input-sewerage-service-level-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import {
|
||||
SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
|
||||
SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
|
||||
@@ -63,7 +64,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
@@ -187,7 +192,9 @@ function mapServiceLevelListToGridRows<
|
||||
return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
|
||||
const { verificationResult, ...rest } = item;
|
||||
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
if (verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@@ -808,13 +815,11 @@ const SewerageServiceLevelInp = () => {
|
||||
? await statisticsInputSewerageServiceLevelApi.submitComplaintList({
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||
submitList: complaintSubmitList,
|
||||
})
|
||||
: await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({
|
||||
...submitInfoBase,
|
||||
userUuid: userInfo.userUnqId,
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||
noApplicable,
|
||||
submitList: residentSubmitList,
|
||||
});
|
||||
@@ -841,7 +846,13 @@ const SewerageServiceLevelInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -1148,18 +1159,7 @@ const SewerageServiceLevelInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 - ";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { nullableTrimmedString } from "@/components/util/input/nullableString";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { callImrServiceWithRollbackOnFailure } from "@/components/util/input/call-imr-with-rollback";
|
||||
import {
|
||||
type PublicSewageTreatWaterReuseItem,
|
||||
type PublicSewageTreatWaterReuseSaveItem,
|
||||
@@ -93,7 +94,11 @@ import {
|
||||
patchFooterVerificationByTab,
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
|
||||
import {
|
||||
formatVerificationTooltip,
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "@/components/util/input/verification-result-display";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
@@ -443,7 +448,9 @@ function mapPublicSewageTreatWaterReuseListToGridRows(
|
||||
instlAprvYmd: formatCompactYmdForGrid(item.instlAprvYmd),
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
if (verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@@ -462,7 +469,9 @@ function mapRecycledWaterStatusListToGridRows(
|
||||
oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
if (verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@@ -480,7 +489,9 @@ function mapRainFallUtilFactStatusListToGridRows(
|
||||
cmptnYmd: formatCompactYmdForGrid(item.cmptnYmd),
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
if (verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@@ -497,7 +508,9 @@ function mapTreatWaterReuseStatusListToGridRows<
|
||||
coRegYmd: formatCompactYmdForGrid(item.coRegYmd),
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
if (verificationResult) {
|
||||
row.verificationResult = normalizeVerificationResultMap(verificationResult);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
@@ -1153,7 +1166,6 @@ const WaterReuseInp = () => {
|
||||
lclgvIdfrTelnoCn: submitInfo.lclgvIdfrTelnoCn,
|
||||
lclgvIdfrDeptNm: submitInfo.lclgvIdfrDeptNm,
|
||||
lclgvIdfrJbgdNm: submitInfo.lclgvIdfrJbgdNm,
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||
};
|
||||
|
||||
let articleSggOptions: Sgg[] | null = null;
|
||||
@@ -1356,7 +1368,13 @@ const WaterReuseInp = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
const imrResponse = await callImrServiceWithRollbackOnFailure(imrParam, {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? "",
|
||||
rollbackStatusCode: activeTabStatus?.lclgvSbmsnSttsCd ?? "",
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
@@ -2064,18 +2082,7 @@ const WaterReuseInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "시계열 검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 - ";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.cmprCn} / 현재 입력값: ${e.nowCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return formatVerificationTooltip(errors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,162 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
|
||||
export interface ApprovalFormData {
|
||||
lastCfmtnPicNm: string;
|
||||
lastCfmtnPicTelNo: string;
|
||||
lastCfmtnPicDeptNm: string;
|
||||
lastCfmtnPicJbgdNm: string;
|
||||
}
|
||||
|
||||
export interface ApprovalInputDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm?: (data: ApprovalFormData) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ApprovalInputDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: ApprovalInputDialogProps) {
|
||||
const [lastCfmtnPicNm, setLastCfmtnPicNm] = useState("");
|
||||
const [lastCfmtnPicTelNo, setLastCfmtnPicTelNo] = useState("");
|
||||
const [lastCfmtnPicDeptNm, setLastCfmtnPicDeptNm] = useState("");
|
||||
const [lastCfmtnPicJbgdNm, setLastCfmtnPicJbgdNm] = useState("");
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const resetForm = () => {
|
||||
setLastCfmtnPicNm("");
|
||||
setLastCfmtnPicTelNo("");
|
||||
setLastCfmtnPicDeptNm("");
|
||||
setLastCfmtnPicJbgdNm("");
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) resetForm();
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetForm();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const formData: ApprovalFormData = {
|
||||
lastCfmtnPicNm: lastCfmtnPicNm.trim(),
|
||||
lastCfmtnPicTelNo: lastCfmtnPicTelNo.trim(),
|
||||
lastCfmtnPicDeptNm: lastCfmtnPicDeptNm.trim(),
|
||||
lastCfmtnPicJbgdNm: lastCfmtnPicJbgdNm.trim(),
|
||||
};
|
||||
await onConfirm?.(formData);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogContent
|
||||
className="max-w-[56rem]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>승인 정보 입력</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">최종 확인 담당자명</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="담당자명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 전화번호
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicTelNo}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicTelNo(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="전화번호를 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 부서명
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicDeptNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicDeptNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="부서명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 직급명
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicJbgdNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicJbgdNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="직급명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => void handleConfirm()}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
|
||||
export interface ApprovalFormData {
|
||||
lastCfmtnPicNm: string;
|
||||
lastCfmtnPicTelnoCn: string;
|
||||
lastCfmtnPicDeptNm: string;
|
||||
lastCfmtnPicJbgdNm: string;
|
||||
}
|
||||
|
||||
export interface ApprovalInputDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm?: (data: ApprovalFormData) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ApprovalInputDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: ApprovalInputDialogProps) {
|
||||
const [lastCfmtnPicNm, setLastCfmtnPicNm] = useState("");
|
||||
const [lastCfmtnPicTelnoCn, setLastCfmtnPicTelnoCn] = useState("");
|
||||
const [lastCfmtnPicDeptNm, setLastCfmtnPicDeptNm] = useState("");
|
||||
const [lastCfmtnPicJbgdNm, setLastCfmtnPicJbgdNm] = useState("");
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const resetForm = () => {
|
||||
setLastCfmtnPicNm("");
|
||||
setLastCfmtnPicTelnoCn("");
|
||||
setLastCfmtnPicDeptNm("");
|
||||
setLastCfmtnPicJbgdNm("");
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) resetForm();
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetForm();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const formData: ApprovalFormData = {
|
||||
lastCfmtnPicNm: lastCfmtnPicNm.trim(),
|
||||
lastCfmtnPicTelnoCn: lastCfmtnPicTelnoCn.trim(),
|
||||
lastCfmtnPicDeptNm: lastCfmtnPicDeptNm.trim(),
|
||||
lastCfmtnPicJbgdNm: lastCfmtnPicJbgdNm.trim(),
|
||||
};
|
||||
await onConfirm?.(formData);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogContent
|
||||
className="max-w-[56rem]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>승인 정보 입력</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">최종 확인 담당자명</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="담당자명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 전화번호
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicTelnoCn}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicTelnoCn(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="전화번호를 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 부서명
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicDeptNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicDeptNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="부서명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 직급명
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicJbgdNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicJbgdNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="직급명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => void handleConfirm()}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,9 +141,9 @@ const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
|
||||
|
||||
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
|
||||
...DEFAULT_FILTERS,
|
||||
laeSbmsnSttsCd: "3",
|
||||
prcsYnList: "1,2",
|
||||
rejectCode: "0",
|
||||
lclgvSbmsnSttsCd: "3",
|
||||
vrfcSttsCdList: "1,2",
|
||||
statsVrfcRjtComCd: "0",
|
||||
pbcpAprvYn: "N",
|
||||
};
|
||||
|
||||
@@ -211,10 +211,10 @@ function StatisticsVerificationReport() {
|
||||
subFilters.statsCd !== ""
|
||||
? subFilters.statsCd
|
||||
: filters.statsCd,
|
||||
prcsYnList: subFilters.prcsYnList,
|
||||
rejectCode: subFilters.rejectCode,
|
||||
vrfcSttsCdList: subFilters.vrfcSttsCdList,
|
||||
statsVrfcRjtComCd: subFilters.statsVrfcRjtComCd,
|
||||
pbcpAprvYn: subFilters.pbcpAprvYn,
|
||||
laeSbmsnSttsCd: subFilters.laeSbmsnSttsCd,
|
||||
lclgvSbmsnSttsCd: subFilters.lclgvSbmsnSttsCd,
|
||||
}),
|
||||
[filters, subFilters],
|
||||
);
|
||||
@@ -712,8 +712,8 @@ function StatisticsVerificationReport() {
|
||||
<div className="flex items-center gap-6">
|
||||
<FilterCombobox
|
||||
label="상태"
|
||||
value={subFilters.prcsYnList}
|
||||
onChange={handleSubFilterChange("prcsYnList")}
|
||||
value={subFilters.vrfcSttsCdList}
|
||||
onChange={handleSubFilterChange("vrfcSttsCdList")}
|
||||
options={statusOptions ?? []}
|
||||
/>
|
||||
<FilterSelect
|
||||
@@ -724,8 +724,8 @@ function StatisticsVerificationReport() {
|
||||
/>
|
||||
<FilterSelect
|
||||
label="반려상태"
|
||||
value={subFilters.rejectCode}
|
||||
onChange={handleSubFilterChange("rejectCode")}
|
||||
value={subFilters.statsVrfcRjtComCd}
|
||||
onChange={handleSubFilterChange("statsVrfcRjtComCd")}
|
||||
options={REJECT_STATUS_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
@@ -743,7 +743,7 @@ function StatisticsVerificationReport() {
|
||||
selectedResultRows.length === 0 ||
|
||||
!selectedResultRows.every(
|
||||
(row) =>
|
||||
row.prcsYn === "1" || row.prcsYn === "2",
|
||||
row.vrfcSttsCd === "1" || row.vrfcSttsCd === "2",
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
+7
-4
@@ -554,17 +554,20 @@ const DetailComponent = ({
|
||||
const userInfo = await getUserInfo();
|
||||
let response;
|
||||
|
||||
const vrfcSn = detailInfo?.commonInfo.vrfcSn;
|
||||
if (vrfcSn == null) {
|
||||
alert("검증 일련번호가 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (!hasPersistedDetails) {
|
||||
response =
|
||||
await generalverificationApiService.RegistVrfcDetail({
|
||||
vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null,
|
||||
await generalverificationApiService.RegistVrfcDetail(vrfcSn, {
|
||||
detailList: rows,
|
||||
rgtrUuid: userInfo.userUnqId,
|
||||
});
|
||||
} else {
|
||||
response =
|
||||
await generalverificationApiService.UpdateVrfcDetail({
|
||||
vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null,
|
||||
await generalverificationApiService.UpdateVrfcDetail(vrfcSn, {
|
||||
detailList: rows,
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
});
|
||||
|
||||
+22
-13
@@ -9,10 +9,10 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { Form, FormControl,FormField, FormItem } from "@/shared/ui/form";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
import type { RegistVrfcMasterRequest,UpdateVrfcMasterRequest } from "@/entities/statistics/model/general-verification-interface";
|
||||
import type { RegistVrfcMasterRequest, UpdateVrfcMasterRequest } from "@/entities/statistics/model/general-verification-interface";
|
||||
import { type GeneralVerificationMasterSubmitFormData, generalVerificationMasterSubmitSchema } from '@/entities/statistics/model/general-verification-master-submit-schema';
|
||||
import { UserApi } from "@/entities/user/api/user-service";
|
||||
import {
|
||||
@@ -146,9 +146,15 @@ const RegistDialog = memo(
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateVrfcMaster = async (data: GeneralVerificationMasterSubmitFormData) => {
|
||||
const handleUpdateVrfcMaster = async (
|
||||
updateVrfcSn: number,
|
||||
body: UpdateVrfcMasterRequest,
|
||||
) => {
|
||||
try {
|
||||
const response = await generalverificationApiService.UpdateVrfcMaster(data as UpdateVrfcMasterRequest);
|
||||
const response = await generalverificationApiService.UpdateVrfcMaster(
|
||||
updateVrfcSn,
|
||||
body,
|
||||
);
|
||||
|
||||
if (response.status === 'success') {
|
||||
alert('수정되었습니다.');
|
||||
@@ -228,7 +234,7 @@ const RegistDialog = memo(
|
||||
if (mutationBlocked) {
|
||||
alert(
|
||||
blockedMessage ??
|
||||
"입력기간에는 검증항목을 등록하거나 수정할 수 없습니다.",
|
||||
"입력기간에는 검증항목을 등록하거나 수정할 수 없습니다.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -258,8 +264,11 @@ const RegistDialog = memo(
|
||||
if (await confirm(confirmMessage)) {
|
||||
const userInfo = await getUserInfo();
|
||||
if (isVrfcSn) {
|
||||
const updateData = {
|
||||
vrfcSn: data.vrfcSn,
|
||||
if (data.vrfcSn == null) {
|
||||
alert('수정 대상이 없습니다.');
|
||||
return;
|
||||
}
|
||||
await handleUpdateVrfcMaster(data.vrfcSn, {
|
||||
vrfcNm: data.vrfcNm,
|
||||
vrfcClsfCd: data.vrfcClsfCd,
|
||||
dcptDgt: data.dcptDgt,
|
||||
@@ -267,10 +276,8 @@ const RegistDialog = memo(
|
||||
...(data.dcptDgt === 0
|
||||
? {}
|
||||
: { dcptPrcsMthCd: data.dcptPrcsMthCd }),
|
||||
trgtStatsCd: data.trgtStatsCd,
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
};
|
||||
await handleUpdateVrfcMaster(updateData);
|
||||
});
|
||||
} else {
|
||||
const registData = {
|
||||
vrfcNm: data.vrfcNm,
|
||||
@@ -298,14 +305,16 @@ const RegistDialog = memo(
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogTitle>
|
||||
<div className="pr-8">
|
||||
{isVrfcSn ?
|
||||
detailData ? detailData.data.vrfcNm
|
||||
:
|
||||
<Skeleton className="h-[14px] rounded-xl w-2/3" />
|
||||
: "일반 검증 등록"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
|
||||
@@ -481,6 +481,11 @@ export type DataGridProps = {
|
||||
headerHeight?: number;
|
||||
/** RealGrid `displayOptions.fitStyle` (기본 EVEN). 컬럼별 width 유지 시 NONE */
|
||||
fitStyle?: GridFitStyle;
|
||||
/**
|
||||
* true면 붙여넣기·편집으로 행을 추가하지 않음.
|
||||
* 검증 결과처럼 조회 행 수가 고정된 그리드에서 사용.
|
||||
*/
|
||||
preventRowAppend?: boolean;
|
||||
};
|
||||
|
||||
export type TabDataGridProps = {
|
||||
|
||||
@@ -8,6 +8,7 @@ export const doList = [
|
||||
"4600000000",
|
||||
"4700000000",
|
||||
"4800000000",
|
||||
"5000000000",
|
||||
];
|
||||
|
||||
export const needBsnmFrgnrPoplSmSidoList = [
|
||||
|
||||
@@ -49,6 +49,43 @@ export const ManholeStatusInpColumn = {
|
||||
styleName: "rg-cell-readonly",
|
||||
footer: { text: "합계" },
|
||||
},
|
||||
{
|
||||
name: "wholMnhlCntSum",
|
||||
fieldName: "wholMnhlCntSum",
|
||||
header: { text: "맨홀총계" },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "frlvlMnhlCntSum",
|
||||
fieldName: "frlvlMnhlCntSum",
|
||||
header: { text: "주철계" },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "thlvlMnhlCntSum",
|
||||
fieldName: "thlvlMnhlCntSum",
|
||||
header: { text: "콘크리트계" },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "picNm",
|
||||
fieldName: "picNm",
|
||||
header: { text: "담당자명" },
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
...intCol("wholMnhlCnt", "계", false),
|
||||
computed: {
|
||||
@@ -88,6 +125,12 @@ export const ManholeStatusInpColumn = {
|
||||
},
|
||||
"admdstNm",
|
||||
"emphsMngSeNm",
|
||||
{
|
||||
name: "adminRefGroup",
|
||||
hideChildHeaders: false,
|
||||
header: { text: "행정참고자료" },
|
||||
items: ["wholMnhlCntSum", "frlvlMnhlCntSum", "thlvlMnhlCntSum", "picNm"],
|
||||
},
|
||||
{
|
||||
name: "mtrlGroup",
|
||||
hideChildHeaders: false,
|
||||
|
||||
@@ -491,6 +491,10 @@ const rawList = [
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'emphsMngSeNm', fieldName: 'emphsMngSeNm', header: { text: '집중강우\n중점관리\n구역구분' }, mergeCells: true },
|
||||
{ name: 'wholMnhlCntSum', fieldName: 'wholMnhlCntSum', header: { text: '맨홀총계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'frlvlMnhlCntSum', fieldName: 'frlvlMnhlCntSum', header: { text: '주철계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'thlvlMnhlCntSum', fieldName: 'thlvlMnhlCntSum', header: { text: '콘크리트계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'picNm', fieldName: 'picNm', header: { text: '담당자명' } },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
@@ -544,6 +548,12 @@ const rawList = [
|
||||
],
|
||||
},
|
||||
'emphsMngSeNm',
|
||||
{
|
||||
name: 'adminRefGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정참고자료' },
|
||||
items: ['wholMnhlCntSum', 'frlvlMnhlCntSum', 'thlvlMnhlCntSum', 'picNm'],
|
||||
},
|
||||
'displayYear',
|
||||
{
|
||||
name: 'mtrlGroup',
|
||||
|
||||
@@ -710,6 +710,10 @@ const rawSewagePipeTotList = [
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, editable: false },
|
||||
{ name: "emphsMngSeNm", fieldName: "emphsMngSeNm", header: { text: "집중강우\n중점관리\n구역구분" }, editable: false },
|
||||
{ name: "wholMnhlCntSum", fieldName: "wholMnhlCntSum", header: { text: "맨홀총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "frlvlMnhlCntSum", fieldName: "frlvlMnhlCntSum", header: { text: "주철계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "thlvlMnhlCntSum", fieldName: "thlvlMnhlCntSum", header: { text: "콘크리트계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "picNm", fieldName: "picNm", header: { text: "담당자명" }, editable: false },
|
||||
{ name: "wholMnhlCnt", fieldName: "wholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "frlvlMnhlCnt", fieldName: "frlvlMnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "sclvlMnhlCnt", fieldName: "sclvlMnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
@@ -720,6 +724,11 @@ const rawSewagePipeTotList = [
|
||||
columnLayout: [
|
||||
{ name: "addrGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] },
|
||||
"emphsMngSeNm",
|
||||
{
|
||||
name: "adminRefGroup",
|
||||
header: { text: "행정참고자료" },
|
||||
items: ["wholMnhlCntSum", "frlvlMnhlCntSum", "thlvlMnhlCntSum", "picNm"],
|
||||
},
|
||||
{
|
||||
name: "mtrlGroup",
|
||||
header: { text: "맨홀뚜껑 재질(개)" },
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ImrRequest,
|
||||
ImrResponse,
|
||||
rollbackStatusRequest,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
|
||||
/** 제출 직후 IMR 요청이 실패하면 검증 이전 상태로 롤백 */
|
||||
export async function callImrServiceWithRollbackOnFailure(
|
||||
imrParam: ImrRequest,
|
||||
rollbackParam: rollbackStatusRequest,
|
||||
): Promise<ImrResponse> {
|
||||
try {
|
||||
return await statisticsApi.callImrService(imrParam);
|
||||
} catch (error) {
|
||||
try {
|
||||
await statisticsApi.rollbackStatus(rollbackParam);
|
||||
} catch {
|
||||
// 롤백 실패는 IMR 실패를 가리지 않음
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { StatisticsInputCommonInfoBase } from "@/entities/statistics/model/statistics-interface";
|
||||
|
||||
import type { VerificationResultLikeMap } from "./verification-result-display";
|
||||
import {
|
||||
normalizeVerificationResultMap,
|
||||
type VerificationResultLikeMap,
|
||||
} from "./verification-result-display";
|
||||
|
||||
export function isVerificationFailedFromCommonInfo(
|
||||
commonInfo: Pick<StatisticsInputCommonInfoBase, "isVerificationFailed"> | null | undefined,
|
||||
@@ -28,6 +31,9 @@ export function patchFooterVerificationByTab(
|
||||
| undefined,
|
||||
): Map<string, VerificationResultLikeMap | null> {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, commonInfo?.verificationResult ?? null);
|
||||
next.set(
|
||||
tabKey,
|
||||
normalizeVerificationResultMap(commonInfo?.verificationResult) ?? null,
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,49 @@ export type VerificationResultLikeMap = Record<
|
||||
VerificationResultLikeItem[]
|
||||
>;
|
||||
|
||||
/** 입력 조회 verificationResult의 기준값/입력값이 null이면 "-" */
|
||||
export function displayVerificationCn(
|
||||
value: string | null | undefined,
|
||||
): string {
|
||||
if (value == null || String(value).trim() === "") return "-";
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function normalizeVerificationResultItem<T extends VerificationResultLikeItem>(
|
||||
item: T,
|
||||
): T {
|
||||
return {
|
||||
...item,
|
||||
nowCn: displayVerificationCn(item.nowCn ?? item.nowVlCn),
|
||||
cmprCn: displayVerificationCn(item.cmprCn ?? item.compVlCn),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeVerificationResultMap<T extends VerificationResultLikeMap>(
|
||||
result: T | null | undefined,
|
||||
): T | undefined {
|
||||
if (!result || typeof result !== "object") return undefined;
|
||||
const next = {} as T;
|
||||
for (const [key, items] of Object.entries(result)) {
|
||||
(next as VerificationResultLikeMap)[key] = Array.isArray(items)
|
||||
? items.map((item) => normalizeVerificationResultItem(item))
|
||||
: [];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 조회 행/객체에 붙은 verificationResult의 cmprCn·nowCn null을 "-"로 바꿈 */
|
||||
export function withNormalizedVerificationResult<T extends object>(item: T): T {
|
||||
if (!("verificationResult" in item) || item.verificationResult == null) {
|
||||
return item;
|
||||
}
|
||||
const verificationResult = normalizeVerificationResultMap(
|
||||
item.verificationResult as VerificationResultLikeMap,
|
||||
);
|
||||
if (!verificationResult) return item;
|
||||
return { ...item, verificationResult };
|
||||
}
|
||||
|
||||
/** 검증 오류 배열 → RealGrid styleName */
|
||||
export function getVerificationStyleName(
|
||||
errors: VerificationResultLikeItem[] | null | undefined,
|
||||
@@ -37,8 +80,8 @@ export function formatVerificationTooltip(
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "모집단 검증 - ";
|
||||
const comp = e.cmprCn ?? e.compVlCn ?? "";
|
||||
const now = e.nowCn ?? e.nowVlCn ?? "";
|
||||
const comp = displayVerificationCn(e.cmprCn ?? e.compVlCn);
|
||||
const now = displayVerificationCn(e.nowCn ?? e.nowVlCn);
|
||||
return `${prefix}${e.rsnExpln}\n(검증 기준값: ${comp} / 현재 입력값: ${now})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -56,10 +56,11 @@ export class GeneralverificationApiService implements GeneralverificationService
|
||||
}
|
||||
|
||||
public async UpdateVrfcMaster(
|
||||
vrfcSn: number,
|
||||
param: UpdateVrfcMasterRequest,
|
||||
): Promise<UpdateVrfcMasterResponse> {
|
||||
const response = await stClient.put<UpdateVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param.vrfcSn}`,
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${vrfcSn}`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
@@ -127,20 +128,22 @@ export class GeneralverificationApiService implements GeneralverificationService
|
||||
}
|
||||
|
||||
public async RegistVrfcDetail(
|
||||
vrfcSn: number,
|
||||
param: VerifyDetailRegistRequest,
|
||||
): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${vrfcSn}/details`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async UpdateVrfcDetail(
|
||||
vrfcSn: number,
|
||||
param: VerifyDetailUpdateRequest,
|
||||
): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.put<RegistVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${vrfcSn}/details`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -15,7 +15,12 @@ export class StatisticsInputStatusApiService implements StatisticsInputStatusSer
|
||||
): Promise<StatsInputStatusListApiResponse> {
|
||||
const response = await stClient.get<StatsInputStatusListApiResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`,
|
||||
{ params: param },
|
||||
{
|
||||
params: param,
|
||||
paramsSerializer: {
|
||||
indexes: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -162,10 +162,10 @@ export const useStatsVrfcRsltData = (
|
||||
params?.sggNm ?? "",
|
||||
params?.sewerBzmnCd ?? "",
|
||||
params?.statsCd ?? "",
|
||||
params?.prcsYnList ?? "",
|
||||
params?.rejectCode ?? "",
|
||||
params?.vrfcSttsCdList ?? "",
|
||||
params?.statsVrfcRjtComCd ?? "",
|
||||
params?.pbcpAprvYn ?? "",
|
||||
params?.laeSbmsnSttsCd ?? "",
|
||||
params?.lclgvSbmsnSttsCd ?? "",
|
||||
],
|
||||
initialPageParam: 1,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
@@ -190,10 +190,10 @@ export const useStatsVrfcRsltData = (
|
||||
sggNm: params.sggNm,
|
||||
sewerBzmnCd: params.sewerBzmnCd,
|
||||
statsCd: params.statsCd,
|
||||
prcsYnList: params.prcsYnList,
|
||||
rejectCode: params.rejectCode,
|
||||
vrfcSttsCdList: params.vrfcSttsCdList,
|
||||
statsVrfcRjtComCd: params.statsVrfcRjtComCd,
|
||||
pbcpAprvYn: params.pbcpAprvYn,
|
||||
laeSbmsnSttsCd: params.laeSbmsnSttsCd,
|
||||
lclgvSbmsnSttsCd: params.lclgvSbmsnSttsCd,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
type ImrStatusResponse,
|
||||
type ReviewerInfoRequest,
|
||||
type ReviewerInfoResponse,
|
||||
type rollbackStatusRequest,
|
||||
type rollbackStatusResponse,
|
||||
type ReviewInputSaveRequest,
|
||||
type ReviewOpinionSaveRequest,
|
||||
type ReviewOpinionSaveResponse,
|
||||
@@ -156,6 +158,17 @@ export class StatisticsApiService implements StatisticsService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 입력 - 검증 이전 상태로 롤백
|
||||
public async rollbackStatus(
|
||||
param: rollbackStatusRequest,
|
||||
): Promise<rollbackStatusResponse> {
|
||||
const response = await stClient.put<rollbackStatusResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/rollback-status`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async callImrService(param: ImrRequest): Promise<ImrResponse> {
|
||||
const response = await client.post<ImrResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.IMR}`,
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
export interface DashboardSearchParams {
|
||||
statsYr: string;
|
||||
dtlInstCd: string;
|
||||
enoCd?: string;
|
||||
laeSeCd?: string;
|
||||
sewerBzmnCd?: string;
|
||||
sido?: string;
|
||||
sggNm?: string;
|
||||
unSubmitOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardApiResponse<T> {
|
||||
status: string;
|
||||
message: string;
|
||||
data: T;
|
||||
code: string;
|
||||
httpStatus: number;
|
||||
}
|
||||
|
||||
export interface VrfcReportResponse {
|
||||
typeCounts: Record<string, number>;
|
||||
list: VrfcReportSelect[];
|
||||
}
|
||||
|
||||
export interface VrfcReportSelect {
|
||||
vrfcCd: string;
|
||||
statsNm: string;
|
||||
rsnExpln: string;
|
||||
admdstNm: string;
|
||||
nowVlCn: string;
|
||||
compVlCn: string;
|
||||
}
|
||||
|
||||
export interface VrfcErrCountResponse {
|
||||
statsYr: string;
|
||||
vrfcErrCount: number;
|
||||
}
|
||||
|
||||
export interface UnSubmitCountResponse {
|
||||
statsYr: string;
|
||||
statusList: UnSubmitStatsResponse[];
|
||||
submittedCount: number;
|
||||
totalCount: number;
|
||||
unSubmittedCount: number;
|
||||
}
|
||||
|
||||
export interface UnSubmitStatsResponse {
|
||||
sbmsnYn: string;
|
||||
sidoSggNm: string;
|
||||
statsCd: string;
|
||||
statsNm: string;
|
||||
statusCd: string;
|
||||
statusNm: string;
|
||||
}
|
||||
|
||||
export interface StatsSubmissionResponse {
|
||||
totalCount: number;
|
||||
submittedCount: number;
|
||||
submissionRate: number;
|
||||
statusLevel: string;
|
||||
}
|
||||
|
||||
export interface EcptReqCountResponse {
|
||||
statsYr: string;
|
||||
ecptReqCount: number;
|
||||
}
|
||||
|
||||
export interface ThreeStageStatusResponse {
|
||||
statsYr: string;
|
||||
lclgvSbmsnSttsCd: string;
|
||||
lclgvSbmsnSttsNm: string;
|
||||
lclgvRejected: boolean;
|
||||
mctpvPicSbmsnSttsCd: string;
|
||||
mctpvPicSbmsnSttsNm: string;
|
||||
mctpvRejected: boolean;
|
||||
pbcpSbmsnSttsCd: string;
|
||||
pbcpSbmsnSttsNm: string;
|
||||
pbcpRejected: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardService {
|
||||
getVrfcReport(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcReportResponse>>;
|
||||
getVrfcErrCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
|
||||
getUnSubmitCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
|
||||
getStatsSubmissionRate(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
|
||||
getEcptReqCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
|
||||
getThreeStageStatus(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<ThreeStageStatusResponse>>;
|
||||
}
|
||||
export interface DashboardSearchParams {
|
||||
statsYr: string;
|
||||
dtlInstCd: string;
|
||||
enoCd?: string;
|
||||
laeSeCd?: string;
|
||||
sewerBzmnCd?: string;
|
||||
sido?: string;
|
||||
sggNm?: string;
|
||||
unSubmitOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardApiResponse<T> {
|
||||
status: string;
|
||||
message: string;
|
||||
data: T;
|
||||
code: string;
|
||||
httpStatus: number;
|
||||
}
|
||||
|
||||
export interface VrfcReportResponse {
|
||||
typeCounts: Record<string, number>;
|
||||
list: VrfcReportSelect[];
|
||||
}
|
||||
|
||||
export interface VrfcReportSelect {
|
||||
vrfcCd: string;
|
||||
statsNm: string;
|
||||
rsnExpln: string;
|
||||
admdstNm: string;
|
||||
nowCn: string;
|
||||
cmprCn: string | null;
|
||||
}
|
||||
|
||||
export interface VrfcErrCountResponse {
|
||||
statsYr: string;
|
||||
vrfcErrCount: number;
|
||||
}
|
||||
|
||||
export interface UnSubmitCountResponse {
|
||||
statsYr: string;
|
||||
statusList: UnSubmitStatsResponse[];
|
||||
submittedCount: number;
|
||||
totalCount: number;
|
||||
unSubmittedCount: number;
|
||||
}
|
||||
|
||||
export interface UnSubmitStatsResponse {
|
||||
sbmsnYn: string;
|
||||
sidoSggNm: string;
|
||||
statsCd: string;
|
||||
statsNm: string;
|
||||
statusCd: string;
|
||||
statusNm: string;
|
||||
}
|
||||
|
||||
export interface StatsSubmissionResponse {
|
||||
totalCount: number;
|
||||
submittedCount: number;
|
||||
submissionRate: number;
|
||||
statusLevel: string;
|
||||
}
|
||||
|
||||
export interface EcptReqCountResponse {
|
||||
statsYr: string;
|
||||
ecptReqCount: number;
|
||||
}
|
||||
|
||||
export interface ThreeStageStatusResponse {
|
||||
statsYr: string;
|
||||
lclgvSbmsnSttsCd: string;
|
||||
lclgvSbmsnSttsNm: string;
|
||||
lclgvRejected: boolean;
|
||||
mctpvPicSbmsnSttsCd: string;
|
||||
mctpvPicSbmsnSttsNm: string;
|
||||
mctpvRejected: boolean;
|
||||
pbcpSbmsnSttsCd: string;
|
||||
pbcpSbmsnSttsNm: string;
|
||||
pbcpRejected: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardService {
|
||||
getVrfcReport(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcReportResponse>>;
|
||||
getVrfcErrCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
|
||||
getUnSubmitCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
|
||||
getStatsSubmissionRate(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
|
||||
getEcptReqCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
|
||||
getThreeStageStatus(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<ThreeStageStatusResponse>>;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ export interface CheckVerificationNameResponse {
|
||||
}
|
||||
|
||||
export interface UpdateVrfcMasterRequest {
|
||||
vrfcSn?: number | null;
|
||||
vrfcNm?: string | null;
|
||||
vrfcClsfCd?: string | null;
|
||||
dcptDgt?: number | null;
|
||||
@@ -184,13 +183,11 @@ export interface VerifyDetailRegistData {
|
||||
}
|
||||
|
||||
export interface VerifyDetailRegistRequest {
|
||||
vrfcSn: number | null;
|
||||
detailList: VerifyDetailRegistData[];
|
||||
rgtrUuid: string;
|
||||
}
|
||||
|
||||
export interface VerifyDetailUpdateRequest {
|
||||
vrfcSn: number | null;
|
||||
detailList: VerifyDetailRegistData[];
|
||||
mdfrUuid: string;
|
||||
}
|
||||
@@ -207,10 +204,10 @@ export interface VerifyResultSearchRequest {
|
||||
sewerBzmnCd?: string | null;
|
||||
statsYr: string;
|
||||
statsCd?: string | null;
|
||||
laeSbmsnSttsCd?: string | null;
|
||||
lclgvSbmsnSttsCd?: string | null;
|
||||
vrfcMthdCd?: string;
|
||||
prcsYnList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려)
|
||||
rejectCode?: string; // 반려 상태 (0: 반려 아님(default), 1: 지자체 반려, 2: 공단 반려)
|
||||
vrfcSttsCdList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려)
|
||||
statsVrfcRjtComCd?: string; // 반려 상태 (0: 반려 아님(default), 1: 지자체 반려, 2: 공단 반려)
|
||||
pbcpAprvYn?: string; // 공단 승인 여부 (Y/N)
|
||||
}
|
||||
|
||||
@@ -234,12 +231,16 @@ export interface VerifyResultItem {
|
||||
vrfcTrgtNm?: string;
|
||||
regDt?: string;
|
||||
rsnExpln?: string;
|
||||
nowVlCn?: string;
|
||||
compVlCn?: string;
|
||||
prcsYn?: string;
|
||||
prcsYnNm?: string;
|
||||
nowCn?: string;
|
||||
cmprCn?: string;
|
||||
lclgvSbmsnSttsCd?: string;
|
||||
lclgvSbmsnSttsNm?: string;
|
||||
vrfcSttsCd?: string;
|
||||
vrfcSttsNm?: string;
|
||||
statsVrfcRjtComCd?: string;
|
||||
statsVrfcRjtComNm?: string;
|
||||
prcsOpnnExpln?: string;
|
||||
prcrId?: string;
|
||||
prcrUuid?: string;
|
||||
rjctOpnnCn?: string;
|
||||
pbcpAprvYn?: string;
|
||||
mdfrUuid?: string;
|
||||
@@ -293,6 +294,7 @@ export interface GeneralverificationService {
|
||||
param: CheckVerificationNameRequest,
|
||||
): Promise<CheckVerificationNameResponse>;
|
||||
UpdateVrfcMaster(
|
||||
vrfcSn: number,
|
||||
param: UpdateVrfcMasterRequest,
|
||||
): Promise<UpdateVrfcMasterResponse>;
|
||||
RegistVrfcMaster(
|
||||
@@ -311,9 +313,11 @@ export interface GeneralverificationService {
|
||||
param: string,
|
||||
): Promise<VerifyColumnListResponse>;
|
||||
RegistVrfcDetail(
|
||||
vrfcSn: number,
|
||||
param: VerifyDetailRegistRequest,
|
||||
): Promise<RegistVrfcMasterResponse>;
|
||||
UpdateVrfcDetail(
|
||||
vrfcSn: number,
|
||||
param: VerifyDetailUpdateRequest,
|
||||
): Promise<RegistVrfcMasterResponse>;
|
||||
CopyVrfcData(
|
||||
|
||||
+1
-3
@@ -117,7 +117,7 @@ export type SlurgeProcessingFacStatusApiResponse =
|
||||
};
|
||||
|
||||
/**
|
||||
* ST00110001 저장 — `saveList` / `deleteList` 항목.
|
||||
* ST00110001 저장 — `saveList` 항목.
|
||||
* `sggNm`·계산필드·검증결과는 저장 요청에서 제외.
|
||||
*/
|
||||
export type SlurgeProcessingFacStatusSaveItem = Omit<
|
||||
@@ -167,7 +167,6 @@ export interface SlurgeProcessingFacStatusSaveRequest {
|
||||
superAdmin?: string;
|
||||
noApplicable?: boolean;
|
||||
saveList: SlurgeProcessingFacStatusSaveItem[];
|
||||
deleteList: SlurgeProcessingFacStatusSaveItem[];
|
||||
}
|
||||
|
||||
export type SlurgeProcessingFacStatusSaveResponse = ApiResponse<unknown> & {
|
||||
@@ -194,7 +193,6 @@ export interface SlurgeProcessingFacStatusSubmitRequest {
|
||||
superAdmin?: string;
|
||||
noApplicable?: boolean;
|
||||
submitList: SlurgeProcessingFacStatusSubmitItem[];
|
||||
deleteList: SlurgeProcessingFacStatusSubmitItem[];
|
||||
}
|
||||
|
||||
export interface SlurgeProcessingFacStatusSubmitResponse {
|
||||
|
||||
-2
@@ -98,7 +98,6 @@ export type SewagePipeMaintainDredgingResponse = ApiResponse<SewagePipeMaintainD
|
||||
/** ST00030001 하수관로 유지관리 > 개보수 저장·제출 API 행 */
|
||||
export interface SewagePipeMaintainRepairSaveItem {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
dfrpWholPrlg: number;
|
||||
cbsysDfrpPrlg: number;
|
||||
@@ -167,7 +166,6 @@ export interface SewagePipeMaintainRepairSubmitResponse {
|
||||
/** ST00030002 하수관로 유지관리 > 관로준설 저장·제출 API 행 */
|
||||
export interface SewagePipeMaintainDredgingSaveItem {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
wholFcltPrlg: number;
|
||||
wholSwagePpl: number;
|
||||
|
||||
-4
@@ -92,7 +92,6 @@ export interface SewerageFinanceSettleRequestParams {
|
||||
*/
|
||||
export interface SewerageFinanceSettleSaveItem {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
soaGovSprtAmt: number;
|
||||
mctpvLcexpAmt: number;
|
||||
@@ -255,7 +254,6 @@ export interface SewerageChargeRequestParams {
|
||||
*/
|
||||
export interface SewerageChargeSaveItem {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
anlLevyQnt: number;
|
||||
sewerLevyAmt: number;
|
||||
@@ -362,7 +360,6 @@ export interface SewerageUsageFeeRequestParams {
|
||||
*/
|
||||
export interface SewerageUsageFeeSaveItem {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
hshusLevyAmt: number;
|
||||
hshusLevyQnt: number;
|
||||
@@ -472,7 +469,6 @@ export interface CauserBurdenRequestParams {
|
||||
*/
|
||||
export interface CauserBurdenSaveItem {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
brdnAmtLevyNocs: number;
|
||||
brdnAmtLevyAmt: number;
|
||||
|
||||
@@ -6,7 +6,8 @@ export interface StatsInputStatusListRequestParams {
|
||||
sewerBzmnCd?: string | null;
|
||||
statsYr: string;
|
||||
statsCd?: string | null;
|
||||
laeSbmsnSttsCd?: string | null;
|
||||
/** 지자체 제출 상태 코드 목록 (쿼리스트링 lclgvSbmsnSttsCd=2&lclgvSbmsnSttsCd=3 형태로 전송) */
|
||||
lclgvSbmsnSttsCd?: string[] | null;
|
||||
}
|
||||
|
||||
export interface StatsInputStatusList {
|
||||
@@ -46,37 +47,43 @@ export interface ReportSearchRequestDto {
|
||||
|
||||
/** 통계 검증 리포트 현황 조회 ResponseDto */
|
||||
export interface ReportStatusResponseDto {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
sewerBzmnCd?: string;
|
||||
statsCd?: string;
|
||||
menuTabName?: string;
|
||||
laeSbmsnSttsCd?: string;
|
||||
laeSbmsnSttsNm?: string;
|
||||
laeSbmsnYmd?: string;
|
||||
statPicSbmsnSttsCd?: string;
|
||||
statPicSbmsnSttsNm?: string;
|
||||
ctpvSbmsnYmd?: string;
|
||||
pbcpSbmsnSttsCd?: string;
|
||||
pbcpSbmsnSttsNm?: string;
|
||||
pbcpCfmtnYmd?: string;
|
||||
laePicNm?: string;
|
||||
laePicTelNo?: string;
|
||||
laeKbrdrDeptNm?: string;
|
||||
laeKbrdrJbgdNm?: string;
|
||||
laeIdfrNm?: string;
|
||||
laeIdfrTelNo?: string;
|
||||
laeIdfrDeptNm?: string;
|
||||
laeIdfrJbgdNm?: string;
|
||||
mctpvIdfrNm?: string;
|
||||
mctpvIdfrTelNo?: string;
|
||||
mctpvIdfrDeptNm?: string;
|
||||
mctpvIdfrJbgdNm?: string;
|
||||
lastCfmtnPicNm?: string;
|
||||
lastCfmtnPicTelNo?: string;
|
||||
lastCfmtnPicDeptNm?: string;
|
||||
lastCfmtnPicJbgdNm?: string;
|
||||
rjctOpnnCn?: string;
|
||||
ctpvNm?: string | null;
|
||||
sggNm?: string | null;
|
||||
sewerBzmnCd?: string | null;
|
||||
statsCd?: string | null;
|
||||
menuTabName?: string | null;
|
||||
lclgvSbmsnSttsCd?: string | null;
|
||||
lclgvSbmsnSttsNm?: string | null;
|
||||
lclgvSbmsnYmd?: string | null;
|
||||
mctpvPicSbmsnSttsCd?: string | null;
|
||||
mctpvPicSbmsnSttsNm?: string | null;
|
||||
ctpvSbmsnYmd?: string | null;
|
||||
pbcpSbmsnSttsCd?: string | null;
|
||||
pbcpSbmsnSttsNm?: string | null;
|
||||
pbcpCfmtnYmd?: string | null;
|
||||
lclgvPicNm?: string | null;
|
||||
lclgvPicTelnoCn?: string | null;
|
||||
lclgvKbrdrDeptNm?: string | null;
|
||||
lclgvKbrdrJbgdNm?: string | null;
|
||||
lclgvIdfrNm?: string | null;
|
||||
lclgvIdfrTelnoCn?: string | null;
|
||||
lclgvIdfrDeptNm?: string | null;
|
||||
lclgvIdfrJbgdNm?: string | null;
|
||||
mctpvIdfrNm?: string | null;
|
||||
mctpvIdfrTelnoCn?: string | null;
|
||||
mctpvIdfrDeptNm?: string | null;
|
||||
mctpvIdfrJbgdNm?: string | null;
|
||||
pbcpPicNm?: string | null;
|
||||
pbcpPicTelnoCn?: string | null;
|
||||
rvwrDeptNm?: string | null;
|
||||
rvwrJbgdNm?: string | null;
|
||||
lastCfmtnPicNm?: string | null;
|
||||
lastCfmtnPicTelnoCn?: string | null;
|
||||
lastCfmtnPicDeptNm?: string | null;
|
||||
lastCfmtnPicJbgdNm?: string | null;
|
||||
splmntRsltCn?: string | null;
|
||||
splmntOpnnCn?: string | null;
|
||||
rjctOpnnCn?: string | null;
|
||||
}
|
||||
|
||||
export type ReportStatusListApiResponse = ApiResponse<
|
||||
|
||||
+4
-3
@@ -99,7 +99,6 @@ export type SeweragePipeInputRegionalFactStatusSaveItem = Omit<
|
||||
"verificationResult" | "admdstCd" | "sggNm" | "admdstNm" | "goalYr"
|
||||
> & {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
goalYr: string | null;
|
||||
};
|
||||
@@ -118,7 +117,6 @@ export type SeweragePipeInputFactInstallStatusSaveItem = Omit<
|
||||
| "rmrk"
|
||||
> & {
|
||||
admdstCd: string | null;
|
||||
sggNm: string | null;
|
||||
admdstNm: string | null;
|
||||
fcltSn: string | null;
|
||||
swtrmtFcltCd: string | null;
|
||||
@@ -310,7 +308,6 @@ export interface SeweragePipeInputPipeTypeStatusSaveItem {
|
||||
rmrk: string | null;
|
||||
lyngPrlgWhol: number;
|
||||
dmltnPrlgWhol: number;
|
||||
pipeByPrstWhol: number;
|
||||
}
|
||||
|
||||
/** 관종별 현황 저장 API 요청 */
|
||||
@@ -422,6 +419,10 @@ export interface SeweragePipeInputManholeStatusItem {
|
||||
folvlMnhlCnt: number;
|
||||
filvlMnhlCnt: number;
|
||||
rmrk: string;
|
||||
wholMnhlCntSum: number;
|
||||
frlvlMnhlCntSum: number;
|
||||
thlvlMnhlCntSum: number;
|
||||
picNm: string | null;
|
||||
verificationResult?: SeweragePipeInputVerificationResult;
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -86,7 +86,6 @@ export interface SewerageSupplyRateRequestParams {
|
||||
|
||||
/** 하수도 보급률 저장 API 요청 body (한 행) */
|
||||
export interface SewerageSupplyRateSaveItem {
|
||||
sggNm: string | null;
|
||||
admdstCd: string | null;
|
||||
admdstNm: string | null;
|
||||
statsTrbtrNm: string | null;
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface ApproveRequest {
|
||||
statsCd: string;
|
||||
mdfrUuid: string;
|
||||
lastCfmtnPicNm?: string;
|
||||
lastCfmtnPicTelNo?: string;
|
||||
lastCfmtnPicTelnoCn?: string;
|
||||
lastCfmtnPicDeptNm?: string;
|
||||
lastCfmtnPicJbgdNm?: string;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,21 @@ export interface cancelSubmitResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 검증 이전 상태로 롤백 요청 */
|
||||
export interface rollbackStatusRequest {
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
sewerBzmnCd: string;
|
||||
rollbackStatusCode: string;
|
||||
mdfrUuid: string;
|
||||
}
|
||||
|
||||
/** 검증 이전 상태로 롤백 응답 */
|
||||
export interface rollbackStatusResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
status: string;
|
||||
data?: T;
|
||||
@@ -173,10 +188,8 @@ export interface VerificationResultListRequest {
|
||||
statsCd?: string;
|
||||
/** 검증 방법 코드 (예: NV, IMR, HB) */
|
||||
vrfcMthdCd?: string;
|
||||
/** 처리여부 코드 목록, 콤마 구분 (예: 0,2) */
|
||||
prcsYnList?: string;
|
||||
/** 반려상태 코드 목록, 콤마 구분 (예: 0,1,2) */
|
||||
rejectCodeList?: string;
|
||||
/** 검증결과상태 코드 목록, 콤마 구분 (예: 0, 0,2, 2) */
|
||||
vrfcSttsCdList?: string;
|
||||
/** @deprecated 신규 API에서는 미사용 */
|
||||
pbcpAprvYn?: string;
|
||||
page?: number;
|
||||
@@ -559,6 +572,7 @@ export interface StatisticsService {
|
||||
saveStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
|
||||
deleteStatsInputPeriod(param: StatsInputPeriodDeleteRequest): Promise<StatsInputPeriodDeleteApiResponse>;
|
||||
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
|
||||
rollbackStatus(param: rollbackStatusRequest): Promise<rollbackStatusResponse>;
|
||||
callImrService(param: ImrRequest): Promise<ImrResponse>;
|
||||
submitException(param: SubmitExceptionRequest): Promise<SubmitExceptionResponse>;
|
||||
getLogList(param: StatisticsLogRequest): Promise<StatisticsLogResponse>;
|
||||
|
||||
@@ -247,6 +247,10 @@ export interface SewagePipeReviewManholeItem {
|
||||
folvlMnhlCnt: number;
|
||||
filvlMnhlCnt: number;
|
||||
rmrk: string | null;
|
||||
wholMnhlCntSum: number;
|
||||
frlvlMnhlCntSum: number;
|
||||
thlvlMnhlCntSum: number;
|
||||
picNm: string | null;
|
||||
}
|
||||
|
||||
export interface SewagePipeManholeResponseData {
|
||||
|
||||
+4
@@ -168,6 +168,10 @@ export interface FcltyManholeSummaryItem {
|
||||
thlvlMnhlCnt: number;
|
||||
folvlMnhlCnt: number;
|
||||
filvlMnhlCnt: number;
|
||||
wholMnhlCntSum: number;
|
||||
frlvlMnhlCntSum: number;
|
||||
thlvlMnhlCntSum: number;
|
||||
picNm: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+277
-277
@@ -1,277 +1,277 @@
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
const READ_ONLY_STYLE = "rg-cell-readonly";
|
||||
|
||||
export const statisticsVerificationReportColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{
|
||||
name: "ctpvNm",
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sggNm",
|
||||
fieldName: "sggNm",
|
||||
header: { text: "시군구" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "menuTabName",
|
||||
fieldName: "menuTabName",
|
||||
header: { text: "구분" },
|
||||
width: 150,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laePicNm",
|
||||
fieldName: "laePicNm",
|
||||
header: { text: "지자체작성자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laePicTelNo",
|
||||
fieldName: "laePicTelNo",
|
||||
header: { text: "연락처" },
|
||||
width: 130,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeKbrdrDeptNm",
|
||||
fieldName: "laeKbrdrDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeKbrdrJbgdNm",
|
||||
fieldName: "laeKbrdrJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeIdfrNm",
|
||||
fieldName: "laeIdfrNm",
|
||||
header: { text: "지자체승인자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeIdfrDeptNm",
|
||||
fieldName: "laeIdfrDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeIdfrJbgdNm",
|
||||
fieldName: "laeIdfrJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lastCfmtnPicNm",
|
||||
fieldName: "lastCfmtnPicNm",
|
||||
header: { text: "공단확인자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lastCfmtnPicDeptNm",
|
||||
fieldName: "lastCfmtnPicDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lastCfmtnPicJbgdNm",
|
||||
fieldName: "lastCfmtnPicJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "rjctOpnnCn",
|
||||
fieldName: "rjctOpnnCn",
|
||||
header: { text: "반려사유" },
|
||||
width: 220,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sewerBzmnCd",
|
||||
fieldName: "sewerBzmnCd",
|
||||
header: { text: "sewerBzmnCd" },
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
name: "statsCd",
|
||||
fieldName: "statsCd",
|
||||
header: { text: "statsCd" },
|
||||
visible: false,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"menuTabName",
|
||||
"laePicNm",
|
||||
"laePicTelNo",
|
||||
"laeKbrdrDeptNm",
|
||||
"laeKbrdrJbgdNm",
|
||||
"laeIdfrNm",
|
||||
"laeIdfrDeptNm",
|
||||
"laeIdfrJbgdNm",
|
||||
"lastCfmtnPicNm",
|
||||
"lastCfmtnPicDeptNm",
|
||||
"lastCfmtnPicJbgdNm",
|
||||
"rjctOpnnCn",
|
||||
"sewerBzmnCd",
|
||||
"statsCd",
|
||||
],
|
||||
};
|
||||
|
||||
export const statisticsVerificationResultColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{
|
||||
name: "ctpvNm",
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sggNm",
|
||||
fieldName: "sggNm",
|
||||
header: { text: "시군구" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "statsNm",
|
||||
fieldName: "statsNm",
|
||||
header: { text: "지표" },
|
||||
width: 140,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "vrfcTrgtNm",
|
||||
fieldName: "vrfcTrgtNm",
|
||||
header: { text: "세부항목" },
|
||||
width: 180,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "regDt",
|
||||
fieldName: "regDt",
|
||||
header: { text: "검증날짜" },
|
||||
width: 150,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "vrfcMthdNm",
|
||||
fieldName: "vrfcMthdNm",
|
||||
header: { text: "검증유형" },
|
||||
width: 100,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "rsnExpln",
|
||||
fieldName: "rsnExpln",
|
||||
header: { text: "오류내역" },
|
||||
width: 200,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "nowVlCn",
|
||||
fieldName: "nowVlCn",
|
||||
header: { text: "입력값" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "compVlCn",
|
||||
fieldName: "compVlCn",
|
||||
header: { text: "비교값" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "prcsYnNm",
|
||||
fieldName: "prcsYnNm",
|
||||
header: { text: "상태" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "prcsOpnnExpln",
|
||||
fieldName: "prcsOpnnExpln",
|
||||
header: { text: "예외사유" },
|
||||
width: 170,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "stVrfRjtComNm",
|
||||
fieldName: "stVrfRjtComNm",
|
||||
header: { text: "반려상태" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "rjctOpnnCn",
|
||||
fieldName: "rjctOpnnCn",
|
||||
header: { text: "반려의견" },
|
||||
width: 170,
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "pbcpAprvYn",
|
||||
fieldName: "pbcpAprvYn",
|
||||
header: { text: "승인여부" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "vrfcSn",
|
||||
fieldName: "vrfcSn",
|
||||
header: { text: "vrfcSn" },
|
||||
visible: false,
|
||||
editable: false,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"statsNm",
|
||||
"vrfcTrgtNm",
|
||||
"regDt",
|
||||
"vrfcMthdNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"prcsOpnnExpln",
|
||||
"stVrfRjtComNm",
|
||||
"rjctOpnnCn",
|
||||
"pbcpAprvYn",
|
||||
"vrfcSn",
|
||||
],
|
||||
};
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
const READ_ONLY_STYLE = "rg-cell-readonly";
|
||||
|
||||
export const statisticsVerificationReportColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{
|
||||
name: "ctpvNm",
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sggNm",
|
||||
fieldName: "sggNm",
|
||||
header: { text: "시군구" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "menuTabName",
|
||||
fieldName: "menuTabName",
|
||||
header: { text: "구분" },
|
||||
width: 150,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lclgvPicNm",
|
||||
fieldName: "lclgvPicNm",
|
||||
header: { text: "지자체작성자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lclgvPicTelnoCn",
|
||||
fieldName: "lclgvPicTelnoCn",
|
||||
header: { text: "연락처" },
|
||||
width: 130,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lclgvKbrdrDeptNm",
|
||||
fieldName: "lclgvKbrdrDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lclgvKbrdrJbgdNm",
|
||||
fieldName: "lclgvKbrdrJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lclgvIdfrNm",
|
||||
fieldName: "lclgvIdfrNm",
|
||||
header: { text: "지자체승인자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lclgvIdfrDeptNm",
|
||||
fieldName: "lclgvIdfrDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lclgvIdfrJbgdNm",
|
||||
fieldName: "lclgvIdfrJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lastCfmtnPicNm",
|
||||
fieldName: "lastCfmtnPicNm",
|
||||
header: { text: "공단확인자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lastCfmtnPicDeptNm",
|
||||
fieldName: "lastCfmtnPicDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lastCfmtnPicJbgdNm",
|
||||
fieldName: "lastCfmtnPicJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "rjctOpnnCn",
|
||||
fieldName: "rjctOpnnCn",
|
||||
header: { text: "반려사유" },
|
||||
width: 220,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sewerBzmnCd",
|
||||
fieldName: "sewerBzmnCd",
|
||||
header: { text: "sewerBzmnCd" },
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
name: "statsCd",
|
||||
fieldName: "statsCd",
|
||||
header: { text: "statsCd" },
|
||||
visible: false,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"menuTabName",
|
||||
"lclgvPicNm",
|
||||
"lclgvPicTelnoCn",
|
||||
"lclgvKbrdrDeptNm",
|
||||
"lclgvKbrdrJbgdNm",
|
||||
"lclgvIdfrNm",
|
||||
"lclgvIdfrDeptNm",
|
||||
"lclgvIdfrJbgdNm",
|
||||
"lastCfmtnPicNm",
|
||||
"lastCfmtnPicDeptNm",
|
||||
"lastCfmtnPicJbgdNm",
|
||||
"rjctOpnnCn",
|
||||
"sewerBzmnCd",
|
||||
"statsCd",
|
||||
],
|
||||
};
|
||||
|
||||
export const statisticsVerificationResultColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{
|
||||
name: "ctpvNm",
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sggNm",
|
||||
fieldName: "sggNm",
|
||||
header: { text: "시군구" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "statsNm",
|
||||
fieldName: "statsNm",
|
||||
header: { text: "지표" },
|
||||
width: 140,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "vrfcTrgtNm",
|
||||
fieldName: "vrfcTrgtNm",
|
||||
header: { text: "세부항목" },
|
||||
width: 180,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "regDt",
|
||||
fieldName: "regDt",
|
||||
header: { text: "검증날짜" },
|
||||
width: 150,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "vrfcMthdNm",
|
||||
fieldName: "vrfcMthdNm",
|
||||
header: { text: "검증유형" },
|
||||
width: 100,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "rsnExpln",
|
||||
fieldName: "rsnExpln",
|
||||
header: { text: "오류내역" },
|
||||
width: 200,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "nowCn",
|
||||
fieldName: "nowCn",
|
||||
header: { text: "입력값" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "cmprCn",
|
||||
fieldName: "cmprCn",
|
||||
header: { text: "비교값" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "vrfcSttsNm",
|
||||
fieldName: "vrfcSttsNm",
|
||||
header: { text: "상태" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "prcsOpnnExpln",
|
||||
fieldName: "prcsOpnnExpln",
|
||||
header: { text: "예외사유" },
|
||||
width: 170,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "statsVrfcRjtComNm",
|
||||
fieldName: "statsVrfcRjtComNm",
|
||||
header: { text: "반려상태" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "rjctOpnnCn",
|
||||
fieldName: "rjctOpnnCn",
|
||||
header: { text: "반려의견" },
|
||||
width: 170,
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "pbcpAprvYn",
|
||||
fieldName: "pbcpAprvYn",
|
||||
header: { text: "승인여부" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "vrfcSn",
|
||||
fieldName: "vrfcSn",
|
||||
header: { text: "vrfcSn" },
|
||||
visible: false,
|
||||
editable: false,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"statsNm",
|
||||
"vrfcTrgtNm",
|
||||
"regDt",
|
||||
"vrfcMthdNm",
|
||||
"rsnExpln",
|
||||
"nowCn",
|
||||
"cmprCn",
|
||||
"vrfcSttsNm",
|
||||
"prcsOpnnExpln",
|
||||
"statsVrfcRjtComNm",
|
||||
"rjctOpnnCn",
|
||||
"pbcpAprvYn",
|
||||
"vrfcSn",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -19,14 +19,14 @@ interface StatsSubmissionPanelProps {
|
||||
const STATUS_LEVEL_TEXT_COLOR = {
|
||||
GREAT: "text-blue-500",
|
||||
GOOD: "text-green-500",
|
||||
WARNING: "text-yellow-500",
|
||||
NORMAL: "text-yellow-500",
|
||||
BAD: "text-red-500",
|
||||
};
|
||||
|
||||
const STATUS_LEVEL_EMOJI = {
|
||||
GREAT: <Laugh size={160} className={STATUS_LEVEL_TEXT_COLOR.GREAT} />,
|
||||
GOOD: <Smile size={160} className={STATUS_LEVEL_TEXT_COLOR.GOOD} />,
|
||||
WARNING: <Meh size={160} className={STATUS_LEVEL_TEXT_COLOR.WARNING} />,
|
||||
NORMAL: <Meh size={160} className={STATUS_LEVEL_TEXT_COLOR.NORMAL} />,
|
||||
BAD: <Frown size={160} className={STATUS_LEVEL_TEXT_COLOR.BAD} />,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,133 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardVrfcReport } from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
import {
|
||||
Badge,
|
||||
type BadgeProps,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
interface VrfcReportPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
NV: "일반",
|
||||
IMR: "I-MR",
|
||||
HB: "HB",
|
||||
};
|
||||
|
||||
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
||||
NV: "gray",
|
||||
IMR: "red",
|
||||
HB: "blue",
|
||||
};
|
||||
|
||||
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useDashboardVrfcReport(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
|
||||
</div>
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-2">
|
||||
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
|
||||
{data.typeCounts &&
|
||||
Object.keys(data.typeCounts).length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(data.typeCounts).map(
|
||||
([type, count]) => (
|
||||
<Badge
|
||||
key={type}
|
||||
color={
|
||||
TYPE_CODE_COLOR_MAP[type]
|
||||
}
|
||||
className="rounded-full px-2 py-1 text-lg"
|
||||
>
|
||||
{TYPE_CODE_NAME_MAP[type]}:{" "}
|
||||
{count}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
|
||||
<div className="max-h-[44rem] overflow-y-auto">
|
||||
{data.list.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
이상치 항목이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 tracking-tight">
|
||||
{data.list.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col rounded-sm border border-border p-3"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.statsNm}
|
||||
</span>
|
||||
<Badge
|
||||
color={
|
||||
TYPE_CODE_COLOR_MAP[
|
||||
item.vrfcCd
|
||||
]
|
||||
}
|
||||
className="text-sm"
|
||||
>
|
||||
{
|
||||
TYPE_CODE_NAME_MAP[
|
||||
item.vrfcCd
|
||||
]
|
||||
}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{item.rsnExpln}
|
||||
</p>
|
||||
<div className="flex justify-between text-lg text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>
|
||||
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
|
||||
현재: {item.nowVlCn ?? "-"}{" "}
|
||||
/ 비교:{" "}
|
||||
{item.compVlCn ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { useDashboardVrfcReport } from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
import {
|
||||
Badge,
|
||||
type BadgeProps,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
interface VrfcReportPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
NV: "일반",
|
||||
IMR: "I-MR",
|
||||
HB: "HB",
|
||||
};
|
||||
|
||||
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
||||
NV: "gray",
|
||||
IMR: "red",
|
||||
HB: "blue",
|
||||
};
|
||||
|
||||
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useDashboardVrfcReport(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
|
||||
</div>
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-2">
|
||||
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
|
||||
{data.typeCounts &&
|
||||
Object.keys(data.typeCounts).length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(data.typeCounts).map(
|
||||
([type, count]) => (
|
||||
<Badge
|
||||
key={type}
|
||||
color={
|
||||
TYPE_CODE_COLOR_MAP[type]
|
||||
}
|
||||
className="rounded-full px-2 py-1 text-lg"
|
||||
>
|
||||
{TYPE_CODE_NAME_MAP[type]}:{" "}
|
||||
{count}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
|
||||
<div className="max-h-[44rem] overflow-y-auto">
|
||||
{data.list.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
이상치 항목이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 tracking-tight">
|
||||
{data.list.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col rounded-sm border border-border p-3"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.statsNm}
|
||||
</span>
|
||||
<Badge
|
||||
color={
|
||||
TYPE_CODE_COLOR_MAP[
|
||||
item.vrfcCd
|
||||
]
|
||||
}
|
||||
className="text-sm"
|
||||
>
|
||||
{
|
||||
TYPE_CODE_NAME_MAP[
|
||||
item.vrfcCd
|
||||
]
|
||||
}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{item.rsnExpln}
|
||||
</p>
|
||||
<div className="flex justify-between text-lg text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>
|
||||
{/* TBD: 증감 표시 방법 (nowCn - cmprCn 계산?) */}
|
||||
현재:{" "}{item.nowCn ?? "-"}
|
||||
/ 비교:{" "}
|
||||
{item.cmprCn ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ function StatisticsPageLayout({ children }: { children: React.ReactNode }) {
|
||||
<TabBar />
|
||||
<PageTitle
|
||||
title={pageTitle?.label ?? ""}
|
||||
menuCd={pageTitle?.menuItem?.menuCd}
|
||||
menuPaths={menuPaths ?? []}
|
||||
/>
|
||||
{children}
|
||||
|
||||
@@ -73,7 +73,10 @@ function Calendar({
|
||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
||||
defaultClassNames.dropdown_root,
|
||||
),
|
||||
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
|
||||
dropdown: cn(
|
||||
"absolute inset-0 opacity-0 [&_option]:bg-background [&_option]:text-foreground",
|
||||
defaultClassNames.dropdown,
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
|
||||
Reference in New Issue
Block a user