Merge commit '7c7cc47d20c3a7b440ba3bb576407605b29233ca' into iipbs/sn/oi

This commit is contained in:
JIeunhuh
2025-05-20 09:34:20 +09:00
11 changed files with 575 additions and 251 deletions
@@ -44,6 +44,7 @@
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
"xlsx": "^0.18.5",
"xlsx-populate": "^1.21.0",
"zod": "^3.24.2"
},
"devDependencies": {
@@ -59,6 +60,7 @@
"@storybook/test": "^8.5.3",
"@storybook/theming": "^8.5.5",
"@vitejs/plugin-react": "^4.3.4",
"@xlsx/xlsx-populate": "^0.0.8",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
@@ -1,42 +1,127 @@
"use client"
import type { CSSProperties } from "react";
import {
Card,
CardContent
} from "./lib/card";
import { Bar, BarChart, CartesianGrid, Legend, XAxis, YAxis } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig
} from "./lib/chart";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import type { CSSProperties } from "react";
export default function Chart(props: { chartConfig: ChartConfig; chartData: any[] | undefined; chartTitle: string}) {
const customStyles = {
legendContainer: {
display: "flex",
justifyContent: "center",
gap: "20px",
fontSize: "9px",
fontWeight : "bold",
marginTop: "10px",
} as CSSProperties,
};
type BarChartProps = {
chartConfig: ChartConfig | undefined;
chartData: any[] | undefined;
chartTitle: string;
chartClassName?: string;
graphSize?: number;
yFieldText?: string;
showLegend?: boolean;
}
// 커스텀 툴팁
const CustomTooltip = ({
active,
payload,
label
}: {
active?: boolean;
payload?: Array<{
name: string;
value: number;
color: string;
}>;
label?: string;
}) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span>
</div>
))}
</div>
);
}
return null;
};
// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가)
const CustomLegend = (props: any) => {
const { payload } = props;
return (
<Card className="h-full">
<CardContent className="h-full">
{props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? (
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
{props.chartTitle}
</div>): null}
<div className="h-[calc(100%-42px)] flex justify-center">
<ChartContainer config={props.chartConfig} className="h-full w-full">
<BarChart accessibilityLayer data={props.chartData} className="h-full">
<CartesianGrid vertical={false} />
<XAxis
dataKey="label"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="dashed" />}/>
<Bar dataKey="val" radius={10} />
</BarChart>
</ChartContainer>
<div style={customStyles.legendContainer}>
{payload.map((entry, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div>
<span className="text-[12px]">{entry.value}</span>
</div>
</CardContent>
</Card>
))}
</div>
);
};
export default function Chart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) {
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEEAD", "#D4A5A5"];
return (
<>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
<div className="!mt-3 text-[2.5rem] font-bold">
{chartTitle}
</div>): null}
<div className="flex justify-center">
<ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="label"
tickLine={true}
tickMargin={10}
axisLine={true}
fontSize={13}
fontWeight={500}
/>
<YAxis
tickLine={true}
tickMargin={10}
tickCount={6}
axisLine={true}
fontSize={12}
fontWeight={500}
label={yFieldText ? { value: yFieldText, className:"text-[13px] foont-bold", position: 'top', textAnchor: 'middle', offset: 20 } : undefined}
/>
<ChartTooltip cursor={false} content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />}/>
{chartData && chartData.length > 0 && (
Object.keys(chartData[0])
.filter(key => key !== "label")
.map((key, idx) => (
<Bar
dataKey={key}
radius={5}
key={key}
barSize={graphSize}
fill={colors[idx % colors.length]}
/>
))
)}
{showLegend && <Legend content={CustomLegend}/>}
</BarChart>
</ChartContainer>
</div>
</>
)
}
@@ -1,37 +1,15 @@
"use client"
import type { CSSProperties } from "react";
import { Bar, CartesianGrid, ComposedChart, Legend, Line, Tooltip, XAxis, YAxis } from "recharts";
import {
Card,
CardContent
} from "./lib/card";
import {
ChartContainer,
type ChartConfig
} from "./lib/chart";
import React from "react";
import type { ContentType } from "recharts/types/component/DefaultLegendContent";
// 툴팁 커스텀 css
/** 차트 기본 스타일 */
const styles = {
tooltipDiv: {
borderRadius: "5px",
backgroundColor: "white",
padding: "10px",
boxShadow: "0px 2px 10px rgba(0,0,0,0.2)",
width: "180px", // 적절한 크기로 설정
} as React.CSSProperties,
tooltipLabel: {
fontWeight: "bold",
fontSize: "12px",
marginBottom: "5px",
borderBottom: "1px solid #ddd",
paddingBottom: "3px",
} as React.CSSProperties,
tooltipText: {
fontSize: "11px",
fontWeight:"bold",
} as React.CSSProperties,
const customStyles = {
legendContainer: {
display: "flex",
justifyContent: "center",
@@ -42,17 +20,39 @@ const styles = {
} as CSSProperties,
};
type LineBarChartProps = {
chartConfig: ChartConfig | undefined;
chartData: any[] | undefined;
chartTitle: string;
chartClassName?: string;
graphSize?: number;
yFieldText?: string | undefined;
yFieldText2?: string | undefined;
showLegend?: boolean;
}
type LegendEntry = {
value: string;
color: string;
type?: string;
payload?: unknown;
};
type LegendProps = {
payload?: LegendEntry[];
};
// 커스텀 툴팁
const CustomTooltip = ({ active, payload, label }) => {
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => {
if (active && payload && payload.length) {
return (
<div style={styles.tooltipDiv}>
<p style={styles.tooltipLabel}>{label}</p>
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span style={styles.tooltipText}>{`${item.name}: ${item.value}`}</span>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span>
</div>
))}
</div>
@@ -62,47 +62,83 @@ const CustomTooltip = ({ active, payload, label }) => {
};
// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가)
const CustomLegend = (props: any) => {
const CustomLegend = (props: LegendProps) => {
const { payload } = props;
if (!payload) return null;
return (
<div style={styles.legendContainer}>
{payload.map((entry, index) => (
<div key={`legend-${index}`} style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<div style={{ width: "12px", height: "12px", backgroundColor: entry.color, borderRadius: "50%" }}></div>
<span>{entry.value}</span>
<div style={customStyles.legendContainer}>
{payload.map((entry: LegendEntry, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div>
<span className="text-[12px]">{entry.value}</span>
</div>
))}
</div>
);
};
export default function LineBarchart(props: { chartConfig: ChartConfig; chartData: any[] | undefined; chartTitle:string }) {
export default function LineBarchart({
chartConfig,
chartData,
chartTitle,
chartClassName = 'w-full h-full',
graphSize = 40,
yFieldText,
yFieldText2,
showLegend = true
}: LineBarChartProps) {
if (!chartData || chartData.length === 0) {
return <div>데이터가 없습니다.</div>;
}
const barConfig = chartConfig?.barVal;
const lineConfig = chartConfig?.lineVal;
return (
<Card className="h-full">
<CardContent className="h-full">
{props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? (
<div className={chartClassName}>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
{props.chartTitle}
</div>): null}
<div className="h-[calc(100%-42px)] flex justify-center">
<ChartContainer config={props.chartConfig} className="h-full w-full" >
<ComposedChart data={props.chartData} className="h-full ">
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="label" fontSize="8px" fontWeight="bold"/>
{/* 왼쪽 Y축: lineVal (정상) */}
<YAxis yAxisId="left" orientation="left" />
{/* 오른쪽 Y축: barVal (위에서 아래로 반전) */}
<YAxis yAxisId="right" orientation="right" reversed />
<Tooltip content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />} />
<Legend content={CustomLegend}/>
{/* 바 차트 (오른쪽 Y축 사용, 위에서 아래로) */}
<Bar yAxisId="right" dataKey="barVal" fill="#8884d8" name="강수량(mm/일)"/>
{/* 라인 차트 (왼쪽 Y축 사용) */}
<Line yAxisId="left" type="monotone" dataKey="lineVal" stroke="orange" strokeWidth={2} dot={{r: 0}} name="BOD(mg/L)"/>
</ComposedChart>
</ChartContainer>
{chartTitle}
</div>
</CardContent>
</Card>
)
) : null}
<div className="h-[calc(100%-42px)] flex justify-center">
<ChartContainer config={chartConfig ?? {}} className="h-full w-full">
<ComposedChart
data={chartData}
height={graphSize * 7.5}
width={graphSize * 12.5}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="label" fontSize="8px" fontWeight="bold"/>
{/* 왼쪽 Y축: lineVal */}
{yFieldText && <YAxis yAxisId="left" className="text-[12px] font-bold" orientation="left" label={{ value: yFieldText, angle: -90, position: 'insideLeft' }} />}
{/* 오른쪽 Y축: barVal */}
{yFieldText2 && <YAxis yAxisId="right" className="text-[12px] font-bold" fontWeight="bold" orientation="right" label={{ value: yFieldText2, angle: -90, position: 'insideRight' }} />}
<Tooltip content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />} />
{showLegend && <Legend content={CustomLegend as ContentType}/>}
{/* 바 차트 (오른쪽 Y축 사용) */}
<Bar
yAxisId="right"
dataKey="barVal"
fill={barConfig?.color || "black"}
name={String(barConfig?.label || "Y축")}
maxBarSize={80}
/>
{/* 라인 차트 (왼쪽 Y축 사용) */}
<Line
yAxisId="left"
type="monotone"
dataKey="lineVal"
stroke={lineConfig?.color || "black"}
strokeWidth={2}
dot={{r: 0}}
name={String(lineConfig?.label || "X축")}
/>
</ComposedChart>
</ChartContainer>
</div>
</div>
);
}
@@ -1,37 +1,24 @@
"use client"
import type { CSSProperties } from "react";
import {
Card,
CardContent
} from "./lib/card";
import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig
} from "./lib/chart";
import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
import type { ContentType } from "recharts/types/component/DefaultLegendContent";
type LineChartProps = {
chartConfig: ChartConfig | undefined;
chartData: ChartDataType | undefined;
chartTitle: string;
chartClassName?: string;
graphSize?: number;
yFieldText?: string;
showLegend?: boolean;
}
/** 차트 기본 스타일 */
const styles = {
tooltipDiv: {
borderRadius: "5px",
backgroundColor: "white",
padding: "10px",
boxShadow: "0px 2px 10px rgba(0,0,0,0.2)",
width: "180px", // 적절한 크기로 설정
} as React.CSSProperties,
tooltipLabel: {
fontWeight: "bold",
fontSize: "12px",
marginBottom: "5px",
borderBottom: "1px solid #ddd",
paddingBottom: "3px",
} as React.CSSProperties,
tooltipText: {
fontSize: "11px",
fontWeight:"bold",
} as React.CSSProperties,
const customStyles = {
legendContainer: {
display: "flex",
justifyContent: "center",
@@ -42,17 +29,28 @@ const styles = {
} as CSSProperties,
};
type LegendEntry = {
value: string;
color: string;
type?: string;
payload?: unknown;
};
type LegendProps = {
payload?: LegendEntry[];
};
// 커스텀 툴팁
const CustomTooltip = ({ active, payload, label }) => {
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => {
if (active && payload && payload.length) {
return (
<div style={styles.tooltipDiv}>
<p style={styles.tooltipLabel}>{label}</p>
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span style={styles.tooltipText}>{`${item.name}: ${item.value}`}</span>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span>
</div>
))}
</div>
@@ -62,60 +60,90 @@ const CustomTooltip = ({ active, payload, label }) => {
};
// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가)
const CustomLegend = (props: any) => {
const CustomLegend = (props: LegendProps) => {
const { payload } = props;
if (!payload) return null;
return (
<div style={styles.legendContainer}>
{payload.map((entry, index) => (
<div key={`legend-${index}`} style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<div style={{ width: "12px", height: "12px", backgroundColor: entry.color, borderRadius: "50%" }}></div>
<span>{entry.value}</span>
<div style={customStyles.legendContainer}>
{payload.map((entry: LegendEntry, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div>
<span className="text-[12px]">{entry.value}</span>
</div>
))}
</div>
);
};
type DataType = {label : string; val:number};
type DataType = {label: string; val: string};
type ChartDataType = {
chartData1: DataType[];
chartData2: DataType[];
[key: string]: DataType[];
};
export default function Linechart(props: { chartConfig: ChartConfig; chartData: ChartDataType; chartTitle:string}) {
const mergedData = Object.values(props.chartData).reduce((acc, dataset) => {
export default function Linechart({
chartConfig,
chartData,
chartTitle,
chartClassName = 'w-full h-full',
graphSize = 40,
yFieldText,
showLegend = true
}: LineChartProps) {
if(!chartData || Object.keys(chartData).length === 0){
return <div>데이터가 없습니다.</div>
}
// 데이터 병합 및 변환
const mergedData = Object.entries(chartData).reduce<Array<{[key: string]: string | number}>>((acc, [key, dataset]) => {
dataset.forEach((entry, index) => {
if (!acc[index]) {
acc[index] = { label: entry.label }; // label 추가
acc[index] = { label: entry.label };
}
const key = Object.keys(props.chartData).find(k => props.chartData[k] === dataset);
if (key) acc[index][key] = entry.val;
acc[index][key] = parseFloat(entry.val);
});
return acc;
}, [] as any[]);
}, []);
return (
<Card className="h-full">
<CardContent className="h-full">
{props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? (
<div className={chartClassName}>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
{props.chartTitle}
</div>): null}
<div className="h-[calc(100%-42px)] flex justify-center">
<ChartContainer config={props.chartConfig} className="h-full w-full">
<LineChart data={mergedData} className="h-full">
<CartesianGrid vertical={false} />
<XAxis dataKey="label" fontSize="8px" fontWeight="bold"/>
<YAxis yAxisId="left" orientation="left" />
<Tooltip content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />} />
<Legend content={CustomLegend}/>
{Object.keys(props.chartData).map((key, index) => (
<Line key={key} yAxisId="left" dataKey={key} type="linear" strokeWidth={2} dot={false} name={key} />
))}
</LineChart>
</ChartContainer>
{chartTitle}
</div>
</CardContent>
</Card>
)
) : null}
<div className="h-[calc(100%-42px)] flex justify-center">
<ChartContainer config={chartConfig ?? {}} className="h-full w-full">
<LineChart
data={mergedData}
height={graphSize * 7.5}
width={graphSize * 12.5}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid vertical={false} />
<XAxis dataKey="label" fontSize="8px" fontWeight="bold"/>
<YAxis yAxisId="left" className="text-[12px] font-bold" orientation="left" label={{ value: yFieldText, className:"text-[12px] foont-bold", angle: -90, position: 'insideLeft' }} />
<Tooltip content={<CustomTooltip />} />
{showLegend && <Legend content={CustomLegend as ContentType}/>}
{Object.keys(chartData).map((key) => {
const config = chartConfig?.[key];
return (
<Line
key={key}
yAxisId="left"
dataKey={key}
type="linear"
strokeWidth={2}
dot={false}
name={String(config?.label || "X축")}
stroke={config?.color}
/>
);
})}
</LineChart>
</ChartContainer>
</div>
</div>
);
}
@@ -18,15 +18,41 @@ const CHART_FORM = {
LINEBAR : 'linebar'
};
function MainChart(props: { chartForm: string; chartConfig: any; chartData: any; chartTitle:string; }) {
/**
* 차트 컴포넌트
* @param chartForm 차트 형태
* @param chartConfig 차트 설정
* @param chartData 차트 데이터
* @param chartTitle 차트 제목
* @param chartClassName 차트 클래스명
* @param graphSize 차트 크기
* @param yFieldText 차트 Y축 텍스트
* @param yFieldText2 차트 Y축 텍스트2(라인바 차트때 사용)
* @param showLegend 차트 범례 표시 여부
* @returns
*/
type MainChartProps = {
chartForm: string;
chartConfig: any;
chartData: any;
chartTitle: string;
chartClassName?: string;
graphSize?: number;
yFieldText? : string | undefined;
yFieldText2? : string | undefined;
showLegend? : boolean;
}
function MainChart(props: MainChartProps) {
if( props.chartForm == CHART_FORM.BAR ){
return (
<BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}>
<BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}>
</BarChart>
);
} else if( props.chartForm == CHART_FORM.LINE ){
return (
<LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}>
<LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}>
</LineChart>
);
} else if( props.chartForm == CHART_FORM.DN ){
@@ -36,7 +62,7 @@ function MainChart(props: { chartForm: string; chartConfig: any; chartData: any;
);
} else if (props.chartForm == CHART_FORM.LINEBAR){
return (
<LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}>
<LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}>
</LineBarchart>
);
}
@@ -0,0 +1,54 @@
import type { sheetParameter } from '@/index';
import * as XlsxPopulate from '@xlsx/xlsx-populate';
const makeCellStyle =(styleQ: XlsxPopulate.StyleQuery )=>{
styleQ.fill({ type: "solid", color: {rgb:"9bc1e7"} });
styleQ.borderEach( { style: "thin", color: { rgb: "FFFFFF" } } );
styleQ.wrapText(true);
// styleQ.fontColor({rgb:"FFFFFF"});
styleQ.fontSize(13);
styleQ.horizontalAlignment("center");
styleQ.verticalAlignment("center");
styleQ.bold(true);
}
const makeExcelFile = async(workbook: XlsxPopulate.Workbook,fileNm:string)=>{
const outputData = await workbook.output("array-buffer");
const blob = new Blob([outputData], { type: "application/octet-stream" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = fileNm+".xlsx";
link.click();
}
export const excelStyle = async (data: ArrayBuffer, range: string = "A1:V3", fileNm:string) => {
const workbook = await XlsxPopulate.Workbook.fromData(data);
const rangeCells = workbook.activeSheet(0)?.sheet(0)?.range(range);
console.log(rangeCells);
rangeCells?.style((styleQ)=>{
makeCellStyle(styleQ)
});
makeExcelFile(workbook, fileNm)
}
export const excelArrayStyle = async (data: ArrayBuffer, range: [typeof sheetParameter], fileNm:string) => {
const workbook = await XlsxPopulate.Workbook.fromData(data);
if(range){
range.forEach((item , index)=>{
const rangeCells = workbook.activeSheet(index)?.sheet(index)?.range(item.headerRange);
console.log(rangeCells);
rangeCells?.style((styleQ)=>{
makeCellStyle(styleQ)
});
} )
}
makeExcelFile(workbook, fileNm)
}
@@ -0,0 +1,156 @@
import * as XLSX from 'xlsx';
import { excelArrayStyle, excelStyle } from './excelStyle';
type ExcelDownloadProps = {
data: JSON | JSON[];
fileName: string;
sheetNameArray: string[];
headerRange: string;
}
export const downloadExcel = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"], headerRange='A1:V3'}: ExcelDownloadProps) => {
const worksheet = XLSX.utils.json_to_sheet(Array.isArray(data) ? data : [data]);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, sheetNameArray[0]);
excelStyle(XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }), headerRange, fileName);
};
export const filePicked=(oFile: File, headerNum:number=0):Promise<JSON | undefined> =>{
// Create A File Reader HTML5
const reader = new FileReader();
return new Promise<JSON | undefined>((resolve, reject) => {
// Ready The Event For When A File Gets Selected
reader.onload = function (e) {
const data = e.target?.result;
if (!data) {
console.error("Failed to read file: e.target.result is null");
resolve(undefined);
return;
}
//셀데이터에서 데이트 형식으로 입력하면 yyyymmdd 형식으로 변환
const wb = XLSX.read(data, { type: 'binary', cellDates: true, dateNF: "yyyymmdd" });
let jsonXls: JSON | undefined;
// Loop Over Each Sheet
wb.SheetNames.forEach(function (sheetName,index ) {
// Obtain The Current Row As CSV
// 일단 첫번째 시트만 읽을 수 있도록 수정
if(index ===0){
const sheet = wb.Sheets[sheetName];
if (sheet) {
jsonXls = XLSX.utils.sheet_to_json(sheet, { range: headerNum, raw:false }) as unknown as JSON;
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
}
});
resolve(jsonXls);
};
reader.onerror = function () {
console.error("Error reading file");
reject(undefined);
};
// Tell JS To Start Reading The File.. You could delay this if desired
reader.readAsBinaryString(oFile);
});
}
export const filePickedArray=(oFile: File, headerNum:number=0):Promise<JSON[] | undefined> =>{
// Create A File Reader HTML5
const reader = new FileReader();
return new Promise<JSON[] | undefined>((resolve, reject) => {
// Ready The Event For When A File Gets Selected
reader.onload = function (e) {
const data = e.target?.result;
if (!data) {
console.error("Failed to read file: e.target.result is null");
resolve(undefined);
return;
}
//셀데이터에서 데이트 형식으로 입력하면 yyyymmdd 형식으로 변환
const wb = XLSX.read(data, { type: 'binary', cellDates: true, dateNF: "yyyymmdd" });
const arrayXls: JSON[] = []
let jsonXls: JSON | undefined;
// Loop Over Each Sheet
wb.SheetNames.forEach(function (sheetName,index ) {
const sheet = wb.Sheets[sheetName];
if (sheet) {
console.log(sheet)
jsonXls = XLSX.utils.sheet_to_json(sheet, { range: headerNum, raw:false }) as unknown as JSON;
arrayXls.push(jsonXls)
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
});
resolve(arrayXls);
};
reader.onerror = function () {
console.error("Error reading file");
reject(undefined);
};
// Tell JS To Start Reading The File.. You could delay this if desired
reader.readAsBinaryString(oFile);
});
}
export const filePickedBlob=(oFile: ArrayBuffer, data:[][],fileNm:string, headerNum:number=0, headerRange='A1:V3'):Promise<JSON | undefined> =>{ // Create A File Reader HTML5
return new Promise<JSON | undefined>(() => {
// Ready The Event For When A File Gets Selected
const wb = XLSX.read(oFile, { type: 'binary' });
// Loop Over Each Sheet
wb.SheetNames.forEach(async function (sheetName, index) {
// Obtain The Current Row As CSV
if(index==0){
const sheet = wb.Sheets[sheetName];
if (sheet) {
//오리진 확인 test 필요
// const sheet = XLSX.utils.json_to_sheet(data);
XLSX.utils.sheet_add_aoa(sheet, data, { origin: 'A'+headerNum });
// XLSX.utils.book_append_sheet(wb, sheet, 'test');
await excelStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), headerRange, fileNm)
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
}
});
});
}
export const sheetParameter ={
data:[[]], headerNum:0, headerRange:'A1:V3'
}
export const filePickedBlobArray= (oFile: ArrayBuffer,fileNm:string, sheetArray: [typeof sheetParameter]):Promise<JSON | undefined> =>{ // Create A File Reader HTML5
return new Promise<JSON | undefined> (async () => {
// Ready The Event For When A File Gets Selected
const wb = XLSX.read(oFile, { type: 'binary' });
// Loop Over Each Sheet
wb.SheetNames.forEach( function (sheetName, index) {
// Obtain The Current Row As CSV
if(sheetArray[index]){
const sheet = wb.Sheets[sheetName];
if (sheet) {
//오리진 확인 test 필요
// const sheet = XLSX.utils.json_to_sheet(data);
XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: 'A'+sheetArray[index].headerNum });
// XLSX.utils.book_append_sheet(wb, sheet, 'test');
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
}
});
await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm)
});
}
@@ -1,84 +0,0 @@
import * as XLSX from 'xlsx';
type ExcelDownloadProps = {
data: JSON | JSON[];
fileName: string;
sheetNameArray: string[];
}
export const downloadExcel = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"] }: ExcelDownloadProps) => {
const worksheet = XLSX.utils.json_to_sheet(Array.isArray(data) ? data : [data]);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, sheetNameArray[0]);
//let buffer = XLSX.write(workbook, { bookType: "xlsx", type: "buffer" });
//XLSX.write(workbook, { bookType: "xlsx", type: "binary" });
XLSX.writeFile(workbook, fileName + '.xlsx');
};
export const filePicked=(oFile: File, headerNum:number=0):Promise<JSON | undefined> =>{
// Create A File Reader HTML5
const reader = new FileReader();
return new Promise<JSON | undefined>((resolve, reject) => {
// Ready The Event For When A File Gets Selected
reader.onload = function (e) {
const data = e.target?.result;
if (!data) {
console.error("Failed to read file: e.target.result is null");
resolve(undefined);
return;
}
const wb = XLSX.read(data, { type: 'binary' });
let jsonXls: JSON | undefined;
//TODO 시트가 2개이상일 경우 마지막 시트의 데이터만 표출됨
// Loop Over Each Sheet
wb.SheetNames.forEach(function (sheetName) {
// Obtain The Current Row As CSV
const sheet = wb.Sheets[sheetName];
if (sheet) {
jsonXls = XLSX.utils.sheet_to_json(sheet, { range: headerNum }) as unknown as JSON;
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
});
resolve(jsonXls);
};
reader.onerror = function () {
console.error("Error reading file");
reject(undefined);
};
// Tell JS To Start Reading The File.. You could delay this if desired
reader.readAsBinaryString(oFile);
});
}
export const filePickedBlob=(oFile: ArrayBuffer, data:[][],fileNm:string, headerNum:number=0):Promise<JSON | undefined> =>{
// Create A File Reader HTML5
return new Promise<JSON | undefined>(() => {
// Ready The Event For When A File Gets Selected
const wb = XLSX.read(oFile, { type: 'binary' });
// Loop Over Each Sheet
wb.SheetNames.forEach(function (sheetName, index) {
// Obtain The Current Row As CSV
if(index==0){
const sheet = wb.Sheets[sheetName];
if (sheet) {
//오리진 확인 test 필요
// const sheet = XLSX.utils.json_to_sheet(data);
XLSX.utils.sheet_add_aoa(sheet, data, { origin: 'A'+headerNum });
// XLSX.utils.book_append_sheet(wb, sheet, 'test');
XLSX.writeFile(wb, `${fileNm}.xlsx`);
} else {
console.error(`Sheet "${sheetName}" is undefined.`);
}
}
});
});
}
@@ -1,7 +1,7 @@
export * from "./chart";
export * from "./date_picker";
export * from "./dialog_ui";
export * from "./excel_download";
export * from "./excel";
export * from "./file-upload";
export * from "./form";
export * from "./form_ui";
@@ -19,3 +19,4 @@ export * from "./tab";
export * from "./table";
export * from "./table_div";
export * from "./title";
@@ -8,16 +8,20 @@ interface TableUpperProps {
row? : number;
rowSpan?: number;
colSpan?: number;
visible?:boolean;
}
const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
const maxRow = upHeadList.reduce((max, head) => Math.max(max, head.row || 0), 0);
const maxRow = upHeadList?.reduce((max, head) => Math.max(max, head.row || 0), 0);
//upHeadList.forEach((item) => item.visible ?item.visible:true)
if(maxRow == 0){
return (
<>
<TableRow className="bg-gray-50">
{upHeadList.map((head: TableUpperProps) => {
{upHeadList
.filter((item) => item.visible != false)
.map((head: TableUpperProps) => {
return (
<TableHead
key={head.id}
@@ -38,7 +42,9 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
<>
{Array.from({ length: maxRow + 1 }, (_, rowIndex) => (
<TableRow key={rowIndex+ " headerRow"} className="bg-gray-50">
{upHeadList.map((head: TableUpperProps) => {
{upHeadList
.filter((item) => item.visible != false)
.map((head: TableUpperProps) => {
if (head.row === rowIndex) {
return (
<TableHead
@@ -14,15 +14,17 @@ type UiTableProps = {
className?: string;
resetPageNo?: boolean;
children?: React.ReactNode | undefined;
boldArray?: number[];
}
type UiHeadProps = {
id: string;
title: string;
align?: string;
visible?:boolean;
}
export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false }: UiTableProps) {
export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false, boldArray = [] }: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [idx, setIdx] = useState<number>(-1);
@@ -58,8 +60,20 @@ export function UiTable({ tableData, headlist, pageSize, total, headName, childr
<TableBody className='text-center'>
{tableData.length > 0 ? tableData?.map((el: { [key: string]: any}, index) => (
<TableRow key={`row-${index}`}>
{headlist?.map((head) => (
{headlist
.filter((item) => item.visible != false)
.map((head) => (
head.id && head.id.length > 1 && (
boldArray.includes(index) ? <TableCell
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${
cellClick && idx === index ? '!font-[1000] cursor-pointer' :
cellClick ? 'cursor-pointer' : ''
} !font-bold`}
onClick={() => handleCellClick(index, head.id)}
key={`cell-${head.id}-${index}`}
>
{typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')}
</TableCell>:
<TableCell
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${
cellClick && idx === index ? '!font-[1000] cursor-pointer' :