{"id":2017,"date":"2025-12-19T14:25:17","date_gmt":"2025-12-19T05:25:17","guid":{"rendered":"https:\/\/hyunsu.com\/wordpress\/?p=2017"},"modified":"2025-12-19T14:27:20","modified_gmt":"2025-12-19T05:27:20","slug":"cloudflare-stream-api","status":"publish","type":"post","link":"https:\/\/hyunsu.com\/wordpress\/?p=2017","title":{"rendered":"Cloudflare Stream API"},"content":{"rendered":"\n<p><\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Cloudflare Stream API \uc5f0\ub3d9 \ub9e4\ub274\uc5bc<\/h1>\n\n\n\n<p>\uc774 \ub9e4\ub274\uc5bc\uc740 Cloudflare Dashboard\ub97c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0, \uc790\uccb4 \uad6c\ucd95\ud55c Admin \ud398\uc774\uc9c0\uc5d0\uc11c API\ub97c \ud1b5\ud574 \ub3d9\uc601\uc0c1\uc744 \uad00\ub9ac\ud558\ub294 \uac1c\ubc1c\uc790\ub97c \uc704\ud574 \uc791\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc0ac\uc804 \uc900\ube44 (Prerequisites)<\/h2>\n\n\n\n<p>API\ub97c \ud638\ucd9c\ud558\uae30 \uc704\ud574 Cloudflare \ub300\uc2dc\ubcf4\ub4dc\uc5d0\uc11c \ub2e4\uc74c \ub450 \uac00\uc9c0 \uc815\ubcf4\ub97c \uba3c\uc800 \ud655\ubcf4\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Account ID<\/strong>: Cloudflare \ub300\uc2dc\ubcf4\ub4dc URL\uc758 <code>dash.cloudflare.com\/<\/code> \ub4a4\uc5d0 \uc788\ub294 \ubb38\uc790\uc5f4, \ub610\ub294 Stream \uba54\ub274 \uc6b0\uce21 \uc0ac\uc774\ub4dc\ubc14\uc5d0\uc11c \ud655\uc778 \uac00\ub2a5.<\/li>\n\n\n\n<li><strong>API Token<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Manage account > Account API tokens > Create Token<\/li>\n\n\n\n<li>\ud15c\ud50c\ub9bf \uc911 <strong>Read and write to Cloudflare Stream and Images\u00a0<\/strong>\uc744 \uc120\ud0dd<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc5c5\ub85c\ub4dc \ubc29\uc2dd \uacb0\uc815 (Architecture)<\/h2>\n\n\n\n<p>\ub3d9\uc601\uc0c1 \ud30c\uc77c\uc740 \ud06c\uae30\uac00 \ud06c\uae30 \ub54c\ubb38\uc5d0 \uc5c5\ub85c\ub4dc \ubc29\uc2dd\uc744 \uc2e0\uc911\ud788 \uc120\ud0dd\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>\ubc29\uc2dd<\/strong><\/td><td><strong>\uc124\uba85<\/strong><\/td><td><strong>\ucd94\ucc9c \uc2dc\ub098\ub9ac\uc624<\/strong><\/td><\/tr><tr><td><strong>A. Simple Upload<\/strong><\/td><td>\ub2e8\uc77c HTTP POST \uc694\uccad\uc73c\ub85c \ud30c\uc77c \uc804\uc1a1 (\ucd5c\ub300 200MB).<\/td><td>\ud504\ub85c\ud544 \uc774\ubbf8\uc9c0, 1\ubd84 \ubbf8\ub9cc \uc9e7\uc740 \uc601\uc0c1.<\/td><\/tr><tr><td><strong>B. Direct Creator Upload<\/strong> (\ucd94\ucc9c)<\/td><td>Admin \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c CF\ub85c \uc9c1\uc811 \uc5c5\ub85c\ub4dc. (\uc11c\ubc84 \ub300\uc5ed\ud3ed \uc808\uc57d)<\/td><td><strong>\uac00\uc7a5 \uc77c\ubc18\uc801\uc778 \uad6c\ucd95 \ubc29\uc2dd.<\/strong><\/td><\/tr><tr><td><strong>C. Tus Resumable Upload<\/strong><\/td><td>\ub124\ud2b8\uc6cc\ud06c \ub04a\uae40 \uc2dc \uc774\uc5b4\uc62c\ub9ac\uae30 \uc9c0\uc6d0, \ub300\uc6a9\ub7c9 \ud30c\uc77c \ucd5c\uc801\ud654.<\/td><td>200MB \uc774\uc0c1, \uc778\ud130\ub137\uc774 \ubd88\uc548\uc815\ud55c \ud658\uacbd.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>\uc774 \uac00\uc774\ub4dc\uc5d0\uc11c\ub294 \uc2e4\ubb34\uc5d0\uc11c \uac00\uc7a5 \ub9ce\uc774 \uc4f0\uc774\ub294 <strong>B (Direct Creator Upload)<\/strong>&nbsp;\uc640 <strong>C (Tus)<\/strong> \ubc29\uc2dd\uc744 \ub2e4\ub8f9\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc2dc\ub098\ub9ac\uc624 A: Direct Creator Upload (\uc77c\ud68c\uc6a9 URL \ubc29\uc2dd)<\/h2>\n\n\n\n<p>\uc774 \ubc29\uc2dd\uc740 \ubcf4\uc548\uc0c1 \uc548\uc804\ud558\uba70, \ubc31\uc5d4\ub4dc \uc11c\ubc84\uc758 \ub300\uc5ed\ud3ed\uc744 \uc18c\ubaa8\ud558\uc9c0 \uc54a\uace0 Admin \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c Cloudflare\ub85c \ubc14\ub85c \ud30c\uc77c\uc744 \uc3d8\ub294 \ubc29\uc2dd\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: \uc5c5\ub85c\ub4dc URL \ubc1c\uae09 \uc694\uccad (Backend)<\/h3>\n\n\n\n<p>Admin \ud398\uc774\uc9c0 \uc0ac\uc6a9\uc790\uac00 &#8220;\uc5c5\ub85c\ub4dc&#8221; \ubc84\ud2bc\uc744 \ub204\ub974\uba74, \ubc31\uc5d4\ub4dc \uc11c\ubc84\ub294 Cloudflare\uc5d0\uac8c &#8220;\uc5c5\ub85c\ub4dc \ud560 \uc8fc\uc18c\ub97c \ub2ec\ub77c&#8221;\uace0 \uc694\uccad\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Endpoint<\/strong>: <code>POST https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{account_id}\/stream\/direct_upload<\/code><\/li>\n\n\n\n<li><strong>Headers<\/strong>:\n<ul class=\"wp-block-list\">\n<li><code>Authorization<\/code>: Bearer <code>&lt;YOUR_API_TOKEN><\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Body (JSON)<\/strong>:\n<ul class=\"wp-block-list\">\n<li><code>maxDurationSeconds<\/code>: \uc601\uc0c1 \ucd5c\ub300 \uae38\uc774 \uc81c\ud55c<\/li>\n\n\n\n<li><code>expiry<\/code>: URL \ub9cc\ub8cc \uc2dc\uac04 (\uc635\uc158)<\/li>\n\n\n\n<li><code>requireSignedURLs<\/code>: \ube44\uacf5\uac1c \uc601\uc0c1 \uc5ec\ubd80 (\uc635\uc158)<\/li>\n\n\n\n<li>\ub354 \ub9ce\uc740 \uc635\uc158:\u00a0<a href=\"https:\/\/developers.cloudflare.com\/api\/resources\/stream\/subresources\/direct_upload\/methods\/create\/\">https:\/\/developers.cloudflare.com\/api\/resources\/stream\/subresources\/direct_upload\/methods\/create\/<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p><strong>Request \uc608\uc2dc (cURL):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">curl -X POST https:\/\/api.cloudflare.com\/client\/v4\/accounts\/&lt;ACCOUNT_ID&gt;\/stream\/direct_upload \\<br> -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\<br> -H \"Content-Type: application\/json\" \\<br> -d '{<br>    \"maxDurationSeconds\": 3600,<br>    \"creator\": \"admin-user-01\",<br>    \"meta\": {\"name\": \"product_demo_v1.mp4\"}<br> }'<\/pre>\n\n\n\n<p><strong>Response (\uc131\uacf5 \uc2dc):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"result\": {<br>    \"uploadURL\": \"https:\/\/upload.cloudflarestream.com\/fc33ac543eb9046a......\",<br>    \"uid\": \"fc33ac543eb......\",<br>    \"watermark\": null,<br>    \"scheduledDeletion\": null<br>  },<br>  \"success\": true,<br>  \"errors\": [],<br>  \"messages\": []<br>}<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>\ud575\uc2ec:<\/strong> \uc5ec\uae30\uc11c \ubc1b\uc740 <code>uploadURL<\/code>\uc744 \ud504\ub860\ud2b8\uc5d4\ub4dc(Admin \ud398\uc774\uc9c0)\ub85c \uc804\ub2ec\ud569\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n\n\n\n<p><strong>\ub3d9\uc601\uc0c1 \uc5c5\ub85c\ub4dc \uc608\uc2dc (cURL):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">curl -X POST \\<br>  --form file=@\/filepath\/filename.mp4 \\<br>  https:\/\/upload.cloudflarestream.com\/fc33ac543eb9046a......<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: \ud30c\uc77c \uc5c5\ub85c\ub4dc (Frontend \/ Admin Page)<\/h3>\n\n\n\n<p>\ud504\ub860\ud2b8\uc5d4\ub4dc\ub294 \ubc31\uc5d4\ub4dc\ub85c\ubd80\ud130 \ubc1b\uc740 <code>uploadURL<\/code>\ub85c \ud30c\uc77c\uc744 <code>POST<\/code> \ud569\ub2c8\ub2e4. (\uc774\ub54c\ub294 API Token\uc774 \ud544\uc694 \uc5c6\uc2b5\ub2c8\ub2e4.)<\/p>\n\n\n\n<p><strong>HTML Form \uc608\uc2dc:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;form action=\"YOUR_UPLOAD_URL_HERE\" method=\"post\" enctype=\"multipart\/form-data\"&gt;<br>  &lt;input type=\"file\" name=\"file\" id=\"file\"\/&gt;<br>  &lt;input type=\"submit\" value=\"Upload\"\/&gt;<br>&lt;\/form&gt;<\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>AJAX\ub098 Fetch API\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc9c4\ud589\ub960(Progress bar)\uc744 \uad6c\ud604\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc2dc\ub098\ub9ac\uc624 B: Tus \ud504\ub85c\ud1a0\ucf5c \uc5c5\ub85c\ub4dc (\ub300\uc6a9\ub7c9\/\uc774\uc5b4\uc62c\ub9ac\uae30)<\/h2>\n\n\n\n<p>\ud30c\uc77c\uc774 \ub9e4\uc6b0 \ud06c\uac70\ub098(GB \ub2e8\uc704), \ub124\ud2b8\uc6cc\ud06c\uac00 \ubd88\uc548\uc815\ud560 \uacbd\uc6b0 <code>tus<\/code> \ud504\ub85c\ud1a0\ucf5c\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \uc9c1\uc811 \uad6c\ud604\ud558\uae30\ubcf4\ub2e4\ub294 <code>tus-js-client<\/code> \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Frontend \uad6c\ud604 (tus-js-client \uc0ac\uc6a9)<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ub77c\uc774\ube0c\ub7ec\ub9ac \uc124\uce58<\/strong>:npm install tus-js-client<\/li>\n\n\n\n<li><strong>\uc5c5\ub85c\ub4dc \ucf54\ub4dc \uc791\uc131<\/strong>:<br>var fs = require(&#8220;fs&#8221;);<br>var tus = require(&#8220;tus-js-client&#8221;);<br><br>\/\/ Specify location of file you would like to upload below<br>var path = __dirname + &#8220;\/test.mp4&#8221;;<br>var file = fs.createReadStream(path);<br>var size = fs.statSync(path).size;<br>var mediaId = &#8220;&#8221;;<br><br>var options = {<br>endpoint: &#8220;https:\/\/api.cloudflare.com\/client\/v4\/accounts\/&lt;ACCOUNT_ID>\/stream&#8221;,<br>headers: {<br>Authorization: &#8220;Bearer &lt;API_TOKEN>&#8221;,<br>},<br>chunkSize: 50 * 1024 * 1024, \/\/ Required a minimum chunk size of 5 MB. Here we use 50 MB.<br>retryDelays: [0, 3000, 5000, 10000, 20000], \/\/ Indicates to tus-js-client the delays after which it will retry if the upload fails.<br>metadata: {<br>name: &#8220;test.mp4&#8221;,<br>filetype: &#8220;video\/mp4&#8221;,<br>\/\/ Optional if you want to include a watermark<br>\/\/ watermark: &#8216;&lt;WATERMARK_UID>&#8217;,<br>},<br>uploadSize: size,<br>onError: function (error) {<br>throw error;<br>},<br>onProgress: function (bytesUploaded, bytesTotal) {<br>var percentage = ((bytesUploaded \/ bytesTotal) * 100).toFixed(2);<br>console.log(bytesUploaded, bytesTotal, percentage + &#8220;%&#8221;);<br>},<br>onSuccess: function () {<br>console.log(&#8220;Upload finished&#8221;);<br>},<br>onAfterResponse: function (req, res) {<br>return new Promise((resolve) => {<br>var mediaIdHeader = res.getHeader(&#8220;stream-media-id&#8221;);<br>if (mediaIdHeader) {<br>mediaId = mediaIdHeader;<br>}<br>resolve();<br>});<br>},<br>};<br><br>var upload = new tus.Upload(file, options);<br>upload.start();<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">\uc601\uc0c1 \uc0c1\ud0dc \ud655\uc778 (Webhooks)<\/h2>\n\n\n\n<p>\uc5c5\ub85c\ub4dc\uac00 \ub05d\ub0ac\ub2e4\uace0 \ubc14\ub85c \uc7ac\uc0dd\ud560 \uc218 \uc788\ub294 \uac83\uc740 \uc544\ub2d9\ub2c8\ub2e4. Cloudflare \ub0b4\ubd80\uc5d0\uc11c \uc778\ucf54\ub529(Encoding) \uacfc\uc815\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. Admin \ud398\uc774\uc9c0\uc5d0\uc11c &#8220;\ucc98\ub9ac \uc911&#8230;&#8221; \uc0c1\ud0dc\ub97c \ud45c\uc2dc\ud558\ub824\uba74 Webhook\uc774 \ud544\uc218\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Webhook \uc124\uc815<\/h3>\n\n\n\n<p>API\ub85c \uc124\uc815\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">curl -X PUT --header 'Authorization: Bearer &lt;API_TOKEN&gt;' \\<br>https:\/\/api.cloudflare.com\/client\/v4\/accounts\/&lt;ACCOUNT_ID&gt;\/stream\/webhook \\<br>--data '{\"notificationUrl\":\"&lt;WEBHOOK_NOTIFICATION_URL&gt;\"}'<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\uc8fc\uc694 \uc774\ubca4\ud2b8 \ud0c0\uc785<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>stream.created<\/code>: \uc601\uc0c1\uc774 \uc0dd\uc131\ub428 (\uc5c5\ub85c\ub4dc \uc9c1\ud6c4)<\/li>\n\n\n\n<li><code>stream.ready<\/code>: \uc778\ucf54\ub529 \uc644\ub8cc, \uc7ac\uc0dd \uac00\ub2a5 \uc0c1\ud0dc (\uac00\uc7a5 \uc911\uc694)<\/li>\n<\/ul>\n\n\n\n<p><strong>Payload \uc608\uc2dc (Server\uac00 \ubc1b\uc744 \ub370\uc774\ud130):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"uid\": \"d440056...\",<br>  \"status\": {<br>    \"state\": \"ready\",<br>    \"pctComplete\": \"100.000000\"<br>  },<br>  \"meta\": {<br>    \"name\": \"product_demo.mp4\"<br>  },<br>  \"created\": \"2023-01-01T10:00:00.000000Z\"<br>}<\/pre>\n\n\n\n<p>\uc11c\ubc84\ub294 \uc774 \uc6f9\ud6c5\uc744 \ubc1b\uc73c\uba74 DB\uc758 \ud574\ub2f9 \uc601\uc0c1 \uc0c1\ud0dc\ub97c <code>Processing<\/code> -&gt; <code>Active<\/code>\ub85c \uc5c5\ub370\uc774\ud2b8\ud558\uba74 \ub429\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc7ac\uc0dd \ubc0f \uad00\ub9ac (Playback &amp; Management)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\uc601\uc0c1 \ubaa9\ub85d \uc870\ud68c (Admin \ud398\uc774\uc9c0\uc6a9)<\/h3>\n\n\n\n<p>Admin \ub9ac\uc2a4\ud2b8 \ud398\uc774\uc9c0\uc5d0 \ubfcc\ub824\uc904 \ub370\uc774\ud130\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Endpoint<\/strong>: <code>GET https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{account_id}\/stream<\/code><\/li>\n\n\n\n<li><strong>Parameters<\/strong>: <code>?status=ready&amp;page=1&amp;per_page=50<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\uc601\uc0c1 \uc7ac\uc0dd (\uc0ac\uc6a9\uc790 \ud398\uc774\uc9c0\uc6a9)<\/h3>\n\n\n\n<p>\uc5c5\ub85c\ub4dc \uc644\ub8cc \ud6c4 \ud68d\ub4dd\ud55c <code>uid<\/code> (Video ID)\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>HLS\/DASH URL (\ucee4\uc2a4\ud140 \ud50c\ub808\uc774\uc5b4\uc6a9)<\/strong>:\n<ul class=\"wp-block-list\">\n<li><code>https:\/\/customer-&lt;CODE>.cloudflarestream.com\/&lt;UID>\/manifest\/video.m3u8<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>iFrame Embed (\uac00\uc7a5 \uc26c\uc6b4 \ubc29\ubc95)<\/strong>:&lt;iframe<br>src=&#8221;https:\/\/customer-&lt;CODE>cloudflarestream.com\/&lt;UID>\/iframe&#8221;<br>style=&#8221;border: none&#8221;<br>allow=&#8221;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&#8221;<br>allowfullscreen=&#8221;true&#8221;><br>&lt;\/iframe><\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc694\uc57d<\/h2>\n\n\n\n<p>Admin \ud398\uc774\uc9c0 \uac1c\ubc1c \uc2dc \ub2e4\uc74c \ud750\ub984\uc744 \ub530\ub974\uc2ed\uc2dc\uc624.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Backend<\/strong>: <code>Direct Upload URL<\/code> \ubc1c\uae09 API \uad6c\ud604.<\/li>\n\n\n\n<li><strong>Frontend<\/strong>: \ubc1c\uae09\ubc1b\uc740 URL\ub85c \ud30c\uc77c <code>POST<\/code> (\ub610\ub294 TUS \uc0ac\uc6a9) \uad6c\ud604.<\/li>\n\n\n\n<li><strong>Backend<\/strong>: Cloudflare Webhook \uc218\uc2e0 API (<code>POST \/webhook\/stream<\/code>) \uad6c\ud604\ud558\uc5ec DB \uc0c1\ud0dc \uc5c5\ub370\uc774\ud2b8.<\/li>\n\n\n\n<li><strong>Frontend<\/strong>: DB\uc5d0 \uc800\uc7a5\ub41c <code>uid<\/code>\ub97c \uc774\uc6a9\ud558\uc5ec Player \uc5f0\ub3d9.<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">\ubc31\uc5d4\ub4dc \uac1c\ubc1c<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Python (Flask\/Requests \uc0ac\uc6a9)<\/strong>&nbsp;\uc608\uc81c\ub85c \uc124\uba85\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ubc31\uc5d4\ub4dc\ub294 \ub450 \uac00\uc9c0 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Direct Upload URL \ubc1c\uae09<\/strong>\u00a0(200MB \ubbf8\ub9cc \ud30c\uc77c\uc6a9)<\/li>\n\n\n\n<li><strong>TUS Upload URL \ubc1c\uae09<\/strong>\u00a0(200MB \uc774\uc0c1 \ub300\uc6a9\ub7c9 \ud30c\uc77c\uc6a9)<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">&#x26a0;&#xfe0f; \uc911\uc694: \ubcf4\uc548 \uc124\uc815 (\uacf5\ud1b5 \uc0ac\ud56d)<\/h3>\n\n\n\n<p>\ucf54\ub4dc\uc5d0 API \ud0a4\ub97c \uc9c1\uc811 \ud558\ub4dc\ucf54\ub529\ud558\uc9c0 \ub9c8\uc2ed\uc2dc\uc624.&nbsp;<code>.env<\/code>&nbsp;\ud30c\uc77c\uc774\ub098 \ud658\uacbd \ubcc0\uc218\uc5d0\uc11c \ubd88\ub7ec\uc624\ub294 \ubc29\uc2dd\uc744 \uad8c\uc7a5\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>CLOUDFLARE_ACCOUNT_ID<\/code>: \uacc4\uc815 ID<\/li>\n\n\n\n<li><code>CLOUDFLARE_API_TOKEN<\/code>: Stream \uad8c\ud55c\uc774 \uc788\ub294 API \ud1a0\ud070<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">1. Direct Upload URL \ubc1c\uae09 (200MB \ubbf8\ub9cc)<\/h2>\n\n\n\n<p>Simple Upload \ubc29\uc2dd\uc744 \uc704\ud55c \uc77c\ud68c\uc6a9 \uc5c5\ub85c\ub4dc URL\uc744 \ubc1c\uae09\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc124\uce58:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pip install flask flask-cors requests python-dotenv<\/pre>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>app.py<\/code>):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import os<br>import requests<br>from flask import Flask, request, jsonify<br>from flask_cors import CORS<br>from dotenv import load_dotenv<br><br>load_dotenv()<br><br>app = Flask(__name__)<br>CORS(app)<br><br>ACCOUNT_ID = os.getenv(\"CLOUDFLARE_ACCOUNT_ID\")<br>API_TOKEN = os.getenv(\"CLOUDFLARE_API_TOKEN\")<br><br>@app.route('\/api\/get-upload-url', methods=['POST'])<br>def get_direct_upload_url():<br>    if not ACCOUNT_ID or not API_TOKEN:<br>        return jsonify({\"error\": \"Missing Cloudflare credentials\"}), 500<br><br>    url = f\"https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{ACCOUNT_ID}\/stream\/direct_upload\"<br>    headers = {<br>        \"Authorization\": f\"Bearer {API_TOKEN}\",<br>        \"Content-Type\": \"application\/json\"<br>    }<br>    <br>    payload = {<br>        \"maxDurationSeconds\": 3600,<br>        \"requireSignedURLs\": False<br>    }<br><br>    try:<br>        response = requests.post(url, headers=headers, json=payload)<br>        response.raise_for_status()<br>        result = response.json().get(\"result\")<br>        return jsonify({\"uploadURL\": result.get(\"uploadURL\"), \"uid\": result.get(\"uid\")})<br>    except requests.exceptions.RequestException as e:<br>        return jsonify({\"error\": str(e)}), 500<br><br>if __name__ == '__main__':<br>    app.run(port=5000, debug=True)<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. TUS Upload URL \ubc1c\uae09 (200MB \uc774\uc0c1)<\/h2>\n\n\n\n<p>\ub300\uc6a9\ub7c9 \ud30c\uc77c\uc744 \uc704\ud55c TUS \uc5c5\ub85c\ub4dc URL\uc744 \ubc1c\uae09\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>app.py<\/code>\uc5d0 \ucd94\uac00):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@app.route('\/api\/get-tus-url', methods=['POST', 'OPTIONS'])<br>def get_tus_url():<br>    # CORS preflight \ucc98\ub9ac<br>    if request.method == 'OPTIONS':<br>        response = app.make_response('')<br>        response.status_code = 200<br>        response.headers['Access-Control-Allow-Origin'] = '*'<br>        response.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'<br>        response.headers['Access-Control-Allow-Headers'] = '*'<br>        response.headers['Access-Control-Expose-Headers'] = 'Location'<br>        return response<br>    <br>    if not ACCOUNT_ID or not API_TOKEN:<br>        return jsonify({\"error\": \"Missing Cloudflare credentials\"}), 500<br><br>    # \ud30c\uc77c \ud06c\uae30 \uac00\uc838\uc624\uae30<br>    data = request.get_json() or {}<br>    file_size = data.get('fileSize')<br>    <br>    if not file_size:<br>        return jsonify({\"error\": \"Missing fileSize in request\"}), 400<br><br>    # Cloudflare Stream TUS endpoint (direct_user=true \ud30c\ub77c\ubbf8\ud130 \ud544\uc218)<br>    cf_endpoint = f\"https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{ACCOUNT_ID}\/stream?direct_user=true\"<br>    <br>    headers = {<br>        \"Authorization\": f\"Bearer {API_TOKEN}\",<br>        \"Tus-Resumable\": \"1.0.0\",<br>        \"Upload-Length\": str(file_size),<br>    }<br>    <br>    try:<br>        # Cloudflare\uc5d0 TUS \uc5c5\ub85c\ub4dc \uc0dd\uc131 \uc694\uccad<br>        cf_response = requests.post(cf_endpoint, headers=headers)<br>        cf_response.raise_for_status()<br>        <br>        # Location \ud5e4\ub354\uc5d0\uc11c \uc5c5\ub85c\ub4dc URL \uac00\uc838\uc624\uae30<br>        upload_url = cf_response.headers.get('Location')<br>        <br>        if not upload_url:<br>            return jsonify({\"error\": \"No Location header in response\"}), 500<br>        <br>        # Location \ud5e4\ub354\ub85c \uc5c5\ub85c\ub4dc URL \ubc18\ud658<br>        response = app.make_response('')<br>        response.status_code = 200<br>        response.headers['Location'] = upload_url<br>        response.headers['Access-Control-Expose-Headers'] = 'Location'<br>        response.headers['Access-Control-Allow-Origin'] = '*'<br>        <br>        return response<br>    except requests.exceptions.RequestException as e:<br>        return jsonify({\"error\": str(e)}), 500<\/pre>\n\n\n\n<h1 class=\"wp-block-heading\">\ud504\ub860\ud2b8\uc5d4\ub4dc \uac1c\ubc1c<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ubc31\uc5d4\ub4dc\uc5d0\uc11c <code>uploadURL<\/code>\uc744 \ubc1b\uc544\uc654\ub2e4\ub294 \uac00\uc815\ud558\uc5d0, \ud504\ub860\ud2b8\uc5d4\ub4dc(Admin \ud398\uc774\uc9c0)\uc5d0\uc11c \uc2e4\uc81c\ub85c \ud30c\uc77c\uc744 \uc5c5\ub85c\ub4dc\ud558\ub294 \ub450 \uac00\uc9c0 \ubc29\uc2dd\uc744 \uc548\ub0b4\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ub450 \ubc29\uc2dd \ubaa8\ub450 \uacf5\ud1b5\uc801\uc73c\ub85c <strong>&#8220;1. \ubc31\uc5d4\ub4dc\uc5d0\uc11c uploadURL \uac00\uc838\uc624\uae30&#8221; -&gt; &#8220;2. Cloudflare\ub85c \uc5c5\ub85c\ub4dc\ud558\uae30&#8221;<\/strong>&nbsp;\uc758 \ud750\ub984\uc744 \uac00\uc9d1\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Simple Form \ubc29\uc2dd (Fetch API \uc0ac\uc6a9)<\/h2>\n\n\n\n<p>\ubcc4\ub3c4\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc5c6\uc774 \ube0c\ub77c\uc6b0\uc800 \ub0b4\uc7a5 \uae30\ub2a5\ub9cc\uc73c\ub85c \uad6c\ud604\ud569\ub2c8\ub2e4. \uad6c\ud604\uc774 \uac00\uc7a5 \uc27d\uc9c0\ub9cc, <strong>\ub124\ud2b8\uc6cc\ud06c\uac00 \ub04a\uae30\uba74 \ucc98\uc74c\ubd80\ud130 \ub2e4\uc2dc \uc62c\ub824\uc57c \ud569\ub2c8\ub2e4.<\/strong> (200MB \ubbf8\ub9cc \ud30c\uc77c \uad8c\uc7a5)<\/p>\n\n\n\n<p><strong>HTML \uad6c\uc870:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;h3&gt;Simple Upload (Max 200MB)&lt;\/h3&gt;<br>&lt;input type=\"file\" id=\"simpleFileInput\" accept=\"video\/*\"&gt;<br>&lt;button onclick=\"startSimpleUpload()\"&gt;\uc5c5\ub85c\ub4dc \uc2dc\uc791&lt;\/button&gt;<br><br>&lt;div id=\"simpleStatus\"&gt;\ub300\uae30 \uc911...&lt;\/div&gt;<\/pre>\n\n\n\n<p><strong>JavaScript \uad6c\ud604:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">async function startSimpleUpload() {<br>    const fileInput = document.getElementById('simpleFileInput');<br>    const statusDiv = document.getElementById('simpleStatus');<br>    const file = fileInput.files[0];<br><br>    if (!file) {<br>        alert(\"\ud30c\uc77c\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\");<br>        return;<br>    }<br><br>    try {<br>        statusDiv.innerText = \"1. \uc5c5\ub85c\ub4dc URL \uc694\uccad \uc911...\";<br>        <br>        \/\/ [Step 1] \ubc31\uc5d4\ub4dc API\ub97c \ud638\ucd9c\ud558\uc5ec Cloudflare uploadURL\uc744 \ubc1b\uc544\uc635\ub2c8\ub2e4.<br>        \/\/ (\uc55e\uc11c \uc791\uc131\ud55c Node.js\/Python \ubc31\uc5d4\ub4dc \uc5d4\ub4dc\ud3ec\uc778\ud2b8)<br>        const response = await fetch('\/api\/get-upload-url', { method: 'POST' });<br>        const data = await response.json();<br>        const uploadURL = data.uploadURL; \/\/ \ubc31\uc5d4\ub4dc\uac00 \ub118\uaca8\uc900 1\ud68c\uc6a9 URL<br><br>        statusDiv.innerText = \"2. Cloudflare\ub85c \uc804\uc1a1 \uc911...\";<br><br>        \/\/ [Step 2] \ubc1b\uc544\uc628 URL\ub85c \ud30c\uc77c\uc744 \uc9c1\uc811 POST \ud569\ub2c8\ub2e4.<br>        const formData = new FormData();<br>        formData.append('file', file);<br><br>        const cfResponse = await fetch(uploadURL, {<br>            method: 'POST',<br>            body: formData<br>        });<br><br>        if (cfResponse.ok) {<br>            statusDiv.innerText = \"&#x2705; \uc5c5\ub85c\ub4dc \uc644\ub8cc! (\uc778\ucf54\ub529 \ub300\uae30 \uc911)\";<br>            \/\/ \uc5ec\uae30\uc11c cfResponse.headers.get(\"stream-media-id\") \ub4f1\uc73c\ub85c ID \ud655\uc778 \uac00\ub2a5<br>        } else {<br>            statusDiv.innerText = \"&#x274c; \uc5c5\ub85c\ub4dc \uc2e4\ud328\";<br>        }<br><br>    } catch (error) {<br>        console.error(error);<br>        statusDiv.innerText = \"\uc5d0\ub7ec \ubc1c\uc0dd: \" + error.message;<br>    }<br>}<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Tus \ubc29\uc2dd (Resumable Upload &#8211; \ucd94\ucc9c)<\/h2>\n\n\n\n<p><code>tus-js-client<\/code> \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4. <strong>\uc5c5\ub85c\ub4dc \uc911 \uc778\ud130\ub137\uc774 \ub04a\uaca8\ub3c4 \uc7ac\uc5f0\uacb0 \uc2dc \uba48\ucd98 \uacf3\ubd80\ud130 \uc774\uc5b4\uc62c\ub9ac\uae30\uac00 \uac00\ub2a5<\/strong>\ud558\uba70, \ub300\uc6a9\ub7c9 \ud30c\uc77c(GB \ub2e8\uc704)\ub3c4 \uc548\uc815\uc801\uc73c\ub85c \ucc98\ub9ac\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\ub77c\uc774\ube0c\ub7ec\ub9ac \ucd94\uac00 (CDN \ub610\ub294 npm):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;script src=\"https:\/\/cdn.jsdelivr.net\/npm\/tus-js-client@latest\/dist\/tus.min.js\"&gt;&lt;\/script&gt;<\/pre>\n\n\n\n<p><strong>HTML \uad6c\uc870:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;h3&gt;Tus Resumable Upload (\ub300\uc6a9\ub7c9 \uac00\ub2a5)&lt;\/h3&gt;<br>&lt;input type=\"file\" id=\"tusFileInput\" accept=\"video\/*\"&gt;<br>&lt;button onclick=\"startTusUpload()\"&gt;Tus \uc5c5\ub85c\ub4dc \uc2dc\uc791&lt;\/button&gt;<br><br>&lt;progress id=\"progressBar\" value=\"0\" max=\"100\" style=\"width:100%\"&gt;&lt;\/progress&gt;<br>&lt;div id=\"tusStatus\"&gt;0%&lt;\/div&gt;<\/pre>\n\n\n\n<p><strong>JavaScript \uad6c\ud604:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">async function startTusUpload() {<br>    const fileInput = document.getElementById('tusFileInput');<br>    const statusDiv = document.getElementById('tusStatus');<br>    const progressBar = document.getElementById('progressBar');<br>    const file = fileInput.files[0];<br><br>    if (!file) {<br>        alert(\"\ud30c\uc77c\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\");<br>        return;<br>    }<br><br>    statusDiv.innerText = \"\uc5c5\ub85c\ub4dc URL \uc694\uccad \uc911...\";<br><br>    \/\/ [Step 1] \ubc31\uc5d4\ub4dc\uc5d0\uc11c uploadURL \uac00\uc838\uc624\uae30<br>    \/\/ \uc8fc\uc758: TUS\ub97c \uc4f8 \ub54c\ub3c4 \ubcf4\uc548\uc744 \uc704\ud574 Direct Upload URL\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.<br>    const response = await fetch('\/api\/get-upload-url', { method: 'POST' });<br>    const data = await response.json();<br>    const uploadURL = data.uploadURL; <br><br>    \/\/ [Step 2] Tus \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815<br>    \/\/ uploadURL \uc790\uccb4\uac00 TUS \uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.<br>    const upload = new tus.Upload(file, {<br>        endpoint: uploadURL, <br>        retryDelays: [0, 3000, 5000, 10000, 20000], \/\/ \uc2e4\ud328 \uc2dc \uc7ac\uc2dc\ub3c4 \uac04\uaca9<br>        chunkSize: 50 * 1024 * 1024, \/\/ 50MB \ub2e8\uc704\ub85c \ucabc\uac1c\uc11c \uc804\uc1a1 (\uc11c\ubc84 \ubd80\ud558 \uc870\uc808)<br>        metadata: {<br>            filename: file.name,<br>            filetype: file.type<br>        },<br>        onError: function(error) {<br>            console.log(\"Failed because: \" + error);<br>            statusDiv.innerText = \"\uc5d0\ub7ec: \" + error;<br>        },<br>        onProgress: function(bytesUploaded, bytesTotal) {<br>            const percentage = ((bytesUploaded \/ bytesTotal) * 100).toFixed(2);<br>            progressBar.value = percentage;<br>            statusDiv.innerText = `\uc5c5\ub85c\ub4dc \uc911: ${percentage}%`;<br>        },<br>        onSuccess: function() {<br>            console.log(\"Download %s from %s\", upload.file.name, upload.url);<br>            statusDiv.innerText = \"&#x2705; \uc5c5\ub85c\ub4dc \uc131\uacf5! \ucc98\ub9ac \uc900\ube44 \uc911...\";<br>            <br>            \/\/ TUS\ub294 onSuccess \uc2dc\uc810\uc758 upload.url\uc5d0 \uc601\uc0c1 ID\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc744 \uc218 \uc788\uc73c\ubbc0\ub85c<br>            \/\/ \ubc31\uc5d4\ub4dc\uc5d0\uc11c \ubbf8\ub9ac \ubc1b\uc740 uid\ub97c \uc0ac\uc6a9\ud558\uac70\ub098, Header\ub97c \ud655\uc778\ud574\uc57c \ud569\ub2c8\ub2e4.<br>        }<br>    });<br><br>    \/\/ \uc5c5\ub85c\ub4dc \uc2dc\uc791 (\uc774\uc804\uc5d0 \uc911\ub2e8\ub41c \uc801\uc774 \uc788\ub2e4\uba74 \uc790\ub3d9\uc73c\ub85c \uc774\uc5b4\uc62c\ub9ac\uae30 \uc2dc\ub3c4)<br>    upload.findPreviousUploads().then(function (previousUploads) {<br>        if (previousUploads.length) {<br>            upload.resumeFromPreviousUpload(previousUploads[0]);<br>        }<br>        upload.start();<br>    });<br>}<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ubc29\uc2dd \ube44\uad50 \uc694\uc57d<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>\ud2b9\uc9d5<\/strong><\/td><td><strong>Simple (Fetch\/Form)<\/strong><\/td><td><strong>Tus (Resumable)<\/strong><\/td><\/tr><tr><td><strong>\uad6c\ud604 \ub09c\uc774\ub3c4<\/strong><\/td><td>\uc26c\uc6c0 (\ub77c\uc774\ube0c\ub7ec\ub9ac \ubd88\ud544\uc694)<\/td><td>\uc911\uac04 (\ub77c\uc774\ube0c\ub7ec\ub9ac \ud544\uc694)<\/td><\/tr><tr><td><strong>\ub124\ud2b8\uc6cc\ud06c \uc548\uc815\uc131<\/strong><\/td><td>\ub0ae\uc74c (\ub04a\uae30\uba74 \ucc98\uc74c\ubd80\ud130 \ub2e4\uc2dc)<\/td><td><strong>\ub192\uc74c (\uc774\uc5b4\uc62c\ub9ac\uae30 \uc9c0\uc6d0)<\/strong><\/td><\/tr><tr><td><strong>\ub300\uc6a9\ub7c9 \ud30c\uc77c<\/strong><\/td><td>\ube44\ucd94\ucc9c (\ud0c0\uc784\uc544\uc6c3 \uc704\ud5d8)<\/td><td><strong>\uac15\ub825 \ucd94\ucc9c (GB \ub2e8\uc704 \uac00\ub2a5)<\/strong><\/td><\/tr><tr><td><strong>\ucd94\ucc9c \ub300\uc0c1<\/strong><\/td><td>1\ubd84 \ubbf8\ub9cc \uc9e7\uc740 \ud074\ub9bd, \ud504\ub85c\ud544 \uc601\uc0c1<\/td><td>\uac15\uc758 \uc601\uc0c1, \uc601\ud654, \uace0\ud654\uc9c8 \uc790\ub8cc<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\">Webhook<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Cloudflare Stream\uc758 \uc778\ucf54\ub529(\ucc98\ub9ac) \uacfc\uc815\uc740 \ube44\ub3d9\uae30\uc801\uc73c\ub85c \uc77c\uc5b4\ub0a9\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc5c5\ub85c\ub4dc\uac00 \ub05d\ub09c \ud6c4 <strong>&#8220;\uc601\uc0c1\uc774 \uc7ac\uc0dd \uc900\ube44\uac00 \ub418\uc5c8\uc2b5\ub2c8\ub2e4(Ready)&#8221;<\/strong>&nbsp;\ub77c\ub294 \uc2e0\ud638\ub97c \ubc1b\uae30 \uc704\ud574 Webhook \uc11c\ubc84\uac00 \ud544\uc694\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\uac00\uc7a5 \uc911\uc694\ud55c \uc810\uc740 <strong>\ubcf4\uc548(Signature Verification)<\/strong>&nbsp;\uc785\ub2c8\ub2e4. \uc544\ubb34\ub098 \ub0b4 \uc11c\ubc84\uc5d0 &#8220;\uc601\uc0c1 \uc644\ub8cc\ub428&#8221;\uc774\ub77c\uace0 \uac00\uc9dc \uc694\uccad\uc744 \ubcf4\ub0b4\uba74 \uc548 \ub418\uae30 \ub54c\ubb38\uc5d0, Cloudflare\uac00 \ubcf4\ub0b8 \uc694\uccad\uc774 \ub9de\ub294\uc9c0 \uac80\uc99d\ud558\ub294 \ub85c\uc9c1\uc744 \ubc18\ub4dc\uc2dc \ud3ec\ud568\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc0ac\uc804 \uc900\ube44 (Webhook Secret)<\/h2>\n\n\n\n<p>\uba3c\uc800 Cloudflare Dashboard\uc5d0\uc11c Webhook\uc744 \uc0dd\uc131\ud558\uace0 \ube44\ubc00\ud0a4\ub97c \uc5bb\uc5b4\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>Request \uc608\uc2dc (cURL):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">curl -X PUT --header 'Authorization: Bearer &lt;API_TOKEN&gt;' \\<br>https:\/\/api.cloudflare.com\/client\/v4\/accounts\/&lt;ACCOUNT_ID&gt;\/stream\/webhook \\<br>--data '{\"notificationUrl\":\"&lt;WEBHOOK_NOTIFICATION_URL&gt;\"}'<\/pre>\n\n\n\n<p><strong>Response (\uc131\uacf5 \uc2dc):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"result\": {<br>    \"notificationUrl\": \"http:\/\/www.your-service-webhook-handler.com\",<br>    \"modified\": \"2019-01-01T01:02:21.076571Z\"<br>    \"secret\": \"85011ed3a913c6ad5f9cf6c5573cc0a7\"<br>  },<br>  \"success\": true,<br>  \"errors\": [],<br>  \"messages\": []<br>}<\/pre>\n\n\n\n<p><strong>notificationURL<\/strong>: \ub0b4 \ubc31\uc5d4\ub4dc API \uc8fc\uc18c (\uc608: <code>https:\/\/api.mydomain.com\/webhook\/stream<\/code>)<\/p>\n\n\n\n<p><strong>\uc751\ub2f5\uc73c\ub85c \ubc1b\uc740 secret<\/strong> (\uc608:&nbsp;<code>85011ed3a913c6ad5f9cf6c5573cc0a7<\/code>)\uc744 \ubcf5\uc0ac\ud574 \ub461\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Node.js (Express) \uc0d8\ud50c \ucf54\ub4dc<\/h2>\n\n\n\n<p>Express\ub97c \uc0ac\uc6a9\ud560 \uacbd\uc6b0, \uc11c\uba85 \uac80\uc99d\uc744 \uc704\ud574 <strong>Raw Body(\uac00\uacf5\ub418\uc9c0 \uc54a\uc740 \uc6d0\ubcf8 \ub370\uc774\ud130)<\/strong>&nbsp;\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uc544\ub798 \ucf54\ub4dc\ub294 \uc774\ub97c \ucc98\ub9ac\ud558\ub294 \ubbf8\ub4e4\uc6e8\uc5b4 \uc124\uc815\uc774 \ud3ec\ud568\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc124\uce58:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">npm install express dotenv<\/pre>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>webhook.js<\/code>):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">require('dotenv').config();<br>const express = require('express');<br>const crypto = require('crypto');<br><br>const app = express();<br>const WEBHOOK_SECRET = process.env.CF_STREAM_WEBHOOK_SECRET; \/\/ .env\uc5d0\uc11c \uac00\uc838\uc634<br><br>\/\/ &#x26a0;&#xfe0f; \uc911\uc694: \uc11c\uba85 \uac80\uc99d\uc744 \uc704\ud574 Raw Body\ub97c \ubcf4\uc874\ud574\uc57c \ud569\ub2c8\ub2e4.<br>app.use(express.json({<br>  verify: (req, res, buf) =&gt; {<br>    req.rawBody = buf;<br>  }<br>}));<br><br>app.post('\/webhook\/stream', (req, res) =&gt; {<br>  const signatureHeader = req.headers['webhook-signature'];<br>  <br>  if (!signatureHeader) {<br>    return res.status(400).send('Missing Signature Header');<br>  }<br><br>  \/\/ 1. \ud5e4\ub354 \ud30c\uc2f1 (time\uacfc signature \ubd84\ub9ac)<br>  \/\/ \ud5e4\ub354 \ud615\uc2dd \uc608: \"time=1234567890,sig1=abcd...\"<br>  const elements = signatureHeader.split(',');<br>  const time = elements.find(e =&gt; e.startsWith('time=')).split('=')[1];<br>  const sig1 = elements.find(e =&gt; e.startsWith('sig1=')).split('=')[1];<br><br>  \/\/ 2. \uac80\uc99d\uc6a9 \ubb38\uc790\uc5f4 \uc0dd\uc131<br>  const stringToSign = `${time}.${req.rawBody}`;<br><br>  \/\/ 3. HMAC SHA-256 \ud574\uc2dc \uc0dd\uc131<br>  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);<br>  const digest = hmac.update(stringToSign).digest('hex');<br><br>  \/\/ 4. \uc11c\uba85 \ube44\uad50 (Timing Attack \ubc29\uc9c0\ub97c \uc704\ud574 timingSafeEqual \uc0ac\uc6a9 \uad8c\uc7a5)<br>  \/\/ \uc5ec\uae30\uc11c\ub294 \uac04\ub2e8\ud788 \ubb38\uc790\uc5f4 \ube44\uad50\ub85c \ucc98\ub9ac\ud558\uc9c0\ub9cc, \uc2e4\ubb34\uc5d0\uc120 crypto.timingSafeEqual \uc0ac\uc6a9 \ucd94\ucc9c<br>  if (digest !== sig1) {<br>    console.error('&#x274c; \uc11c\uba85 \ubd88\uc77c\uce58! \uc704\uc870\ub41c \uc694\uccad\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.');<br>    return res.status(401).send('Invalid Signature');<br>  }<br><br>  \/\/ 5. \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1 \ucc98\ub9ac<br>  const event = req.body;<br>  const uid = event.uid;<br>  const status = event.status.state; \/\/ 'ready', 'queued', 'inprogress' \ub4f1<br><br>  console.log(`&#x1f514; Webhook \uc218\uc2e0: Video [${uid}] is [${status}]`);<br><br>  if (status === 'ready') {<br>    \/\/ &#x2705; DB \uc5c5\ub370\uc774\ud2b8 \ub85c\uc9c1 (\uc608\uc2dc)<br>    \/\/ updateVideoStatus(uid, 'ACTIVE');<br>    console.log(`&gt;&gt; DB \uc5c5\ub370\uc774\ud2b8 \uc644\ub8cc: \uc601\uc0c1(${uid})\uc774 \ud65c\uc131\ud654\ub418\uc5c8\uc2b5\ub2c8\ub2e4.`);<br>  }<br><br>  \/\/ Cloudflare\uc5d0\uac8c \uc798 \ubc1b\uc558\ub2e4\uace0 \uc751\ub2f5 (200 OK)<br>  res.status(200).send('OK');<br>});<br><br>app.listen(3000, () =&gt; {<br>  console.log('Server running on port 3000');<br>});<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Python (Flask) \uc0d8\ud50c \ucf54\ub4dc<\/h2>\n\n\n\n<p>Python\uc740 <code>hmac<\/code> \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc11c\uba85\uc744 \uac80\uc99d\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc124\uce58:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pip install flask python-dotenv<\/pre>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>webhook.py<\/code>):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import os<br>import hmac<br>import hashlib<br>from flask import Flask, request, jsonify<br>from dotenv import load_dotenv<br><br>load_dotenv()<br><br>app = Flask(__name__)<br>WEBHOOK_SECRET = os.getenv(\"CF_STREAM_WEBHOOK_SECRET\")<br><br>@app.route('\/webhook\/stream', methods=['POST'])<br>def handle_stream_webhook():<br>    signature_header = request.headers.get('Webhook-Signature')<br>    <br>    if not signature_header:<br>        return \"Missing Signature\", 400<br><br>    # 1. \ud5e4\ub354 \ud30c\uc2f1<br>    # \"time=12345,sig1=abcd\" \ud615\ud0dc\ub97c \ub515\uc154\ub108\ub9ac\ub85c \ubcc0\ud658<br>    parts = {k: v for k, v in [x.split('=') for x in signature_header.split(',')]}<br>    timestamp = parts.get('time')<br>    signature = parts.get('sig1')<br><br>    # 2. \uac80\uc99d\uc6a9 \ubb38\uc790\uc5f4 \uc0dd\uc131 (\ubc1b\uc740 \uadf8\ub300\ub85c\uc758 bytes \ub370\uc774\ud130 \ud544\uc694)<br>    request_body = request.get_data()<br>    string_to_sign = f\"{timestamp}.\".encode('utf-8') + request_body<br><br>    # 3. HMAC SHA-256 \ud574\uc2dc \uc0dd\uc131<br>    generated_sig = hmac.new(<br>        WEBHOOK_SECRET.encode('utf-8'),<br>        string_to_sign,<br>        hashlib.sha256<br>    ).hexdigest()<br><br>    # 4. \uc11c\uba85 \ube44\uad50<br>    if not hmac.compare_digest(generated_sig, signature):<br>        print(\"&#x274c; \uc11c\uba85 \ubd88\uc77c\uce58! \uc704\uc870\ub41c \uc694\uccad\uc785\ub2c8\ub2e4.\")<br>        return \"Invalid Signature\", 401<br><br>    # 5. \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1 \ucc98\ub9ac<br>    data = request.json<br>    uid = data.get('uid')<br>    status = data.get('status', {}).get('state')<br><br>    print(f\"&#x1f514; Webhook \uc218\uc2e0: Video [{uid}] is [{status}]\")<br><br>    if status == 'ready':<br>        # &#x2705; DB \uc5c5\ub370\uc774\ud2b8 \ub85c\uc9c1 \uc218\ud589<br>        # db.update_status(uid, 'active')<br>        print(f\"&gt;&gt; DB \uc5c5\ub370\uc774\ud2b8 \uc644\ub8cc: \uc601\uc0c1({uid}) \uc7ac\uc0dd \uac00\ub2a5\")<br><br>    return \"OK\", 200<br><br>if __name__ == '__main__':<br>    app.run(port=3000)<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc694\uc57d<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ubcf4\uc548<\/strong>: <code>Webhook-Signature<\/code> \ud5e4\ub354\uc640 <code>Raw Body<\/code>\ub97c \uc870\ud569\ud574 \ud574\uc2dc\ub97c \ub9cc\ub4e4\uace0, Secret\uacfc \ub300\uc870\ud558\uc5ec <strong>\uc704\ubcc0\uc870\ub97c \ubc29\uc9c0<\/strong>\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc774\ubca4\ud2b8<\/strong>: <code>status.state<\/code>\uac00 <code>ready<\/code>\uac00 \ub418\uc5c8\uc744 \ub54c DB\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uc5ec \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc601\uc0c1\uc744 \ub178\ucd9c\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc751\ub2f5<\/strong>: \uc11c\ubc84\ub294 \ucc98\ub9ac\uac00 \ub05d\ub098\uba74 \ubc18\ub4dc\uc2dc <strong>200 OK<\/strong>\ub97c \ub9ac\ud134\ud574\uc57c Cloudflare\uac00 \uc7ac\uc804\uc1a1(Retry)\uc744 \ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">\uc5d0\ub7ec \ub300\uc751<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\uc6b4\uc601 \ud658\uacbd\uc5d0\uc11c &#8220;\uc131\uacf5\ud558\ub294 \ucf00\uc774\uc2a4&#8221;\ub9cc \uace0\ub824\ud558\uba74 \ub098\uc911\uc5d0 \ud070 \ub0ad\ud328\ub97c \ubd05\ub2c8\ub2e4. \ub3d9\uc601\uc0c1 \uc11c\ube44\uc2a4\ub294 \ud30c\uc77c \ud06c\uae30\uac00 \ud06c\uace0 \ub124\ud2b8\uc6cc\ud06c \ubcc0\uc218\uac00 \ub9ce\uc544 <strong>\ubc29\uc5b4\uc801\uc778 \ucf54\ub529(Defensive Programming)<\/strong>&nbsp;\uc774 \ud544\uc218\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ud06c\uac8c <strong>1. \uc5c5\ub85c\ub4dc \uc911 \uc2e4\ud328 (Frontend)<\/strong>&nbsp;\uc640 <strong>2. \uc778\ucf54\ub529\/\ucc98\ub9ac \uc911 \uc2e4\ud328 (Backend Webhook)<\/strong> \ub450 \uac00\uc9c0 \uc2dc\uc810\uc73c\ub85c \ub098\ub204\uc5b4 \ub300\uc751 \ub85c\uc9c1\uc744 \uc815\ub9ac\ud574 \ub4dc\ub9bd\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud504\ub860\ud2b8\uc5d4\ub4dc: \uc5c5\ub85c\ub4dc \uc911\ub2e8 \ubc0f \uc2e4\ud328 \ub300\uc751<\/h2>\n\n\n\n<p>\uc0ac\uc6a9\uc790\uac00 \ube0c\ub77c\uc6b0\uc800\ub97c \ub2eb\uac70\ub098, \uc640\uc774\ud30c\uc774\uac00 \ub04a\uae30\uac70\ub098, \ud30c\uc77c\uc774 \uaddc\uaca9\uc5d0 \ub9de\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. TUS \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \uc7ac\uc2dc\ub3c4(Retry) \uc635\uc158 \ud65c\uc6a9<\/h3>\n\n\n\n<p>\uc55e\uc11c \uc791\uc131\ud55c TUS \ucf54\ub4dc\uc5d0 \uc7ac\uc2dc\ub3c4 \ub85c\uc9c1\uc774 \uc774\ubbf8 \ud3ec\ud568\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c \uc0ac\uc6a9\uc790\uc5d0\uac8c <strong>&#8220;\ubb34\uc5c7\uc774 \uc798\ubabb\ub418\uc5c8\ub294\uc9c0&#8221;<\/strong> \uba85\ud655\ud788 \uc54c\ub824\uc8fc\ub294 UX \ucc98\ub9ac\uac00 \uc911\uc694\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">const upload = new tus.Upload(file, {<br>    endpoint: uploadURL,<br>    retryDelays: [0, 1000, 3000, 5000], \/\/ \ub124\ud2b8\uc6cc\ud06c \uc77c\uc2dc \ub2e8\uc808 \uc2dc 4\ubc88 \uc7ac\uc2dc\ub3c4<br>    onError: function(error) {<br>        console.error(\"Upload Failed:\", error);<br>        <br>        \/\/ &#x1f6a8; UX \ub300\uc751: \uc5d0\ub7ec \uba54\uc2dc\uc9c0\ub97c \ubd84\uc11d\ud558\uc5ec \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc548\ub0b4<br>        if (error.originalRequest &amp;&amp; error.originalRequest.status === 413) {<br>            alert(\"\ud30c\uc77c\uc774 \ub108\ubb34 \ud07d\ub2c8\ub2e4. (\ucd5c\ub300 \uc6a9\ub7c9 \ucd08\uacfc)\");<br>        } else if (error.message.includes(\"NetworkError\")) {<br>            alert(\"\ub124\ud2b8\uc6cc\ud06c \uc5f0\uacb0\uc774 \ubd88\uc548\uc815\ud569\ub2c8\ub2e4. \uc7a0\uc2dc \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.\");<br>        } else {<br>            alert(\"\uc5c5\ub85c\ub4dc \uc911 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uace0\uce68 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.\");<br>        }<br>        <br>        \/\/ UI \uc0c1\ud0dc \uc5c5\ub370\uc774\ud2b8: '\uc7ac\uc2dc\ub3c4 \ubc84\ud2bc' \ub178\ucd9c<br>        document.getElementById('retryBtn').style.display = 'block';<br>    },<br>    \/\/ ... \uae30\ud0c0 \uc124\uc815<br>});<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">B. \ube0c\ub77c\uc6b0\uc800 \uc774\ud0c8 \ubc29\uc9c0 (<code>beforeunload<\/code>)<\/h3>\n\n\n\n<p>\uc0ac\uc6a9\uc790\uac00 \uc5c5\ub85c\ub4dc \uc911\uc5d0 \uc2e4\uc218\ub85c \ud0ed\uc744 \ub2eb\uac70\ub098 \ub4a4\ub85c \uac00\uae30\ub97c \ub204\ub974\ub294 \uac83\uc744 \ubc29\uc9c0\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">window.addEventListener(\"beforeunload\", function (e) {<br>    \/\/ \uc5c5\ub85c\ub4dc\uac00 \uc9c4\ud589 \uc911\uc77c \ub54c\ub9cc \uacbd\uace0<br>    if (isUploading) { <br>        e.preventDefault();<br>        e.returnValue = \"\"; \/\/ Chrome\uc5d0\uc11c\ub294 \ud45c\uc900 \uba54\uc2dc\uc9c0\uac00 \ud45c\uc2dc\ub428<br>    }<br>});<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ubc31\uc5d4\ub4dc: \uc778\ucf54\ub529 \uc2e4\ud328 \ub300\uc751 (Webhook \ud655\uc7a5)<\/h2>\n\n\n\n<p>\uc5c5\ub85c\ub4dc\ub294 \uc131\uacf5\ud588\ub294\ub370, Cloudflare \ub0b4\ubd80\uc5d0\uc11c \uc778\ucf54\ub529\ud558\ub2e4\uac00 \uc8fd\ub294 \uacbd\uc6b0\uc785\ub2c8\ub2e4. (\uc608: \ud30c\uc77c\uc774 \uae68\uc838\uc788\uac70\ub098, \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \ucf54\ub371\uc778 \uacbd\uc6b0).<\/p>\n\n\n\n<p>\uc774 \uacbd\uc6b0 Cloudflare\ub294 <code>status.state<\/code> \uac12\uc744 <code>error<\/code>\ub85c \ud574\uc11c Webhook\uc744 \ubcf4\ub0c5\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Webhook \ud578\ub4e4\ub7ec \uc218\uc815 (Node.js \uc608\uc2dc)<\/h3>\n\n\n\n<p>\uae30\uc874 <code>ready<\/code> \uc0c1\ud0dc\ub9cc \uccb4\ud06c\ud558\ub358 \ub85c\uc9c1\uc5d0 <code>error<\/code> \ucc98\ub9ac\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ ... (\uc11c\uba85 \uac80\uc99d \ub85c\uc9c1\uc740 \ub3d9\uc77c) ...<br><br>\/\/ 5. \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1 \ucc98\ub9ac (\ud655\uc7a5\ub428)<br>const event = req.body;<br>const uid = event.uid;<br>const status = event.status.state; \/\/ 'ready', 'queued', 'error' \ub4f1<br>const errReason = event.status.errorReasonCode || event.status.errorReasonText; \/\/ \uc5d0\ub7ec \uc0c1\uc138 \uc0ac\uc720<br><br>console.log(`&#x1f514; Webhook: Video [${uid}] status is [${status}]`);<br><br>if (status === 'ready') {<br>    \/\/ [\uc131\uacf5] \uc11c\ube44\uc2a4 \ud65c\uc131\ud654<br>    await db.updateVideoStatus(uid, 'ACTIVE'); <br><br>} else if (status === 'error') {<br>    \/\/ [\uc2e4\ud328] &#x1f6a8; \uc5ec\uae30\uac00 \ud575\uc2ec\uc785\ub2c8\ub2e4.<br>    console.error(`&#x274c; \uc778\ucf54\ub529 \uc2e4\ud328! UID: ${uid}, Reason: ${errReason}`);<br>    <br>    \/\/ 1. DB \uc0c1\ud0dc\ub97c 'FAILED'\ub85c \ubcc0\uacbd (\uc0ac\uc6a9\uc790\uc5d0\uac8c \"\ucc98\ub9ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\" \ud45c\uc2dc\uc6a9)<br>    await db.updateVideoStatus(uid, 'FAILED', errReason);<br>    <br>    \/\/ 2. \uad00\ub9ac\uc790(\uc6b4\uc601\ud300)\uc5d0\uac8c \uc54c\ub9bc \ubc1c\uc1a1 (Slack, Email \ub4f1)<br>    \/\/ sendSlackAlert(`\uc601\uc0c1 \uc778\ucf54\ub529 \uc2e4\ud328: ${uid} - ${errReason}`);<br><br>} else if (status === 'downloading') {<br>    \/\/ (URL\ub85c \uc5c5\ub85c\ub4dc \uc2dc) \ub2e4\uc6b4\ub85c\ub4dc \uc911<br>    await db.updateVideoStatus(uid, 'PROCESSING');<br>}<br><br>res.status(200).send('OK');<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. &#8220;\uc548\uc804\ub9dd&#8221; \uad6c\ucd95: Polling (Cron Job)<\/h2>\n\n\n\n<p>Webhook\uc740 99.9% \uc2e0\ub8b0\ud560 \uc218 \uc788\uc9c0\ub9cc, <strong>\ub0b4 \uc11c\ubc84\uac00 \uc810\uac80 \uc911\uc774\uac70\ub098 \ub2e4\uc6b4\ub418\uc5c8\uc744 \ub54c<\/strong> Webhook\uc744 \ub193\uce60 \uc218 \uc788\uc2b5\ub2c8\ub2e4. Cloudflare\uac00 \uba87 \ubc88 \uc7ac\uc2dc\ub3c4\ud558\uae34 \ud558\uc9c0\ub9cc, \uc601\uc6d0\ud788 \uc7ac\uc2dc\ub3c4\ud558\uc9c4 \uc54a\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ub530\ub77c\uc11c <strong>&#8220;\uace0\uc544 \uc0c1\ud0dc(Stuck)&#8221;<\/strong>&nbsp;\uac00 \ub41c \uc601\uc0c1\uc744 \uad6c\uc81c\ud558\uae30 \uc704\ud574 \uc8fc\uae30\uc801\uc778 \ubc30\uce58 \uc791\uc5c5(Cron)\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\ub85c\uc9c1 \uc124\uba85:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>DB\uc5d0\uc11c \uc5c5\ub85c\ub4dc\ud55c \uc9c0 <strong>30\ubd84\uc774 \uc9c0\ub0ac\ub294\ub370\ub3c4 \uc5ec\uc804\ud788 <code>Processing<\/code> \uc0c1\ud0dc<\/strong>\uc778 \uc601\uc0c1\ub4e4\uc744 \ucc3e\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li>Cloudflare API\ub85c \ud574\ub2f9 \uc601\uc0c1\ub4e4\uc758 \ud604\uc7ac \uc0c1\ud0dc\ub97c \uc870\ud68c\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li>\uc0c1\ud0dc\uac00 <code>ready<\/code>\ub098 <code>error<\/code>\ub85c \ubcc0\ud574\uc788\ub2e4\uba74 DB\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<p><strong>Python (Scheduler) \uc608\uc2dc:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import requests<br># \uac00\uc0c1\uc758 DB \ubaa8\ub4c8<br>from myapp.models import Video <br><br>def check_stuck_videos():<br>    # 1. 30\ubd84 \ub118\uac8c \ucc98\ub9ac \uc911\uc778 \uc601\uc0c1 \uc870\ud68c<br>    stuck_videos = Video.objects.filter(status='PROCESSING', created_at__lte=time_30_mins_ago)<br><br>    for video in stuck_videos:<br>        # 2. Cloudflare API\ub85c \uc0c1\ud0dc \uc870\ud68c<br>        url = f\"https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{ACCOUNT_ID}\/stream\/{video.uid}\"<br>        resp = requests.get(url, headers=HEADERS)<br>        <br>        if resp.status_code == 200:<br>            cf_data = resp.json()['result']<br>            current_state = cf_data['status']['state'] # ready, error \ub4f1<br><br>            # 3. \uc0c1\ud0dc \ub3d9\uae30\ud654<br>            if current_state == 'ready':<br>                video.status = 'ACTIVE'<br>                video.save()<br>                print(f\"\ubcf5\uad6c\ub428: {video.uid} -&gt; ACTIVE\")<br>            elif current_state == 'error':<br>                video.status = 'FAILED'<br>                video.save()<br>                print(f\"\ubcf5\uad6c\ub428: {video.uid} -&gt; FAILED\")<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. \uc5d0\ub7ec \ubc1c\uc0dd \uc2dc \uc0ac\uc6a9\uc790 \uacbd\ud5d8 (UX) \uc2dc\ub098\ub9ac\uc624<\/h2>\n\n\n\n<p>\uac1c\ubc1c\uc790\uac00 \uc544\ub2cc <strong>\ucd5c\uc885 \uc0ac\uc6a9\uc790(\uace0\uac1d)<\/strong> \uc785\uc7a5\uc5d0\uc11c\uc758 \uc2dc\ub098\ub9ac\uc624\ub294 \ub2e4\uc74c\uacfc \uac19\uc544\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\uc5c5\ub85c\ub4dc \uc2e4\ud328 \uc2dc (Frontend):<\/strong>\n<ul class=\"wp-block-list\">\n<li>&#8220;\ub124\ud2b8\uc6cc\ud06c \ubb38\uc81c\ub85c \uc5c5\ub85c\ub4dc\uac00 \uc911\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4. [\uc774\uc5b4\uc62c\ub9ac\uae30] \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.&#8221; (TUS \ud65c\uc6a9)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>\uc778\ucf54\ub529 \uc2e4\ud328 \uc2dc (Backend):<\/strong>\n<ul class=\"wp-block-list\">\n<li>Admin \ud398\uc774\uc9c0 \ub9ac\uc2a4\ud2b8\uc5d0 <strong>[\ucc98\ub9ac \uc2e4\ud328 &#x26a0;&#xfe0f;]<\/strong> \ub77c\ubca8 \ud45c\uc2dc.<\/li>\n\n\n\n<li>\ub9c8\uc6b0\uc2a4 \uc624\ubc84 \uc2dc &#8220;\ud30c\uc77c \ud615\uc2dd\uc774 \uc190\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc778\ucf54\ub529\ud574\uc11c \uc62c\ub824\uc8fc\uc138\uc694.&#8221; \ud234\ud301 \uc81c\uacf5.<\/li>\n\n\n\n<li>\ud574\ub2f9 \uc601\uc0c1\uc740 \ud50c\ub808\uc774\uc5b4\uc5d0\uc11c \uc7ac\uc0dd \uc2dc\ub3c4 \uc790\uccb4\ub97c \ub9c9\uc544\uc57c \ud568.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc694\uc57d \uccb4\ud06c\ub9ac\uc2a4\ud2b8<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>&#x2705; <strong>Frontend<\/strong>: <code>tus.onError<\/code> \ud578\ub4e4\ub7ec\uc5d0\uc11c \uc5d0\ub7ec \uc885\ub958\ubcc4 \uba54\uc2dc\uc9c0 \ubd84\uae30 \ucc98\ub9ac.<\/li>\n\n\n\n<li>&#x2705; <strong>Frontend<\/strong>: <code>beforeunload<\/code> \uc774\ubca4\ud2b8\ub85c \uc2e4\uc218\ub85c \ud0ed \ub2eb\uae30 \ubc29\uc9c0.<\/li>\n\n\n\n<li>&#x2705; <strong>Backend<\/strong>: Webhook\uc5d0\uc11c <code>status === 'error'<\/code> \uc77c \ub54c DB \uc5c5\ub370\uc774\ud2b8 \ubc0f \uad00\ub9ac\uc790 \uc54c\ub9bc.<\/li>\n\n\n\n<li>&#x2705; <strong>Backend<\/strong>: Webhook \ub204\ub77d \ub300\ube44\uc6a9 <strong>\uc2a4\ucf00\uc904\ub7ec(Cron)<\/strong> \uad6c\ud604.<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">Singed URL<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Cloudflare Stream\uc5d0\uc11c \ubcf4\uc548\uc774 \ud544\uc694\ud55c \uc601\uc0c1(\uc720\ub8cc \uac15\uc758, \uc0ac\ub0b4 \uad50\uc721 \uc790\ub8cc \ub4f1)\uc740 <strong>Signed URL(\uc11c\uba85\ub41c URL)<\/strong> \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\uc774 \ubc29\uc2dd\uc758 \ud575\uc2ec\uc740 <strong>&#8220;\ub0b4 \uc11c\ubc84\uac00 \ubc1c\uae09\ud55c &#8216;\ucd9c\uc785\uc99d(Token)&#8217;\uc744 \uac00\uc9c4 \uc0ac\ub78c\ub9cc \uc601\uc0c1\uc744 \ubcfc \uc218 \uc788\ub2e4&#8221;<\/strong>&nbsp;\ub294 \uac83\uc785\ub2c8\ub2e4. \ucd9c\uc785\uc99d\uc5d0\ub294 \uc720\ud6a8\uae30\uac04(\uc608: 1\uc2dc\uac04)\uacfc \ud2b9\uc815 \uaddc\uce59(\uc608: \ud2b9\uc815 IP\ub9cc \ud5c8\uc6a9)\uc744 \uc2ec\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc601\uc0c1 \uc7a0\uadf8\uae30 (Locking the Video)<\/h2>\n\n\n\n<p>\uc601\uc0c1\uc744 \uc5c5\ub85c\ub4dc\ud560 \ub54c\ub098 \uc5c5\ub85c\ub4dc \ud6c4\uc5d0 &#8220;\uc774 \uc601\uc0c1\uc740 \ud1a0\ud070\uc774 \ud544\uc694\ud574&#8221;\ub77c\uace0 \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uc5c5\ub85c\ub4dc \uc2dc<\/strong>: <code>requireSignedURLs: true<\/code> \uc124\uc815 (\uc55e\uc11c \uc5c5\ub85c\ub4dc API \uc124\uba85 \ucc38\uc870)<\/li>\n\n\n\n<li><strong>\uc5c5\ub85c\ub4dc \ud6c4 (API)<\/strong>:curl -X POST https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{ACCOUN_ID}\/stream\/{uid} \\<br>&#8230; -d &#8216;{&#8220;requireSignedURLs&#8221;: true}&#8217;<\/li>\n<\/ul>\n\n\n\n<p>\uc774 \uc124\uc815\uc774 \ucf1c\uc9c0\uba74, \uae30\uc874\uc758 \uacf5\uac1c URL\ub85c \uc811\uc18d \uc2dc <code>403 Forbidden<\/code> \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc635\uc158 1: \/token \uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc0ac\uc6a9\ud558\uae30<\/h2>\n\n\n\n<p>\ud14c\uc2a4\ud2b8 \ubaa9\uc801 \ub610\ub294 \ud558\ub8e8\uc5d0 1000\uac1c \uc774\ud558\uc758 \ud1a0\ud070\uc744 \uc0dd\uc131\ud560 \ub54c\ub294 \uc774 \ubc29\ubc95\uc744 \ucd94\ucc9c\ud569\ub2c8\ub2e4.&nbsp;<\/p>\n\n\n\n<p>\ud1a0\ud070\uc744 \ub9cc\ub4e4 \ub54c\ub9c8\ub2e4 Cloudflare API \ucf5c\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ud1a0\ud070\uc758 \uae30\ubcf8 \uc720\ud6a8\uc2dc\uac04\uc740 1\uc2dc\uac04\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">cURL \uc608\uc81c<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">curl --request POST \\<br>https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{account_id}\/stream\/{video_uid}\/token \\<br>--header \"Authorization: Bearer &lt;API_TOKEN&gt;\"<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\uc751\ub2f5 \uc608:<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"result\": {<br>    \"token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ\"<br>  },<br>  \"success\": true,<br>  \"errors\": [],<br>  \"messages\": []<br>}<\/pre>\n\n\n\n<p><code>token<\/code> \uac12\uc744 \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0 \uc804\ub2ec\ud55c\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc635\uc1582: \uc11c\uba85 \ud0a4 \uc0ac\uc6a9\ud558\uae30<\/h2>\n\n\n\n<p>\uc11c\uba85\ud0a4\ub97c \uc0ac\uc6a9\ud558\uba74 \ub9e4\ubc88 \uc2a4\ud2b8\ub9bc API\ub97c \ucf5c \ud560 \ud544\uc694\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step1: \/Strean\/key \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc5d0\uc11c \ud0a4 \uc5bb\uae30<\/h3>\n\n\n\n<p><strong>cURL \uc608\uc81c:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">curl --request POST \\<br>\"https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{account_id}\/stream\/keys\" \\<br>--header \"Authorization: Bearer &lt;API_TOKEN&gt;\"<\/pre>\n\n\n\n<p><strong>Response \uc608:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"result\": {<br>    \"id\": \"8f926b2b01f383510025a78a4dcbf6a\",<br>    \"pem\": \"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBemtHbXhCekFGMnBIMURiWmgyVGoyS3ZudlBVTkZmUWtNeXNCbzJlZzVqemRKTmRhCmtwMEphUHhoNkZxOTYveTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgrYkR3TEdTVldGMEx3QnloMDYKN01Rb0xySHA3MDEycXBVNCtLODUyT1hMRVVlWVBrOHYzRlpTQ2VnMVdLRW5URC9oSmhVUTFsTmNKTWN3MXZUbQpHa2o0empBUTRBSFAvdHFERHFaZ3lMc1Vma2NsRDY3SVRkZktVZGtFU3lvVDVTcnFibHNFelBYcm9qaFlLWGk3CjFjak1yVDlFS0JCenhZSVEyOVRaZitnZU5ya0t4a2xMZTJzTUFML0VWZkFjdGkrc2ZqMkkyeEZKZmQ4aklmL2UKdHBCSVJZVDEza2FLdHUyYmk0R2IrV1BLK0toQjdTNnFGODlmTHdJREFRQUJBb0lCQUYzeXFuNytwNEtpM3ZmcgpTZmN4ZmRVV0xGYTEraEZyWk1mSHlaWEFJSnB1MDc0eHQ2ZzdqbXM3Tm0rTFVhSDV0N3R0bUxURTZacy91RXR0CjV3SmdQTjVUaFpTOXBmMUxPL3BBNWNmR2hFN1pMQ2wvV2ZVNXZpSFMyVDh1dGlRcUYwcXpLZkxCYk5kQW1MaWQKQWl4blJ6UUxDSzJIcmlvOW1KVHJtSUUvZENPdG80RUhYdHpZWjByOVordHRxMkZrd3pzZUdaK0tvd09JaWtvTgp2NWFOMVpmRGhEVG0wdG1Vd0tLbjBWcmZqalhRdFdjbFYxTWdRejhwM2xScWhISmJSK29PL1NMSXZqUE16dGxOCm5GV1ZEdTRmRHZsSjMyazJzSllNL2tRVUltT3V5alY3RTBBcm5vR2lBREdGZXFxK1UwajluNUFpNTJ6aTBmNloKdFdvwdju39xOFJWQkwxL2tvWFVmYk00S04ydVFadUdjaUdGNjlCRDJ1S3o1eGdvTwowVTBZNmlFNG9Cek5GUW5hWS9kayt5U1dsQWp2MkgraFBrTGpvZlRGSGlNTmUycUVNaUFaeTZ5cmRkSDY4VjdIClRNRllUQlZQaHIxT0dxZlRmc00vRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE1DZ1lFQTFQRVkKbGIybDU4blVianRZOFl6Uk1vQVo5aHJXMlhwM3JaZjE0Q0VUQ1dsVXFZdCtRN0NyN3dMQUVjbjdrbFk1RGF3QgpuTXJsZXl3S0crTUEvU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUythWWljemJsYmJqU0RqWXVjCkdSNzIrb1FlMzJjTXhjczJNRlBWcHVibjhjalBQbnZKd0k5aUpGVUNnWUVBMjM3UmNKSEdCTjVFM2FXLzd3ekcKbVBuUm1JSUczeW9UU0U3OFBtbHo2bXE5eTVvcSs5aFpaNE1Fdy9RbWFPMDF5U0xRdEY4QmY2TFN2RFh4QWtkdwpWMm5ra0svWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoCkplcGkvZFhRWFBWeFoxYXV4YldGL3VzQ2dZRUFxWnhVVWNsYVlYS2dzeUN3YXM0WVAxcEwwM3h6VDR5OTBOYXUKY05USFhnSzQvY2J2VHFsbGVaNCtNSzBxcGRmcDM5cjIrZFdlemVvNUx4YzBUV3Z5TDMxVkZhT1AyYk5CSUpqbwpVbE9ldFkwMitvWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyClNLYXNySFVDZ1lCYmRvL1orN1M3dEZSaDZlamJib2h3WGNDRVd4eXhXT2ZMcHdXNXdXT3dlWWZwWTh4cm5pNzQKdGRObHRoRXM4SHhTaTJudEh3TklLSEVlYmJ4eUh1UG5pQjhaWHBwNEJRNTYxczhjR1Z1ZSszbmVFUzBOTDcxZApQL1ZxUWpySFJrd3V5ckRFV2VCeEhUL0FvVEtEeSt3OTQ2SFM5V1dPTGJvbXQrd3g0NytNdWc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=\",<br>    \"jwk\": \"eyJ1c2UiOiJzaWciLCJrdHkiOiJSU0EiLCJraWQiOiI4ZjkyNmIyYjAxZjM4MzUxNzAwMjVhNzhhNGRjYmY2YSIsImFsZyI6IlJTMjU2IiwibiI6InprR214QnpBRjJwSDFEYlpoMlRqMkt2bnZQVU5GZlFrTXlzQm8yZWc1anpkSk5kYWtwMEphUHhoNkZxOTZfeTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgtYkR3TEdTVldGMEx3QnloMDY3TVFvTHJIcDcwMTJxcFU0LUs4NTJPWExFVWVZUGs4djNGWlNDZWcxV0tFblREX2hKaFVRMWxOY0pNY3cxdlRtR2tqNHpqQVE0QUhQX3RxRERxWmd5THNVZmtjbEQ2N0lUZGZLVWRrRVN5b1Q1U3JxYmxzRXpQWHJvamhZS1hpNzFjak1yVDlFS0JCenhZSVEyOVRaZi1nZU5ya0t4a2xMZTJzTUFMX0VWZkFjdGktc2ZqMkkyeEZKZmQ4aklmX2V0cEJJUllUMTNrYUt0dTJiaTRHYi1XUEstS2hCN1M2cUY4OWZMdyIsImUiOiJBUUFCIiwiZCI6IlhmS3FmdjZuZ3FMZTktdEo5ekY5MVJZc1ZyWDZFV3RreDhmSmxjQWdtbTdUdmpHM3FEdU9henMyYjR0Um9mbTN1MjJZdE1UcG16LTRTMjNuQW1BODNsT0ZsTDJsX1VzNy1rRGx4OGFFVHRrc0tYOVo5VG0tSWRMWlB5NjJKQ29YU3JNcDhzRnMxMENZdUowQ0xHZEhOQXNJcllldUtqMllsT3VZZ1Q5MEk2MmpnUWRlM05oblN2MW42MjJyWVdURE94NFpuNHFqQTRpS1NnMl9sbzNWbDhPRU5PYlMyWlRBb3FmUld0LU9OZEMxWnlWWFV5QkRQeW5lVkdxRWNsdEg2Zzc5SXNpLU04ek8yVTJjVlpVTzdoOE8tVW5mYVRhd2xnei1SQlFpWTY3S05Yc1RRQ3VlZ2FJQU1ZVjZxcjVUU1Ai2odx5iT0xSX3BtMWFpdktyUSIsInAiOiI5X1o5ZUpGTWI5X3E4UlZCTDFfa29YVWZiTTRLTjJ1UVp1R2NpR0Y2OUJEMnVLejV4Z29PMFUwWTZpRTRvQnpORlFuYVlfZGsteVNXbEFqdjJILWhQa0xqb2ZURkhpTU5lMnFFTWlBWnk2eXJkZEg2OFY3SFRNRllUQlZQaHIxT0dxZlRmc01fRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE0iLCJxIjoiMVBFWWxiMmw1OG5VYmp0WThZelJNb0FaOWhyVzJYcDNyWmYxNENFVENXbFVxWXQtUTdDcjd3TEFFY243a2xZNURhd0JuTXJsZXl3S0ctTUFfU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUy1hWWljemJsYmJqU0RqWXVjR1I3Mi1vUWUzMmNNeGNzMk1GUFZwdWJuOGNqUFBudkp3STlpSkZVIiwiZHAiOiIyMzdSY0pIR0JONUUzYVdfN3d6R21QblJtSUlHM3lvVFNFNzhQbWx6Nm1xOXk1b3EtOWhaWjRNRXdfUW1hTzAxeVNMUXRGOEJmNkxTdkRYeEFrZHdWMm5ra0tfWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoSmVwaV9kWFFYUFZ4WjFhdXhiV0ZfdXMiLCJkcSI6InFaeFVVY2xhWVhLZ3N5Q3dhczRZUDFwTDAzeHpUNHk5ME5hdWNOVEhYZ0s0X2NidlRxbGxlWjQtTUswcXBkZnAzOXIyLWRXZXplbzVMeGMwVFd2eUwzMVZGYU9QMmJOQklKam9VbE9ldFkwMi1vWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyU0thc3JIVSIsInFpIjoiVzNhUDJmdTB1N1JVWWVubzIyNkljRjNBaEZzY3NWam55NmNGdWNGanNIbUg2V1BNYTU0dS1MWFRaYllSTFBCOFVvdHA3UjhEU0NoeEhtMjhjaDdqNTRnZkdWNmFlQVVPZXRiUEhCbGJudnQ1M2hFdERTLTlYVF8xYWtJNngwWk1Mc3F3eEZuZ2NSMF93S0V5Zzh2c1BlT2gwdlZsamkyNkpyZnNNZU9fakxvIn0=\",<br>    \"created\": \"2021-06-15T21:06:54.763937286Z\"<br>  },<br>  \"success\": true,<br>  \"errors\": [],<br>  \"messages\": []<br>}<\/pre>\n\n\n\n<p><code>id<\/code>, <code>pem<\/code> \uc744 \uc800\uc7a5\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: \ud0a4\ub97c \uc774\uc6a9\ud574\uc11c token \ub9cc\ub4e4\uae30<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js \uc608\uc81c<\/h3>\n\n\n\n<p><code>jsonwebtoken<\/code>&nbsp;\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc124\uce58:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">npm install jsonwebtoken dotenv<\/pre>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>token-gen.js<\/code>):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">require('dotenv').config();<br>const jwt = require('jsonwebtoken');<br><br>\/\/ \ud658\uacbd\ubcc0\uc218 \ub610\ub294 \uc9c1\uc811 \uc785\ub825<br>const PRIVATE_KEY = process.env.CF_STREAM_PRIVATE_KEY;  \/\/ pem \uac12<br>const KEY_ID = process.env.CF_STREAM_KEY_ID;  \/\/ id  \uac12<br>const VIDEO_UID = \"VIDEO_UID_HERE\"; \/\/ \uc7a0\uadf8\ub824\ub294 \uc601\uc0c1 ID<br><br>function generateSignedToken(videoUid) {<br>  \/\/ 1. \ub9cc\ub8cc \uc2dc\uac04 \uc124\uc815 (\ud604\uc7ac \uc2dc\uac04 + 1\uc2dc\uac04)<br>  \/\/ Math.floor(Date.now() \/ 1000) -&gt; \ud604\uc7ac \ucd08(seconds)<br>  const expiresIn = Math.floor(Date.now() \/ 1000) + 3600;<br><br>  \/\/ 2. \ud398\uc774\ub85c\ub4dc \uad6c\uc131 (\ubb38\uc11c\uc758 data \uac1d\uccb4\uc640 \ub3d9\uc77c)<br>  const payload = {<br>    sub: videoUid,    \/\/ \uc601\uc0c1 ID (Subject)<br>    kid: KEY_ID,      \/\/ Key ID (Payload\uc5d0\ub3c4 \ud3ec\ud568)<br>    exp: expiresIn,   \/\/ \ub9cc\ub8cc \uc2dc\uac04<br>    \/\/ 3. \uc811\uadfc \uc81c\uc5b4 \uaddc\uce59 (Access Rules) - Worker \uc608\uc81c\uc640 \ub3d9\uc77c\ud558\uac8c \uad6c\uc131<br>    accessRules: [<br>      {<br>        type: \"ip.geoip.country\",<br>        action: \"allow\",<br>        country: [\"GB\"], \/\/ \uc601\uad6d\ub9cc \ud5c8\uc6a9<br>      },<br>      {<br>        type: \"any\",<br>        action: \"block\", \/\/ \ub098\uba38\uc9c0\ub294 \ucc28\ub2e8<br>      },<br>    ],<br>  };<br><br>  \/\/ 4. \ud1a0\ud070 \uc11c\uba85 (RS256 \uc54c\uace0\ub9ac\uc998)<br>  const token = jwt.sign(payload, PRIVATE_KEY, {<br>    algorithm: 'RS256',<br>    header: {<br>      kid: KEY_ID, \/\/ \ud5e4\ub354\uc5d0\ub3c4 Key ID \ud544\uc218<br>    },<br>    \/\/ jsonwebtoken \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 payload\uc5d0 'exp'\uac00 \uc788\uc73c\uba74 \uc790\ub3d9\uc73c\ub85c \ucc98\ub9ac\ud558\uc9c0\ub9cc,<br>    \/\/ \uba85\uc2dc\uc801\uc73c\ub85c \ub123\uc5c8\uc73c\ubbc0\ub85c \uc635\uc158\uc5d0\uc11c\ub294 \uc81c\uc678\ud558\uac70\ub098 \uadf8\ub300\ub85c \ub461\ub2c8\ub2e4.<br>  });<br><br>  return token;<br>}<br><br>\/\/ \uc2e4\ud589 \ubc0f \ud655\uc778<br>const token = generateSignedToken(VIDEO_UID);<br>console.log(\"&#x2705; Generated Token:\", token);<br>console.log(`&#x1f517; Signed URL: https:\/\/customer-&lt;YOUR_CODE&gt;.cdn.cloudflare.net\/${token}\/${VIDEO_UID}\/manifest\/video.m3u8`);<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Python \uc608\uc81c<\/h3>\n\n\n\n<p><code>PyJWT<\/code>&nbsp;\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc124\uce58:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pip install pyjwt cryptography python-dotenv<\/pre>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>token_gen.py<\/code>):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import os<br>import time<br>import jwt<br>from dotenv import load_dotenv<br><br>load_dotenv()<br><br># PEM \ud615\uc2dd\uc758 Private Key (\ubb38\uc790\uc5f4 \uadf8\ub300\ub85c \uac00\uc838\uc640\uc57c \ud568)<br>PRIVATE_KEY = os.getenv(\"CF_STREAM_PRIVATE_KEY\")<br>KEY_ID = os.getenv(\"CF_STREAM_KEY_ID\")<br>VIDEO_UID = \"VIDEO_UID_HERE\"<br><br>def generate_signed_token(video_uid):<br>    # 1. \ub9cc\ub8cc \uc2dc\uac04 \uc124\uc815 (\ud604\uc7ac \uc2dc\uac04 + 1\uc2dc\uac04)<br>    expires_in = int(time.time()) + 3600<br><br>    # 2. \ud398\uc774\ub85c\ub4dc \uad6c\uc131 (Worker \uc608\uc81c\uc758 data \ubd80\ubd84)<br>    payload = {<br>        \"sub\": video_uid,  # \uc601\uc0c1 ID<br>        \"kid\": KEY_ID,     # Key ID<br>        \"exp\": expires_in, # \ub9cc\ub8cc \uc2dc\uac04<br>        # 3. \uc811\uadfc \uc81c\uc5b4 \uaddc\uce59 (Access Rules)<br>        \"accessRules\": [<br>            {<br>                \"type\": \"ip.geoip.country\",<br>                \"action\": \"allow\",<br>                \"country\": [\"GB\"] # \uc601\uad6d\ub9cc \ud5c8\uc6a9<br>            },<br>            {<br>                \"type\": \"any\",<br>                \"action\": \"block\" # \ub098\uba38\uc9c0 \ucc28\ub2e8<br>            }<br>        ]<br>    }<br><br>    # 4. \ud5e4\ub354 \uc124\uc815<br>    headers = {<br>        \"kid\": KEY_ID, # \ud5e4\ub354\uc5d0 Key ID \uba85\uc2dc<br>        \"alg\": \"RS256\"<br>    }<br><br>    # 5. \ud1a0\ud070 \uc11c\uba85<br>    token = jwt.encode(<br>        payload,<br>        PRIVATE_KEY,<br>        algorithm=\"RS256\",<br>        headers=headers<br>    )<br><br>    return token<br><br>if __name__ == \"__main__\":<br>    token = generate_signed_token(VIDEO_UID)<br>    <br>    # Python \ubc84\uc804\uc5d0 \ub530\ub77c jwt.encode\uac00 bytes\ub97c \ub9ac\ud134\ud560 \uc218 \uc788\uc73c\ubbc0\ub85c decode \ucc98\ub9ac<br>    if isinstance(token, bytes):<br>        token = token.decode('utf-8')<br>        <br>    print(f\"&#x2705; Generated Token: {token}\")<br>    print(f\"&#x1f517; Signed URL: https:\/\/customer-&lt;YOUR_CODE&gt;.cdn.cloudflare.net\/{token}\/{VIDEO_UID}\/manifest\/video.m3u8\")<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud504\ub860\ud2b8\uc5d4\ub4dc: \uc7ac\uc0dd URL \uad6c\uc131<\/h2>\n\n\n\n<p>\ubc31\uc5d4\ub4dc\ub85c\ubd80\ud130 \ubc1b\uc740 <code>token<\/code>\uc744 URL \uc0ac\uc774\uc5d0 \ub07c\uc6cc \ub123\uc73c\uba74 \ub429\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">URL \ud328\ud134<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>HLS (Manifest):\u00a0https:\/\/customer-{CODE}.cloudflarestream.com\/{TOKEN}\/manifest\/video.m3u8<\/li>\n\n\n\n<li>DASH:\u00a0https:\/\/customer-{CODE}.cloudflarestream.com\/{TOKEN}\/manifest\/video.mpd<\/li>\n\n\n\n<li>iFrame (Embed):\u00a0https:\/\/customer-{CODE}.cloudflarestream.com\/{TOKEN}\/iframe<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">JavaScript \uc608\uc2dc (Video.js \ub4f1 \ud50c\ub808\uc774\uc5b4 \uc0ac\uc6a9 \uc2dc)<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ 1. \ubc31\uc5d4\ub4dc\uc5d0\uc11c \ud1a0\ud070\uc744 \uc694\uccad<br>const response = await fetch(`\/api\/video-token\/${videoId}`);<br>const { token } = await response.json();<br><br>\/\/ 2. URL \uc870\ub9bd<br>const signedSrc = `https:\/\/customer-xxxxx.cloudflarestream.com\/${token}\/manifest\/video.m3u8`;<br><br>\/\/ 3. \ud50c\ub808\uc774\uc5b4 \ub85c\ub4dc<br>player.src({<br>    src: signedSrc,<br>    type: 'application\/x-mpegURL'<br>});<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">iFrame Embed \uc608\uc2dc<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;iframe<br>  src=\"https:\/\/customer-&lt;CODE&gt;.cloudflarestream.com\/eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ\/iframe\"<br>  style=\"border: none;\"<br>  height=\"720\"<br>  width=\"1280\"<br>  allow=\"accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;\"<br>  allowfullscreen=\"true\"<br>&gt;&lt;\/iframe&gt;<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uace0\uae09 \ubcf4\uc548: Access Rules (\uc120\ud0dd \uc0ac\ud56d)<\/h2>\n\n\n\n<p>\ud1a0\ud070\uc744 \ub204\uad70\uac00 \ud0c8\ucde8\ud574\uc11c \ub2e4\ub978 \uc0ac\ub78c\uc5d0\uac8c \uc904 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub97c \ub9c9\uae30 \uc704\ud574 \ud1a0\ud070 \uc0dd\uc131 \uc2dc Payload\uc5d0 <code>accessRules<\/code>\ub97c \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc608\uc2dc: &#8220;\ud55c\uad6d(KR)\uc5d0\uc11c \uc811\uc18d\ud558\ub294 IP\ub9cc \ud5c8\uc6a9&#8221;<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"accessRules\": [<br>    {<br>      \"type\": \"ip.geoip.country\",<br>      \"action\": \"allow\",<br>      \"country\": [\"KR\"]<br>    },<br>    {<br>      \"type\": \"any\",<br>      \"action\": \"block\"<br>    }<br>  ]<br>}<\/pre>\n\n\n\n<p>\uc774\ub807\uac8c \ud558\uba74 \ud1a0\ud070\uc774 \uc720\ucd9c\ub418\uc5b4\ub3c4 \ud574\uc678\uc5d0\uc11c\ub294 \uc7ac\uc0dd\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><br>    request_body = request.get_data()<br>    string_to_sign = f\"{timestamp}.\".encode('utf-8') + request_body<br><br>    # 3. HMAC SHA-256 \ud574\uc2dc \uc0dd\uc131<br>    generated_sig = hmac.new(<br>        WEBHOOK_SECRET.encode('utf-8'),<br>        string_to_sign,<br>        hashlib.sha256<br>    ).hexdigest()<br><br>    # 4. \uc11c\uba85 \ube44\uad50<br>    if not hmac.compare_digest(generated_sig, signature):<br>        print(\"&#x274c; \uc11c\uba85 \ubd88\uc77c\uce58! \uc704\uc870\ub41c \uc694\uccad\uc785\ub2c8\ub2e4.\")<br>        return \"Invalid Signature\", 401<br><br>    # 5. \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1 \ucc98\ub9ac<br>    data = request.json<br>    uid = data.get('uid')<br>    status = data.get('status', {}).get('state')<br><br>    print(f\"&#x1f514; Webhook \uc218\uc2e0: Video [{uid}] is [{status}]\")<br><br>    if status == 'ready':<br>        # &#x2705; DB \uc5c5\ub370\uc774\ud2b8 \ub85c\uc9c1 \uc218\ud589<br>        # db.update_status(uid, 'active')<br>        print(f\"&gt;&gt; DB \uc5c5\ub370\uc774\ud2b8 \uc644\ub8cc: \uc601\uc0c1({uid}) \uc7ac\uc0dd \uac00\ub2a5\")<br><br>    return \"OK\", 200<br><br>if __name__ == '__main__':<br>    app.run(port=3000)<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc694\uc57d<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ubcf4\uc548<\/strong>: <code>Webhook-Signature<\/code> \ud5e4\ub354\uc640 <code>Raw Body<\/code>\ub97c \uc870\ud569\ud574 \ud574\uc2dc\ub97c \ub9cc\ub4e4\uace0, Secret\uacfc \ub300\uc870\ud558\uc5ec <strong>\uc704\ubcc0\uc870\ub97c \ubc29\uc9c0<\/strong>\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc774\ubca4\ud2b8<\/strong>: <code>status.state<\/code>\uac00 <code>ready<\/code>\uac00 \ub418\uc5c8\uc744 \ub54c DB\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uc5ec \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc601\uc0c1\uc744 \ub178\ucd9c\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc751\ub2f5<\/strong>: \uc11c\ubc84\ub294 \ucc98\ub9ac\uac00 \ub05d\ub098\uba74 \ubc18\ub4dc\uc2dc <strong>200 OK<\/strong>\ub97c \ub9ac\ud134\ud574\uc57c Cloudflare\uac00 \uc7ac\uc804\uc1a1(Retry)\uc744 \ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">\uc5d0\ub7ec \ub300\uc751<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\uc6b4\uc601 \ud658\uacbd\uc5d0\uc11c &#8220;\uc131\uacf5\ud558\ub294 \ucf00\uc774\uc2a4&#8221;\ub9cc \uace0\ub824\ud558\uba74 \ub098\uc911\uc5d0 \ud070 \ub0ad\ud328\ub97c \ubd05\ub2c8\ub2e4. \ub3d9\uc601\uc0c1 \uc11c\ube44\uc2a4\ub294 \ud30c\uc77c \ud06c\uae30\uac00 \ud06c\uace0 \ub124\ud2b8\uc6cc\ud06c \ubcc0\uc218\uac00 \ub9ce\uc544 <strong>\ubc29\uc5b4\uc801\uc778 \ucf54\ub529(Defensive Programming)<\/strong>&nbsp;\uc774 \ud544\uc218\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ud06c\uac8c <strong>1. \uc5c5\ub85c\ub4dc \uc911 \uc2e4\ud328 (Frontend)<\/strong>&nbsp;\uc640 <strong>2. \uc778\ucf54\ub529\/\ucc98\ub9ac \uc911 \uc2e4\ud328 (Backend Webhook)<\/strong> \ub450 \uac00\uc9c0 \uc2dc\uc810\uc73c\ub85c \ub098\ub204\uc5b4 \ub300\uc751 \ub85c\uc9c1\uc744 \uc815\ub9ac\ud574 \ub4dc\ub9bd\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud504\ub860\ud2b8\uc5d4\ub4dc: \uc5c5\ub85c\ub4dc \uc911\ub2e8 \ubc0f \uc2e4\ud328 \ub300\uc751<\/h2>\n\n\n\n<p>\uc0ac\uc6a9\uc790\uac00 \ube0c\ub77c\uc6b0\uc800\ub97c \ub2eb\uac70\ub098, \uc640\uc774\ud30c\uc774\uac00 \ub04a\uae30\uac70\ub098, \ud30c\uc77c\uc774 \uaddc\uaca9\uc5d0 \ub9de\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. TUS \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \uc7ac\uc2dc\ub3c4(Retry) \uc635\uc158 \ud65c\uc6a9<\/h3>\n\n\n\n<p>\uc55e\uc11c \uc791\uc131\ud55c TUS \ucf54\ub4dc\uc5d0 \uc7ac\uc2dc\ub3c4 \ub85c\uc9c1\uc774 \uc774\ubbf8 \ud3ec\ud568\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c \uc0ac\uc6a9\uc790\uc5d0\uac8c <strong>&#8220;\ubb34\uc5c7\uc774 \uc798\ubabb\ub418\uc5c8\ub294\uc9c0&#8221;<\/strong> \uba85\ud655\ud788 \uc54c\ub824\uc8fc\ub294 UX \ucc98\ub9ac\uac00 \uc911\uc694\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">const upload = new tus.Upload(file, {<br>    endpoint: uploadURL,<br>    retryDelays: [0, 1000, 3000, 5000], \/\/ \ub124\ud2b8\uc6cc\ud06c \uc77c\uc2dc \ub2e8\uc808 \uc2dc 4\ubc88 \uc7ac\uc2dc\ub3c4<br>    onError: function(error) {<br>        console.error(\"Upload Failed:\", error);<br>        <br>        \/\/ &#x1f6a8; UX \ub300\uc751: \uc5d0\ub7ec \uba54\uc2dc\uc9c0\ub97c \ubd84\uc11d\ud558\uc5ec \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc548\ub0b4<br>        if (error.originalRequest &amp;&amp; error.originalRequest.status === 413) {<br>            alert(\"\ud30c\uc77c\uc774 \ub108\ubb34 \ud07d\ub2c8\ub2e4. (\ucd5c\ub300 \uc6a9\ub7c9 \ucd08\uacfc)\");<br>        } else if (error.message.includes(\"NetworkError\")) {<br>            alert(\"\ub124\ud2b8\uc6cc\ud06c \uc5f0\uacb0\uc774 \ubd88\uc548\uc815\ud569\ub2c8\ub2e4. \uc7a0\uc2dc \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.\");<br>        } else {<br>            alert(\"\uc5c5\ub85c\ub4dc \uc911 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uace0\uce68 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.\");<br>        }<br>        <br>        \/\/ UI \uc0c1\ud0dc \uc5c5\ub370\uc774\ud2b8: '\uc7ac\uc2dc\ub3c4 \ubc84\ud2bc' \ub178\ucd9c<br>        document.getElementById('retryBtn').style.display = 'block';<br>    },<br>    \/\/ ... \uae30\ud0c0 \uc124\uc815<br>});<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">B. \ube0c\ub77c\uc6b0\uc800 \uc774\ud0c8 \ubc29\uc9c0 (<code>beforeunload<\/code>)<\/h3>\n\n\n\n<p>\uc0ac\uc6a9\uc790\uac00 \uc5c5\ub85c\ub4dc \uc911\uc5d0 \uc2e4\uc218\ub85c \ud0ed\uc744 \ub2eb\uac70\ub098 \ub4a4\ub85c \uac00\uae30\ub97c \ub204\ub974\ub294 \uac83\uc744 \ubc29\uc9c0\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">window.addEventListener(\"beforeunload\", function (e) {<br>    \/\/ \uc5c5\ub85c\ub4dc\uac00 \uc9c4\ud589 \uc911\uc77c \ub54c\ub9cc \uacbd\uace0<br>    if (isUploading) { <br>        e.preventDefault();<br>        e.returnValue = \"\"; \/\/ Chrome\uc5d0\uc11c\ub294 \ud45c\uc900 \uba54\uc2dc\uc9c0\uac00 \ud45c\uc2dc\ub428<br>    }<br>});<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ubc31\uc5d4\ub4dc: \uc778\ucf54\ub529 \uc2e4\ud328 \ub300\uc751 (Webhook \ud655\uc7a5)<\/h2>\n\n\n\n<p>\uc5c5\ub85c\ub4dc\ub294 \uc131\uacf5\ud588\ub294\ub370, Cloudflare \ub0b4\ubd80\uc5d0\uc11c \uc778\ucf54\ub529\ud558\ub2e4\uac00 \uc8fd\ub294 \uacbd\uc6b0\uc785\ub2c8\ub2e4. (\uc608: \ud30c\uc77c\uc774 \uae68\uc838\uc788\uac70\ub098, \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \ucf54\ub371\uc778 \uacbd\uc6b0).<\/p>\n\n\n\n<p>\uc774 \uacbd\uc6b0 Cloudflare\ub294 <code>status.state<\/code> \uac12\uc744 <code>error<\/code>\ub85c \ud574\uc11c Webhook\uc744 \ubcf4\ub0c5\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Webhook \ud578\ub4e4\ub7ec \uc218\uc815 (Node.js \uc608\uc2dc)<\/h3>\n\n\n\n<p>\uae30\uc874 <code>ready<\/code> \uc0c1\ud0dc\ub9cc \uccb4\ud06c\ud558\ub358 \ub85c\uc9c1\uc5d0 <code>error<\/code> \ucc98\ub9ac\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ ... (\uc11c\uba85 \uac80\uc99d \ub85c\uc9c1\uc740 \ub3d9\uc77c) ...<br><br>\/\/ 5. \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1 \ucc98\ub9ac (\ud655\uc7a5\ub428)<br>const event = req.body;<br>const uid = event.uid;<br>const status = event.status.state; \/\/ 'ready', 'queued', 'error' \ub4f1<br>const errReason = event.status.errorReasonCode || event.status.errorReasonText; \/\/ \uc5d0\ub7ec \uc0c1\uc138 \uc0ac\uc720<br><br>console.log(`&#x1f514; Webhook: Video [${uid}] status is [${status}]`);<br><br>if (status === 'ready') {<br>    \/\/ [\uc131\uacf5] \uc11c\ube44\uc2a4 \ud65c\uc131\ud654<br>    await db.updateVideoStatus(uid, 'ACTIVE'); <br><br>} else if (status === 'error') {<br>    \/\/ [\uc2e4\ud328] &#x1f6a8; \uc5ec\uae30\uac00 \ud575\uc2ec\uc785\ub2c8\ub2e4.<br>    console.error(`&#x274c; \uc778\ucf54\ub529 \uc2e4\ud328! UID: ${uid}, Reason: ${errReason}`);<br>    <br>    \/\/ 1. DB \uc0c1\ud0dc\ub97c 'FAILED'\ub85c \ubcc0\uacbd (\uc0ac\uc6a9\uc790\uc5d0\uac8c \"\ucc98\ub9ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\" \ud45c\uc2dc\uc6a9)<br>    await db.updateVideoStatus(uid, 'FAILED', errReason);<br>    <br>    \/\/ 2. \uad00\ub9ac\uc790(\uc6b4\uc601\ud300)\uc5d0\uac8c \uc54c\ub9bc \ubc1c\uc1a1 (Slack, Email \ub4f1)<br>    \/\/ sendSlackAlert(`\uc601\uc0c1 \uc778\ucf54\ub529 \uc2e4\ud328: ${uid} - ${errReason}`);<br><br>} else if (status === 'downloading') {<br>    \/\/ (URL\ub85c \uc5c5\ub85c\ub4dc \uc2dc) \ub2e4\uc6b4\ub85c\ub4dc \uc911<br>    await db.updateVideoStatus(uid, 'PROCESSING');<br>}<br><br>res.status(200).send('OK');<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. &#8220;\uc548\uc804\ub9dd&#8221; \uad6c\ucd95: Polling (Cron Job)<\/h2>\n\n\n\n<p>Webhook\uc740 99.9% \uc2e0\ub8b0\ud560 \uc218 \uc788\uc9c0\ub9cc, <strong>\ub0b4 \uc11c\ubc84\uac00 \uc810\uac80 \uc911\uc774\uac70\ub098 \ub2e4\uc6b4\ub418\uc5c8\uc744 \ub54c<\/strong> Webhook\uc744 \ub193\uce60 \uc218 \uc788\uc2b5\ub2c8\ub2e4. Cloudflare\uac00 \uba87 \ubc88 \uc7ac\uc2dc\ub3c4\ud558\uae34 \ud558\uc9c0\ub9cc, \uc601\uc6d0\ud788 \uc7ac\uc2dc\ub3c4\ud558\uc9c4 \uc54a\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ub530\ub77c\uc11c <strong>&#8220;\uace0\uc544 \uc0c1\ud0dc(Stuck)&#8221;<\/strong>&nbsp;\uac00 \ub41c \uc601\uc0c1\uc744 \uad6c\uc81c\ud558\uae30 \uc704\ud574 \uc8fc\uae30\uc801\uc778 \ubc30\uce58 \uc791\uc5c5(Cron)\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\ub85c\uc9c1 \uc124\uba85:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>DB\uc5d0\uc11c \uc5c5\ub85c\ub4dc\ud55c \uc9c0 <strong>30\ubd84\uc774 \uc9c0\ub0ac\ub294\ub370\ub3c4 \uc5ec\uc804\ud788 <code>Processing<\/code> \uc0c1\ud0dc<\/strong>\uc778 \uc601\uc0c1\ub4e4\uc744 \ucc3e\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li>Cloudflare API\ub85c \ud574\ub2f9 \uc601\uc0c1\ub4e4\uc758 \ud604\uc7ac \uc0c1\ud0dc\ub97c \uc870\ud68c\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li>\uc0c1\ud0dc\uac00 <code>ready<\/code>\ub098 <code>error<\/code>\ub85c \ubcc0\ud574\uc788\ub2e4\uba74 DB\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<p><strong>Python (Scheduler) \uc608\uc2dc:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import requests<br># \uac00\uc0c1\uc758 DB \ubaa8\ub4c8<br>from myapp.models import Video <br><br>def check_stuck_videos():<br>    # 1. 30\ubd84 \ub118\uac8c \ucc98\ub9ac \uc911\uc778 \uc601\uc0c1 \uc870\ud68c<br>    stuck_videos = Video.objects.filter(status='PROCESSING', created_at__lte=time_30_mins_ago)<br><br>    for video in stuck_videos:<br>        # 2. Cloudflare API\ub85c \uc0c1\ud0dc \uc870\ud68c<br>        url = f\"https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{ACCOUNT_ID}\/stream\/{video.uid}\"<br>        resp = requests.get(url, headers=HEADERS)<br>        <br>        if resp.status_code == 200:<br>            cf_data = resp.json()['result']<br>            current_state = cf_data['status']['state'] # ready, error \ub4f1<br><br>            # 3. \uc0c1\ud0dc \ub3d9\uae30\ud654<br>            if current_state == 'ready':<br>                video.status = 'ACTIVE'<br>                video.save()<br>                print(f\"\ubcf5\uad6c\ub428: {video.uid} -&gt; ACTIVE\")<br>            elif current_state == 'error':<br>                video.status = 'FAILED'<br>                video.save()<br>                print(f\"\ubcf5\uad6c\ub428: {video.uid} -&gt; FAILED\")<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. \uc5d0\ub7ec \ubc1c\uc0dd \uc2dc \uc0ac\uc6a9\uc790 \uacbd\ud5d8 (UX) \uc2dc\ub098\ub9ac\uc624<\/h2>\n\n\n\n<p>\uac1c\ubc1c\uc790\uac00 \uc544\ub2cc <strong>\ucd5c\uc885 \uc0ac\uc6a9\uc790(\uace0\uac1d)<\/strong> \uc785\uc7a5\uc5d0\uc11c\uc758 \uc2dc\ub098\ub9ac\uc624\ub294 \ub2e4\uc74c\uacfc \uac19\uc544\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\uc5c5\ub85c\ub4dc \uc2e4\ud328 \uc2dc (Frontend):<\/strong>\n<ul class=\"wp-block-list\">\n<li>&#8220;\ub124\ud2b8\uc6cc\ud06c \ubb38\uc81c\ub85c \uc5c5\ub85c\ub4dc\uac00 \uc911\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4. [\uc774\uc5b4\uc62c\ub9ac\uae30] \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.&#8221; (TUS \ud65c\uc6a9)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>\uc778\ucf54\ub529 \uc2e4\ud328 \uc2dc (Backend):<\/strong>\n<ul class=\"wp-block-list\">\n<li>Admin \ud398\uc774\uc9c0 \ub9ac\uc2a4\ud2b8\uc5d0 <strong>[\ucc98\ub9ac \uc2e4\ud328 &#x26a0;&#xfe0f;]<\/strong> \ub77c\ubca8 \ud45c\uc2dc.<\/li>\n\n\n\n<li>\ub9c8\uc6b0\uc2a4 \uc624\ubc84 \uc2dc &#8220;\ud30c\uc77c \ud615\uc2dd\uc774 \uc190\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc778\ucf54\ub529\ud574\uc11c \uc62c\ub824\uc8fc\uc138\uc694.&#8221; \ud234\ud301 \uc81c\uacf5.<\/li>\n\n\n\n<li>\ud574\ub2f9 \uc601\uc0c1\uc740 \ud50c\ub808\uc774\uc5b4\uc5d0\uc11c \uc7ac\uc0dd \uc2dc\ub3c4 \uc790\uccb4\ub97c \ub9c9\uc544\uc57c \ud568.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc694\uc57d \uccb4\ud06c\ub9ac\uc2a4\ud2b8<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>&#x2705; <strong>Frontend<\/strong>: <code>tus.onError<\/code> \ud578\ub4e4\ub7ec\uc5d0\uc11c \uc5d0\ub7ec \uc885\ub958\ubcc4 \uba54\uc2dc\uc9c0 \ubd84\uae30 \ucc98\ub9ac.<\/li>\n\n\n\n<li>&#x2705; <strong>Frontend<\/strong>: <code>beforeunload<\/code> \uc774\ubca4\ud2b8\ub85c \uc2e4\uc218\ub85c \ud0ed \ub2eb\uae30 \ubc29\uc9c0.<\/li>\n\n\n\n<li>&#x2705; <strong>Backend<\/strong>: Webhook\uc5d0\uc11c <code>status === 'error'<\/code> \uc77c \ub54c DB \uc5c5\ub370\uc774\ud2b8 \ubc0f \uad00\ub9ac\uc790 \uc54c\ub9bc.<\/li>\n\n\n\n<li>&#x2705; <strong>Backend<\/strong>: Webhook \ub204\ub77d \ub300\ube44\uc6a9 <strong>\uc2a4\ucf00\uc904\ub7ec(Cron)<\/strong> \uad6c\ud604.<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">Singed URL<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Cloudflare Stream\uc5d0\uc11c \ubcf4\uc548\uc774 \ud544\uc694\ud55c \uc601\uc0c1(\uc720\ub8cc \uac15\uc758, \uc0ac\ub0b4 \uad50\uc721 \uc790\ub8cc \ub4f1)\uc740 <strong>Signed URL(\uc11c\uba85\ub41c URL)<\/strong> \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\uc774 \ubc29\uc2dd\uc758 \ud575\uc2ec\uc740 <strong>&#8220;\ub0b4 \uc11c\ubc84\uac00 \ubc1c\uae09\ud55c &#8216;\ucd9c\uc785\uc99d(Token)&#8217;\uc744 \uac00\uc9c4 \uc0ac\ub78c\ub9cc \uc601\uc0c1\uc744 \ubcfc \uc218 \uc788\ub2e4&#8221;<\/strong>&nbsp;\ub294 \uac83\uc785\ub2c8\ub2e4. \ucd9c\uc785\uc99d\uc5d0\ub294 \uc720\ud6a8\uae30\uac04(\uc608: 1\uc2dc\uac04)\uacfc \ud2b9\uc815 \uaddc\uce59(\uc608: \ud2b9\uc815 IP\ub9cc \ud5c8\uc6a9)\uc744 \uc2ec\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc601\uc0c1 \uc7a0\uadf8\uae30 (Locking the Video)<\/h2>\n\n\n\n<p>\uc601\uc0c1\uc744 \uc5c5\ub85c\ub4dc\ud560 \ub54c\ub098 \uc5c5\ub85c\ub4dc \ud6c4\uc5d0 &#8220;\uc774 \uc601\uc0c1\uc740 \ud1a0\ud070\uc774 \ud544\uc694\ud574&#8221;\ub77c\uace0 \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uc5c5\ub85c\ub4dc \uc2dc<\/strong>: <code>requireSignedURLs: true<\/code> \uc124\uc815 (\uc55e\uc11c \uc5c5\ub85c\ub4dc API \uc124\uba85 \ucc38\uc870)<\/li>\n\n\n\n<li><strong>\uc5c5\ub85c\ub4dc \ud6c4 (API)<\/strong>:curl -X POST https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{ACCOUN_ID}\/stream\/{uid} \\<br>&#8230; -d &#8216;{&#8220;requireSignedURLs&#8221;: true}&#8217;<\/li>\n<\/ul>\n\n\n\n<p>\uc774 \uc124\uc815\uc774 \ucf1c\uc9c0\uba74, \uae30\uc874\uc758 \uacf5\uac1c URL\ub85c \uc811\uc18d \uc2dc <code>403 Forbidden<\/code> \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc635\uc158 1: \/token \uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc0ac\uc6a9\ud558\uae30<\/h2>\n\n\n\n<p>\ud14c\uc2a4\ud2b8 \ubaa9\uc801 \ub610\ub294 \ud558\ub8e8\uc5d0 1000\uac1c \uc774\ud558\uc758 \ud1a0\ud070\uc744 \uc0dd\uc131\ud560 \ub54c\ub294 \uc774 \ubc29\ubc95\uc744 \ucd94\ucc9c\ud569\ub2c8\ub2e4.&nbsp;<\/p>\n\n\n\n<p>\ud1a0\ud070\uc744 \ub9cc\ub4e4 \ub54c\ub9c8\ub2e4 Cloudflare API \ucf5c\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p>\ud1a0\ud070\uc758 \uae30\ubcf8 \uc720\ud6a8\uc2dc\uac04\uc740 1\uc2dc\uac04\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">cURL \uc608\uc81c<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">curl --request POST \\<br>https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{account_id}\/stream\/{video_uid}\/token \\<br>--header \"Authorization: Bearer &lt;API_TOKEN&gt;\"<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\uc751\ub2f5 \uc608:<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"result\": {<br>    \"token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ\"<br>  },<br>  \"success\": true,<br>  \"errors\": [],<br>  \"messages\": []<br>}<\/pre>\n\n\n\n<p><code>token<\/code> \uac12\uc744 \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0 \uc804\ub2ec\ud55c\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uc635\uc1582: \uc11c\uba85 \ud0a4 \uc0ac\uc6a9\ud558\uae30<\/h2>\n\n\n\n<p>\uc11c\uba85\ud0a4\ub97c \uc0ac\uc6a9\ud558\uba74 \ub9e4\ubc88 \uc2a4\ud2b8\ub9bc API\ub97c \ucf5c \ud560 \ud544\uc694\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step1: \/Strean\/key \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc5d0\uc11c \ud0a4 \uc5bb\uae30<\/h3>\n\n\n\n<p><strong>cURL \uc608\uc81c:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">curl --request POST \\<br>\"https:\/\/api.cloudflare.com\/client\/v4\/accounts\/{account_id}\/stream\/keys\" \\<br>--header \"Authorization: Bearer &lt;API_TOKEN&gt;\"<\/pre>\n\n\n\n<p><strong>Response \uc608:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"result\": {<br>    \"id\": \"8f926b2b01f383510025a78a4dcbf6a\",<br>    \"pem\": \"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBemtHbXhCekFGMnBIMURiWmgyVGoyS3ZudlBVTkZmUWtNeXNCbzJlZzVqemRKTmRhCmtwMEphUHhoNkZxOTYveTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgrYkR3TEdTVldGMEx3QnloMDYKN01Rb0xySHA3MDEycXBVNCtLODUyT1hMRVVlWVBrOHYzRlpTQ2VnMVdLRW5URC9oSmhVUTFsTmNKTWN3MXZUbQpHa2o0empBUTRBSFAvdHFERHFaZ3lMc1Vma2NsRDY3SVRkZktVZGtFU3lvVDVTcnFibHNFelBYcm9qaFlLWGk3CjFjak1yVDlFS0JCenhZSVEyOVRaZitnZU5ya0t4a2xMZTJzTUFML0VWZkFjdGkrc2ZqMkkyeEZKZmQ4aklmL2UKdHBCSVJZVDEza2FLdHUyYmk0R2IrV1BLK0toQjdTNnFGODlmTHdJREFRQUJBb0lCQUYzeXFuNytwNEtpM3ZmcgpTZmN4ZmRVV0xGYTEraEZyWk1mSHlaWEFJSnB1MDc0eHQ2ZzdqbXM3Tm0rTFVhSDV0N3R0bUxURTZacy91RXR0CjV3SmdQTjVUaFpTOXBmMUxPL3BBNWNmR2hFN1pMQ2wvV2ZVNXZpSFMyVDh1dGlRcUYwcXpLZkxCYk5kQW1MaWQKQWl4blJ6UUxDSzJIcmlvOW1KVHJtSUUvZENPdG80RUhYdHpZWjByOVordHRxMkZrd3pzZUdaK0tvd09JaWtvTgp2NWFOMVpmRGhEVG0wdG1Vd0tLbjBWcmZqalhRdFdjbFYxTWdRejhwM2xScWhISmJSK29PL1NMSXZqUE16dGxOCm5GV1ZEdTRmRHZsSjMyazJzSllNL2tRVUltT3V5alY3RTBBcm5vR2lBREdGZXFxK1UwajluNUFpNTJ6aTBmNloKdFdvwdju39xOFJWQkwxL2tvWFVmYk00S04ydVFadUdjaUdGNjlCRDJ1S3o1eGdvTwowVTBZNmlFNG9Cek5GUW5hWS9kayt5U1dsQWp2MkgraFBrTGpvZlRGSGlNTmUycUVNaUFaeTZ5cmRkSDY4VjdIClRNRllUQlZQaHIxT0dxZlRmc00vRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE1DZ1lFQTFQRVkKbGIybDU4blVianRZOFl6Uk1vQVo5aHJXMlhwM3JaZjE0Q0VUQ1dsVXFZdCtRN0NyN3dMQUVjbjdrbFk1RGF3QgpuTXJsZXl3S0crTUEvU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUythWWljemJsYmJqU0RqWXVjCkdSNzIrb1FlMzJjTXhjczJNRlBWcHVibjhjalBQbnZKd0k5aUpGVUNnWUVBMjM3UmNKSEdCTjVFM2FXLzd3ekcKbVBuUm1JSUczeW9UU0U3OFBtbHo2bXE5eTVvcSs5aFpaNE1Fdy9RbWFPMDF5U0xRdEY4QmY2TFN2RFh4QWtkdwpWMm5ra0svWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoCkplcGkvZFhRWFBWeFoxYXV4YldGL3VzQ2dZRUFxWnhVVWNsYVlYS2dzeUN3YXM0WVAxcEwwM3h6VDR5OTBOYXUKY05USFhnSzQvY2J2VHFsbGVaNCtNSzBxcGRmcDM5cjIrZFdlemVvNUx4YzBUV3Z5TDMxVkZhT1AyYk5CSUpqbwpVbE9ldFkwMitvWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyClNLYXNySFVDZ1lCYmRvL1orN1M3dEZSaDZlamJib2h3WGNDRVd4eXhXT2ZMcHdXNXdXT3dlWWZwWTh4cm5pNzQKdGRObHRoRXM4SHhTaTJudEh3TklLSEVlYmJ4eUh1UG5pQjhaWHBwNEJRNTYxczhjR1Z1ZSszbmVFUzBOTDcxZApQL1ZxUWpySFJrd3V5ckRFV2VCeEhUL0FvVEtEeSt3OTQ2SFM5V1dPTGJvbXQrd3g0NytNdWc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=\",<br>    \"jwk\": \"eyJ1c2UiOiJzaWciLCJrdHkiOiJSU0EiLCJraWQiOiI4ZjkyNmIyYjAxZjM4MzUxNzAwMjVhNzhhNGRjYmY2YSIsImFsZyI6IlJTMjU2IiwibiI6InprR214QnpBRjJwSDFEYlpoMlRqMkt2bnZQVU5GZlFrTXlzQm8yZWc1anpkSk5kYWtwMEphUHhoNkZxOTZfeTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgtYkR3TEdTVldGMEx3QnloMDY3TVFvTHJIcDcwMTJxcFU0LUs4NTJPWExFVWVZUGs4djNGWlNDZWcxV0tFblREX2hKaFVRMWxOY0pNY3cxdlRtR2tqNHpqQVE0QUhQX3RxRERxWmd5THNVZmtjbEQ2N0lUZGZLVWRrRVN5b1Q1U3JxYmxzRXpQWHJvamhZS1hpNzFjak1yVDlFS0JCenhZSVEyOVRaZi1nZU5ya0t4a2xMZTJzTUFMX0VWZkFjdGktc2ZqMkkyeEZKZmQ4aklmX2V0cEJJUllUMTNrYUt0dTJiaTRHYi1XUEstS2hCN1M2cUY4OWZMdyIsImUiOiJBUUFCIiwiZCI6IlhmS3FmdjZuZ3FMZTktdEo5ekY5MVJZc1ZyWDZFV3RreDhmSmxjQWdtbTdUdmpHM3FEdU9henMyYjR0Um9mbTN1MjJZdE1UcG16LTRTMjNuQW1BODNsT0ZsTDJsX1VzNy1rRGx4OGFFVHRrc0tYOVo5VG0tSWRMWlB5NjJKQ29YU3JNcDhzRnMxMENZdUowQ0xHZEhOQXNJcllldUtqMllsT3VZZ1Q5MEk2MmpnUWRlM05oblN2MW42MjJyWVdURE94NFpuNHFqQTRpS1NnMl9sbzNWbDhPRU5PYlMyWlRBb3FmUld0LU9OZEMxWnlWWFV5QkRQeW5lVkdxRWNsdEg2Zzc5SXNpLU04ek8yVTJjVlpVTzdoOE8tVW5mYVRhd2xnei1SQlFpWTY3S05Yc1RRQ3VlZ2FJQU1ZVjZxcjVUU1Ai2odx5iT0xSX3BtMWFpdktyUSIsInAiOiI5X1o5ZUpGTWI5X3E4UlZCTDFfa29YVWZiTTRLTjJ1UVp1R2NpR0Y2OUJEMnVLejV4Z29PMFUwWTZpRTRvQnpORlFuYVlfZGsteVNXbEFqdjJILWhQa0xqb2ZURkhpTU5lMnFFTWlBWnk2eXJkZEg2OFY3SFRNRllUQlZQaHIxT0dxZlRmc01fRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE0iLCJxIjoiMVBFWWxiMmw1OG5VYmp0WThZelJNb0FaOWhyVzJYcDNyWmYxNENFVENXbFVxWXQtUTdDcjd3TEFFY243a2xZNURhd0JuTXJsZXl3S0ctTUFfU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUy1hWWljemJsYmJqU0RqWXVjR1I3Mi1vUWUzMmNNeGNzMk1GUFZwdWJuOGNqUFBudkp3STlpSkZVIiwiZHAiOiIyMzdSY0pIR0JONUUzYVdfN3d6R21QblJtSUlHM3lvVFNFNzhQbWx6Nm1xOXk1b3EtOWhaWjRNRXdfUW1hTzAxeVNMUXRGOEJmNkxTdkRYeEFrZHdWMm5ra0tfWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoSmVwaV9kWFFYUFZ4WjFhdXhiV0ZfdXMiLCJkcSI6InFaeFVVY2xhWVhLZ3N5Q3dhczRZUDFwTDAzeHpUNHk5ME5hdWNOVEhYZ0s0X2NidlRxbGxlWjQtTUswcXBkZnAzOXIyLWRXZXplbzVMeGMwVFd2eUwzMVZGYU9QMmJOQklKam9VbE9ldFkwMi1vWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyU0thc3JIVSIsInFpIjoiVzNhUDJmdTB1N1JVWWVubzIyNkljRjNBaEZzY3NWam55NmNGdWNGanNIbUg2V1BNYTU0dS1MWFRaYllSTFBCOFVvdHA3UjhEU0NoeEhtMjhjaDdqNTRnZkdWNmFlQVVPZXRiUEhCbGJudnQ1M2hFdERTLTlYVF8xYWtJNngwWk1Mc3F3eEZuZ2NSMF93S0V5Zzh2c1BlT2gwdlZsamkyNkpyZnNNZU9fakxvIn0=\",<br>    \"created\": \"2021-06-15T21:06:54.763937286Z\"<br>  },<br>  \"success\": true,<br>  \"errors\": [],<br>  \"messages\": []<br>}<\/pre>\n\n\n\n<p><code>id<\/code>, <code>pem<\/code> \uc744 \uc800\uc7a5\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: \ud0a4\ub97c \uc774\uc6a9\ud574\uc11c token \ub9cc\ub4e4\uae30<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js \uc608\uc81c<\/h3>\n\n\n\n<p><code>jsonwebtoken<\/code>&nbsp;\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc124\uce58:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">npm install jsonwebtoken dotenv<\/pre>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>token-gen.js<\/code>):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">require('dotenv').config();<br>const jwt = require('jsonwebtoken');<br><br>\/\/ \ud658\uacbd\ubcc0\uc218 \ub610\ub294 \uc9c1\uc811 \uc785\ub825<br>const PRIVATE_KEY = process.env.CF_STREAM_PRIVATE_KEY;  \/\/ pem \uac12<br>const KEY_ID = process.env.CF_STREAM_KEY_ID;  \/\/ id  \uac12<br>const VIDEO_UID = \"VIDEO_UID_HERE\"; \/\/ \uc7a0\uadf8\ub824\ub294 \uc601\uc0c1 ID<br><br>function generateSignedToken(videoUid) {<br>  \/\/ 1. \ub9cc\ub8cc \uc2dc\uac04 \uc124\uc815 (\ud604\uc7ac \uc2dc\uac04 + 1\uc2dc\uac04)<br>  \/\/ Math.floor(Date.now() \/ 1000) -&gt; \ud604\uc7ac \ucd08(seconds)<br>  const expiresIn = Math.floor(Date.now() \/ 1000) + 3600;<br><br>  \/\/ 2. \ud398\uc774\ub85c\ub4dc \uad6c\uc131 (\ubb38\uc11c\uc758 data \uac1d\uccb4\uc640 \ub3d9\uc77c)<br>  const payload = {<br>    sub: videoUid,    \/\/ \uc601\uc0c1 ID (Subject)<br>    kid: KEY_ID,      \/\/ Key ID (Payload\uc5d0\ub3c4 \ud3ec\ud568)<br>    exp: expiresIn,   \/\/ \ub9cc\ub8cc \uc2dc\uac04<br>    \/\/ 3. \uc811\uadfc \uc81c\uc5b4 \uaddc\uce59 (Access Rules) - Worker \uc608\uc81c\uc640 \ub3d9\uc77c\ud558\uac8c \uad6c\uc131<br>    accessRules: [<br>      {<br>        type: \"ip.geoip.country\",<br>        action: \"allow\",<br>        country: [\"GB\"], \/\/ \uc601\uad6d\ub9cc \ud5c8\uc6a9<br>      },<br>      {<br>        type: \"any\",<br>        action: \"block\", \/\/ \ub098\uba38\uc9c0\ub294 \ucc28\ub2e8<br>      },<br>    ],<br>  };<br><br>  \/\/ 4. \ud1a0\ud070 \uc11c\uba85 (RS256 \uc54c\uace0\ub9ac\uc998)<br>  const token = jwt.sign(payload, PRIVATE_KEY, {<br>    algorithm: 'RS256',<br>    header: {<br>      kid: KEY_ID, \/\/ \ud5e4\ub354\uc5d0\ub3c4 Key ID \ud544\uc218<br>    },<br>    \/\/ jsonwebtoken \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 payload\uc5d0 'exp'\uac00 \uc788\uc73c\uba74 \uc790\ub3d9\uc73c\ub85c \ucc98\ub9ac\ud558\uc9c0\ub9cc,<br>    \/\/ \uba85\uc2dc\uc801\uc73c\ub85c \ub123\uc5c8\uc73c\ubbc0\ub85c \uc635\uc158\uc5d0\uc11c\ub294 \uc81c\uc678\ud558\uac70\ub098 \uadf8\ub300\ub85c \ub461\ub2c8\ub2e4.<br>  });<br><br>  return token;<br>}<br><br>\/\/ \uc2e4\ud589 \ubc0f \ud655\uc778<br>const token = generateSignedToken(VIDEO_UID);<br>console.log(\"&#x2705; Generated Token:\", token);<br>console.log(`&#x1f517; Signed URL: https:\/\/customer-&lt;YOUR_CODE&gt;.cdn.cloudflare.net\/${token}\/${VIDEO_UID}\/manifest\/video.m3u8`);<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Python \uc608\uc81c<\/h3>\n\n\n\n<p><code>PyJWT<\/code>&nbsp;\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc124\uce58:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pip install pyjwt cryptography python-dotenv<\/pre>\n\n\n\n<p><strong>\ucf54\ub4dc (<code>token_gen.py<\/code>):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import os<br>import time<br>import jwt<br>from dotenv import load_dotenv<br><br>load_dotenv()<br><br># PEM \ud615\uc2dd\uc758 Private Key (\ubb38\uc790\uc5f4 \uadf8\ub300\ub85c \uac00\uc838\uc640\uc57c \ud568)<br>PRIVATE_KEY = os.getenv(\"CF_STREAM_PRIVATE_KEY\")<br>KEY_ID = os.getenv(\"CF_STREAM_KEY_ID\")<br>VIDEO_UID = \"VIDEO_UID_HERE\"<br><br>def generate_signed_token(video_uid):<br>    # 1. \ub9cc\ub8cc \uc2dc\uac04 \uc124\uc815 (\ud604\uc7ac \uc2dc\uac04 + 1\uc2dc\uac04)<br>    expires_in = int(time.time()) + 3600<br><br>    # 2. \ud398\uc774\ub85c\ub4dc \uad6c\uc131 (Worker \uc608\uc81c\uc758 data \ubd80\ubd84)<br>    payload = {<br>        \"sub\": video_uid,  # \uc601\uc0c1 ID<br>        \"kid\": KEY_ID,     # Key ID<br>        \"exp\": expires_in, # \ub9cc\ub8cc \uc2dc\uac04<br>        # 3. \uc811\uadfc \uc81c\uc5b4 \uaddc\uce59 (Access Rules)<br>        \"accessRules\": [<br>            {<br>                \"type\": \"ip.geoip.country\",<br>                \"action\": \"allow\",<br>                \"country\": [\"GB\"] # \uc601\uad6d\ub9cc \ud5c8\uc6a9<br>            },<br>            {<br>                \"type\": \"any\",<br>                \"action\": \"block\" # \ub098\uba38\uc9c0 \ucc28\ub2e8<br>            }<br>        ]<br>    }<br><br>    # 4. \ud5e4\ub354 \uc124\uc815<br>    headers = {<br>        \"kid\": KEY_ID, # \ud5e4\ub354\uc5d0 Key ID \uba85\uc2dc<br>        \"alg\": \"RS256\"<br>    }<br><br>    # 5. \ud1a0\ud070 \uc11c\uba85<br>    token = jwt.encode(<br>        payload,<br>        PRIVATE_KEY,<br>        algorithm=\"RS256\",<br>        headers=headers<br>    )<br><br>    return token<br><br>if __name__ == \"__main__\":<br>    token = generate_signed_token(VIDEO_UID)<br>    <br>    # Python \ubc84\uc804\uc5d0 \ub530\ub77c jwt.encode\uac00 bytes\ub97c \ub9ac\ud134\ud560 \uc218 \uc788\uc73c\ubbc0\ub85c decode \ucc98\ub9ac<br>    if isinstance(token, bytes):<br>        token = token.decode('utf-8')<br>        <br>    print(f\"&#x2705; Generated Token: {token}\")<br>    print(f\"&#x1f517; Signed URL: https:\/\/customer-&lt;YOUR_CODE&gt;.cdn.cloudflare.net\/{token}\/{VIDEO_UID}\/manifest\/video.m3u8\")<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud504\ub860\ud2b8\uc5d4\ub4dc: \uc7ac\uc0dd URL \uad6c\uc131<\/h2>\n\n\n\n<p>\ubc31\uc5d4\ub4dc\ub85c\ubd80\ud130 \ubc1b\uc740 <code>token<\/code>\uc744 URL \uc0ac\uc774\uc5d0 \ub07c\uc6cc \ub123\uc73c\uba74 \ub429\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">URL \ud328\ud134<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>HLS (Manifest):\u00a0https:\/\/customer-{CODE}.cloudflarestream.com\/{TOKEN}\/manifest\/video.m3u8<\/li>\n\n\n\n<li>DASH:\u00a0https:\/\/customer-{CODE}.cloudflarestream.com\/{TOKEN}\/manifest\/video.mpd<\/li>\n\n\n\n<li>iFrame (Embed):\u00a0https:\/\/customer-{CODE}.cloudflarestream.com\/{TOKEN}\/iframe<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">JavaScript \uc608\uc2dc (Video.js \ub4f1 \ud50c\ub808\uc774\uc5b4 \uc0ac\uc6a9 \uc2dc)<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ 1. \ubc31\uc5d4\ub4dc\uc5d0\uc11c \ud1a0\ud070\uc744 \uc694\uccad<br>const response = await fetch(`\/api\/video-token\/${videoId}`);<br>const { token } = await response.json();<br><br>\/\/ 2. URL \uc870\ub9bd<br>const signedSrc = `https:\/\/customer-xxxxx.cloudflarestream.com\/${token}\/manifest\/video.m3u8`;<br><br>\/\/ 3. \ud50c\ub808\uc774\uc5b4 \ub85c\ub4dc<br>player.src({<br>    src: signedSrc,<br>    type: 'application\/x-mpegURL'<br>});<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">iFrame Embed \uc608\uc2dc<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;iframe<br>  src=\"https:\/\/customer-&lt;CODE&gt;.cloudflarestream.com\/eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ\/iframe\"<br>  style=\"border: none;\"<br>  height=\"720\"<br>  width=\"1280\"<br>  allow=\"accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;\"<br>  allowfullscreen=\"true\"<br>&gt;&lt;\/iframe&gt;<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\uace0\uae09 \ubcf4\uc548: Access Rules (\uc120\ud0dd \uc0ac\ud56d)<\/h2>\n\n\n\n<p>\ud1a0\ud070\uc744 \ub204\uad70\uac00 \ud0c8\ucde8\ud574\uc11c \ub2e4\ub978 \uc0ac\ub78c\uc5d0\uac8c \uc904 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub97c \ub9c9\uae30 \uc704\ud574 \ud1a0\ud070 \uc0dd\uc131 \uc2dc Payload\uc5d0 <code>accessRules<\/code>\ub97c \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p><strong>\uc608\uc2dc: &#8220;\ud55c\uad6d(KR)\uc5d0\uc11c \uc811\uc18d\ud558\ub294 IP\ub9cc \ud5c8\uc6a9&#8221;<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{<br>  \"accessRules\": [<br>    {<br>      \"type\": \"ip.geoip.country\",<br>      \"action\": \"allow\",<br>      \"country\": [\"KR\"]<br>    },<br>    {<br>      \"type\": \"any\",<br>      \"action\": \"block\"<br>    }<br>  ]<br>}<\/pre>\n\n\n\n<p>\uc774\ub807\uac8c \ud558\uba74 \ud1a0\ud070\uc774 \uc720\ucd9c\ub418\uc5b4\ub3c4 \ud574\uc678\uc5d0\uc11c\ub294 \uc7ac\uc0dd\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cloudflare Stream API \uc5f0\ub3d9 \ub9e4\ub274\uc5bc \uc774 \ub9e4\ub274\uc5bc\uc740 Cloudflare Dashboard\ub97c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0, \uc790\uccb4 \uad6c\ucd95\ud55c Admin \ud398\uc774\uc9c0\uc5d0\uc11c API\ub97c \ud1b5\ud574 \ub3d9\uc601\uc0c1\uc744 \uad00\ub9ac\ud558\ub294 \uac1c\ubc1c\uc790\ub97c \uc704\ud574 \uc791\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc0ac\uc804 \uc900\ube44 (Prerequisites) API\ub97c \ud638\ucd9c\ud558\uae30 \uc704\ud574 Cloudflare \ub300\uc2dc\ubcf4\ub4dc\uc5d0\uc11c \ub2e4\uc74c \ub450 \uac00\uc9c0 \uc815\ubcf4\ub97c \uba3c\uc800 \ud655\ubcf4\ud574\uc57c \ud569\ub2c8\ub2e4. \uc5c5\ub85c\ub4dc \ubc29\uc2dd \uacb0\uc815 (Architecture) \ub3d9\uc601\uc0c1 \ud30c\uc77c\uc740 \ud06c\uae30\uac00 \ud06c\uae30 \ub54c\ubb38\uc5d0 \uc5c5\ub85c\ub4dc \ubc29\uc2dd\uc744 \uc2e0\uc911\ud788 \uc120\ud0dd\ud574\uc57c \ud569\ub2c8\ub2e4. \ubc29\uc2dd \uc124\uba85 \ucd94\ucc9c \uc2dc\ub098\ub9ac\uc624 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-2017","post","type-post","status-publish","format-standard","hentry","category-3"],"_links":{"self":[{"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/2017","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2017"}],"version-history":[{"count":3,"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/2017\/revisions"}],"predecessor-version":[{"id":2021,"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/2017\/revisions\/2021"}],"wp:attachment":[{"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2017"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2017"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hyunsu.com\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2017"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}