From 5e778d30522f6e6ee90c42e834abdb8113d3880c Mon Sep 17 00:00:00 2001
From: pgb <gbpark@gdnsi.com>
Date: Thu, 16 Jan 2025 17:45:54 +0900
Subject: [PATCH] =?UTF-8?q?=EC=B6=A9=EB=8F=8C=ED=95=B4=EA=B2=B0=20?=
 =?UTF-8?q?=EB=B0=8F=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20=EC=BB=A4?=
 =?UTF-8?q?=EB=B0=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 plm-api/src/routes/app.js                     |   2 +
 plm-api/src/services/contract.service.js      | 114 ++++-
 .../common/codeCategoryMngList/page.tsx       |  46 +-
 plm-app/src/app/(admin)/common/menu/page.tsx  |  42 +-
 plm-app/src/components/common/Table/index.tsx | 401 +++++++++++++++---
 5 files changed, 496 insertions(+), 109 deletions(-)

diff --git a/plm-api/src/routes/app.js b/plm-api/src/routes/app.js
index 7dbc428..d95b427 100644
--- a/plm-api/src/routes/app.js
+++ b/plm-api/src/routes/app.js
@@ -21,6 +21,7 @@ const oemMngController = require("../controllers/app/common/oemmng.controller");
 const productgroupController = require("../controllers/app/common/productgroup.controller");
 const productController = require("../controllers/app/common/product.controller");
 const carMngController = require("../controllers/app/common/carmng.controller");
+const contractController = require("../controllers/app/contract/contract.controller");
 
 router.use("/health", healthController);
 router.use("/auth", authController);
@@ -41,5 +42,6 @@ router.use("/oemmng", oemMngController);
 router.use("/productgroup", productgroupController);
 router.use("/product", productController);
 router.use("/carmng", carMngController);
+router.use("/contract", contractController);
 
 module.exports = router;
diff --git a/plm-api/src/services/contract.service.js b/plm-api/src/services/contract.service.js
index a5f5822..81f5624 100644
--- a/plm-api/src/services/contract.service.js
+++ b/plm-api/src/services/contract.service.js
@@ -1,5 +1,5 @@
 // src/services/contract.service.js
-const { Contract, ContractDetail, Company } = require("../models");
+const { Contract, ContractDetail, Company, ContractMgmt} = require("../models");
 const alertService = require("./alert.service");
 const { Op } = require("sequelize");
 // 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();
diff --git a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx
index b463702..50c875d 100644
--- a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx
+++ b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx
@@ -422,29 +422,6 @@ const CommonCodePage = () => {
         <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",
       header: "하위요소",
@@ -468,6 +445,29 @@ const CommonCodePage = () => {
         </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",
       header: "삭제",
diff --git a/plm-app/src/app/(admin)/common/menu/page.tsx b/plm-app/src/app/(admin)/common/menu/page.tsx
index 83fb641..f380dec 100644
--- a/plm-app/src/app/(admin)/common/menu/page.tsx
+++ b/plm-app/src/app/(admin)/common/menu/page.tsx
@@ -373,27 +373,6 @@ const MenusPage = () => {
         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",
       header: "하위요소",
@@ -417,6 +396,27 @@ const MenusPage = () => {
         </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",
       accessorKey: "isActive",
diff --git a/plm-app/src/components/common/Table/index.tsx b/plm-app/src/components/common/Table/index.tsx
index 70c2681..5e2add6 100644
--- a/plm-app/src/components/common/Table/index.tsx
+++ b/plm-app/src/components/common/Table/index.tsx
@@ -13,6 +13,8 @@ import {
   type OnChangeFn,
   type Row,
   type TableOptions,
+  type GroupingColumnDef,
+  type Cell,
 } from "@tanstack/react-table";
 import { ChevronRight, ChevronDown } from "lucide-react";
 
@@ -44,10 +46,79 @@ interface PLMTableProps<T extends BaseRow> {
   onCheckedChange?: (checkedRows: T[]) => void;
   onPageSizeChange?: (newPageSize: number) => void; // 새로운 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>;
 
+// 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>({
   columns: userColumns,
   data,
@@ -57,6 +128,12 @@ export function PLMTable<T extends BaseRow>({
   onCheckedChange,
   onPageSizeChange,
   isTreeTable = false, // 기본값은 false
+  renderExpandedRow,
+  enableExpander = false,
+  treeConfig,
+  expanderConfig,
+  onCellEdit,
+  editableColumns,
 }: PLMTableProps<T>) {
   const [expanded, setExpanded] = useState<CustomExpandedState>({});
   const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
@@ -218,7 +295,7 @@ export function PLMTable<T extends BaseRow>({
               <Button
                 variant="ghost"
                 size="icon"
-                className="h-8 w-8 p-0"
+                className={`h-8 w-8 p-0 ${row.depth > 0 ? 'ml-4' : ''}`}
                 onClick={(e) => {
                   e.stopPropagation();
                   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);
     
     return result;
-  }, [userColumns, enableCheckbox, onCheckedChange, isTreeTable]);
+  }, [userColumns, enableCheckbox, onCheckedChange, isTreeTable, enableExpander]);
 
   const handleExpandedChange: OnChangeFn<ExpandedState> = useCallback((updater) => {
     setExpanded(old => {
@@ -307,77 +412,245 @@ export function PLMTable<T extends BaseRow>({
       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 (
     <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" }}
       >
-        <table className="w-full border-separate border-spacing-0 text-sm leading-normal text-gray-700 table-fixed">
-          <thead>
-            {table.getHeaderGroups().map((headerGroup) => (
-              <tr key={headerGroup.id}>
-                {headerGroup.headers.map((header) => (
-                  <th
-                    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"
-                    style={{ width: header.column.columnDef.meta?.width }}
-                  >
-                    {flexRender(
-                      header.column.columnDef.header,
-                      header.getContext()
-                    )}
-                  </th>
-                ))}
-              </tr>
-            ))}
-          </thead>
-          <tbody>
-            {table.getRowModel().rows.map((row) => (
-              <tr
-                key={row.id}
-                className={`transition-colors hover:bg-gray-50`}
-              >
-                {row.getVisibleCells().map((cell) => {
-                  const isFirstCell = cell.column.id === 'expander';
-                  return (
-                    <td
-                      key={cell.id}
-                      className={`py-3 px-4 border-b border-gray-200 overflow-visible
-                        ${cell.column.columnDef.meta?.className || ""}
-                        ${isFirstCell ? 'relative' : ''}`}
-                      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 + 16}px` : undefined,
-                      }}
-                    >
-                      {isFirstCell && Array.from({ length: row.depth }).map((_, index) => (
-                        <span
-                          key={`line-${index}`}
-                          className="absolute h-full w-px bg-gray-200"
-                          style={{
-                            left: `${(index + 1) * 24}px`,
-                            top: 0,
-                            zIndex: 1
+        <div className="absolute top-0 left-0 right-0 bottom-0 overflow-auto">
+          <table className="min-w-full border-separate border-spacing-0 text-[13px]"> {/* text-xs에서 text-[13px]로 변경 */}
+            <thead className="sticky top-0 z-10 bg-gray-100">
+              {table.getHeaderGroups().map((headerGroup, groupIndex) => {
+                // 헤더 그룹의 전체 개수
+                const totalHeaderGroups = table.getHeaderGroups().length;
+                
+                return (
+                  <tr key={headerGroup.id} className="bg-gray-50">
+                    {headerGroup.headers.map((header) => {
+                      const isUtilityColumn = header.column.id === 'select' || header.column.id === 'expander';
+                      const isNestedHeader = header.column.columns?.length > 0;
+                      
+                      // 유틸리티 컬럼(체크박스, expander)은 첫 번째 행에서만 표시하고 전체 높이로 설정
+                      if (groupIndex > 0 && isUtilityColumn) {
+                        return null;
+                      }
+
+                      // 부모 컬럼이 없는 일반 컬럼은 첫 번째 행에서만 표시하고 전체 높이로 설정
+                      if (groupIndex > 0 && !header.column.parent && !isNestedHeader) {
+                        return null;
+                      }
+
+                      // 중첩된 컬럼의 자식은 마지막 행에서만 표시
+                      if (groupIndex < totalHeaderGroups - 1 && !isNestedHeader && header.column.parent) {
+                        return null;
+                      }
+
+                      // rowSpan 계산
+                      let rowSpan = 1;
+                      if (isUtilityColumn || (!isNestedHeader && !header.column.parent)) {
+                        rowSpan = totalHeaderGroups; // 전체 높이
+                      } else if (!isNestedHeader && header.column.parent) {
+                        rowSpan = 1; // 자식 컬럼은 한 줄
+                      } else if (isNestedHeader) {
+                        rowSpan = 1; // 그룹 헤더는 한 줄
+                      }
+
+                      return (
+                        <th
+                          key={header.id}
+                          colSpan={header.colSpan}
+                          rowSpan={rowSpan}
+                          className={`
+                            sticky top-0 z-10 
+                            text-center py-0.5 px-2
+                            font-semibold text-gray-900 
+                            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',
                           }}
-                        />
-                      ))}
-                      <div className={isFirstCell ? 'relative z-10' : ''}>
-                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
-                      </div>
-                    </td>
-                  );
-                })}
-              </tr>
-            ))}
-          </tbody>
-        </table>
+                        >
+                          {flexRender(
+                            header.column.columnDef.header,
+                            header.getContext()
+                          )}
+                        </th>
+                      );
+                    })}
+                  </tr>
+                );
+              })}
+            </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 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">
           <span>페이지당 행:</span>
           <select