[as] feat: 재원협의 보완요청 로직 작업
This commit is contained in:
@@ -2,7 +2,6 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
|
||||
strg: "strg",
|
||||
aply: "aply",
|
||||
del: "del",
|
||||
splmntCmptn: "splmntCmptn",
|
||||
fnrsCnsltnAncmnt: "fnrsCnsltnAncmnt",
|
||||
envoSbmsn: "envoSbmsn",
|
||||
rjct: "rjct",
|
||||
@@ -32,7 +31,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.del,
|
||||
],
|
||||
SL: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt],
|
||||
},
|
||||
ROLE_USER04: {
|
||||
|
||||
@@ -83,6 +83,24 @@ export const fnrsCnsltnRegistSchema = z.object({
|
||||
atchFileGroupId: z.string(),
|
||||
}),
|
||||
),
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().optional(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
splmntDmndDt: z.string().optional(),
|
||||
splmntDmndAtchFileGroupId: z.string().optional(),
|
||||
splmntDmndAtchFileNm: z.string().optional(),
|
||||
splmntCn: z.string().optional(),
|
||||
splmntPicNm: z.string().optional(),
|
||||
splmntCmptnDt: z.string().optional(),
|
||||
splmntAtchFileGroupId: z.string().optional(),
|
||||
splmntAtchFileNm: z.string().optional(),
|
||||
splmntAtchFileAtflId: z.string().optional(),
|
||||
cmptnYn: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
privacyConsent: z.enum(["Y", "N"], {
|
||||
required_error: "개인정보 수집/이용 동의는 필수입니다",
|
||||
}),
|
||||
|
||||
@@ -34,6 +34,7 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
|
||||
const bizCstList = data.bizCstList ?? [];
|
||||
const cklstResultList = data.cklstResultList;
|
||||
const esntlFileList = data.esntlFileList;
|
||||
const splmntDmndList = data.splmntDmndList;
|
||||
|
||||
return {
|
||||
fnrsCnsltnMng: {
|
||||
@@ -94,6 +95,23 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
|
||||
picRvwCn: item.picRvwCn,
|
||||
})) ?? [],
|
||||
esntlAtchFileList: mapEsntlAtchFileList(esntlFileList),
|
||||
splmntDmndList:
|
||||
splmntDmndList?.map((item) => ({
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
splmntArtclComCd: item.splmntArtclComCd,
|
||||
splmntArtclCdNm: item.splmntArtclCdNm,
|
||||
splmntDmndCn: item.splmntDmndCn,
|
||||
splmntDmndDt: item.splmntDmndDt,
|
||||
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId ?? "",
|
||||
splmntDmndAtchFileNm: item.splmntDmndAtchFileInfo?.orgnlFileNm ?? "",
|
||||
splmntCn: item.splmntCn ?? "",
|
||||
splmntPicNm: item.splmntPicNm ?? "",
|
||||
splmntCmptnDt: item.splmntCmptnDt ?? "",
|
||||
splmntAtchFileGroupId: item.splmntAtchFileGroupId ?? "",
|
||||
splmntAtchFileNm: item.splmntAtchFileInfo?.orgnlFileNm ?? "",
|
||||
splmntAtchFileAtflId: item.splmntAtchFileInfo?.atflId ?? "",
|
||||
cmptnYn: item.cmptnYn,
|
||||
})) ?? [],
|
||||
privacyConsent: "Y",
|
||||
};
|
||||
}
|
||||
@@ -130,6 +148,7 @@ export function getCreateDefaultValues(
|
||||
fnrsSharList: [],
|
||||
cklstResultList: [],
|
||||
esntlAtchFileList: [],
|
||||
splmntDmndList: [],
|
||||
privacyConsent: "N",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,8 +112,7 @@ export function buildSupplementReqPayload(
|
||||
toSplmntDmndSaveItem(item),
|
||||
);
|
||||
|
||||
const nextSn =
|
||||
existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1;
|
||||
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,
|
||||
@@ -129,7 +128,7 @@ export function buildSupplementReqPayload(
|
||||
};
|
||||
}
|
||||
|
||||
function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
|
||||
export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
|
||||
return {
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
splmntArtclComCd: item.splmntArtclComCd,
|
||||
|
||||
@@ -40,8 +40,8 @@ import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import {
|
||||
buildSavePayload,
|
||||
buildSupplementReqPayload,
|
||||
buildSupplementSubmitPayload,
|
||||
buildTechReviewReplyPayload,
|
||||
toSplmntDmndSaveItem,
|
||||
} from "../../lib/save-payload-utils";
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
@@ -219,15 +219,6 @@ function FloatingActionButtons({
|
||||
router.push("/fnrs-cnsltn");
|
||||
}
|
||||
|
||||
async function handleSplmntCmptnClick() {
|
||||
const payload = buildSupplementSubmitPayload(data, checklistFormState);
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn,
|
||||
request: payload,
|
||||
successMessage: "보완완료 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFnrsCnsltnAncmntClick() {
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo,
|
||||
@@ -380,15 +371,18 @@ function FloatingActionButtons({
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildTechReviewReplyPayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"KECO",
|
||||
"TE",
|
||||
"SV",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
);
|
||||
const payload = {
|
||||
...buildTechReviewReplyPayload(
|
||||
data,
|
||||
checklistFormState,
|
||||
"KECO",
|
||||
"TE",
|
||||
"SV",
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
),
|
||||
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
|
||||
};
|
||||
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
|
||||
@@ -439,11 +433,6 @@ function FloatingActionButtons({
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn) && (
|
||||
<Button onClick={handleSplmntCmptnClick} disabled={isProcessing}>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && (
|
||||
<Button onClick={handleFnrsCnsltnAncmntClick} disabled={isProcessing}>
|
||||
재원협의 고시
|
||||
@@ -518,7 +507,7 @@ function FloatingActionButtons({
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
onOpenChange={() => setActiveDialog(null)}
|
||||
isSubmitting={isProcessing}
|
||||
onConfirm={(payload) => void handleDmndSplmntConfirm(payload)}
|
||||
onConfirm={handleDmndSplmntConfirm}
|
||||
/>
|
||||
|
||||
<ElaprApprovalDialog
|
||||
@@ -558,19 +547,6 @@ async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSplmntCmptn(
|
||||
context: ActionHandlerContext<FnrsCnsltnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const docNo = context.data.detDto?.fnrsCnsltnDocNo;
|
||||
if (!docNo) throw new Error("문서번호가 없습니다.");
|
||||
await fnrsCnsltnMngService.submitSupplement(docNo, context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePrgrsSttsComCd(
|
||||
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
|
||||
): Promise<void> {
|
||||
@@ -634,7 +610,6 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
del: handleDel,
|
||||
splmntCmptn: handleSplmntCmptn,
|
||||
fnrsCnsltnAncmnt: handleChangePrgrsSttsComCd,
|
||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
||||
rjct: handleChangePrgrsSttsComCd,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import {
|
||||
type FnrsCnsltnApplySaveRequestDto,
|
||||
fnrsCnsltnMngBaseKey,
|
||||
type FnrsCnsltnMngDetResponseDto,
|
||||
fnrsCnsltnMngService,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
isFormChecklistComplete,
|
||||
PrivacyConsentSection,
|
||||
recalculateFnrsSharTotals,
|
||||
SplmntDmndFormSection,
|
||||
toFnrsSharSaveList,
|
||||
validateBizCstSharBalance,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
@@ -56,6 +58,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const isEditMode = mode === "edit";
|
||||
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
|
||||
|
||||
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
|
||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||
@@ -73,10 +76,10 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||
});
|
||||
|
||||
async function submitForm(
|
||||
function buildFormPayload(
|
||||
data: FnrsCnsltnRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "ST",
|
||||
): Promise<string | undefined> {
|
||||
prgrsSttsComCd: "SV" | "ST" | "TE",
|
||||
): FnrsCnsltnApplySaveRequestDto | undefined {
|
||||
const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, sharItems);
|
||||
const validationMessage = validateBizCstSharBalance(
|
||||
data.bizCstList,
|
||||
@@ -86,12 +89,12 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
);
|
||||
if (validationMessage) {
|
||||
alert(validationMessage);
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { taskSeComCd: formTaskSeComCd, ...fnrsCnsltnMngRest } = data.fnrsCnsltnMng;
|
||||
|
||||
const payload = {
|
||||
return {
|
||||
fnrsCnsltnMng: {
|
||||
...fnrsCnsltnMngRest,
|
||||
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
|
||||
@@ -106,11 +109,71 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
selected: item.selected ?? undefined,
|
||||
})),
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
splmntDmndList: data.splmntDmndList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
|
||||
...item,
|
||||
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitForm(
|
||||
data: FnrsCnsltnRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "ST",
|
||||
): Promise<string | undefined> {
|
||||
const payload = buildFormPayload(data, prgrsSttsComCd);
|
||||
if (!payload) return undefined;
|
||||
return fnrsCnsltnMngService.saveApply(payload);
|
||||
}
|
||||
|
||||
async function submitSupplementForm(data: FnrsCnsltnRegistFormData): Promise<string | undefined> {
|
||||
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
||||
if (!docNo) return undefined;
|
||||
|
||||
const payload = buildFormPayload(data, "TE");
|
||||
if (!payload) return undefined;
|
||||
|
||||
return fnrsCnsltnMngService.submitSupplement(docNo, payload);
|
||||
}
|
||||
|
||||
async function onSplmntCmptn(data: FnrsCnsltnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
||||
(item) => !item.splmntCn || !item.splmntCn.trim(),
|
||||
);
|
||||
if (hasEmptySplmntCn) {
|
||||
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
||||
if (!docNo) {
|
||||
alert("문서번호가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const result = await submitSupplementForm(data);
|
||||
if (!result) return;
|
||||
|
||||
alert("보완완료 처리되었습니다.");
|
||||
router.push(`/fnrs-cnsltn/${docNo}`);
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findById", docNo],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
alert("보완완료 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave(data: FnrsCnsltnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
@@ -210,6 +273,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||
/>
|
||||
<SplmntDmndFormSection readOnly={!isSplmntPrgrsStts} />
|
||||
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
@@ -221,23 +285,40 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
>
|
||||
목록
|
||||
</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="button"
|
||||
onClick={form.handleSubmit(onSplmntCmptn)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRef } from "react";
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
@@ -19,7 +19,6 @@ import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
import { ChecklistSection } from "./sections/checklist-section";
|
||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||
import { FnrsSharSection } from "./sections/fnrs-shar-section";
|
||||
import { SupplementDemandSection } from "./sections/supplement-demand-section";
|
||||
import {
|
||||
TechReviewOpinionSection,
|
||||
type TechReviewOpinionSectionHandle,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { FnrsCnsltnSplmntDmndResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Badge,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
interface SupplementDemandSectionProps {
|
||||
splmntDmndList: FnrsCnsltnSplmntDmndResponseDto[];
|
||||
}
|
||||
|
||||
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
|
||||
if (splmntDmndList.length === 0) return null;
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="보완요청">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[8rem]">항목</TableHead>
|
||||
<TableHead>보완요청 내용</TableHead>
|
||||
<TableHead>보완 내용</TableHead>
|
||||
<TableHead className="w-[8rem]">완료여부</TableHead>
|
||||
<TableHead className="w-[12rem]">요청일시</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{splmntDmndList.map((item) => (
|
||||
<TableRow key={item.splmntDmndSn}>
|
||||
<TableCell>{item.splmntArtclCdNm || "-"}</TableCell>
|
||||
<TableCell className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</TableCell>
|
||||
<TableCell className="whitespace-pre-wrap">{item.splmntCn || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge color={item.cmptnYn === "Y" ? "green" : "gray"}>
|
||||
{item.cmptnYn === "Y" ? "완료" : "미완료"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.splmntDmndDt ? dayjs(item.splmntDmndDt).format("YYYY-MM-DD HH:mm") : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { SupplementDemandSection };
|
||||
@@ -2,7 +2,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
|
||||
strg: "strg",
|
||||
aply: "aply",
|
||||
del: "del",
|
||||
splmntCmptn: "splmntCmptn",
|
||||
instlSncnAncmnt: "instlSncnAncmnt",
|
||||
envoSbmsn: "envoSbmsn",
|
||||
rjct: "rjct",
|
||||
@@ -32,7 +31,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
|
||||
INSTL_SNCN_VIEW_BUTTON_ACTION.del,
|
||||
],
|
||||
SL: [INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn],
|
||||
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
|
||||
},
|
||||
ROLE_USER04: {
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
toSplmntDmndSaveItem,
|
||||
} from "../../lib/save-payload-utils";
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { SupplementDemandSectionHandle } from "../sections/supplement-demand-section";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
@@ -55,7 +54,6 @@ interface FloatingActionButtonsProps {
|
||||
dtlInstCd: string | undefined;
|
||||
checklistFormState: ChecklistFormState;
|
||||
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
|
||||
supplementDemandRef?: React.RefObject<SupplementDemandSectionHandle | null>;
|
||||
opnrNm?: string;
|
||||
}
|
||||
|
||||
@@ -69,7 +67,6 @@ function FloatingActionButtons({
|
||||
dtlInstCd,
|
||||
checklistFormState,
|
||||
techReviewOpinionRef,
|
||||
supplementDemandRef,
|
||||
opnrNm,
|
||||
}: FloatingActionButtonsProps) {
|
||||
const router = useRouter();
|
||||
@@ -397,9 +394,7 @@ function FloatingActionButtons({
|
||||
getReviewOpinionFormData(),
|
||||
opnrNm,
|
||||
),
|
||||
splmntDmndList:
|
||||
supplementDemandRef?.current?.getSplmntDmndList() ??
|
||||
(data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
|
||||
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
|
||||
};
|
||||
|
||||
await executeAction({
|
||||
@@ -451,16 +446,6 @@ function FloatingActionButtons({
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn) && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn })
|
||||
}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt) && (
|
||||
<Button onClick={handleInstlSncnAncmntClick} disabled={isProcessing}>
|
||||
설치인가 고시
|
||||
@@ -575,19 +560,6 @@ async function handleDel(context: ActionHandlerContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSplmntCmptn(
|
||||
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const docNo = context.data.detDto?.instlSncnDocNo;
|
||||
if (!docNo) throw new Error("문서번호가 없습니다.");
|
||||
await instlSncnService.submitSupplement(docNo, context.request);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePrgrsSttsComCd(
|
||||
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
|
||||
): Promise<void> {
|
||||
@@ -648,7 +620,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
del: handleDel,
|
||||
splmntCmptn: handleSplmntCmptn,
|
||||
instlSncnAncmnt: handleChangePrgrsSttsComCd,
|
||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
||||
rjct: handleChangePrgrsSttsComCd,
|
||||
|
||||
@@ -83,7 +83,10 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
splmntDmndList: data.splmntDmndList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => item,
|
||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
|
||||
...item,
|
||||
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -104,6 +107,14 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
async function onSplmntCmptn(data: InstlSncnRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
||||
(item) => !item.splmntCn || !item.splmntCn.trim(),
|
||||
);
|
||||
if (hasEmptySplmntCn) {
|
||||
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const docNo = initialData?.detDto.instlSncnDocNo;
|
||||
if (!docNo) {
|
||||
alert("문서번호가 없습니다.");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRef } from "react";
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
@@ -18,10 +18,6 @@ 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 {
|
||||
SupplementDemandSection,
|
||||
type SupplementDemandSectionHandle,
|
||||
} from "./sections/supplement-demand-section";
|
||||
import {
|
||||
TechReviewOpinionSection,
|
||||
type TechReviewOpinionSectionHandle,
|
||||
@@ -34,7 +30,6 @@ interface InstlViewPageProps {
|
||||
function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
|
||||
const supplementDemandRef = useRef<SupplementDemandSectionHandle>(null);
|
||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||
const { formState: checklistFormState, setFormState: setChecklistFormState } =
|
||||
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
|
||||
@@ -100,11 +95,7 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
rvwList={rvwList}
|
||||
editableReviewStep={editableReviewStep}
|
||||
/>
|
||||
<SupplementDemandSection
|
||||
ref={supplementDemandRef}
|
||||
splmntDmndList={splmntDmndList}
|
||||
canEditCmptnYn={userInfo?.dtlInstCd === "ROLE_USER01"}
|
||||
/>
|
||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||
<FloatingActionButtons
|
||||
data={data}
|
||||
docSeComCd="INSTL"
|
||||
@@ -113,7 +104,6 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
dtlInstCd={userInfo?.dtlInstCd}
|
||||
checklistFormState={checklistFormState}
|
||||
techReviewOpinionRef={techReviewOpinionRef}
|
||||
supplementDemandRef={supplementDemandRef}
|
||||
opnrNm={userInfo?.userName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -36,4 +36,5 @@ export {
|
||||
type SplmntDmndDialogConfirmPayload,
|
||||
} from "./ui/splmnt-dmnd-dialog";
|
||||
export { type SplmntDmndFormItem, SplmntDmndFormSection } from "./ui/splmnt-dmnd-form-section";
|
||||
export { SupplementDemandSection } from "./ui/supplement-demand-section";
|
||||
export { TaskSePickerDialog } from "./ui/task-se-picker-dialog";
|
||||
|
||||
+114
-181
@@ -1,181 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import type {
|
||||
InstlSncnSplmntDmndResponseDto,
|
||||
InstlSncnSplmntDmndSaveDto,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Badge,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { toSplmntDmndSaveItem } from "../../lib/save-payload-utils";
|
||||
|
||||
interface SupplementDemandSectionProps {
|
||||
splmntDmndList: InstlSncnSplmntDmndResponseDto[];
|
||||
canEditCmptnYn: boolean;
|
||||
}
|
||||
|
||||
export interface SupplementDemandSectionHandle {
|
||||
getSplmntDmndList: () => InstlSncnSplmntDmndSaveDto[];
|
||||
}
|
||||
|
||||
const SupplementDemandSection = forwardRef<
|
||||
SupplementDemandSectionHandle,
|
||||
SupplementDemandSectionProps
|
||||
>(function SupplementDemandSection({ splmntDmndList, canEditCmptnYn }, ref) {
|
||||
const [cmptnYnBySn, setCmptnYnBySn] = useState<Record<number, string>>({});
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getSplmntDmndList: () =>
|
||||
splmntDmndList.map((item) => {
|
||||
const saveItem = toSplmntDmndSaveItem(item);
|
||||
const sn = item.splmntDmndSn;
|
||||
if (sn == null || cmptnYnBySn[sn] == null) return saveItem;
|
||||
return { ...saveItem, cmptnYn: cmptnYnBySn[sn] };
|
||||
}),
|
||||
}),
|
||||
[splmntDmndList, cmptnYnBySn],
|
||||
);
|
||||
|
||||
function getCmptnYn(item: InstlSncnSplmntDmndResponseDto) {
|
||||
const sn = item.splmntDmndSn;
|
||||
if (sn != null && cmptnYnBySn[sn] != null) return cmptnYnBySn[sn];
|
||||
return item.cmptnYn ?? "N";
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="보완요청 및 완료 현황">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="8%" />
|
||||
<col width="30%" />
|
||||
<col width="8%" />
|
||||
<col width="30%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>보완항목</TableHead>
|
||||
<TableHead>보완요청일시</TableHead>
|
||||
<TableHead>보완요청내용</TableHead>
|
||||
<TableHead>보완완료일시</TableHead>
|
||||
<TableHead>보완완료내용</TableHead>
|
||||
<TableHead>완료여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{splmntDmndList.map((item) => {
|
||||
const sn = item.splmntDmndSn;
|
||||
const cmptnYn = getCmptnYn(item);
|
||||
const dmndAtchFileNm = item.splmntDmndAtchFileInfo?.orgnlFileNm;
|
||||
const cmptnAtchFileNm = item.splmntAtchFileInfo?.orgnlFileNm;
|
||||
|
||||
return (
|
||||
<TableRow key={sn ?? `${item.splmntArtclComCd}-${item.splmntDmndDt}`}>
|
||||
<TableCell align="center" className="align-top">
|
||||
{item.splmntArtclCdNm || "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="whitespace-nowrap align-top">
|
||||
{item.splmntDmndDt ? dayjs(item.splmntDmndDt).format("YYYY-MM-DD HH:mm") : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<div className="space-y-2">
|
||||
<div className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</div>
|
||||
{item.splmntDmndAtchFileGroupId ? (
|
||||
<a
|
||||
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(item.splmntDmndAtchFileGroupId!)}
|
||||
>
|
||||
<Paperclip className="my-1 size-5 shrink-0" />
|
||||
{dmndAtchFileNm || "다운로드"}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center" className="whitespace-nowrap align-top">
|
||||
{item.splmntCmptnDt
|
||||
? dayjs(item.splmntCmptnDt).format("YYYY-MM-DD HH:mm")
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<div className="space-y-2">
|
||||
<div className="whitespace-pre-wrap">{item.splmntCn || "-"}</div>
|
||||
{item.splmntAtchFileGroupId ? (
|
||||
<a
|
||||
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(item.splmntAtchFileGroupId!)}
|
||||
>
|
||||
<Paperclip className="my-1 size-5 shrink-0" />
|
||||
{cmptnAtchFileNm || "다운로드"}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center" className="align-top">
|
||||
{canEditCmptnYn && sn != null ? (
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
value={cmptnYn}
|
||||
onValueChange={(value) =>
|
||||
setCmptnYnBySn((prev) => ({ ...prev, [sn]: value }))
|
||||
}
|
||||
className="flex justify-center gap-4"
|
||||
>
|
||||
<FieldLabel
|
||||
htmlFor={`splmnt-cmptn-y-${sn}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="Y" id={`splmnt-cmptn-y-${sn}`} />
|
||||
<span className="font-normal">완료</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`splmnt-cmptn-n-${sn}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem value="N" id={`splmnt-cmptn-n-${sn}`} />
|
||||
<span className="font-normal">미완료</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
) : (
|
||||
<Badge size="lg" color={cmptnYn === "Y" ? "green" : "gray"}>
|
||||
{cmptnYn === "Y" ? "완료" : "미완료"}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
});
|
||||
|
||||
export { SupplementDemandSection };
|
||||
"use client";
|
||||
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { SplmntDmndResponseExtension, SplmntDmndSaveBase } from "@/shared/model";
|
||||
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import {
|
||||
Badge,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { SectionCollapsible } from "./section-collapsible";
|
||||
|
||||
type SplmntDmndItem = SplmntDmndSaveBase & SplmntDmndResponseExtension;
|
||||
|
||||
interface SupplementDemandSectionProps {
|
||||
splmntDmndList: SplmntDmndItem[];
|
||||
}
|
||||
|
||||
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="보완요청 및 완료 현황">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="8%" />
|
||||
<col width="30%" />
|
||||
<col width="8%" />
|
||||
<col width="30%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>보완항목</TableHead>
|
||||
<TableHead>보완요청일시</TableHead>
|
||||
<TableHead>보완요청내용</TableHead>
|
||||
<TableHead>보완완료일시</TableHead>
|
||||
<TableHead>보완완료내용</TableHead>
|
||||
<TableHead>완료여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{splmntDmndList.map((item) => {
|
||||
const sn = item.splmntDmndSn;
|
||||
const cmptnYn = item.cmptnYn ?? "N";
|
||||
const dmndAtchFileNm = item.splmntDmndAtchFileInfo?.orgnlFileNm;
|
||||
const cmptnAtchFileNm = item.splmntAtchFileInfo?.orgnlFileNm;
|
||||
|
||||
return (
|
||||
<TableRow key={sn ?? `${item.splmntArtclComCd}-${item.splmntDmndDt}`}>
|
||||
<TableCell align="center" className="align-top">
|
||||
{item.splmntArtclCdNm || "-"}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="whitespace-nowrap align-top">
|
||||
{item.splmntDmndDt ? dayjs(item.splmntDmndDt).format("YYYY-MM-DD HH:mm") : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<div className="space-y-2">
|
||||
<div className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</div>
|
||||
{item.splmntDmndAtchFileGroupId ? (
|
||||
<a
|
||||
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(item.splmntDmndAtchFileGroupId!)}
|
||||
>
|
||||
<Paperclip className="my-1 size-5 shrink-0" />
|
||||
{dmndAtchFileNm || "다운로드"}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center" className="whitespace-nowrap align-top">
|
||||
{item.splmntCmptnDt
|
||||
? dayjs(item.splmntCmptnDt).format("YYYY-MM-DD HH:mm")
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<div className="space-y-2">
|
||||
<div className="whitespace-pre-wrap">{item.splmntCn || "-"}</div>
|
||||
{item.splmntAtchFileGroupId ? (
|
||||
<a
|
||||
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(item.splmntAtchFileGroupId!)}
|
||||
>
|
||||
<Paperclip className="my-1 size-5 shrink-0" />
|
||||
{cmptnAtchFileNm || "다운로드"}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center" className="align-top">
|
||||
<Badge size="lg" color={cmptnYn === "Y" ? "green" : "gray"}>
|
||||
{cmptnYn === "Y" ? "완료" : "미완료"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { SupplementDemandSection };
|
||||
Reference in New Issue
Block a user