[as] feat: 재원협의 수정

This commit is contained in:
2026-09-22 10:23:10 +09:00
parent e909dc81ad
commit 87fbf15e2b
3 changed files with 67 additions and 37 deletions
@@ -51,7 +51,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.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply,
],
RA: [
@@ -174,6 +174,11 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
async function onApply(data: FnrsCnsltnRegistFormData) {
if (isSubmitting) return;
if (data.privacyConsent !== "Y") {
alert("개인정보 수집/이용에 동의해야 신청이 가능합니다.");
return;
}
if (!isFormChecklistComplete(data.cklstResultList)) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
@@ -4,15 +4,14 @@ import { useEffect, useMemo, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useDownloadAttachmentByAtflId } from "@/entities/attach";
import type { AtflMngClientResponseDto } from "@/entities/fnrs-cnsltn";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
import { useFnrsSharSeMngTree } from "@/entities/fnrs-shar-se";
import {
Button,
cn,
Input,
Skeleton,
@@ -37,6 +36,7 @@ import { calcDiffPercent } from "../lib/format-utils";
import { BizCstTotalDisplayTable } from "./biz-cst-total-display-table";
const SHAR_FILE_KEY_PREFIX = "fnrs-shar::";
const SHAR_FILE_ACCEPT = ".hwp,.hwpx";
interface FnrsSharFormData {
fnrsSharList: FnrsSharListItem[];
@@ -53,6 +53,12 @@ interface FnrsSharFormData {
type SharFieldKey = "exstnAmt" | "chgRqtAmt" | "rmrkCn";
interface SharUploadedFile {
fileKey: string;
fileName: string;
atflId?: string;
}
interface FnrsSharFormSectionProps {
initialSharTree?: FnrsSharBizCstTreeDto[];
initialSharAtchFiles?: AtflMngClientResponseDto[];
@@ -62,12 +68,23 @@ function createSharUploadKey(file: File) {
return `${SHAR_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function toInitialSharFile(file?: AtflMngClientResponseDto): SharUploadedFile | null {
if (!file?.orgnlFileNm) return null;
return {
fileKey: file.atflId ? `server::${file.atflId}` : `${SHAR_FILE_KEY_PREFIX}${file.atflCd}`,
fileName: file.orgnlFileNm,
atflId: file.atflId,
};
}
function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsSharFormSectionProps) {
const { control, setValue, getValues } = useFormContext<FnrsSharFormData>();
const fnrsSharList = useWatch({ control, name: "fnrsSharList" }) ?? [];
const [uploadedFileName, setUploadedFileName] = useState(
() => initialSharAtchFiles?.[0]?.orgnlFileNm ?? "",
const [uploadedFile, setUploadedFile] = useState<SharUploadedFile | null>(() =>
toInitialSharFile(initialSharAtchFiles?.[0]),
);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const { data: masterTree = [], isLoading: isMasterLoading } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree();
@@ -78,9 +95,13 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
const isLoading = initialSharTree?.length ? false : isMasterLoading;
const { uploadFileAsync, isUploading } = useTus({
onSuccess: (atflCd) => {
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd) return;
setValue("fnrsCnsltnMng.sharAtchFileGroupId", atflCd, { shouldDirty: true });
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
const isSharUploading = Object.values(isUploading).some(Boolean);
@@ -120,16 +141,32 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
applySharList(draft);
}
async function handleSharFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
async function handleSharFileAdd(files: FileList | null) {
const file = files?.[0];
if (!file) return;
const fileKey = createSharUploadKey(file);
setUploadedFileName(file.name);
await uploadFileAsync(file, { fileKey });
event.target.value = "";
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, { fileKey });
} catch {
// onError callback handles cleanup
}
}
const sharAtchFiles = uploadedFile
? [
{
fileKey: uploadedFile.fileKey,
fileName: uploadedFile.fileName,
onDownload: uploadedFile.atflId
? () => downloadByAtflId(uploadedFile.atflId!)
: undefined,
},
]
: [];
if (isLoading) {
return (
<SectionCollapsible title="재원분담">
@@ -257,31 +294,20 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
<div className="mt-6 px-2 pb-2">
<h3 className="mb-2 text-xl font-semibold">첨부파일</h3>
<div className="flex flex-wrap items-center gap-2">
{isSharUploading && uploadedFileName ? (
<TextShimmer className="max-w-lg text-xl" active>
{uploadedFileName}
</TextShimmer>
) : (
<Input
value={uploadedFileName}
readOnly
placeholder="총사업비 신청서(.hwp) 파일을 등록해주세요."
className="max-w-lg rounded-sm"
/>
)}
<Button type="button" color="white" disabled={isSharUploading} asChild>
<label className="cursor-pointer">
첨부파일
<input
type="file"
accept=".hwp,.hwpx"
className="hidden"
onChange={handleSharFileChange}
/>
</label>
</Button>
</div>
{!uploadedFile ? (
<p className="mb-2 text-lg text-muted-foreground">
총사업비 신청서(.hwp) 파일을 등록해주세요.
</p>
) : null}
<AttachmentFileField
files={sharAtchFiles}
inputId="fnrs-shar-atch-file-input"
isUploadingByKey={isUploading}
accept={SHAR_FILE_ACCEPT}
multiple={false}
addDisabled={isSharUploading}
onAdd={(fileList) => void handleSharFileAdd(fileList)}
/>
</div>
</SectionCollapsible>
);