361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { FormProvider, useForm } from "react-hook-form";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
|
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
|
import { useAcntgSeMngTree } from "@/entities/acntg";
|
|
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
|
import {
|
|
fnrsCnsltnKeys,
|
|
type FnrsCnsltnMngDetResponseDto,
|
|
fnrsCnsltnMngService,
|
|
} from "@/entities/fnrs-cnsltn";
|
|
import { useFnrsSharSeMngTree } from "@/entities/fnrs-shar-se";
|
|
import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng";
|
|
import {
|
|
AttachmentsFormSection,
|
|
buildAcntgCostItems,
|
|
buildFnrsSharItems,
|
|
BusinessCostFormSection,
|
|
BusinessScaleFormSection,
|
|
ChecklistFormSection,
|
|
ContactInfoFormSection,
|
|
FnrsSharFormSection,
|
|
isFormChecklistComplete,
|
|
isFormEsntlAtchComplete,
|
|
PrivacyConsentSection,
|
|
ProgressStatusSection,
|
|
SplmntDmndFormSection,
|
|
} from "@/widgets/pbadms-sprt";
|
|
import { Button } from "@common/components";
|
|
|
|
import {
|
|
type FnrsCnsltnRegistFormData,
|
|
fnrsCnsltnRegistSchema,
|
|
} from "../lib/fnrs-cnsltn-regist-schema";
|
|
import {
|
|
buildFormPayload,
|
|
getCreateDefaultValues,
|
|
mapDetailToFormData,
|
|
} from "../lib/form-data-mapper";
|
|
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
|
|
|
import { PwrkCnsltnDocMngDialog } from "./components/pwrk-cnsltn-doc-mng-dialog";
|
|
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
|
|
interface FnrsFormPageProps {
|
|
mode: "create" | "edit";
|
|
initialData?: FnrsCnsltnMngDetResponseDto;
|
|
createDefaults?: {
|
|
taskSeComCd?: string[];
|
|
admdstCd?: string;
|
|
sido?: string;
|
|
gugun?: string;
|
|
};
|
|
}
|
|
|
|
function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) {
|
|
const queryClient = useQueryClient();
|
|
const router = useRouter();
|
|
const { confirm } = useConfirm();
|
|
const userInfo = useUserStore((state) => state.userInfo);
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isPwrkCnsltnDocMngOpen, setIsPwrkCnsltnDocMngOpen] = useState(false);
|
|
const isEditMode = mode === "edit";
|
|
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
|
|
const fnrsCnsltnDocNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
|
|
|
const { data: pwrkCnsltnData } = usePwrkCnsltnMngByUpDocNo(
|
|
fnrsCnsltnDocNo,
|
|
isSplmntPrgrsStts && Boolean(fnrsCnsltnDocNo),
|
|
);
|
|
const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData));
|
|
|
|
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
|
|
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
|
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "FNRS" });
|
|
const sharTree =
|
|
isEditMode && initialData?.fnrsSharList?.length ? initialData.fnrsSharList : masterSharTree;
|
|
const sharItems = useMemo(() => buildFnrsSharItems(sharTree), [sharTree]);
|
|
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
|
|
|
const form = useForm<FnrsCnsltnRegistFormData>({
|
|
resolver: zodResolver(fnrsCnsltnRegistSchema),
|
|
mode: "onChange",
|
|
defaultValues:
|
|
isEditMode && initialData
|
|
? mapDetailToFormData(initialData)
|
|
: getCreateDefaultValues(userInfo, createDefaults),
|
|
});
|
|
|
|
function createFormPayload(data: FnrsCnsltnRegistFormData, prgrsSttsComCd: string) {
|
|
const result = buildFormPayload(data, prgrsSttsComCd, {
|
|
isEditMode,
|
|
docNo: initialData?.detDto?.fnrsCnsltnDocNo,
|
|
sharItems,
|
|
costItems,
|
|
});
|
|
if ("validationMessage" in result) {
|
|
alert(result.validationMessage);
|
|
return undefined;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function submitForm(
|
|
data: FnrsCnsltnRegistFormData,
|
|
prgrsSttsComCd: "SV" | "ST",
|
|
): Promise<string | undefined> {
|
|
const payload = createFormPayload(data, prgrsSttsComCd);
|
|
if (!payload) return undefined;
|
|
return fnrsCnsltnMngService.saveApply(payload);
|
|
}
|
|
|
|
async function submitSupplementForm(data: FnrsCnsltnRegistFormData): Promise<string | undefined> {
|
|
const beforePrgrsSttsComCd = initialData?.detDto?.beforePrgrsSttsComCd;
|
|
if (!beforePrgrsSttsComCd) return undefined;
|
|
|
|
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
|
const payload = createFormPayload(data, beforePrgrsSttsComCd);
|
|
if (!docNo || !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("보완 요청 내용이 있습니다.\n보완 완료 내용을 입력해 주세요.");
|
|
return;
|
|
}
|
|
|
|
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
|
if (!docNo) {
|
|
alert("문서번호가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
const result = await submitSupplementForm(data);
|
|
if (!result) return;
|
|
|
|
alert("보완 완료 처리되었습니다.");
|
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
|
router.push(withListQs(`/fnrs-cnsltn/${docNo}`));
|
|
} catch (error) {
|
|
console.error("보완 완료 실패:", error);
|
|
alert("보완 완료 처리 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
async function onSave(data: FnrsCnsltnRegistFormData) {
|
|
if (isSubmitting) return;
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
const fnrsCnsltnDocNo = await submitForm(data, "SV");
|
|
if (!fnrsCnsltnDocNo) return;
|
|
|
|
alert("저장되었습니다.");
|
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
|
router.push(withListQs(`/fnrs-cnsltn/${fnrsCnsltnDocNo}/edit`));
|
|
} catch (error) {
|
|
console.error("저장 실패:", error);
|
|
alert("저장 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
async function onApply(data: FnrsCnsltnRegistFormData) {
|
|
if (isSubmitting) return;
|
|
|
|
if (data.privacyConsent !== "Y") {
|
|
alert("개인정보 수집/이용에 동의해야 신청이 가능합니다.");
|
|
return;
|
|
}
|
|
|
|
if (!isFormChecklistComplete(data.cklstResultList)) {
|
|
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
|
return;
|
|
}
|
|
|
|
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
|
|
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
const fnrsCnsltnDocNo = await submitForm(data, "ST");
|
|
if (!fnrsCnsltnDocNo) return;
|
|
|
|
alert("신청 완료되었습니다.");
|
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
|
router.push(withListQs(`/fnrs-cnsltn/${fnrsCnsltnDocNo}`));
|
|
} catch (error) {
|
|
console.error("신청 실패:", error);
|
|
alert("신청 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
function handleApplyClick() {
|
|
form.handleSubmit(onApply)();
|
|
}
|
|
|
|
async function handleDelClick() {
|
|
if (isSubmitting) return;
|
|
|
|
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
|
|
if (!docNo) {
|
|
alert("문서번호가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
if (!(await confirm("삭제하시겠습니까?"))) return;
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
await fnrsCnsltnMngService.remove(docNo);
|
|
alert("삭제되었습니다.");
|
|
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
|
router.push(getListHref("/fnrs-cnsltn"));
|
|
} catch (error) {
|
|
console.error("삭제 실패:", error);
|
|
alert("삭제 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
|
<SectionCollapsibleGroup>
|
|
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
|
<SectionCollapsibleToggle />
|
|
<p className="text-sm text-muted-foreground">
|
|
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
|
</p>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{initialData?.detDto?.fnrsCnsltnDocNo && (
|
|
<ProgressStatusSection
|
|
columns={PROGRESS_STATUS_COLUMNS}
|
|
sttsChgHstryList={initialData?.sttsChgHstryList ?? []}
|
|
elaprDocNo={initialData.detDto.fnrsCnsltnDocNo}
|
|
/>
|
|
)}
|
|
<BasicInfoFormSection />
|
|
<ContactInfoFormSection fieldPrefix="fnrsCnsltnMng" />
|
|
<PrivacyConsentSection />
|
|
<BusinessScaleFormSection />
|
|
<BusinessCostFormSection />
|
|
<FnrsSharFormSection
|
|
initialSharTree={isEditMode ? initialData?.fnrsSharList : undefined}
|
|
initialSharAtchFiles={isEditMode ? initialData?.sharAtchFileList : undefined}
|
|
/>
|
|
<ChecklistFormSection docSeComCd="FNRS" fieldPrefix="fnrsCnsltnMng" />
|
|
<AttachmentsFormSection
|
|
docSeComCd="FNRS"
|
|
fieldPrefix="fnrsCnsltnMng"
|
|
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
|
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
|
/>
|
|
<SplmntDmndFormSection
|
|
initialData={initialData?.splmntDmndList}
|
|
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">
|
|
<Button
|
|
color="white"
|
|
type="button"
|
|
onClick={() => router.push(getListHref("/fnrs-cnsltn"))}
|
|
disabled={isSubmitting}
|
|
>
|
|
목록
|
|
</Button>
|
|
{!isSplmntPrgrsStts ? (
|
|
<>
|
|
<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>
|
|
</>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{hasCompletedDraft ? (
|
|
<Button
|
|
color="orange"
|
|
type="button"
|
|
onClick={() => setIsPwrkCnsltnDocMngOpen(true)}
|
|
disabled={isSubmitting}
|
|
>
|
|
실무협의서 관리
|
|
</Button>
|
|
) : null}
|
|
<Button
|
|
type="button"
|
|
onClick={form.handleSubmit(onSplmntCmptn)}
|
|
disabled={isSubmitting}
|
|
>
|
|
보완 완료
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionCollapsibleGroup>
|
|
</form>
|
|
{hasCompletedDraft && fnrsCnsltnDocNo ? (
|
|
<PwrkCnsltnDocMngDialog
|
|
open={isPwrkCnsltnDocMngOpen}
|
|
fnrsCnsltnDocNo={fnrsCnsltnDocNo}
|
|
canSave
|
|
onClose={() => setIsPwrkCnsltnDocMngOpen(false)}
|
|
/>
|
|
) : null}
|
|
</FormProvider>
|
|
);
|
|
}
|
|
|
|
export { FnrsFormPage };
|