414 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
		
		
			
		
	
	
			414 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| 
								 | 
							
								<template>
							 | 
						|||
| 
								 | 
							
								  <div>
							 | 
						|||
| 
								 | 
							
								    <PageDescription>
							 | 
						|||
| 
								 | 
							
								      <h1>배양 그래프 (멀티)</h1>
							 | 
						|||
| 
								 | 
							
								      <div class="box">
							 | 
						|||
| 
								 | 
							
								        <h2>1. 그래프 구성 및 기능</h2>
							 | 
						|||
| 
								 | 
							
								        <ul>
							 | 
						|||
| 
								 | 
							
								          <li>총 30개의 그래프를 한 화면에 동시에 볼 수 있습니다.</li>
							 | 
						|||
| 
								 | 
							
								          <li>렌더링 최적화를 위해 최소한의 기능만 제공됩니다.</li>
							 | 
						|||
| 
								 | 
							
								        </ul>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								      <div class="box">
							 | 
						|||
| 
								 | 
							
								        <h2>2. 출력되는 데이터 개수</h2>
							 | 
						|||
| 
								 | 
							
								        <ul>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            그래프 30개 × 시리즈 12개 × 10분 간격(100시간, 601포인트) =
							 | 
						|||
| 
								 | 
							
								            <span class="highlight">216,360개</span>
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								          <li>화면 렌더링 시간은 약 600ms ~ 800ms 정도 소요됩니다.</li>
							 | 
						|||
| 
								 | 
							
								        </ul>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								      <div class="box">
							 | 
						|||
| 
								 | 
							
								        <h2>3. 그래프 복사</h2>
							 | 
						|||
| 
								 | 
							
								        <ul>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            <span class="highlight">그래프 복사</span> 버튼을 클릭하면 그래프가
							 | 
						|||
| 
								 | 
							
								            클립보드에 복사됩니다.
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								        </ul>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								    </PageDescription>
							 | 
						|||
| 
								 | 
							
								    <div class="multi-graph-list">
							 | 
						|||
| 
								 | 
							
								      <div v-for="series in seriesList" :key="series.name" class="single-graph">
							 | 
						|||
| 
								 | 
							
								        <div class="graph-title">
							 | 
						|||
| 
								 | 
							
								          {{ series.name }}<span v-if="series.unit"> {{ series.unit }}</span>
							 | 
						|||
| 
								 | 
							
								          <button class="copy-btn" @click="copyChartImage(series.name)">
							 | 
						|||
| 
								 | 
							
								            그래프 복사
							 | 
						|||
| 
								 | 
							
								          </button>
							 | 
						|||
| 
								 | 
							
								        </div>
							 | 
						|||
| 
								 | 
							
								        <div
							 | 
						|||
| 
								 | 
							
								          :ref="el => setGraphRef(series.name, el as Element | null)"
							 | 
						|||
| 
								 | 
							
								          class="echarts-graph"
							 | 
						|||
| 
								 | 
							
								        ></div>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								    </div>
							 | 
						|||
| 
								 | 
							
								  </div>
							 | 
						|||
| 
								 | 
							
								</template>
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								<script setup lang="ts">
							 | 
						|||
| 
								 | 
							
								import { ref, onMounted, nextTick } from "vue";
							 | 
						|||
| 
								 | 
							
								import * as echarts from "echarts";
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 파스텔 컬러 15개 반복
							 | 
						|||
| 
								 | 
							
								const pastelColors = [
							 | 
						|||
| 
								 | 
							
								  "#A3D8F4",
							 | 
						|||
| 
								 | 
							
								  "#F7B7A3",
							 | 
						|||
| 
								 | 
							
								  "#B5EAD7",
							 | 
						|||
| 
								 | 
							
								  "#FFDAC1",
							 | 
						|||
| 
								 | 
							
								  "#C7CEEA",
							 | 
						|||
| 
								 | 
							
								  "#FFF1BA",
							 | 
						|||
| 
								 | 
							
								  "#FFB7B2",
							 | 
						|||
| 
								 | 
							
								  "#B4A7D6",
							 | 
						|||
| 
								 | 
							
								  "#AED9E0",
							 | 
						|||
| 
								 | 
							
								  "#FFC3A0",
							 | 
						|||
| 
								 | 
							
								  "#E2F0CB",
							 | 
						|||
| 
								 | 
							
								  "#FFB347",
							 | 
						|||
| 
								 | 
							
								  "#C1C8E4",
							 | 
						|||
| 
								 | 
							
								  "#FFFACD",
							 | 
						|||
| 
								 | 
							
								  "#FFD1DC",
							 | 
						|||
| 
								 | 
							
								];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 그래프 개수 변수로 분리
							 | 
						|||
| 
								 | 
							
								const NUM_GRAPHS = 30;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 30개 y축 정보 (culture-graph.vue와 동일하게 수정)
							 | 
						|||
| 
								 | 
							
								const yAxisList = [
							 | 
						|||
| 
								 | 
							
								  { name: "ORP", unit: "", color: pastelColors[0], min: 0, max: 1000 },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Air flow",
							 | 
						|||
| 
								 | 
							
								    unit: "(L/min)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[1],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 30,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  { name: "DO", unit: "", color: pastelColors[2], min: 0, max: 200 },
							 | 
						|||
| 
								 | 
							
								  { name: "Feed TK1", unit: "(L)", color: pastelColors[3], min: 0, max: 10 },
							 | 
						|||
| 
								 | 
							
								  { name: "Feed TK2", unit: "(L)", color: pastelColors[4], min: 0, max: 10 },
							 | 
						|||
| 
								 | 
							
								  { name: "pH", unit: "", color: pastelColors[5], min: 6.0, max: 8.0 },
							 | 
						|||
| 
								 | 
							
								  { name: "Pressure", unit: "(bar)", color: pastelColors[6], min: 0, max: 2 },
							 | 
						|||
| 
								 | 
							
								  { name: "RPM", unit: "", color: pastelColors[7], min: 0, max: 3000 },
							 | 
						|||
| 
								 | 
							
								  { name: "CO2", unit: "(%)", color: pastelColors[8], min: 0, max: 10 },
							 | 
						|||
| 
								 | 
							
								  { name: "JAR Vol", unit: "(L)", color: pastelColors[9], min: 0, max: 20 },
							 | 
						|||
| 
								 | 
							
								  { name: "WEIGHT", unit: "(kg)", color: pastelColors[10], min: 0, max: 100 },
							 | 
						|||
| 
								 | 
							
								  { name: "O2", unit: "(%)", color: pastelColors[11], min: 0, max: 100 },
							 | 
						|||
| 
								 | 
							
								  { name: "NV", unit: "", color: pastelColors[12], min: 0, max: 10 },
							 | 
						|||
| 
								 | 
							
								  { name: "NIR", unit: "", color: pastelColors[13], min: 0, max: 10 },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Temperature",
							 | 
						|||
| 
								 | 
							
								    unit: "(℃)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[14],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 50,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  { name: "Humidity", unit: "(%)", color: pastelColors[0], min: 0, max: 100 },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Flow Rate",
							 | 
						|||
| 
								 | 
							
								    unit: "(L/min)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[1],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 60,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Conductivity",
							 | 
						|||
| 
								 | 
							
								    unit: "(μS/cm)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[2],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 600,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  { name: "Turbidity", unit: "(NTU)", color: pastelColors[3], min: 0, max: 12 },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Dissolved Solids",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[4],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 250,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Alkalinity",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[5],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 120,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Hardness",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[6],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 100,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  { name: "Chlorine", unit: "(mg/L)", color: pastelColors[7], min: 0, max: 6 },
							 | 
						|||
| 
								 | 
							
								  { name: "Nitrate", unit: "(mg/L)", color: pastelColors[8], min: 0, max: 25 },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Phosphate",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[9],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 12,
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  { name: "Sulfate", unit: "(mg/L)", color: pastelColors[10], min: 0, max: 60 },
							 | 
						|||
| 
								 | 
							
								  { name: "Ammonia", unit: "(mg/L)", color: pastelColors[11], min: 0, max: 18 },
							 | 
						|||
| 
								 | 
							
								  { name: "Nitrite", unit: "(mg/L)", color: pastelColors[12], min: 0, max: 6 },
							 | 
						|||
| 
								 | 
							
								  { name: "BOD", unit: "(mg/L)", color: pastelColors[13], min: 0, max: 35 },
							 | 
						|||
| 
								 | 
							
								  { name: "COD", unit: "(mg/L)", color: pastelColors[14], min: 0, max: 120 },
							 | 
						|||
| 
								 | 
							
								];
							 | 
						|||
| 
								 | 
							
								// NUM_GRAPHS까지 자동 생성
							 | 
						|||
| 
								 | 
							
								if (yAxisList.length < NUM_GRAPHS) {
							 | 
						|||
| 
								 | 
							
								  for (let i = yAxisList.length; i < NUM_GRAPHS; i++) {
							 | 
						|||
| 
								 | 
							
								    yAxisList.push({
							 | 
						|||
| 
								 | 
							
								      name: `Graph ${i + 1}`,
							 | 
						|||
| 
								 | 
							
								      unit: "",
							 | 
						|||
| 
								 | 
							
								      color: pastelColors[i % pastelColors.length],
							 | 
						|||
| 
								 | 
							
								      min: 0,
							 | 
						|||
| 
								 | 
							
								      max: 100 + (i % 10) * 100, // 100, 200, ... 1000 반복
							 | 
						|||
| 
								 | 
							
								    });
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// x축 간격(초) - 5초 또는 1분(60초) 등으로 변경 가능
							 | 
						|||
| 
								 | 
							
								// 예시: const X_INTERVAL = 5;   // 5초 간격
							 | 
						|||
| 
								 | 
							
								//      const X_INTERVAL = 60;  // 1분 간격
							 | 
						|||
| 
								 | 
							
								const X_INTERVAL = 600; // 필요시 60으로 변경
							 | 
						|||
| 
								 | 
							
								// 100시간치, X_INTERVAL 간격
							 | 
						|||
| 
								 | 
							
								const xLabels = Array.from(
							 | 
						|||
| 
								 | 
							
								  { length: (100 * 60 * 60) / X_INTERVAL + 1 },
							 | 
						|||
| 
								 | 
							
								  (_, i) => i * X_INTERVAL
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 부드러운 곡선형 + 구간별 변화 가데이터 생성 함수 (xLabels를 파라미터로 받도록 변경)
							 | 
						|||
| 
								 | 
							
								function smoothData(
							 | 
						|||
| 
								 | 
							
								  min: number,
							 | 
						|||
| 
								 | 
							
								  max: number,
							 | 
						|||
| 
								 | 
							
								  xLabels: number[],
							 | 
						|||
| 
								 | 
							
								  phase = 0,
							 | 
						|||
| 
								 | 
							
								  _amp = 1, // amp는 사용하지 않으므로 _amp로 변경
							 | 
						|||
| 
								 | 
							
								  offset = 0,
							 | 
						|||
| 
								 | 
							
								  seriesIndex: number
							 | 
						|||
| 
								 | 
							
								) {
							 | 
						|||
| 
								 | 
							
								  let _prevValue = 0.5;
							 | 
						|||
| 
								 | 
							
								  const values = [];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 데이터 범위 축소: min+10% ~ max-10%
							 | 
						|||
| 
								 | 
							
								  const rangeMin = min + (max - min) * 0.1;
							 | 
						|||
| 
								 | 
							
								  const rangeMax = max - (max - min) * 0.1;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 시리즈별 패턴 다양화: 증가/감소/진동/트렌드 섞기
							 | 
						|||
| 
								 | 
							
								  // 패턴 결정 (시리즈 인덱스에 따라)
							 | 
						|||
| 
								 | 
							
								  const trendType = seriesIndex % 4; // 0:증가, 1:감소, 2:진동, 3:랜덤
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 진동폭(ampVar, randomFactor)은 원복 (더 낮게)
							 | 
						|||
| 
								 | 
							
								  const ampVar = 0.2 + (seriesIndex % 5) * 0.1; // 0.2~0.6
							 | 
						|||
| 
								 | 
							
								  const randomFactor = 0.01 + 0.01 * (seriesIndex % 4); // 0.01~0.04
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  for (let i = 0; i < xLabels.length; i++) {
							 | 
						|||
| 
								 | 
							
								    const t = i / (xLabels.length - 1);
							 | 
						|||
| 
								 | 
							
								    let base;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 오르내림(파동 주기/계수)을 키움 (파동이 더 자주, 더 크게)
							 | 
						|||
| 
								 | 
							
								    if (trendType === 0) {
							 | 
						|||
| 
								 | 
							
								      base = 0.2 + 0.6 * t + 0.13 * Math.sin(phase + t * Math.PI * ampVar * 8);
							 | 
						|||
| 
								 | 
							
								    } else if (trendType === 1) {
							 | 
						|||
| 
								 | 
							
								      base = 0.8 - 0.6 * t + 0.13 * Math.cos(phase + t * Math.PI * ampVar * 8);
							 | 
						|||
| 
								 | 
							
								    } else if (trendType === 2) {
							 | 
						|||
| 
								 | 
							
								      base = 0.5 + 0.22 * Math.sin(phase + t * Math.PI * ampVar * 12);
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      base = 0.5 + 0.08 * Math.sin(phase + t * Math.PI * ampVar * 6) + 0.2 * t;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 노이즈는 낮게
							 | 
						|||
| 
								 | 
							
								    const noise = (Math.random() - 0.5) * randomFactor;
							 | 
						|||
| 
								 | 
							
								    base += noise;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    base = Math.max(0, Math.min(1, base));
							 | 
						|||
| 
								 | 
							
								    const value = +(rangeMin + (rangeMax - rangeMin) * base + offset).toFixed(
							 | 
						|||
| 
								 | 
							
								      2
							 | 
						|||
| 
								 | 
							
								    );
							 | 
						|||
| 
								 | 
							
								    _prevValue = base;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    values.push([xLabels[i], value]);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  return values;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 시리즈 데이터 30개 생성 → 각 그래프마다 12개 라인(시리즈)로 확장
							 | 
						|||
| 
								 | 
							
								const NUM_LINES_PER_GRAPH = 12;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const seriesList = yAxisList.map((y, idx) => {
							 | 
						|||
| 
								 | 
							
								  // 12개 라인(시리즈) 생성
							 | 
						|||
| 
								 | 
							
								  const lines = Array.from({ length: NUM_LINES_PER_GRAPH }, (_, lineIdx) => ({
							 | 
						|||
| 
								 | 
							
								    name: String(lineIdx + 1), // "1", "2", ... "12"
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[lineIdx % pastelColors.length],
							 | 
						|||
| 
								 | 
							
								    data: smoothData(
							 | 
						|||
| 
								 | 
							
								      y.min,
							 | 
						|||
| 
								 | 
							
								      y.max,
							 | 
						|||
| 
								 | 
							
								      xLabels,
							 | 
						|||
| 
								 | 
							
								      Math.PI / (idx + 1 + lineIdx), // phase를 다르게
							 | 
						|||
| 
								 | 
							
								      1,
							 | 
						|||
| 
								 | 
							
								      0,
							 | 
						|||
| 
								 | 
							
								      idx + lineIdx * 2 // 패턴 다양화
							 | 
						|||
| 
								 | 
							
								    ),
							 | 
						|||
| 
								 | 
							
								  }));
							 | 
						|||
| 
								 | 
							
								  return {
							 | 
						|||
| 
								 | 
							
								    name: y.name,
							 | 
						|||
| 
								 | 
							
								    unit: y.unit,
							 | 
						|||
| 
								 | 
							
								    color: y.color,
							 | 
						|||
| 
								 | 
							
								    min: y.min,
							 | 
						|||
| 
								 | 
							
								    max: y.max,
							 | 
						|||
| 
								 | 
							
								    lines, // 12개 시리즈 배열
							 | 
						|||
| 
								 | 
							
								  };
							 | 
						|||
| 
								 | 
							
								});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 차트 렌더링
							 | 
						|||
| 
								 | 
							
								const graphRefs = ref<Record<string, HTMLDivElement | null>>({});
							 | 
						|||
| 
								 | 
							
								const chartInstances = ref<Record<string, echarts.ECharts | null>>({});
							 | 
						|||
| 
								 | 
							
								function setGraphRef(name: string, el: Element | null) {
							 | 
						|||
| 
								 | 
							
								  if (el && el instanceof HTMLDivElement) {
							 | 
						|||
| 
								 | 
							
								    graphRefs.value[name] = el;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function renderAllCharts() {
							 | 
						|||
| 
								 | 
							
								  nextTick(() => {
							 | 
						|||
| 
								 | 
							
								    seriesList.forEach(series => {
							 | 
						|||
| 
								 | 
							
								      const el = graphRefs.value[series.name];
							 | 
						|||
| 
								 | 
							
								      if (!el) return;
							 | 
						|||
| 
								 | 
							
								      if (chartInstances.value[series.name]) {
							 | 
						|||
| 
								 | 
							
								        chartInstances.value[series.name]?.dispose();
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      const chart = echarts.init(el);
							 | 
						|||
| 
								 | 
							
								      chart.setOption({
							 | 
						|||
| 
								 | 
							
								        grid: { left: 50, right: 20, top: 30, bottom: 40, containLabel: true },
							 | 
						|||
| 
								 | 
							
								        xAxis: {
							 | 
						|||
| 
								 | 
							
								          type: "value",
							 | 
						|||
| 
								 | 
							
								          axisLabel: {
							 | 
						|||
| 
								 | 
							
								            fontSize: 11,
							 | 
						|||
| 
								 | 
							
								            color: "#666",
							 | 
						|||
| 
								 | 
							
								            formatter: (v: number) =>
							 | 
						|||
| 
								 | 
							
								              `${Math.floor(v / 3600)}:${String(Math.floor((v % 3600) / 60)).padStart(2, "0")}`,
							 | 
						|||
| 
								 | 
							
								          },
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								        yAxis: {
							 | 
						|||
| 
								 | 
							
								          type: "value",
							 | 
						|||
| 
								 | 
							
								          min: series.min,
							 | 
						|||
| 
								 | 
							
								          max: series.max,
							 | 
						|||
| 
								 | 
							
								          name: series.unit ? `${series.name} ${series.unit}` : series.name,
							 | 
						|||
| 
								 | 
							
								          nameTextStyle: {
							 | 
						|||
| 
								 | 
							
								            color: series.color,
							 | 
						|||
| 
								 | 
							
								            fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								            fontSize: 12,
							 | 
						|||
| 
								 | 
							
								          },
							 | 
						|||
| 
								 | 
							
								          axisLabel: { color: series.color, fontWeight: "bold", fontSize: 12 },
							 | 
						|||
| 
								 | 
							
								          axisLine: { lineStyle: { color: series.color, width: 2 } },
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								        series: series.lines.map(line => ({
							 | 
						|||
| 
								 | 
							
								          name: line.name,
							 | 
						|||
| 
								 | 
							
								          data: line.data,
							 | 
						|||
| 
								 | 
							
								          type: "line",
							 | 
						|||
| 
								 | 
							
								          lineStyle: { color: line.color, width: 1 },
							 | 
						|||
| 
								 | 
							
								          symbol: "none",
							 | 
						|||
| 
								 | 
							
								        })),
							 | 
						|||
| 
								 | 
							
								        animation: false,
							 | 
						|||
| 
								 | 
							
								        legend: {
							 | 
						|||
| 
								 | 
							
								          show: true,
							 | 
						|||
| 
								 | 
							
								          bottom: 8, // 하단에 위치
							 | 
						|||
| 
								 | 
							
								          left: "center", // 중앙 정렬
							 | 
						|||
| 
								 | 
							
								          orient: "horizontal",
							 | 
						|||
| 
								 | 
							
								          itemWidth: 18,
							 | 
						|||
| 
								 | 
							
								          itemHeight: 10,
							 | 
						|||
| 
								 | 
							
								          icon: "circle",
							 | 
						|||
| 
								 | 
							
								          textStyle: { fontSize: 12, padding: [0, 4, 0, 0] },
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								        tooltip: { show: true, trigger: "axis" },
							 | 
						|||
| 
								 | 
							
								      });
							 | 
						|||
| 
								 | 
							
								      chartInstances.value[series.name] = chart;
							 | 
						|||
| 
								 | 
							
								    });
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 이미지 복사 함수 추가
							 | 
						|||
| 
								 | 
							
								async function copyImageToClipboard(dataUrl: string) {
							 | 
						|||
| 
								 | 
							
								  const res = await fetch(dataUrl);
							 | 
						|||
| 
								 | 
							
								  const blob = await res.blob();
							 | 
						|||
| 
								 | 
							
								  await navigator.clipboard.write([
							 | 
						|||
| 
								 | 
							
								    new window.ClipboardItem({ [blob.type]: blob }),
							 | 
						|||
| 
								 | 
							
								  ]);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								async function copyChartImage(name: string) {
							 | 
						|||
| 
								 | 
							
								  const chart = chartInstances.value[name];
							 | 
						|||
| 
								 | 
							
								  if (!chart) return;
							 | 
						|||
| 
								 | 
							
								  const dataUrl = chart.getDataURL({
							 | 
						|||
| 
								 | 
							
								    type: "png",
							 | 
						|||
| 
								 | 
							
								    pixelRatio: 2,
							 | 
						|||
| 
								 | 
							
								    backgroundColor: "#fff",
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								  try {
							 | 
						|||
| 
								 | 
							
								    await copyImageToClipboard(dataUrl);
							 | 
						|||
| 
								 | 
							
								    alert("그래프가 클립보드에 복사되었습니다!");
							 | 
						|||
| 
								 | 
							
								  } catch {
							 | 
						|||
| 
								 | 
							
								    alert("클립보드 복사에 실패했습니다. 브라우저 권한을 확인하세요.");
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								onMounted(() => {
							 | 
						|||
| 
								 | 
							
								  renderAllCharts();
							 | 
						|||
| 
								 | 
							
								});
							 | 
						|||
| 
								 | 
							
								</script>
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								<style scoped>
							 | 
						|||
| 
								 | 
							
								.multi-graph-list {
							 | 
						|||
| 
								 | 
							
								  display: grid;
							 | 
						|||
| 
								 | 
							
								  grid-template-columns: repeat(4, 1fr);
							 | 
						|||
| 
								 | 
							
								  gap: 20px 1.5%;
							 | 
						|||
| 
								 | 
							
								  padding: 16px 0;
							 | 
						|||
| 
								 | 
							
								  align-content: flex-start;
							 | 
						|||
| 
								 | 
							
								  flex-direction: row;
							 | 
						|||
| 
								 | 
							
								  background: #fff;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.single-graph {
							 | 
						|||
| 
								 | 
							
								  min-height: 260px;
							 | 
						|||
| 
								 | 
							
								  width: 100%;
							 | 
						|||
| 
								 | 
							
								  flex: 0 0 100%;
							 | 
						|||
| 
								 | 
							
								  max-width: 100%;
							 | 
						|||
| 
								 | 
							
								  background: #fff;
							 | 
						|||
| 
								 | 
							
								  border: 1px solid #e0e0e0;
							 | 
						|||
| 
								 | 
							
								  border-radius: 8px;
							 | 
						|||
| 
								 | 
							
								  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
							 | 
						|||
| 
								 | 
							
								  padding: 10px 16px 10px 16px;
							 | 
						|||
| 
								 | 
							
								  position: relative;
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  flex-direction: column;
							 | 
						|||
| 
								 | 
							
								  margin-bottom: 20px;
							 | 
						|||
| 
								 | 
							
								  align-items: flex-start;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.graph-title {
							 | 
						|||
| 
								 | 
							
								  font-weight: bold;
							 | 
						|||
| 
								 | 
							
								  font-size: 1.05rem;
							 | 
						|||
| 
								 | 
							
								  color: #1976d2;
							 | 
						|||
| 
								 | 
							
								  margin-bottom: 6px;
							 | 
						|||
| 
								 | 
							
								  width: 100%;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.echarts-graph {
							 | 
						|||
| 
								 | 
							
								  min-height: 200px;
							 | 
						|||
| 
								 | 
							
								  height: 200px !important;
							 | 
						|||
| 
								 | 
							
								  width: 100%;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.copy-btn {
							 | 
						|||
| 
								 | 
							
								  margin-left: 10px;
							 | 
						|||
| 
								 | 
							
								  font-size: 0.9rem;
							 | 
						|||
| 
								 | 
							
								  padding: 2px 8px;
							 | 
						|||
| 
								 | 
							
								  border: 1px solid #1976d2;
							 | 
						|||
| 
								 | 
							
								  background: #e3f2fd;
							 | 
						|||
| 
								 | 
							
								  color: #1976d2;
							 | 
						|||
| 
								 | 
							
								  border-radius: 4px;
							 | 
						|||
| 
								 | 
							
								  cursor: pointer;
							 | 
						|||
| 
								 | 
							
								  transition: background 0.2s;
							 | 
						|||
| 
								 | 
							
								  float: right;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.copy-btn:hover {
							 | 
						|||
| 
								 | 
							
								  background: #bbdefb;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								</style>
							 |