기타 보관함/개발자정보

React 17 챠트 에제 클래스형 컴퍼넌트

오아름 샘 2025. 5. 14. 23:41
반응형

 

 

import React, { Component } from 'react';
import axios from 'axios';
import { 
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
  Cell, LabelList
} from 'recharts';
import { RefreshCw } from 'lucide-react';

// 타입 정의
interface DashboardState {
  testPlan: {
    scenario: { mapped: number, total: number },
    testCase: { mapped: number, total: number },
    activity: { mapped: number, total: number }
  },
  testExecution: {
    scenario: { success: number, total: number },
    testCase: { success: number, total: number },
    activity: { success: number, total: number }
  },
  answerSheet: {
    plan: number,
    execution: number
  },
  notification: {
    error: number,
    checkTarget: number
  },
  personalResults: {
    person1: { scenario: number, testCase: number, activity: number, total: number },
    person2: { scenario: number, testCase: number, activity: number, total: number },
    person3: { scenario: number, testCase: number, activity: number, total: number }
  },
  workTrend: {
    process: Array<{
      date: string,
      success: number,
      failure: number,
      error: number,
      notExecuted: number
    }>,
    answerSheet: Array<{
      date: string,
      success: number,
      failure: number,
      error: number,
      notExecuted: number
    }>,
    notification: Array<{
      date: string,
      success: number,
      failure: number,
      error: number,
      notExecuted: number
    }>
  },
  currentTime: string,
  loading: boolean
}

class TestDashboard extends Component<{}, DashboardState> {
  constructor(props: {}) {
    super(props);
    
    this.state = {
      testPlan: {
        scenario: { mapped: 0, total: 0 },
        testCase: { mapped: 0, total: 0 },
        activity: { mapped: 0, total: 0 }
      },
      testExecution: {
        scenario: { success: 0, total: 0 },
        testCase: { success: 0, total: 0 },
        activity: { success: 0, total: 0 }
      },
      answerSheet: {
        plan: 0,
        execution: 0
      },
      notification: {
        error: 0,
        checkTarget: 0
      },
      personalResults: {
        person1: { scenario: 0, testCase: 0, activity: 0, total: 0 },
        person2: { scenario: 0, testCase: 0, activity: 0, total: 0 },
        person3: { scenario: 0, testCase: 0, activity: 0, total: 0 }
      },
      workTrend: {
        process: [],
        answerSheet: [],
        notification: []
      },
      currentTime: new Date().toLocaleString(),
      loading: false
    };
  }

  componentDidMount() {
    this.fetchDashboardData();
    // 1분마다 시간 업데이트
    this.timer = setInterval(() => {
      this.setState({
        currentTime: new Date().toLocaleString()
      });
    }, 60000);
  }

  componentWillUnmount() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }

  timer: NodeJS.Timeout | null = null;

  fetchDashboardData = async () => {
    this.setState({ loading: true });
    try {
      const response = await axios.get('http://127.0.0.1:8080/do/initDashboard');
      const data = response.data;
      
      // 실제 API 응답에 맞게 데이터를 파싱하여 상태를 업데이트합니다.
      // 여기서는 예시 데이터로 가정합니다.
      this.setState({
        testPlan: {
          scenario: { mapped: data.testPlan?.scenario?.mapped || 45, total: data.testPlan?.scenario?.total || 50 },
          testCase: { mapped: data.testPlan?.testCase?.mapped || 120, total: data.testPlan?.testCase?.total || 150 },
          activity: { mapped: data.testPlan?.activity?.mapped || 280, total: data.testPlan?.activity?.total || 300 }
        },
        testExecution: {
          scenario: { success: data.testExecution?.scenario?.success || 40, total: data.testExecution?.scenario?.total || 50 },
          testCase: { success: data.testExecution?.testCase?.success || 130, total: data.testExecution?.testCase?.total || 150 },
          activity: { success: data.testExecution?.activity?.success || 270, total: data.testExecution?.activity?.total || 300 }
        },
        answerSheet: {
          plan: data.answerSheet?.plan || 80,
          execution: data.answerSheet?.execution || 70
        },
        notification: {
          error: data.notification?.error || 5,
          checkTarget: data.notification?.checkTarget || 20
        },
        personalResults: {
          person1: { 
            scenario: data.personalResults?.person1?.scenario || 15, 
            testCase: data.personalResults?.person1?.testCase || 50, 
            activity: data.personalResults?.person1?.activity || 100,
            total: data.personalResults?.person1?.total || 165
          },
          person2: { 
            scenario: data.personalResults?.person2?.scenario || 20, 
            testCase: data.personalResults?.person2?.testCase || 60, 
            activity: data.personalResults?.person2?.activity || 120,
            total: data.personalResults?.person2?.total || 200
          },
          person3: { 
            scenario: data.personalResults?.person3?.scenario || 15, 
            testCase: data.personalResults?.person3?.testCase || 40, 
            activity: data.personalResults?.person3?.activity || 80,
            total: data.personalResults?.person3?.total || 135
          }
        },
        workTrend: {
          process: data.workTrend?.process || [
            { date: '05-10', success: 20, failure: 5, error: 2, notExecuted: 3 },
            { date: '05-11', success: 25, failure: 3, error: 1, notExecuted: 1 },
            { date: '05-12', success: 18, failure: 4, error: 2, notExecuted: 6 },
            { date: '05-13', success: 22, failure: 2, error: 1, notExecuted: 5 },
            { date: '05-14', success: 30, failure: 3, error: 0, notExecuted: 2 }
          ],
          answerSheet: data.workTrend?.answerSheet || [
            { date: '05-10', success: 15, failure: 3, error: 1, notExecuted: 1 },
            { date: '05-11', success: 18, failure: 2, error: 0, notExecuted: 0 },
            { date: '05-12', success: 12, failure: 4, error: 2, notExecuted: 2 },
            { date: '05-13', success: 16, failure: 1, error: 1, notExecuted: 2 },
            { date: '05-14', success: 20, failure: 2, error: 0, notExecuted: 3 }
          ],
          notification: data.workTrend?.notification || [
            { date: '05-10', success: 4, failure: 1, error: 0, notExecuted: 0 },
            { date: '05-11', success: 3, failure: 2, error: 0, notExecuted: 0 },
            { date: '05-12', success: 5, failure: 0, error: 1, notExecuted: 0 },
            { date: '05-13', success: 4, failure: 1, error: 0, notExecuted: 0 },
            { date: '05-14', success: 6, failure: 0, error: 0, notExecuted: 0 }
          ]
        },
        currentTime: new Date().toLocaleString()
      });
    } catch (error) {
      console.error('데이터 불러오기 오류:', error);
      // 오류 처리를 위한 추가 로직
    } finally {
      this.setState({ loading: false });
    }
  };

  handleRefresh = () => {
    this.fetchDashboardData();
  };

  // 상단 통계 카드 컴포넌트
  renderStatCard = (title: string, data: any, type: string) => {
    let content;
    
    if (type === 'testPlan' || type === 'testExecution') {
      const firstKey = type === 'testPlan' ? 'mapped' : 'success';
      content = (
        <div className="flex flex-col">
          <div className="flex justify-between mb-1">
            <span>시나리오:</span> 
            <span className="font-bold">{data.scenario[firstKey]}/{data.scenario.total}</span>
          </div>
          <div className="flex justify-between mb-1">
            <span>테스트 케이스:</span> 
            <span className="font-bold">{data.testCase[firstKey]}/{data.testCase.total}</span>
          </div>
          <div className="flex justify-between">
            <span>액티비티:</span> 
            <span className="font-bold">{data.activity[firstKey]}/{data.activity.total}</span>
          </div>
        </div>
      );
    } else if (type === 'answerSheet') {
      content = (
        <div className="flex flex-col">
          <div className="flex justify-between mb-1">
            <span>계획:</span> 
            <span className="font-bold">{data.plan}</span>
          </div>
          <div className="flex justify-between">
            <span>수행:</span> 
            <span className="font-bold">{data.execution}</span>
          </div>
        </div>
      );
    } else if (type === 'notification') {
      content = (
        <div className="flex flex-col">
          <div className="flex justify-between mb-1">
            <span>오류:</span> 
            <span className="font-bold">{data.error}</span>
          </div>
          <div className="flex justify-between">
            <span>체크대상:</span> 
            <span className="font-bold">{data.checkTarget}</span>
          </div>
        </div>
      );
    }
    
    return (
      <div className="bg-white p-4 rounded-lg shadow">
        <h3 className="text-lg font-bold mb-2 border-b pb-2">{title}</h3>
        {content}
      </div>
    );
  };

  // Personal Results 차트 데이터 준비
  preparePersonalResultsData = () => {
    const { personalResults } = this.state;
    return [
      { name: '담당자1', 시나리오: personalResults.person1.scenario, 테스트케이스: personalResults.person1.testCase, 액티비티: personalResults.person1.activity, 전체: personalResults.person1.total },
      { name: '담당자2', 시나리오: personalResults.person2.scenario, 테스트케이스: personalResults.person2.testCase, 액티비티: personalResults.person2.activity, 전체: personalResults.person2.total },
      { name: '담당자3', 시나리오: personalResults.person3.scenario, 테스트케이스: personalResults.person3.testCase, 액티비티: personalResults.person3.activity, 전체: personalResults.person3.total }
    ];
  };

  // Work Trend 스택 바 차트 렌더링
  renderStackedBarChart = (data: any[], title: string) => {
    return (
      <div className="bg-white p-4 rounded-lg shadow h-full">
        <h3 className="text-lg font-bold mb-4">{title}</h3>
        <ResponsiveContainer width="100%" height={220}>
          <BarChart data={data} barSize={30}>
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Legend />
            <Bar dataKey="success" stackId="a" fill="#4caf50" name="성공" />
            <Bar dataKey="failure" stackId="a" fill="#ff9800" name="실패" />
            <Bar dataKey="error" stackId="a" fill="#f44336" name="오류" />
            <Bar dataKey="notExecuted" stackId="a" fill="#9e9e9e" name="비수행" />
          </BarChart>
        </ResponsiveContainer>
      </div>
    );
  };

  render() {
    const { testPlan, testExecution, answerSheet, notification, workTrend, currentTime, loading } = this.state;
    const personalResultsData = this.preparePersonalResultsData();

    return (
      <div className="p-6 bg-gray-100 min-h-screen">
        <div className="mb-6">
          <h1 className="text-2xl font-bold mb-1">테스트 관리 대시보드</h1>
          <p className="text-gray-600">테스트 계획 수립 및 실행 현황을 한눈에 확인하세요.</p>
        </div>

        {/* 상단 통계 카드 영역 */}
        <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
          {this.renderStatCard('테스트 계획', testPlan, 'testPlan')}
          {this.renderStatCard('테스트 수행', testExecution, 'testExecution')}
          {this.renderStatCard('정답지', answerSheet, 'answerSheet')}
          {this.renderStatCard('알람', notification, 'notification')}
        </div>

        {/* 차트 영역 */}
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
          {/* Personal Results 차트 */}
          <div className="bg-white p-4 rounded-lg shadow">
            <h3 className="text-lg font-bold mb-4">Personal Results</h3>
            <ResponsiveContainer width="100%" height={300}>
              <BarChart data={personalResultsData} barGap={8}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="name" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Bar dataKey="시나리오" fill="#8884d8">
                  <LabelList dataKey="시나리오" position="top" />
                </Bar>
                <Bar dataKey="테스트케이스" fill="#82ca9d">
                  <LabelList dataKey="테스트케이스" position="top" />
                </Bar>
                <Bar dataKey="액티비티" fill="#ffc658">
                  <LabelList dataKey="액티비티" position="top" />
                </Bar>
                <Bar dataKey="전체" fill="#ff8042">
                  <LabelList dataKey="전체" position="top" />
                </Bar>
              </BarChart>
            </ResponsiveContainer>
          </div>

          {/* Work Trend 차트 */}
          <div className="grid grid-cols-1 gap-4">
            <h3 className="text-lg font-bold ml-4">Work Trend</h3>
            {this.renderStackedBarChart(workTrend.process, "프로세스")}
            {this.renderStackedBarChart(workTrend.answerSheet, "정답지")}
            {this.renderStackedBarChart(workTrend.notification, "알람")}
          </div>
        </div>

        {/* 푸터 영역 */}
        <div className="flex justify-between items-center bg-white p-4 rounded-lg shadow">
          <div className="text-gray-600">
            마지막 업데이트: {currentTime}
          </div>
          <button 
            className="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg"
            onClick={this.handleRefresh}
            disabled={loading}
          >
            <RefreshCw size={16} />
            {loading ? '로딩 중...' : '새로 고침'}
          </button>
        </div>
      </div>
    );
  }
}

export default TestDashboard;
반응형