Files
frontend/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx
T
jcchoi c2396f093b
Deploy Frontend / deploy (push) Successful in 8m31s
필수입력 안내 메세지
2026-10-07 19:43:06 +09:00

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 };