[as] feat: 보완요청 기능 구현
This commit is contained in:
@@ -65,6 +65,14 @@ async function techReview(id: string, payload: InstlSncnApplySaveRequestDto): Pr
|
||||
await client.post(`${BASE}/${id}/tech-review`, payload);
|
||||
}
|
||||
|
||||
async function submitReqSupplement(
|
||||
id: string,
|
||||
payload: InstlSncnApplySaveRequestDto,
|
||||
): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/reqsupplement`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
await client.delete(`${BASE}/${id}`);
|
||||
}
|
||||
@@ -80,6 +88,7 @@ export const instlSncnService = {
|
||||
saveApply,
|
||||
update,
|
||||
techReview,
|
||||
submitReqSupplement,
|
||||
remove,
|
||||
changePrgrsSttsComCd,
|
||||
} satisfies InstlSncnMngService;
|
||||
|
||||
@@ -99,6 +99,19 @@ export function useInstlSncnTechReview() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnSubmitReqSupplement() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) =>
|
||||
instlSncnService.submitReqSupplement(id, payload),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstlSncnRemove() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
useInstlSncnList,
|
||||
useInstlSncnSidoCombo,
|
||||
useInstlSncnSigunguCombo,
|
||||
useInstlSncnSubmitReqSupplement,
|
||||
useInstlSncnTechReview,
|
||||
} from "./api/queries";
|
||||
export {
|
||||
|
||||
@@ -190,15 +190,15 @@ export interface InstlSncnApplySaveRequestDto extends PbadmsApplySaveHeader {
|
||||
/** 설치인가관리 정보 */
|
||||
instlSncnMng: InstlSncnMngSaveRequestDto;
|
||||
/** 사업규모 목록 */
|
||||
bzScaleList: InstlSncnBzScaleSaveDto[];
|
||||
bzScaleList?: InstlSncnBzScaleSaveDto[];
|
||||
/** 총사업비 목록 */
|
||||
bizCstList: InstlSncnBizCstSaveRequestDto[];
|
||||
bizCstList?: InstlSncnBizCstSaveRequestDto[];
|
||||
/** 체크리스트 검토결과 목록 */
|
||||
cklstResultList: InstlSncnCklstResultSaveDto[];
|
||||
cklstResultList?: InstlSncnCklstResultSaveDto[];
|
||||
/** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */
|
||||
cklstResultMap?: Record<string, InstlSncnCklstResultSaveDto[]>;
|
||||
/** 필수첨부파일 목록 */
|
||||
esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[];
|
||||
esntlAtchFileList?: InstlSncnEsntlAtchFileSaveDto[];
|
||||
/** 검토의견 목록 */
|
||||
rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[];
|
||||
/** 보완요구 목록 */
|
||||
@@ -222,8 +222,11 @@ export type InstlSncnRvwOpnnResponseDto = InstlSncnRvwOpnnSaveRequestDto &
|
||||
};
|
||||
|
||||
/** 설치인가관리 상세 응답 DTO */
|
||||
export interface InstlSncnMngDetResponse
|
||||
extends PbadmsMngDetResponseBase<InstlSncnMngResponseDto, InstlSncnRvwOpnnResponseDto> {
|
||||
export interface InstlSncnMngDetResponse extends PbadmsMngDetResponseBase<
|
||||
InstlSncnMngResponseDto,
|
||||
InstlSncnRvwOpnnResponseDto,
|
||||
InstlSncnSplmntDmndSaveDto
|
||||
> {
|
||||
detDto: InstlSncnMngResponseDto;
|
||||
bizSclList: BizSclTreeDto[];
|
||||
bizCstList: BizCstTreeDto[];
|
||||
@@ -239,6 +242,7 @@ export interface InstlSncnMngService {
|
||||
saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise<string>;
|
||||
update: (id: string, payload: InstlSncnMngUpdateRequestDto) => Promise<void>;
|
||||
techReview: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<void>;
|
||||
submitReqSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise<string>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
changePrgrsSttsComCd: (payload: DocPrgrsUpdateRequestDto) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
|
||||
dmndSplmnt: "dmndSplmnt",
|
||||
atrzAprv: "atrzAprv",
|
||||
atrzRply: "atrzRply",
|
||||
grndsChck: "grndsChck",
|
||||
} as const;
|
||||
|
||||
export type FnrsCnsltnViewButtonAction =
|
||||
@@ -44,10 +43,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
|
||||
LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
||||
TP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply],
|
||||
},
|
||||
ROLE_USER01: {
|
||||
TE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.grndsChck],
|
||||
SL: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||
|
||||
@@ -1,150 +1,168 @@
|
||||
import type {
|
||||
BizCstItem,
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
BzScaleItem,
|
||||
CklstResultItem,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnSplmntDmndItem,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import { mapFnrsSharDetToFormList, toFnrsSharSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { ChecklistFormState, ReviewStepCode } from "../model/view-types";
|
||||
|
||||
import { toCklstSaveDto } from "./checklist-utils";
|
||||
import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||
|
||||
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] {
|
||||
return bizSclList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
cnstpSeCd: node.cnstpSeCd,
|
||||
bscPl: node.bscPlanQty,
|
||||
budgetReflct: node.bgtRfltQty,
|
||||
thtiDsgn: node.tstmDesignQty,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] {
|
||||
return bizCstList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
acntgSeCd: node.acntgSeCd,
|
||||
bscPlanAmt: node.bscPlanAmt,
|
||||
bgtRfltAmt: node.bgtRfltAmt,
|
||||
tstmDesignAmt: node.tstmDesignAmt,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||
}
|
||||
|
||||
const bizSclList = data.bizSclList ?? [];
|
||||
const bizCstList = data.bizCstList ?? [];
|
||||
const cklstResultList = data.cklstResultList ?? [];
|
||||
const esntlFileList = data.esntlFileList ?? [];
|
||||
|
||||
return {
|
||||
fnrsCnsltnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewReplyPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
|
||||
opnrNm?: string,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSupplementReqPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
splmntDmndCn: string,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||
}
|
||||
|
||||
const nextSn =
|
||||
(data.splmntDmndList?.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) ?? 0) + 1;
|
||||
|
||||
const splmntDmndList: FnrsCnsltnSplmntDmndItem[] = [
|
||||
...(data.splmntDmndList ?? []),
|
||||
{
|
||||
splmntDmndSn: nextSn,
|
||||
splmntDmndCn,
|
||||
cmptnYn: "N",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
fnrsCnsltnMng: detDto,
|
||||
splmntDmndList,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSupplementSubmitPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
|
||||
splmntDmndList: data.splmntDmndList,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewPayload(
|
||||
det: FnrsCnsltnMngResponseDto,
|
||||
cklstResultList: CklstResultItem[],
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
return {
|
||||
fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" },
|
||||
bzScaleList: [],
|
||||
bizCstList: [],
|
||||
cklstResultList,
|
||||
esntlAtchFileList: [],
|
||||
};
|
||||
}
|
||||
import type {
|
||||
BizCstItem,
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
BzScaleItem,
|
||||
CklstResultItem,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnSplmntDmndItem,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import {
|
||||
mapFnrsSharDetToFormList,
|
||||
type SplmntDmndDialogConfirmPayload,
|
||||
toFnrsSharSaveList,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { ChecklistFormState, ReviewStepCode } from "../model/view-types";
|
||||
|
||||
import { toCklstSaveDto } from "./checklist-utils";
|
||||
import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||
|
||||
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] {
|
||||
return bizSclList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
cnstpSeCd: node.cnstpSeCd,
|
||||
bscPl: node.bscPlanQty,
|
||||
budgetReflct: node.bgtRfltQty,
|
||||
thtiDsgn: node.tstmDesignQty,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] {
|
||||
return bizCstList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
acntgSeCd: node.acntgSeCd,
|
||||
bscPlanAmt: node.bscPlanAmt,
|
||||
bgtRfltAmt: node.bgtRfltAmt,
|
||||
tstmDesignAmt: node.tstmDesignAmt,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
throw new Error("상세 정보(detDto)가 없습니다.");
|
||||
}
|
||||
|
||||
const bizSclList = data.bizSclList ?? [];
|
||||
const bizCstList = data.bizCstList ?? [];
|
||||
const cklstResultList = data.cklstResultList ?? [];
|
||||
const esntlFileList = data.esntlFileList ?? [];
|
||||
|
||||
return {
|
||||
fnrsCnsltnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewReplyPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
|
||||
opnrNm?: string,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSupplementReqPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
payload: SplmntDmndDialogConfirmPayload,
|
||||
checklistFormState: ChecklistFormState,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
const existingItems: FnrsCnsltnSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) =>
|
||||
toSplmntDmndSaveItem(item),
|
||||
);
|
||||
|
||||
const nextSn =
|
||||
existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1;
|
||||
|
||||
const newItems: FnrsCnsltnSplmntDmndItem[] = payload.items.map((item, index) => ({
|
||||
splmntDmndSn: nextSn + index,
|
||||
splmntArtclComCd: item.splmntArtclComCd,
|
||||
splmntDmndCn: item.splmntDmndCn,
|
||||
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId,
|
||||
cmptnYn: "N",
|
||||
}));
|
||||
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
|
||||
splmntDmndList: [...existingItems, ...newItems],
|
||||
};
|
||||
}
|
||||
|
||||
function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
|
||||
return {
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
splmntArtclComCd: item.splmntArtclComCd,
|
||||
splmntDmndCn: item.splmntDmndCn,
|
||||
splmntDmndDt: item.splmntDmndDt,
|
||||
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId,
|
||||
splmntCn: item.splmntCn,
|
||||
splmntPicNm: item.splmntPicNm,
|
||||
splmntCmptnDt: item.splmntCmptnDt,
|
||||
splmntAtchFileGroupId: item.splmntAtchFileGroupId,
|
||||
cmptnYn: item.cmptnYn,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSupplementSubmitPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
|
||||
splmntDmndList: data.splmntDmndList,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewPayload(
|
||||
det: FnrsCnsltnMngResponseDto,
|
||||
cklstResultList: CklstResultItem[],
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
return {
|
||||
fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" },
|
||||
bzScaleList: [],
|
||||
bizCstList: [],
|
||||
cklstResultList,
|
||||
esntlAtchFileList: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
fnrsCnsltnMngService,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { TextInputDialog } from "@/widgets/pbadms-sprt";
|
||||
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import {
|
||||
@@ -402,12 +402,12 @@ function FloatingActionButtons({
|
||||
setActiveDialog("dmndSplmnt");
|
||||
}
|
||||
|
||||
async function handleDmndSplmntConfirm(splmntDmndCn: string) {
|
||||
async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) {
|
||||
setActiveDialog(null);
|
||||
const payload = buildSupplementReqPayload(data, splmntDmndCn);
|
||||
const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState);
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||
request: payload,
|
||||
request: requestPayload,
|
||||
successMessage: "보완요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
@@ -514,13 +514,11 @@ function FloatingActionButtons({
|
||||
onConfirm={handleElaprTmpltConfirm}
|
||||
/>
|
||||
|
||||
<TextInputDialog
|
||||
<SplmntDmndDialog
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
title="보완요청 내용 입력"
|
||||
placeholder="보완요청 내용을 입력하세요."
|
||||
isSubmitting={isProcessing}
|
||||
onConfirm={handleDmndSplmntConfirm}
|
||||
onConfirm={(payload) => void handleDmndSplmntConfirm(payload)}
|
||||
/>
|
||||
|
||||
<ElaprApprovalDialog
|
||||
@@ -632,11 +630,6 @@ async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
async function handleGrndsChck(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 현장점검 등록 API 호출
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> = {
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
@@ -654,7 +647,6 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
|
||||
dmndSplmnt: handleDmndSplmnt,
|
||||
atrzAprv: handleAtrzAprv,
|
||||
atrzRply: handleAtrzRply,
|
||||
grndsChck: handleGrndsChck,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -15,7 +15,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
|
||||
dmndSplmnt: "dmndSplmnt",
|
||||
atrzAprv: "atrzAprv",
|
||||
atrzRply: "atrzRply",
|
||||
grndsChck: "grndsChck",
|
||||
} as const;
|
||||
|
||||
export type InstlSncnViewButtonAction =
|
||||
@@ -44,10 +43,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
||||
LE: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply, INSTL_SNCN_VIEW_BUTTON_ACTION.rqstTechRvw],
|
||||
TP: [INSTL_SNCN_VIEW_BUTTON_ACTION.rvwRply],
|
||||
},
|
||||
ROLE_USER01: {
|
||||
TE: [INSTL_SNCN_VIEW_BUTTON_ACTION.grndsChck],
|
||||
SL: [INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const;
|
||||
|
||||
@@ -24,23 +24,23 @@ export const instlSncnRegistSchema = z.object({
|
||||
aplcntMblTelno: z.string().optional(),
|
||||
aplcntOgdpNm: z.string().optional(),
|
||||
aplcntEmlAddr: z.string().optional(),
|
||||
instlSncnDocNo: z.string().optional(),
|
||||
instlSncnDocNo: z.string().nullish(),
|
||||
admdstCd: z.string().optional(),
|
||||
envoSeComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().nullish(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
rtlztnFcltSeComCd: z.string().optional(),
|
||||
rtlztnUsgSeComCd: z.string().optional(),
|
||||
envoSeCdNm: z.string().optional(),
|
||||
sncnSeCdNm: z.string().optional(),
|
||||
taskSeCdNm: z.string().optional(),
|
||||
prgrsSttsCdNm: z.string().optional(),
|
||||
sido: z.string().optional(),
|
||||
gugun: z.string().optional(),
|
||||
regDt: z.string().optional(),
|
||||
rgtr: z.string().optional(),
|
||||
rgtrNm: z.string().optional(),
|
||||
mdfcnDt: z.string().optional(),
|
||||
envoSeCdNm: z.string().nullish(),
|
||||
sncnSeCdNm: z.string().nullish(),
|
||||
taskSeCdNm: z.string().nullish(),
|
||||
prgrsSttsCdNm: z.string().nullish(),
|
||||
sido: z.string().nullish(),
|
||||
gugun: z.string().nullish(),
|
||||
regDt: z.string().nullish(),
|
||||
rgtr: z.string().nullish(),
|
||||
rgtrNm: z.string().nullish(),
|
||||
mdfcnDt: z.string().nullish(),
|
||||
}),
|
||||
bzScaleList: z.array(
|
||||
z.object({
|
||||
@@ -66,7 +66,7 @@ export const instlSncnRegistSchema = z.object({
|
||||
cklstRvwSn: z.number(),
|
||||
rvwStpComCd: z.string(),
|
||||
selected: z.enum(["Y", "N"]).nullable(),
|
||||
picRvwCn: z.string().nullable().optional(),
|
||||
picRvwCn: z.string().nullish(),
|
||||
}),
|
||||
),
|
||||
esntlAtchFileList: z.array(
|
||||
|
||||
@@ -1,104 +1,131 @@
|
||||
import type {
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnBizCstSaveRequestDto,
|
||||
InstlSncnBzScaleSaveDto,
|
||||
InstlSncnCklstResultSaveDto,
|
||||
InstlSncnEsntlAtchFileSaveDto,
|
||||
InstlSncnMngDetResponse,
|
||||
InstlSncnMngResponseDto,
|
||||
ReviewStepCode,
|
||||
} from "@/entities/instl-sncn";
|
||||
|
||||
import type { ChecklistFormState } from "../model/view-types";
|
||||
|
||||
import { toCklstSaveDto } from "./checklist-utils";
|
||||
import { type ReviewOpinionFormData, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||
|
||||
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): InstlSncnBzScaleSaveDto[] {
|
||||
return bizSclList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
cnstpSeCd: node.cnstpSeCd,
|
||||
bscPl: node.bscPlanQty,
|
||||
budgetReflct: node.bgtRfltQty,
|
||||
thtiDsgn: node.tstmDesignQty,
|
||||
diff: node.tstmDesignQty - node.bgtRfltQty,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSaveRequestDto[] {
|
||||
return bizCstList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
acntgSeCd: node.acntgSeCd,
|
||||
bscPlanAmt: node.bscPlanAmt,
|
||||
bgtRfltAmt: node.bgtRfltAmt,
|
||||
tstmDesignAmt: node.tstmDesignAmt,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): InstlSncnEsntlAtchFileSaveDto[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data;
|
||||
|
||||
return {
|
||||
instlSncnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewReplyPayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
reviewOpinionFormData?: ReviewOpinionFormData | null,
|
||||
opnrNm?: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewPayload(
|
||||
det: InstlSncnMngResponseDto,
|
||||
cklstResultList: InstlSncnCklstResultSaveDto[],
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
return {
|
||||
instlSncnMng: { ...det, prgrsSttsComCd: "ER" },
|
||||
bzScaleList: [],
|
||||
bizCstList: [],
|
||||
cklstResultList,
|
||||
esntlAtchFileList: [],
|
||||
};
|
||||
}
|
||||
import type {
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnBizCstSaveRequestDto,
|
||||
InstlSncnBzScaleSaveDto,
|
||||
InstlSncnEsntlAtchFileSaveDto,
|
||||
InstlSncnMngDetResponse,
|
||||
InstlSncnSplmntDmndSaveDto,
|
||||
ReviewStepCode,
|
||||
} from "@/entities/instl-sncn";
|
||||
import type { SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { ChecklistFormState } from "../model/view-types";
|
||||
|
||||
import { toCklstSaveDto } from "./checklist-utils";
|
||||
import { type ReviewOpinionFormData, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||
|
||||
export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): InstlSncnBzScaleSaveDto[] {
|
||||
return bizSclList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
cnstpSeCd: node.cnstpSeCd,
|
||||
bscPl: node.bscPlanQty,
|
||||
budgetReflct: node.bgtRfltQty,
|
||||
thtiDsgn: node.tstmDesignQty,
|
||||
diff: node.tstmDesignQty - node.bgtRfltQty,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSaveRequestDto[] {
|
||||
return bizCstList
|
||||
.filter((node) => node.useYn === "Y")
|
||||
.map((node) => ({
|
||||
acntgSeCd: node.acntgSeCd,
|
||||
bscPlanAmt: node.bscPlanAmt,
|
||||
bgtRfltAmt: node.bgtRfltAmt,
|
||||
tstmDesignAmt: node.tstmDesignAmt,
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): InstlSncnEsntlAtchFileSaveDto[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data;
|
||||
|
||||
return {
|
||||
instlSncnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTechReviewReplyPayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
reviewStep: ReviewStepCode,
|
||||
prgrsSttsComCd: string,
|
||||
techReViewSaveCd: string,
|
||||
reviewOpinionFormData?: ReviewOpinionFormData | null,
|
||||
opnrNm?: string,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
|
||||
techReViewSaveCd,
|
||||
rvwStpCd: reviewStep,
|
||||
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSupplementReqPayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
payload: SplmntDmndDialogConfirmPayload,
|
||||
checklistFormState: ChecklistFormState,
|
||||
): InstlSncnApplySaveRequestDto {
|
||||
const existingItems: InstlSncnSplmntDmndSaveDto[] = (data.splmntDmndList ?? []).map((item) =>
|
||||
toSplmntDmndSaveItem(item),
|
||||
);
|
||||
|
||||
const nextSn = existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1;
|
||||
|
||||
const newItems: InstlSncnSplmntDmndSaveDto[] = payload.items.map((item, index) => ({
|
||||
splmntDmndSn: nextSn + index,
|
||||
splmntArtclComCd: item.splmntArtclComCd,
|
||||
splmntDmndCn: item.splmntDmndCn,
|
||||
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId,
|
||||
cmptnYn: "N",
|
||||
}));
|
||||
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
|
||||
splmntDmndList: [...existingItems, ...newItems],
|
||||
};
|
||||
}
|
||||
|
||||
function toSplmntDmndSaveItem(item: InstlSncnSplmntDmndSaveDto): InstlSncnSplmntDmndSaveDto {
|
||||
return {
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
splmntArtclComCd: item.splmntArtclComCd,
|
||||
splmntDmndCn: item.splmntDmndCn,
|
||||
splmntDmndDt: item.splmntDmndDt,
|
||||
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId,
|
||||
splmntCn: item.splmntCn,
|
||||
splmntPicNm: item.splmntPicNm,
|
||||
splmntCmptnDt: item.splmntCmptnDt,
|
||||
splmntAtchFileGroupId: item.splmntAtchFileGroupId,
|
||||
cmptnYn: item.cmptnYn,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
instlSncnService,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { TextInputDialog } from "@/widgets/pbadms-sprt";
|
||||
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import {
|
||||
@@ -37,7 +37,11 @@ import {
|
||||
needsRoleUser01AtrzResolve,
|
||||
} from "../../lib/button-config";
|
||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import { buildSavePayload, buildTechReviewReplyPayload } from "../../lib/save-payload-utils";
|
||||
import {
|
||||
buildSavePayload,
|
||||
buildSupplementReqPayload,
|
||||
buildTechReviewReplyPayload,
|
||||
} from "../../lib/save-payload-utils";
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
@@ -401,11 +405,12 @@ function FloatingActionButtons({
|
||||
setActiveDialog("dmndSplmnt");
|
||||
}
|
||||
|
||||
async function handleDmndSplmntConfirm(splmntCn: string) {
|
||||
async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) {
|
||||
setActiveDialog(null);
|
||||
const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState);
|
||||
await executeAction({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||
request: splmntCn,
|
||||
request: requestPayload,
|
||||
successMessage: "보완요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
@@ -517,13 +522,11 @@ function FloatingActionButtons({
|
||||
onConfirm={handleElaprTmpltConfirm}
|
||||
/>
|
||||
|
||||
<TextInputDialog
|
||||
<SplmntDmndDialog
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
title="보완요청 내용 입력"
|
||||
placeholder="보완요청 내용을 입력하세요."
|
||||
isSubmitting={isProcessing}
|
||||
onConfirm={(value) => void handleDmndSplmntConfirm(value)}
|
||||
onConfirm={handleDmndSplmntConfirm}
|
||||
/>
|
||||
|
||||
<ElaprApprovalDialog
|
||||
@@ -601,9 +604,17 @@ async function handleDsgntRvwr(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDmndSplmnt(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 보완요청 API 호출
|
||||
context.onSuccess?.();
|
||||
async function handleDmndSplmnt(
|
||||
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const docNo = context.data.detDto.instlSncnDocNo;
|
||||
if (!docNo) throw new Error("문서번호가 없습니다.");
|
||||
await instlSncnService.submitReqSupplement(docNo, context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAtrzAprv(context: ActionHandlerContext): Promise<void> {
|
||||
@@ -616,11 +627,6 @@ async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
async function handleGrndsChck(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 현장점검 등록 API 호출
|
||||
context.onSuccess?.();
|
||||
}
|
||||
|
||||
const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
@@ -638,7 +644,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
||||
dmndSplmnt: handleDmndSplmnt,
|
||||
atrzAprv: handleAtrzAprv,
|
||||
atrzRply: handleAtrzRply,
|
||||
grndsChck: handleGrndsChck,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -48,6 +48,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const isEditMode = mode === "edit";
|
||||
const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL";
|
||||
|
||||
const form = useForm<InstlSncnRegistFormData>({
|
||||
resolver: zodResolver(instlSncnRegistSchema),
|
||||
@@ -186,23 +187,36 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
{!isSplmntPrgrsStts ? (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
|
||||
삭제
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
type="button"
|
||||
onClick={handleDelClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,5 +30,9 @@ export { ContactInfoFormSection } from "./ui/contact-info-form-section";
|
||||
export { FnrsSharFormSection } from "./ui/fnrs-shar-form-section";
|
||||
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
|
||||
export { SectionCollapsible } from "./ui/section-collapsible";
|
||||
export {
|
||||
SplmntDmndDialog,
|
||||
type SplmntDmndDialogConfirmItem,
|
||||
type SplmntDmndDialogConfirmPayload,
|
||||
} from "./ui/splmnt-dmnd-dialog";
|
||||
export { TaskSePickerDialog } from "./ui/task-se-picker-dialog";
|
||||
export { TextInputDialog } from "./ui/text-input-dialog";
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useId, useState } from "react";
|
||||
|
||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
const SPLMNT_ARTCL_UP_CD = "SPLMNTARTCL";
|
||||
const SPLMNT_DMND_FILE_KEY_PREFIX = "splmnt-dmnd::";
|
||||
|
||||
interface UploadedSplmntFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflId?: string;
|
||||
atflCd?: string;
|
||||
}
|
||||
|
||||
interface SplmntDmndRow {
|
||||
rowKey: string;
|
||||
splmntArtclComCd: string;
|
||||
splmntDmndCn: string;
|
||||
splmntDmndAtchFileGroupId: string;
|
||||
uploadedFile: UploadedSplmntFile | null;
|
||||
}
|
||||
|
||||
interface SplmntDmndDialogConfirmItem {
|
||||
splmntArtclComCd: string;
|
||||
splmntDmndCn: string;
|
||||
splmntDmndAtchFileGroupId?: string;
|
||||
}
|
||||
|
||||
interface SplmntDmndDialogConfirmPayload {
|
||||
items: SplmntDmndDialogConfirmItem[];
|
||||
}
|
||||
|
||||
interface SplmntDmndDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
isSubmitting?: boolean;
|
||||
onConfirm: (payload: SplmntDmndDialogConfirmPayload) => void;
|
||||
}
|
||||
|
||||
function createRowKey() {
|
||||
return `splmnt-row-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
function createEmptyRow(): SplmntDmndRow {
|
||||
return {
|
||||
rowKey: createRowKey(),
|
||||
splmntArtclComCd: "",
|
||||
splmntDmndCn: "",
|
||||
splmntDmndAtchFileGroupId: "",
|
||||
uploadedFile: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createUploadKey(rowKey: string, file: File) {
|
||||
return `${SPLMNT_DMND_FILE_KEY_PREFIX}${rowKey}::${getDefaultFileKey(file)}`;
|
||||
}
|
||||
|
||||
function getRowKeyFromFileKey(fileKey: string): string | null {
|
||||
if (!fileKey.startsWith(SPLMNT_DMND_FILE_KEY_PREFIX)) return null;
|
||||
const rest = fileKey.slice(SPLMNT_DMND_FILE_KEY_PREFIX.length);
|
||||
const separatorIndex = rest.indexOf("::");
|
||||
if (separatorIndex < 0) return null;
|
||||
return rest.slice(0, separatorIndex);
|
||||
}
|
||||
|
||||
function isRowsValid(rows: SplmntDmndRow[]) {
|
||||
return rows.every((row) => row.splmntArtclComCd.trim() && row.splmntDmndCn.trim());
|
||||
}
|
||||
|
||||
function SplmntDmndDialog({ open, onOpenChange, isSubmitting, onConfirm }: SplmntDmndDialogProps) {
|
||||
const formId = useId();
|
||||
const [rows, setRows] = useState<SplmntDmndRow[]>([createEmptyRow()]);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
const rowKey = getRowKeyFromFileKey(fileKey);
|
||||
if (!rowKey) return;
|
||||
|
||||
setRows((prev) =>
|
||||
prev.map((row) => {
|
||||
if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row;
|
||||
return {
|
||||
...row,
|
||||
splmntDmndAtchFileGroupId: atflCd,
|
||||
uploadedFile: { ...row.uploadedFile, atflCd, atflId },
|
||||
};
|
||||
}),
|
||||
);
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
const rowKey = getRowKeyFromFileKey(fileKey);
|
||||
if (!rowKey) return;
|
||||
|
||||
setRows((prev) =>
|
||||
prev.map((row) => {
|
||||
if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row;
|
||||
return { ...row, uploadedFile: null };
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setRows([createEmptyRow()]);
|
||||
}, [open]);
|
||||
|
||||
const isAnyUploading = rows.some(
|
||||
(row) => row.uploadedFile != null && (isUploading[row.uploadedFile.fileKey] ?? false),
|
||||
);
|
||||
const canSubmit = isRowsValid(rows) && !isAnyUploading && !isSubmitting;
|
||||
|
||||
function updateRow(rowKey: string, patch: Partial<SplmntDmndRow>) {
|
||||
setRows((prev) => prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch } : row)));
|
||||
}
|
||||
|
||||
function handleAddRow() {
|
||||
setRows((prev) => [...prev, createEmptyRow()]);
|
||||
}
|
||||
|
||||
function handleRemoveRow(rowKey: string) {
|
||||
setRows((prev) => (prev.length <= 1 ? prev : prev.filter((row) => row.rowKey !== rowKey)));
|
||||
}
|
||||
|
||||
async function handleFileUpload(rowKey: string, files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = createUploadKey(rowKey, file);
|
||||
const targetRow = rows.find((row) => row.rowKey === rowKey);
|
||||
const existingAtflCd = targetRow?.splmntDmndAtchFileGroupId;
|
||||
|
||||
updateRow(rowKey, {
|
||||
uploadedFile: { fileName: file.name, fileKey },
|
||||
});
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove(rowKey: string) {
|
||||
const targetRow = rows.find((row) => row.rowKey === rowKey);
|
||||
if (!targetRow?.uploadedFile) return;
|
||||
|
||||
const { fileKey, atflId } = targetRow.uploadedFile;
|
||||
uploads[fileKey]?.abort();
|
||||
|
||||
if (atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(atflId);
|
||||
} catch (error) {
|
||||
console.error("보완요청 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
updateRow(rowKey, {
|
||||
uploadedFile: null,
|
||||
splmntDmndAtchFileGroupId: "",
|
||||
});
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (!canSubmit) return;
|
||||
|
||||
onConfirm({
|
||||
items: rows.map((row) => ({
|
||||
splmntArtclComCd: row.splmntArtclComCd.trim(),
|
||||
splmntDmndCn: row.splmntDmndCn.trim(),
|
||||
splmntDmndAtchFileGroupId: row.splmntDmndAtchFileGroupId || undefined,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[100rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>보완요청</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h4 className="text-xl font-semibold">보완 내용</h4>
|
||||
<Button type="button" size="sm" color="green" onClick={handleAddRow}>
|
||||
항목 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="50%" />
|
||||
<col width="20%" />
|
||||
{rows.length > 1 ? <col width="10%" /> : null}
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>보완 항목</TableHead>
|
||||
<TableHead>보완 의견</TableHead>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
{rows.length > 1 ? <TableHead>관리</TableHead> : null}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => {
|
||||
const uploading = row.uploadedFile
|
||||
? (isUploading[row.uploadedFile.fileKey] ?? false)
|
||||
: false;
|
||||
const fileInputId = `${formId}-${row.rowKey}-file`;
|
||||
|
||||
return (
|
||||
<TableRow key={row.rowKey}>
|
||||
<TableCell align="center" className="align-top">
|
||||
<CodeSelect
|
||||
upCd={SPLMNT_ARTCL_UP_CD}
|
||||
value={row.splmntArtclComCd || undefined}
|
||||
onValueChange={(value) =>
|
||||
updateRow(row.rowKey, { splmntArtclComCd: value })
|
||||
}
|
||||
placeholder="보완항목 선택"
|
||||
className="w-full min-w-[12rem]"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<Textarea
|
||||
value={row.splmntDmndCn}
|
||||
onChange={(event) =>
|
||||
updateRow(row.rowKey, { splmntDmndCn: event.target.value })
|
||||
}
|
||||
placeholder="보완 의견을 입력하세요."
|
||||
rows={4}
|
||||
className="min-h-[8rem] !text-xl"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<div className="flex items-center gap-2">
|
||||
{row.uploadedFile ? (
|
||||
<div className="flex items-stretch gap-2">
|
||||
<span className="text-xl">{row.uploadedFile.fileName}</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-8 shrink-0 !bg-destructive"
|
||||
disabled={uploading}
|
||||
onClick={() => void handleFileRemove(row.rowKey)}
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="button" size="sm" disabled={uploading} asChild>
|
||||
<label htmlFor={fileInputId} className="cursor-pointer">
|
||||
파일추가
|
||||
<input
|
||||
id={fileInputId}
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(row.rowKey, event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
{rows.length > 1 ? (
|
||||
<TableCell align="center" className="align-top">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveRow(row.rowKey)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</TableCell>
|
||||
) : null}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="button" onClick={handleConfirm} disabled={!canSubmit}>
|
||||
보완요청
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { SplmntDmndDialog };
|
||||
export type { SplmntDmndDialogConfirmItem, SplmntDmndDialogConfirmPayload };
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
interface TextInputDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
isSubmitting?: boolean;
|
||||
onConfirm: (text: string) => void;
|
||||
}
|
||||
|
||||
function TextInputDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
isSubmitting,
|
||||
onConfirm,
|
||||
}: TextInputDialogProps) {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setValue(defaultValue ?? "");
|
||||
}, [defaultValue, open]);
|
||||
|
||||
function handleConfirm() {
|
||||
onConfirm(value.trim());
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[44rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
rows={6}
|
||||
className="!text-xl"
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleConfirm}
|
||||
disabled={isSubmitting || value.trim().length === 0}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { TextInputDialog };
|
||||
Reference in New Issue
Block a user