Files
frontend/apps/sn/app/components/ui/verticalTable.tsx
T

336 lines
13 KiB
TypeScript

/**
* 📄 VerticalTable Component
* 세로 방향 테이블 컴포넌트
*
* - 세로 방향 테이블 렌더링 및 데이터 표시
* - 편집 가능 필드 지원 (select, input 타입)
* - 입력 값 검증 및 에러 메시지 표시
* - 페이지네이션 및 데이터 페이징 처리
* - 두 개의 테이블 동시 표시 지원
* - 폼 데이터 변경 콜백 및 저장 기능
*/
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
import { Table, TableBody, TableCell, TableRow, Button } from "@common/components";
import { useEffect, useState } from "react";
import { confirmToast, showConfirmToast } from '@/components/ui/toast'
import { toast, Toaster } from "react-hot-toast";
type UiTableProps = {
headName: React.ReactNode | undefined;
tableName1?: React.ReactNode | undefined;
tableData: { [key: string]: string | number | boolean | object }[];
headlist: UiHeadProps[];
pageSize: number;
total: number;
children?: Record<string, React.ReactNode> | undefined;
cellClick: ((index: number | undefined, headId: string) => void) | undefined;
onPageChange: (page: number) => void;
className?: string;
resetPageNo?: boolean;
isEditable?: boolean;
tableName2?: React.ReactNode | undefined;
tableData2?: { [key: string]: string | number | boolean | object }[];
headlist2?: UiHeadProps[];
pageSize2?: number;
total2?: number;
children2?: Record<string, React.ReactNode> | undefined;
cellClick2?: ((index: number | undefined, headId: string) => void) | undefined;
onPageChange2?: (page: number) => void;
className2?: string;
resetPageNo2?: boolean;
isEditable2?: boolean;
showSecondTable?: boolean;
showSaveButton?: boolean;
onSaveClick?: () => void;
onFormDataChange?: (formData: { [key: string]: string | number | boolean }) => void; // 추가
};
type UiHeadProps = {
id: string;
title: string;
align?: string;
type?: string;
options?: string[];
placeholder?: string;
isNumber?: boolean;
length?: number;
};
export function VerticalTable({
headName,
tableName1,
tableData,
headlist,
pageSize,
total,
children,
onPageChange,
className = "",
resetPageNo = false,
isEditable = false,
tableName2,
headlist2,
isEditable2 = false,
showSecondTable = false,
showSaveButton = false,
onSaveClick,
onFormDataChange,
}: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [formData, setFormData] = useState<{ [key: string]: string | number | boolean }>({});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
useEffect(() => {
if (resetPageNo) {
setCur(1);
onPageChange(1);
}
}, [onPageChange, resetPageNo]);
useEffect(() => {
if (tableData.length > 0 && tableData[0]) {
const newData = JSON.parse(JSON.stringify(tableData[0]));
setFormData(newData);
setErrors({});
}
}, [tableData]);
const handleInputChange = (id: string, value: string | number | boolean) => {
const newFormData = {
...formData,
[id]: value,
};
setFormData(newFormData);
if (errors[id]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[id];
return newErrors;
});
}
if (onFormDataChange) {
onFormDataChange(newFormData);
}
};
const handlePageChange = (page: number) => {
setCur(page);
onPageChange(page);
};
const showError = (fieldId: string, message: string) => {
setErrors(prev => ({
...prev,
[fieldId]: message
}));
};
return (
<>
{headName ? (
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl mx-3 font-medium text-label">{headName}</h3>
{showSaveButton && onSaveClick && (
<Button
onClick={onSaveClick}
className="px-4 mr-3 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
>
저장
</Button>
)}
</div>
) : null}
<div className={`card !m-[1.2rem] !p-0r ${className}`}>
{tableName1 && ( // tableName prop이 있으면 표시
<div className="text-xl font-semibold my-2 mx-3">{tableName1}</div>
)}
<Table variant="secondary">
<TableBody className="text-center">
{headlist?.map((head) => (
<TableRow key={head.id}>
{/* 컬럼 제목 */}
<TableCell className={`font-bold bg-blue-100 text-xs w-[120px] whitespace-nowrap`}>
{head.title}
</TableCell>
{/* 데이터 값 또는 입력 필드 */}
<TableCell className="text-xs">
{children && children[head.id] ? children[head.id] : isEditable ? (
head.type === "select" ? (
<select
id={head.id}
value={String(formData[head.id]) ?? "-"}
onChange={(e) => handleInputChange(head.id, e.target.value)}
className="w-full border border-gray-300 rounded-sm px-2 py-1 text-xl border-radius-0"
>
{head.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
) : head.type === "input" ? (
<div className="w-full">
<input
id={head.id}
type="text"
value={String(formData[head.id] ?? "")}
onChange={(e) => {
if (head.isNumber) {
const newValue = e.target.value;
if (newValue === "") {
handleInputChange(head.id, newValue);
return;
}
const isValidNumber = /^[0-9]+$/.test(newValue);
if (!isValidNumber) {
showError(head.id, `${head.title}는 숫자만 입력 가능합니다.`);
return;
}
if (head.length && newValue.length > head.length) {
showError(head.id, `${head.title}는 최대 ${head.length}자까지 입력 가능합니다.`);
return;
}
const numericValue = parseInt(newValue);
if (isNaN(numericValue) || numericValue < 0) return;
handleInputChange(head.id, numericValue);
} else {
const newValue = e.target.value;
if (head.length && newValue.length > head.length) {
showError(head.id, `${head.title}는 최대 ${head.length}자까지 입력 가능합니다.`);
return;
}
handleInputChange(head.id, e.target.value);
}
}}
className="w-full text-xl border-radius-0 border border-gray-300 rounded-sm px-2 py-1"
placeholder={head.placeholder}
/>
{errors[head.id] && (
<div className="text-red-500 font-bold text-xl mt-1 text-left">
{errors[head.id]}
</div>
)}
</div>
) : (
<span>{formData[head.id] ?? "-"}</span>
)
) : (
<span>{formData[head.id] ?? "-"}</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{tableName2 && ( // tableName prop이 있으면 표시
<div className="text-xl font-semibold mt-8 my-2 mx-3">{tableName2}</div>
)}
{showSecondTable && (
<Table variant="secondary">
<TableBody className="text-center">
{headlist2?.map((head) => (
<TableRow key={head.id}>
{/* 컬럼 제목 */}
<TableCell className={`font-bold bg-blue-100 text-xs w-[120px] whitespace-nowrap`}>
{head.title}
</TableCell>
{/* 데이터 값 또는 입력 필드 */}
<TableCell className="text-xs">
{children && children[head.id] ? children[head.id] : isEditable2 ? (
head.type === "select" ? (
<select
id={head.id}
value={formData[head.id] !== undefined ? String(formData[head.id]) : ""}
onChange={(e) => handleInputChange(head.id, e.target.value)}
className="w-full border border-gray-300 rounded-sm px-2 py-1 text-xl border-radius-0"
>
{head.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
) : head.type === "input" ? (
<div className="w-full">
<input
id={head.id}
type={head.isNumber ? "number" : "text"}
value={formData[head.id] !== undefined ? String(formData[head.id]) : ""}
onChange={(e) => {
if (head.isNumber) {
const newValue = e.target.value;
if (newValue === "") {
handleInputChange(head.id, newValue);
return;
}
const isValidNumber = /^[0-9]+$/.test(newValue);
if (!isValidNumber) {
showError(head.id, `${head.title}는 숫자만 입력 가능합니다.`);
return;
}
if (head.length && newValue.length > head.length) {
showError(head.id, `${head.title}는 최대 ${head.length}자까지 입력 가능합니다.`);
return;
}
const numericValue = parseInt(newValue);
if (isNaN(numericValue) || numericValue < 0) return;
handleInputChange(head.id, numericValue);
} else {
const newValue = e.target.value;
if (head.length && newValue.length > head.length) {
showError(head.id, `${head.title}는 최대 ${head.length}자까지 입력 가능합니다.`);
return;
}
handleInputChange(head.id, e.target.value);
}
}}
className="w-full text-xl border-radius-0 border border-gray-300 rounded-sm px-2 py-1"
placeholder={head.placeholder}
/>
{errors[head.id] && (
<div className="text-red-500 font-bold text-xl mt-1 text-left">
{errors[head.id]}
</div>
)}
</div>
) : (
<span>{formData[head.id] ?? "-"}</span>
)
) : (
<span>{formData[head.id] ?? "-"}</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{total > 0 && (
<UiPageNations
total={total}
cur={cur}
pageSize={pageSize}
onPageChange={handlePageChange}
/>
)}
</div>
<Toaster position="top-center"></Toaster>
</>
);
}