Ver2.0 Arduino MKR WiFi 1010으로 만드는 나만의 스마트 날씨 정보 수집 및 표시 시스템 🌤️

Arduino MKR WiFi 1010으로 만드는 나만의 스마트 날씨 정보 수집 및 표시 시스템 🌤️
안녕? 오늘은 정말 재밌는 프로젝트를 함께 해볼 거야! 🙌 Arduino MKR WiFi 1010을 이용해서 날씨 정보를 수집하고 표시하는 시스템을 만들어 볼 거거든. 이 프로젝트는 IoT의 기본이면서도 실생활에 바로 활용할 수 있는 멋진 작품이 될 거야. 재능넷에서도 이런 IoT 프로젝트 관련 재능이 많이 거래되고 있다고 하니, 이 글을 통해 배운 내용을 활용해보는 것도 좋을 것 같아!
📚 목차
- Arduino MKR WiFi 1010 소개
- 프로젝트 준비물
- 하드웨어 설정하기
- Arduino IDE 설정
- 날씨 API 연동하기
- 데이터 수집 및 처리
- 디스플레이에 정보 표시하기
- 전체 코드 및 설명
- 확장 아이디어
- 문제 해결 및 팁
1. Arduino MKR WiFi 1010 소개 🔍
Arduino MKR WiFi 1010은 초소형 사이즈에 WiFi 기능이 내장된 강력한 마이크로컨트롤러야. 기존의 Arduino UNO보다 훨씬 작으면서도 더 많은 기능을 갖추고 있지. 특히 WiFi 연결이 내장되어 있어서 IoT 프로젝트를 만들기에 딱이야!
주요 스펙 💻
• 프로세서: SAMD21 Cortex-M0+ 32bit 저전력 ARM MCU
• 동작 전압: 3.3V
• 디지털 I/O 핀: 8개
• 아날로그 입력 핀: 7개 (ADC 12bit)
• 아날로그 출력 핀: 1개 (DAC 10bit)
• 내장 WiFi 모듈: u-blox NINA-W10 모듈
• 플래시 메모리: 256KB
• SRAM: 32KB
• 크기: 61.5mm × 25mm
이 보드의 가장 큰 장점은 WiFi 연결과 저전력 설계야. 배터리로 동작하는 IoT 프로젝트에 딱 맞는 선택이지. 오늘 우리가 만들 날씨 정보 시스템도 이 장점을 십분 활용할 거야!
2. 프로젝트 준비물 📋
우리의 날씨 정보 수집 및 표시 시스템을 만들기 위해 필요한 것들을 알아볼게. 모든 부품은 온라인이나 전자부품 상점에서 쉽게 구할 수 있어.
하드웨어 🔧
- Arduino MKR WiFi 1010 보드
- OLED 디스플레이 (SSD1306 128x64 I2C)
- DHT22 온습도 센서 (선택사항)
- BMP280 기압 센서 (선택사항)
- 브레드보드
- 점퍼 와이어
- USB 케이블 (Arduino 연결용)
- 5V 전원 어댑터 또는 보조 배터리 (옵션)
소프트웨어 💾
- Arduino IDE (최신 버전)
- 필요한 라이브러리:
- WiFiNINA
- ArduinoHttpClient
- Adafruit SSD1306
- Adafruit GFX
- ArduinoJson
- DHT 센서 라이브러리 (선택사항)
- Adafruit BMP280 라이브러리 (선택사항)
- 날씨 API 키 (OpenWeatherMap 등)
예상 비용 💰
• Arduino MKR WiFi 1010: 약 35,000원 ~ 45,000원
• OLED 디스플레이: 약 5,000원 ~ 10,000원
• 센서류: 약 5,000원 ~ 15,000원
• 기타 부품: 약 5,000원 ~ 10,000원
총 예상 비용: 약 50,000원 ~ 80,000원
재능넷에서 전자공학 관련 재능을 찾아보면 이런 부품들을 어디서 구매하는 것이 좋을지 조언을 얻을 수도 있을 거야. 또는 직접 프로젝트 제작을 의뢰할 수도 있겠지! 😉
3. 하드웨어 설정하기 🔌
이제 우리의 날씨 정보 시스템을 위한 하드웨어를 연결해볼게. 복잡해 보일 수 있지만 차근차근 따라오면 어렵지 않아!
OLED 디스플레이 연결 📟
OLED 디스플레이는 I2C 통신을 사용하기 때문에 연결이 매우 간단해:
• VCC → 3.3V
• GND → GND
• SCL → SCL (디지털 핀 12)
• SDA → SDA (디지털 핀 11)
DHT22 센서 연결 (선택사항) 🌡️
로컬 온습도 측정을 위한 DHT22 센서 연결:
• VCC → 3.3V
• GND → GND
• DATA → 디지털 핀 2
• 10K 옴 저항을 VCC와 DATA 핀 사이에 연결
BMP280 센서 연결 (선택사항) 🌪️
기압 측정을 위한 BMP280 센서도 I2C 통신을 사용:
• VCC → 3.3V
• GND → GND
• SCL → SCL (디지털 핀 12)
• SDA → SDA (디지털 핀 11)
연결 시 주의사항 ⚠️
• 항상 전원을 끈 상태에서 연결하세요!
• 점퍼 와이어 색상을 일관되게 사용하면 나중에 문제 해결이 쉬워요 (빨간색: 전원, 검은색: 접지, 기타: 신호)
• I2C 장치들은 같은 버스를 공유할 수 있어요 (OLED와 BMP280 모두 SCL/SDA 핀에 연결)
• 센서 핀이 구부러지지 않도록 조심하세요
• 연결이 느슨하면 불안정한 동작의 원인이 될 수 있어요
모든 연결이 완료되면 USB 케이블을 통해 Arduino를 컴퓨터에 연결해봐. 아직 코드를 업로드하지는 않았지만, 보드에 전원이 들어오는지 확인할 수 있을 거야. 전원 LED가 켜지면 하드웨어 연결은 성공! 🎉
4. Arduino IDE 설정 🖥️
하드웨어 연결이 끝났으니 이제 소프트웨어 설정을 해볼게. Arduino IDE를 설치하고 필요한 라이브러리와 보드를 추가하는 과정이야.
Arduino IDE 설치 💿
- Arduino 공식 웹사이트(https://www.arduino.cc/en/software)에서 최신 버전의 Arduino IDE를 다운로드해.
- 운영체제에 맞는 버전을 선택하고 설치 과정을 따라가면 돼.
- 설치가 완료되면 Arduino IDE를 실행해봐.
MKR WiFi 1010 보드 추가 🔄
- Arduino IDE를 실행하고 상단 메뉴에서 [툴] → [보드] → [보드 매니저]를 선택해.
- 검색창에 "MKR"을 입력하고 "Arduino SAMD Boards (32-bits ARM Cortex-M0+)"를 찾아 설치해.
- 설치가 완료되면 [툴] → [보드]에서 "Arduino MKR WiFi 1010"을 선택해.
필요한 라이브러리 설치 📚
- 상단 메뉴에서 [스케치] → [라이브러리 포함하기] → [라이브러리 관리]를 선택해.
- 다음 라이브러리들을 검색하고 설치해:
- WiFiNINA (Arduino MKR WiFi 1010의 WiFi 기능 사용)
- ArduinoHttpClient (HTTP 요청 처리)
- Adafruit SSD1306 (OLED 디스플레이 제어)
- Adafruit GFX (그래픽 라이브러리)
- ArduinoJson (JSON 데이터 파싱)
- DHT sensor library (DHT22 센서 사용 시)
- Adafruit BMP280 Library (BMP280 센서 사용 시)
간단한 테스트 코드 작성 ✏️
모든 설정이 제대로 되었는지 확인하기 위해 간단한 테스트 코드를 작성해보자:
#include <SPI.h>
#include <WiFiNINA.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Serial.begin(9600);
while (!Serial);
// OLED 초기화
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 할당 실패"));
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Arduino MKR WiFi 1010");
display.println("테스트 성공!");
display.display();
Serial.println("OLED 테스트 완료!");
}
void loop() {
// 여기서는 아무것도 하지 않음
}
이 코드를 업로드하고 OLED 디스플레이에 텍스트가 표시되면 기본 설정이 완료된 거야! 🎉
문제 해결 팁 🔧
• 포트가 보이지 않는 경우: 드라이버가 제대로 설치되지 않았을 수 있어. Arduino 웹사이트에서 추가 드라이버를 설치해봐.
• 라이브러리 충돌: 라이브러리 버전이 맞지 않아 충돌이 발생할 수 있어. 최신 버전으로 업데이트해보자.
• 컴파일 오류: 오류 메시지를 잘 읽고 해당 라인을 확인해. 대부분 문법 오류나 라이브러리 문제일 가능성이 높아.
• 업로드 실패: 보드 선택이 올바른지, USB 케이블이 제대로 연결되었는지 확인해봐.
5. 날씨 API 연동하기 🌐
이제 실시간 날씨 정보를 가져오기 위해 날씨 API를 연동해볼게. 우리는 무료로 사용할 수 있는 OpenWeatherMap API를 사용할 거야.
OpenWeatherMap API 키 발급받기 🔑
- OpenWeatherMap 웹사이트(https://openweathermap.org/)에 접속해서 회원가입을 해.
- 가입 후 로그인하고 [API keys] 탭으로 이동해.
- 기본 API 키가 생성되어 있을 거야. 이 키를 복사해두자.
- 무료 계정은 분당 60회, 하루 1,000,000회 API 호출이 가능해. 우리 프로젝트에는 충분해!
WiFi 연결 설정 📶
Arduino가 인터넷에 연결되어야 API를 호출할 수 있어. WiFi 연결 코드를 작성해보자:
#include <WiFiNINA.h>
// WiFi 설정
const char ssid[] = "your_wifi_ssid"; // WiFi 네트워크 이름
const char pass[] = "your_wifi_password"; // WiFi 비밀번호
int status = WL_IDLE_STATUS;
void setup() {
Serial.begin(9600);
while (!Serial);
// WiFi 연결 시도
Serial.print("WiFi에 연결 중... ");
status = WiFi.begin(ssid, pass);
// WiFi 연결 대기
while (status != WL_CONNECTED) {
Serial.print(".");
delay(500);
status = WiFi.status();
}
Serial.println();
Serial.println("WiFi에 연결되었습니다!");
Serial.print("IP 주소: ");
Serial.println(WiFi.localIP());
}
void loop() {
// 여기서는 아무것도 하지 않음
}
날씨 API 요청 보내기 🌦️
이제 OpenWeatherMap API에 요청을 보내 날씨 데이터를 가져와보자:
#include <ArduinoHttpClient.h>
#include <ArduinoJson.h>
// API 설정
const char server[] = "api.openweathermap.org";
const String apiKey = "your_api_key_here";
const String city = "Seoul"; // 원하는 도시 이름
const String country = "kr"; // 국가 코드
// HTTP 클라이언트 설정
WiFiClient wifi;
HttpClient client = HttpClient(wifi, server, 80);
void getWeatherData() {
Serial.println("날씨 데이터 요청 중...");
// API 요청 URL 구성
String path = "/data/2.5/weather?q=" + city + "," + country + "&units=metric&appid=" + apiKey;
// HTTP GET 요청 보내기
client.get(path);
// 응답 상태 코드 확인
int statusCode = client.responseStatusCode();
Serial.print("상태 코드: ");
Serial.println(statusCode);
// 응답이 성공적이면 데이터 처리
if (statusCode == 200) {
String response = client.responseBody();
Serial.println("응답 데이터:");
Serial.println(response);
// JSON 파싱
DynamicJsonDocument doc(1024);
deserializeJson(doc, response);
// 날씨 데이터 추출
float temp = doc["main"]["temp"];
float humidity = doc["main"]["humidity"];
String weatherDesc = doc["weather"][0]["description"];
Serial.print("온도: ");
Serial.print(temp);
Serial.println("°C");
Serial.print("습도: ");
Serial.print(humidity);
Serial.println("%");
Serial.print("날씨 상태: ");
Serial.println(weatherDesc);
}
else {
Serial.println("API 요청 실패");
}
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
getWeatherData();
delay(60000); // 1분마다 업데이트 (API 호출 제한 고려)
}
}
API 사용 팁 💡
• API 호출 빈도를 너무 높게 설정하지 마세요. 무료 계정은 제한이 있어요.
• 날씨 데이터는 자주 변하지 않으므로 5~15분 간격으로 업데이트하는 것이 적당해요.
• API 응답에는 다양한 정보가 포함되어 있어요. 필요한 데이터만 추출해서 사용하세요.
• 도시 이름 대신 위도/경도로 검색하면 더 정확한 결과를 얻을 수 있어요.
• 에러 처리를 잘 해두면 네트워크 문제가 발생해도 시스템이 계속 작동할 수 있어요.
6. 데이터 수집 및 처리 📊
이제 API에서 받아온 날씨 데이터와 로컬 센서에서 측정한 데이터를 수집하고 처리하는 방법을 알아볼게.
JSON 데이터 파싱하기 🔍
OpenWeatherMap API는 JSON 형식으로 데이터를 반환해. ArduinoJson 라이브러리를 사용해 이 데이터를 파싱해보자:
// 날씨 데이터 구조체 정의
struct WeatherData {
float temperature;
float humidity;
float pressure;
float windSpeed;
String weatherMain;
String weatherDescription;
String cityName;
long sunrise;
long sunset;
};
WeatherData currentWeather;
// JSON 응답 파싱 함수
void parseWeatherData(String json) {
// JSON 문서 생성 (크기는 응답 크기에 따라 조정)
DynamicJsonDocument doc(1024);
// JSON 파싱
DeserializationError error = deserializeJson(doc, json);
// 파싱 오류 확인
if (error) {
Serial.print("JSON 파싱 실패: ");
Serial.println(error.c_str());
return;
}
// 데이터 추출 및 저장
currentWeather.temperature = doc["main"]["temp"];
currentWeather.humidity = doc["main"]["humidity"];
currentWeather.pressure = doc["main"]["pressure"];
currentWeather.windSpeed = doc["wind"]["speed"];
currentWeather.weatherMain = doc["weather"][0]["main"].as<String>();
currentWeather.weatherDescription = doc["weather"][0]["description"].as<String>();
currentWeather.cityName = doc["name"].as<String>();
currentWeather.sunrise = doc["sys"]["sunrise"];
currentWeather.sunset = doc["sys"]["sunset"];
// 데이터 확인 출력
Serial.println("파싱된 날씨 데이터:");
Serial.print("도시: ");
Serial.println(currentWeather.cityName);
Serial.print("온도: ");
Serial.print(currentWeather.temperature);
Serial.println("°C");
Serial.print("날씨: ");
Serial.println(currentWeather.weatherDescription);
}
로컬 센서 데이터 수집 (선택사항) 🌡️
DHT22와 BMP280 센서에서 로컬 온습도 및 기압 데이터를 수집해보자:
#include <DHT.h>
#include <Adafruit_BMP280.h>
#define DHTPIN 2 // DHT22 센서 연결 핀
#define DHTTYPE DHT22 // DHT 센서 타입
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP280 bmp;
// 로컬 센서 데이터 구조체
struct LocalSensorData {
float temperature;
float humidity;
float pressure;
float altitude;
};
LocalSensorData localData;
// 센서 초기화
void setupSensors() {
dht.begin();
if (!bmp.begin(0x76)) { // BMP280 I2C 주소 (0x76 또는 0x77)
Serial.println("BMP280 센서를 찾을 수 없습니다!");
}
// BMP280 설정
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, // 작동 모드
Adafruit_BMP280::SAMPLING_X2, // 온도 오버샘플링
Adafruit_BMP280::SAMPLING_X16, // 압력 오버샘플링
Adafruit_BMP280::FILTER_X16, // 필터링
Adafruit_BMP280::STANDBY_MS_500); // 대기 시간
}
// 센서 데이터 읽기
void readLocalSensors() {
// DHT22 센서 읽기
localData.humidity = dht.readHumidity();
float tempDHT = dht.readTemperature();
// BMP280 센서 읽기
float tempBMP = bmp.readTemperature();
localData.pressure = bmp.readPressure() / 100.0F; // hPa 단위로 변환
localData.altitude = bmp.readAltitude(1013.25); // 해수면 기준 고도
// 온도 데이터 유효성 검사 및 평균 계산
if (isnan(tempDHT)) {
localData.temperature = tempBMP;
}
else if (isnan(tempBMP)) {
localData.temperature = tempDHT;
}
else {
// 두 센서의 평균값 사용
localData.temperature = (tempDHT + tempBMP) / 2.0;
}
// 데이터 출력
Serial.println("로컬 센서 데이터:");
Serial.print("온도: ");
Serial.print(localData.temperature);
Serial.println("°C");
Serial.print("습도: ");
Serial.print(localData.humidity);
Serial.println("%");
Serial.print("기압: ");
Serial.print(localData.pressure);
Serial.println("hPa");
}
API 데이터와 로컬 데이터 비교 📈
API에서 받은 날씨 데이터와 로컬 센서 데이터를 비교하면 재미있는 인사이트를 얻을 수 있어:
void compareWeatherData() {
Serial.println("날씨 데이터 비교:");
Serial.print("API 온도: ");
Serial.print(currentWeather.temperature);
Serial.println("°C");
Serial.print("로컬 온도: ");
Serial.print(localData.temperature);
Serial.println("°C");
float tempDiff = abs(currentWeather.temperature - localData.temperature);
Serial.print("온도 차이: ");
Serial.print(tempDiff);
Serial.println("°C");
Serial.print("API 습도: ");
Serial.print(currentWeather.humidity);
Serial.println("%");
Serial.print("로컬 습도: ");
Serial.print(localData.humidity);
Serial.println("%");
float humidityDiff = abs(currentWeather.humidity - localData.humidity);
Serial.print("습도 차이: ");
Serial.print(humidityDiff);
Serial.println("%");
}
데이터 저장 및 관리 💾
Arduino MKR WiFi 1010은 제한된 메모리를 가지고 있어. 데이터를 효율적으로 관리하는 방법을 고려해야 해:
- 최신 데이터만 저장하고 표시하기
- 시간별 평균 데이터 계산하기
- 중요한 변화가 있을 때만 데이터 업데이트하기
- SD 카드 모듈을 추가하여 장기 데이터 저장하기 (선택사항)
데이터 수집과 처리가 완료되면 이제 이 정보를 디스플레이에 표시할 차례야! 🎯
7. 디스플레이에 정보 표시하기 📱
수집한 날씨 데이터를 OLED 디스플레이에 보기 좋게 표시해보자. Adafruit GFX와 SSD1306 라이브러리를 사용하면 다양한 그래픽 요소를 표현할 수 있어!
OLED 디스플레이 초기화 🖥️
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setupDisplay() {
// OLED 초기화
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 할당 실패"));
for(;;);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.display();
// 시작 화면 표시
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("날씨 정보 시스템");
display.println("초기화 중...");
display.display();
delay(2000);
}
날씨 정보 표시 화면 디자인 🎨
날씨 정보를 보기 좋게 표시하는 함수를 만들어보자:
// 날씨 아이콘 표시 함수
void drawWeatherIcon(int x, int y, String weatherType) {
if (weatherType == "Clear") {
// 맑음 아이콘
display.drawCircle(x + 6, y + 6, 5, SSD1306_WHITE);
display.drawLine(x + 6, y - 3, x + 6, y - 1, SSD1306_WHITE);
display.drawLine(x + 6, y + 13, x + 6, y + 15, SSD1306_WHITE);
display.drawLine(x - 3, y + 6, x - 1, y + 6, SSD1306_WHITE);
display.drawLine(x + 13, y + 6, x + 15, y + 6, SSD1306_WHITE);
}
else if (weatherType == "Clouds") {
// 구름 아이콘
display.drawCircle(x + 6, y + 4, 3, SSD1306_WHITE);
display.drawRoundRect(x + 2, y + 7, 10, 5, 2, SSD1306_WHITE);
}
else if (weatherType == "Rain") {
// 비 아이콘
display.drawRoundRect(x + 2, y + 2, 10, 5, 2, SSD1306_WHITE);
display.drawLine(x + 4, y + 10, x + 3, y + 14, SSD1306_WHITE);
display.drawLine(x + 8, y + 10, x + 7, y + 14, SSD1306_WHITE);
}
else if (weatherType == "Snow") {
// 눈 아이콘
display.drawRoundRect(x + 2, y + 2, 10, 5, 2, SSD1306_WHITE);
display.drawLine(x + 4, y + 10, x + 4, y + 14, SSD1306_WHITE);
display.drawLine(x + 8, y + 10, x + 8, y + 14, SSD1306_WHITE);
display.drawLine(x + 2, y + 12, x + 6, y + 12, SSD1306_WHITE);
display.drawLine(x + 6, y + 12, x + 10, y + 12, SSD1306_WHITE);
}
else {
// 기본 아이콘
display.drawRect(x + 2, y + 2, 10, 10, SSD1306_WHITE);
}
}
// 메인 날씨 정보 표시 함수
void displayWeatherInfo() {
display.clearDisplay();
// 도시 이름 표시
display.setTextSize(1);
display.setCursor(0, 0);
display.print(currentWeather.cityName);
// 날씨 아이콘 표시
drawWeatherIcon(110, 0, currentWeather.weatherMain);
// 온도 표시
display.setTextSize(2);
display.setCursor(0, 16);
display.print(currentWeather.temperature, 1);
display.print(" C");
// 날씨 설명
display.setTextSize(1);
display.setCursor(0, 36);
display.print(currentWeather.weatherDescription);
// 습도 및 기압
display.setCursor(0, 48);
display.print("습도: ");
display.print(currentWeather.humidity, 0);
display.print("% ");
display.print(currentWeather.pressure, 0);
display.print("hPa");
display.display();
}
// 로컬 센서 정보 표시 함수
void displayLocalSensorInfo() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("실내 센서 데이터");
display.setCursor(0, 16);
display.print("온도: ");
display.print(localData.temperature, 1);
display.print(" C");
display.setCursor(0, 28);
display.print("습도: ");
display.print(localData.humidity, 1);
display.print(" %");
display.setCursor(0, 40);
display.print("기압: ");
display.print(localData.pressure, 1);
display.print(" hPa");
display.setCursor(0, 52);
display.print("고도: ");
display.print(localData.altitude, 0);
display.print(" m");
display.display();
}
디스플레이 최적화 팁 💡
• OLED 디스플레이는 장시간 같은 내용을 표시하면 번인 현상이 발생할 수 있어요.
• 화면을 주기적으로 변경하거나 화면 보호기 기능을 추가하세요.
• 텍스트 크기를 너무 작게 설정하면 가독성이 떨어집니다.
• 중요한 정보는 화면 상단이나 중앙에 배치하세요.
• 아이콘과 텍스트를 적절히 조합하면 정보 전달력이 높아집니다.
• 여러 화면을 번갈아 표시하여 더 많은 정보를 보여줄 수 있어요.
화면 전환 기능 구현 🔄
여러 정보를 번갈아 표시하는 화면 전환 기능을 구현해보자:
unsigned long lastScreenChange = 0;
int currentScreen = 0;
const int SCREEN_COUNT = 3;
const long SCREEN_CHANGE_INTERVAL = 5000; // 5초마다 화면 전환
void updateDisplay() {
unsigned long currentTime = millis();
// 화면 전환 시간이 되었는지 확인
if (currentTime - lastScreenChange >= SCREEN_CHANGE_INTERVAL) {
lastScreenChange = currentTime;
currentScreen = (currentScreen + 1) % SCREEN_COUNT;
}
// 현재 화면에 따라 다른 정보 표시
switch (currentScreen) {
case 0:
displayWeatherInfo();
break;
case 1:
displayLocalSensorInfo();
break;
case 2:
displayForecastInfo();
break;
}
}
void loop() {
// 주기적으로 데이터 업데이트
if (millis() - lastDataUpdate >= DATA_UPDATE_INTERVAL) {
if (WiFi.status() == WL_CONNECTED) {
getWeatherData();
}
readLocalSensors();
lastDataUpdate = millis();
}
// 디스플레이 업데이트
updateDisplay();
delay(100); // CPU 부하 감소
}
8. 전체 코드 및 설명 📝
이제 지금까지 배운 내용을 모두 합쳐서 완전한 코드를 만들어보자. 이 코드는 WiFi 연결, API 데이터 수집, 센서 데이터 읽기, 디스플레이 표시 기능을 모두 포함하고 있어.
/*
* Arduino MKR WiFi 1010 날씨 정보 수집 및 표시 시스템
*
* 이 코드는 OpenWeatherMap API에서 날씨 정보를 가져오고,
* 로컬 센서에서 온습도 및 기압 데이터를 수집하여
* OLED 디스플레이에 표시합니다.
*/
#include <SPI.h>
#include <WiFiNINA.h>
#include <ArduinoHttpClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <Adafruit_BMP280.h>
#include <TimeLib.h>
// 디스플레이 설정
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// 센서 설정
#define DHTPIN 2
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP280 bmp;
// WiFi 설정
const char ssid[] = "your_wifi_ssid";
const char pass[] = "your_wifi_password";
int status = WL_IDLE_STATUS;
// API 설정
const char server[] = "api.openweathermap.org";
const String apiKey = "your_api_key_here";
const String city = "Seoul";
const String country = "kr";
WiFiClient wifi;
HttpClient client = HttpClient(wifi, server, 80);
// 시간 간격 설정
const long DATA_UPDATE_INTERVAL = 300000; // 5분마다 데이터 업데이트
const long SCREEN_CHANGE_INTERVAL = 5000; // 5초마다 화면 전환
unsigned long lastDataUpdate = 0;
unsigned long lastScreenChange = 0;
int currentScreen = 0;
const int SCREEN_COUNT = 3;
// 날씨 데이터 구조체
struct WeatherData {
float temperature;
float humidity;
float pressure;
float windSpeed;
String weatherMain;
String weatherDescription;
String cityName;
long sunrise;
long sunset;
String lastUpdate;
};
// 로컬 센서 데이터 구조체
struct LocalSensorData {
float temperature;
float humidity;
float pressure;
float altitude;
};
// 데이터 저장 변수
WeatherData currentWeather;
LocalSensorData localData;
void setup() {
// 시리얼 초기화
Serial.begin(9600);
while (!Serial && millis() < 5000);
Serial.println("Arduino MKR WiFi 1010 날씨 정보 시스템 시작");
// 디스플레이 초기화
setupDisplay();
// WiFi 연결
connectWiFi();
// 센서 초기화
setupSensors();
// 초기 데이터 수집
if (WiFi.status() == WL_CONNECTED) {
getWeatherData();
}
readLocalSensors();
lastDataUpdate = millis();
}
void loop() {
// 주기적으로 데이터 업데이트
if (millis() - lastDataUpdate >= DATA_UPDATE_INTERVAL) {
if (WiFi.status() == WL_CONNECTED) {
getWeatherData();
}
readLocalSensors();
lastDataUpdate = millis();
}
// 디스플레이 업데이트
updateDisplay();
delay(100); // CPU 부하 감소
}
// WiFi 연결 함수
void connectWiFi() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("WiFi 연결 중...");
display.display();
Serial.print("WiFi에 연결 중... ");
status = WiFi.begin(ssid, pass);
int attempts = 0;
while (status != WL_CONNECTED && attempts < 10) {
Serial.print(".");
display.print(".");
display.display();
delay(1000);
status = WiFi.status();
attempts++;
}
if (status == WL_CONNECTED) {
Serial.println();
Serial.println("WiFi에 연결되었습니다!");
Serial.print("IP 주소: ");
Serial.println(WiFi.localIP());
display.clearDisplay();
display.setCursor(0, 0);
display.println("WiFi 연결 성공!");
display.println(WiFi.localIP());
display.display();
delay(2000);
} else {
Serial.println();
Serial.println("WiFi 연결 실패");
display.clearDisplay();
display.setCursor(0, 0);
display.println("WiFi 연결 실패");
display.println("오프라인 모드로 진행");
display.display();
delay(2000);
}
}
// 디스플레이 초기화 함수
void setupDisplay() {
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 할당 실패"));
for(;;);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("날씨 정보 시스템");
display.println("초기화 중...");
display.display();
delay(2000);
}
// 센서 초기화 함수
void setupSensors() {
dht.begin();
if (!bmp.begin(0x76)) {
Serial.println("BMP280 센서를 찾을 수 없습니다!");
display.clearDisplay();
display.setCursor(0, 0);
display.println("BMP280 센서 오류");
display.display();
delay(2000);
} else {
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
Adafruit_BMP280::SAMPLING_X2,
Adafruit_BMP280::SAMPLING_X16,
Adafruit_BMP280::FILTER_X16,
Adafruit_BMP280::STANDBY_MS_500);
}
}
// 날씨 데이터 가져오기 함수
void getWeatherData() {
Serial.println("날씨 데이터 요청 중...");
// API 요청 URL 구성
String path = "/data/2.5/weather?q=" + city + "," + country + "&units=metric&appid=" + apiKey;
// HTTP GET 요청 보내기
client.get(path);
// 응답 상태 코드 확인
int statusCode = client.responseStatusCode();
Serial.print("상태 코드: ");
Serial.println(statusCode);
// 응답이 성공적이면 데이터 처리
if (statusCode == 200) {
String response = client.responseBody();
parseWeatherData(response);
// 현재 시간을 마지막 업데이트 시간으로 저장
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", hour(), minute(), second());
currentWeather.lastUpdate = String(timeStr);
}
else {
Serial.println("API 요청 실패");
}
}
// JSON 응답 파싱 함수
void parseWeatherData(String json) {
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, json);
if (error) {
Serial.print("JSON 파싱 실패: ");
Serial.println(error.c_str());
return;
}
currentWeather.temperature = doc["main"]["temp"];
currentWeather.humidity = doc["main"]["humidity"];
currentWeather.pressure = doc["main"]["pressure"];
currentWeather.windSpeed = doc["wind"]["speed"];
currentWeather.weatherMain = doc["weather"][0]["main"].as<String>();
currentWeather.weatherDescription = doc["weather"][0]["description"].as<String>();
currentWeather.cityName = doc["name"].as<String>();
currentWeather.sunrise = doc["sys"]["sunrise"];
currentWeather.sunset = doc["sys"]["sunset"];
Serial.println("날씨 데이터 파싱 완료");
}
// 로컬 센서 데이터 읽기 함수
void readLocalSensors() {
// DHT22 센서 읽기
localData.humidity = dht.readHumidity();
float tempDHT = dht.readTemperature();
// BMP280 센서 읽기
float tempBMP = bmp.readTemperature();
localData.pressure = bmp.readPressure() / 100.0F;
localData.altitude = bmp.readAltitude(1013.25);
// 온도 데이터 유효성 검사 및 평균 계산
if (isnan(tempDHT)) {
localData.temperature = tempBMP;
}
else if (isnan(tempBMP)) {
localData.temperature = tempDHT;
}
else {
localData.temperature = (tempDHT + tempBMP) / 2.0;
}
Serial.println("로컬 센서 데이터 읽기 완료");
}
// 디스플레이 업데이트 함수
void updateDisplay() {
unsigned long currentTime = millis();
// 화면 전환 시간이 되었는지 확인
if (currentTime - lastScreenChange >= SCREEN_CHANGE_INTERVAL) {
lastScreenChange = currentTime;
currentScreen = (currentScreen + 1) % SCREEN_COUNT;
}
// 현재 화면에 따라 다른 정보 표시
switch (currentScreen) {
case 0:
displayWeatherInfo();
break;
case 1:
displayLocalSensorInfo();
break;
case 2:
displayComparisonInfo();
break;
}
}
// 날씨 아이콘 표시 함수
void drawWeatherIcon(int x, int y, String weatherType) {
if (weatherType == "Clear") {
// 맑음 아이콘
display.drawCircle(x + 6, y + 6, 5, SSD1306_WHITE);
display.drawLine(x + 6, y - 3, x + 6, y - 1, SSD1306_WHITE);
display.drawLine(x + 6, y + 13, x + 6, y + 15, SSD1306_WHITE);
display.drawLine(x - 3, y + 6, x - 1, y + 6, SSD1306_WHITE);
display.drawLine(x + 13, y + 6, x + 15, y + 6, SSD1306_WHITE);
}
else if (weatherType == "Clouds") {
// 구름 아이콘
display.drawCircle(x + 6, y + 4, 3, SSD1306_WHITE);
display.drawRoundRect(x + 2, y + 7, 10, 5, 2, SSD1306_WHITE);
}
else if (weatherType == "Rain") {
// 비 아이콘
display.drawRoundRect(x + 2, y + 2, 10, 5, 2, SSD1306_WHITE);
display.drawLine(x + 4, y + 10, x + 3, y + 14, SSD1306_WHITE);
display.drawLine(x + 8, y + 10, x + 7, y + 14, SSD1306_WHITE);
}
else if (weatherType == "Snow") {
// 눈 아이콘
display.drawRoundRect(x + 2, y + 2, 10, 5, 2, SSD1306_WHITE);
display.drawLine(x + 4, y + 10, x + 4, y + 14, SSD1306_WHITE);
display.drawLine(x + 8, y + 10, x + 8, y + 14, SSD1306_WHITE);
display.drawLine(x + 2, y + 12, x + 6, y + 12, SSD1306_WHITE);
display.drawLine(x + 6, y + 12, x + 10, y + 12, SSD1306_WHITE);
}
else {
// 기본 아이콘
display.drawRect(x + 2, y + 2, 10, 10, SSD1306_WHITE);
}
}
// 날씨 정보 표시 함수
void displayWeatherInfo() {
display.clearDisplay();
// 도시 이름 표시
display.setTextSize(1);
display.setCursor(0, 0);
display.print(currentWeather.cityName);
// 날씨 아이콘 표시
drawWeatherIcon(110, 0, currentWeather.weatherMain);
// 온도 표시
display.setTextSize(2);
display.setCursor(0, 16);
display.print(currentWeather.temperature, 1);
display.print(" C");
// 날씨 설명
display.setTextSize(1);
display.setCursor(0, 36);
display.print(currentWeather.weatherDescription);
// 습도 및 기압
display.setCursor(0, 48);
display.print("습도: ");
display.print(currentWeather.humidity, 0);
display.print("% ");
display.print(currentWeather.pressure, 0);
display.print("hPa");
// 마지막 업데이트 시간
display.setCursor(0, 56);
display.print("업데이트: ");
display.print(currentWeather.lastUpdate);
display.display();
}
// 로컬 센서 정보 표시 함수
void displayLocalSensorInfo() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("실내 센서 데이터");
display.setCursor(0, 16);
display.print("온도: ");
display.print(localData.temperature, 1);
display.print(" C");
display.setCursor(0, 28);
display.print("습도: ");
display.print(localData.humidity, 1);
display.print(" %");
display.setCursor(0, 40);
display.print("기압: ");
display.print(localData.pressure, 1);
display.print(" hPa");
display.setCursor(0, 52);
display.print("고도: ");
display.print(localData.altitude, 0);
display.print(" m");
display.display();
}
// 비교 정보 표시 함수
void displayComparisonInfo() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("API vs 로컬 센서");
display.setCursor(0, 16);
display.print("온도 차이: ");
float tempDiff = abs(currentWeather.temperature - localData.temperature);
display.print(tempDiff, 1);
display.print(" C");
display.setCursor(0, 28);
display.print("습도 차이: ");
float humidityDiff = abs(currentWeather.humidity - localData.humidity);
display.print(humidityDiff, 1);
display.print(" %");
display.setCursor(0, 40);
display.print("기압 차이: ");
float pressureDiff = abs(currentWeather.pressure - localData.pressure);
display.print(pressureDiff, 1);
display.print(" hPa");
display.display();
}
코드 구조 설명 📚
이 코드는 다음과 같은 주요 부분으로 구성되어 있어:
- 라이브러리 및 변수 선언: 필요한 라이브러리를 포함하고 전역 변수를 선언합니다.
- setup() 함수: 시스템 초기화, WiFi 연결, 센서 설정, 초기 데이터 수집을 수행합니다.
- loop() 함수: 주기적으로 데이터를 업데이트하고 디스플레이를 갱신합니다.
- connectWiFi() 함수: WiFi 네트워크에 연결합니다.
- getWeatherData() 함수: OpenWeatherMap API에서 날씨 데이터를 가져옵니다.
- parseWeatherData() 함수: JSON 응답을 파싱하여 날씨 데이터를 추출합니다.
- readLocalSensors() 함수: DHT22와 BMP280 센서에서 데이터를 읽습니다.
- updateDisplay() 함수: 화면 전환 로직을 처리합니다.
- display...() 함수들: 다양한 정보 화면을 OLED에 표시합니다.
9. 확장 아이디어 💡
기본 날씨 정보 시스템을 만들었으니, 이제 더 발전시킬 수 있는 아이디어를 살펴볼게!
하드웨어 확장 🔌
- 더 큰 디스플레이 추가: 1.8" 또는 2.4" TFT LCD 디스플레이를 사용하면 더 많은 정보와 그래픽을 표시할 수 있어.
- SD 카드 모듈: 날씨 데이터를 장기간 저장하고 분석할 수 있어.
- RTC(Real-Time Clock) 모듈: 정확한 시간 정보를 유지할 수 있어.
- 추가 센서: UV 센서, 미세먼지 센서, 풍향/풍속 센서 등을 추가하여 더 다양한 환경 데이터를 수집할 수 있어.
- 배터리 및 태양광 패널: 전원 없이도 작동하는 독립형 기상 스테이션을 만들 수 있어.
소프트웨어 확장 💻
- 일기 예보 추가: OpenWeatherMap의 5일 예보 API를 사용하여 미래 날씨 정보를 표시할 수 있어.
- 데이터 로깅 및 그래프: 시간에 따른 온도, 습도 변화를 그래프로 표시할 수 있어.
- WiFi 매니저: 웹 인터페이스를 통해 WiFi 설정을 변경할 수 있는 기능을 추가할 수 있어.
- 알림 기능: 특정 기상 조건(폭우, 폭설, 폭염 등)에서 알림을 보내는 기능을 추가할 수 있어.
- 웹 서버: Arduino에 웹 서버를 구현하여 브라우저에서 날씨 데이터를 확인할 수 있어.
IoT 연동 🌐
- MQTT 프로토콜: 날씨 데이터를 MQTT 브로커에 발행하여 다른 IoT 장치와 공유할 수 있어.
- ThingSpeak 연동: 클라우드 플랫폼에 데이터를 저장하고 분석할 수 있어.
- 스마트홈 연동: 날씨 조건에 따라 스마트홈 기기를 제어할 수 있어 (예: 비가 오면 창문 닫기).
- 모바일 앱 연동: Blynk 같은 플랫폼을 사용해 모바일 앱에서 날씨 데이터를 확인할 수 있어.
- 음성 비서 연동: Alexa나 Google Assistant와 연동하여 음성으로 날씨 정보를 물어볼 수 있어.
프로젝트 활용 아이디어 🏆
• 스마트 농장: 온실이나 농장의 환경을 모니터링하고 최적의 조건을 유지할 수 있어.
• 홈 오토메이션: 날씨에 따라 자동으로 창문, 블라인드, 에어컨 등을 제어할 수 있어.
• 교육용 기상 관측소: 학교나 과학 동아리에서 기상 관측 및 데이터 분석 교육에 활용할 수 있어.
• 개인 기상 기록: 취미로 장기간 지역 날씨를 기록하고 분석할 수 있어.
• 야외 활동 도우미: 등산, 캠핑, 낚시 등 야외 활동 시 현장의 날씨 정보를 실시간으로 확인할 수 있어.
재능넷에서는 이런 확장 프로젝트에 관심 있는 사람들을 위한 다양한 재능이 거래되고 있어. 아두이노 프로그래밍, IoT 시스템 구축, 센서 연동 등의 전문 지식을 가진 사람들에게 도움을 받을 수 있지! 🚀
10. 문제 해결 및 팁 🔧
프로젝트를 진행하다 보면 다양한 문제가 발생할 수 있어. 자주 발생하는 문제와 해결 방법, 그리고 유용한 팁을 알아보자!
자주 발생하는 문제 ⚠️
1. WiFi 연결 문제
증상: WiFi에 연결되지 않거나 자주 끊김
해결 방법:
• WiFi 신호 강도 확인: Arduino를 라우터 가까이에 두거나 WiFi 확장기 사용
• WiFi 비밀번호 재확인: 특수 문자가 포함된 경우 이스케이프 처리 필요
• WiFi 재연결 로직 추가: 연결이 끊어지면 자동으로 재연결 시도하는 코드 추가
• 라우터 설정 확인: 일부 라우터는 2.4GHz와 5GHz를 같은 SSID로 사용 (Arduino는 2.4GHz만 지원)
2. API 요청 실패
증상: API 응답 코드가 200이 아니거나 응답이 없음
해결 방법:
• API 키 확인: 올바른 API 키를 사용하고 있는지 확인
• 요청 URL 확인: 도시 이름, 국가 코드 등이 올바르게 입력되었는지 확인
• API 호출 제한 확인: 무료 계정의 API 호출 제한을 초과하지 않았는지 확인
• 오프라인 모드 추가: API 요청 실패 시 로컬 센서 데이터만 표시하는 기능 추가
3. 센서 데이터 오류
증상: 센서 데이터가 이상하거나 NaN(Not a Number) 값 반환
해결 방법:
• 센서 연결 확인: 배선이 올바르게 연결되었는지 확인
• 센서 전원 확인: 안정적인 전원 공급이 이루어지는지 확인
• 라이브러리 버전 확인: 최신 버전의 센서 라이브러리 사용
• 데이터 유효성 검사: isnan() 함수로 유효하지 않은 값 필터링
4. 메모리 부족
증상: 스케치가 불안정하게 작동하거나 재부팅됨
해결 방법:
• 문자열 최적화: F() 매크로를 사용하여 문자열을 플래시 메모리에 저장
• 변수 타입 최적화: int 대신 byte 사용, float 대신 int 사용 (필요에 따라)
• 버퍼 크기 조정: JSON 문서 크기 등을 필요한 만큼만 할당
• 불필요한 기능 제거: 사용하지 않는 기능이나 라이브러리 제거
5. 디스플레이 문제
증상: 디스플레이에 아무것도 표시되지 않거나 깨짐
해결 방법:
• I2C 주소 확인: 일부 OLED는 0x3C 대신 0x3D 주소 사용
• 디스플레이 초기화 확인: begin() 함수가 성공적으로 호출되었는지 확인
• 배선 확인: SDA, SCL 핀이 올바르게 연결되었는지 확인
• display.display() 호출 확인: 모든 그리기 작업 후 반드시 호출 필요
성능 최적화 팁 🚀
- 전력 소비 최적화: WiFi.lowPowerMode() 사용, 불필요할 때 WiFi 끄기
- 데이터 업데이트 간격 조정: 날씨 데이터는 5~15분마다, 센서 데이터는 1~5분마다 업데이트
- 메모리 사용 모니터링: freeMemory() 함수로 가용 메모리 확인
- 디스플레이 업데이트 최적화: 변경된 부분만 업데이트하거나 필요할 때만 전체 화면 갱신
- 에러 처리 강화: try-catch 구문 대신 명시적 에러 체크 사용
디버깅 팁 🔍
문제가 발생했을 때 효과적으로 디버깅하는 방법:
- 시리얼 모니터 활용: 중요한 지점마다 Serial.println()을 추가하여 프로그램 흐름 추적
- 단계별 테스트: 전체 시스템을 한 번에 테스트하지 말고, 각 부분(WiFi, 센서, API, 디스플레이)을 개별적으로 테스트
- LED 상태 표시: 내장 LED를 사용하여 다양한 상태(연결 중, 오류, 데이터 수신 등)를 표시
- 디스플레이 디버그 모드: 디스플레이에 디버그 정보를 표시하는 특수 모드 추가
- 타임스탬프 추가: 로그 메시지에 시간 정보를 추가하여 타이밍 문제 파악
유지 보수 팁 🔄
장기간 안정적으로 시스템을 운영하기 위한 팁:
- 정기적인 재부팅: watchdog 타이머를 사용하여 일정 시간마다 자동 재부팅
- 원격 모니터링: 상태 데이터를 클라우드에 업로드하여 원격으로 모니터링
- 오류 로깅: 발생한 오류를 기록하여 나중에 분석
- 배터리 모니터링: 배터리로 작동하는 경우 배터리 잔량을 확인하고 표시
- OTA 업데이트: 원격으로 펌웨어를 업데이트할 수 있는 기능 추가
문제가 발생했을 때 당황하지 말고 차근차근 원인을 찾아보자. 대부분의 문제는 위의 방법으로 해결할 수 있어. 그래도 해결이 어렵다면, 재능넷에서 Arduino 전문가의 도움을 받는 것도 좋은 방법이야! 🤝
마무리 🎉
이렇게 Arduino MKR WiFi 1010을 이용한 날씨 정보 수집 및 표시 시스템을 만들어보았어! 이 프로젝트를 통해 Arduino 프로그래밍, 센서 데이터 수집, API 연동, 디스플레이 제어 등 다양한 기술을 배울 수 있었지.
처음에는 복잡해 보일 수 있지만, 단계별로 차근차근 따라하다 보면 생각보다 쉽게 만들 수 있어. 그리고 이 기본 시스템을 바탕으로 자신만의 아이디어를 추가하여 더욱 멋진 프로젝트로 발전시킬 수 있을 거야!
만약 이 프로젝트에 대해 더 궁금한 점이 있거나, 도움이 필요하다면 재능넷의 전문가들에게 문의해보는 것도 좋은 방법이야. 다양한 IoT 프로젝트 경험을 가진 전문가들이 당신의 프로젝트를 한 단계 업그레이드하는 데 도움을 줄 수 있을 거야.
자, 이제 직접 만들어볼 차례야! 🚀 행운을 빌어!
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.