[as] refactor: 재원협의 폼 useWatch + Controller 방식 제거
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Download } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
@@ -78,7 +78,7 @@ function AttachmentsFormSection({
|
|||||||
initialEsntlFiles,
|
initialEsntlFiles,
|
||||||
initialEtcFiles,
|
initialEtcFiles,
|
||||||
}: AttachmentsFormSectionProps) {
|
}: AttachmentsFormSectionProps) {
|
||||||
const { control, getValues, setValue } = useFormContext<AttachmentsFormData>();
|
const { getValues, setValue } = useFormContext<AttachmentsFormData>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<Record<string, UploadedFileItem[]>>({});
|
const [uploadedFiles, setUploadedFiles] = useState<Record<string, UploadedFileItem[]>>({});
|
||||||
const [etcFiles, setEtcFiles] = useState<EtcFileItem[]>([]);
|
const [etcFiles, setEtcFiles] = useState<EtcFileItem[]>([]);
|
||||||
@@ -373,89 +373,83 @@ function AttachmentsFormSection({
|
|||||||
return (
|
return (
|
||||||
<SectionCollapsible title="첨부파일">
|
<SectionCollapsible title="첨부파일">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Controller
|
<div className="px-2">
|
||||||
name="esntlAtchFileList"
|
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||||
control={control}
|
<Table variant="secondary" className="table-fixed">
|
||||||
render={() => (
|
<colgroup>
|
||||||
<div className="px-2">
|
<col width="44%" />
|
||||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
<col width="12%" />
|
||||||
<Table variant="secondary" className="table-fixed">
|
<col width="44%" />
|
||||||
<colgroup>
|
</colgroup>
|
||||||
<col width="44%" />
|
<TableHeader>
|
||||||
<col width="12%" />
|
<TableRow>
|
||||||
<col width="44%" />
|
<TableHead className="w-[30rem]" align="left">
|
||||||
</colgroup>
|
파일명
|
||||||
<TableHeader>
|
</TableHead>
|
||||||
<TableRow>
|
<TableHead>파일 확장자</TableHead>
|
||||||
<TableHead className="w-[30rem]" align="left">
|
<TableHead>파일첨부</TableHead>
|
||||||
파일명
|
</TableRow>
|
||||||
</TableHead>
|
</TableHeader>
|
||||||
<TableHead>파일 확장자</TableHead>
|
<TableBody>
|
||||||
<TableHead>파일첨부</TableHead>
|
{essentialFiles.length === 0 ? (
|
||||||
</TableRow>
|
<TableRow>
|
||||||
</TableHeader>
|
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
|
||||||
<TableBody>
|
조회된 필수 첨부파일 항목이 없습니다.
|
||||||
{essentialFiles.length === 0 ? (
|
</TableCell>
|
||||||
<TableRow>
|
</TableRow>
|
||||||
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
|
) : (
|
||||||
조회된 필수 첨부파일 항목이 없습니다.
|
essentialFiles.map((file) => {
|
||||||
|
const atflCd = file.atchFileGroupId ?? "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={file.esntlAtchFileId}>
|
||||||
|
<TableHead
|
||||||
|
className="group break-keep font-medium"
|
||||||
|
data-required={file.esntlAtchYn === "Y"}
|
||||||
|
align="left"
|
||||||
|
>
|
||||||
|
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
|
||||||
|
{file.esntlAtchFileNm}
|
||||||
|
{atflCd ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="white"
|
||||||
|
onClick={() => downloadTemplate(atflCd)}
|
||||||
|
>
|
||||||
|
<Download className="!size-5 shrink-0" />
|
||||||
|
양식내려받기
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
|
||||||
|
<TableCell className="space-y-2">
|
||||||
|
<AttachmentFileField
|
||||||
|
files={(uploadedFiles[file.esntlAtchFileId] ?? []).map((item) => ({
|
||||||
|
fileKey: item.fileKey,
|
||||||
|
fileName: item.file?.name ?? item.fileName,
|
||||||
|
onDownload: item.atflId
|
||||||
|
? () => downloadByAtflId(item.atflId!)
|
||||||
|
: undefined,
|
||||||
|
}))}
|
||||||
|
inputId={`${file.esntlAtchFileId}-file-input`}
|
||||||
|
isUploadingByKey={isUploading}
|
||||||
|
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
|
||||||
|
onAdd={(fileList) =>
|
||||||
|
void handleEssentialFileUpload(file.esntlAtchFileId, fileList)
|
||||||
|
}
|
||||||
|
onRemove={(index) =>
|
||||||
|
void removeEssentialFile(file.esntlAtchFileId, index)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
);
|
||||||
essentialFiles.map((file) => {
|
})
|
||||||
const atflCd = file.atchFileGroupId ?? "";
|
)}
|
||||||
|
</TableBody>
|
||||||
return (
|
</Table>
|
||||||
<TableRow key={file.esntlAtchFileId}>
|
</div>
|
||||||
<TableHead
|
|
||||||
className="group break-keep font-medium"
|
|
||||||
data-required={file.esntlAtchYn === "Y"}
|
|
||||||
align="left"
|
|
||||||
>
|
|
||||||
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
|
|
||||||
{file.esntlAtchFileNm}
|
|
||||||
{atflCd ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
color="white"
|
|
||||||
onClick={() => downloadTemplate(atflCd)}
|
|
||||||
>
|
|
||||||
<Download className="!size-5 shrink-0" />
|
|
||||||
양식내려받기
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
|
|
||||||
<TableCell className="space-y-2">
|
|
||||||
<AttachmentFileField
|
|
||||||
files={(uploadedFiles[file.esntlAtchFileId] ?? []).map((item) => ({
|
|
||||||
fileKey: item.fileKey,
|
|
||||||
fileName: item.file?.name ?? item.fileName,
|
|
||||||
onDownload: item.atflId
|
|
||||||
? () => downloadByAtflId(item.atflId!)
|
|
||||||
: undefined,
|
|
||||||
}))}
|
|
||||||
inputId={`${file.esntlAtchFileId}-file-input`}
|
|
||||||
isUploadingByKey={isUploading}
|
|
||||||
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
|
|
||||||
onAdd={(fileList) =>
|
|
||||||
void handleEssentialFileUpload(file.esntlAtchFileId, fileList)
|
|
||||||
}
|
|
||||||
onRemove={(index) =>
|
|
||||||
void removeEssentialFile(file.esntlAtchFileId, index)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import { Field } from "@/shared/ui/field";
|
import { Field } from "@/shared/ui/field";
|
||||||
@@ -31,6 +31,16 @@ interface BusinessCostFormData {
|
|||||||
|
|
||||||
type CostFieldKey = "bscPlanAmt" | "bgtRfltAmt" | "tstmDesignAmt";
|
type CostFieldKey = "bscPlanAmt" | "bgtRfltAmt" | "tstmDesignAmt";
|
||||||
|
|
||||||
|
function isSameBizCstCodes(
|
||||||
|
current: BusinessCostFormData["bizCstList"],
|
||||||
|
next: BusinessCostFormData["bizCstList"],
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
current.length === next.length &&
|
||||||
|
current.every((item, index) => item.acntgSeCd === next[index]?.acntgSeCd)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function BusinessCostFormSection() {
|
function BusinessCostFormSection() {
|
||||||
const { control, setValue, getValues } = useFormContext<BusinessCostFormData>();
|
const { control, setValue, getValues } = useFormContext<BusinessCostFormData>();
|
||||||
const bizCstList = useWatch({ control, name: "bizCstList" }) ?? [];
|
const bizCstList = useWatch({ control, name: "bizCstList" }) ?? [];
|
||||||
@@ -42,22 +52,22 @@ function BusinessCostFormSection() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (costItems.length === 0) return;
|
if (costItems.length === 0) return;
|
||||||
|
|
||||||
const existingMap = new Map(getValues("bizCstList").map((item) => [item.acntgSeCd, item]));
|
const current = getValues("bizCstList") ?? [];
|
||||||
|
const existingMap = new Map(current.map((item) => [item.acntgSeCd, item]));
|
||||||
|
const next = costItems.map((item) => {
|
||||||
|
const existing = existingMap.get(item.code);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
setValue(
|
return {
|
||||||
"bizCstList",
|
acntgSeCd: item.code,
|
||||||
costItems.map((item) => {
|
bscPlanAmt: 0,
|
||||||
const existing = existingMap.get(item.code);
|
bgtRfltAmt: 0,
|
||||||
if (existing) return existing;
|
tstmDesignAmt: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
if (isSameBizCstCodes(current, next)) return;
|
||||||
acntgSeCd: item.code,
|
setValue("bizCstList", next);
|
||||||
bscPlanAmt: 0,
|
|
||||||
bgtRfltAmt: 0,
|
|
||||||
tstmDesignAmt: 0,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}, [costItems, getValues, setValue]);
|
}, [costItems, getValues, setValue]);
|
||||||
|
|
||||||
function handleCostChange(index: number, fieldKey: CostFieldKey, value: string) {
|
function handleCostChange(index: number, fieldKey: CostFieldKey, value: string) {
|
||||||
@@ -119,58 +129,37 @@ function BusinessCostFormSection() {
|
|||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`bizCstList.${idx}.bgtRfltAmt`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.bgtRfltAmt ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||||
value={field.value || ""}
|
/>
|
||||||
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`bizCstList.${idx}.bscPlanAmt`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.bscPlanAmt ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||||
value={field.value || ""}
|
/>
|
||||||
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`bizCstList.${idx}.tstmDesignAmt`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.tstmDesignAmt ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleCostChange(idx, "tstmDesignAmt", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||||
value={field.value || ""}
|
/>
|
||||||
onChange={(e) => handleCostChange(idx, "tstmDesignAmt", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import { Field } from "@/shared/ui/field";
|
import { Field } from "@/shared/ui/field";
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
|
||||||
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
|
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
|
||||||
|
|
||||||
interface BusinessScaleFormData {
|
interface BusinessScaleFormData {
|
||||||
bzScaleList: Array<{
|
bzScaleList: Array<{
|
||||||
@@ -31,6 +31,16 @@ interface BusinessScaleFormData {
|
|||||||
|
|
||||||
type ScaleFieldKey = "bscPl" | "budgetReflct" | "thtiDsgn";
|
type ScaleFieldKey = "bscPl" | "budgetReflct" | "thtiDsgn";
|
||||||
|
|
||||||
|
function isSameBzScaleCodes(
|
||||||
|
current: BusinessScaleFormData["bzScaleList"],
|
||||||
|
next: BusinessScaleFormData["bzScaleList"],
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
current.length === next.length &&
|
||||||
|
current.every((item, index) => item.cnstpSeCd === next[index]?.cnstpSeCd)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function BusinessScaleFormSection() {
|
function BusinessScaleFormSection() {
|
||||||
const { control, setValue, getValues } = useFormContext<BusinessScaleFormData>();
|
const { control, setValue, getValues } = useFormContext<BusinessScaleFormData>();
|
||||||
const bzScaleList = useWatch({ control, name: "bzScaleList" }) ?? [];
|
const bzScaleList = useWatch({ control, name: "bzScaleList" }) ?? [];
|
||||||
@@ -42,7 +52,8 @@ function BusinessScaleFormSection() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scaleItems.length === 0) return;
|
if (scaleItems.length === 0) return;
|
||||||
|
|
||||||
const existingMap = new Map(getValues("bzScaleList").map((item) => [item.cnstpSeCd, item]));
|
const current = getValues("bzScaleList") ?? [];
|
||||||
|
const existingMap = new Map(current.map((item) => [item.cnstpSeCd, item]));
|
||||||
|
|
||||||
const next = scaleItems.map((item) => {
|
const next = scaleItems.map((item) => {
|
||||||
const existing = existingMap.get(item.code);
|
const existing = existingMap.get(item.code);
|
||||||
@@ -57,6 +68,7 @@ function BusinessScaleFormSection() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isSameBzScaleCodes(current, next)) return;
|
||||||
setValue("bzScaleList", recalculateBzScaleTotals(next, scaleItems));
|
setValue("bzScaleList", recalculateBzScaleTotals(next, scaleItems));
|
||||||
}, [scaleItems, getValues, setValue]);
|
}, [scaleItems, getValues, setValue]);
|
||||||
|
|
||||||
@@ -112,58 +124,37 @@ function BusinessScaleFormSection() {
|
|||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`bzScaleList.${idx}.bscPl`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.bscPl ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleScaleChange(idx, "bscPl", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||||
value={field.value || ""}
|
/>
|
||||||
onChange={(e) => handleScaleChange(idx, "bscPl", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`bzScaleList.${idx}.budgetReflct`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.budgetReflct ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleScaleChange(idx, "budgetReflct", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||||
value={field.value || ""}
|
/>
|
||||||
onChange={(e) => handleScaleChange(idx, "budgetReflct", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`bzScaleList.${idx}.thtiDsgn`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.thtiDsgn ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleScaleChange(idx, "thtiDsgn", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||||
value={field.value || ""}
|
/>
|
||||||
onChange={(e) => handleScaleChange(idx, "thtiDsgn", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { SectionCollapsible } from "@/shared/ui";
|
import { SectionCollapsible } from "@/shared/ui";
|
||||||
import { Field, FieldLabel } from "@/shared/ui/field";
|
|
||||||
import { cklstMngService, type CklstTreeNode } from "@/entities/cklst";
|
import { cklstMngService, type CklstTreeNode } from "@/entities/cklst";
|
||||||
import { useCodeList } from "@/entities/code";
|
import { useCodeList } from "@/entities/code";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
RadioGroup,
|
|
||||||
RadioGroupItem,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -125,11 +122,21 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
|||||||
}
|
}
|
||||||
}, [checklistTabs, activeTab]);
|
}, [checklistTabs, activeTab]);
|
||||||
|
|
||||||
|
function handleSelectChange(resultIdx: number, value: "Y" | "N") {
|
||||||
|
const currentList = getValues("cklstResultList") as CklstResultListItem[];
|
||||||
|
const row = currentList[resultIdx];
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const newList = [...currentList];
|
||||||
|
newList[resultIdx] = { ...row, selected: value };
|
||||||
|
setValue("cklstResultList", newList, { shouldDirty: true });
|
||||||
|
}
|
||||||
|
|
||||||
function handleSelectAll(tabCode: string, value: "Y" | "N") {
|
function handleSelectAll(tabCode: string, value: "Y" | "N") {
|
||||||
const tab = checklistTabs.find((t) => t.code === tabCode);
|
const tab = checklistTabs.find((t) => t.code === tabCode);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
const currentList = getValues("cklstResultList");
|
const currentList = getValues("cklstResultList") as CklstResultListItem[];
|
||||||
const newList = [...currentList];
|
const newList = [...currentList];
|
||||||
|
|
||||||
tab.items
|
tab.items
|
||||||
@@ -141,7 +148,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setValue("cklstResultList", newList);
|
setValue("cklstResultList", newList, { shouldDirty: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSelectedTaskCodes = Boolean(taskSeComCdArray?.length);
|
const hasSelectedTaskCodes = Boolean(taskSeComCdArray?.length);
|
||||||
@@ -234,6 +241,8 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
|||||||
{tab.items.map((item) => {
|
{tab.items.map((item) => {
|
||||||
const resultIdx = cklstRvwSnIndexMap.get(item.cklstRvwSn) ?? -1;
|
const resultIdx = cklstRvwSnIndexMap.get(item.cklstRvwSn) ?? -1;
|
||||||
const showRadios = item.bchckUseYn === "Y";
|
const showRadios = item.bchckUseYn === "Y";
|
||||||
|
const selected = cklstResultList[resultIdx]?.selected;
|
||||||
|
const radioName = `check-${item.cklstRvwSn}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={item.cklstRvwSn}>
|
<TableRow key={item.cklstRvwSn}>
|
||||||
@@ -248,66 +257,30 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
|||||||
{item.content}
|
{item.content}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{showRadios && resultIdx >= 0 ? (
|
{showRadios && resultIdx >= 0 ? (
|
||||||
<Controller
|
<>
|
||||||
name={`cklstResultList.${resultIdx}.selected`}
|
<TableCell className="text-center">
|
||||||
control={control}
|
<input
|
||||||
render={({ field, fieldState }) => (
|
type="radio"
|
||||||
<>
|
name={radioName}
|
||||||
<TableCell className="text-center">
|
value="Y"
|
||||||
<RadioGroup
|
checked={selected === "Y"}
|
||||||
name={field.name}
|
onChange={() => handleSelectChange(resultIdx, "Y")}
|
||||||
value={field.value === "Y" ? "Y" : undefined}
|
aria-label="예"
|
||||||
onValueChange={field.onChange}
|
className="cursor-pointer"
|
||||||
className="flex justify-center"
|
/>
|
||||||
>
|
</TableCell>
|
||||||
<FieldLabel
|
<TableCell className="text-center">
|
||||||
htmlFor={`check-${item.cklstRvwSn}-Y`}
|
<input
|
||||||
className="!w-auto justify-center border-none"
|
type="radio"
|
||||||
>
|
name={radioName}
|
||||||
<Field
|
value="N"
|
||||||
orientation="horizontal"
|
checked={selected === "N"}
|
||||||
data-invalid={fieldState.invalid}
|
onChange={() => handleSelectChange(resultIdx, "N")}
|
||||||
className="w-auto"
|
aria-label="아니오"
|
||||||
>
|
className="cursor-pointer"
|
||||||
<RadioGroupItem
|
/>
|
||||||
value="Y"
|
</TableCell>
|
||||||
id={`check-${item.cklstRvwSn}-Y`}
|
</>
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
aria-label="예"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</FieldLabel>
|
|
||||||
</RadioGroup>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<RadioGroup
|
|
||||||
name={field.name}
|
|
||||||
value={field.value === "N" ? "N" : undefined}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
className="flex justify-center"
|
|
||||||
>
|
|
||||||
<FieldLabel
|
|
||||||
htmlFor={`check-${item.cklstRvwSn}-N`}
|
|
||||||
className="!w-auto justify-center border-none"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
orientation="horizontal"
|
|
||||||
data-invalid={fieldState.invalid}
|
|
||||||
className="w-auto"
|
|
||||||
>
|
|
||||||
<RadioGroupItem
|
|
||||||
value="N"
|
|
||||||
id={`check-${item.cklstRvwSn}-N`}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
aria-label="아니오"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</FieldLabel>
|
|
||||||
</RadioGroup>
|
|
||||||
</TableCell>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||||
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
|
||||||
@@ -78,6 +78,13 @@ function toInitialSharFile(file?: AtflMngClientResponseDto): SharUploadedFile |
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSameFnrsSharCodes(current: FnrsSharListItem[], next: FnrsSharListItem[]) {
|
||||||
|
return (
|
||||||
|
current.length === next.length &&
|
||||||
|
current.every((item, index) => item.fnrsSharSeCd === next[index]?.fnrsSharSeCd)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsSharFormSectionProps) {
|
function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsSharFormSectionProps) {
|
||||||
const { control, setValue, getValues } = useFormContext<FnrsSharFormData>();
|
const { control, setValue, getValues } = useFormContext<FnrsSharFormData>();
|
||||||
const fnrsSharList = useWatch({ control, name: "fnrsSharList" }) ?? [];
|
const fnrsSharList = useWatch({ control, name: "fnrsSharList" }) ?? [];
|
||||||
@@ -117,6 +124,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
|||||||
: mergeFnrsSharTreeWithForm(sharTree, []);
|
: mergeFnrsSharTreeWithForm(sharTree, []);
|
||||||
|
|
||||||
const withTotals = recalculateFnrsSharTotals(merged, sharItems);
|
const withTotals = recalculateFnrsSharTotals(merged, sharItems);
|
||||||
|
if (isSameFnrsSharCodes(existing, withTotals)) return;
|
||||||
setValue("fnrsSharList", withTotals);
|
setValue("fnrsSharList", withTotals);
|
||||||
}, [sharItems, sharTree, initialSharTree, getValues, setValue]);
|
}, [sharItems, sharTree, initialSharTree, getValues, setValue]);
|
||||||
|
|
||||||
@@ -226,40 +234,26 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
|||||||
{item.label}
|
{item.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`fnrsSharList.${idx}.exstnAmt`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.exstnAmt ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleSharChange(idx, "exstnAmt", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={cn("text-right", item.readonly && "bg-muted")}
|
||||||
value={field.value ?? ""}
|
/>
|
||||||
onChange={(e) => handleSharChange(idx, "exstnAmt", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={cn("text-right", item.readonly && "bg-muted")}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`fnrsSharList.${idx}.chgRqtAmt`}
|
<Input
|
||||||
control={control}
|
type="number"
|
||||||
render={({ field, fieldState }) => (
|
value={data.chgRqtAmt ?? ""}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
onChange={(e) => handleSharChange(idx, "chgRqtAmt", e.target.value)}
|
||||||
<Input
|
readOnly={item.readonly}
|
||||||
type="number"
|
className={cn("text-right", item.readonly && "bg-muted")}
|
||||||
value={field.value ?? ""}
|
/>
|
||||||
onChange={(e) => handleSharChange(idx, "chgRqtAmt", e.target.value)}
|
</Field>
|
||||||
readOnly={item.readonly}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
className={cn("text-right", item.readonly && "bg-muted")}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
@@ -270,19 +264,12 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
|||||||
<div className="text-right">{calcDiffPercent(diff, data.exstnAmt || 0)}</div>
|
<div className="text-right">{calcDiffPercent(diff, data.exstnAmt || 0)}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Controller
|
<Field>
|
||||||
name={`fnrsSharList.${idx}.rmrkCn`}
|
<Input
|
||||||
control={control}
|
value={data.rmrkCn ?? ""}
|
||||||
render={({ field, fieldState }) => (
|
onChange={(e) => handleSharChange(idx, "rmrkCn", e.target.value)}
|
||||||
<Field data-invalid={fieldState.invalid}>
|
/>
|
||||||
<Input
|
</Field>
|
||||||
value={field.value ?? ""}
|
|
||||||
onChange={(e) => handleSharChange(idx, "rmrkCn", e.target.value)}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user