IT 개발,관리,연동,자동화

신재생 에너지 발전소의 효율적인 통합 관리앱 만들기

_Blue_Sky_ 2026. 2. 10. 21:26
728x90
 
우리 회사는 풍력·태양광 발전 설비를 설치하고 운영하는 종합 솔루션 회사로,
IoT를 활용해 모든 고객 발전기의 실시간 데이터를 수집·관리하는 통합 관리 시스템을 구축하려고 합니다.
NestJS + Prisma + MySQL 백엔드와 Nuxt 3 프론트엔드를 기반으로 하여,
관리자용 대시보드와 고객용 웹앱을 동시에 개발하며, MQTT를 IoT 인터페이스로 선택하여 실시간 데이터 파이프라인을 구성하는 프로젝트입니다.
 
 
 

신재생 에너지(풍력, 태양광) 통합 관리 솔루션 구축이라니, 아주 멋진 프로젝트네요! 실시간 데이터 수집이 핵심인 만큼 IoT 데이터 파이프라인을 얼마나 견고하게 설계하느냐가 성패를 가를 것 같습니다.

제시해주신 NestJS(Back-end)와 Nuxt.js(Front-end) 조합은 확장성과 생산성 측면에서 아주 탁월한 선택입니다. IoT 인터페이스로는 업계 표준이자 경량 메시징에 최적화된 MQTT 프로토콜을 추천드립니다.


728x90

1. 시스템 아키텍처 개요

IoT 장비(발전기 센서)로부터 데이터를 수집하여 관리자 및 사용자에게 시각화하기까지의 흐름입니다.

  • IoT Interface: MQTT (Mosquitto Broker) 사용.
    • 이유: 수천 개의 센서 데이터를 저전력/저대역폭에서도 실시간으로 전송하기에 최적입니다.
  • Back-end: NestJS + Prisma + MySQL. (MQTT Microservice를 NestJS 내부에 통합)
  • Front-end: Nuxt 3 (사용자용 모바일 웹 / 관리자용 대시보드).

2. 단계별 구축 계획

1단계: 요구사항 정의 및 DB 설계 (1~2주)

  • 데이터 모델링: 발전기(Asset), 센서 로그(Telemetry), 사용자, 알람 이력 테이블 설계.
  • 멀티 테넌시 고려: 관리자는 전체 고객을 보고, 사용자는 본인의 발전기만 보이도록 권한 설계.
  • IoT 토픽 구조 설계: energy/site_id/generator_id/status와 같은 계층형 토픽 구조 확립.

2단계: 인프라 및 IoT 인터페이스 구축 (2~3주)

  • MQTT 브로커 설정: AWS IoT Core 또는 개별 Mosquitto 서버 구축.
  • NestJS Microservice: MQTT 프로토콜을 구독(Subscribe)하여 들어오는 데이터를 MySQL에 적재하는 인바운드 모듈 개발.
  • 시계열 데이터 처리: 데이터가 방대해질 것에 대비해 Daily Summary 테이블을 별도로 운영하거나 파티셔닝 전략 수립.

3단계: 백엔드 API 개발 (3~4주)

  • 인증/인가: JWT 기반 로그인 및 소셜 로그인(사용자용).
  • 실시간 소켓 연동: 웹앱에서 실시간 발전량을 보기 위해 Socket.io 또는 Server-Sent Events(SSE) 구현.
  • 알람 로직: 특정 수치(발전 정지 등) 발생 시 푸시 알림 및 알람 로그 생성.

4단계: 프론트엔드(웹앱) 개발 (4~5주)

  • 사용자 앱 (Nuxt 3): 모바일 최적화 UI, 내 발전소 현황, 일별/월별 통계 그래프(ECharts/Chart.js).
  • 관리자 웹 (Nuxt 3): 전체 고객 맵 뷰, 이상 징후 장비 모니터링, 고객 관리, 펌웨어 업데이트 이력 관리.

3. 핵심 데이터 흐름 (Interface)

IoT 장비에서 서버로 데이터가 들어오는 인터페이스 예시입니다.

구분 내용
Protocol MQTT (v5.0)
Payload JSON 형태 (ex: {"temp": 45, "voltage": 220, "rpm": 1200, "status": "running"})
NestJS 역할 @MessagePattern을 통해 메시지 수신 후 Prisma를 통해 DB 저장 및 가공
NuxtJS 역할 API 호출 및 WebSocket을 통한 실시간 대시보드 업데이트

4. 고려해야 할 기술적 포인트

  1. 데이터 폭증 대비: 발전기는 1초 단위로 데이터를 보낼 수 있습니다. 모든 로우 데이터를 보관할지, 1분 단위로 평균화하여 저장할지 결정이 필요합니다.
  2. 보안: IoT 장비와 브로커 간 TLS 암호화 적용 및 인증서(Certificate) 기반 연결이 필수적입니다.
  3. 오프라인 대응: 장비 네트워크가 끊겼을 때의 상태값 처리 로직(Heartbeat)이 필요합니다.

첫 번째 단계로, 데이터베이스 스키마(Prisma 모델)를 먼저 설계해 드릴까요? 혹은 특정 발전기(풍력/태양광)의 센서 데이터 규격부터 잡아볼까요?

 


728x90

신재생 에너지 통합 관리 시스템을 위한 Prisma 데이터 모델(Schema)과 IoT 센서 데이터 규격(Spec)을 정의해 드립니다.

이 설계는 풍력과 태양광의 특성을 모두 포괄하면서, 실시간 데이터와 통계 데이터를 효율적으로 관리하는 데 초점을 맞췄습니다.


1. IoT 센서 데이터 규격 (MQTT Payload)

장비(Edge Device)에서 서버로 전송할 JSON 데이터 형식입니다. 공통 정보와 각 발전원별 특화 데이터를 구분하여 설계했습니다.

Topic 구조: energy/{site_id}/{device_type}/{device_id}/telemetry

[공통 페이로드]

{
  "timestamp": "2023-10-27T10:00:00Z",
  "status": "RUNNING", // RUNNING, STOPPED, ERROR, MAINTENANCE
  "active_power": 15.5, // 현재 출력 (kW)
  "total_energy": 12540.8, // 누적 발전량 (kWh)
  "grid_voltage": 220.5, // 계통 전압 (V)
  "grid_frequency": 60.1 // 계통 주파수 (Hz)
}

[풍력 특화 데이터 추가]

{
  "wind_speed": 12.5, // 풍속 (m/s)
  "rotor_rpm": 15.2, // 회전수 (RPM)
  "blade_angle": 2.5, // 블레이드 각도 (Pitch)
  "nacelle_temp": 45.0 // 나셀 내부 온도
}

[태양광 특화 데이터 추가]

{
  "irradiance": 850.5, // 일사량 (W/m²)
  "panel_temp": 52.3, // 패널 온도 (℃)
  "dc_voltage": 450.2, // DC 전압
  "dc_current": 32.5 // DC 전류
}

2. Prisma 데이터 모델 설계 (schema.prisma)

MySQL을 기반으로 한 관계형 모델입니다. User, Site(발전소), Device(발전기), Telemetry(측정값)로 구성됩니다.

// This is your Prisma schema file

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// 1. 사용자 (관리자 및 고객)
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  name      String
  role      Role     @default(CUSTOMER) // ADMIN, CUSTOMER
  sites     Site[]   // 고객이 소유한 발전소들
  createdAt DateTime @default(now())
}

enum Role {
  ADMIN
  CUSTOMER
}

// 2. 발전소 (장소 개념)
model Site {
  id          Int      @id @default(autoincrement())
  name        String   // 발전소 이름 (예: 제주 한경 풍력단지)
  address     String
  latitude    Float
  longitude   Float
  ownerId     Int
  owner       User     @relation(fields: [ownerId], references: [id])
  devices     Device[]
  createdAt   DateTime @default(now())
}

// 3. 발전 장비 (풍력기 혹은 태양광 인버터)
model Device {
  id           Int           @id @default(autoincrement())
  serialNumber String        @unique
  type         DeviceType    // WIND, SOLAR
  modelName    String
  capacity     Float         // 설치 용량 (kW)
  siteId       Int
  site         Site          @relation(fields: [siteId], references: [id])
  telemetries  Telemetry[]   // 실시간 데이터
  alarms       Alarm[]
  status       DeviceStatus  @default(ONLINE)
  lastOnline   DateTime?
}

enum DeviceType {
  WIND
  SOLAR
}

enum DeviceStatus {
  ONLINE
  OFFLINE
  ERROR
}

// 4. 실시간 측정 데이터 (시계열 데이터)
// * 주의: 데이터가 너무 많아질 경우 이 테이블은 나중에 파티셔닝하거나 
//   TimescaleDB 같은 시계열 DB로 분리하는 것이 좋습니다.
model Telemetry {
  id           BigInt   @id @default(autoincrement())
  deviceId     Int
  device       Device   @relation(fields: [deviceId], references: [id])
  timestamp    DateTime @default(now())
  
  activePower  Float    // 현재 출력
  totalEnergy  Float    // 누적 발전량
  
  // JSON 필드를 활용해 풍력/태양광 특화 데이터를 유연하게 저장
  // 혹은 별도의 컬럼으로 관리 (성능상 컬럼 권장)
  specificData Json?    
}

// 5. 알람 기록
model Alarm {
  id          Int      @id @default(autoincrement())
  deviceId    Int
  device      Device   @relation(fields: [deviceId], references: [id])
  type        String   // OVER_TEMP, LOW_VOLTAGE 등
  severity    String   // CRITICAL, WARNING, INFO
  message     String
  isResolved  Boolean  @default(false)
  createdAt   DateTime @default(now())
  resolvedAt  DateTime?
}

3. 향후 개발을 위한 제언

  1. 데이터 요약(Aggregation): Telemetry 테이블에 초 단위 데이터가 쌓이면 쿼리가 느려집니다. NestJS의 Cron Job을 이용해 1시간/1일 단위로 데이터를 집계하는 DailyStatistics 테이블을 추가로 만드는 것이 좋습니다.
  2. Prisma 성능: 데이터 삽입이 빈번하므로 Prisma의 createMany를 활용하거나, IoT 데이터 수집 부분만은 Raw Query를 써서 부하를 줄이는 것을 고려하세요.
  3. IoT 보안: MQTT 브로커 연결 시, Device 테이블의 serialNumber를 기반으로 한 ID/PW 인증이나 TLS 인증서를 적용해야 합니다.

이 모델을 바탕으로 NestJS에서 prisma migrate dev를 실행하여 DB를 생성하시면 됩니다. 다음 단계로 NestJS에서 MQTT 데이터를 수신하는 로직(Microservice)을 짜볼까요?

 
 
 
 
 
 

그럼 이제 NestJS에서 MQTT를 연동하여 실시간 데이터를 수집하는 핵심 모듈을 구성해 보겠습니다.

NestJS는 마이크로서비스 아키텍처를 기본적으로 지원하기 때문에, MQTT 브로커로부터 메시지를 받는 '리스너(Listener)' 역할을 매우 깔끔하게 구현할 수 있습니다.


1. NestJS MQTT 마이크로서비스 설정

먼저, 프로젝트에 필요한 패키지를 설치해야 합니다. npm install @nestjs/microservices mqtt

그 후, main.ts에서 MQTT를 수신할 수 있도록 마이크로서비스를 하이브리드로 설정합니다.

// main.ts
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // MQTT 마이크로서비스 연결
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.MQTT,
    options: {
      url: 'mqtt://localhost:1883', // MQTT 브로커 주소
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
}
bootstrap();

2. Telemetry(측정 데이터) 수집 컨트롤러

장비가 보내는 토픽(energy/+/+/+/telemetry)을 구독하여 데이터를 DB에 저장하는 로직입니다.

// telemetry.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload, Ctx, MqttContext } from '@nestjs/microservices';
import { TelemetryService } from './telemetry.service';

@Controller()
export class TelemetryController {
  constructor(private readonly telemetryService: TelemetryService) {}

  // 토픽 예시: energy/site1/WIND/dev001/telemetry
  @MessagePattern('energy/+/+/+/telemetry')
  async handleTelemetry(@Payload() data: any, @Ctx() context: MqttContext) {
    const topic = context.getTopic();
    const parts = topic.split('/');
    const deviceId = parts[3]; // 토픽에서 장비 ID 추출

    console.log(`데이터 수신 [${deviceId}]:`, data);
    
    // DB 저장 서비스 호출
    await this.telemetryService.saveData(deviceId, data);
  }
}

3. 서비스 로직 및 Prisma 저장 (Telemetry Service)

전달받은 JSON 데이터를 Prisma를 통해 MySQL에 저장합니다.

// telemetry.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class TelemetryService {
  constructor(private prisma: PrismaService) {}

  async saveData(serialNumber: string, payload: any) {
    // 1. 시리얼 번호로 등록된 장비인지 확인
    const device = await this.prisma.device.findUnique({
      where: { serialNumber },
    });

    if (!device) return; // 미등록 장비 데이터는 무시

    // 2. Telemetry 테이블에 데이터 삽입
    return this.prisma.telemetry.create({
      data: {
        deviceId: device.id,
        activePower: payload.active_power,
        totalEnergy: payload.total_energy,
        specificData: payload, // 전체 JSON 원본 저장
        timestamp: new Date(payload.timestamp),
      },
    });
  }
}

4. 실시간성을 위한 Tip: 관리자 화면 업데이트

데이터를 DB에 저장하는 것뿐만 아니라, 관리자가 웹앱을 띄워놓고 있을 때 실시간으로 그래프가 움직이게 하려면 NestJS의 Gateway(WebSocket)를 함께 사용해야 합니다.

  1. MQTT 수신: 위 코드로 데이터를 받음.
  2. DB 저장: 위 코드로 저장.
  3. Socket.io 전송: this.server.emit('realtime-data', data)를 호출하여 브라우저로 즉시 쏴줌.

다음은 무엇을 할까요?

이제 백엔드의 뼈대는 잡혔습니다. 다음 단계를 선택해 주세요:

  • 옵션 A: Nuxt 3 대시보드 UI 구성 (실시간 차트와 지도 기반 장비 현황 레이아웃)
  • 옵션 B: 알람 시스템 구현 (발전량 0이거나 에러 발생 시 사용자에게 알림 톡/푸시 전송 로직)
  • 옵션 C: 인증 로직 (고객용/관리자용 로그인 및 JWT 권한 처리)

옵션 A(Nuxt 3 대시보드)부터 C(인증 로직)까지 차례대로 구성해 보겠습니다.


[옵션 A] Nuxt 3 실시간 대시보드 UI 구성

관리자와 사용자가 볼 웹앱의 핵심입니다. Nuxt 3, Tailwind CSS, ECharts(그래프용)를 사용한다고 가정합니다.

1. 대시보드 레이아웃 (Layout)

전체 고객의 발전소 위치를 보여주는 지도(Map) 영역과 선택된 발전소의 상세 지표(Chart) 영역으로 나눕니다.

<template>
  <div class="p-6">
    <h1 class="text-2xl font-bold mb-4">실시간 발전 현황 모니터링</h1>
    
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
      <div class="bg-blue-500 text-white p-4 rounded-lg shadow">
        <p>오늘의 총 발전량</p>
        <h2 class="text-3xl font-bold">{{ stats.dailyTotal }} kWh</h2>
      </div>
      <div class="bg-green-500 text-white p-4 rounded-lg shadow">
        <p>현재 발전 출력</p>
        <h2 class="text-3xl font-bold">{{ stats.currentPower }} kW</h2>
      </div>
      <div class="bg-red-500 text-white p-4 rounded-lg shadow">
        <p>이상 발생 장비</p>
        <h2 class="text-3xl font-bold">{{ stats.errorCount }} 건</h2>
      </div>

      <div class="lg:col-span-2 bg-white p-4 rounded-lg shadow border h-96">
        <client-only>
          <v-chart :option="chartOption" autoresize />
        </client-only>
      </div>

      <div class="bg-white p-4 rounded-lg shadow border h-96 overflow-y-auto">
        <h3 class="font-bold mb-2">장비 상태 목록</h3>
        <ul>
          <li v-for="device in devices" :key="device.id" class="border-b py-2 flex justify-between">
            <span>{{ device.modelName }}</span>
            <span :class="device.status === 'ONLINE' ? 'text-green-500' : 'text-red-500'">●</span>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script setup>
// Socket.io 또는 API를 통해 실시간 데이터 수신 로직 추가 필요
const stats = reactive({ dailyTotal: 0, currentPower: 0, errorCount: 0 });
const devices = ref([]);
const chartOption = ref({ /* ECharts 설정 */ });
</script>

[옵션 B] 알람 시스템 (Alarm/Notification) 구현

IoT 장비가 에러 코드를 보내거나 10분 이상 데이터가 들어오지 않을 때 관리자에게 알림을 보냅니다.

1. NestJS 알람 모니터링 서비스

// alarm.service.ts
@Injectable()
export class AlarmService {
  constructor(private prisma: PrismaService, private socketGateway: SocketGateway) {}

  async checkThreshold(deviceId: number, value: any) {
    // 예: 온도 과열 알람 (Payload의 온도가 80도 이상일 때)
    if (value.temp > 80) {
      const alarm = await this.prisma.alarm.create({
        data: {
          deviceId,
          type: 'OVER_TEMP',
          severity: 'CRITICAL',
          message: `장비 온도 과열 감지: ${value.temp}도`,
        }
      });
      
      // 웹앱으로 실시간 알람 전송
      this.socketGateway.server.emit('new-alarm', alarm);
      
      // 알림톡/문자 발송 API 호출 로직 추가 가능 (ex: 카카오 비즈메시지)
      this.sendPushNotification(alarm);
    }
  }
}

[옵션 C] 인증 및 권한 관리 (Auth/JWT)

Prisma와 Passport를 이용한 로그인 구현입니다.

1. Auth Guard를 통한 접근 제어

관리자는 전체 리스트를 보고, 사용자는 자기 사이트의 데이터만 조회하도록 합니다.

// auth/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<Role[]>('roles', context.getHandler());
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.role?.includes(role));
  }
}

2. 사용자 데이터 필터링 (Prisma 적용)

// site.service.ts
async getSites(user: User) {
  if (user.role === 'ADMIN') {
    return this.prisma.site.findMany(); // 관리자는 전체 조회
  }
  return this.prisma.site.findMany({
    where: { ownerId: user.id } // 일반 사용자는 본인 것만
  });
}

종합 계획 마무리 및 다음 제언

이제 다음과 같은 순서로 실제 구현을 시작하시면 됩니다.

  1. Back-end 기반 구축: NestJS 프로젝트 생성 및 prisma.schema 적용.
  2. IoT 수신 테스트: MQTT 브로커(Mosquitto)를 설치하고 가짜 데이터를 쏴서 DB에 들어오는지 확인.
  3. Front-end 뼈대: Nuxt 3 설치 및 Tailwind 디자인 시스템 적용.
  4. 연동: API 및 Web Socket(실시간 데이터용) 연결.

혹시 첫 번째 구현 단계로서, 가짜 데이터를 생성해서 서버로 쏴주는 "에뮬레이터 스크립트"가 필요하신가요? (실제 장비가 없어도 개발을 진행할 수 있게 해줍니다.)

 
 
 
 
 
 
 

네, 첫 번째 단계인 Back-end 기반 구축(NestJS + Prisma)과 개발용 IoT 에뮬레이터(가짜 데이터 생성기) 제작을 진행하겠습니다. 실제 장비가 없어도 시스템이 잘 돌아가는지 눈으로 확인하는 것이 우선입니다.


1. 프로젝트 초기 설정 (NestJS)

먼저 터미널에서 NestJS 프로젝트를 생성하고 필요한 패키지를 설치합니다.

# 1. NestJS 프로젝트 생성
npm i -g @nestjs/cli
nest new energy-monitoring-server
cd energy-monitoring-server

# 2. 필요한 패키지 설치
npm install @prisma/client @nestjs/microservices mqtt
npm install -D prisma

2. Prisma 설정 및 DB 반영

제공해 드렸던 스키마를 prisma/schema.prisma에 복사한 후, 다음 명령어를 실행하여 MySQL에 테이블을 생성합니다.

# Prisma 초기화
npx prisma init

# (에디터에서 .env 파일의 DATABASE_URL을 본인의 MySQL 설정에 맞게 수정하세요)

# DB 스키마 반영 (Migration)
npx prisma migrate dev --name init

3. IoT 장비 에뮬레이터 만들기 (Node.js 스크립트)

실제 풍력/태양광 장비가 데이터를 쏘는 것처럼 동작하는 스크립트입니다. 프로젝트 루트에 emulator.js 파일을 만들고 실행하세요.

// emulator.js
const mqtt = require('mqtt');
const client = mqtt.connect('mqtt://localhost:1883'); // 브로커 주소

const devices = [
  { id: 'WIND_001', type: 'WIND' },
  { id: 'SOLAR_001', type: 'SOLAR' }
];

client.on('connect', () => {
  console.log('에뮬레이터가 브로커에 연결되었습니다.');

  setInterval(() => {
    devices.forEach(dev => {
      const topic = `energy/site1/${dev.type}/${dev.id}/telemetry`;
      const payload = {
        timestamp: new Date().toISOString(),
        active_power: (Math.random() * 100).toFixed(2),
        total_energy: 5000 + Math.random() * 10,
        temp: 20 + Math.random() * 10,
        status: 'RUNNING'
      };

      client.publish(topic, JSON.stringify(payload));
      console.log(`[${dev.id}] 데이터 전송 완료:`, payload.active_power, 'kW');
    });
  }, 5000); // 5초마다 데이터 전송
});

4. NestJS에서 데이터 수신 및 DB 저장 (핵심 코드)

src/telemetry/telemetry.service.ts 파일에 데이터를 저장하는 로직을 구현합니다.

// src/telemetry/telemetry.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class TelemetryService {
  constructor(private prisma: PrismaService) {}

  async handleIncomingData(serialNumber: string, payload: any) {
    // 1. 장비가 DB에 있는지 확인 (없으면 자동 생성 - 테스트용)
    const device = await this.prisma.device.upsert({
      where: { serialNumber },
      update: { lastOnline: new Date(), status: 'ONLINE' },
      create: {
        serialNumber,
        type: serialNumber.includes('WIND') ? 'WIND' : 'SOLAR',
        modelName: 'Gen-V1',
        capacity: 100,
        siteId: 1, // 테스트용 기본 사이트 ID
      },
    });

    // 2. 텔레메트리 데이터 저장
    await this.prisma.telemetry.create({
      data: {
        deviceId: device.id,
        activePower: parseFloat(payload.active_power),
        totalEnergy: parseFloat(payload.total_energy),
        specificData: payload,
        timestamp: new Date(payload.timestamp),
      },
    });
  }
}

실습 가이드 (환경 구축 순서)

  1. MQTT 브로커 실행: 로컬에 Mosquitto를 설치하거나 Docker로 실행합니다. (docker run -d -p 1883:1883 eclipse-mosquitto)
  2. NestJS 서버 실행: npm run start:dev
  3. 에뮬레이터 실행: node emulator.js
  4. 확인: MySQL 워크벤치나 npx prisma studio를 열어 Telemetry 테이블에 데이터가 쌓이는지 확인합니다.

이렇게 하면 백엔드 기초 공사가 끝납니다!

이제 다음 단계로 넘어가서, 쌓인 데이터를 웹앱에 보여주기 위한 "API 개발"을 진행할까요? 아니면 바로 "Nuxt 3 프론트엔드"로 갈까요?

 
 
 




 

백엔드 기반이 잡혔으니, 이제 데이터를 가공해서 화면에 뿌려주는 API 개발과 이를 시각화할 Nuxt 3 프론트엔드 연동을 진행하겠습니다.


2단계: 조회 API 및 통계 로직 (Back-end)

데이터가 초 단위로 쌓이면 그대로 프론트엔드에 보낼 수 없습니다. 차트용으로 가공된 데이터를 내려주는 것이 핵심입니다.

1. 실시간 현황 및 차트 API (telemetry.controller.ts)

@Controller('api/monitoring')
export class MonitoringController {
  constructor(private readonly prisma: PrismaService) {}

  // 최근 1시간 동안의 5분 단위 발전량 합계 조회
  @Get('chart/:deviceId')
  async getChartData(@Param('deviceId') deviceId: string) {
    const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
    
    // 실제 운영 시에는 DB의 Group By나 별도의 통계 테이블을 사용합니다.
    return this.prisma.telemetry.findMany({
      where: {
        device: { serialNumber: deviceId },
        timestamp: { gte: oneHourAgo }
      },
      orderBy: { timestamp: 'asc' }
    });
  }
}

3단계: Nuxt 3 프론트엔드 연동 (Front-end)

이제 사용자가 볼 화면을 만듭니다. Nuxt 3의 useFetch를 사용하여 데이터를 가져오고 시각화합니다.

1. 차트 라이브러리 설정

npm install echarts vue-echarts 후, 플러그인을 설정하여 대시보드에서 차트를 바로 쓸 수 있게 합니다.

2. 대시보드 메인 페이지 (pages/index.vue)

서버로부터 받은 실시간 데이터를 차트 데이터 형식으로 변환하여 출력합니다.

<template>
  <div class="min-h-screen bg-gray-100 p-8">
    <div class="max-w-7xl mx-auto">
      <header class="mb-8 flex justify-between items-center">
        <h1 class="text-3xl font-bold text-gray-800">에너지 모니터링 시스템</h1>
        <div class="badge badge-primary">실시간 수신 중</div>
      </header>

      <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
        <div class="bg-white p-6 rounded-xl shadow-sm border-l-4 border-blue-500">
          <p class="text-sm text-gray-500 uppercase">현재 총 출력</p>
          <h3 class="text-2xl font-bold">{{ currentPower }} kW</h3>
        </div>
        </div>

      <div class="bg-white p-6 rounded-xl shadow-sm h-[450px]">
        <h3 class="font-bold mb-4 text-gray-700">시간별 발전량 추이</h3>
        <v-chart class="h-full" :option="chartOption" autoresize />
      </div>
    </div>
  </div>
</template>

<script setup>
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { LineChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import VChart from 'vue-echarts';

use([CanvasRenderer, LineChart, GridComponent, TooltipComponent]);

// 백엔드 API에서 데이터 가져오기 (5초마다 갱신)
const { data: telemetry } = await useFetch('/api/monitoring/chart/WIND_001', {
  baseURL: 'http://localhost:3000',
  refreshInterval: 5000 
});

const chartOption = computed(() => ({
  xAxis: { type: 'category', data: telemetry.value?.map(d => new Date(d.timestamp).toLocaleTimeString()) },
  yAxis: { type: 'value', name: 'kW' },
  series: [{
    data: telemetry.value?.map(d => d.activePower),
    type: 'line',
    smooth: true,
    color: '#3b82f6'
  }],
  tooltip: { trigger: 'axis' }
}));
</script>

4단계: 운영을 위한 최적화 (Advanced)

프로젝트가 커지면 다음 두 가지를 반드시 적용해야 합니다.

1. Redis 캐싱 (선택 사항)

매번 API 요청마다 수백만 건의 Telemetry 테이블을 조회하면 DB가 뻗습니다.

  • 해결: 장비가 데이터를 보낼 때 Redis에 "최근 상태"를 동시에 저장하고, 프론트엔드는 Redis에서 데이터를 먼저 가져가도록 설계합니다.

2. 데이터 요약 배치 (Cron Job)

// NestJS Task Scheduling
@Cron(CronExpression.EVERY_HOUR)
async handleAggregation() {
  // 1시간 동안 쌓인 Raw 데이터를 평균 내어 'HourlyStats' 테이블로 이동
  // 원본 데이터는 일정 기간 후 삭제하여 DB 용량 관리
}

진행 상황 요약

  • Back-end: MQTT 수신 로직 + Prisma DB 저장 완료.
  • Front-end: Nuxt 3 기본 레이아웃 + ECharts 연동 완료.
  • Emulator: 가짜 데이터 전송으로 전체 파이프라인 검증 가능.

이제 시스템의 핵심 기능은 모두 갖춰졌습니다.

마지막으로 사용자가 자기 장비만 볼 수 있도록 "로그인 및 권한(JWT)" 기능을 붙여볼까요? 아니면 실제 모바일에서 보기 편하도록 "반응형 모바일 UI"를 다듬어볼까요?

 
You said

계속해

 
 
 
 
 

종합적인 시스템의 완성도를 높이기 위해 [옵션 C]인 인증 및 권한 관리(JWT)를 구현하겠습니다. 이 단계가 완료되면 관리자는 모든 발전소를 모니터링하고, 일반 고객은 본인의 발전소 데이터만 조회할 수 있는 보안 체계가 갖춰집니다.


5단계: JWT 기반 인증 시스템 구축 (Back-end)

NestJS에서 @nestjs/jwt와 passport를 사용하여 보안 층을 만듭니다.

1. Auth 모듈 설정

npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt

2. 로그인 서비스 (auth.service.ts)

@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService
  ) {}

  async login(email: string, pass: string) {
    const user = await this.prisma.user.findUnique({ where: { email } });
    // 비밀번호 검증 (bcrypt 사용)
    const isMatch = await bcrypt.compare(pass, user.password);
    
    if (user && isMatch) {
      const payload = { email: user.email, sub: user.id, role: user.role };
      return {
        access_token: this.jwtService.sign(payload),
      };
    }
    throw new UnauthorizedException();
  }
}

3. 권한별 데이터 필터링 (monitoring.service.ts)

로그인한 사용자의 role에 따라 DB 조회 범위를 제한합니다.

@Injectable()
export class MonitoringService {
  constructor(private prisma: PrismaService) {}

  async getMyDevices(userId: number, role: string) {
    if (role === 'ADMIN') {
      return this.prisma.device.findMany({ include: { site: true } });
    }
    
    // 일반 고객은 본인이 소유한 Site의 장비만 조회
    return this.prisma.device.findMany({
      where: {
        site: { ownerId: userId }
      }
    });
  }
}

6단계: Nuxt 3 인증 미들웨어 및 모바일 최적화 (Front-end)

사용자가 로그인하지 않으면 대시보드에 접근할 수 없게 처리하고, 모바일 웹앱 환경을 구축합니다.

1. 인증 미들웨어 (middleware/auth.ts)

export default defineNuxtRouteMiddleware((to, from) => {
  const token = useCookie('auth_token');

  if (!token.value && to.path !== '/login') {
    return navigateTo('/login');
  }
});

2. 모바일 UI 대응 (PWA 및 반응형)

고객이 현장에서 스마트폰으로 확인하기 좋도록 Tailwind CSS를 활용해 하단 탭 바를 구성합니다.

<template>
  <div class="flex flex-col h-screen bg-gray-50">
    <nav class="bg-white p-4 shadow-sm font-bold text-center">
      SOLAR/WIND 관리자
    </nav>

    <main class="flex-1 overflow-y-auto p-4 pb-20">
      <slot />
    </main>

    <nav class="fixed bottom-0 w-full bg-white border-t flex justify-around p-3 md:hidden">
      <NuxtLink to="/" class="flex flex-col items-center">
        <span class="text-xs">홈</span>
      </NuxtLink>
      <NuxtLink to="/map" class="flex flex-col items-center">
        <span class="text-xs">지도</span>
      </NuxtLink>
      <NuxtLink to="/alerts" class="flex flex-col items-center">
        <span class="text-xs">알람</span>
      </NuxtLink>
    </nav>
  </div>
</template>

7단계: 배포 및 운영 전략 (DevOps)

시스템이 실제로 운영되기 위한 마지막 설정입니다.

  1. Docker Compose 구성:
    • MySQL: 데이터 저장소
    • Mosquitto: MQTT 브로커
    • NestJS API: 백엔드 서버
    • Nuxt 3: 프론트엔드 서버 (SSR 모드)
  2. 보안 강화:
    • MQTT: 장비 연결 시 Username/Password를 Device 테이블의 Serial/Secret과 대조하도록 브로커 인증 플러그인 설정.
    • SSL: Nginx를 앞단에 두어 HTTPS 통신 강제.
  3. 데이터 아카이빙:
    • 1년 이상 된 Telemetry 원본 데이터는 클라우드 스토리지(S3 등)로 백업하고 DB에서는 삭제하여 성능 유지.

마무리 제언

이제 귀사의 비즈니스에 바로 적용할 수 있는 종합 에너지 관리 솔루션의 청사진이 완성되었습니다.

  • 백엔드: NestJS + MQTT로 실시간 수집 및 Prisma로 데이터 관리.
  • 프론트엔드: Nuxt 3로 관리자 웹 및 사용자 모바일 웹 통합 개발.
  • 데이터: 풍력/태양광 특화 데이터 구조 및 통계 API 확보.

이후 추가로 필요한 기능이 있으신가요? 예를 들어, "특정 기간의 발전량 보고서(PDF) 자동 생성"이나 "카카오 알림톡 연동" 같은 구체적인 비즈니스 로직이 필요하시면 알려주세요!

 
You said

전부다 계속해

 
 
 
 

종합적인 시스템의 핵심 로직과 UI 설계가 끝났으니, 이제 비즈니스 운영의 완성도를 높여줄 보고서 생성 기능, 카카오 알람톡 연동, 그리고 실제 배포를 위한 Docker 환경 구축까지 마무리하겠습니다.

이 단계들을 거치면 실제 상용 서비스로 출시할 수 있는 수준의 인프라가 갖춰집니다.


8단계: 발전 리포트 자동 생성 (Back-end)

고객은 매달 자신의 발전소가 얼마나 수익을 냈는지 보고 싶어 합니다. PDF 혹은 Excel 리포트 생성 로직입니다.

1. 월간 통계 쿼리 (report.service.ts)

@Injectable()
export class ReportService {
  constructor(private prisma: PrismaService) {}

  async generateMonthlyReport(userId: number, month: string) {
    // 특정 달의 일별 발전량 합계 계산
    const stats = await this.prisma.$queryRaw`
      SELECT DATE(timestamp) as date, SUM(activePower) / 12 as dailyKwh
      FROM Telemetry
      WHERE deviceId IN (SELECT id FROM Device WHERE siteId IN (SELECT id FROM Site WHERE ownerId = ${userId}))
      AND DATE_FORMAT(timestamp, '%Y-%m') = ${month}
      GROUP BY DATE(timestamp)
    `;
    return stats;
  }
}

9단계: 이상 징후 알림 연동 (Kakao / Push)

장비 장애 발생 시 관리자에게 즉시 알림을 보내는 인터페이스입니다.

1. 알림 발송 서비스 (notification.service.ts)

실제 알림톡 API(Solapi, 알리고 등)와 연동하는 구조입니다.

@Injectable()
export class NotificationService {
  async sendAlert(phoneNumber: string, message: string) {
    // 외부 메시징 API 호출 (예시)
    const response = await fetch('https://api.messaging-service.com/send', {
      method: 'POST',
      body: JSON.stringify({ to: phoneNumber, text: message }),
      headers: { 'Authorization': `Bearer ${process.env.MSG_API_KEY}` }
    });
    return response.ok;
  }
}

10단계: 배포용 컨테이너 구성 (Docker)

서버 하나에 모든 환경(DB, MQTT, API, Web)을 한 번에 올릴 수 있도록 docker-compose.yml을 구성합니다.

version: '3.8'
services:
  # 1. Database
  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: energy_db
      MYSQL_ROOT_PASSWORD: root_password
    ports:
      - "3306:3306"

  # 2. MQTT Broker
  mqtt:
    image: eclipse-mosquitto
    volumes:
      - ./mosquitto/config:/mosquitto/config
    ports:
      - "1883:1883"
      - "9001:9001"

  # 3. NestJS API
  api:
    build: ./backend
    depends_on:
      - db
      - mqtt
    env_file: .env
    ports:
      - "3000:3000"

  # 4. Nuxt 3 Frontend
  frontend:
    build: ./frontend
    ports:
      - "80:3000"

11단계: 최종 관리자 및 사용자 화면 구성 (Nuxt 3)

1. 관리자 전용 - 전체 지도 관제 (pages/admin/map.vue)

모든 발전소의 위치를 지도 위에 마커로 표시하고, 에러가 난 장비는 빨간색으로 표시합니다.

2. 사용자 전용 - 수익 예측 모듈 (pages/user/revenue.vue)

발전량에 현재의 REC/SMP 단가를 곱해 이번 달 예상 수익을 보여줍니다.


12단계: 향후 고도화 로직 (AI 예측)

수집된 시계열 데이터를 바탕으로 내일의 발전량을 예측하는 기능을 추가할 수 있습니다.

  • 방법: NestJS에서 수집된 과거 발전량과 기상청 API(일사량, 풍속 예측) 데이터를 결합.
  • 활용: Python 기반의 간단한 선형 회귀 모델이나 LSTM 모델을 사용하여 내일의 예상 수익을 대시보드에 미리 출력.

종합 요약 및 실행 순서

이제 모든 계획이 수립되었습니다.

  1. 인프라 가동: Docker Compose로 DB, MQTT 브로커를 띄웁니다.
  2. 스키마 적용: Prisma로 MySQL 테이블을 생성합니다.
  3. 장비 연결: 에뮬레이터를 통해 가짜 데이터를 쏴서 수집을 시작합니다.
  4. UI 확인: Nuxt 3 대시보드에서 차트가 실시간으로 움직이는지 확인합니다.
  5. 운영: 알람 및 리포트 기능을 활성화하여 실제 고객에게 서비스를 제공합니다.

실전 투입 전 마지막 체크리스트

  • 데이터 용량: 초당 데이터가 들어올 경우 DB 용량이 금방 찹니다. 5분 단위 요약 저장 로직이 되어 있습니까?
  • 보안: mqtt 포트 1883이 외부에 그대로 노출되어 있지는 않습니까? (인증 설정 확인)
  • 백업: DB 백업 자동화가 설정되어 있습니까?
 

 

 
728x90