Browse Source

엑셀1차

home 5 months ago
parent
commit
2a81164160
4 changed files with 203 additions and 2 deletions
  1. 2 1
      package.json
  2. BIN
      public/공사계약.xlsx
  3. 1 1
      src/data/menuItems.ts
  4. 200 0
      src/pages/score/Sheet.tsx

+ 2 - 1
package.json

@@ -16,7 +16,8 @@
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "react-router-dom": "^7.6.2",
-    "tailwindcss": "^4.1.7"
+    "tailwindcss": "^4.1.7",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@types/navermaps": "^3.9.1",

BIN
public/공사계약.xlsx


+ 1 - 1
src/data/menuItems.ts

@@ -53,7 +53,7 @@ const menuItems: MenuItem[] = [
         description: "반도산전의 주요 실적입니다.",
         children: [
             { label: "인증서 및 면허", href: "certification" },
-            { label: "주요실적", href: "notice" }
+            { label: "주요실적", href: "sheet" }
         ]
     }
 ];

+ 200 - 0
src/pages/score/Sheet.tsx

@@ -0,0 +1,200 @@
+import React, {useEffect, useState} from "react";
+import * as XLSX from "xlsx";
+import Tabs, {TabItem} from "@/components/common/Tabs";
+
+// 📁 엑셀 파일 가져오기
+const fetchExcelFile = async (url: string): Promise<ArrayBuffer> => {
+  const response = await fetch(encodeURI(url));
+  if (!response.ok) throw new Error("엑셀 파일 로드 실패");
+  return await response.arrayBuffer();
+};
+
+// 📁 워크북 파싱
+const parseWorkbook = (buffer: ArrayBuffer): XLSX.WorkBook => {
+  return XLSX.read(buffer, {type: "array", cellStyles: true});
+};
+
+// 🎨 강조 셀 판단
+const isHighlightedCell = (cell: XLSX.CellObject | undefined): boolean => {
+  const fill = (cell as any)?.s?.fill;
+  const fg = fill?.fgColor?.rgb || fill?.bgColor?.rgb;
+  return fg?.toLowerCase()?.includes("ffff00"); // 노란색
+};
+
+// 📄 시트 → JSON 변환 (강조 포함)
+const convertSheetToJson = (sheet: XLSX.WorkSheet): any[] => {
+  const range = XLSX.utils.decode_range(sheet["!ref"]!);
+  const rows: any[] = [];
+
+  let headerRowIndex = -1;
+  let headers: string[] = [];
+
+  // 실제 헤더 행 탐색
+  for (let R = range.s.r; R <= range.e.r; ++R) {
+    const rowValues: string[] = [];
+
+    for (let C = range.s.c; C <= range.e.c; ++C) {
+      const cellAddress = XLSX.utils.encode_cell({r: R, c: C});
+      const cell = sheet[cellAddress];
+      const value = cell?.v?.toString().trim() || "";
+
+      rowValues.push(value);
+    }
+
+    // '발주처' 같은 주요 컬럼이 있으면 헤더로 판단
+    if (rowValues.some((v) => ["발주처", "공사명", "계약금액", "계약일"].includes(v))) {
+      // 헤더 처리 시 공백 제거
+      headers = rowValues.map((v) => v.replace(/\s+/g, ""));
+      headerRowIndex = R;
+      break;
+    }
+  }
+
+  if (headerRowIndex === -1) return []; // 헤더 못 찾은 경우
+
+  // 본문 데이터 파싱
+  for (let R = headerRowIndex + 1; R <= range.e.r; ++R) {
+    const row: any = {};
+    let hasValue = false;
+    let highlight = false;
+
+    for (let C = range.s.c; C <= range.e.c; ++C) {
+      const cellAddress = XLSX.utils.encode_cell({r: R, c: C});
+      const cell = sheet[cellAddress];
+      const value = cell?.v ?? "";
+      const key = headers[C];
+
+      if (key) {
+        row[key] = value;
+        if (value !== "") hasValue = true;
+        if (isHighlightedCell(cell)) highlight = true;
+      }
+    }
+
+    if (hasValue) {
+      row._highlight = highlight;
+      rows.push(row);
+    }
+  }
+
+  return rows;
+};
+
+// 📦 워크북 → 연도별 데이터 구조로 변환
+const extractDataFromWorkbook = (workbook: XLSX.WorkBook): Record<string, any[]> => {
+  const result: Record<string, any[]> = {};
+
+  workbook.SheetNames.forEach((sheetName) => {
+    const year = sheetName.replace("공사계약현황", "").trim();
+    const sheet = workbook.Sheets[sheetName];
+    result[year] = convertSheetToJson(sheet);
+  });
+
+  return result;
+};
+
+const Sheet: React.FC = () => {
+  const [dataByYear, setDataByYear] = useState<Record<string, any[]>>({});
+  const [activeTab, setActiveTab] = useState<string>("");
+
+  useEffect(() => {
+    const loadExcelData = async () => {
+      try {
+        const buffer = await fetchExcelFile("/공사계약.xlsx");
+        const workbook = parseWorkbook(buffer);
+        const parsedData = extractDataFromWorkbook(workbook);
+        setDataByYear(parsedData);
+        console.log(parsedData);
+        setActiveTab(Object.keys(parsedData)[0]);
+      } catch (err) {
+        console.error("❌ 엑셀 데이터 처리 중 오류:", err);
+      }
+    };
+
+    loadExcelData();
+  }, []);
+
+  const tabItems: TabItem[] = Object.keys(dataByYear)
+    .map((key) => {
+      const year = key.match(/\d{4}/)?.[0] ?? key; // 4자리 숫자만 추출
+      return {
+        id: year,
+        label: year,
+      };
+    });
+
+  const filteredRows = dataByYear[activeTab]
+    ?.filter((row) => !String(row["공사명"] || "")
+      .replace(/\s/g, "")
+      .includes("소계")
+    ) ?? [];
+
+  const totalAmount = filteredRows.reduce((sum, row) => {
+    const value = Number(row["계약금액"]);
+    return sum + (isNaN(value) ? 0 : value);
+  }, 0);
+
+  return (
+    <div className="max-w-7xl mx-auto px-4 py-10">
+      {activeTab && (
+        <Tabs items={tabItems} activeTab={activeTab} onTabChange={setActiveTab}>
+          <div className="text-center mb-6">
+            <div className="inline-block font-bold underline text-xl tracking-widest">
+              공 사 계 약 현 황 ({activeTab})
+            </div>
+          </div>
+          <div className="overflow-y-scroll max-h-[600px]">
+            <table className="min-w-full border border-gray-300 text-sm table-fixed">
+            <thead>
+            <tr className="bg-gray-100 text-center sticky top-0 z-10">
+              <th className="border px-2 py-2 w-[10%] bg-gray-100">발주처</th>
+              <th className="border px-2 py-2 w-[40%] bg-gray-100">공사명</th>
+              <th className="border px-2 py-2 w-[15%] bg-gray-100">계약금액</th>
+              <th className="border px-2 py-2 w-[11%] bg-gray-100">계약일</th>
+              <th className="border px-2 py-2 w-[11%] bg-gray-100">착공일</th>
+              <th className="border px-2 py-2 w-[13%] bg-gray-100">준공일</th>
+            </tr>
+            </thead>
+            <tbody>
+            {filteredRows.length > 0 ? (
+              filteredRows.slice(0, -1).map((row: any, index: number) => (
+                <tr key={index} className={row._highlight ? "bg-yellow-200" : ""}>
+                  <td className="border px-2 py-1 w-[10%] truncate font-bold">{row["발주처"]}</td>
+                  <td className="border px-2 py-1 w-[40%] truncate">{row["공사명"]}</td>
+                  <td className="border px-2 py-1 w-[15%] text-right">
+                    {typeof row["계약금액"] === "number"
+                      ? new Intl.NumberFormat("ko-KR").format(row["계약금액"])
+                      : row["계약금액"]}
+                  </td>
+                  <td className="border px-2 py-1 w-[11%] text-center">{row["계약일"]}</td>
+                  <td className="border px-2 py-1 w-[11%] text-center">{row["착공일"]}</td>
+                  <td className="border px-2 py-1 w-[13%] text-center">{row["준공일"]}</td>
+                </tr>
+              ))
+            ) : (
+              <tr>
+                <td className="border px-4 py-2 text-center" colSpan={6}>
+                  데이터가 없습니다.
+                </td>
+              </tr>
+            )}
+            </tbody>
+            <tfoot>
+            <tr className="bg-gray-100 font-bold text-center">
+              <td className="border px-2 py-1"/>
+              <td className="border px-2 py-1">합계</td>
+              <td className="border px-2 py-1 text-right">
+                {new Intl.NumberFormat("ko-KR").format(totalAmount)}
+              </td>
+              <td className="border px-2 py-1" colSpan={3}/>
+            </tr>
+            </tfoot>
+          </table>
+          </div>
+        </Tabs>
+      )}
+    </div>
+  );
+};
+
+export default Sheet;