์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŒ IoT ์„ผ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์šฐ๋“œ์— ์—…๋กœ๋“œํ•˜๋Š” ์™„์ „ ์ •๋ณต ์†Œ์Šค ๊ฐ€์ด๋“œ
ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ ยท ํ”„๋กœ๊ทธ๋žจ/์†Œ์Šค

๐ŸŒ IoT ์„ผ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์šฐ๋“œ์— ์—…๋กœ๋“œํ•˜๋Š” ์™„์ „ ์ •๋ณต ์†Œ์Šค ๊ฐ€์ด๋“œ

์„ผ์„œ์—์„œ ํด๋ผ์šฐ๋“œ๊นŒ์ง€, ์‹ค์ „ ์ฝ”๋“œ๋กœ ๋ฐฐ์šฐ๋Š” IoT ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ ๐Ÿš€

๐ŸŒก๏ธ IoT ์„ผ์„œ ์˜จ๋„ / ์Šต๋„ ์••๋ ฅ / ๊ฐ€์†๋„ GPS / ์กฐ๋„ ๐Ÿ“ก ๊ฒŒ์ดํŠธ์›จ์ด ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ”„๋กœํ† ์ฝœ ๋ณ€ํ™˜ MQTT / HTTP โš™๏ธ ๋ฏธ๋“ค์›จ์–ด ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต ํ•„ํ„ฐ๋ง / ์••์ถ• ์ธ์ฆ / ์•”ํ˜ธํ™” โ˜๏ธ ํด๋ผ์šฐ๋“œ AWS / GCP Azure / Firebase ์ €์žฅ / ๋ถ„์„ IoT ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ ํ๋ฆ„๋„ ์„ผ์„œ โ†’ ๊ฒŒ์ดํŠธ์›จ์ด โ†’ ๋ฏธ๋“ค์›จ์–ด โ†’ ํด๋ผ์šฐ๋“œ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๋ถ€ํ„ฐ ํด๋ผ์šฐ๋“œ ์ €์žฅ๊นŒ์ง€ ์ „ ๊ณผ์ •์„ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค MQTT ยท HTTP REST ยท WebSocket ยท AWS IoT ยท Firebase ยท Azure IoT Hub
๐Ÿค” IoT ํด๋ผ์šฐ๋“œ ์—…๋กœ๋“œ, ์™œ ์ด๊ฒŒ ์ค‘์š”ํ•ด?

์†”์งํžˆ ๋งํ•˜๋ฉด IoT ์„ผ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์šฐ๋“œ์— ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ ์ฒ˜์Œ์—” ๋ณ„๊ฑฐ ์•„๋‹Œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด์ž–์•„์š”?
"๊ทธ๋ƒฅ HTTP๋กœ POST ๋‚ ๋ฆฌ๋ฉด ๋˜๋Š” ๊ฑฐ ์•„๋ƒ?" ์ด๋ ‡๊ฒŒ ์ƒ๊ฐํ•˜๋Š” ๋ถ„๋“ค ๋งŽ์„ ํ…๋ฐใ…‹ใ…‹ใ…‹
๊ทผ๋ฐ ์‹ค์ œ๋กœ ํ•ด๋ณด๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ๊ณ ๋ คํ•  ๊ฒŒ ์—„์ฒญ ๋งŽ์•„์š”. ์ง„์งœ๋กœ์š” ๐Ÿ˜…

IoT ํ™˜๊ฒฝ์€ ์ผ๋ฐ˜ ์›น ๊ฐœ๋ฐœ์ด๋ž‘ ๋‹ค๋ฅด๊ฒŒ ๋„คํŠธ์›Œํฌ๊ฐ€ ๋ถˆ์•ˆ์ •ํ•˜๊ณ , ๋””๋ฐ”์ด์Šค ๋ฆฌ์†Œ์Šค๊ฐ€ ์ œํ•œ์ ์ด๊ณ ,
๋ฐฐํ„ฐ๋ฆฌ ์ˆ˜๋ช…๋„ ์‹ ๊ฒฝ ์จ์•ผ ํ•˜๊ณ , ๋ณด์•ˆ๋„ ๋นก์„ธ๊ฒŒ ์ฑ™๊ฒจ์•ผ ํ•ด์š”.
๊ฑฐ๊ธฐ๋‹ค ์ˆ˜์‹ญ~์ˆ˜์ฒœ ๊ฐœ์˜ ์„ผ์„œ๊ฐ€ ๋™์‹œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์Ÿ์•„๋‚ด๋Š” ์ƒํ™ฉ๋„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜์ฃ .

์ด ๊ธ€์—์„œ๋Š” ์‹ค์ œ๋กœ ํ˜„์—…์—์„œ ์“ฐ์ด๋Š” IoT ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ ๋œฏ์–ด๋ณผ ๊ฑฐ์˜ˆ์š”.
Python, Arduino(C++), Node.js ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์–‘ํ•œ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ(AWS IoT, Firebase, Azure IoT Hub)์—
๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์„ ์‹ค์ „ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ• ๊ฒŒ์š”. ๋ ˆ์ธ ๊ณ  ๐Ÿš€

๐Ÿ’ก ์ด ๊ธ€์˜ ๋Œ€์ƒ ๋…์ž IoT ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๊ฐœ๋ฐœ์ž, ๋ผ์ฆˆ๋ฒ ๋ฆฌํŒŒ์ด/์•„๋‘์ด๋…ธ ๋‹ค๋ฃจ๋Š” ๋ฉ”์ด์ปค,
ํด๋ผ์šฐ๋“œ ์—ฐ๋™์ด ์ฒ˜์Œ์ธ ๋ถ„๋“ค, ์‹ค์ „ ์†Œ์Šค ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•œ ๋ชจ๋“  ๋ถ„๋“ค!

๐Ÿ“ก IoT ๋ฐ์ดํ„ฐ ์ „์†ก ํ”„๋กœํ† ์ฝœ ์ด์ •๋ฆฌ

ํด๋ผ์šฐ๋“œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ฆฌ๊ธฐ ์ „์— ์–ด๋–ค ํ”„๋กœํ† ์ฝœ๋กœ ๋ณด๋‚ผ์ง€๋ถ€ํ„ฐ ๊ฒฐ์ •ํ•ด์•ผ ํ•ด์š”.
ํ”„๋กœํ† ์ฝœ ์„ ํƒ์ด ์ž˜๋ชป๋˜๋ฉด ๋ฐฐํ„ฐ๋ฆฌ ๋นจ๋ฆฌ ๋‹ณ๊ณ , ๋ฐ์ดํ„ฐ ์œ ์‹ค๋˜๊ณ , ์ง€์—ฐ ๋ฐœ์ƒํ•˜๊ณ ... ๋‚œ๋ฆฌ ๋‚ฉ๋‹ˆ๋‹ค ๐Ÿ˜‚

ํ”„๋กœํ† ์ฝœ ํŠน์ง• ์ ํ•ฉํ•œ ์ƒํ™ฉ ์˜ค๋ฒ„ํ—ค๋“œ
MQTT ๊ฒฝ๋Ÿ‰ pub/sub ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ๋ฐฐํ„ฐ๋ฆฌ ๊ธฐ๊ธฐ, ๋ถˆ์•ˆ์ • ๋„คํŠธ์›Œํฌ ๋งค์šฐ ๋‚ฎ์Œ
HTTP/REST ๋ฒ”์šฉ ์š”์ฒญ-์‘๋‹ต ๋ฐฉ์‹ ๋‹จ์ˆœ ๋ฐ์ดํ„ฐ ์ „์†ก, ์›น ์—ฐ๋™ ์ค‘๊ฐ„
WebSocket ์–‘๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง, ์ œ์–ด ์ค‘๊ฐ„
CoAP UDP ๊ธฐ๋ฐ˜ ๊ฒฝ๋Ÿ‰ ํ”„๋กœํ† ์ฝœ ์ดˆ์ €์ „๋ ฅ ์„ผ์„œ, ํ˜‘๋Œ€์—ญ ํ†ต์‹  ๋งค์šฐ ๋‚ฎ์Œ
AMQP ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๋ฉ”์‹œ์ง€ ํ ๋Œ€๊ทœ๋ชจ ์‚ฐ์—… IoT ๋†’์Œ
โš ๏ธ MQTT vs HTTP ์„ ํƒ ๊ธฐ์ค€ ๋ฐฐํ„ฐ๋ฆฌ๋กœ ๋™์ž‘ํ•˜๋Š” ์†Œํ˜• ์„ผ์„œ๋ผ๋ฉด ๋ฌด์กฐ๊ฑด MQTT ์“ฐ์„ธ์š”!
HTTP๋Š” ๋งค๋ฒˆ TCP ์—ฐ๊ฒฐ์„ ์ƒˆ๋กœ ๋งบ์–ด์„œ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ํฌ๊ฑฐ๋“ ์š”.
MQTT๋Š” ํ•œ ๋ฒˆ ์—ฐ๊ฒฐํ•˜๊ณ  ๊ณ„์† ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฐ์ดํ„ฐ๋งŒ ์˜๋Š” ๋ฐฉ์‹์ด๋ผ ํ›จ์”ฌ ํšจ์œจ์ ์ด์—์š”.
๐Ÿ”ง MQTT ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ

MQTT(Message Queuing Telemetry Transport)๋Š” 1999๋…„์— IBM์ด ๋งŒ๋“  ํ”„๋กœํ† ์ฝœ์ธ๋ฐ,
์ง€๊ธˆ๋„ IoT ์„ธ๊ณ„์—์„œ ์‚ฌ์‹ค์ƒ ํ‘œ์ค€์œผ๋กœ ์“ฐ์ด๊ณ  ์žˆ์–ด์š”.

ํ•ต์‹ฌ ๊ฐœ๋…์€ ์„ธ ๊ฐ€์ง€์˜ˆ์š”:
Publisher(๋ฐœํ–‰์ž) - ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๋Š” ์„ผ์„œ/๋””๋ฐ”์ด์Šค
Broker(๋ธŒ๋กœ์ปค) - ๋ฉ”์‹œ์ง€๋ฅผ ์ค‘๊ณ„ํ•˜๋Š” ์„œ๋ฒ„ (Mosquitto, AWS IoT Core ๋“ฑ)
Subscriber(๊ตฌ๋…์ž) - ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ผ์šฐ๋“œ/์•ฑ

ํ† ํ”ฝ(Topic)์ด๋ผ๋Š” ๊ฐœ๋…์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ถ„๋ฅ˜ํ•ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด home/livingroom/temperature ์ด๋Ÿฐ ์‹์œผ๋กœ ๊ณ„์ธต ๊ตฌ์กฐ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”.


๐Ÿ Python์œผ๋กœ MQTT ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ

์ž, ์ด์ œ ์ง„์งœ ์ฝ”๋“œ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค! ์ฒซ ๋ฒˆ์งธ๋Š” Python + MQTT ์กฐํ•ฉ์ด์—์š”.
๋ผ์ฆˆ๋ฒ ๋ฆฌํŒŒ์ด๋‚˜ ์ผ๋ฐ˜ ๋ฆฌ๋ˆ…์Šค ์„œ๋ฒ„์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ์กฐํ•ฉ์ด์ฃ  ๐Ÿ˜Ž

paho-mqtt ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์˜ˆ์š”. ์„ค์น˜๋ถ€ํ„ฐ ์‹œ์ž‘!

# ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜
pip install paho-mqtt
pip install requests  # HTTP ์—…๋กœ๋“œ์šฉ
pip install python-dotenv  # ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ์šฉ
๐Ÿ“Œ ๊ธฐ๋ณธ MQTT ํผ๋ธ”๋ฆฌ์…” ์†Œ์Šค (์„ผ์„œ โ†’ ๋ธŒ๋กœ์ปค)
import paho.mqtt.client as mqtt
import json
import time
import random
import ssl
from datetime import datetime

# ===== ์„ค์ •๊ฐ’ =====
BROKER_HOST = "your-broker.example.com"  # MQTT ๋ธŒ๋กœ์ปค ์ฃผ์†Œ
BROKER_PORT = 8883                        # TLS ํฌํŠธ (์ผ๋ฐ˜์€ 1883)
CLIENT_ID = "sensor_device_001"
TOPIC = "home/livingroom/sensors"
USERNAME = "your_username"
PASSWORD = "your_password"

# TLS ์ธ์ฆ์„œ ๊ฒฝ๋กœ (๋ณด์•ˆ ์—ฐ๊ฒฐ ์‹œ ํ•„์š”)
CA_CERT = "./certs/ca.crt"
CLIENT_CERT = "./certs/client.crt"
CLIENT_KEY = "./certs/client.key"

# ===== ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋“ค =====
def on_connect(client, userdata, flags, rc):
    """๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ์„ฑ๊ณต/์‹คํŒจ ์ฝœ๋ฐฑ"""
    if rc == 0:
        print(f"โœ… ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ์„ฑ๊ณต! ์ฝ”๋“œ: {rc}")
    else:
        print(f"โŒ ์—ฐ๊ฒฐ ์‹คํŒจ. ์ฝ”๋“œ: {rc}")
        # rc ์ฝ”๋“œ ์˜๋ฏธ:
        # 1: ์ž˜๋ชป๋œ ํ”„๋กœํ† ์ฝœ ๋ฒ„์ „
        # 2: ํด๋ผ์ด์–ธํŠธ ID ๊ฑฐ๋ถ€
        # 3: ์„œ๋ฒ„ ์‚ฌ์šฉ ๋ถˆ๊ฐ€
        # 4: ์ž˜๋ชป๋œ ์‚ฌ์šฉ์ž๋ช…/๋น„๋ฐ€๋ฒˆํ˜ธ
        # 5: ๊ถŒํ•œ ์—†์Œ

def on_publish(client, userdata, mid):
    """๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ์™„๋ฃŒ ์ฝœ๋ฐฑ"""
    print(f"๐Ÿ“ค ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ์™„๋ฃŒ. MID: {mid}")

def on_disconnect(client, userdata, rc):
    """์—ฐ๊ฒฐ ํ•ด์ œ ์ฝœ๋ฐฑ"""
    if rc != 0:
        print(f"โš ๏ธ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—ฐ๊ฒฐ ํ•ด์ œ. ์žฌ์—ฐ๊ฒฐ ์‹œ๋„...")

# ===== ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ ํ•จ์ˆ˜ =====
def read_sensor_data():
    """
    ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” GPIO, I2C, SPI ๋“ฑ์œผ๋กœ ์„ผ์„œ ์ฝ๊ธฐ
    ์—ฌ๊ธฐ์„œ๋Š” ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
    """
    return {
        "device_id": CLIENT_ID,
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "temperature": round(random.uniform(18.0, 35.0), 2),  # ์„ญ์”จ
        "humidity": round(random.uniform(30.0, 80.0), 2),      # %
        "pressure": round(random.uniform(1000.0, 1020.0), 2),  # hPa
        "battery": round(random.uniform(3.0, 4.2), 2),         # V
        "rssi": random.randint(-90, -40)                        # dBm (์‹ ํ˜ธ ๊ฐ•๋„)
    }

# ===== MQTT ํด๋ผ์ด์–ธํŠธ ์„ค์ • =====
def setup_mqtt_client():
    client = mqtt.Client(
        client_id=CLIENT_ID,
        clean_session=True,
        protocol=mqtt.MQTTv311  # MQTT 3.1.1 ๋ฒ„์ „ ์‚ฌ์šฉ
    )
    
    # ์ธ์ฆ ์„ค์ •
    client.username_pw_set(USERNAME, PASSWORD)
    
    # TLS/SSL ์„ค์ • (๋ณด์•ˆ ์—ฐ๊ฒฐ)
    client.tls_set(
        ca_certs=CA_CERT,
        certfile=CLIENT_CERT,
        keyfile=CLIENT_KEY,
        tls_version=ssl.PROTOCOL_TLSv1_2
    )
    
    # ์ฝœ๋ฐฑ ๋“ฑ๋ก
    client.on_connect = on_connect
    client.on_publish = on_publish
    client.on_disconnect = on_disconnect
    
    # Last Will Testament (LWT) ์„ค์ •
    # ๋””๋ฐ”์ด์Šค๊ฐ€ ๊ฐ‘์ž๊ธฐ ๋Š๊ฒผ์„ ๋•Œ ๋ธŒ๋กœ์ปค๊ฐ€ ์ž๋™์œผ๋กœ ๋ฐœํ–‰ํ•˜๋Š” ๋ฉ”์‹œ์ง€
    lwt_payload = json.dumps({
        "device_id": CLIENT_ID,
        "status": "offline",
        "timestamp": datetime.utcnow().isoformat() + "Z"
    })
    client.will_set(
        topic=f"home/devices/{CLIENT_ID}/status",
        payload=lwt_payload,
        qos=1,
        retain=True
    )
    
    return client

# ===== ๋ฉ”์ธ ์‹คํ–‰ =====
def main():
    client = setup_mqtt_client()
    
    # ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ (๋น„๋™๊ธฐ)
    client.connect(BROKER_HOST, BROKER_PORT, keepalive=60)
    
    # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋„คํŠธ์›Œํฌ ๋ฃจํ”„ ์‹œ์ž‘
    client.loop_start()
    
    # ์˜จ๋ผ์ธ ์ƒํƒœ ๋ฐœํ–‰
    online_payload = json.dumps({
        "device_id": CLIENT_ID,
        "status": "online",
        "timestamp": datetime.utcnow().isoformat() + "Z"
    })
    client.publish(
        topic=f"home/devices/{CLIENT_ID}/status",
        payload=online_payload,
        qos=1,
        retain=True
    )
    
    print("๐Ÿš€ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ „์†ก ์‹œ์ž‘!")
    
    try:
        while True:
            # ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ
            sensor_data = read_sensor_data()
            
            # JSON ์ง๋ ฌํ™”
            payload = json.dumps(sensor_data, ensure_ascii=False)
            
            # MQTT ๋ฐœํ–‰
            # QoS ๋ ˆ๋ฒจ:
            # 0: ์ตœ๋Œ€ 1ํšŒ ์ „๋‹ฌ (๋น ๋ฅด์ง€๋งŒ ์œ ์‹ค ๊ฐ€๋Šฅ)
            # 1: ์ตœ์†Œ 1ํšŒ ์ „๋‹ฌ (์ค‘๋ณต ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์œ ์‹ค ์—†์Œ)
            # 2: ์ •ํ™•ํžˆ 1ํšŒ ์ „๋‹ฌ (๊ฐ€์žฅ ์•ˆ์ „ํ•˜์ง€๋งŒ ๋А๋ฆผ)
            result = client.publish(
                topic=TOPIC,
                payload=payload,
                qos=1,
                retain=False
            )
            
            if result.rc == mqtt.MQTT_ERR_SUCCESS:
                print(f"โœ… ์ „์†ก ์„ฑ๊ณต: {sensor_data['temperature']}ยฐC, {sensor_data['humidity']}%")
            else:
                print(f"โŒ ์ „์†ก ์‹คํŒจ. ์—๋Ÿฌ ์ฝ”๋“œ: {result.rc}")
            
            time.sleep(5)  # 5์ดˆ๋งˆ๋‹ค ์ „์†ก
            
    except KeyboardInterrupt:
        print("\nโ›” ์ข…๋ฃŒ ์‹ ํ˜ธ ๊ฐ์ง€. ์ •๋ฆฌ ์ค‘...")
    finally:
        client.loop_stop()
        client.disconnect()
        print("๐Ÿ‘‹ ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์™„๋ฃŒ")

if __name__ == "__main__":
    main()
๐Ÿ’ก QoS ๋ ˆ๋ฒจ ์„ ํƒ ํŒ ์˜จ๋„/์Šต๋„ ๊ฐ™์€ ์ฃผ๊ธฐ์  ๋ฐ์ดํ„ฐ โ†’ QoS 0 (๋น ๋ฅธ ์ „์†ก, ์•ฝ๊ฐ„์˜ ์œ ์‹ค ํ—ˆ์šฉ)
์•Œ๋žŒ/๊ฒฝ๊ณ  ๋ฐ์ดํ„ฐ โ†’ QoS 1 (๋ฐ˜๋“œ์‹œ ๋„๋‹ฌํ•ด์•ผ ํ•จ)
๊ธˆ์œต/์˜๋ฃŒ ๋ฐ์ดํ„ฐ โ†’ QoS 2 (์ •ํ™•ํžˆ ํ•œ ๋ฒˆ๋งŒ ์ „๋‹ฌ ๋ณด์žฅ)
๐Ÿ”„ ์žฌ์—ฐ๊ฒฐ ๋กœ์ง ์ถ”๊ฐ€ (์‹ค์ „ ํ•„์ˆ˜!)

์‹ค์ œ IoT ํ™˜๊ฒฝ์—์„œ๋Š” ๋„คํŠธ์›Œํฌ๊ฐ€ ๋š๋š ๋Š๊ฒจ์š”ใ…‹ใ…‹
๊ทธ๋ž˜์„œ ์žฌ์—ฐ๊ฒฐ ๋กœ์ง์€ ์ง„์งœ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. ์—†์œผ๋ฉด ๋‚˜์ค‘์— ์—„์ฒญ ํ›„ํšŒํ•ด์š” ๐Ÿ˜…

import paho.mqtt.client as mqtt
import json
import time
import threading
from datetime import datetime

class RobustMQTTPublisher:
    """
    ์žฌ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ์ด ํฌํ•จ๋œ ์•ˆ์ •์ ์ธ MQTT ํผ๋ธ”๋ฆฌ์…”
    ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„(Exponential Backoff) ๋ฐฉ์‹์œผ๋กœ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
    """
    
    def __init__(self, broker_host, broker_port, client_id, topic):
        self.broker_host = broker_host
        self.broker_port = broker_port
        self.client_id = client_id
        self.topic = topic
        self.is_connected = False
        self.reconnect_delay = 1      # ์ดˆ๊ธฐ ์žฌ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (์ดˆ)
        self.max_reconnect_delay = 60 # ์ตœ๋Œ€ ์žฌ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (์ดˆ)
        self.message_queue = []       # ์˜คํ”„๋ผ์ธ ์ค‘ ์Œ“์ธ ๋ฉ”์‹œ์ง€ ํ
        self.max_queue_size = 100     # ์ตœ๋Œ€ ํ ํฌ๊ธฐ
        
        self.client = mqtt.Client(client_id=client_id)
        self.client.on_connect = self._on_connect
        self.client.on_disconnect = self._on_disconnect
        self.client.on_publish = self._on_publish
        
    def _on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            self.is_connected = True
            self.reconnect_delay = 1  # ์„ฑ๊ณต ์‹œ ๋”œ๋ ˆ์ด ๋ฆฌ์…‹
            print(f"โœ… [{datetime.now().strftime('%H:%M:%S')}] ์—ฐ๊ฒฐ ์„ฑ๊ณต!")
            
            # ์˜คํ”„๋ผ์ธ ์ค‘ ์Œ“์ธ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
            self._flush_queue()
        else:
            print(f"โŒ ์—ฐ๊ฒฐ ์‹คํŒจ. RC: {rc}")
    
    def _on_disconnect(self, client, userdata, rc):
        self.is_connected = False
        if rc != 0:
            print(f"โš ๏ธ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—ฐ๊ฒฐ ํ•ด์ œ. {self.reconnect_delay}์ดˆ ํ›„ ์žฌ์—ฐ๊ฒฐ...")
            threading.Thread(target=self._reconnect, daemon=True).start()
    
    def _on_publish(self, client, userdata, mid):
        pass  # ๋ฐœํ–‰ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
    
    def _reconnect(self):
        """์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ๋ฐฉ์‹ ์žฌ์—ฐ๊ฒฐ"""
        while not self.is_connected:
            try:
                self.client.reconnect()
                break
            except Exception as e:
                print(f"๐Ÿ”„ ์žฌ์—ฐ๊ฒฐ ์‹คํŒจ: {e}. {self.reconnect_delay}์ดˆ ํ›„ ์žฌ์‹œ๋„...")
                time.sleep(self.reconnect_delay)
                # ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: 1 โ†’ 2 โ†’ 4 โ†’ 8 โ†’ ... โ†’ 60์ดˆ
                self.reconnect_delay = min(
                    self.reconnect_delay * 2,
                    self.max_reconnect_delay
                )
    
    def _flush_queue(self):
        """์˜คํ”„๋ผ์ธ ์ค‘ ์Œ“์ธ ๋ฉ”์‹œ์ง€ ์ผ๊ด„ ์ „์†ก"""
        if self.message_queue:
            print(f"๐Ÿ“ฆ ํ์— ์Œ“์ธ {len(self.message_queue)}๊ฐœ ๋ฉ”์‹œ์ง€ ์ „์†ก ์ค‘...")
            while self.message_queue and self.is_connected:
                msg = self.message_queue.pop(0)
                self.client.publish(self.topic, msg, qos=1)
                time.sleep(0.1)  # ๋ธŒ๋กœ์ปค ๊ณผ๋ถ€ํ•˜ ๋ฐฉ์ง€
    
    def publish(self, data: dict):
        """๋ฐ์ดํ„ฐ ๋ฐœํ–‰ (์˜คํ”„๋ผ์ธ ์‹œ ํ์— ์ €์žฅ)"""
        payload = json.dumps(data, ensure_ascii=False)
        
        if self.is_connected:
            result = self.client.publish(self.topic, payload, qos=1)
            return result.rc == mqtt.MQTT_ERR_SUCCESS
        else:
            # ์˜คํ”„๋ผ์ธ ์ƒํƒœ: ํ์— ์ €์žฅ
            if len(self.message_queue) < self.max_queue_size:
                self.message_queue.append(payload)
                print(f"๐Ÿ“ฅ ์˜คํ”„๋ผ์ธ ํ ์ €์žฅ. ํ˜„์žฌ ํ ํฌ๊ธฐ: {len(self.message_queue)}")
            else:
                print("โš ๏ธ ํ ๊ฐ€๋“ ์ฐธ! ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๋ฉ”์‹œ์ง€ ์‚ญ์ œ")
                self.message_queue.pop(0)
                self.message_queue.append(payload)
            return False
    
    def connect(self):
        self.client.connect(self.broker_host, self.broker_port, keepalive=60)
        self.client.loop_start()
    
    def disconnect(self):
        self.client.loop_stop()
        self.client.disconnect()

# ์‚ฌ์šฉ ์˜ˆ์‹œ
publisher = RobustMQTTPublisher(
    broker_host="broker.example.com",
    broker_port=1883,
    client_id="sensor_001",
    topic="sensors/data"
)
publisher.connect()

# ๋ฐ์ดํ„ฐ ์ „์†ก
publisher.publish({
    "temperature": 25.3,
    "humidity": 60.5,
    "timestamp": datetime.utcnow().isoformat() + "Z"
})

โ˜๏ธ AWS IoT Core ์—ฐ๋™ ์™„์ „ ๊ฐ€์ด๋“œ

AWS IoT Core๋Š” ํ˜„์žฌ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” IoT ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ์ด์—์š”.
์ˆ˜์‹ญ์–ต ๊ฐœ์˜ ๋””๋ฐ”์ด์Šค๋ฅผ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๊ณ , ๋‹ค๋ฅธ AWS ์„œ๋น„์Šค(DynamoDB, S3, Lambda ๋“ฑ)์™€
์—ฐ๋™์ด ์‰ฌ์›Œ์„œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ™˜๊ฒฝ์—์„œ ํŠนํžˆ ์ธ๊ธฐ ๋งŽ์•„์š” ๐Ÿ˜Ž

AWS IoT Core ์•„ํ‚คํ…์ฒ˜ ๋””๋ฐ”์ด์Šค โ†’ IoT Core โ†’ ๊ทœ์น™ ์—”์ง„ โ†’ AWS ์„œ๋น„์Šค ๐Ÿ“ฑ IoT ๋””๋ฐ”์ด์Šค MQTT/TLS X.509 ์ธ์ฆ์„œ โ˜๏ธ AWS IoT Core ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ๋””๋ฐ”์ด์Šค ์„€๋„์šฐ ๊ทœ์น™ ์—”์ง„ ๋””๋ฐ”์ด์Šค ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ โš™๏ธ ๊ทœ์น™ ์—”์ง„ SQL ์ฟผ๋ฆฌ ์กฐ๊ฑด ํ•„ํ„ฐ๋ง ์•ก์…˜ ํŠธ๋ฆฌ๊ฑฐ ๐Ÿ—„๏ธ DynamoDB ์‹œ๊ณ„์—ด ์ €์žฅ ๐Ÿชฃ S3 ์›์‹œ ๋ฐ์ดํ„ฐ ์ €์žฅ โšก Lambda ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ๐Ÿ“Š Kinesis ์ŠคํŠธ๋ฆผ ๋ถ„์„ ๐Ÿ” ๋ณด์•ˆ ๋ ˆ์ด์–ด X.509 ์ธ์ฆ์„œ ๊ธฐ๋ฐ˜ ์ƒํ˜ธ TLS ์ธ์ฆ | AWS IAM ์ •์ฑ… | ๋””๋ฐ”์ด์Šค ์ •์ฑ… | ๋ฆฌ์†Œ์Šค ๊ธฐ๋ฐ˜ ์ •์ฑ…
๐Ÿ”‘ AWS IoT Core ์—ฐ๊ฒฐ ์ค€๋น„ (์ธ์ฆ์„œ ๋ฐœ๊ธ‰)

AWS IoT Core๋Š” X.509 ์ธ์ฆ์„œ ๊ธฐ๋ฐ˜์œผ๋กœ ๋””๋ฐ”์ด์Šค๋ฅผ ์ธ์ฆํ•ด์š”.
๋น„๋ฐ€๋ฒˆํ˜ธ ๋ฐฉ์‹์ด ์•„๋‹ˆ๋ผ ์ธ์ฆ์„œ ๋ฐฉ์‹์ด๋ผ ํ›จ์”ฌ ์•ˆ์ „ํ•˜์ฃ !
AWS ์ฝ˜์†”์—์„œ Thing์„ ์ƒ์„ฑํ•˜๊ณ  ์ธ์ฆ์„œ๋ฅผ ๋‹ค์šด๋กœ๋“œํ•ด์•ผ ํ•ด์š”.

# AWS CLI๋กœ IoT Thing ์ƒ์„ฑ ๋ฐ ์ธ์ฆ์„œ ๋ฐœ๊ธ‰
# (AWS CLI ์„ค์น˜ ๋ฐ ์ž๊ฒฉ์ฆ๋ช… ์„ค์ • ํ•„์š”)

# 1. Thing ์ƒ์„ฑ
aws iot create-thing --thing-name "MySensor001"

# 2. ์ธ์ฆ์„œ ์ƒ์„ฑ ๋ฐ ํ™œ์„ฑํ™”
aws iot create-keys-and-certificate \
    --set-as-active \
    --certificate-pem-outfile "device.cert.pem" \
    --public-key-outfile "device.public.key" \
    --private-key-outfile "device.private.key"

# 3. ์ •์ฑ… ์ƒ์„ฑ (JSON ํŒŒ์ผ๋กœ ์ €์žฅ ํ›„ ์‹คํ–‰)
# iot-policy.json ๋‚ด์šฉ:
# {
#   "Version": "2012-10-17",
#   "Statement": [{
#     "Effect": "Allow",
#     "Action": ["iot:Connect", "iot:Publish", "iot:Subscribe", "iot:Receive"],
#     "Resource": ["arn:aws:iot:ap-northeast-2:*:*"]
#   }]
# }

aws iot create-policy \
    --policy-name "SensorPolicy" \
    --policy-document file://iot-policy.json

# 4. ์ •์ฑ…์„ ์ธ์ฆ์„œ์— ์—ฐ๊ฒฐ
aws iot attach-policy \
    --policy-name "SensorPolicy" \
    --target "arn:aws:iot:ap-northeast-2:123456789:cert/your-cert-id"

# 5. ์ธ์ฆ์„œ๋ฅผ Thing์— ์—ฐ๊ฒฐ
aws iot attach-thing-principal \
    --thing-name "MySensor001" \
    --principal "arn:aws:iot:ap-northeast-2:123456789:cert/your-cert-id"

# 6. AWS IoT ์—”๋“œํฌ์ธํŠธ ํ™•์ธ
aws iot describe-endpoint --endpoint-type iot:Data-ATS
# ๊ฒฐ๊ณผ: "xxxxx-ats.iot.ap-northeast-2.amazonaws.com"
๐Ÿ Python์œผ๋กœ AWS IoT Core ์—ฐ๊ฒฐ ์†Œ์Šค
"""
AWS IoT Core MQTT ์—ฐ๊ฒฐ ๋ฐ ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ
ํ•„์š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: pip install awsiotsdk
"""

from awscrt import mqtt
from awsiot import mqtt_connection_builder
import json
import time
import threading
from datetime import datetime

# ===== AWS IoT ์„ค์ • =====
ENDPOINT = "xxxxx-ats.iot.ap-northeast-2.amazonaws.com"
CLIENT_ID = "MySensor001"
TOPIC = "sensors/MySensor001/data"
CERT_FILEPATH = "./certs/device.cert.pem"
PRI_KEY_FILEPATH = "./certs/device.private.key"
CA_FILEPATH = "./certs/AmazonRootCA1.pem"  # AWS ๋ฃจํŠธ CA ์ธ์ฆ์„œ

# ์—ฐ๊ฒฐ ์™„๋ฃŒ ์ด๋ฒคํŠธ
connected_future = threading.Event()
disconnect_future = threading.Event()

def on_connection_interrupted(connection, error, **kwargs):
    """์—ฐ๊ฒฐ ์ค‘๋‹จ ์ฝœ๋ฐฑ"""
    print(f"โš ๏ธ ์—ฐ๊ฒฐ ์ค‘๋‹จ: {error}")

def on_connection_resumed(connection, return_code, session_present, **kwargs):
    """์—ฐ๊ฒฐ ์žฌ๊ฐœ ์ฝœ๋ฐฑ"""
    print(f"โœ… ์—ฐ๊ฒฐ ์žฌ๊ฐœ. ์„ธ์…˜ ์œ ์ง€: {session_present}")

def on_connection_success(connection, callback_data):
    """์—ฐ๊ฒฐ ์„ฑ๊ณต ์ฝœ๋ฐฑ"""
    print(f"๐ŸŽ‰ AWS IoT Core ์—ฐ๊ฒฐ ์„ฑ๊ณต!")
    connected_future.set()

def on_connection_failure(connection, callback_data):
    """์—ฐ๊ฒฐ ์‹คํŒจ ์ฝœ๋ฐฑ"""
    print(f"โŒ ์—ฐ๊ฒฐ ์‹คํŒจ: {callback_data.error}")

def on_connection_closed(connection, callback_data):
    """์—ฐ๊ฒฐ ์ข…๋ฃŒ ์ฝœ๋ฐฑ"""
    print("๐Ÿ‘‹ ์—ฐ๊ฒฐ ์ข…๋ฃŒ")
    disconnect_future.set()

# ===== MQTT ์—ฐ๊ฒฐ ๋นŒ๋“œ =====
mqtt_connection = mqtt_connection_builder.mtls_from_path(
    endpoint=ENDPOINT,
    port=8883,
    cert_filepath=CERT_FILEPATH,
    pri_key_filepath=PRI_KEY_FILEPATH,
    ca_filepath=CA_FILEPATH,
    client_id=CLIENT_ID,
    clean_session=False,  # ์„ธ์…˜ ์œ ์ง€ (QoS 1 ๋ฉ”์‹œ์ง€ ๋ณด์žฅ)
    keep_alive_secs=30,
    on_connection_interrupted=on_connection_interrupted,
    on_connection_resumed=on_connection_resumed,
    on_connection_success=on_connection_success,
    on_connection_failure=on_connection_failure,
    on_connection_closed=on_connection_closed
)

print(f"๐Ÿ”Œ AWS IoT Core ์—ฐ๊ฒฐ ์ค‘... ({ENDPOINT})")
connect_future = mqtt_connection.connect()
connect_future.result()  # ์—ฐ๊ฒฐ ์™„๋ฃŒ๊นŒ์ง€ ๋Œ€๊ธฐ

print("โœ… ์—ฐ๊ฒฐ ์™„๋ฃŒ! ๋ฐ์ดํ„ฐ ์ „์†ก ์‹œ์ž‘")

# ===== ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ „์†ก =====
def send_sensor_data(temperature, humidity, pressure):
    """์„ผ์„œ ๋ฐ์ดํ„ฐ๋ฅผ AWS IoT Core๋กœ ์ „์†ก"""
    payload = {
        "device_id": CLIENT_ID,
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "location": {
            "building": "A๋™",
            "floor": 3,
            "room": "์„œ๋ฒ„์‹ค"
        },
        "sensors": {
            "temperature": {
                "value": temperature,
                "unit": "celsius"
            },
            "humidity": {
                "value": humidity,
                "unit": "percent"
            },
            "pressure": {
                "value": pressure,
                "unit": "hPa"
            }
        },
        "metadata": {
            "firmware_version": "1.2.3",
            "battery_level": 85,
            "signal_strength": -65
        }
    }
    
    message_json = json.dumps(payload, ensure_ascii=False)
    
    # MQTT ๋ฐœํ–‰
    publish_future, packet_id = mqtt_connection.publish(
        topic=TOPIC,
        payload=message_json,
        qos=mqtt.QoS.AT_LEAST_ONCE  # QoS 1
    )
    
    publish_future.result()  # ๋ฐœํ–‰ ์™„๋ฃŒ ๋Œ€๊ธฐ
    print(f"๐Ÿ“ค ์ „์†ก ์™„๋ฃŒ (ํŒจํ‚ท ID: {packet_id}): {temperature}ยฐC, {humidity}%")

# ===== ๋””๋ฐ”์ด์Šค ์„€๋„์šฐ ์—…๋ฐ์ดํŠธ =====
def update_device_shadow(state: dict):
    """
    AWS IoT ๋””๋ฐ”์ด์Šค ์„€๋„์šฐ ์—…๋ฐ์ดํŠธ
    ์„€๋„์šฐ: ๋””๋ฐ”์ด์Šค์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ํด๋ผ์šฐ๋“œ์— ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ
    ์˜คํ”„๋ผ์ธ ๋””๋ฐ”์ด์Šค์˜ ์ƒํƒœ๋„ ํด๋ผ์šฐ๋“œ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅ
    """
    shadow_topic = f"$aws/things/{CLIENT_ID}/shadow/update"
    shadow_payload = {
        "state": {
            "reported": state  # ๋””๋ฐ”์ด์Šค๊ฐ€ ์‹ค์ œ๋กœ ๋ณด๊ณ ํ•˜๋Š” ์ƒํƒœ
        }
    }
    
    mqtt_connection.publish(
        topic=shadow_topic,
        payload=json.dumps(shadow_payload),
        qos=mqtt.QoS.AT_LEAST_ONCE
    )
    print(f"๐Ÿ”„ ๋””๋ฐ”์ด์Šค ์„€๋„์šฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ")

# ๋ฉ”์ธ ๋ฃจํ”„
try:
    import random
    while True:
        temp = round(random.uniform(18.0, 35.0), 2)
        hum = round(random.uniform(30.0, 80.0), 2)
        pres = round(random.uniform(1000.0, 1020.0), 2)
        
        send_sensor_data(temp, hum, pres)
        
        # ์„€๋„์šฐ๋„ ์ฃผ๊ธฐ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ
        update_device_shadow({
            "temperature": temp,
            "humidity": hum,
            "status": "active"
        })
        
        time.sleep(10)

except KeyboardInterrupt:
    print("\nโ›” ์ข…๋ฃŒ ์ค‘...")
    disconnect_future_result = mqtt_connection.disconnect()
    disconnect_future_result.result()
    print("โœ… ์ •์ƒ ์ข…๋ฃŒ")

๐Ÿ”ฅ Firebase Realtime Database / Firestore ์—ฐ๋™

Firebase๋Š” ๊ตฌ๊ธ€์ด ๋งŒ๋“  BaaS(Backend as a Service)์ธ๋ฐ,
์†Œ๊ทœ๋ชจ IoT ํ”„๋กœ์ ํŠธ๋‚˜ ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ดํ•‘์— ์ง„์งœ ์ตœ๊ณ ์˜ˆ์š”!
์„ค์ •์ด ๊ฐ„๋‹จํ•˜๊ณ , ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ์ง€์›๋ผ์„œ
๋Œ€์‹œ๋ณด๋“œ ๋งŒ๋“ค๊ธฐ๋„ ์—„์ฒญ ํŽธํ•ด์š” ใ…‹ใ…‹ ๐Ÿ˜„

ํŠนํžˆ Firebase Realtime Database๋Š” WebSocket ๊ธฐ๋ฐ˜์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋˜๊ณ ,
Firestore๋Š” ๋” ๊ฐ•๋ ฅํ•œ ์ฟผ๋ฆฌ์™€ ํ™•์žฅ์„ฑ์„ ์ œ๊ณตํ•ด์š”.

๐Ÿ Python์œผ๋กœ Firebase ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ
"""
Firebase Realtime Database / Firestore IoT ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ
pip install firebase-admin
"""

import firebase_admin
from firebase_admin import credentials, db, firestore
import json
import time
from datetime import datetime
import random

# ===== Firebase ์ดˆ๊ธฐํ™” =====
# Firebase ์ฝ˜์†”์—์„œ ์„œ๋น„์Šค ๊ณ„์ • ํ‚ค ๋‹ค์šด๋กœ๋“œ ํ•„์š”
cred = credentials.Certificate("./firebase-service-account.json")

firebase_admin.initialize_app(cred, {
    'databaseURL': 'https://your-project-id-default-rtdb.firebaseio.com/'
})

# Firestore ํด๋ผ์ด์–ธํŠธ
firestore_db = firestore.client()

# ===== Realtime Database ์—…๋กœ๋“œ =====
def upload_to_realtime_db(device_id: str, sensor_data: dict):
    """
    Firebase Realtime Database์— ์„ผ์„œ ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ
    ๊ตฌ์กฐ: /devices/{device_id}/latest ์— ์ตœ์‹  ๋ฐ์ดํ„ฐ ์ €์žฅ
          /devices/{device_id}/history/{timestamp} ์— ์ด๋ ฅ ์ €์žฅ
    """
    ref = db.reference(f'/devices/{device_id}')
    
    # ์ตœ์‹  ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ (๋ฎ์–ด์“ฐ๊ธฐ)
    ref.child('latest').set(sensor_data)
    
    # ์ด๋ ฅ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ํ‚ค๋กœ ์‚ฌ์šฉ)
    timestamp_key = datetime.utcnow().strftime('%Y%m%d_%H%M%S_%f')
    ref.child('history').child(timestamp_key).set(sensor_data)
    
    print(f"โœ… Realtime DB ์—…๋กœ๋“œ ์™„๋ฃŒ: {device_id}")

# ===== Firestore ์—…๋กœ๋“œ =====
def upload_to_firestore(device_id: str, sensor_data: dict):
    """
    Firestore์— ์„ผ์„œ ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ
    ์ปฌ๋ ‰์…˜: sensor_readings
    ๋ฌธ์„œ: ์ž๋™ ID ์ƒ์„ฑ
    """
    # ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€
    sensor_data['created_at'] = firestore.SERVER_TIMESTAMP
    sensor_data['device_id'] = device_id
    
    # ์ปฌ๋ ‰์…˜์— ๋ฌธ์„œ ์ถ”๊ฐ€
    doc_ref = firestore_db.collection('sensor_readings').add(sensor_data)
    print(f"โœ… Firestore ์—…๋กœ๋“œ ์™„๋ฃŒ. ๋ฌธ์„œ ID: {doc_ref[1].id}")
    
    # ๋””๋ฐ”์ด์Šค ์ƒํƒœ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ (upsert)
    device_ref = firestore_db.collection('devices').document(device_id)
    device_ref.set({
        'last_seen': firestore.SERVER_TIMESTAMP,
        'latest_data': sensor_data,
        'status': 'online'
    }, merge=True)  # merge=True: ๊ธฐ์กด ํ•„๋“œ ์œ ์ง€ํ•˜๋ฉด์„œ ์—…๋ฐ์ดํŠธ

# ===== ๋ฐฐ์น˜ ์—…๋กœ๋“œ (์—ฌ๋Ÿฌ ์„ผ์„œ ๋™์‹œ ์ฒ˜๋ฆฌ) =====
def batch_upload_to_firestore(readings: list):
    """
    ์—ฌ๋Ÿฌ ์„ผ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฐ์น˜๋กœ ํ•œ ๋ฒˆ์— ์—…๋กœ๋“œ
    Firestore ๋ฐฐ์น˜ ์“ฐ๊ธฐ: ์ตœ๋Œ€ 500๊ฐœ ๋ฌธ์„œ ๋™์‹œ ์ฒ˜๋ฆฌ
    """
    batch = firestore_db.batch()
    collection_ref = firestore_db.collection('sensor_readings')
    
    for reading in readings[:500]:  # ์ตœ๋Œ€ 500๊ฐœ ์ œํ•œ
        doc_ref = collection_ref.document()  # ์ž๋™ ID
        reading['created_at'] = firestore.SERVER_TIMESTAMP
        batch.set(doc_ref, reading)
    
    batch.commit()
    print(f"โœ… ๋ฐฐ์น˜ ์—…๋กœ๋“œ ์™„๋ฃŒ: {len(readings)}๊ฐœ ๋ฌธ์„œ")

# ===== ์‹ค์‹œ๊ฐ„ ๋ฆฌ์Šค๋„ˆ (ํด๋ผ์šฐ๋“œ โ†’ ๋””๋ฐ”์ด์Šค ๋ช…๋ น ์ˆ˜์‹ ) =====
def setup_command_listener(device_id: str):
    """
    Firestore์—์„œ ๋””๋ฐ”์ด์Šค ๋ช…๋ น์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆ˜์‹ 
    ํด๋ผ์šฐ๋“œ์—์„œ ๋””๋ฐ”์ด์Šค๋ฅผ ์›๊ฒฉ ์ œ์–ดํ•  ๋•Œ ์‚ฌ์šฉ
    """
    def on_snapshot(doc_snapshot, changes, read_time):
        for change in changes:
            if change.type.name == 'ADDED' or change.type.name == 'MODIFIED':
                command = change.document.to_dict()
                print(f"๐Ÿ“ฉ ๋ช…๋ น ์ˆ˜์‹ : {command}")
                
                # ๋ช…๋ น ์ฒ˜๋ฆฌ
                if command.get('action') == 'reboot':
                    print("๐Ÿ”„ ์žฌ๋ถ€ํŒ… ๋ช…๋ น ์‹คํ–‰!")
                elif command.get('action') == 'update_interval':
                    new_interval = command.get('interval', 10)
                    print(f"โฑ๏ธ ์ „์†ก ์ฃผ๊ธฐ ๋ณ€๊ฒฝ: {new_interval}์ดˆ")
                
                # ๋ช…๋ น ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ‘œ์‹œ
                change.document.reference.update({
                    'status': 'executed',
                    'executed_at': firestore.SERVER_TIMESTAMP
                })
    
    # ๋ช…๋ น ์ปฌ๋ ‰์…˜ ๊ตฌ๋…
    commands_ref = firestore_db.collection('devices').document(device_id)\
                               .collection('commands')\
                               .where('status', '==', 'pending')
    
    watch = commands_ref.on_snapshot(on_snapshot)
    return watch

# ===== ๋ฉ”์ธ ์‹คํ–‰ =====
DEVICE_ID = "sensor_001"

# ๋ช…๋ น ๋ฆฌ์Šค๋„ˆ ์‹œ์ž‘
command_watcher = setup_command_listener(DEVICE_ID)

print("๐Ÿš€ Firebase ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ ์‹œ์ž‘!")

try:
    while True:
        sensor_data = {
            "temperature": round(random.uniform(18.0, 35.0), 2),
            "humidity": round(random.uniform(30.0, 80.0), 2),
            "co2": round(random.uniform(400, 1200), 0),
            "lux": round(random.uniform(0, 1000), 1),
            "timestamp": datetime.utcnow().isoformat() + "Z"
        }
        
        # Realtime DB์™€ Firestore ๋™์‹œ ์—…๋กœ๋“œ
        upload_to_realtime_db(DEVICE_ID, sensor_data)
        upload_to_firestore(DEVICE_ID, sensor_data)
        
        time.sleep(10)

except KeyboardInterrupt:
    command_watcher.unsubscribe()
    print("โœ… ์ข…๋ฃŒ ์™„๋ฃŒ")
๐Ÿ’ก Firebase ์š”๊ธˆ ์ฃผ์˜! Firebase ๋ฌด๋ฃŒ ํ”Œ๋žœ(Spark)์€ Realtime DB 1GB ์ €์žฅ, 10GB/์›” ๋‹ค์šด๋กœ๋“œ ์ œํ•œ์ด์—์š”.
IoT ๋ฐ์ดํ„ฐ๋Š” ๋น ๋ฅด๊ฒŒ ์Œ“์ด๋‹ˆ๊นŒ ๋ฐ์ดํ„ฐ ๋ณด์กด ์ •์ฑ…(TTL)์„ ๊ผญ ์„ค์ •ํ•˜์„ธ์š”!
์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋Š” Cloud Functions๋กœ ์ž๋™ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ BigQuery๋กœ ์ด๊ด€ํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”.

๐Ÿ”ท Azure IoT Hub ์—ฐ๋™ ์†Œ์Šค

๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ์˜ Azure IoT Hub๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ™˜๊ฒฝ์—์„œ ๊ฐ•์„ธ์˜ˆ์š”.
ํŠนํžˆ Windows ๊ธฐ๋ฐ˜ ์ธํ”„๋ผ๋ฅผ ์“ฐ๋Š” ๊ธฐ์—…๋“ค์ด ๋งŽ์ด ์„ ํƒํ•˜์ฃ .
Azure IoT Hub๋Š” ๋””๋ฐ”์ด์Šค ๊ด€๋ฆฌ, ๋ณด์•ˆ, ์Šค์ผ€์ผ๋ง ๋ชจ๋‘ ๊ฐ•๋ ฅํ•ด์š” ๐Ÿ’ช

"""
Azure IoT Hub ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ
pip install azure-iot-device
"""

import asyncio
import json
import random
from datetime import datetime
from azure.iot.device.aio import IoTHubDeviceClient
from azure.iot.device import Message

# ===== Azure IoT Hub ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด =====
# Azure ํฌํ„ธ โ†’ IoT Hub โ†’ ๋””๋ฐ”์ด์Šค โ†’ ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด ๋ณต์‚ฌ
CONNECTION_STRING = "HostName=your-hub.azure-devices.net;DeviceId=MySensor001;SharedAccessKey=your-key=="

# ===== ์ง์ ‘ ๋ฉ”์„œ๋“œ ํ•ธ๋“ค๋Ÿฌ (ํด๋ผ์šฐ๋“œ โ†’ ๋””๋ฐ”์ด์Šค ๋ช…๋ น) =====
async def method_request_handler(method_request):
    """
    Azure IoT Hub ์ง์ ‘ ๋ฉ”์„œ๋“œ ์ˆ˜์‹  ํ•ธ๋“ค๋Ÿฌ
    ํด๋ผ์šฐ๋“œ์—์„œ ๋””๋ฐ”์ด์Šค๋กœ ์ง์ ‘ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ธฐ๋Šฅ
    """
    print(f"๐Ÿ“ฉ ์ง์ ‘ ๋ฉ”์„œ๋“œ ์ˆ˜์‹ : {method_request.name}")
    print(f"   ํŽ˜์ด๋กœ๋“œ: {method_request.payload}")
    
    if method_request.name == "reboot":
        print("๐Ÿ”„ ์žฌ๋ถ€ํŒ… ๋ช…๋ น ์‹คํ–‰!")
        response_payload = {"result": "reboot_initiated", "status": "success"}
        status_code = 200
        
    elif method_request.name == "get_status":
        response_payload = {
            "device_id": "MySensor001",
            "uptime": 3600,
            "firmware": "v1.2.3",
            "status": "healthy"
        }
        status_code = 200
        
    elif method_request.name == "set_interval":
        new_interval = method_request.payload.get("interval", 10)
        print(f"โฑ๏ธ ์ „์†ก ์ฃผ๊ธฐ ๋ณ€๊ฒฝ: {new_interval}์ดˆ")
        response_payload = {"result": f"interval_set_to_{new_interval}s"}
        status_code = 200
        
    else:
        response_payload = {"error": "unknown_method"}
        status_code = 404
    
    # ์‘๋‹ต ๋ฐ˜ํ™˜
    from azure.iot.device import MethodResponse
    method_response = MethodResponse.create_from_method_request(
        method_request, status_code, response_payload
    )
    await client.send_method_response(method_response)

# ===== ํŠธ์œˆ ํŒจ์น˜ ํ•ธ๋“ค๋Ÿฌ (์›ํ•˜๋Š” ์†์„ฑ ์ˆ˜์‹ ) =====
async def twin_patch_handler(patch):
    """
    Azure IoT ๋””๋ฐ”์ด์Šค ํŠธ์œˆ์˜ desired ์†์„ฑ ๋ณ€๊ฒฝ ์ˆ˜์‹ 
    ํด๋ผ์šฐ๋“œ์—์„œ ๋””๋ฐ”์ด์Šค ์„ค์ •์„ ์›๊ฒฉ์œผ๋กœ ๋ณ€๊ฒฝํ•  ๋•Œ ์‚ฌ์šฉ
    """
    print(f"๐Ÿ”ง ํŠธ์œˆ ํŒจ์น˜ ์ˆ˜์‹ : {patch}")
    
    if "telemetry_interval" in patch:
        print(f"โฑ๏ธ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ์ฃผ๊ธฐ ๋ณ€๊ฒฝ: {patch['telemetry_interval']}์ดˆ")
    
    if "threshold" in patch:
        print(f"๐ŸŒก๏ธ ์ž„๊ณ„๊ฐ’ ๋ณ€๊ฒฝ: {patch['threshold']}")
    
    # reported ์†์„ฑ ์—…๋ฐ์ดํŠธ (์„ค์ • ์ ์šฉ ํ™•์ธ)
    reported_patch = {
        "telemetry_interval": patch.get("telemetry_interval", 10),
        "last_config_update": datetime.utcnow().isoformat() + "Z"
    }
    await client.patch_twin_reported_properties(reported_patch)

# ===== ๋ฉ”์ธ ๋น„๋™๊ธฐ ํ•จ์ˆ˜ =====
async def main():
    global client
    
    # ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ๋ฐ ์—ฐ๊ฒฐ
    client = IoTHubDeviceClient.create_from_connection_string(CONNECTION_STRING)
    
    # ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก
    client.on_method_request_received = method_request_handler
    client.on_twin_desired_properties_patch_received = twin_patch_handler
    
    await client.connect()
    print("โœ… Azure IoT Hub ์—ฐ๊ฒฐ ์™„๋ฃŒ!")
    
    # ์ดˆ๊ธฐ ํŠธ์œˆ reported ์†์„ฑ ์„ค์ •
    await client.patch_twin_reported_properties({
        "firmware_version": "1.2.3",
        "hardware_version": "rev_B",
        "startup_time": datetime.utcnow().isoformat() + "Z",
        "capabilities": ["temperature", "humidity", "pressure"]
    })
    
    try:
        while True:
            # ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            temperature = round(random.uniform(18.0, 35.0), 2)
            humidity = round(random.uniform(30.0, 80.0), 2)
            
            # Azure IoT Hub ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ
            telemetry = {
                "temperature": temperature,
                "humidity": humidity,
                "timestamp": datetime.utcnow().isoformat() + "Z"
            }
            
            message = Message(json.dumps(telemetry))
            
            # ๋ฉ”์‹œ์ง€ ์†์„ฑ ์„ค์ • (๋ผ์šฐํŒ…์— ํ™œ์šฉ)
            message.content_encoding = "utf-8"
            message.content_type = "application/json"
            
            # ์ปค์Šคํ…€ ์†์„ฑ ์ถ”๊ฐ€ (IoT Hub ๋ฉ”์‹œ์ง€ ๋ผ์šฐํŒ… ์กฐ๊ฑด์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
            message.custom_properties["device_type"] = "temperature_sensor"
            message.custom_properties["location"] = "building_A"
            
            # ์˜จ๋„ ์ž„๊ณ„๊ฐ’ ์ดˆ๊ณผ ์‹œ ์•Œ๋žŒ ์†์„ฑ ์ถ”๊ฐ€
            if temperature > 30.0:
                message.custom_properties["alert"] = "high_temperature"
                print(f"๐Ÿšจ ๊ณ ์˜จ ๊ฒฝ๋ณด! {temperature}ยฐC")
            
            # ๋ฉ”์‹œ์ง€ ์ „์†ก
            await client.send_message(message)
            print(f"๐Ÿ“ค Azure IoT Hub ์ „์†ก: {temperature}ยฐC, {humidity}%")
            
            await asyncio.sleep(10)
            
    except KeyboardInterrupt:
        print("\nโ›” ์ข…๋ฃŒ ์ค‘...")
    finally:
        await client.disconnect()
        print("โœ… ์—ฐ๊ฒฐ ์ข…๋ฃŒ")

# ์‹คํ–‰
asyncio.run(main())

๐Ÿค– Arduino/ESP32๋กœ HTTP REST API ์—…๋กœ๋“œ

์•„๋‘์ด๋…ธ๋‚˜ ESP32 ๊ฐ™์€ ๋งˆ์ดํฌ๋กœ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ง์ ‘ ํด๋ผ์šฐ๋“œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ฆฌ๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ์•„์š”.
์ด๋•Œ๋Š” C++ ๊ธฐ๋ฐ˜์˜ Arduino ์ฝ”๋“œ๋ฅผ ์จ์•ผ ํ•˜์ฃ .
ESP32๋Š” WiFi๊ฐ€ ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด์„œ IoT ํ”„๋กœ์ ํŠธ์—์„œ ์—„์ฒญ ์ธ๊ธฐ ์žˆ์–ด์š” ๐Ÿ˜Ž

๐Ÿ“Ÿ ESP32 + HTTP POST ์†Œ์Šค (Arduino IDE)
/*
 * ESP32 IoT ์„ผ์„œ ๋ฐ์ดํ„ฐ HTTP ์—…๋กœ๋“œ
 * ํ•„์š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ:
 * - WiFi (๋‚ด์žฅ)
 * - HTTPClient (๋‚ด์žฅ)
 * - ArduinoJson (๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋งค๋‹ˆ์ €์—์„œ ์„ค์น˜)
 * - DHT sensor library (์˜จ์Šต๋„ ์„ผ์„œ์šฉ)
 */

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <time.h>

// ===== WiFi ์„ค์ • =====
const char* WIFI_SSID = "your_wifi_ssid";
const char* WIFI_PASSWORD = "your_wifi_password";

// ===== ์„œ๋ฒ„ ์„ค์ • =====
const char* SERVER_URL = "https://api.yourserver.com/v1/sensors/data";
const char* API_KEY = "your_api_key_here";
const char* DEVICE_ID = "esp32_sensor_001";

// ===== ์„ผ์„œ ์„ค์ • =====
#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

// ===== ์ „์†ก ์ฃผ๊ธฐ =====
const unsigned long SEND_INTERVAL = 30000;  // 30์ดˆ
unsigned long lastSendTime = 0;

// ===== WiFi ์—ฐ๊ฒฐ ํ•จ์ˆ˜ =====
void connectWiFi() {
    Serial.print("WiFi ์—ฐ๊ฒฐ ์ค‘: ");
    Serial.println(WIFI_SSID);
    
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 30) {
        delay(1000);
        Serial.print(".");
        attempts++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nโœ… WiFi ์—ฐ๊ฒฐ ์„ฑ๊ณต!");
        Serial.print("IP ์ฃผ์†Œ: ");
        Serial.println(WiFi.localIP());
    } else {
        Serial.println("\nโŒ WiFi ์—ฐ๊ฒฐ ์‹คํŒจ. ์žฌ์‹œ์ž‘...");
        ESP.restart();
    }
}

// ===== NTP ์‹œ๊ฐ„ ๋™๊ธฐํ™” =====
void syncTime() {
    configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov");
    Serial.print("์‹œ๊ฐ„ ๋™๊ธฐํ™” ์ค‘");
    
    struct tm timeinfo;
    while (!getLocalTime(&timeinfo)) {
        Serial.print(".");
        delay(500);
    }
    Serial.println("\nโœ… ์‹œ๊ฐ„ ๋™๊ธฐํ™” ์™„๋ฃŒ");
}

// ===== ISO 8601 ํƒ€์ž„์Šคํƒฌํ”„ ์ƒ์„ฑ =====
String getTimestamp() {
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo)) {
        return "1970-01-01T00:00:00Z";
    }
    
    char timestamp[30];
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
    return String(timestamp);
}

// ===== HTTP POST ๋ฐ์ดํ„ฐ ์ „์†ก =====
bool sendSensorData(float temperature, float humidity) {
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("โš ๏ธ WiFi ์—ฐ๊ฒฐ ๋Š๊น€. ์žฌ์—ฐ๊ฒฐ...");
        connectWiFi();
        return false;
    }
    
    HTTPClient http;
    
    // HTTPS ์—ฐ๊ฒฐ ์„ค์ •
    http.begin(SERVER_URL);
    http.addHeader("Content-Type", "application/json");
    http.addHeader("X-API-Key", API_KEY);
    http.addHeader("X-Device-ID", DEVICE_ID);
    http.setTimeout(10000);  // 10์ดˆ ํƒ€์ž„์•„์›ƒ
    
    // JSON ํŽ˜์ด๋กœ๋“œ ์ƒ์„ฑ
    StaticJsonDocument<512> doc;
    doc["device_id"] = DEVICE_ID;
    doc["timestamp"] = getTimestamp();
    
    JsonObject sensors = doc.createNestedObject("sensors");
    sensors["temperature"] = round(temperature * 100) / 100.0;
    sensors["humidity"] = round(humidity * 100) / 100.0;
    
    JsonObject meta = doc.createNestedObject("metadata");
    meta["wifi_rssi"] = WiFi.RSSI();
    meta["free_heap"] = ESP.getFreeHeap();
    meta["uptime"] = millis() / 1000;
    
    String payload;
    serializeJson(doc, payload);
    
    Serial.println("๐Ÿ“ค ์ „์†ก ์ค‘: " + payload);
    
    // POST ์š”์ฒญ
    int httpCode = http.POST(payload);
    
    if (httpCode > 0) {
        String response = http.getString();
        Serial.printf("โœ… HTTP ์‘๋‹ต: %d\n", httpCode);
        Serial.println("์‘๋‹ต ๋ณธ๋ฌธ: " + response);
        http.end();
        return httpCode == 200 || httpCode == 201;
    } else {
        Serial.printf("โŒ HTTP ์˜ค๋ฅ˜: %s\n", http.errorToString(httpCode).c_str());
        http.end();
        return false;
    }
}

// ===== ์žฌ์‹œ๋„ ๋กœ์ง ํฌํ•จ ์ „์†ก =====
void sendWithRetry(float temperature, float humidity, int maxRetries = 3) {
    for (int i = 0; i < maxRetries; i++) {
        if (sendSensorData(temperature, humidity)) {
            return;  // ์„ฑ๊ณต
        }
        
        Serial.printf("โš ๏ธ ์ „์†ก ์‹คํŒจ. %d/%d ์žฌ์‹œ๋„...\n", i + 1, maxRetries);
        delay(2000 * (i + 1));  // ์ ์ง„์  ๋Œ€๊ธฐ
    }
    
    Serial.println("โŒ ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ. ๋ฐ์ดํ„ฐ ์œ ์‹ค");
    // ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” SPIFFS/LittleFS์— ๋กœ์ปฌ ์ €์žฅ ํ›„ ๋‚˜์ค‘์— ์žฌ์ „์†ก
}

// ===== ๋”ฅ์Šฌ๋ฆฝ ๋ชจ๋“œ (๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ) =====
void enterDeepSleep(int seconds) {
    Serial.printf("๐Ÿ’ค ๋”ฅ์Šฌ๋ฆฝ ์ง„์ž…: %d์ดˆ\n", seconds);
    esp_sleep_enable_timer_wakeup(seconds * 1000000ULL);  // ๋งˆ์ดํฌ๋กœ์ดˆ ๋‹จ์œ„
    esp_deep_sleep_start();
}

// ===== Setup =====
void setup() {
    Serial.begin(115200);
    Serial.println("\n๐Ÿš€ ESP32 IoT ์„ผ์„œ ์‹œ์ž‘!");
    
    dht.begin();
    connectWiFi();
    syncTime();
    
    Serial.println("โœ… ์ดˆ๊ธฐํ™” ์™„๋ฃŒ");
}

// ===== Loop =====
void loop() {
    unsigned long currentTime = millis();
    
    if (currentTime - lastSendTime >= SEND_INTERVAL) {
        // ์„ผ์„œ ์ฝ๊ธฐ
        float humidity = dht.readHumidity();
        float temperature = dht.readTemperature();
        
        // ์„ผ์„œ ์ฝ๊ธฐ ์‹คํŒจ ์ฒดํฌ
        if (isnan(humidity) || isnan(temperature)) {
            Serial.println("โŒ ์„ผ์„œ ์ฝ๊ธฐ ์‹คํŒจ!");
        } else {
            Serial.printf("๐ŸŒก๏ธ ์˜จ๋„: %.2fยฐC, ๐Ÿ’ง ์Šต๋„: %.2f%%\n", temperature, humidity);
            sendWithRetry(temperature, humidity);
        }
        
        lastSendTime = currentTime;
    }
    
    // ๋ฐฐํ„ฐ๋ฆฌ ๊ตฌ๋™ ์‹œ ๋”ฅ์Šฌ๋ฆฝ ์‚ฌ์šฉ (์•„๋ž˜ ์ฃผ์„ ํ•ด์ œ)
    // sendWithRetry(temperature, humidity);
    // enterDeepSleep(30);  // 30์ดˆ ์Šฌ๋ฆฝ ํ›„ ๊นจ์–ด๋‚จ
}
๐Ÿ“ก ESP32 + MQTT ์†Œ์Šค (๋” ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•)
/*
 * ESP32 MQTT ๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ (PubSubClient ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)
 * ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋งค๋‹ˆ์ €์—์„œ "PubSubClient" ์„ค์น˜ ํ•„์š”
 */

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>

const char* WIFI_SSID = "your_ssid";
const char* WIFI_PASSWORD = "your_password";

// MQTT ๋ธŒ๋กœ์ปค ์„ค์ • (HiveMQ Cloud ์˜ˆ์‹œ)
const char* MQTT_BROKER = "your-cluster.hivemq.cloud";
const int MQTT_PORT = 8883;  // TLS ํฌํŠธ
const char* MQTT_USER = "your_username";
const char* MQTT_PASSWORD = "your_password";
const char* MQTT_CLIENT_ID = "esp32_001";
const char* MQTT_TOPIC = "sensors/esp32_001/data";

// HiveMQ Cloud ๋ฃจํŠธ CA ์ธ์ฆ์„œ
const char* ROOT_CA = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" \
"... (์‹ค์ œ ์ธ์ฆ์„œ ๋‚ด์šฉ) ...\n" \
"-----END CERTIFICATE-----\n";

WiFiClientSecure espClient;
PubSubClient mqttClient(espClient);

// MQTT ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์ฝœ๋ฐฑ (๊ตฌ๋… ์‹œ ์‚ฌ์šฉ)
void mqttCallback(char* topic, byte* payload, unsigned int length) {
    String message = "";
    for (int i = 0; i < length; i++) {
        message += (char)payload[i];
    }
    
    Serial.println("๐Ÿ“ฉ ์ˆ˜์‹  ํ† ํ”ฝ: " + String(topic));
    Serial.println("   ๋ฉ”์‹œ์ง€: " + message);
    
    // JSON ํŒŒ์‹ฑ
    StaticJsonDocument<256> doc;
    DeserializationError error = deserializeJson(doc, message);
    
    if (!error) {
        if (doc.containsKey("command")) {
            String cmd = doc["command"].as<String>();
            if (cmd == "reboot") {
                Serial.println("๐Ÿ”„ ์žฌ๋ถ€ํŒ… ๋ช…๋ น!");
                delay(1000);
                ESP.restart();
            }
        }
    }
}

void reconnectMQTT() {
    int retries = 0;
    while (!mqttClient.connected() && retries < 5) {
        Serial.print("MQTT ์—ฐ๊ฒฐ ์‹œ๋„...");
        
        if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASSWORD)) {
            Serial.println("โœ… ์—ฐ๊ฒฐ ์„ฑ๊ณต!");
            
            // ๋ช…๋ น ์ˆ˜์‹ ์„ ์œ„ํ•œ ๊ตฌ๋…
            String cmdTopic = "sensors/" + String(MQTT_CLIENT_ID) + "/commands";
            mqttClient.subscribe(cmdTopic.c_str());
            
        } else {
            Serial.printf("โŒ ์‹คํŒจ. ์ƒํƒœ: %d. 5์ดˆ ํ›„ ์žฌ์‹œ๋„...\n", mqttClient.state());
            delay(5000);
            retries++;
        }
    }
}

void setup() {
    Serial.begin(115200);
    
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nโœ… WiFi ์—ฐ๊ฒฐ!");
    
    // TLS ์„ค์ •
    espClient.setCACert(ROOT_CA);
    
    mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
    mqttClient.setCallback(mqttCallback);
    mqttClient.setBufferSize(1024);  // ๋ฒ„ํผ ํฌ๊ธฐ ์ฆ๊ฐ€
    
    reconnectMQTT();
}

void loop() {
    if (!mqttClient.connected()) {
        reconnectMQTT();
    }
    mqttClient.loop();
    
    static unsigned long lastMsg = 0;
    if (millis() - lastMsg > 10000) {
        lastMsg = millis();
        
        // JSON ํŽ˜์ด๋กœ๋“œ
        StaticJsonDocument<256> doc;
        doc["device_id"] = MQTT_CLIENT_ID;
        doc["temperature"] = 25.3;
        doc["humidity"] = 60.5;
        doc["uptime"] = millis() / 1000;
        
        char payload[256];
        serializeJson(doc, payload);
        
        if (mqttClient.publish(MQTT_TOPIC, payload, true)) {
            Serial.println("๐Ÿ“ค MQTT ์ „์†ก ์™„๋ฃŒ: " + String(payload));
        } else {
            Serial.println("โŒ MQTT ์ „์†ก ์‹คํŒจ");
        }
    }
}

๐ŸŸข Node.js๋กœ IoT ๊ฒŒ์ดํŠธ์›จ์ด ์„œ๋ฒ„ ๊ตฌ์ถ•

์—ฌ๋Ÿฌ ์„ผ์„œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ์•„์„œ ํด๋ผ์šฐ๋“œ๋กœ ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ์ดํŠธ์›จ์ด ์„œ๋ฒ„๋ฅผ Node.js๋กœ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”.
Node.js๋Š” ๋น„๋™๊ธฐ I/O๊ฐ€ ๊ฐ•์ ์ด๋ผ ์—ฌ๋Ÿฌ ์„ผ์„œ ์—ฐ๊ฒฐ์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋”ฑ์ด์—์š”!
์‹ค์ œ ํ˜„์—…์—์„œ๋„ ์—ฃ์ง€ ๊ฒŒ์ดํŠธ์›จ์ด๋กœ Node.js ๋งŽ์ด ์จ์š” ๐Ÿ˜Ž

/**
 * Node.js IoT ๊ฒŒ์ดํŠธ์›จ์ด ์„œ๋ฒ„
 * ์—ฌ๋Ÿฌ ์„ผ์„œ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ โ†’ ์ฒ˜๋ฆฌ โ†’ ํด๋ผ์šฐ๋“œ ์—…๋กœ๋“œ
 * 
 * npm install mqtt axios winston node-cron
 */

const mqtt = require('mqtt');
const axios = require('axios');
const winston = require('winston');
const cron = require('node-cron');

// ===== ๋กœ๊ฑฐ ์„ค์ • =====
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.colorize(),
        winston.format.printf(({ timestamp, level, message }) => {
            return `[${timestamp}] ${level}: ${message}`;
        })
    ),
    transports: [
        new winston.transports.Console(),
        new winston.transports.File({ filename: 'gateway.log' })
    ]
});

// ===== ์„ค์ • =====
const CONFIG = {
    mqtt: {
        broker: 'mqtt://localhost:1883',
        topics: ['sensors/#'],  // ๋ชจ๋“  ์„ผ์„œ ํ† ํ”ฝ ๊ตฌ๋…
        clientId: 'gateway_server_001'
    },
    cloud: {
        endpoint: 'https://api.yourcloud.com/v1/telemetry',
        apiKey: process.env.CLOUD_API_KEY,
        batchSize: 50,          // ๋ฐฐ์น˜ ํฌ๊ธฐ
        flushInterval: 5000     // 5์ดˆ๋งˆ๋‹ค ๋ฐฐ์น˜ ์ „์†ก
    }
};

// ===== ๋ฐ์ดํ„ฐ ๋ฒ„ํผ (๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์šฉ) =====
class DataBuffer {
    constructor(maxSize, flushInterval, flushCallback) {
        this.buffer = [];
        this.maxSize = maxSize;
        this.flushCallback = flushCallback;
        
        // ์ฃผ๊ธฐ์  ํ”Œ๋Ÿฌ์‹œ
        setInterval(() => {
            if (this.buffer.length > 0) {
                this.flush();
            }
        }, flushInterval);
    }
    
    add(data) {
        this.buffer.push(data);
        
        // ๋ฒ„ํผ ๊ฐ€๋“ ์ฐจ๋ฉด ์ฆ‰์‹œ ํ”Œ๋Ÿฌ์‹œ
        if (this.buffer.length >= this.maxSize) {
            this.flush();
        }
    }
    
    flush() {
        if (this.buffer.length === 0) return;
        
        const batch = [...this.buffer];
        this.buffer = [];
        
        this.flushCallback(batch).catch(err => {
            logger.error(`๋ฐฐ์น˜ ์ „์†ก ์‹คํŒจ: ${err.message}`);
            // ์‹คํŒจํ•œ ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ๋ฒ„ํผ์— ์ถ”๊ฐ€ (์žฌ์‹œ๋„)
            this.buffer.unshift(...batch);
        });
    }
}

// ===== ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ์ •์ œ =====
function validateAndClean(rawData) {
    const required = ['device_id', 'timestamp'];
    
    for (const field of required) {
        if (!rawData[field]) {
            throw new Error(`ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ: ${field}`);
        }
    }
    
    // ์˜จ๋„ ๋ฒ”์œ„ ๊ฒ€์ฆ (-50ยฐC ~ 100ยฐC)
    if (rawData.temperature !== undefined) {
        if (rawData.temperature < -50 || rawData.temperature > 100) {
            throw new Error(`์˜จ๋„ ๋ฒ”์œ„ ์ดˆ๊ณผ: ${rawData.temperature}`);
        }
    }
    
    // ์Šต๋„ ๋ฒ”์œ„ ๊ฒ€์ฆ (0% ~ 100%)
    if (rawData.humidity !== undefined) {
        if (rawData.humidity < 0 || rawData.humidity > 100) {
            throw new Error(`์Šต๋„ ๋ฒ”์œ„ ์ดˆ๊ณผ: ${rawData.humidity}`);
        }
    }
    
    // ํƒ€์ž„์Šคํƒฌํ”„ ์ •๊ทœํ™”
    const ts = new Date(rawData.timestamp);
    if (isNaN(ts.getTime())) {
        rawData.timestamp = new Date().toISOString();
    }
    
    // ๊ฒŒ์ดํŠธ์›จ์ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
    rawData.gateway_id = CONFIG.mqtt.clientId;
    rawData.received_at = new Date().toISOString();
    
    return rawData;
}

// ===== ํด๋ผ์šฐ๋“œ ๋ฐฐ์น˜ ์—…๋กœ๋“œ =====
async function uploadBatch(batch) {
    const payload = {
        batch_id: `batch_${Date.now()}`,
        count: batch.length,
        data: batch
    };
    
    const response = await axios.post(CONFIG.cloud.endpoint, payload, {
        headers: {
            'Content-Type': 'application/json',
            'X-API-Key': CONFIG.cloud.apiKey,
            'X-Gateway-ID': CONFIG.mqtt.clientId
        },
        timeout: 30000  // 30์ดˆ ํƒ€์ž„์•„์›ƒ
    });
    
    logger.info(`โœ… ๋ฐฐ์น˜ ์—…๋กœ๋“œ ์™„๋ฃŒ: ${batch.length}๊ฐœ โ†’ HTTP ${response.status}`);
    return response.data;
}

// ===== ์žฌ์‹œ๋„ ๋ž˜ํผ =====
async function withRetry(fn, maxRetries = 3, delay = 1000) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            return await fn();
        } catch (error) {
            if (i === maxRetries - 1) throw error;
            logger.warn(`์žฌ์‹œ๋„ ${i + 1}/${maxRetries}: ${error.message}`);
            await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
        }
    }
}

// ===== ํ†ต๊ณ„ ์ˆ˜์ง‘ =====
const stats = {
    received: 0,
    processed: 0,
    failed: 0,
    uploaded: 0
};

// 1๋ถ„๋งˆ๋‹ค ํ†ต๊ณ„ ์ถœ๋ ฅ
cron.schedule('* * * * *', () => {
    logger.info(`๐Ÿ“Š ํ†ต๊ณ„ | ์ˆ˜์‹ : ${stats.received} | ์ฒ˜๋ฆฌ: ${stats.processed} | ์‹คํŒจ: ${stats.failed} | ์—…๋กœ๋“œ: ${stats.uploaded}`);
});

// ===== ๋ฉ”์ธ ๊ฒŒ์ดํŠธ์›จ์ด =====
const dataBuffer = new DataBuffer(
    CONFIG.cloud.batchSize,
    CONFIG.cloud.flushInterval,
    (batch) => withRetry(() => uploadBatch(batch))
);

// MQTT ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ
const mqttClient = mqtt.connect(CONFIG.mqtt.broker, {
    clientId: CONFIG.mqtt.clientId,
    clean: false,
    reconnectPeriod: 5000,
    connectTimeout: 30000
});

mqttClient.on('connect', () => {
    logger.info('โœ… MQTT ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ์™„๋ฃŒ');
    
    // ๋ชจ๋“  ์„ผ์„œ ํ† ํ”ฝ ๊ตฌ๋…
    CONFIG.mqtt.topics.forEach(topic => {
        mqttClient.subscribe(topic, { qos: 1 }, (err) => {
            if (!err) logger.info(`๐Ÿ“ก ๊ตฌ๋… ์‹œ์ž‘: ${topic}`);
        });
    });
});

mqttClient.on('message', (topic, message) => {
    stats.received++;
    
    try {
        const rawData = JSON.parse(message.toString());
        const cleanData = validateAndClean(rawData);
        
        dataBuffer.add(cleanData);
        stats.processed++;
        
        logger.debug(`๐Ÿ“ฅ ์ˆ˜์‹ : ${topic} โ†’ ${cleanData.device_id}`);
        
    } catch (error) {
        stats.failed++;
        logger.error(`โŒ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹คํŒจ [${topic}]: ${error.message}`);
    }
});

mqttClient.on('error', (error) => {
    logger.error(`MQTT ์˜ค๋ฅ˜: ${error.message}`);
});

mqttClient.on('reconnect', () => {
    logger.warn('๐Ÿ”„ MQTT ์žฌ์—ฐ๊ฒฐ ์ค‘...');
});

logger.info('๐Ÿš€ IoT ๊ฒŒ์ดํŠธ์›จ์ด ์„œ๋ฒ„ ์‹œ์ž‘!');

๐Ÿ” ๋ณด์•ˆ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

IoT ๋ณด์•ˆ์€ ์ง„์งœ ์ค‘์š”ํ•œ๋ฐ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์†Œํ™€ํžˆ ํ•˜๋Š” ๋ถ€๋ถ„์ด์—์š”.
2016๋…„ ๋ฏธ๋ผ์ด ๋ด‡๋„ท ์‚ฌ๊ฑด ๊ธฐ์–ตํ•˜์„ธ์š”? ์ˆ˜์‹ญ๋งŒ ๊ฐœ์˜ IoT ๊ธฐ๊ธฐ๊ฐ€ ํ•ดํ‚น๋‹นํ•ด์„œ
๋Œ€๊ทœ๋ชจ DDoS ๊ณต๊ฒฉ์— ์‚ฌ์šฉ๋์ž–์•„์š”. ๊ทธ ํ”ผํ•ด๊ฐ€ ์—„์ฒญ๋‚ฌ์ฃ  ๐Ÿ˜ฑ

๋ณด์•ˆ์€ ๋‚˜์ค‘์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์ฒ˜์Œ๋ถ€ํ„ฐ ์„ค๊ณ„์— ํฌํ•จํ•ด์•ผ ํ•ด์š”!

๐Ÿ” IoT ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๐Ÿ”‘ ์ธ์ฆ & ๊ถŒํ•œ โœ“ X.509 ์ธ์ฆ์„œ ๊ธฐ๋ฐ˜ ๋””๋ฐ”์ด์Šค ์ธ์ฆ โœ“ ์ตœ์†Œ ๊ถŒํ•œ ์›์น™ (Least Privilege) โœ“ ๋””๋ฐ”์ด์Šค๋ณ„ ๊ณ ์œ  ์ž๊ฒฉ์ฆ๋ช… ์‚ฌ์šฉ โœ“ API ํ‚ค ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ (ํ•˜๋“œ์ฝ”๋”ฉ ๊ธˆ์ง€) โœ“ ์ •๊ธฐ์  ์ธ์ฆ์„œ ๊ฐฑ์‹  (์ž๋™ํ™” ๊ถŒ์žฅ) ๐Ÿ”’ ์•”ํ˜ธํ™” & ์ „์†ก ๋ณด์•ˆ โœ“ TLS 1.2 ์ด์ƒ ํ•„์ˆ˜ ์‚ฌ์šฉ โœ“ ํŽ˜์ด๋กœ๋“œ ์•”ํ˜ธํ™” (๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ) โœ“ ์ธ์ฆ์„œ ํ•€๋‹ (์ค‘๊ฐ„์ž ๊ณต๊ฒฉ ๋ฐฉ์ง€) โœ“ MQTT ๋ธŒ๋กœ์ปค TLS ํฌํŠธ 8883 ์‚ฌ์šฉ โœ“ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ (HMAC) ๐ŸŒ ๋„คํŠธ์›Œํฌ ๋ณด์•ˆ โœ“ ๋ฐฉํ™”๋ฒฝ์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ํฌํŠธ ์ฐจ๋‹จ โœ“ VPN/์ „์šฉ๋ง ์‚ฌ์šฉ (๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ) โœ“ ์†๋„ ์ œํ•œ (Rate Limiting) ์„ค์ • โœ“ ์ด์ƒ ํŠธ๋ž˜ํ”ฝ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ โš™๏ธ ์šด์˜ ๋ณด์•ˆ โœ“ ํŽŒ์›จ์–ด OTA ์—…๋ฐ์ดํŠธ ์„œ๋ช… ๊ฒ€์ฆ โœ“ ๋ณด์•ˆ ๋กœ๊ทธ ์ค‘์•™ ์ˆ˜์ง‘ ๋ฐ ๋ถ„์„ โœ“ ์ทจ์•ฝํ•œ ๋””๋ฐ”์ด์Šค ์›๊ฒฉ ๊ฒฉ๋ฆฌ ๊ธฐ๋Šฅ โœ“ ์ •๊ธฐ ๋ณด์•ˆ ๊ฐ์‚ฌ ๋ฐ ์นจํˆฌ ํ…Œ์ŠคํŠธ
๐Ÿ”‘ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๋ฏผ๊ฐ ์ •๋ณด ๊ด€๋ฆฌํ•˜๊ธฐ
"""
๋ฏผ๊ฐ ์ •๋ณด๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์ ˆ๋Œ€ ์•ˆ ๋ฉ๋‹ˆ๋‹ค!
.env ํŒŒ์ผ๊ณผ python-dotenv๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
"""

# .env ํŒŒ์ผ (์ ˆ๋Œ€ Git์— ์ปค๋ฐ‹ํ•˜์ง€ ๋งˆ์„ธ์š”!)
# .gitignore์— .env ์ถ”๊ฐ€ ํ•„์ˆ˜!
"""
AWS_IOT_ENDPOINT=xxxxx-ats.iot.ap-northeast-2.amazonaws.com
AWS_IOT_CLIENT_ID=MySensor001
MQTT_USERNAME=your_username
MQTT_PASSWORD=your_secure_password
FIREBASE_PROJECT_ID=your-project-id
AZURE_CONNECTION_STRING=HostName=...
"""

# Python ์ฝ”๋“œ์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ
from dotenv import load_dotenv
import os

load_dotenv()  # .env ํŒŒ์ผ ๋กœ๋“œ

AWS_ENDPOINT = os.getenv('AWS_IOT_ENDPOINT')
CLIENT_ID = os.getenv('AWS_IOT_CLIENT_ID')
MQTT_USER = os.getenv('MQTT_USERNAME')
MQTT_PASS = os.getenv('MQTT_PASSWORD')

# ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ˆ„๋ฝ ์ฒดํฌ
required_vars = ['AWS_IOT_ENDPOINT', 'AWS_IOT_CLIENT_ID', 'MQTT_USERNAME', 'MQTT_PASSWORD']
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
    raise EnvironmentError(f"ํ•„์ˆ˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ˆ„๋ฝ: {', '.join(missing)}")

print("โœ… ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋กœ๋“œ ์™„๋ฃŒ")

# ===== HMAC ์„œ๋ช…์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ =====
import hmac
import hashlib
import base64

SECRET_KEY = os.getenv('HMAC_SECRET_KEY', 'default_secret').encode()

def sign_payload(payload: str) -> str:
    """ํŽ˜์ด๋กœ๋“œ์— HMAC-SHA256 ์„œ๋ช… ์ถ”๊ฐ€"""
    signature = hmac.new(SECRET_KEY, payload.encode(), hashlib.sha256).digest()
    return base64.b64encode(signature).decode()

def verify_payload(payload: str, signature: str) -> bool:
    """์„œ๋ช… ๊ฒ€์ฆ"""
    expected = sign_payload(payload)
    return hmac.compare_digest(expected, signature)

# ์‚ฌ์šฉ ์˜ˆ์‹œ
import json
data = {"temperature": 25.3, "timestamp": "2024-01-01T00:00:00Z"}
payload_str = json.dumps(data, sort_keys=True)
sig = sign_payload(payload_str)

# ์ „์†ก ์‹œ ์„œ๋ช… ํฌํ•จ
message = {
    "payload": data,
    "signature": sig
}

# ์ˆ˜์‹  ์ธก์—์„œ ๊ฒ€์ฆ
is_valid = verify_payload(payload_str, sig)
print(f"์„œ๋ช… ๊ฒ€์ฆ: {'โœ… ์œ ํšจ' if is_valid else 'โŒ ์œ„์กฐ ๊ฐ์ง€!'}")

๐Ÿ“Š ๋ฐ์ดํ„ฐ ์ตœ์ ํ™” ๋ฐ ์••์ถ• ๊ธฐ๋ฒ•

IoT ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ์ดํ„ฐ ํฌ๊ธฐ๋ฅผ ์ค„์ด๋Š” ๊ฒƒ์ด ๋ฐฐํ„ฐ๋ฆฌ ์ˆ˜๋ช…๊ณผ ํ†ต์‹  ๋น„์šฉ์— ์ง๊ฒฐ๋ผ์š”.
JSON์€ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์ข‹์ง€๋งŒ ์šฉ๋Ÿ‰์ด ํฌ๊ฑฐ๋“ ์š”. ์‹ค์ œ ํ˜„์—…์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์จ์š”.

ํฌ๋งท ํŠน์ง• ํฌ๊ธฐ ๋น„๊ต ์ ํ•ฉํ•œ ์ƒํ™ฉ
JSON ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์›€, ๋ฒ”์šฉ 100% (๊ธฐ์ค€) ๋””๋ฒ„๊น…, ์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ
MessagePack JSON์˜ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฒ„์ „ ~50~70% ์ค‘๊ฐ„ ๊ทœ๋ชจ, ๋น ๋ฅธ ์ง๋ ฌํ™”
Protocol Buffers ๊ตฌ๊ธ€ ๊ฐœ๋ฐœ, ์Šคํ‚ค๋งˆ ๊ธฐ๋ฐ˜ ~30~50% ๋Œ€๊ทœ๋ชจ, ์—„๊ฒฉํ•œ ์Šคํ‚ค๋งˆ
CBOR RFC 7049, IoT ํ‘œ์ค€ ~40~60% CoAP๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ
gzip ์••์ถ• ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ์— ํšจ๊ณผ์  ~20~40% ๋ฐฐ์น˜ ์ „์†ก, HTTP
"""
MessagePack์œผ๋กœ IoT ๋ฐ์ดํ„ฐ ์••์ถ• ์ „์†ก
pip install msgpack
"""

import msgpack
import json
import gzip
import time

# ์ƒ˜ํ”Œ ์„ผ์„œ ๋ฐ์ดํ„ฐ
sensor_data = {
    "device_id": "sensor_001",
    "timestamp": "2024-01-15T09:30:00Z",
    "temperature": 25.34,
    "humidity": 60.12,
    "pressure": 1013.25,
    "co2": 450,
    "lux": 320.5,
    "battery": 3.85,
    "rssi": -65
}

# ===== ํฌ๊ธฐ ๋น„๊ต =====
# 1. JSON (์ผ๋ฐ˜)
json_bytes = json.dumps(sensor_data).encode('utf-8')
print(f"JSON: {len(json_bytes)} bytes")

# 2. JSON + gzip
json_gzip = gzip.compress(json_bytes, compresslevel=6)
print(f"JSON + gzip: {len(json_gzip)} bytes ({len(json_gzip)/len(json_bytes)*100:.1f}%)")

# 3. MessagePack
msgpack_bytes = msgpack.packb(sensor_data, use_bin_type=True)
print(f"MessagePack: {len(msgpack_bytes)} bytes ({len(msgpack_bytes)/len(json_bytes)*100:.1f}%)")

# 4. MessagePack + gzip
msgpack_gzip = gzip.compress(msgpack_bytes, compresslevel=6)
print(f"MessagePack + gzip: {len(msgpack_gzip)} bytes ({len(msgpack_gzip)/len(json_bytes)*100:.1f}%)")

# ===== ๋ฐฐ์น˜ ๋ฐ์ดํ„ฐ ์••์ถ• ์ „์†ก =====
def compress_batch(data_list: list) -> bytes:
    """๋ฐฐ์น˜ ๋ฐ์ดํ„ฐ๋ฅผ MessagePack + gzip์œผ๋กœ ์••์ถ•"""
    packed = msgpack.packb(data_list, use_bin_type=True)
    compressed = gzip.compress(packed, compresslevel=6)
    return compressed

def decompress_batch(compressed: bytes) -> list:
    """์••์ถ• ํ•ด์ œ"""
    decompressed = gzip.decompress(compressed)
    return msgpack.unpackb(decompressed, raw=False)

# 100๊ฐœ ๋ฐ์ดํ„ฐ ๋ฐฐ์น˜ ์••์ถ• ํ…Œ์ŠคํŠธ
batch = [sensor_data.copy() for _ in range(100)]
compressed = compress_batch(batch)
original_size = len(json.dumps(batch).encode())
print(f"\n๋ฐฐ์น˜ 100๊ฐœ:")
print(f"  ์›๋ณธ JSON: {original_size:,} bytes")
print(f"  ์••์ถ• ํ›„: {len(compressed):,} bytes ({len(compressed)/original_size*100:.1f}%)")

# ===== ๋ธํƒ€ ์ธ์ฝ”๋”ฉ (๋ณ€ํ™”๋Ÿ‰๋งŒ ์ „์†ก) =====
class DeltaEncoder:
    """
    ์ด์ „ ๊ฐ’๊ณผ์˜ ์ฐจ์ด(๋ธํƒ€)๋งŒ ์ „์†กํ•˜๋Š” ์ธ์ฝ”๋”
    ์„ผ์„œ ๊ฐ’์ด ์ฒœ์ฒœํžˆ ๋ณ€ํ•  ๋•Œ ๋งค์šฐ ํšจ๊ณผ์ 
    """
    def __init__(self, threshold=0.1):
        self.last_values = {}
        self.threshold = threshold  # ๋ณ€ํ™” ์ž„๊ณ„๊ฐ’
    
    def encode(self, device_id: str, data: dict) -> dict:
        """๋ณ€ํ™”๊ฐ€ ์žˆ๋Š” ํ•„๋“œ๋งŒ ๋ฐ˜ํ™˜"""
        if device_id not in self.last_values:
            self.last_values[device_id] = data.copy()
            return data  # ์ฒซ ๋ฒˆ์งธ๋Š” ์ „์ฒด ์ „์†ก
        
        delta = {
            "device_id": device_id,
            "timestamp": data.get("timestamp"),
            "type": "delta"
        }
        
        changed = False
        for key, value in data.items():
            if key in ("device_id", "timestamp"):
                continue
            
            last_val = self.last_values[device_id].get(key)
            if last_val is None or abs(value - last_val) > self.threshold:
                delta[key] = value
                self.last_values[device_id][key] = value
                changed = True
        
        if not changed:
            return None  # ๋ณ€ํ™” ์—†์œผ๋ฉด ์ „์†ก ์•ˆ ํ•จ
        
        return delta

# ์‚ฌ์šฉ ์˜ˆ์‹œ
encoder = DeltaEncoder(threshold=0.5)

readings = [
    {"device_id": "s001", "timestamp": "T1", "temperature": 25.0, "humidity": 60.0},
    {"device_id": "s001", "timestamp": "T2", "temperature": 25.1, "humidity": 60.0},  # ๋ณ€ํ™” ์—†์Œ
    {"device_id": "s001", "timestamp": "T3", "temperature": 25.8, "humidity": 61.2},  # ์˜จ๋„ ๋ณ€ํ™”
    {"device_id": "s001", "timestamp": "T4", "temperature": 25.9, "humidity": 61.3},  # ๋ฏธ์„ธ ๋ณ€ํ™”
]

for reading in readings:
    delta = encoder.encode("s001", reading)
    if delta:
        print(f"๐Ÿ“ค ์ „์†ก: {delta}")
    else:
        print(f"โญ๏ธ ์Šคํ‚ต (๋ณ€ํ™” ์—†์Œ): {reading['timestamp']}")

๐Ÿ—๏ธ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์Šค๋งˆํŠธ ํ™ˆ ์„ผ์„œ ์‹œ์Šคํ…œ ์™„์„ฑ ์†Œ์Šค

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์™„์„ฑ๋œ ์†Œ์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”!
๋ผ์ฆˆ๋ฒ ๋ฆฌํŒŒ์ด + ๋‹ค์ค‘ ์„ผ์„œ + AWS IoT Core + DynamoDB ์กฐํ•ฉ์ด์—์š”.
์ด ์ •๋„๋ฉด ์‹ค์ œ ์Šค๋งˆํŠธํ™ˆ ํ”„๋กœ์ ํŠธ์— ๋ฐ”๋กœ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์ด์—์š” ๐Ÿ˜Ž

์ฐธ๊ณ ๋กœ ์ด๋Ÿฐ IoT ํ”„๋กœ์ ํŠธ ๊ฐœ๋ฐœ ์†Œ์Šค๋‚˜ ์ปค์Šคํ…€ ๊ตฌํ˜„์ด ํ•„์š”ํ•˜๋‹ค๋ฉด
์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ IoT/์ž„๋ฒ ๋””๋“œ ์ „๋ฌธ ๊ฐœ๋ฐœ์ž๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์–ด์š”!

"""
์Šค๋งˆํŠธํ™ˆ IoT ์„ผ์„œ ์‹œ์Šคํ…œ - ์™„์„ฑ ์†Œ์Šค
ํŒŒ์ผ ๊ตฌ์กฐ:
  smart_home/
  โ”œโ”€โ”€ config.py          # ์„ค์ • ๊ด€๋ฆฌ
  โ”œโ”€โ”€ sensors.py         # ์„ผ์„œ ์ถ”์ƒํ™” ๋ ˆ์ด์–ด
  โ”œโ”€โ”€ uploader.py        # ํด๋ผ์šฐ๋“œ ์—…๋กœ๋“œ ๋ชจ๋“ˆ
  โ”œโ”€โ”€ local_storage.py   # ๋กœ์ปฌ ์ž„์‹œ ์ €์žฅ
  โ””โ”€โ”€ main.py            # ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ
"""

# ===== config.py =====
import os
from dataclasses import dataclass
from dotenv import load_dotenv

load_dotenv()

@dataclass
class Config:
    # AWS IoT
    aws_endpoint: str = os.getenv('AWS_IOT_ENDPOINT', '')
    aws_client_id: str = os.getenv('AWS_IOT_CLIENT_ID', 'smart_home_001')
    aws_cert_path: str = os.getenv('AWS_CERT_PATH', './certs/device.cert.pem')
    aws_key_path: str = os.getenv('AWS_KEY_PATH', './certs/device.private.key')
    aws_ca_path: str = os.getenv('AWS_CA_PATH', './certs/AmazonRootCA1.pem')
    
    # ์„ผ์„œ ์„ค์ •
    send_interval: int = int(os.getenv('SEND_INTERVAL', '30'))  # ์ดˆ
    
    # ๋กœ์ปฌ ์ €์žฅ์†Œ
    local_db_path: str = os.getenv('LOCAL_DB_PATH', './data/sensor_cache.db')
    max_local_records: int = int(os.getenv('MAX_LOCAL_RECORDS', '10000'))

config = Config()

# ===== sensors.py =====
import json
from datetime import datetime
from abc import ABC, abstractmethod

class BaseSensor(ABC):
    """์„ผ์„œ ์ถ”์ƒ ๊ธฐ๋ณธ ํด๋ž˜์Šค"""
    
    def __init__(self, sensor_id: str, location: str):
        self.sensor_id = sensor_id
        self.location = location
    
    @abstractmethod
    def read(self) -> dict:
        """์„ผ์„œ ๊ฐ’ ์ฝ๊ธฐ (์„œ๋ธŒํด๋ž˜์Šค์—์„œ ๊ตฌํ˜„)"""
        pass
    
    def get_reading(self) -> dict:
        """ํ‘œ์ค€ํ™”๋œ ์„ผ์„œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜"""
        raw = self.read()
        return {
            "sensor_id": self.sensor_id,
            "location": self.location,
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "data": raw,
            "metadata": {
                "sensor_type": self.__class__.__name__,
                "firmware": "1.0.0"
            }
        }

class DHT22Sensor(BaseSensor):
    """DHT22 ์˜จ์Šต๋„ ์„ผ์„œ"""
    
    def __init__(self, sensor_id: str, location: str, pin: int):
        super().__init__(sensor_id, location)
        self.pin = pin
        # ์‹ค์ œ ํ™˜๊ฒฝ: import Adafruit_DHT
        # self.dht = Adafruit_DHT.DHT22
    
    def read(self) -> dict:
        # ์‹ค์ œ ํ™˜๊ฒฝ:
        # humidity, temperature = Adafruit_DHT.read_retry(self.dht, self.pin)
        import random
        return {
            "temperature": round(random.uniform(18.0, 35.0), 2),
            "humidity": round(random.uniform(30.0, 80.0), 2),
            "unit": {"temperature": "celsius", "humidity": "percent"}
        }

class MQ135Sensor(BaseSensor):
    """MQ-135 ๊ณต๊ธฐ์งˆ ์„ผ์„œ (CO2, VOC)"""
    
    def __init__(self, sensor_id: str, location: str, adc_channel: int):
        super().__init__(sensor_id, location)
        self.adc_channel = adc_channel
    
    def read(self) -> dict:
        import random
        return {
            "co2_ppm": round(random.uniform(400, 1500), 0),
            "air_quality_index": random.randint(0, 500),
            "status": "good" if random.random() > 0.3 else "moderate"
        }

class SensorManager:
    """๋‹ค์ค‘ ์„ผ์„œ ๊ด€๋ฆฌ์ž"""
    
    def __init__(self):
        self.sensors = []
    
    def add_sensor(self, sensor: BaseSensor):
        self.sensors.append(sensor)
        return self
    
    def read_all(self) -> list:
        """๋ชจ๋“  ์„ผ์„œ ๋™์‹œ ์ฝ๊ธฐ"""
        import concurrent.futures
        
        readings = []
        with concurrent.futures.ThreadPoolExecutor(max_workers=len(self.sensors)) as executor:
            futures = {executor.submit(sensor.get_reading): sensor for sensor in self.sensors}
            
            for future in concurrent.futures.as_completed(futures):
                try:
                    reading = future.result(timeout=5)
                    readings.append(reading)
                except Exception as e:
                    sensor = futures[future]
                    print(f"โŒ ์„ผ์„œ ์ฝ๊ธฐ ์‹คํŒจ [{sensor.sensor_id}]: {e}")
        
        return readings

# ===== local_storage.py =====
import sqlite3
import json
from datetime import datetime

class LocalStorage:
    """์˜คํ”„๋ผ์ธ ์‹œ ๋กœ์ปฌ SQLite ์ž„์‹œ ์ €์žฅ์†Œ"""
    
    def __init__(self, db_path: str):
        self.db_path = db_path
        self._init_db()
    
    def _init_db(self):
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS sensor_cache (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    data TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    uploaded INTEGER DEFAULT 0
                )
            """)
            conn.execute("CREATE INDEX IF NOT EXISTS idx_uploaded ON sensor_cache(uploaded)")
    
    def save(self, readings: list):
        """์„ผ์„œ ๋ฐ์ดํ„ฐ ๋กœ์ปฌ ์ €์žฅ"""
        with sqlite3.connect(self.db_path) as conn:
            for reading in readings:
                conn.execute(
                    "INSERT INTO sensor_cache (data, created_at) VALUES (?, ?)",
                    (json.dumps(reading), datetime.utcnow().isoformat())
                )
        print(f"๐Ÿ’พ ๋กœ์ปฌ ์ €์žฅ: {len(readings)}๊ฐœ")
    
    def get_pending(self, limit: int = 100) -> list:
        """์—…๋กœ๋“œ ๋Œ€๊ธฐ ์ค‘์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ"""
        with sqlite3.connect(self.db_path) as conn:
            rows = conn.execute(
                "SELECT id, data FROM sensor_cache WHERE uploaded = 0 ORDER BY id LIMIT ?",
                (limit,)
            ).fetchall()
        
        return [(row[0], json.loads(row[1])) for row in rows]
    
    def mark_uploaded(self, ids: list):
        """์—…๋กœ๋“œ ์™„๋ฃŒ ํ‘œ์‹œ"""
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                f"UPDATE sensor_cache SET uploaded = 1 WHERE id IN ({','.join('?' * len(ids))})",
                ids
            )
    
    def cleanup(self, keep_days: int = 7):
        """์˜ค๋ž˜๋œ ์—…๋กœ๋“œ ์™„๋ฃŒ ๋ฐ์ดํ„ฐ ์‚ญ์ œ"""
        with sqlite3.connect(self.db_path) as conn:
            result = conn.execute(
                """DELETE FROM sensor_cache 
                   WHERE uploaded = 1 
                   AND created_at < datetime('now', ?)""",
                (f'-{keep_days} days',)
            )
            print(f"๐Ÿ—‘๏ธ ์ •๋ฆฌ ์™„๋ฃŒ: {result.rowcount}๊ฐœ ์‚ญ์ œ")

# ===== main.py =====
import time
import signal
import threading
from datetime import datetime

class SmartHomeIoT:
    """์Šค๋งˆํŠธํ™ˆ IoT ๋ฉ”์ธ ์‹œ์Šคํ…œ"""
    
    def __init__(self):
        self.running = True
        self.sensor_manager = SensorManager()
        self.local_storage = LocalStorage('./data/cache.db')
        
        # ์„ผ์„œ ๋“ฑ๋ก
        self.sensor_manager\
            .add_sensor(DHT22Sensor("dht22_living", "๊ฑฐ์‹ค", pin=4))\
            .add_sensor(DHT22Sensor("dht22_bedroom", "์นจ์‹ค", pin=17))\
            .add_sensor(MQ135Sensor("mq135_kitchen", "์ฃผ๋ฐฉ", adc_channel=0))
        
        # ์‹œ๊ทธ๋„ ํ•ธ๋“ค๋Ÿฌ (Ctrl+C ์ฒ˜๋ฆฌ)
        signal.signal(signal.SIGINT, self._shutdown)
        signal.signal(signal.SIGTERM, self._shutdown)
        
        print("๐Ÿ  ์Šค๋งˆํŠธํ™ˆ IoT ์‹œ์Šคํ…œ ์‹œ์ž‘!")
    
    def _shutdown(self, signum, frame):
        print("\nโ›” ์ข…๋ฃŒ ์‹ ํ˜ธ ์ˆ˜์‹ . ์•ˆ์ „ํ•˜๊ฒŒ ์ข…๋ฃŒ ์ค‘...")
        self.running = False
    
    def _upload_pending(self):
        """๋กœ์ปฌ์— ์Œ“์ธ ๋ฐ์ดํ„ฐ ์žฌ์ „์†ก (๋ฐฑ๊ทธ๋ผ์šด๋“œ)"""
        while self.running:
            pending = self.local_storage.get_pending(limit=50)
            if pending:
                print(f"๐Ÿ“ค ๋Œ€๊ธฐ ์ค‘์ธ {len(pending)}๊ฐœ ๋ฐ์ดํ„ฐ ์žฌ์ „์†ก ์‹œ๋„...")
                # ์‹ค์ œ ์—…๋กœ๋“œ ๋กœ์ง (AWS IoT ๋“ฑ)
                ids = [item[0] for item in pending]
                self.local_storage.mark_uploaded(ids)
            
            time.sleep(60)  # 1๋ถ„๋งˆ๋‹ค ์ฒดํฌ
    
    def run(self):
        """๋ฉ”์ธ ๋ฃจํ”„"""
        # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์žฌ์ „์†ก ์Šค๋ ˆ๋“œ
        retry_thread = threading.Thread(target=self._upload_pending, daemon=True)
        retry_thread.start()
        
        # ์ •๊ธฐ ์ •๋ฆฌ ์ž‘์—…
        cleanup_counter = 0
        
        while self.running:
            try:
                # ๋ชจ๋“  ์„ผ์„œ ์ฝ๊ธฐ
                readings = self.sensor_manager.read_all()
                print(f"๐Ÿ“Š {len(readings)}๊ฐœ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์™„๋ฃŒ")
                
                # ๋กœ์ปฌ ์ €์žฅ (๋ฐฑ์—…)
                self.local_storage.save(readings)
                
                # ํด๋ผ์šฐ๋“œ ์—…๋กœ๋“œ (์—ฌ๊ธฐ์„œ AWS IoT, Firebase ๋“ฑ ์—ฐ๋™)
                # publisher.publish_batch(readings)
                
                # 7์ผ๋งˆ๋‹ค ์ •๋ฆฌ
                cleanup_counter += 1
                if cleanup_counter >= 20160:  # 30์ดˆ * 20160 = 7์ผ
                    self.local_storage.cleanup(keep_days=7)
                    cleanup_counter = 0
                
                time.sleep(30)  # 30์ดˆ ๋Œ€๊ธฐ
                
            except Exception as e:
                print(f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
                time.sleep(5)
        
        print("โœ… ์‹œ์Šคํ…œ ์ข…๋ฃŒ ์™„๋ฃŒ")

if __name__ == "__main__":
    system = SmartHomeIoT()
    system.run()

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

์ฝ”๋“œ ์งœ๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•˜์ง€๋งŒ, ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ์ตœ์ ํ™”๊ฐ€ ์ง„์งœ ์ค‘์š”ํ•ด์š”.
์„ผ์„œ๊ฐ€ ์ˆ˜์‹ญ ๊ฐœ๋งŒ ๋„˜์–ด๊ฐ€๋„ ๊ด€๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์ง€๊ฑฐ๋“ ์š” ๐Ÿ˜…
๊ทธ๋ž˜์„œ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์ฒด๊ณ„๋ฅผ ๊ฐ–์ถ”๋Š” ๊ฒŒ ๋‚˜์ค‘์— ์—„์ฒญ ํŽธํ•ด์š”!

๐Ÿ“ˆ Prometheus + Grafana๋กœ IoT ๋ชจ๋‹ˆํ„ฐ๋ง
"""
Prometheus ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘์œผ๋กœ IoT ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง
pip install prometheus_client
"""

from prometheus_client import Counter, Gauge, Histogram, start_http_server
import time

# ===== ๋ฉ”ํŠธ๋ฆญ ์ •์˜ =====
# ์ „์†ก ์„ฑ๊ณต/์‹คํŒจ ์นด์šดํ„ฐ
messages_sent_total = Counter(
    'iot_messages_sent_total',
    'Total messages sent to cloud',
    ['device_id', 'status']  # ๋ ˆ์ด๋ธ”
)

# ํ˜„์žฌ ์„ผ์„œ ๊ฐ’ ๊ฒŒ์ด์ง€
sensor_temperature = Gauge(
    'iot_sensor_temperature_celsius',
    'Current temperature reading',
    ['device_id', 'location']
)

sensor_humidity = Gauge(
    'iot_sensor_humidity_percent',
    'Current humidity reading',
    ['device_id', 'location']
)

# ์ „์†ก ์ง€์—ฐ ํžˆ์Šคํ† ๊ทธ๋žจ
upload_duration = Histogram(
    'iot_upload_duration_seconds',
    'Time spent uploading data to cloud',
    ['cloud_provider'],
    buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
)

# ์—ฐ๊ฒฐ ์ƒํƒœ ๊ฒŒ์ด์ง€
connection_status = Gauge(
    'iot_connection_status',
    'MQTT connection status (1=connected, 0=disconnected)',
    ['broker']
)

# ===== ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ ์˜ˆ์‹œ =====
def record_upload(device_id: str, success: bool, duration: float, provider: str):
    """์—…๋กœ๋“œ ๊ฒฐ๊ณผ ๊ธฐ๋ก"""
    status = 'success' if success else 'failure'
    messages_sent_total.labels(device_id=device_id, status=status).inc()
    upload_duration.labels(cloud_provider=provider).observe(duration)

def update_sensor_metrics(device_id: str, location: str, temp: float, hum: float):
    """์„ผ์„œ ๊ฐ’ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ"""
    sensor_temperature.labels(device_id=device_id, location=location).set(temp)
    sensor_humidity.labels(device_id=device_id, location=location).set(hum)

# Prometheus ๋ฉ”ํŠธ๋ฆญ ์„œ๋ฒ„ ์‹œ์ž‘ (ํฌํŠธ 8000)
start_http_server(8000)
print("๐Ÿ“Š Prometheus ๋ฉ”ํŠธ๋ฆญ ์„œ๋ฒ„ ์‹œ์ž‘: http://localhost:8000/metrics")

# ์‹œ๋ฎฌ๋ ˆ์ด์…˜
import random
while True:
    temp = random.uniform(20, 30)
    hum = random.uniform(40, 70)
    
    start = time.time()
    # ์‹ค์ œ ์—…๋กœ๋“œ ๋กœ์ง...
    time.sleep(random.uniform(0.1, 0.5))  # ์—…๋กœ๋“œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    duration = time.time() - start
    
    update_sensor_metrics("sensor_001", "๊ฑฐ์‹ค", temp, hum)
    record_upload("sensor_001", True, duration, "aws_iot")
    connection_status.labels(broker="aws_iot_core").set(1)
    
    print(f"๐Ÿ“Š ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ: {temp:.1f}ยฐC, {hum:.1f}%")
    time.sleep(10)
๐Ÿ’ก Grafana ๋Œ€์‹œ๋ณด๋“œ ์„ค์ • ํŒ Prometheus์—์„œ ์ˆ˜์ง‘ํ•œ ๋ฉ”ํŠธ๋ฆญ์„ Grafana๋กœ ์‹œ๊ฐํ™”ํ•˜๋ฉด ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ๊ฐ€๋Šฅํ•ด์š”.
Docker Compose๋กœ Prometheus + Grafana๋ฅผ ํ•œ ๋ฒˆ์— ์˜ฌ๋ฆฌ๋ฉด ์„ค์ •์ด ํ›จ์”ฌ ํŽธํ•ด์š”!
AWS CloudWatch, Azure Monitor, Google Cloud Monitoring๋„ ๊ฐ ํ”Œ๋žซํผ ๋‚ด์žฅ ๋ชจ๋‹ˆํ„ฐ๋ง์œผ๋กœ ํ™œ์šฉ ๊ฐ€๋Šฅํ•ด์š”.

๐ŸŽฏ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ๋ณ„ ๋น„๊ต ๋ฐ ์„ ํƒ ๊ฐ€์ด๋“œ

์–ด๋–ค ํด๋ผ์šฐ๋“œ๋ฅผ ์„ ํƒํ• ์ง€ ๊ณ ๋ฏผ๋˜๋Š” ๋ถ„๋“ค์„ ์œ„ํ•ด ์ •๋ฆฌํ•ด๋ดค์–ด์š”!
ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ์™€ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์ตœ์ ์˜ ์„ ํƒ์ด ๋‹ฌ๋ผ์ ธ์š” ๐Ÿ˜Š
์ฐธ๊ณ ๋กœ ์ด๋Ÿฐ IoT ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„๋‚˜ ๊ตฌํ˜„์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”!

ํ”Œ๋žซํผ ์žฅ์  ๋‹จ์  ์ถ”์ฒœ ๊ทœ๋ชจ ๋ฌด๋ฃŒ ํ‹ฐ์–ด
AWS IoT Core ๊ฐ€์žฅ ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„, ๋†’์€ ์•ˆ์ •์„ฑ, ๋‹ค์–‘ํ•œ ์—ฐ๋™ ์„œ๋น„์Šค ๋ณต์žกํ•œ ์„ค์ •, ๋น„์šฉ ์˜ˆ์ธก ์–ด๋ ค์›€ ์ค‘~๋Œ€๊ทœ๋ชจ ์›” 250๋งŒ ๋ฉ”์‹œ์ง€
Firebase ๋น ๋ฅธ ์„ค์ •, ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”, ๋ชจ๋ฐ”์ผ ์•ฑ ์—ฐ๋™ ์‰ฌ์›€ ๋Œ€๊ทœ๋ชจ ์ฟผ๋ฆฌ ์ œํ•œ, ๋น„์šฉ ๊ธ‰์ฆ ๊ฐ€๋Šฅ ์†Œ~์ค‘๊ทœ๋ชจ 1GB ์ €์žฅ, 10GB/์›”
Azure IoT Hub ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์นœํ™”์ , MS ์ƒํƒœ๊ณ„ ํ†ตํ•ฉ ๋ณต์žกํ•œ ์„ค์ •, ๋†’์€ ํ•™์Šต ๊ณก์„  ์ค‘~๋Œ€๊ทœ๋ชจ 8,000 ๋ฉ”์‹œ์ง€/์ผ
Google Cloud IoT BigQuery ์—ฐ๋™, ML ํŒŒ์ดํ”„๋ผ์ธ ๊ฐ•์  2023๋…„ ์„œ๋น„์Šค ์ข…๋ฃŒ (์ฃผ์˜!) - -
HiveMQ Cloud ์ˆœ์ˆ˜ MQTT ๋ธŒ๋กœ์ปค, ๊ฐ„๋‹จํ•œ ์„ค์ • ์ €์žฅ/๋ถ„์„ ๊ธฐ๋Šฅ ์—†์Œ ์†Œ~์ค‘๊ทœ๋ชจ 100 ์—ฐ๊ฒฐ, 10GB/์›”
ThingsBoard ์˜คํ”ˆ์†Œ์Šค, ์ž์ฒด ํ˜ธ์ŠคํŒ… ๊ฐ€๋Šฅ ์šด์˜ ๋ถ€๋‹ด, ์ปค๋ฎค๋‹ˆํ‹ฐ ์ง€์› ์†Œ~์ค‘๊ทœ๋ชจ ์˜คํ”ˆ์†Œ์Šค ๋ฌด๋ฃŒ
โš ๏ธ Google Cloud IoT Core ์„œ๋น„์Šค ์ข…๋ฃŒ ์ฃผ์˜! Google Cloud IoT Core๋Š” 2023๋…„ 8์›”์— ์„œ๋น„์Šค๊ฐ€ ์ข…๋ฃŒ๋์–ด์š”.
๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ ๋ถ„๋“ค์€ AWS IoT Core๋‚˜ HiveMQ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ํ•„์š”ํ•ด์š”!
๐ŸŽฏ ์ƒํ™ฉ๋ณ„ ์ถ”์ฒœ ์กฐํ•ฉ
๐Ÿ  ์Šค๋งˆํŠธํ™ˆ / ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ โ†’ Firebase Realtime DB + Flutter/React ์•ฑ
โ†’ ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ดํ•‘, ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„ ์‰ฌ์›€
๐Ÿญ ์‚ฐ์—…์šฉ IoT / ์Šค๋งˆํŠธํŒฉํ† ๋ฆฌ โ†’ AWS IoT Core + DynamoDB + Lambda + QuickSight
โ†’ ๋†’์€ ์•ˆ์ •์„ฑ, ๋Œ€๊ทœ๋ชจ ๋””๋ฐ”์ด์Šค ๊ด€๋ฆฌ, ๋ถ„์„ ํŒŒ์ดํ”„๋ผ์ธ
๐Ÿข ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ / MS ์ธํ”„๋ผ ํ™˜๊ฒฝ โ†’ Azure IoT Hub + Azure Stream Analytics + Power BI
โ†’ ๊ธฐ์กด MS ์ธํ”„๋ผ์™€ ํ†ตํ•ฉ, ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๋ณด์•ˆ ์ •์ฑ… ์ค€์ˆ˜
๐Ÿ’ฐ ๋น„์šฉ ์ ˆ๊ฐ / ์ž์ฒด ํ˜ธ์ŠคํŒ… โ†’ Mosquitto MQTT + InfluxDB + Grafana (์ž์ฒด ์„œ๋ฒ„)
โ†’ ์ดˆ๊ธฐ ๋น„์šฉ ์—†์Œ, ์™„์ „ํ•œ ๋ฐ์ดํ„ฐ ํ†ต์ œ๊ถŒ, ์šด์˜ ๋ถ€๋‹ด ์žˆ์Œ

๐Ÿ”ง ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฐ€์ด๋“œ

IoT ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์ง„์งœ ๋ณ„๋ณ„ ๋ฌธ์ œ๊ฐ€ ๋‹ค ์ƒ๊ฒจ์š”ใ…‹ใ…‹
์ž์ฃผ ๋งŒ๋‚˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด์š”!

๋ฌธ์ œ ์ฆ์ƒ ์›์ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
MQTT ์—ฐ๊ฒฐ ์‹คํŒจ (RC=5) ์ธ์ฆ ์ •๋ณด ์˜ค๋ฅ˜ username/password ๋˜๋Š” ์ธ์ฆ์„œ ํ™•์ธ
TLS ํ•ธ๋“œ์…ฐ์ดํฌ ์‹คํŒจ ์ธ์ฆ์„œ ๋งŒ๋ฃŒ ๋˜๋Š” CA ๋ถˆ์ผ์น˜ ์ธ์ฆ์„œ ๊ฐฑ์‹ , CA ์ธ์ฆ์„œ ์—…๋ฐ์ดํŠธ
๋ฉ”์‹œ์ง€ ์œ ์‹ค (QoS 0) ๋„คํŠธ์›Œํฌ ๋ถˆ์•ˆ์ • QoS 1 ๋˜๋Š” 2๋กœ ๋ณ€๊ฒฝ
๋ฐ์ดํ„ฐ ์ค‘๋ณต ์ˆ˜์‹  QoS 1 ์žฌ์ „์†ก ๋ฉ”์‹œ์ง€ ID๋กœ ์ค‘๋ณต ์ œ๊ฑฐ ๋กœ์ง ์ถ”๊ฐ€
ESP32 ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ JSON ํŽ˜์ด๋กœ๋“œ ๊ณผ๋‹ค MessagePack ์‚ฌ์šฉ, ๋ถˆํ•„์š” ํ•„๋“œ ์ œ๊ฑฐ
๋ฐฐํ„ฐ๋ฆฌ ๋นจ๋ฆฌ ์†Œ๋ชจ WiFi ํ•ญ์ƒ ์ผœ์ง ๋”ฅ์Šฌ๋ฆฝ ๋ชจ๋“œ ํ™œ์šฉ, ์ „์†ก ์ฃผ๊ธฐ ๋Š˜๋ฆฌ๊ธฐ
ํด๋ผ์šฐ๋“œ ๋น„์šฉ ๊ธ‰์ฆ ๋ฐ์ดํ„ฐ ์ „์†ก๋Ÿ‰ ๊ณผ๋‹ค ๋ฐฐ์น˜ ์ „์†ก, ๋ธํƒ€ ์ธ์ฝ”๋”ฉ, ์••์ถ• ์ ์šฉ
ํƒ€์ž„์Šคํƒฌํ”„ ๋ถˆ์ผ์น˜ ๋””๋ฐ”์ด์Šค ์‹œ๊ฐ„ ๋™๊ธฐํ™” ์•ˆ ๋จ NTP ์„œ๋ฒ„ ๋™๊ธฐํ™” ํ•„์ˆ˜
"""
MQTT ์—ฐ๊ฒฐ ๋ฌธ์ œ ๋””๋ฒ„๊น… ๋„๊ตฌ
"""

import paho.mqtt.client as mqtt
import logging

# ์ƒ์„ธ ๋กœ๊น… ํ™œ์„ฑํ™”
logging.basicConfig(level=logging.DEBUG)
mqtt_logger = logging.getLogger('mqtt')
mqtt_logger.setLevel(logging.DEBUG)

def debug_mqtt_connection(broker, port, username=None, password=None):
    """MQTT ์—ฐ๊ฒฐ ๋ฌธ์ œ ์ง„๋‹จ"""
    
    client = mqtt.Client()
    
    if username:
        client.username_pw_set(username, password)
    
    # ๋ชจ๋“  ์ด๋ฒคํŠธ ๋กœ๊น…
    def on_log(client, userdata, level, buf):
        print(f"[MQTT LOG] {buf}")
    
    def on_connect(client, userdata, flags, rc):
        rc_messages = {
            0: "โœ… ์—ฐ๊ฒฐ ์„ฑ๊ณต",
            1: "โŒ ์ž˜๋ชป๋œ ํ”„๋กœํ† ์ฝœ ๋ฒ„์ „",
            2: "โŒ ํด๋ผ์ด์–ธํŠธ ID ๊ฑฐ๋ถ€๋จ",
            3: "โŒ ์„œ๋ฒ„ ์‚ฌ์šฉ ๋ถˆ๊ฐ€",
            4: "โŒ ์ž˜๋ชป๋œ ์‚ฌ์šฉ์ž๋ช… ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ",
            5: "โŒ ๊ถŒํ•œ ์—†์Œ (์ธ์ฆ ์‹คํŒจ)"
        }
        print(f"์—ฐ๊ฒฐ ๊ฒฐ๊ณผ: {rc_messages.get(rc, f'์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜: {rc}')}")
    
    client.on_log = on_log
    client.on_connect = on_connect
    
    print(f"๐Ÿ” ์—ฐ๊ฒฐ ์‹œ๋„: {broker}:{port}")
    
    try:
        client.connect(broker, port, keepalive=10)
        client.loop_start()
        
        import time
        time.sleep(5)  # 5์ดˆ ๋Œ€๊ธฐ
        
        client.loop_stop()
        client.disconnect()
        
    except ConnectionRefusedError:
        print(f"โŒ ์—ฐ๊ฒฐ ๊ฑฐ๋ถ€๋จ. ๋ธŒ๋กœ์ปค ์ฃผ์†Œ/ํฌํŠธ ํ™•์ธ: {broker}:{port}")
    except TimeoutError:
        print(f"โŒ ์—ฐ๊ฒฐ ํƒ€์ž„์•„์›ƒ. ๋ฐฉํ™”๋ฒฝ ๋˜๋Š” ๋„คํŠธ์›Œํฌ ํ™•์ธ")
    except Exception as e:
        print(f"โŒ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜: {e}")

# ์‚ฌ์šฉ
debug_mqtt_connection("broker.example.com", 1883, "user", "pass")

โœ… ๋งˆ๋ฌด๋ฆฌ: IoT ํด๋ผ์šฐ๋“œ ์—…๋กœ๋“œ ํ•ต์‹ฌ ์ •๋ฆฌ

์™€ ์ง„์งœ ๊ธธ์—ˆ์ฃ ?ใ…‹ใ…‹ ๊ทผ๋ฐ ์ด๊ฒŒ ๋‹ค ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๋‚ด์šฉ๋“ค์ด์—์š”!
๋งˆ์ง€๋ง‰์œผ๋กœ ํ•ต์‹ฌ ํฌ์ธํŠธ๋งŒ ์ •๋ฆฌํ• ๊ฒŒ์š” ๐Ÿ˜Š

๐ŸŽฏ ํ•ต์‹ฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ํ”„๋กœํ† ์ฝœ ์„ ํƒ ๋ฐฐํ„ฐ๋ฆฌ ๊ธฐ๊ธฐ โ†’ MQTT, ๋‹จ์ˆœ ์—ฐ๋™ โ†’ HTTP REST
  • โœ… ๋ณด์•ˆ ํ•„์ˆ˜ TLS ์•”ํ˜ธํ™”, X.509 ์ธ์ฆ์„œ, ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ํ‚ค ๊ด€๋ฆฌ
  • โœ… ์žฌ์—ฐ๊ฒฐ ๋กœ์ง ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ๋ฐฉ์‹์œผ๋กœ ์•ˆ์ •์ ์ธ ์žฌ์—ฐ๊ฒฐ ๊ตฌํ˜„
  • โœ… ์˜คํ”„๋ผ์ธ ๋Œ€์‘ ๋กœ์ปฌ SQLite ์ €์žฅ โ†’ ์˜จ๋ผ์ธ ๋ณต๊ตฌ ์‹œ ์žฌ์ „์†ก
  • โœ… ๋ฐ์ดํ„ฐ ์ตœ์ ํ™” MessagePack/gzip ์••์ถ•, ๋ธํƒ€ ์ธ์ฝ”๋”ฉ์œผ๋กœ ์ „์†ก๋Ÿ‰ ์ ˆ๊ฐ
  • โœ… QoS ์„ค์ • ๋ฐ์ดํ„ฐ ์ค‘์š”๋„์— ๋”ฐ๋ผ QoS 0/1/2 ์ ์ ˆํžˆ ์„ ํƒ
  • โœ… ๋ชจ๋‹ˆํ„ฐ๋ง Prometheus + Grafana๋กœ ์‹ค์‹œ๊ฐ„ ์‹œ์Šคํ…œ ์ƒํƒœ ํŒŒ์•…
  • โœ… ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ฐœ๋ณ„ ์ „์†ก๋ณด๋‹ค ๋ฐฐ์น˜ ์ „์†ก์œผ๋กœ ํšจ์œจ์„ฑ ํ–ฅ์ƒ
  • โœ… ํƒ€์ž„์Šคํƒฌํ”„ NTP ๋™๊ธฐํ™” ํ•„์ˆ˜, UTC ๊ธฐ์ค€ ISO 8601 ํ˜•์‹ ์‚ฌ์šฉ
  • โœ… ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ชจ๋“  ์˜ˆ์™ธ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ตฌํ˜„

IoT ๊ฐœ๋ฐœ์€ ์ฒ˜์Œ์—” ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ๊ธฐ๋ณธ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋ฉด
์ƒ๊ฐ๋ณด๋‹ค ์žฌ๋ฏธ์žˆ๋Š” ๋ถ„์•ผ์˜ˆ์š”! ์„ผ์„œ์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ํด๋ผ์šฐ๋“œ๋กœ ์˜ฌ๋ผ๊ฐ€๊ณ ,
๊ทธ๊ฒŒ ๋Œ€์‹œ๋ณด๋“œ์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‘œ์‹œ๋˜๋Š” ๊ฑธ ๋ณด๋ฉด ์ง„์งœ ๋ฟŒ๋“ฏํ•˜๊ฑฐ๋“ ์š” ๐Ÿ˜„

์ด ๊ธ€์—์„œ ๋‹ค๋ฃฌ ์†Œ์Šค ์ฝ”๋“œ๋“ค์€ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ๋ฐ”๋กœ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์œผ๋กœ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋ฌผ๋ก  ์‹ค์ œ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์„ค์ •๊ฐ’๊ณผ ์„ผ์„œ ์ฝ๊ธฐ ๋ถ€๋ถ„์€ ์ˆ˜์ •์ด ํ•„์š”ํ•˜๊ฒ ์ง€๋งŒ,
์ „์ฒด์ ์ธ ๊ตฌ์กฐ์™€ ํŒจํ„ด์€ ๊ทธ๋Œ€๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”!

๊ถ๊ธˆํ•œ ์ ์ด๋‚˜ ๋” ์•Œ๊ณ  ์‹ถ์€ ๋‚ด์šฉ์ด ์žˆ์œผ๋ฉด ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š” ๐Ÿ™Œ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.