충돌해결 및 변경사항 커밋
This commit is contained in:
parent
74f6c9ba83
commit
5e778d3052
@ -21,6 +21,7 @@ const oemMngController = require("../controllers/app/common/oemmng.controller");
|
|||||||
const productgroupController = require("../controllers/app/common/productgroup.controller");
|
const productgroupController = require("../controllers/app/common/productgroup.controller");
|
||||||
const productController = require("../controllers/app/common/product.controller");
|
const productController = require("../controllers/app/common/product.controller");
|
||||||
const carMngController = require("../controllers/app/common/carmng.controller");
|
const carMngController = require("../controllers/app/common/carmng.controller");
|
||||||
|
const contractController = require("../controllers/app/contract/contract.controller");
|
||||||
|
|
||||||
router.use("/health", healthController);
|
router.use("/health", healthController);
|
||||||
router.use("/auth", authController);
|
router.use("/auth", authController);
|
||||||
@ -41,5 +42,6 @@ router.use("/oemmng", oemMngController);
|
|||||||
router.use("/productgroup", productgroupController);
|
router.use("/productgroup", productgroupController);
|
||||||
router.use("/product", productController);
|
router.use("/product", productController);
|
||||||
router.use("/carmng", carMngController);
|
router.use("/carmng", carMngController);
|
||||||
|
router.use("/contract", contractController);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// src/services/contract.service.js
|
// src/services/contract.service.js
|
||||||
const { Contract, ContractDetail, Company } = require("../models");
|
const { Contract, ContractDetail, Company, ContractMgmt} = require("../models");
|
||||||
const alertService = require("./alert.service");
|
const alertService = require("./alert.service");
|
||||||
const { Op } = require("sequelize");
|
const { Op } = require("sequelize");
|
||||||
// const { getKoreanSubjectParticle } = require("../utils/koreanParticle");
|
// const { getKoreanSubjectParticle } = require("../utils/koreanParticle");
|
||||||
@ -114,6 +114,118 @@ class ContractService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 영업정보 목록 조회 수정
|
||||||
|
async getContractList(page = 1, limit = 20) {
|
||||||
|
try {
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const { count, rows } = await ContractMgmt.findAndCountAll({
|
||||||
|
attributes: {
|
||||||
|
exclude: ['createdAt', 'updatedAt', 'created_at', 'updated_at'] // 모든 타임스탬프 필드 제외
|
||||||
|
},
|
||||||
|
order: [['created_at', 'DESC']], // created_at으로 수정
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rows || !Array.isArray(rows)) {
|
||||||
|
throw new Error('No data found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터 형식 변환
|
||||||
|
const formatted = rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
// Date 형식 변환 및 검사 추가
|
||||||
|
customer_info: row.customer_info ? new Date(row.customer_info).toISOString() : null,
|
||||||
|
duckil_info: row.duckil_info ? new Date(row.duckil_info).toISOString() : null,
|
||||||
|
register_date: row.register_date ? new Date(row.register_date).toISOString() : null,
|
||||||
|
request_date: row.request_date ? new Date(row.request_date).toISOString() : null,
|
||||||
|
submit_date: row.submit_date ? new Date(row.submit_date).toISOString() : null,
|
||||||
|
created_at: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||||||
|
updated_at: row.updated_at ? new Date(row.updated_at).toISOString() : null,
|
||||||
|
|
||||||
|
// 숫자 데이터 변환
|
||||||
|
progress: row.progress ? parseFloat(row.progress) : null,
|
||||||
|
customer_total: row.customer_total ? parseInt(row.customer_total, 10) : null,
|
||||||
|
duckil_production: row.duckil_production ? parseInt(row.duckil_production, 10) : null,
|
||||||
|
first_quarter: row.first_quarter ? parseInt(row.first_quarter, 10) : null,
|
||||||
|
second_quarter: row.second_quarter ? parseInt(row.second_quarter, 10) : null,
|
||||||
|
third_quarter: row.third_quarter ? parseInt(row.third_quarter, 10) : null,
|
||||||
|
fourth_quarter: row.fourth_quarter ? parseInt(row.fourth_quarter, 10) : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
oemMngs: formatted,
|
||||||
|
total: count,
|
||||||
|
totalPages: Math.ceil(count / limit),
|
||||||
|
currentPage: page,
|
||||||
|
pageSize: limit
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Contract Service Error:', error); // 에러 로깅 추가
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 영업정보 생성 (데이터 타입 변환 추가)
|
||||||
|
async createContractMgmt(contractData) {
|
||||||
|
try {
|
||||||
|
const formattedData = this._formatContractData(contractData);
|
||||||
|
return await ContractMgmt.create(formattedData);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 영업정보 수정 (데이터 타입 변환 추가)
|
||||||
|
async updateContractMgmt(id, updateData) {
|
||||||
|
try {
|
||||||
|
const contract = await ContractMgmt.findByPk(id);
|
||||||
|
if (!contract) throw new Error('Contract not found');
|
||||||
|
|
||||||
|
const formattedData = this._formatContractData(updateData);
|
||||||
|
await contract.update(formattedData);
|
||||||
|
return contract;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 영업정보 삭제
|
||||||
|
async deleteContractMgmt(id) {
|
||||||
|
try {
|
||||||
|
const contract = await ContractMgmt.findByPk(id);
|
||||||
|
if (!contract) throw new Error('Contract not found');
|
||||||
|
|
||||||
|
await contract.destroy();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터 형식 변환 헬퍼 메서드
|
||||||
|
_formatContractData(data) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
// 날짜 문자열을 Date 객체로 변환
|
||||||
|
customer_info: data.customer_info ? new Date(data.customer_info) : null,
|
||||||
|
duckil_info: data.duckil_info ? new Date(data.duckil_info) : null,
|
||||||
|
register_date: data.register_date ? new Date(data.register_date) : null,
|
||||||
|
request_date: data.request_date ? new Date(data.request_date) : null,
|
||||||
|
submit_date: data.submit_date ? new Date(data.submit_date) : null,
|
||||||
|
// 숫자 문자열을 숫자로 변환
|
||||||
|
progress: data.progress ? Number(data.progress) : null,
|
||||||
|
customer_total: data.customer_total ? Number(data.customer_total) : null,
|
||||||
|
duckil_production: data.duckil_production ? Number(data.duckil_production) : null,
|
||||||
|
first_quarter: data.first_quarter ? Number(data.first_quarter) : null,
|
||||||
|
second_quarter: data.second_quarter ? Number(data.second_quarter) : null,
|
||||||
|
third_quarter: data.third_quarter ? Number(data.third_quarter) : null,
|
||||||
|
fourth_quarter: data.fourth_quarter ? Number(data.fourth_quarter) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ContractService();
|
module.exports = new ContractService();
|
||||||
|
@ -422,29 +422,6 @@ const CommonCodePage = () => {
|
|||||||
<div>{new Date(row.original.createdAt).toLocaleDateString()}</div>
|
<div>{new Date(row.original.createdAt).toLocaleDateString()}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "isActive",
|
|
||||||
accessorKey: "isActive",
|
|
||||||
header: "활성화",
|
|
||||||
meta: {
|
|
||||||
width: "200px",
|
|
||||||
textAlign: "center"
|
|
||||||
},
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="text-center">
|
|
||||||
<Switch
|
|
||||||
checked={row.original.isActive}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
toggleActiveMutation.mutate({
|
|
||||||
id: row.original.id,
|
|
||||||
isActive: checked
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "addSub",
|
id: "addSub",
|
||||||
header: "하위요소",
|
header: "하위요소",
|
||||||
@ -468,6 +445,29 @@ const CommonCodePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "isActive",
|
||||||
|
accessorKey: "isActive",
|
||||||
|
header: "활성화",
|
||||||
|
meta: {
|
||||||
|
width: "200px",
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
<Switch
|
||||||
|
checked={row.original.isActive}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
toggleActiveMutation.mutate({
|
||||||
|
id: row.original.id,
|
||||||
|
isActive: checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "삭제",
|
header: "삭제",
|
||||||
|
@ -373,27 +373,6 @@ const MenusPage = () => {
|
|||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "menu_type",
|
|
||||||
accessorKey: "menu_type",
|
|
||||||
header: "타입",
|
|
||||||
meta: {
|
|
||||||
width: "200px",
|
|
||||||
textAlign: "center"
|
|
||||||
},
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="text-center">
|
|
||||||
<span className={cn(
|
|
||||||
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
|
||||||
row.original.menu_type === "0"
|
|
||||||
? "bg-blue-50 text-blue-700 ring-blue-700/10"
|
|
||||||
: "bg-green-50 text-green-700 ring-green-600/20"
|
|
||||||
)}>
|
|
||||||
{row.original.menu_type === "0" ? "관리자 메뉴" : "사용자 메뉴"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "addSub",
|
id: "addSub",
|
||||||
header: "하위요소",
|
header: "하위요소",
|
||||||
@ -417,6 +396,27 @@ const MenusPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "menu_type",
|
||||||
|
accessorKey: "menu_type",
|
||||||
|
header: "타입",
|
||||||
|
meta: {
|
||||||
|
width: "200px",
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
<span className={cn(
|
||||||
|
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||||
|
row.original.menu_type === "0"
|
||||||
|
? "bg-blue-50 text-blue-700 ring-blue-700/10"
|
||||||
|
: "bg-green-50 text-green-700 ring-green-600/20"
|
||||||
|
)}>
|
||||||
|
{row.original.menu_type === "0" ? "관리자 메뉴" : "사용자 메뉴"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "isActive",
|
id: "isActive",
|
||||||
accessorKey: "isActive",
|
accessorKey: "isActive",
|
||||||
|
@ -13,6 +13,8 @@ import {
|
|||||||
type OnChangeFn,
|
type OnChangeFn,
|
||||||
type Row,
|
type Row,
|
||||||
type TableOptions,
|
type TableOptions,
|
||||||
|
type GroupingColumnDef,
|
||||||
|
type Cell,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { ChevronRight, ChevronDown } from "lucide-react";
|
import { ChevronRight, ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
@ -44,10 +46,79 @@ interface PLMTableProps<T extends BaseRow> {
|
|||||||
onCheckedChange?: (checkedRows: T[]) => void;
|
onCheckedChange?: (checkedRows: T[]) => void;
|
||||||
onPageSizeChange?: (newPageSize: number) => void; // 새로운 prop 추가
|
onPageSizeChange?: (newPageSize: number) => void; // 새로운 prop 추가
|
||||||
isTreeTable?: boolean; // 트리 테이블 여부를 결정하는 prop 추가
|
isTreeTable?: boolean; // 트리 테이블 여부를 결정하는 prop 추가
|
||||||
|
renderExpandedRow?: (row: Row<T>) => React.ReactNode; // 확장된 행 렌더링을 위한 prop
|
||||||
|
enableExpander?: boolean; // 확장 기능 활성화 여부
|
||||||
|
treeConfig?: { // 트리 구조 설정
|
||||||
|
getParentId?: (row: T) => string | null;
|
||||||
|
indent?: number;
|
||||||
|
};
|
||||||
|
expanderConfig?: { // 행 확장 설정
|
||||||
|
renderExpandedContent?: (row: Row<T>) => React.ReactNode;
|
||||||
|
expandedContentStyle?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
onCellEdit?: (rowId: string, field: string, value: any) => Promise<void>;
|
||||||
|
editableColumns?: string[]; // 수정 가능한 컬럼 목록
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomExpandedState = Record<string | number, boolean>;
|
type CustomExpandedState = Record<string | number, boolean>;
|
||||||
|
|
||||||
|
// EditableCell 컴포넌트 추가
|
||||||
|
const EditableCell = <T extends BaseRow>({
|
||||||
|
cell,
|
||||||
|
isEditable,
|
||||||
|
onCellEdit,
|
||||||
|
}: {
|
||||||
|
cell: Cell<T, unknown>;
|
||||||
|
isEditable?: boolean;
|
||||||
|
onCellEdit?: (rowId: string, field: string, value: any) => Promise<void>;
|
||||||
|
}) => {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editValue, setEditValue] = useState<string>('');
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
if (!isEditable) return;
|
||||||
|
setIsEditing(true);
|
||||||
|
setEditValue(String(cell.getValue() || ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (onCellEdit) {
|
||||||
|
try {
|
||||||
|
await onCellEdit(cell.row.original.id, String(cell.column.id), editValue);
|
||||||
|
setIsEditing(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update cell:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isEditing]);
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className="w-full px-2 py-1 border rounded"
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
|
onBlur={handleSave}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSave()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onDoubleClick={handleDoubleClick}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function PLMTable<T extends BaseRow>({
|
export function PLMTable<T extends BaseRow>({
|
||||||
columns: userColumns,
|
columns: userColumns,
|
||||||
data,
|
data,
|
||||||
@ -57,6 +128,12 @@ export function PLMTable<T extends BaseRow>({
|
|||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
isTreeTable = false, // 기본값은 false
|
isTreeTable = false, // 기본값은 false
|
||||||
|
renderExpandedRow,
|
||||||
|
enableExpander = false,
|
||||||
|
treeConfig,
|
||||||
|
expanderConfig,
|
||||||
|
onCellEdit,
|
||||||
|
editableColumns,
|
||||||
}: PLMTableProps<T>) {
|
}: PLMTableProps<T>) {
|
||||||
const [expanded, setExpanded] = useState<CustomExpandedState>({});
|
const [expanded, setExpanded] = useState<CustomExpandedState>({});
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
@ -218,7 +295,7 @@ export function PLMTable<T extends BaseRow>({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 p-0"
|
className={`h-8 w-8 p-0 ${row.depth > 0 ? 'ml-4' : ''}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
row.toggleExpanded();
|
row.toggleExpanded();
|
||||||
@ -236,11 +313,39 @@ export function PLMTable<T extends BaseRow>({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 확장/축소 버튼 컬럼 - enableExpander가 true일 때만 추가
|
||||||
|
if (enableExpander) {
|
||||||
|
result.push({
|
||||||
|
id: 'expander',
|
||||||
|
meta: { width: "40px" },
|
||||||
|
header: () => null,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
row.toggleExpanded();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.getIsExpanded() ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 나머지 컬럼들 추가
|
// 나머지 컬럼들 추가
|
||||||
result.push(...userColumns);
|
result.push(...userColumns);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [userColumns, enableCheckbox, onCheckedChange, isTreeTable]);
|
}, [userColumns, enableCheckbox, onCheckedChange, isTreeTable, enableExpander]);
|
||||||
|
|
||||||
const handleExpandedChange: OnChangeFn<ExpandedState> = useCallback((updater) => {
|
const handleExpandedChange: OnChangeFn<ExpandedState> = useCallback((updater) => {
|
||||||
setExpanded(old => {
|
setExpanded(old => {
|
||||||
@ -307,77 +412,245 @@ export function PLMTable<T extends BaseRow>({
|
|||||||
onPageSizeChange(newPageSize);
|
onPageSizeChange(newPageSize);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const getHeaderDepth = (columns: GroupingColumnDef<T, any>[]): number => {
|
||||||
|
let maxDepth = 1;
|
||||||
|
for (const column of columns) {
|
||||||
|
if ('columns' in column && Array.isArray(column.columns)) {
|
||||||
|
const subDepth = getHeaderDepth(column.columns as GroupingColumnDef<T, any>[]) + 1;
|
||||||
|
maxDepth = Math.max(maxDepth, subDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxDepth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRenderExpandedRow = (row: Row<T>) => {
|
||||||
|
const item = row.original;
|
||||||
|
return (
|
||||||
|
<div className="pl-12 pr-4 py-2 bg-gray-50">
|
||||||
|
<table className="min-w-full border-separate border-spacing-0 text-xs">
|
||||||
|
{Object.entries(item).map(([key, value]) => {
|
||||||
|
// id나 기본 표시되는 컬럼들은 제외
|
||||||
|
if (key === 'id' || columns.some(col => 'accessorKey' in col && col.accessorKey === key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr key={key}>
|
||||||
|
<th className="w-48 py-1 px-2 text-left font-medium text-gray-700">{key}</th>
|
||||||
|
<td className="py-1 px-2">{String(value)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 트리 구조 관련 로직
|
||||||
|
const renderTreeCell = useCallback((row: Row<T>) => {
|
||||||
|
if (!isTreeTable || !treeConfig) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
style={{ paddingLeft: `${(row.depth || 0) * (treeConfig.indent || 24)}px` }}
|
||||||
|
>
|
||||||
|
{/* 트리 라인 렌더링 */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [isTreeTable, treeConfig]);
|
||||||
|
|
||||||
|
// 행 확장 관련 로직
|
||||||
|
const renderExpandedRowContent = useCallback((row: Row<T>) => {
|
||||||
|
if (!enableExpander || !expanderConfig?.renderExpandedContent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={expanderConfig.expandedContentStyle}>
|
||||||
|
{expanderConfig.renderExpandedContent(row)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [enableExpander, expanderConfig]);
|
||||||
|
|
||||||
|
// renderCell 함수를 컴포넌트로 교체
|
||||||
|
const renderCell = useCallback((cell: Cell<T, unknown>) => {
|
||||||
|
const isEditable = editableColumns?.includes(String(cell.column.id));
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
cell={cell}
|
||||||
|
isEditable={isEditable}
|
||||||
|
onCellEdit={onCellEdit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [editableColumns, onCellEdit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="relative overflow-y-auto overflow-x-auto bg-white border border-gray-200 rounded-lg shadow-sm"
|
className="relative border border-gray-200 rounded-lg shadow-sm"
|
||||||
style={{ height: "600px" }}
|
style={{ height: "600px" }}
|
||||||
>
|
>
|
||||||
<table className="w-full border-separate border-spacing-0 text-sm leading-normal text-gray-700 table-fixed">
|
<div className="absolute top-0 left-0 right-0 bottom-0 overflow-auto">
|
||||||
<thead>
|
<table className="min-w-full border-separate border-spacing-0 text-[13px]"> {/* text-xs에서 text-[13px]로 변경 */}
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<thead className="sticky top-0 z-10 bg-gray-100">
|
||||||
<tr key={headerGroup.id}>
|
{table.getHeaderGroups().map((headerGroup, groupIndex) => {
|
||||||
{headerGroup.headers.map((header) => (
|
// 헤더 그룹의 전체 개수
|
||||||
<th
|
const totalHeaderGroups = table.getHeaderGroups().length;
|
||||||
key={header.id}
|
|
||||||
className="sticky top-0 z-10 bg-gray-50 text-center py-3 px-4 font-semibold text-gray-900 border-b border-gray-200 whitespace-nowrap overflow-hidden text-ellipsis"
|
return (
|
||||||
style={{ width: header.column.columnDef.meta?.width }}
|
<tr key={headerGroup.id} className="bg-gray-50">
|
||||||
>
|
{headerGroup.headers.map((header) => {
|
||||||
{flexRender(
|
const isUtilityColumn = header.column.id === 'select' || header.column.id === 'expander';
|
||||||
header.column.columnDef.header,
|
const isNestedHeader = header.column.columns?.length > 0;
|
||||||
header.getContext()
|
|
||||||
)}
|
// 유틸리티 컬럼(체크박스, expander)은 첫 번째 행에서만 표시하고 전체 높이로 설정
|
||||||
</th>
|
if (groupIndex > 0 && isUtilityColumn) {
|
||||||
))}
|
return null;
|
||||||
</tr>
|
}
|
||||||
))}
|
|
||||||
</thead>
|
// 부모 컬럼이 없는 일반 컬럼은 첫 번째 행에서만 표시하고 전체 높이로 설정
|
||||||
<tbody>
|
if (groupIndex > 0 && !header.column.parent && !isNestedHeader) {
|
||||||
{table.getRowModel().rows.map((row) => (
|
return null;
|
||||||
<tr
|
}
|
||||||
key={row.id}
|
|
||||||
className={`transition-colors hover:bg-gray-50`}
|
// 중첩된 컬럼의 자식은 마지막 행에서만 표시
|
||||||
>
|
if (groupIndex < totalHeaderGroups - 1 && !isNestedHeader && header.column.parent) {
|
||||||
{row.getVisibleCells().map((cell) => {
|
return null;
|
||||||
const isFirstCell = cell.column.id === 'expander';
|
}
|
||||||
return (
|
|
||||||
<td
|
// rowSpan 계산
|
||||||
key={cell.id}
|
let rowSpan = 1;
|
||||||
className={`py-3 px-4 border-b border-gray-200 overflow-visible
|
if (isUtilityColumn || (!isNestedHeader && !header.column.parent)) {
|
||||||
${cell.column.columnDef.meta?.className || ""}
|
rowSpan = totalHeaderGroups; // 전체 높이
|
||||||
${isFirstCell ? 'relative' : ''}`}
|
} else if (!isNestedHeader && header.column.parent) {
|
||||||
style={{
|
rowSpan = 1; // 자식 컬럼은 한 줄
|
||||||
width: cell.column.columnDef.meta?.width,
|
} else if (isNestedHeader) {
|
||||||
maxWidth: cell.column.columnDef.meta?.width,
|
rowSpan = 1; // 그룹 헤더는 한 줄
|
||||||
textAlign: cell.column.columnDef.meta?.textAlign || 'left',
|
}
|
||||||
paddingLeft: isFirstCell ? `${row.depth * 24 + 16}px` : undefined,
|
|
||||||
}}
|
return (
|
||||||
>
|
<th
|
||||||
{isFirstCell && Array.from({ length: row.depth }).map((_, index) => (
|
key={header.id}
|
||||||
<span
|
colSpan={header.colSpan}
|
||||||
key={`line-${index}`}
|
rowSpan={rowSpan}
|
||||||
className="absolute h-full w-px bg-gray-200"
|
className={`
|
||||||
style={{
|
sticky top-0 z-10
|
||||||
left: `${(index + 1) * 24}px`,
|
text-center py-0.5 px-2
|
||||||
top: 0,
|
font-semibold text-gray-900
|
||||||
zIndex: 1
|
whitespace-nowrap overflow-hidden text-ellipsis
|
||||||
|
bg-gray-100
|
||||||
|
${isNestedHeader ? 'font-bold' : ''}
|
||||||
|
last:border-r-0
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
width: header.column.columnDef.meta?.width,
|
||||||
|
minWidth: header.column.columnDef.meta?.width,
|
||||||
|
height: '24px', // 더 작게 조정
|
||||||
|
borderBottom: '1px solid #D1D5DB',
|
||||||
|
borderRight: '1px solid #D1D5DB',
|
||||||
|
borderTop: groupIndex === 0 ? '1px solid #D1D5DB' : 'none',
|
||||||
|
borderLeft: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
))}
|
{flexRender(
|
||||||
<div className={isFirstCell ? 'relative z-10' : ''}>
|
header.column.columnDef.header,
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
header.getContext()
|
||||||
</div>
|
)}
|
||||||
</td>
|
</th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
</tbody>
|
})}
|
||||||
</table>
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-50">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<React.Fragment key={row.id}>
|
||||||
|
<tr
|
||||||
|
className="transition-colors hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell, cellIndex) => {
|
||||||
|
const isFirstCell = cell.column.id === 'expander';
|
||||||
|
const isSecondCell = cellIndex === 1; // expander 다음 셀 확인
|
||||||
|
const hasExpandButton = row.getCanExpand();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={`py-0.5 px-3 border-b border-gray-200
|
||||||
|
${cell.column.columnDef.meta?.className || ""}
|
||||||
|
${isFirstCell ? 'relative' : ''}
|
||||||
|
${isSecondCell && hasExpandButton ? 'border-l-0' : ''}
|
||||||
|
${!isFirstCell ? 'border-r border-gray-200' : ''}`}
|
||||||
|
style={{
|
||||||
|
width: cell.column.columnDef.meta?.width,
|
||||||
|
maxWidth: cell.column.columnDef.meta?.width,
|
||||||
|
textAlign: cell.column.columnDef.meta?.textAlign || 'left',
|
||||||
|
paddingLeft: isFirstCell ? `${row.depth * 24 + 12}px` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFirstCell && row.depth > 0 && (
|
||||||
|
<>
|
||||||
|
{/* 수직 라인 */}
|
||||||
|
<span
|
||||||
|
className="absolute h-full w-px bg-gray-200"
|
||||||
|
style={{
|
||||||
|
left: '24px',
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 수평 라인 */}
|
||||||
|
<span
|
||||||
|
className="absolute w-4 h-px bg-gray-200"
|
||||||
|
style={{
|
||||||
|
left: '24px',
|
||||||
|
top: '50%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className={`relative ${isFirstCell ? 'z-10' : ''}`}>
|
||||||
|
{renderCell(cell)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
{/* 확장된 행 렌더링 */}
|
||||||
|
{row.getIsExpanded() && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={row.getVisibleCells().length}
|
||||||
|
className="border-b border-gray-200 bg-transparent"
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
{/* 왼쪽 세로 선 */}
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-0 bottom-0 w-[1px] bg-gray-200"
|
||||||
|
style={{
|
||||||
|
left: `${row.depth * 24 + 24}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 확장된 내용을 선보다 오른쪽으로 이동 */}
|
||||||
|
<div
|
||||||
|
className="ml-[48px] py-2"
|
||||||
|
style={{
|
||||||
|
width: 'calc(100% - 64px)', // 전체 너비에서 왼쪽 여백(48px)과 오른쪽 여백(16px)을 뺌
|
||||||
|
marginRight: '16px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderExpandedRowContent(row)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-3 text-sm text-gray-700">
|
<div className="flex items-center justify-between py-3 text-[13px] text-gray-700"> {/* text-sm에서 text-[13px]로 변경 */}
|
||||||
<div className="w-1/3 flex items-center gap-2">
|
<div className="w-1/3 flex items-center gap-2">
|
||||||
<span>페이지당 행:</span>
|
<span>페이지당 행:</span>
|
||||||
<select
|
<select
|
||||||
|
Loading…
Reference in New Issue
Block a user