{"id":11651,"date":"2026-04-29T14:21:06","date_gmt":"2026-04-29T05:21:06","guid":{"rendered":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=11651"},"modified":"2026-04-29T14:27:13","modified_gmt":"2026-04-29T05:27:13","slug":"fcm%ec%9c%bc%eb%a1%9c-%ec%84%9c%eb%b2%84-%eb%94%94%ec%8a%a4%ed%81%ac-%ec%83%81%ed%83%9c%eb%a5%bc-%ec%95%88%eb%93%9c%eb%a1%9c%ec%9d%b4%eb%93%9c-%ec%95%b1%ec%97%90-%ed%91%b8%ec%8b%9c-%ec%95%8c%eb%a6%bc","status":"publish","type":"post","link":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=11651","title":{"rendered":"FCM\uc73c\ub85c \uc11c\ubc84 \ub514\uc2a4\ud06c \uc0c1\ud0dc\ub97c \uc548\ub4dc\ub85c\uc774\ub4dc \uc571\uc5d0 \ud478\uc2dc \uc54c\ub9bc \ubcf4\ub0b4\uae30"},"content":{"rendered":"<h1>FCM\uc73c\ub85c \uc11c\ubc84 \ub514\uc2a4\ud06c \uc0c1\ud0dc\ub97c \uc548\ub4dc\ub85c\uc774\ub4dc \uc571\uc5d0 \ud478\uc2dc \uc54c\ub9bc \ubcf4\ub0b4\uae30<\/h1>\n<p>\uc11c\ubc84 \ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9\uc774 80%\ub97c \ucd08\uacfc\ud558\uba74 Firebase Cloud Messaging(FCM)\uc744 \ud1b5\ud574 \uc548\ub4dc\ub85c\uc774\ub4dc \uc571\uc5d0 \uc989\uc2dc \ud478\uc2dc \uc54c\ub9bc\uc744 \uc804\uc1a1\ud558\ub294 \ubc29\ubc95\uc744 \ub2e8\uacc4\ubcc4\ub85c \uc124\uba85\ud569\ub2c8\ub2e4.<\/p>\n<h2>1. \uc804\uccb4 \uc544\ud0a4\ud14d\ucc98 \uac1c\uc694<\/h2>\n<pre><code class=\"language-text\">[Linux Server]\n  \u251c\u2500\u2500 cron + Python \uc2a4\ud06c\ub9bd\ud2b8 (\ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9 \uac10\uc2dc)\n  \u2502       \u2502\n  \u2502       \u2502 80% \ucd08\uacfc \uc2dc HTTP POST\n  \u2502       \u25bc\n[Firebase Cloud Messaging (FCM)]\n  \u2502       \u2502\n  \u2502       \u2502 Push Notification (Topic \ub610\ub294 Token \ubc29\uc2dd)\n  \u2502       \u25bc\n[Android App] \u00d7 N\uba85 (5\uba85 \uc774\uc0c1)<\/code><\/pre>\n<p><strong>\ud575\uc2ec \uc804\uc1a1 \ubc29\uc2dd: Topic \uad6c\ub3c5<\/strong><\/p>\n<p>\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 5\uba85 \uc774\uc0c1\uc778 \uacbd\uc6b0, \uac01 \uae30\uae30\uc758 \ud1a0\ud070\uc744 \uc11c\ubc84\uc5d0\uc11c \uc77c\uc77c\uc774 \uad00\ub9ac\ud558\ub294 \ub300\uc2e0 <strong>FCM Topic<\/strong>(<code>\/topics\/disk-alert<\/code>)\uc744 \uc0ac\uc6a9\ud569\ub2c8\ub2e4. \uc571 \uc124\uce58 \uc2dc \ud574\ub2f9 \ud1a0\ud53d\uc744 \uad6c\ub3c5\ud558\uba74, \uc11c\ubc84\ub294 \ud1a0\ud53d \ud558\ub098\uc5d0\ub9cc \uba54\uc2dc\uc9c0\ub97c \ubcf4\ub0b4\uba74 \uad6c\ub3c5\ud55c \ubaa8\ub4e0 \uae30\uae30\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc804\ub2ec\ub429\ub2c8\ub2e4.<\/p>\n<hr \/>\n<h2>2. Firebase \ud504\ub85c\uc81d\ud2b8 \uc124\uc815<\/h2>\n<h3>2-1. Firebase \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131<\/h3>\n<ol>\n<li><a href=\"https:\/\/console.firebase.google.com\/\">Firebase Console<\/a> \uc811\uc18d<\/li>\n<li><strong>\ud504\ub85c\uc81d\ud2b8 \ucd94\uac00<\/strong> \ud074\ub9ad \u2192 \ud504\ub85c\uc81d\ud2b8 \uc774\ub984 \uc785\ub825 (\uc608: <code>server-disk-monitor<\/code>)<\/li>\n<li>Google Analytics\ub294 \uc120\ud0dd \uc0ac\ud56d \u2192 <strong>\ud504\ub85c\uc81d\ud2b8 \ub9cc\ub4e4\uae30<\/strong><\/li>\n<\/ol>\n<h3>2-2. \uc548\ub4dc\ub85c\uc774\ub4dc \uc571 \ub4f1\ub85d<\/h3>\n<ol>\n<li>\ud504\ub85c\uc81d\ud2b8 \ud648 \u2192 <strong>Android \uc544\uc774\ucf58<\/strong> \ud074\ub9ad<\/li>\n<li>\ud328\ud0a4\uc9c0 \uc774\ub984 \uc785\ub825 (\uc608: <code>com.example.diskmonitor<\/code>)<\/li>\n<li><strong>google-services.json<\/strong> \ub2e4\uc6b4\ub85c\ub4dc \u2192 \uc548\ub4dc\ub85c\uc774\ub4dc \ud504\ub85c\uc81d\ud2b8\uc758 <code>app\/<\/code> \ud3f4\ub354\uc5d0 \ubcf5\uc0ac<\/li>\n<\/ol>\n<h3>2-3. \uc11c\ube44\uc2a4 \uacc4\uc815 \ud0a4(Service Account Key) \ubc1c\uae09<\/h3>\n<p>\uc11c\ubc84\uc5d0\uc11c FCM API\ub97c \ud638\ucd9c\ud558\ub824\uba74 \uc11c\ube44\uc2a4 \uacc4\uc815 \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4.<\/p>\n<ol>\n<li>Firebase Console \u2192 <strong>\ud504\ub85c\uc81d\ud2b8 \uc124\uc815<\/strong> \u2192 <strong>\uc11c\ube44\uc2a4 \uacc4\uc815<\/strong> \ud0ed<\/li>\n<li><strong>\uc0c8 \ube44\uacf5\uac1c \ud0a4 \uc0dd\uc131<\/strong> \ud074\ub9ad \u2192 JSON \ud30c\uc77c \ub2e4\uc6b4\ub85c\ub4dc<\/li>\n<li>\uc11c\ubc84\uc758 \uc548\uc804\ud55c \uacbd\ub85c\uc5d0 \uc800\uc7a5 (\uc608: <code>\/etc\/fcm\/service-account.json<\/code>)<\/li>\n<\/ol>\n<pre><code class=\"language-bash\"># \ud30c\uc77c \uad8c\ud55c\uc744 root\ub9cc \uc77d\uc744 \uc218 \uc788\ub3c4\ub85d \uc124\uc815\nsudo chmod 600 \/etc\/fcm\/service-account.json\nsudo chown root:root \/etc\/fcm\/service-account.json<\/code><\/pre>\n<hr \/>\n<h2>3. \uc11c\ubc84 \uc124\uc815 (Python \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud06c\ub9bd\ud2b8)<\/h2>\n<h3>3-1. \ud544\uc694 \ud328\ud0a4\uc9c0 \uc124\uce58<\/h3>\n<pre><code class=\"language-bash\">pip install google-auth requests psutil<\/code><\/pre>\n<h3>3-2. FCM \uba54\uc2dc\uc9c0 \uc804\uc1a1 \uc2a4\ud06c\ub9bd\ud2b8 \uc791\uc131<\/h3>\n<p><code>\/usr\/local\/bin\/disk_monitor.py<\/code> \ub85c \uc800\uc7a5\ud569\ub2c8\ub2e4.<\/p>\n<pre><code class=\"language-python\">#!\/usr\/bin\/env python3\n&quot;&quot;&quot;\n\uc11c\ubc84 \ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9\uc744 \uac10\uc2dc\ud558\uace0 80% \ucd08\uacfc \uc2dc FCM\uc73c\ub85c \ud478\uc2dc \uc54c\ub9bc\uc744 \uc804\uc1a1\ud569\ub2c8\ub2e4.\n&quot;&quot;&quot;\n\nimport psutil\nimport requests\nimport json\nimport os\nimport logging\nfrom google.oauth2 import service_account\nfrom google.auth.transport.requests import Request\n\n# \u2500\u2500 \uc124\uc815 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nSERVICE_ACCOUNT_FILE = &quot;\/etc\/fcm\/service-account.json&quot;\nDISK_THRESHOLD_PERCENT = 80          # \uc54c\ub9bc \ubc1c\uc1a1 \uae30\uc900 (%)\nMONITOR_PATH = &quot;\/&quot;                   # \uac10\uc2dc\ud560 \ub9c8\uc6b4\ud2b8 \ud3ec\uc778\ud2b8\nFCM_TOPIC = &quot;disk-alert&quot;             # \uad6c\ub3c5 \ud1a0\ud53d \uc774\ub984\nPROJECT_ID = &quot;your-firebase-project-id&quot;  # Firebase \ud504\ub85c\uc81d\ud2b8 ID\ub85c \ubcc0\uacbd\n\nFCM_URL = f&quot;https:\/\/fcm.googleapis.com\/v1\/projects\/{PROJECT_ID}\/messages:send&quot;\nSCOPES = [&quot;https:\/\/www.googleapis.com\/auth\/firebase.messaging&quot;]\n\nlogging.basicConfig(\n    level=logging.INFO,\n    format=&quot;%(asctime)s [%(levelname)s] %(message)s&quot;,\n    handlers=[\n        logging.FileHandler(&quot;\/var\/log\/disk_monitor.log&quot;),\n        logging.StreamHandler()\n    ]\n)\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef get_access_token() -&gt; str:\n    &quot;&quot;&quot;\uc11c\ube44\uc2a4 \uacc4\uc815\uc73c\ub85c OAuth2 \uc561\uc138\uc2a4 \ud1a0\ud070 \ubc1c\uae09&quot;&quot;&quot;\n    credentials = service_account.Credentials.from_service_account_file(\n        SERVICE_ACCOUNT_FILE, scopes=SCOPES\n    )\n    credentials.refresh(Request())\n    return credentials.token\n\ndef get_disk_usage(path: str) -&gt; tuple[float, str, str]:\n    &quot;&quot;&quot;\ub514\uc2a4\ud06c \uc0ac\uc6a9\ub960, \uc0ac\uc6a9\ub7c9, \uc804\uccb4 \uc6a9\ub7c9 \ubc18\ud658&quot;&quot;&quot;\n    usage = psutil.disk_usage(path)\n    percent = usage.percent\n    used_gb = usage.used \/ (1024 ** 3)\n    total_gb = usage.total \/ (1024 ** 3)\n    return percent, f&quot;{used_gb:.1f}GB&quot;, f&quot;{total_gb:.1f}GB&quot;\n\ndef send_fcm_notification(percent: float, used: str, total: str) -&gt; bool:\n    &quot;&quot;&quot;FCM Topic\uc73c\ub85c \ud478\uc2dc \uc54c\ub9bc \uc804\uc1a1&quot;&quot;&quot;\n    token = get_access_token()\n    hostname = os.uname().nodename\n\n    payload = {\n        &quot;message&quot;: {\n            &quot;topic&quot;: FCM_TOPIC,\n            &quot;notification&quot;: {\n                &quot;title&quot;: f&quot;\u26a0\ufe0f \ub514\uc2a4\ud06c \uacbd\uace0: {hostname}&quot;,\n                &quot;body&quot;: (\n                    f&quot;\ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9\uc774 {percent:.1f}%\uc5d0 \ub3c4\ub2ec\ud588\uc2b5\ub2c8\ub2e4.\\n&quot;\n                    f&quot;\uc0ac\uc6a9: {used} \/ \uc804\uccb4: {total}&quot;\n                )\n            },\n            &quot;data&quot;: {\n                &quot;type&quot;: &quot;DISK_ALERT&quot;,\n                &quot;hostname&quot;: hostname,\n                &quot;mount_path&quot;: MONITOR_PATH,\n                &quot;percent&quot;: str(percent),\n                &quot;used&quot;: used,\n                &quot;total&quot;: total\n            },\n            &quot;android&quot;: {\n                &quot;priority&quot;: &quot;high&quot;,\n                &quot;notification&quot;: {\n                    &quot;channel_id&quot;: &quot;disk_alert_channel&quot;,\n                    &quot;sound&quot;: &quot;default&quot;,\n                    &quot;click_action&quot;: &quot;DISK_ALERT_ACTIVITY&quot;\n                }\n            }\n        }\n    }\n\n    headers = {\n        &quot;Authorization&quot;: f&quot;Bearer {token}&quot;,\n        &quot;Content-Type&quot;: &quot;application\/json&quot;\n    }\n\n    response = requests.post(FCM_URL, headers=headers, json=payload, timeout=10)\n\n    if response.status_code == 200:\n        logging.info(f&quot;FCM \uc804\uc1a1 \uc131\uacf5: {response.json().get(&#039;name&#039;)}&quot;)\n        return True\n    else:\n        logging.error(f&quot;FCM \uc804\uc1a1 \uc2e4\ud328: {response.status_code} - {response.text}&quot;)\n        return False\n\ndef main():\n    percent, used, total = get_disk_usage(MONITOR_PATH)\n    logging.info(f&quot;\ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9: {percent:.1f}% ({used}\/{total})&quot;)\n\n    if percent &gt; DISK_THRESHOLD_PERCENT:\n        logging.warning(f&quot;\uc784\uacc4\uac12 \ucd08\uacfc ({DISK_THRESHOLD_PERCENT}%)! FCM \uc54c\ub9bc \uc804\uc1a1 \uc911...&quot;)\n        send_fcm_notification(percent, used, total)\n    else:\n        logging.info(&quot;\uc815\uc0c1 \ubc94\uc704\uc785\ub2c8\ub2e4. \uc54c\ub9bc \ubd88\ud544\uc694.&quot;)\n\nif __name__ == &quot;__main__&quot;:\n    main()<\/code><\/pre>\n<blockquote>\n<p><strong>PROJECT_ID<\/strong>\ub97c Firebase \ud504\ub85c\uc81d\ud2b8 \uc124\uc815 \ud398\uc774\uc9c0\uc5d0\uc11c \ud655\uc778\ud55c \uc2e4\uc81c \ud504\ub85c\uc81d\ud2b8 ID\ub85c \ubc18\ub4dc\uc2dc \ubcc0\uacbd\ud558\uc138\uc694.<\/p>\n<\/blockquote>\n<h3>3-3. \uc2a4\ud06c\ub9bd\ud2b8 \uc2e4\ud589 \uad8c\ud55c \ubc0f cron \uc124\uc815<\/h3>\n<pre><code class=\"language-bash\"># \uc2e4\ud589 \uad8c\ud55c \ubd80\uc5ec\nsudo chmod +x \/usr\/local\/bin\/disk_monitor.py\n\n# cron\uc73c\ub85c 5\ubd84\ub9c8\ub2e4 \uc2e4\ud589 (root \uad8c\ud55c)\nsudo crontab -e<\/code><\/pre>\n<p>crontab\uc5d0 \uc544\ub798 \ub0b4\uc6a9\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4:<\/p>\n<pre><code class=\"language-cron\"># 5\ubd84\ub9c8\ub2e4 \ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9 \uac10\uc2dc\n*\/5 * * * * \/usr\/bin\/python3 \/usr\/local\/bin\/disk_monitor.py<\/code><\/pre>\n<h3>3-4. \uc911\ubcf5 \uc54c\ub9bc \ubc29\uc9c0 (\uc120\ud0dd \uc0ac\ud56d)<\/h3>\n<p>80%\ub97c \ucd08\uacfc\ud558\ub294 \uc0c1\ud0dc\uac00 \uacc4\uc18d\ub418\uba74 5\ubd84\ub9c8\ub2e4 \uc54c\ub9bc\uc774 \ubc1c\uc1a1\ub429\ub2c8\ub2e4. \uc911\ubcf5 \ubc1c\uc1a1\uc744 \ub9c9\uc73c\ub824\uba74 \ud50c\ub798\uadf8 \ud30c\uc77c\uc744 \ud65c\uc6a9\ud558\uc138\uc694.<\/p>\n<pre><code class=\"language-python\">FLAG_FILE = &quot;\/tmp\/disk_alert_sent&quot;\n\ndef main():\n    percent, used, total = get_disk_usage(MONITOR_PATH)\n\n    if percent &gt; DISK_THRESHOLD_PERCENT:\n        if not os.path.exists(FLAG_FILE):\n            send_fcm_notification(percent, used, total)\n            open(FLAG_FILE, &quot;w&quot;).close()  # \ud50c\ub798\uadf8 \uc0dd\uc131\n            logging.warning(&quot;\uc54c\ub9bc \uc804\uc1a1 \uc644\ub8cc. \ud50c\ub798\uadf8 \ud30c\uc77c \uc0dd\uc131.&quot;)\n        else:\n            logging.info(&quot;\uc774\ubbf8 \uc54c\ub9bc \ubc1c\uc1a1\ub428. \uc911\ubcf5 \uc804\uc1a1 \uac74\ub108\ub700.&quot;)\n    else:\n        # \uc815\uc0c1 \ubcf5\uadc0 \uc2dc \ud50c\ub798\uadf8 \uc0ad\uc81c\n        if os.path.exists(FLAG_FILE):\n            os.remove(FLAG_FILE)\n            logging.info(&quot;\ub514\uc2a4\ud06c \uc815\uc0c1 \ubcf5\uadc0. \ud50c\ub798\uadf8 \ud30c\uc77c \uc0ad\uc81c.&quot;)<\/code><\/pre>\n<hr \/>\n<h2>4. \uc548\ub4dc\ub85c\uc774\ub4dc \uc571 \uc124\uc815<\/h2>\n<h3>4-1. build.gradle \uc758\uc874\uc131 \ucd94\uac00<\/h3>\n<p><strong>\ud504\ub85c\uc81d\ud2b8 \uc218\uc900 <code>build.gradle<\/code>:<\/strong><\/p>\n<pre><code class=\"language-groovy\">\/\/ build.gradle (Project)\nplugins {\n    id &#039;com.google.gms.google-services&#039; version &#039;4.4.1&#039; apply false\n}<\/code><\/pre>\n<p><strong>\uc571 \uc218\uc900 <code>app\/build.gradle<\/code>:<\/strong><\/p>\n<pre><code class=\"language-groovy\">\/\/ build.gradle (App)\nplugins {\n    id &#039;com.android.application&#039;\n    id &#039;com.google.gms.google-services&#039;\n}\n\ndependencies {\n    \/\/ Firebase BoM (\ubc84\uc804 \ud1b5\ud569 \uad00\ub9ac)\n    implementation platform(&#039;com.google.firebase:firebase-bom:33.1.0&#039;)\n    implementation &#039;com.google.firebase:firebase-messaging&#039;\n}<\/code><\/pre>\n<h3>4-2. FCM \uc11c\ube44\uc2a4 \ud074\ub798\uc2a4 \uad6c\ud604<\/h3>\n<p><code>MyFirebaseMessagingService.kt<\/code>:<\/p>\n<pre><code class=\"language-kotlin\">package com.example.diskmonitor\n\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport androidx.core.app.NotificationCompat\nimport com.google.firebase.messaging.FirebaseMessagingService\nimport com.google.firebase.messaging.RemoteMessage\n\nclass MyFirebaseMessagingService : FirebaseMessagingService() {\n\n    companion object {\n        const val CHANNEL_ID = &quot;disk_alert_channel&quot;\n        const val CHANNEL_NAME = &quot;\ub514\uc2a4\ud06c \uacbd\uace0 \uc54c\ub9bc&quot;\n        const val CHANNEL_DESC = &quot;\uc11c\ubc84 \ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9 \uacbd\uace0 \uc54c\ub9bc&quot;\n    }\n\n    \/**\n     * FCM \uba54\uc2dc\uc9c0 \uc218\uc2e0 \ucc98\ub9ac\n     *\/\n    override fun onMessageReceived(remoteMessage: RemoteMessage) {\n        super.onMessageReceived(remoteMessage)\n\n        val title = remoteMessage.notification?.title ?: &quot;\ub514\uc2a4\ud06c \uacbd\uace0&quot;\n        val body = remoteMessage.notification?.body ?: &quot;\uc11c\ubc84 \ub514\uc2a4\ud06c\ub97c \ud655\uc778\ud558\uc138\uc694.&quot;\n        val data = remoteMessage.data  \/\/ \uc11c\ubc84\uc5d0\uc11c \ubcf4\ub0b8 data \ud544\ub4dc\n\n        sendNotification(title, body, data)\n    }\n\n    \/**\n     * \ud1a0\ud070 \uac31\uc2e0 \uc2dc \ud638\ucd9c (\uc11c\ubc84\uc5d0 \uc0c8 \ud1a0\ud070 \ub4f1\ub85d \ud544\uc694 \uc2dc \uc5ec\uae30\uc11c \ucc98\ub9ac)\n     *\/\n    override fun onNewToken(token: String) {\n        super.onNewToken(token)\n        \/\/ \ud544\uc694 \uc2dc \ud1a0\ud070\uc744 \uc790\uccb4 \uc11c\ubc84\uc5d0 \uc804\uc1a1\n        \/\/ sendTokenToServer(token)\n    }\n\n    private fun sendNotification(\n        title: String,\n        body: String,\n        data: Map&lt;String, String&gt;\n    ) {\n        createNotificationChannel()\n\n        val intent = Intent(this, MainActivity::class.java).apply {\n            flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP\n            \/\/ data \ud544\ub4dc\ub97c Intent\ub85c \uc804\ub2ec\n            data.forEach { (key, value) -&gt; putExtra(key, value) }\n        }\n\n        val pendingIntent = PendingIntent.getActivity(\n            this, 0, intent,\n            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        val hostname = data[&quot;hostname&quot;] ?: &quot;\uc11c\ubc84&quot;\n        val percent = data[&quot;percent&quot;] ?: &quot;?&quot;\n\n        val notification = NotificationCompat.Builder(this, CHANNEL_ID)\n            .setSmallIcon(R.drawable.ic_warning)  \/\/ \uacbd\uace0 \uc544\uc774\ucf58 (res\uc5d0 \ucd94\uac00 \ud544\uc694)\n            .setContentTitle(title)\n            .setContentText(body)\n            .setStyle(NotificationCompat.BigTextStyle().bigText(body))\n            .setPriority(NotificationCompat.PRIORITY_HIGH)\n            .setAutoCancel(true)\n            .setContentIntent(pendingIntent)\n            .build()\n\n        val notificationManager =\n            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n        notificationManager.notify(hostname.hashCode(), notification)\n    }\n\n    private fun createNotificationChannel() {\n        if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {\n            val channel = NotificationChannel(\n                CHANNEL_ID,\n                CHANNEL_NAME,\n                NotificationManager.IMPORTANCE_HIGH\n            ).apply {\n                description = CHANNEL_DESC\n                enableVibration(true)\n            }\n            val manager = getSystemService(NotificationManager::class.java)\n            manager.createNotificationChannel(channel)\n        }\n    }\n}<\/code><\/pre>\n<h3>4-3. Topic \uad6c\ub3c5 \uc124\uc815<\/h3>\n<p><code>MainActivity.kt<\/code>\uc5d0\uc11c \uc571 \uc2e4\ud589 \uc2dc \uc790\ub3d9\uc73c\ub85c \ud1a0\ud53d\uc744 \uad6c\ub3c5\ud569\ub2c8\ub2e4:<\/p>\n<pre><code class=\"language-kotlin\">package com.example.diskmonitor\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport com.google.firebase.messaging.FirebaseMessaging\n\nclass MainActivity : AppCompatActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        subscribeToTopic()\n    }\n\n    private fun subscribeToTopic() {\n        FirebaseMessaging.getInstance().subscribeToTopic(&quot;disk-alert&quot;)\n            .addOnCompleteListener { task -&gt;\n                if (task.isSuccessful) {\n                    \/\/ \uad6c\ub3c5 \uc131\uacf5\n                    android.util.Log.d(&quot;FCM&quot;, &quot;disk-alert \ud1a0\ud53d \uad6c\ub3c5 \uc131\uacf5&quot;)\n                } else {\n                    android.util.Log.e(&quot;FCM&quot;, &quot;\ud1a0\ud53d \uad6c\ub3c5 \uc2e4\ud328&quot;, task.exception)\n                }\n            }\n    }\n}<\/code><\/pre>\n<h3>4-4. AndroidManifest.xml \uc124\uc815<\/h3>\n<pre><code class=\"language-xml\">&lt;manifest xmlns:android=&quot;http:\/\/schemas.android.com\/apk\/res\/android&quot;&gt;\n\n    &lt;!-- \uc54c\ub9bc \uad8c\ud55c (Android 13 \uc774\uc0c1 \ud544\uc218) --&gt;\n    &lt;uses-permission android:name=&quot;android.permission.POST_NOTIFICATIONS&quot;\/&gt;\n    &lt;uses-permission android:name=&quot;android.permission.INTERNET&quot;\/&gt;\n\n    &lt;application ... &gt;\n\n        &lt;!-- FCM \uc11c\ube44\uc2a4 \ub4f1\ub85d --&gt;\n        &lt;service\n            android:name=&quot;.MyFirebaseMessagingService&quot;\n            android:exported=&quot;false&quot;&gt;\n            &lt;intent-filter&gt;\n                &lt;action android:name=&quot;com.google.firebase.MESSAGING_EVENT&quot;\/&gt;\n            &lt;\/intent-filter&gt;\n        &lt;\/service&gt;\n\n        &lt;!-- \uc54c\ub9bc \uc544\uc774\ucf58 \ubc0f \uc0c9\uc0c1 \uae30\ubcf8\uac12 \uc124\uc815 --&gt;\n        &lt;meta-data\n            android:name=&quot;com.google.firebase.messaging.default_notification_icon&quot;\n            android:resource=&quot;@drawable\/ic_warning&quot;\/&gt;\n        &lt;meta-data\n            android:name=&quot;com.google.firebase.messaging.default_notification_color&quot;\n            android:resource=&quot;@color\/colorAccent&quot;\/&gt;\n        &lt;meta-data\n            android:name=&quot;com.google.firebase.messaging.default_notification_channel_id&quot;\n            android:value=&quot;disk_alert_channel&quot;\/&gt;\n\n    &lt;\/application&gt;\n&lt;\/manifest&gt;<\/code><\/pre>\n<h3>4-5. \uc54c\ub9bc \uad8c\ud55c \uc694\uccad (Android 13 \uc774\uc0c1)<\/h3>\n<pre><code class=\"language-kotlin\">\/\/ MainActivity.kt - onCreate\uc5d0 \ucd94\uac00\nprivate fun requestNotificationPermission() {\n    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.TIRAMISU) {\n        if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS)\n            != android.content.pm.PackageManager.PERMISSION_GRANTED\n        ) {\n            requestPermissions(\n                arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),\n                1001\n            )\n        }\n    }\n}<\/code><\/pre>\n<hr \/>\n<h2>5. \ub2e4\uc911 \ud074\ub77c\uc774\uc5b8\ud2b8(5\uba85 \uc774\uc0c1) \uc804\uc1a1 \uc804\ub7b5<\/h2>\n<h3>Topic \ubc29\uc2dd vs Token \ubc29\uc2dd \ube44\uad50<\/h3>\n<table>\n<thead>\n<tr>\n<th>\ud56d\ubaa9<\/th>\n<th>Topic \ubc29\uc2dd (\uad8c\uc7a5)<\/th>\n<th>Token \ubc29\uc2dd<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>\ub300\uc0c1<\/strong><\/td>\n<td>\ud1a0\ud53d \uad6c\ub3c5\uc790 \uc804\uccb4<\/td>\n<td>\ud2b9\uc815 \uae30\uae30 \uc9c0\uc815<\/td>\n<\/tr>\n<tr>\n<td><strong>\uc11c\ubc84 \uad00\ub9ac<\/strong><\/td>\n<td>\ud1a0\ud070 \uc800\uc7a5 \ubd88\ud544\uc694<\/td>\n<td>DB\uc5d0 \ubaa8\ub4e0 \ud1a0\ud070 \uc800\uc7a5 \ud544\uc694<\/td>\n<\/tr>\n<tr>\n<td><strong>\ud655\uc7a5\uc131<\/strong><\/td>\n<td>\ubb34\uc81c\ud55c<\/td>\n<td>\ubcf5\uc7a1\ub3c4 \uc99d\uac00<\/td>\n<\/tr>\n<tr>\n<td><strong>\uac1c\uc778\ud654<\/strong><\/td>\n<td>\ubd88\uac00<\/td>\n<td>\uac00\ub2a5<\/td>\n<\/tr>\n<tr>\n<td><strong>\uc801\ud569\ud55c \uacbd\uc6b0<\/strong><\/td>\n<td>5\uba85 \uc774\uc0c1, \uacf5\ud1b5 \uc54c\ub9bc<\/td>\n<td>\uac1c\uc778\ubcc4 \ub9de\ucda4 \uc54c\ub9bc<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<blockquote>\n<p>\uc774 \uac00\uc774\ub4dc\uc5d0\uc11c\ub294 <strong>Topic \ubc29\uc2dd<\/strong>\uc744 \uc0ac\uc6a9\ud569\ub2c8\ub2e4. \uc0ac\uc6a9\uc790 5\uba85\uc774 <code>disk-alert<\/code> \ud1a0\ud53d\uc744 \uad6c\ub3c5\ud558\uba74 \uc11c\ubc84\ub294 \ud1a0\ud53d \ud558\ub098\uc5d0\ub9cc \uba54\uc2dc\uc9c0\ub97c \ubcf4\ub0b4\uba74 \ub429\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n<h3>\ud2b9\uc815 \uc0ac\uc6a9\uc790\uc5d0\uac8c\ub9cc \uc54c\ub9bc \ubcf4\ub0b4\uae30 (\uace0\uae09)<\/h3>\n<p>\uad00\ub9ac\uc790 \uadf8\ub8f9\uacfc \uc77c\ubc18 \uc0ac\uc6a9\uc790\ub97c \ub098\ub204\uc5b4 \uc54c\ub9bc\uc744 \uc81c\uc5b4\ud558\ub824\uba74 \uc5ec\ub7ec \ud1a0\ud53d\uc744 \ud65c\uc6a9\ud558\uc138\uc694:<\/p>\n<pre><code class=\"language-python\"># \uc2ec\uac01 \uacbd\uace0(90% \ucd08\uacfc)\ub294 \uad00\ub9ac\uc790 \ud1a0\ud53d\uc73c\ub85c \uc804\uc1a1\nADMIN_TOPIC = &quot;disk-alert-admin&quot;    # \uad00\ub9ac\uc790 \uc804\uc6a9\nUSER_TOPIC  = &quot;disk-alert&quot;          # \uc804\uccb4 \uad6c\ub3c5\uc790\n\nif percent &gt; 90:\n    send_fcm_notification(percent, used, total, topic=ADMIN_TOPIC)\nelif percent &gt; 80:\n    send_fcm_notification(percent, used, total, topic=USER_TOPIC)<\/code><\/pre>\n<pre><code class=\"language-kotlin\">\/\/ \uc548\ub4dc\ub85c\uc774\ub4dc - \uad00\ub9ac\uc790 \uc571\uc5d0\uc11c \ucd94\uac00 \ud1a0\ud53d \uad6c\ub3c5\nFirebaseMessaging.getInstance().subscribeToTopic(&quot;disk-alert-admin&quot;)<\/code><\/pre>\n<hr \/>\n<h2>6. \ud14c\uc2a4\ud2b8 \ubc0f \uac80\uc99d<\/h2>\n<h3>6-1. \uc2a4\ud06c\ub9bd\ud2b8 \uc9c1\uc811 \uc2e4\ud589 \ud14c\uc2a4\ud2b8<\/h3>\n<pre><code class=\"language-bash\"># \uc2e4\uc81c \ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9\uc73c\ub85c \ud14c\uc2a4\ud2b8\npython3 \/usr\/local\/bin\/disk_monitor.py\n\n# \uc784\uacc4\uac12\uc744 \ub0ae\ucdb0\uc11c \uac15\uc81c \uc54c\ub9bc \ud14c\uc2a4\ud2b8 (\uc2a4\ud06c\ub9bd\ud2b8\uc5d0\uc11c \uc784\uc2dc\ub85c \ubcc0\uacbd)\nDISK_THRESHOLD_PERCENT = 0  # \ud56d\uc0c1 \uc54c\ub9bc \ubc1c\uc1a1<\/code><\/pre>\n<h3>6-2. Firebase Console\uc5d0\uc11c \ud14c\uc2a4\ud2b8 \uba54\uc2dc\uc9c0 \uc804\uc1a1<\/h3>\n<ol>\n<li>Firebase Console \u2192 <strong>Messaging<\/strong> \u2192 <strong>\uc0c8 \ucea0\ud398\uc778<\/strong> \u2192 <strong>Firebase \uc54c\ub9bc \uba54\uc2dc\uc9c0<\/strong><\/li>\n<li>\uc54c\ub9bc \uc81c\ubaa9\/\ub0b4\uc6a9 \uc785\ub825<\/li>\n<li>\ud0c0\uac9f: <strong>\ud1a0\ud53d<\/strong> \u2192 <code>disk-alert<\/code> \uc785\ub825<\/li>\n<li><strong>\uc9c0\uae08 \ubcf4\ub0b4\uae30<\/strong> \ud074\ub9ad<\/li>\n<\/ol>\n<h3>6-3. cURL\ub85c FCM API \uc9c1\uc811 \ud14c\uc2a4\ud2b8<\/h3>\n<pre><code class=\"language-bash\"># \uc561\uc138\uc2a4 \ud1a0\ud070 \ubc1c\uae09 (gcloud CLI \ud544\uc694)\nACCESS_TOKEN=$(gcloud auth print-access-token)\n\ncurl -X POST \\\n  &quot;https:\/\/fcm.googleapis.com\/v1\/projects\/YOUR_PROJECT_ID\/messages:send&quot; \\\n  -H &quot;Authorization: Bearer $ACCESS_TOKEN&quot; \\\n  -H &quot;Content-Type: application\/json&quot; \\\n  -d &#039;{\n    &quot;message&quot;: {\n      &quot;topic&quot;: &quot;disk-alert&quot;,\n      &quot;notification&quot;: {\n        &quot;title&quot;: &quot;\ud14c\uc2a4\ud2b8 \uc54c\ub9bc&quot;,\n        &quot;body&quot;: &quot;FCM \uc5f0\ub3d9 \ud14c\uc2a4\ud2b8\uc785\ub2c8\ub2e4.&quot;\n      }\n    }\n  }&#039;<\/code><\/pre>\n<h3>6-4. \ub85c\uadf8 \ud655\uc778<\/h3>\n<pre><code class=\"language-bash\"># \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud06c\ub9bd\ud2b8 \ub85c\uadf8 \ud655\uc778\ntail -f \/var\/log\/disk_monitor.log\n\n# cron \uc2e4\ud589 \ub85c\uadf8 \ud655\uc778\ngrep CRON \/var\/log\/syslog | tail -20<\/code><\/pre>\n<hr \/>\n<h2>\uc815\ub9ac<\/h2>\n<table>\n<thead>\n<tr>\n<th>\ub2e8\uacc4<\/th>\n<th>\uc791\uc5c5 \ub0b4\uc6a9<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\u2460 Firebase<\/td>\n<td>\ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 \u2192 \uc11c\ube44\uc2a4 \uacc4\uc815 \ud0a4 \ubc1c\uae09<\/td>\n<\/tr>\n<tr>\n<td>\u2461 \uc11c\ubc84<\/td>\n<td>Python \uc2a4\ud06c\ub9bd\ud2b8\ub85c \ub514\uc2a4\ud06c \uac10\uc2dc \u2192 FCM HTTP v1 API \ud638\ucd9c<\/td>\n<\/tr>\n<tr>\n<td>\u2462 cron<\/td>\n<td>5\ubd84\ub9c8\ub2e4 \uc2a4\ud06c\ub9bd\ud2b8 \uc2e4\ud589<\/td>\n<\/tr>\n<tr>\n<td>\u2463 Android<\/td>\n<td>FCM SDK \ucd94\uac00 \u2192 <code>disk-alert<\/code> \ud1a0\ud53d \uad6c\ub3c5 \u2192 \uc54c\ub9bc \ud45c\uc2dc<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Topic \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud558\uba74 \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uba87 \uba85\uc774 \ub418\uc5b4\ub3c4<\/strong> \uc11c\ubc84 \ucf54\ub4dc \ubcc0\uacbd \uc5c6\uc774 \ubaa8\ub4e0 \uad6c\ub3c5\uc790\uc5d0\uac8c \uc54c\ub9bc\uc744 \uc804\ub2ec\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub514\uc2a4\ud06c \uacbd\uace0 \uc678\uc5d0\ub3c4 CPU, \uba54\ubaa8\ub9ac \ub4f1\uc758 \uc9c0\ud45c\ub3c4 \ub3d9\uc77c\ud55c \uad6c\uc870\ub85c \ud655\uc7a5\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>FCM\uc73c\ub85c \uc11c\ubc84 \ub514\uc2a4\ud06c \uc0c1\ud0dc\ub97c \uc548\ub4dc\ub85c\uc774\ub4dc \uc571\uc5d0 \ud478\uc2dc \uc54c\ub9bc \ubcf4\ub0b4\uae30 \uc11c\ubc84 \ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9\uc774 80%\ub97c \ucd08\uacfc\ud558\uba74 Firebase Cloud Messaging(FCM)\uc744 \ud1b5\ud574 \uc548\ub4dc\ub85c\uc774\ub4dc \uc571\uc5d0 \uc989\uc2dc \ud478\uc2dc \uc54c\ub9bc\uc744 \uc804\uc1a1\ud558\ub294 \ubc29\ubc95\uc744 \ub2e8\uacc4\ubcc4\ub85c \uc124\uba85\ud569\ub2c8\ub2e4. 1. \uc804\uccb4 \uc544\ud0a4\ud14d\ucc98 \uac1c\uc694 [Linux Server] \u251c\u2500\u2500 cron + Python \uc2a4\ud06c\ub9bd\ud2b8 (\ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9 \uac10\uc2dc) \u2502 \u2502 \u2502 \u2502 80% \ucd08\uacfc \uc2dc HTTP POST \u2502 \u25bc [Firebase Cloud Messaging\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=11651\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12],"tags":[],"class_list":["post-11651","post","type-post","status-publish","format-standard","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/11651","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11651"}],"version-history":[{"count":1,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/11651\/revisions"}],"predecessor-version":[{"id":11652,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/11651\/revisions\/11652"}],"wp:attachment":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11651"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11651"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11651"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}