diff --git a/README.md b/README.md index f918726..72535bc 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ NEXT_PUBLIC_API_URL=http://localhost:8080/api NEXT_PUBLIC_BYPASS_AUTH=false # API URL 설정 -NEXT_PUBLIC_API_URL=http://192.168.0.224:8080/api +NEXT_PUBLIC_API_URL=http://localhost:8080/api ``` **.env.production** (프로덕션 환경) @@ -69,7 +69,7 @@ NEXT_PUBLIC_API_URL=http://192.168.0.224:8080/api NEXT_PUBLIC_BYPASS_AUTH=false # API URL 설정 -NEXT_PUBLIC_API_URL=http://192.168.0.224:8080/api +NEXT_PUBLIC_API_URL=http://localhost:8080/api ``` **주요 환경 변수**: diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 21ab3f0..c1b0c44 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { chartService } from '@/lib/api'; import { Card, CardContent, CardTitle, CardHeader, CardDescription } from '@/components/ui/card'; import { LineChart as LineChartIcon, @@ -12,7 +13,7 @@ import { PlusCircle, } from 'lucide-react'; import { v4 as uuidv4 } from 'uuid'; -import type { ChartData } from '@/types/chart'; +import type { ChartData, DrawChartData } from '@/types/chart'; import DashboardShell from '@/components/dashboard-shell'; import TotalCharts from '@/components/dashboards/TotalCharts'; import { Button } from '@/components/ui/button'; @@ -141,10 +142,56 @@ const sampleCharts: ChartData[] = [ ]; export default function Dashboard() { - const [charts, setCharts] = useState(sampleCharts); + const [charts, setCharts] = useState([]); + const [drawCharts, setDrawCharts] = useState([]); const [isChartModalOpen, setIsChartModalOpen] = useState(false); const [fileModalOpen, setFileModalOpen] = useState(false); + useEffect(() => { + console.log('대시보드 컴포넌트가 마운트되었습니다.'); + + const fetchCharts = async () => { + console.log('차트 데이터를 불러오는 중...'); + try { + const charts = await chartService.getCharts(); + console.log(`총 ${charts.length}개의 차트를 불러왔습니다.`); + + const transformedCharts = charts.map( + chart => + ({ + id: chart.id, // userId를 id로 변환 + title: chart.title, + type: chart.chartType.toLowerCase(), + description: chart.description, + esg: chart.category.toLowerCase(), + labels: chart.data.map(item => item.label), // data에서 labels 추출 + datasets: [ + { + label: chart.indicator, + data: chart.data.map(item => item.key), // data에서 key를 추출 + backgroundColor: chart.data.map(() => '#3498db'), + borderColor: chart.data.map(() => '#2980b9'), + borderWidth: 1, + }, + ], + options: { + plugins: { legend: { display: true } }, + scales: { x: { beginAtZero: true } }, + }, + colSpan: chart.chartGrid || 1, // chartGrid를 colSpan으로 매핑 + }) as DrawChartData + ); + + setDrawCharts(transformedCharts); + } catch (error) { + console.error('차트 데이터를 불러오는 중 오류 발생:', error); + setCharts(sampleCharts); // 오류 발생 시 샘플 데이터 사용 + } + }; + + fetchCharts(); // 함수 호출 + }, []); + // 차트 추가 함수 const addChart = (newChart: ChartData) => { setCharts(prev => [...prev, { ...newChart, id: uuidv4() }]); @@ -195,7 +242,7 @@ export default function Dashboard() { const chartContainerStyle = { height: '250px', width: '100%' }; try { - switch (chart.type) { + switch (chart.chartType) { case 'bar': { const options = { ...baseOptions, ...(chart.options || {}) } as ChartOptions<'bar'>; const data = chartDataProp as ChartJSData<'bar'>; @@ -255,18 +302,18 @@ export default function Dashboard() { } default: { // 컴파일 타임에 모든 케이스를 처리했는지 확인 (never 타입 활용) - const exhaustiveCheck: never = chart.type; + const exhaustiveCheck: never = chart.chartType; return ( -

+

지원되지 않는 차트 유형입니다: {exhaustiveCheck}

); } } } catch (error) { - console.error('Error rendering chart:', chart.id, chart.type, error); + console.error('Error rendering chart:', chart.id, chart.chartType, error); return ( -

+

차트를 렌더링하는 중 오류가 발생했습니다.

); @@ -296,14 +343,14 @@ export default function Dashboard() { <> -
- {charts.map(chart => ( +
+ {drawCharts.map(chart => (
diff --git a/src/app/modal/chartinput-form.tsx b/src/app/modal/chartinput-form.tsx index 5cccac7..b012592 100644 --- a/src/app/modal/chartinput-form.tsx +++ b/src/app/modal/chartinput-form.tsx @@ -1,4 +1,5 @@ 'use client'; +import api from '@/lib/api'; // API 호출을 위한 라이브러리 import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { @@ -8,8 +9,8 @@ import { DialogTitle, DialogClose, } from '@/components/ui/dialog'; -import { ESGCombobox } from './combobox'; -import DataTable from './datatable'; +import { ESGCombobox, esgIndicators } from './combobox'; // ESG 항목 선택 컴포넌트 +import DataTable from './datatable'; // 데이터 입력 테이블 컴포넌트 import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { @@ -18,55 +19,42 @@ import { SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; +} from '@/components/ui/select'; // Select 관련 컴포넌트 추가 import { v4 as uuidv4 } from 'uuid'; -import { ChartType, ChartData } from '@/types/chart'; -import api from '@/lib/api'; +import { ChartType, ChartData } from '@/types/chart'; // ChartDataset 임포트 제거 interface ESGChartDialogProps { open: boolean; setOpen: (open: boolean) => void; - onChartAdd?: (chart: ChartData) => void; + onChartAdd?: (chart: ChartData) => void; // 차트 추가 콜백 } export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProps) { - const [step, setStep] = useState<'combobox' | 'datatable'>('combobox'); + const [step, setStep] = useState<'combobox' | 'datatable'>('combobox'); // 현재 단계 상태 const [chartType, setChartType] = useState('bar'); const [chartTitle, setChartTitle] = useState(''); const [chartDescription, setChartDescription] = useState(''); const [colSpan, setColSpan] = useState<1 | 2 | 3 | 4>(1); - const [selectedESG, setSelectedESG] = useState(null); - const [labels, setLabels] = useState([]); - const [datasets, setDatasets] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [selectedESG, setSelectedESG] = useState(null); // ESG 항목 상태 추가 + const [labels, setLabels] = useState([]); // labels 상태 추가 + const [datasets, setDatasets] = useState([]); // datasets 상태 타입 수정 + const [isLoading, setIsLoading] = useState(false); // 로딩 상태 추가 + const [data, setData] = useState<{ label: string; key: number; unit?: string }[]>([]); - /** - * 다음 단계로 이동하는 함수 - * 첫 번째 단계(combobox)에서는 데이터 입력 단계로 전환하고 - * 두 번째 단계(datatable)에서는 저장 처리를 수행합니다. - */ const handleNext = () => { if (step === 'combobox') { - setStep('datatable'); + setStep('datatable'); // 다음 단계로 전환 } else { handleSave(); } }; - /** - * 이전 단계로 돌아가는 함수 - * 데이터 입력 단계에서 ESG 항목 선택 단계로 돌아갑니다. - */ + // 이전 단계로 돌아가는 함수 추가 const handleBack = () => { setStep('combobox'); }; - /** - * 데이터 테이블 변경 콜백 함수 - * DataTable 컴포넌트에서 데이터가 변경될 때 호출됩니다. - * @param newLabels 새로운 라벨 배열 - * @param newDatasets 새로운 데이터셋 배열 - */ + // 데이터 테이블 변경 콜백 (DataTable에서 호출) const handleDataChange = useCallback( (newLabels: string[], newDatasets: ChartData['datasets']) => { setLabels(newLabels); @@ -75,20 +63,23 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp [] ); - /** - * ESG 항목 변경 콜백 함수 - * ESGCombobox 컴포넌트에서 선택 항목이 변경될 때 호출됩니다. - * @param value 선택된 ESG 항목 값 - */ + // ESG 항목 변경 콜백 (ESGCombobox에서 호출) const handleESGChange = useCallback((value: string | null) => { setSelectedESG(value); }, []); - /** - * 차트 데이터 저장 함수 - * 입력된 모든 데이터를 검증하고 API를 통해 저장합니다. - */ + function findESGCategoryByLabel(id: string): 'environment' | 'social' | 'governance' | null { + for (const category in esgIndicators) { + const indicators = esgIndicators[category as keyof typeof esgIndicators]; + if (indicators.some(indicator => indicator.id === id)) { + return category as 'environment' | 'social' | 'governance'; + } + } + return null; // 못 찾은 경우 + } + const handleSave = async () => { + // async 추가 if (!chartTitle) { alert('차트 제목을 입력해주세요'); return; @@ -97,6 +88,7 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp alert('ESG 항목을 선택해주세요.'); return; } + // 데이터 유효성 검사 활성화 if ( step === 'datatable' && (labels.length === 0 || @@ -108,55 +100,133 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp return; } - setIsLoading(true); + setIsLoading(true); // 로딩 시작 + // 새 차트 객체 생성 시 상태 값 사용 const newChart: ChartData = { id: uuidv4(), title: chartTitle, description: chartDescription, type: chartType, colSpan: colSpan, - esg: selectedESG, - labels: labels, - datasets: datasets, + esg: selectedESG, // ESG 항목 추가 (null 아님을 위에서 확인) + labels: labels, // 상태에서 가져온 labels 사용 + datasets: datasets, // 상태에서 가져온 datasets 사용 createdAt: new Date(), updatedAt: new Date(), }; try { - // 차트 서비스를 사용하여 차트 생성 - const savedChart = await api.chart.createChart(newChart); - + // 백엔드 API 호출 (엔드포인트는 예시) //고정... + const response = await api.chart.createChart({ + title: chartTitle, + chartType: chartType, + description: chartDescription, + labels: labels, + chartGrid: colSpan, + data: data, + indicator: selectedESG, + category: findESGCategoryByLabel(selectedESG)?.charAt(0).toUpperCase() ?? 'E', + }); + + if (!response.title) { + // 오류 처리 (예: 사용자에게 알림) + throw new Error(`API 오류: ${response.data}`); + } + + const savedChart = await response.data; // 저장된 차트 데이터 (선택적) console.log('차트 저장 성공:', savedChart); + // 부모 컴포넌트로 전달 if (onChartAdd) { - onChartAdd(savedChart); + onChartAdd(savedChart); // 저장된 데이터 전달 (백엔드 응답 사용) } + // 폼 초기화 및 닫기 resetForm(); setOpen(false); } catch (error) { console.error('차트 저장 실패:', error); + // 사용자에게 오류 알림 (예: alert 또는 toast 메시지) alert('차트 저장 중 오류가 발생했습니다.'); } finally { - setIsLoading(false); + setIsLoading(false); // 로딩 종료 } }; - /** - * 폼 초기화 함수 - * 모든 입력 필드와 상태를 초기값으로 리셋합니다. - */ const resetForm = () => { setChartTitle(''); setChartDescription(''); setChartType('bar'); setColSpan(1); - setSelectedESG(null); - setLabels([]); - setDatasets([]); + setSelectedESG(null); // ESG 항목 초기화 + setLabels([]); // labels 초기화 + setDatasets([]); // datasets 초기화 }; + // 차트 타입별 샘플 데이터 생성 함수 리팩토링 (더 이상 사용되지 않을 수 있으므로 주석 처리 또는 삭제 가능) + /* + const getSampleData = (type: ChartType): { labels?: string[], datasets?: ChartData['datasets'] } => { + switch (type) { + case 'bar': + return { + labels: ['카테고리1', '카테고리2', '카테고리3'], + datasets: [ + { + label: '샘플 데이터', // label 추가 + data: [65, 78, 82], // data로 변경 + backgroundColor: ['blue', 'green', 'purple'] // backgroundColor로 변경 + } + ] + }; + case 'line': + return { + labels: ['1월', '2월', '3월', '4월', '5월', '6월'], + datasets: [ + { + label: 'Dataset 1', // label로 변경 + data: [65, 59, 80, 81, 56, 55] // data로 변경 + }, + { + label: 'Dataset 2', // label로 변경 + data: [28, 48, 40, 19, 86, 27] // data로 변경 + } + ] + }; + case 'pie': + case 'donut': // donut 타입 추가 + return { + labels: ['항목 1', '항목 2', '항목 3'], // labels 추가 + datasets: [ + { + label: '샘플 데이터', // label 추가 + data: [45, 30, 25], // data로 변경 (숫자형) + backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'] // 배경색 추가 + } + ] + }; + case 'area': // area 타입 추가 + return { + labels: ['1월', '2월', '3월', '4월', '5월', '6월'], + datasets: [ + { + label: '샘플 데이터', // label 추가 + data: [30, 45, 40, 55, 60, 65], // data로 변경 + borderColor: 'rgb(75, 192, 192)', // borderColor 추가 + backgroundColor: 'rgba(75, 192, 192, 0.2)', // backgroundColor 추가 + fill: true // fill 속성 추가 + } + ] + }; + default: + // 모든 타입에 대해 처리했는지 확인 (never 타입 활용) + const exhaustiveCheck: never = type; + console.error(`Unhandled chart type: ${exhaustiveCheck}`); + return {}; + } + }; + */ + return ( @@ -168,16 +238,10 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp > -
+
{step === 'combobox' && ( -
-
- -
- -
-
- +
+ {/* 차트 제목 입력 */}
+ {/* ESG 항목 선택 */} +
+ +
+ {/* ESGCombobox에 콜백 및 값 전달 */} + +
+
+ + {/* 차트 설명 입력 */}
+ {/* 차트 유형 선택 */}
+ {/* 차트 크기 선택 */}