{"id":191,"date":"2026-10-27T21:49:35","date_gmt":"2026-10-27T21:49:35","guid":{"rendered":"https:\/\/dinggo.duckdns.org\/?p=191"},"modified":"2026-10-31T09:54:41","modified_gmt":"2026-10-31T09:54:41","slug":"%ea%b0%9c%eb%b0%9c%ec%9d%bc%ec%a7%80-06-%eb%b0%b1%ec%97%94%eb%93%9c-%ec%b4%88%eb%8c%80%ec%bd%94%eb%93%9c-%ed%9a%8c%ec%9b%90%ea%b0%80%ec%9e%85-argon2id-%ec%95%94%ed%98%b8%ed%99%94-%eb%8b%a4","status":"publish","type":"post","link":"https:\/\/dingdong.goodday.ing\/?p=191","title":{"rendered":"[\uac1c\ubc1c\uc77c\uc9c0 #06 | \ubc31\uc5d4\ub4dc] \ucd08\ub300\ucf54\ub4dc \ud68c\uc6d0\uac00\uc785, Argon2id \uc554\ud638\ud654 &amp; \ub2e4\uc911 \uae30\uae30 JWT \uc778\uc99d \uad6c\ud604\ud558\uae30"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">\uc548\ub155\ud558\uc138\uc694! \uce74\uce74\uc624\ud1a1 \uc2a4\ud0c0\uc77c\uc758 \uac00\uc871\/\uce5c\uad6c\uc6a9 \ud504\ub77c\uc774\ube57 \ud06c\ub85c\uc2a4\ud50c\ub7ab\ud3fc \uba54\uc2e0\uc800 <strong>&#8216;\ub529\ub3d9(DingDong)&#8217;<\/strong> \ub9cc\ub4e4\uae30 \ud504\ub85c\uc81d\ud2b8 6\ud3b8\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc9c0\ub09c [<strong>5\ud3b8<\/strong>]\uc5d0\uc11c\ub294 \uba54\uc2e0\uc800\uc758 \uc2ec\uc7a5\uc778 PostgreSQL 16 \uc2a4\ud0a4\ub9c8(11\uac1c \ud575\uc2ec \ud14c\uc774\ube14)\ub97c \uad6c\ucd95\ud558\uace0, <code>sqlc<\/code>\uc640 <code>pgxpool<\/code>\uc744 \uc5f0\ub3d9\ud574 \ud0c0\uc785 \uc548\uc804\ud55c DB \ub808\uc774\uc5b4\ub97c \uc644\uc131\ud588\uc2b5\ub2c8\ub2e4.<br>\uc774\ubc88 6\ud3b8\uc5d0\uc11c\ub294 \uadf8 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc704\uc5d0 <strong>\ud504\ub77c\uc774\ube57 \ucd08\ub300\ucf54\ub4dc \uac80\uc99d, \uc694\uc998 \uad8c\uc7a5\ub418\ub294 Argon2id \ube44\ubc00\ubc88\ud638 \ud574\uc2f1, \ub2e4\uc911 \uae30\uae30(\uc2a4\ub9c8\ud2b8\ud3f0\/\ud0dc\ube14\ub9bf\/PC) \ub3d9\uc2dc \uc811\uc18d\uc744 \uc9c0\uc6d0\ud558\ub294 JWT Access\/Refresh \ud1a0\ud070 \uc2dc\uc2a4\ud15c, \uadf8\ub9ac\uace0 Chi \uc778\uc99d \ubbf8\ub4e4\uc6e8\uc5b4<\/strong>\ub97c \ucc98\uc74c\ubd80\ud130 \ub05d\uae4c\uc9c0 \uad6c\ud604\ud574 \ubcf4\uaca0\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\">1. \ub529\ub3d9 \uba54\uc2e0\uc800\uc758 4\ub300 \ubcf4\uc548 &amp; \uc778\uc99d \uc124\uacc4 \uc6d0\uce59<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uac00\uc871\uacfc \uc9c0\uc778\ub07c\ub9ac\ub9cc \uc548\uc804\ud558\uac8c \uc0ac\uc6a9\ud558\ub294 \ud504\ub77c\uc774\ube57 \uba54\uc2e0\uc800\uc774\uae30 \ub54c\ubb38\uc5d0, \uc544\ubb34\ub098 \uac00\uc785\ud560 \uc218 \uc5c6\ub3c4\ub85d \uc785\uad6c\ubd80\ud130 \ucca0\uc800\ud558\uac8c \uc124\uacc4\ud588\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<div class=\"wp-block-canaryblue-mermaid-diagram is-theme-default\" data-mermaid-label=\"Mermaid diagram\" data-mermaid-theme=\"default\"><pre class=\"wp-block-canaryblue-mermaid-diagram__source\" data-mermaid-source=\"true\">flowchart LR\n    classDef titleStyle fill:#0f172a,stroke:#0284c7,stroke-width:2px,color:#ffffff;\n    classDef nodeStyle fill:#1e293b,stroke:#475569,stroke-width:2px,color:#ffffff;\n\n    A[\"\ud074\ub77c\uc774\uc5b8\ud2b8 (\ud68c\uc6d0\uac00\uc785 \uc694\uccad)\"]:::titleStyle -->|\"1. \uc785\ub825\uac12 \uac80\uc99d + Argon2id \ud574\uc2f1\"| B[\"\ud2b8\ub79c\uc7ad\uc158 \uc2dc\uc791 (WithTx)\"]:::nodeStyle\n    B -->|\"2. \ud558\ub098\uc758 \ud2b8\ub79c\uc7ad\uc158 \uc548\uc5d0\uc11c \uc21c\uc11c\ub300\ub85c \uc2e4\ud589\"| C[\"\ucd08\ub300\ucf54\ub4dc \ucc28\uac10 \u2192 users \u2192 auth_identities \u2192 devices\"]:::nodeStyle\n    C -->|\"3. \ubaa8\ub450 \uc131\uacf5\ud558\uba74 \ucee4\ubc0b (\ud558\ub098\ub77c\ub3c4 \uc2e4\ud328\ud558\uba74 \ub864\ubc31)\"| D[\"JWT \ubc1c\uae09 &amp; \uc751\ub2f5\"]:::titleStyle<\/pre><div class=\"wp-block-canaryblue-mermaid-diagram__output\"><\/div><\/div>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\ucd08\ub300\ucf54\ub4dc \uae30\ubc18 \ud3d0\uc1c4\ud615 \uac00\uc785 (<code>invite_codes<\/code>):<\/strong> \uad00\ub9ac\uc790\uac00 \ubc1c\uae09\ud55c \ucd08\ub300\ucf54\ub4dc\uac00 \uc788\uc5b4\uc57c\ub9cc \uac00\uc785\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub3d9\uc2dc\uc5d0 \uc5ec\ub7ec \uba85\uc774 \uac19\uc740 \ucf54\ub4dc\ub85c \uac00\uc785\ud560 \ub54c \ud55c\ub3c4(<code>max_uses<\/code>)\uac00 \ub6ab\ub9ac\uc9c0 \uc54a\ub3c4\ub85d DB \ub808\ubca8\uc758 \uc6d0\uc790\uc801 \uc5c5\ub370\uc774\ud2b8(<code>UPDATE ... WHERE used_count &lt; max_uses<\/code>)\ub97c \uc801\uc6a9\ud569\ub2c8\ub2e4. \ucd08\ub300\ucf54\ub4dc \ucc28\uac10\uc740 \ud68c\uc6d0 \uc0dd\uc131\uacfc <strong>\uac19\uc740 \ud2b8\ub79c\uc7ad\uc158<\/strong> \uc548\uc5d0\uc11c \uc2e4\ud589\ub418\ubbc0\ub85c, \uac00\uc785\uc774 \uc911\uac04\uc5d0 \uc2e4\ud328\ud558\uba74 \uc0ac\uc6a9 \ud69f\uc218\ub3c4 \ud568\uaed8 \ub418\ub3cc\uc544\uac11\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\ucc28\uc138\ub300 \ube44\ubc00\ubc88\ud638 \ud574\uc2f1 (<code>Argon2id<\/code>):<\/strong> 2015\ub144 \ud328\uc2a4\uc6cc\ub4dc \ud574\uc2f1 \ub300\ud68c(PHC) \uc6b0\uc2b9 \uc54c\uace0\ub9ac\uc998\uc778 <strong>Argon2id<\/strong>\ub294 \uba54\ubaa8\ub9ac \ud558\ub4dc(Memory-hard) \ubc29\uc2dd\uc774\ub77c, GPU\/ASIC\uc744 \uc774\uc6a9\ud55c \ubb34\ucc28\ubcc4 \ub300\uc785(Brute-force) \uacf5\uaca9\uc758 \ube44\uc6a9\uc744 \ud06c\uac8c \ub192\uc5ec \uc90d\ub2c8\ub2e4. (<code>bcrypt<\/code>\ub3c4 \uc5ec\uc804\ud788 \uc548\uc804\ud558\ub2e4\uace0 \ud3c9\uac00\ubc1b\ub294 \uc54c\uace0\ub9ac\uc998\uc774\uc9c0\ub9cc, \ucd5c\uadfc \uc2e0\uaddc \uc11c\ube44\uc2a4\uc5d0\ub294 Argon2id\uac00 \uc6b0\uc120 \uad8c\uc7a5\ub429\ub2c8\ub2e4.)<\/li>\n\n\n\n<li><strong>\ub2e4\uc911 \uae30\uae30 \uc138\uc158 \ubd84\ub9ac (<code>devices<\/code> \ud14c\uc774\ube14):<\/strong> \uc2a4\ub9c8\ud2b8\ud3f0, \uc544\uc774\ud328\ub4dc, \ub9e5\ubd81\uc5d0\uc11c \ub3d9\uc2dc\uc5d0 \ub85c\uadf8\uc778\ud574\ub3c4 \uc11c\ub85c \ud295\uae30\uc9c0 \uc54a\ub3c4\ub85d \uae30\uae30\ubcc4\ub85c \uace0\uc720\ud55c <strong>Refresh Token<\/strong>\uc744 \ubc1c\uae09\ud558\uace0, \uc6d0\ubcf8\uc774 \uc544\ub2cc <strong>SHA-256 \ud574\uc2dc\uac12<\/strong>\ub9cc DB\uc5d0 \uc800\uc7a5\ud569\ub2c8\ub2e4. DB\uac00 \uc720\ucd9c\ub418\uc5b4\ub3c4 \ud574\uc2dc\ub9cc\uc73c\ub85c\ub294 \uc6d0\ubcf8 \ud1a0\ud070\uc744 \uc54c\uc544\ub0bc \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. (Refresh Token\uc740 32\ubc14\uc774\ud2b8 \uc554\ud638\ud559\uc801 \ub09c\uc218\ub77c\uc11c SHA-256\ub9cc\uc73c\ub85c \ucda9\ubd84\ud569\ub2c8\ub2e4.)<\/li>\n\n\n\n<li><strong>\ucd08\uacbd\ub7c9 Access Token + \uc548\uc804\ud55c Refresh Token:<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>Access Token (\uc720\ud6a8\uae30\uac04 15\ubd84):<\/strong> API \ud638\ucd9c \ubc0f WebSocket \uc5f0\uacb0 \uc2dc \uc0ac\uc6a9. DB \uc870\ud68c \uc5c6\uc774 \uc11c\ubc84 \uba54\ubaa8\ub9ac\uc5d0\uc11c \uc989\uc2dc \uac80\uc99d(Stateless).<\/li>\n\n\n\n<li><strong>Refresh Token (\uc720\ud6a8\uae30\uac04 30\uc77c):<\/strong> Access Token \ub9cc\ub8cc \uc2dc \uc7ac\ubc1c\uae09\uc6a9. \uae30\uae30 \ubd84\uc2e4 \uc2dc \ud2b9\uc815 \uae30\uae30\uc758 Refresh Token\uc744 \uc0ad\uc81c\ud574 \uc6d0\uaca9 \ub85c\uadf8\uc544\uc6c3\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li>\u26a0\ufe0f \uc774\ubbf8 \ubc1c\uae09\ub41c Access Token\uc740 \ucd5c\ub300 15\ubd84\uac04 \uacc4\uc18d \uc720\ud6a8\ud558\uace0, \uc774\ubbf8 \uc5f0\uacb0\ub41c WebSocket\ub3c4 \uc790\ub3d9\uc73c\ub85c \ub04a\uae30\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc6d0\uaca9 \ub85c\uadf8\uc544\uc6c3\ud560 \ub54c\ub294 \ud574\ub2f9 \uae30\uae30\uc758 WebSocket \uc5f0\uacb0\ub3c4 \uc11c\ubc84\uc5d0\uc11c \ud568\uaed8 \ub04a\uc5b4\uc57c \ud558\uba70, \uc774 \ubd80\ubd84\uc740 \ucc44\ud305(WebSocket) \ud3b8\uc5d0\uc11c \ub2e4\ub8f9\ub2c8\ub2e4.<\/li>\n<\/ul>\n<\/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\">2. \ud544\uc218 \uc554\ud638\ud654 \ubc0f JWT \ud328\ud0a4\uc9c0 \uc124\uce58<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\ub0b4 \ub9e5(Mac) \ud130\ubbf8\ub110\uc758 <code>server\/<\/code> \ud3f4\ub354\uc5d0\uc11c Go \uacf5\uc2dd \ud655\uc7a5 \uc554\ud638\ud654 \ud328\ud0a4\uc9c0(<code>x\/crypto<\/code>)\uc640 \ub110\ub9ac \uc4f0\uc774\ub294 JWT \ub77c\uc774\ube0c\ub7ec\ub9ac(<code>golang-jwt\/jwt\/v5<\/code>), UUID \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc124\uce58\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">cd server\n\n# 1. Argon2id \uc554\ud638\ud654\ub97c \uc704\ud55c Go \uacf5\uc2dd \ud655\uc7a5 \ud328\ud0a4\uc9c0\ngo get golang.org\/x\/crypto\/argon2\n\n# 2. JWT \uc0dd\uc131 \ubc0f \uac80\uc99d \ub77c\uc774\ube0c\ub7ec\ub9ac (v5)\ngo get github.com\/golang-jwt\/jwt\/v5\n\n# 3. UUID \ub77c\uc774\ube0c\ub7ec\ub9ac (\uc0ac\uc6a9\uc790 ID \ud0c0\uc785)\ngo get github.com\/google\/uuid<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. \uc778\uc99d \uad00\ub828 SQL \ucffc\ub9ac \uc791\uc131 \ubc0f <code>sqlc<\/code> \ucf54\ub4dc \uc0dd\uc131<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">5\ud3b8\uc5d0\uc11c \ub9cc\ub4e0 <code>server\/queries\/<\/code> \ud3f4\ub354 \uc548\uc5d0 \uc778\uc99d \uc804\uc6a9 \ucffc\ub9ac \ud30c\uc77c(<code>auth.sql<\/code>)\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.1 <code>server\/queries\/auth.sql<\/code> \uc791\uc131<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">VS Code\uc5d0\uc11c <code>server\/queries\/auth.sql<\/code> \ud30c\uc77c\uc744 \ub9cc\ub4e4\uace0 \uc544\ub798 \ucffc\ub9ac\ub97c \ubd99\uc5ec\ub123\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- name: UseInviteCode :one\n-- \uc720\ud6a8\uae30\uac04\uc774 \ub0a8\uace0 \uc0ac\uc6a9 \ud69f\uc218\uac00 \ucd08\uacfc\ub418\uc9c0 \uc54a\uc740 \ucd08\ub300\ucf54\ub4dc\ub9cc \uc6d0\uc790\uc801\uc73c\ub85c 1 \uc99d\uac00\uc2dc\ud0b4\nUPDATE invite_codes\nSET used_count = used_count + 1\nWHERE code = $1\n  AND expires_at > now()\n  AND used_count &lt; max_uses\nRETURNING *;\n\n-- name: CreateInviteCode :one\nINSERT INTO invite_codes (code, owner_id, max_uses, expires_at)\nVALUES ($1, $2, $3, $4)\nRETURNING *;\n\n-- name: CreateAuthIdentity :one\nINSERT INTO auth_identities (user_id, provider, provider_uid, password_hash)\nVALUES ($1, $2, $3, $4)\nRETURNING *;\n\n-- name: GetAuthIdentityByUID :one\nSELECT * FROM auth_identities\nWHERE provider = $1 AND provider_uid = $2;\n\n-- name: CreateDevice :one\nINSERT INTO devices (\n    user_id, platform, name, refresh_token_hash, refresh_expires_at, fcm_token, app_version, last_active_at\n)\nVALUES ($1, $2, $3, $4, $5, $6, $7, now())\nRETURNING *;\n\n-- name: GetDeviceByRefreshToken :one\nSELECT * FROM devices\nWHERE refresh_token_hash = $1\n  AND refresh_expires_at > now();\n\n-- name: UpdateDeviceRefreshToken :one\nUPDATE devices\nSET refresh_token_hash = $2,\n    refresh_expires_at = $3,\n    last_active_at = now()\nWHERE id = $1\nRETURNING *;\n\n-- name: DeleteDevice :exec\nDELETE FROM devices\nWHERE id = $1 AND user_id = $2;<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 <strong>\ucc38\uace0:<\/strong> <code>expires_at &gt; now()<\/code> \uc870\uac74\uc740 <code>expires_at<\/code>\uc774 <code>NULL<\/code>\uc774\uba74 \ud56d\uc0c1 \uac70\uc9d3\uc774 \ub418\uc5b4 \uadf8 \ucf54\ub4dc\ub294 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. 5\ud3b8 \uc2a4\ud0a4\ub9c8\uc5d0\uc11c <code>invite_codes.expires_at<\/code>\uc774 <code>NOT NULL<\/code>\uc778\uc9c0 \ud655\uc778\ud574 \uc8fc\uc138\uc694. (\ub9cc\ub8cc \uc5c6\ub294 \ucf54\ub4dc\ub97c \ud5c8\uc6a9\ud558\uace0 \uc2f6\ub2e4\uba74 <code>AND (expires_at IS NULL OR expires_at &gt; now())<\/code>\ub85c \ubc14\uafb8\uba74 \ub429\ub2c8\ub2e4.)<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">3.2 <code>sqlc generate<\/code> \uc2e4\ud589\ud558\uc5ec Go \ucf54\ub4dc \uc790\ub3d9 \uc0dd\uc131<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ud130\ubbf8\ub110\uc5d0\uc11c \uba85\ub839\uc5b4 \ud55c \uc904\ub85c \ubc29\uae08 \uc791\uc131\ud55c SQL\uc744 Go \ud568\uc218\ub85c \ubcc0\ud658\ud569\ub2c8\ub2e4!<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">sqlc generate<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\uc2e4\ud589 \uc989\uc2dc <code>internal\/database\/db\/auth.sql.go<\/code> \ud30c\uc77c\uc774 \uc0dd\uc131\ub418\uba70, <code>UseInviteCode<\/code>, <code>CreateAuthIdentity<\/code>, <code>CreateDevice<\/code> \ub4f1\uc758 \ud0c0\uc785 \uc548\uc804\ud55c \uba54\uc11c\ub4dc\uac00 \uc900\ube44\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\">4. \ubcf4\uc548 \ud575\uc2ec \ubaa8\ub4c8 \uad6c\ud604 (<code>internal\/auth\/<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\uc81c <code>server\/internal\/auth\/<\/code> \ud3f4\ub354\ub97c \ub9cc\ub4e4\uace0 4\uac1c\uc758 \ud575\uc2ec \ud30c\uc77c\uc744 \ucc28\ub840\ub300\ub85c \uc791\uc131\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">server\/internal\/auth\/\n \u251c\u2500 password.go      # 1. Argon2id \ube44\ubc00\ubc88\ud638 \ud574\uc2f1 &amp; \uac80\uc99d, SHA-256 \/ HMAC \ud574\uc2dc \uc720\ud2f8\n \u251c\u2500 jwt.go           # 2. JWT Access Token \ubc0f \ub79c\ub364 Refresh Token \uc0dd\uc131\uae30\n \u251c\u2500 middleware.go    # 3. Chi \ub77c\uc6b0\ud130\uc6a9 JWT \uc778\uc99d \uc218\ubb38\uc7a5 \ubbf8\ub4e4\uc6e8\uc5b4\n \u2514\u2500 handler.go       # 4. \ud68c\uc6d0\uac00\uc785(\/register), \ub85c\uadf8\uc778(\/login), \ud1a0\ud070 \uac31\uc2e0(\/refresh) API<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4.1 \ube44\ubc00\ubc88\ud638 \ud574\uc2f1 \ubc0f \ubcf4\uc548 \uc720\ud2f8 (<code>server\/internal\/auth\/password.go<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Argon2id(\uba54\ubaa8\ub9ac 64MB, \ubc18\ubcf5 1\ud68c, \ubcd1\ub82c 4\uc2a4\ub808\ub4dc)<\/strong> \uc124\uc815\uc785\ub2c8\ub2e4. OWASP\uac00 \uc81c\uc2dc\ud558\ub294 \ucd5c\uc18c \uad8c\uc7a5 \uc870\ud569(\uc608: \uba54\ubaa8\ub9ac 19MiB\u00b7\ubc18\ubcf5 2\ud68c\u00b7\ubcd1\ub82c 1, \ub610\ub294 \uba54\ubaa8\ub9ac 46MiB\u00b7\ubc18\ubcf5 1\ud68c\u00b7\ubcd1\ub82c 1)\ubcf4\ub2e4 \uba54\ubaa8\ub9ac\ub97c \ub109\ub109\ud558\uac8c \uc4f0\ub294 \uc124\uc815\uc774\ub77c, \ucd5c\uc18c \uae30\uc900 \uc774\uc0c1\uc774\ub77c\uace0 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u26a0\ufe0f <strong>NAS \uc790\uc6d0 \ucc38\uace0:<\/strong> \ud574\uc2f1 \ud55c \ubc88\uc5d0 \uba54\ubaa8\ub9ac 64MB\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4. \uac00\uc871\uc6a9 \uaddc\ubaa8\uc5d0\uc11c\ub294 \ubb38\uc81c\uc5c6\uc9c0\ub9cc, \ub3d9\uc2dc\uc5d0 \uc218\uc2ed \uba85\uc774 \uac00\uc785\/\ub85c\uadf8\uc778\ud558\uba74 \uba54\ubaa8\ub9ac\uac00 \uc21c\uac04\uc801\uc73c\ub85c \ucee4\uc9d1\ub2c8\ub2e4. \ub098\uc911\uc5d0 \uc678\ubd80\uc5d0 \uacf5\uac1c\ud55c\ub2e4\uba74 \ub3d9\uc2dc \ud574\uc2f1 \uac1c\uc218\ub97c \uc81c\ud55c(\uc138\ub9c8\ud3ec\uc5b4 \ub4f1)\ud558\ub294 \uac83\uc744 \uace0\ub824\ud558\uc138\uc694.<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"golang\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">package auth\n\nimport (\n\t\"crypto\/hmac\"\n\t\"crypto\/rand\"\n\t\"crypto\/sha256\"\n\t\"crypto\/subtle\"\n\t\"encoding\/base64\"\n\t\"encoding\/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org\/x\/crypto\/argon2\"\n)\n\n\/\/ Argon2id \ud30c\ub77c\ubbf8\ud130 (OWASP \ucd5c\uc18c \uad8c\uc7a5 \uc774\uc0c1, \uc800\uc0ac\uc591 NAS\uc5d0\uc11c\ub3c4 \ub3d9\uc791\ud558\ub294 \uc218\uc900)\nconst (\n\targonTime    = 1\n\targonMemory  = 64 * 1024 \/\/ 64 MB\n\targonThreads = 4\n\targonKeyLen  = 32\n\tsaltLen      = 16\n)\n\n\/\/ HashPassword: \ud3c9\ubb38 \ube44\ubc00\ubc88\ud638\ub97c Argon2id \ud45c\uc900 \ubb38\uc790\uc5f4 \ud3ec\ub9f7\uc73c\ub85c \ud574\uc2f1\ud569\ub2c8\ub2e4.\nfunc HashPassword(password string) (string, error) {\n\tsalt := make([]byte, saltLen)\n\tif _, err := rand.Read(salt); err != nil {\n\t\treturn \"\", err\n\t}\n\n\thash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)\n\n\tb64Salt := base64.RawStdEncoding.EncodeToString(salt)\n\tb64Hash := base64.RawStdEncoding.EncodeToString(hash)\n\n\t\/\/ \ud45c\uc900 PHC \ubb38\uc790\uc5f4 \ud3ec\ub9f7\uc73c\ub85c \ubc18\ud658\n\tencoded := fmt.Sprintf(\"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s\",\n\t\targon2.Version, argonMemory, argonTime, argonThreads, b64Salt, b64Hash)\n\treturn encoded, nil\n}\n\n\/\/ VerifyPassword: \uc785\ub825\ub41c \ud3c9\ubb38 \ube44\ubc00\ubc88\ud638\uc640 DB\uc758 \ud574\uc2dc\uac12\uc744 \ud0c0\uc774\ubc0d \uacf5\uaca9\uc5d0 \uc548\uc804\ud558\uac8c \ube44\uad50\ud569\ub2c8\ub2e4.\n\/\/ \ud574\uc2dc \ubb38\uc790\uc5f4 \uc548\uc5d0 \uc800\uc7a5\ub41c \ud30c\ub77c\ubbf8\ud130(m, t, p)\ub85c \uac80\uc99d\ud558\ubbc0\ub85c,\n\/\/ \ub098\uc911\uc5d0 \uc704\uc758 const \uac12\uc744 \ubc14\uafd4\ub3c4 \uae30\uc874 \uc0ac\uc6a9\uc790\uc758 \ube44\ubc00\ubc88\ud638\ub294 \uacc4\uc18d \uac80\uc99d\ub429\ub2c8\ub2e4.\nfunc VerifyPassword(password, encodedHash string) (bool, error) {\n\tparts := strings.Split(encodedHash, \"$\")\n\t\/\/ \"$argon2id$v=19$m=...,t=...,p=...$salt$hash\" \u2192 \uc55e\uc774 \ube48 \ubb38\uc790\uc5f4\uc774\ub77c 6\uc870\uac01\n\tif len(parts) != 6 || parts[1] != \"argon2id\" {\n\t\treturn false, errors.New(\"\uc798\ubabb\ub41c \ud574\uc2dc \ud3ec\ub9f7\uc785\ub2c8\ub2e4\")\n\t}\n\n\tvar version int\n\tif _, err := fmt.Sscanf(parts[2], \"v=%d\", &amp;version); err != nil {\n\t\treturn false, err\n\t}\n\tif version != argon2.Version {\n\t\treturn false, errors.New(\"\uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 Argon2 \ubc84\uc804\uc785\ub2c8\ub2e4\")\n\t}\n\n\tvar memory, iterations uint32\n\tvar threads uint8\n\tif _, err := fmt.Sscanf(parts[3], \"m=%d,t=%d,p=%d\", &amp;memory, &amp;iterations, &amp;threads); err != nil {\n\t\treturn false, err\n\t}\n\n\tsalt, err := base64.RawStdEncoding.DecodeString(parts[4])\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tdecodedHash, err := base64.RawStdEncoding.DecodeString(parts[5])\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tcomparisonHash := argon2.IDKey([]byte(password), salt, iterations, memory, threads, uint32(len(decodedHash)))\n\n\t\/\/ ConstantTimeCompare\ub85c \ud0c0\uc774\ubc0d \uacf5\uaca9(Timing Attack) \ubc29\uc9c0\n\treturn subtle.ConstantTimeCompare(decodedHash, comparisonHash) == 1, nil\n}\n\n\/\/ HashSHA256: Refresh Token\ucc98\ub7fc \ucda9\ubd84\ud788 \uae34 \uc554\ud638\ud559\uc801 \ub09c\uc218\ub97c DB\uc5d0 \uc800\uc7a5\ud558\uae30 \uc804\n\/\/ \ub2e8\ubc29\ud5a5 SHA-256 \ud574\uc2dc\ub85c \ubcc0\ud658\ud569\ub2c8\ub2e4.\n\/\/ \u26a0\ufe0f \uc804\ud654\ubc88\ud638\ucc98\ub7fc \uacbd\uc6b0\uc758 \uc218\uac00 \uc791\uc740 \uac12\uc5d0\ub294 \uc0ac\uc6a9\ud558\uc9c0 \ub9c8\uc138\uc694! (\uc544\ub798 HashHMAC \uc0ac\uc6a9)\nfunc HashSHA256(input string) string {\n\tsum := sha256.Sum256([]byte(input))\n\treturn hex.EncodeToString(sum[:])\n}\n\n\/\/ HashHMAC: \uc804\ud654\ubc88\ud638(E.164)\ucc98\ub7fc \uacbd\uc6b0\uc758 \uc218\uac00 \uc791\uc740 \uac12\uc744 \uc11c\ubc84 \ube44\ubc00\ud0a4\ub85c \ud574\uc2f1\ud569\ub2c8\ub2e4.\n\/\/ \uadf8\ub0e5 SHA-256\ub9cc \uc4f0\uba74 \uc804\ud654\ubc88\ud638 \ub300\uc5ed(\uc57d \uc218\ucc9c\ub9cc \uac00\uc9c0)\uc744 \uc804\ubd80 \uacc4\uc0b0\ud574 \uc6d0\ub798 \ubc88\ud638\ub97c \ubcf5\uc6d0\ud560 \uc218 \uc788\uc9c0\ub9cc,\n\/\/ \ube44\ubc00\ud0a4\uac00 \ub4e4\uc5b4\uac04 HMAC\uc740 \ud0a4 \uc5c6\uc774\ub294 \ubcf5\uc6d0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\nfunc HashHMAC(key []byte, input string) string {\n\tmac := hmac.New(sha256.New, key)\n\tmac.Write([]byte(input))\n\treturn hex.EncodeToString(mac.Sum(nil))\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4.2 JWT \ubc0f Refresh \ud1a0\ud070 \ub9e4\ub2c8\uc800 (<code>server\/internal\/auth\/jwt.go<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Access Token\uc740 <code>HMAC-SHA256(HS256)<\/code>\uc73c\ub85c \uc11c\uba85\ud558\uace0, Refresh Token\uc740 \uc608\uce21 \ubd88\uac00\ub2a5\ud55c 32\ubc14\uc774\ud2b8 \uc554\ud638\ud559\uc801 \ub09c\uc218\ub85c \ubc1c\uae09\ud569\ub2c8\ub2e4. \ud1a0\ud070 \uac80\uc99d \uc2dc\uc5d0\ub294 <strong>\ud5c8\uc6a9 \uc54c\uace0\ub9ac\uc998(HS256)\uacfc \ubc1c\uae09\uc790(Issuer)\ub97c \uba85\uc2dc\uc801\uc73c\ub85c \ud655\uc778<\/strong>\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"golang\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">package auth\n\nimport (\n\t\"crypto\/rand\"\n\t\"encoding\/base64\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com\/golang-jwt\/jwt\/v5\"\n\t\"github.com\/google\/uuid\"\n)\n\nconst tokenIssuer = \"dingdong-server\"\n\ntype Claims struct {\n\tUserID uuid.UUID `json:\"uid\"`\n\tRole   string    `json:\"role\"`\n\tjwt.RegisteredClaims\n}\n\ntype TokenManager struct {\n\tsecretKey []byte\n}\n\nfunc NewTokenManager(secret string) *TokenManager {\n\treturn &amp;TokenManager{secretKey: []byte(secret)}\n}\n\n\/\/ GenerateAccessToken: 15\ubd84 \uc218\uba85\uc758 \ucd08\uacbd\ub7c9 JWT Access Token \ubc1c\uae09\nfunc (tm *TokenManager) GenerateAccessToken(userID uuid.UUID, role string) (string, error) {\n\tnow := time.Now()\n\tclaims := Claims{\n\t\tUserID: userID,\n\t\tRole:   role,\n\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\tIssuedAt:  jwt.NewNumericDate(now),\n\t\t\tExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),\n\t\t\tIssuer:    tokenIssuer,\n\t\t},\n\t}\n\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\treturn token.SignedString(tm.secretKey)\n}\n\n\/\/ ValidateAccessToken: JWT \uc11c\uba85, \ub9cc\ub8cc\uc2dc\uac04, \uc54c\uace0\ub9ac\uc998, \ubc1c\uae09\uc790 \uac80\uc99d\nfunc (tm *TokenManager) ValidateAccessToken(tokenStr string) (*Claims, error) {\n\ttoken, err := jwt.ParseWithClaims(\n\t\ttokenStr,\n\t\t&amp;Claims{},\n\t\tfunc(t *jwt.Token) (interface{}, error) {\n\t\t\treturn tm.secretKey, nil\n\t\t},\n\t\tjwt.WithValidMethods([]string{\"HS256\"}), \/\/ HS256 \uc678\uc758 \uc54c\uace0\ub9ac\uc998\uc740 \uac70\ubd80\n\t\tjwt.WithIssuer(tokenIssuer),             \/\/ \uc6b0\ub9ac \uc11c\ubc84\uac00 \ubc1c\uae09\ud55c \ud1a0\ud070\ub9cc \ud5c8\uc6a9\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclaims, ok := token.Claims.(*Claims)\n\tif !ok || !token.Valid {\n\t\treturn nil, errors.New(\"\uc720\ud6a8\ud558\uc9c0 \uc54a\uc740 \ud1a0\ud070\uc785\ub2c8\ub2e4\")\n\t}\n\treturn claims, nil\n}\n\n\/\/ GenerateRefreshToken: 30\uc77c \uc218\uba85\uc758 \uc554\ud638\ud559\uc801 \ub09c\uc218 Refresh Token \uc0dd\uc131 (\uc6d0\ubcf8\uacfc DB\uc800\uc7a5\uc6a9 \ud574\uc2dc \ud568\uaed8 \ubc18\ud658)\nfunc (tm *TokenManager) GenerateRefreshToken() (rawToken string, tokenHash string, expiresAt time.Time, err error) {\n\tb := make([]byte, 32)\n\tif _, err = rand.Read(b); err != nil {\n\t\treturn \"\", \"\", time.Time{}, err\n\t}\n\trawToken = base64.RawURLEncoding.EncodeToString(b)\n\ttokenHash = HashSHA256(rawToken)\n\texpiresAt = time.Now().Add(30 * 24 * time.Hour) \/\/ 30\uc77c \uc720\uc9c0\n\treturn rawToken, tokenHash, expiresAt, nil\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4.3 \uc778\uc99d \uc218\ubb38\uc7a5 \ubbf8\ub4e4\uc6e8\uc5b4 (<code>server\/internal\/auth\/middleware.go<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc694\uccad \ud5e4\ub354\uc5d0 \ub2f4\uc544 \ubcf4\ub0b8 <code>Authorization: Bearer &lt;\ud1a0\ud070&gt;<\/code>\uc744 \uac80\uc0ac\ud558\uace0, \ud1b5\uacfc\ud558\uba74 \uc694\uccad \ucee8\ud14d\uc2a4\ud2b8(<code>r.Context()<\/code>)\uc5d0 \ub85c\uadf8\uc778\ud55c \uc720\uc800\uc758 UUID\uc640 \uad8c\ud55c\uc744 \uc2ec\uc5b4\uc8fc\ub294 \ubbf8\ub4e4\uc6e8\uc5b4\uc785\ub2c8\ub2e4. \uc5d0\ub7ec \uc751\ub2f5\uc740 4.4\uc5d0\uc11c \ub9cc\ub4e4 <code>writeJSONError<\/code>\ub97c \uc0ac\uc6a9\ud574 <code>Content-Type: application\/json<\/code>\uc73c\ub85c \ub0b4\ub824\uac11\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"golang\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">package auth\n\nimport (\n\t\"context\"\n\t\"net\/http\"\n\t\"strings\"\n\n\t\"github.com\/google\/uuid\"\n)\n\ntype contextKey string\n\nconst (\n\tUserIDKey contextKey = \"userID\"\n\tRoleKey   contextKey = \"userRole\"\n)\n\n\/\/ Authenticator: \ubcf4\ud638\ub41c \ub77c\uc6b0\ud130 \uadf8\ub8f9 \uc55e\uc5d0 \uc138\uc6cc\ub450\ub294 JWT \uac80\uc99d \ubbf8\ub4e4\uc6e8\uc5b4\nfunc Authenticator(tm *TokenManager) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tauthHeader := r.Header.Get(\"Authorization\")\n\t\t\tif authHeader == \"\" || !strings.HasPrefix(authHeader, \"Bearer \") {\n\t\t\t\twriteJSONError(w, \"\uc778\uc99d \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4\", http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttokenStr := strings.TrimPrefix(authHeader, \"Bearer \")\n\t\t\tclaims, err := tm.ValidateAccessToken(tokenStr)\n\t\t\tif err != nil {\n\t\t\t\twriteJSONError(w, \"\ub9cc\ub8cc\ub418\uc5c8\uac70\ub098 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc740 \ud1a0\ud070\uc785\ub2c8\ub2e4\", http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t\/\/ \ucee8\ud14d\uc2a4\ud2b8\uc5d0 \uc720\uc800 ID\uc640 Role \uc8fc\uc785\n\t\t\tctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)\n\t\t\tctx = context.WithValue(ctx, RoleKey, claims.Role)\n\t\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t\t})\n\t}\n}\n\n\/\/ GetUserID: \ud578\ub4e4\ub7ec \ub0b4\ubd80\uc5d0\uc11c \ud604\uc7ac \ub85c\uadf8\uc778\ud55c \uc0ac\uc6a9\uc790\uc758 UUID\ub97c \uaebc\ub0b4\ub294 \ud5ec\ud37c \ud568\uc218\nfunc GetUserID(ctx context.Context) (uuid.UUID, bool) {\n\tid, ok := ctx.Value(UserIDKey).(uuid.UUID)\n\treturn id, ok\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4.4 \ud68c\uc6d0\uac00\uc785 \/ \ub85c\uadf8\uc778 \/ \ud1a0\ud070 \uac31\uc2e0 \ud578\ub4e4\ub7ec (<code>server\/internal\/auth\/handler.go<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">5\ud3b8\uc5d0\uc11c \ub9cc\ub4e0 <strong><code>database.WithTx<\/code> \ud2b8\ub79c\uc7ad\uc158 \ub798\ud37c<\/strong>\uac00 \uc5ec\uae30\uc11c \ube5b\uc744 \ubc1c\ud569\ub2c8\ub2e4! \ud68c\uc6d0\uac00\uc785 \uc2dc \u2460 \ucd08\ub300\ucf54\ub4dc \ucc28\uac10 \u27a1\ufe0f \u2461 \uc720\uc800 \uc0dd\uc131 \u27a1\ufe0f \u2462 \ub85c\uadf8\uc778 \uacc4\uc815 \uc0dd\uc131 \u27a1\ufe0f \u2463 \uae30\uae30 \uc138\uc158 \ub4f1\ub85d\uc774\ub77c\ub294 4\ub2e8\uacc4 \uc791\uc5c5\uc774 <strong>\ub2e8 \ud558\ub098\ub77c\ub3c4 \uc2e4\ud328\ud558\uba74 \uc804\ubd80 \uae54\ub054\ud558\uac8c \ub864\ubc31<\/strong>(Rollback)\ub429\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\ubc88 \ud578\ub4e4\ub7ec\uc5d0\ub294 \ub450 \uac00\uc9c0 \ubcf4\uc548 \uc7a5\uce58\uac00 \ub4e4\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uc804\ud654\ubc88\ud638\ub294 HMAC-SHA256(\uc11c\ubc84 \ube44\ubc00\ud0a4)\ub85c \uc800\uc7a5:<\/strong> \uacbd\uc6b0\uc758 \uc218\uac00 \uc791\uc740 \uc804\ud654\ubc88\ud638\ub294 \uc77c\ubc18 SHA-256\ub9cc\uc73c\ub85c\ub294 \ubcf5\uc6d0\ub420 \uc218 \uc788\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\ub85c\uadf8\uc778 \uc2dc \uc874\uc7ac\ud558\uc9c0 \uc54a\ub294 \uc544\uc774\ub514\uc5ec\ub3c4 \ub354\ubbf8 \ud574\uc2dc \uac80\uc99d\uc744 \ud55c \ubc88 \uc218\ud589:<\/strong> \uc751\ub2f5 \uc18d\ub3c4 \ucc28\uc774\ub85c &#8220;\uc774 \uc544\uc774\ub514\ub294 \uac00\uc785\ub418\uc5b4 \uc788\ub2e4&#8221;\ub294 \uc0ac\uc2e4\uc774 \ub178\ucd9c\ub418\ub294 \uac83\uc744 \ub9c9\uc2b5\ub2c8\ub2e4.<\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u26a0\ufe0f <strong>\uc8fc\uc758:<\/strong> \uc544\ub798 \ucf54\ub4dc\uc758 <code>import<\/code> \uacbd\ub85c\uc5d0 \uc788\ub294 <code>github.com\/yourname\/dingdong-server<\/code> \ubd80\ubd84\uc740 \ubcf8\uc778\uc758 <code>go.mod<\/code> \ubaa8\ub4c8 \uc774\ub984\uc5d0 \ub9de\uac8c \ubcc0\uacbd\ud574 \uc8fc\uc138\uc694!<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"golang\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">package auth\n\nimport (\n\t\"encoding\/json\"\n\t\"errors\"\n\t\"net\/http\"\n\t\"time\"\n\n\t\"github.com\/jackc\/pgx\/v5\/pgxpool\"\n\t\"github.com\/yourname\/dingdong-server\/internal\/database\"\n\t\"github.com\/yourname\/dingdong-server\/internal\/database\/db\"\n)\n\ntype Handler struct {\n\tpool      *pgxpool.Pool\n\tqueries   *db.Queries\n\ttm        *TokenManager\n\tphoneKey  []byte \/\/ \uc804\ud654\ubc88\ud638 HMAC \ud574\uc2f1\uc6a9 \uc11c\ubc84 \ube44\ubc00\ud0a4\n\tdummyHash string \/\/ \ub85c\uadf8\uc778 \uc2dc \uc5c6\ub294 \uc544\uc774\ub514\uc758 \uc751\ub2f5 \uc2dc\uac04\uc744 \ub9de\ucd94\uae30 \uc704\ud55c \ub354\ubbf8 \ud574\uc2dc\n}\n\nfunc NewHandler(pool *pgxpool.Pool, queries *db.Queries, tm *TokenManager, phoneKey []byte) *Handler {\n\tdummy, err := HashPassword(\"dingdong-dummy-password\")\n\tif err != nil {\n\t\tpanic(\"\ub354\ubbf8 \ud574\uc2dc \uc0dd\uc131 \uc2e4\ud328: \" + err.Error()) \/\/ \ub09c\uc218 \uc0dd\uc131 \uc2e4\ud328\ub294 \uc0ac\uc2e4\uc0c1 \uc77c\uc5b4\ub098\uc9c0 \uc54a\uc74c\n\t}\n\treturn &amp;Handler{pool: pool, queries: queries, tm: tm, phoneKey: phoneKey, dummyHash: dummy}\n}\n\n\/\/ --- \uc694\uccad\/\uc751\ub2f5 DTO ---\n\ntype RegisterRequest struct {\n\tInviteCode  string  `json:\"inviteCode\"`\n\tLoginID     string  `json:\"loginId\"`\n\tPassword    string  `json:\"password\"`\n\tDisplayName string  `json:\"displayName\"`\n\tPhone       *string `json:\"phone,omitempty\"` \/\/ \uc608: \"+821012345678\"\n\tPlatform    string  `json:\"platform\"`        \/\/ \"android\" | \"ios\" | \"macos\"\n\tDeviceName  string  `json:\"deviceName\"`      \/\/ \uc608: \"\uc544\ube60 Mac Studio\"\n}\n\ntype LoginRequest struct {\n\tLoginID    string `json:\"loginId\"`\n\tPassword   string `json:\"password\"`\n\tPlatform   string `json:\"platform\"`\n\tDeviceName string `json:\"deviceName\"`\n}\n\ntype RefreshRequest struct {\n\tRefreshToken string `json:\"refreshToken\"`\n}\n\ntype AuthResponse struct {\n\tAccessToken  string  `json:\"accessToken\"`\n\tRefreshToken string  `json:\"refreshToken\"`\n\tUser         db.User `json:\"user\"`\n}\n\n\/\/ 1. \ucd08\ub300\ucf54\ub4dc \uae30\ubc18 \ud68c\uc6d0\uac00\uc785 (POST \/api\/v1\/auth\/register)\nfunc (h *Handler) Register(w http.ResponseWriter, r *http.Request) {\n\tvar req RegisterRequest\n\tr.Body = http.MaxBytesReader(w, r.Body, 1&lt;&lt;20) \/\/ \uc694\uccad \ubcf8\ubb38 1MB \uc81c\ud55c\n\tif err := json.NewDecoder(r.Body).Decode(&amp;req); err != nil {\n\t\twriteJSONError(w, \"\uc798\ubabb\ub41c \uc694\uccad \ud615\uc2dd\uc785\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif req.InviteCode == \"\" || req.LoginID == \"\" || len(req.Password) &lt; 8 || len(req.Password) > 128 || req.DisplayName == \"\" {\n\t\twriteJSONError(w, \"\ud544\uc218 \uc785\ub825\uac12\uc774 \ub204\ub77d\ub418\uc5c8\uac70\ub098 \ube44\ubc00\ubc88\ud638\uac00 \ub108\ubb34 \uc9e7\uc2b5\ub2c8\ub2e4(\ucd5c\uc18c 8\uc790, \ucd5c\ub300 128\uc790)\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif req.Platform == \"\" {\n\t\treq.Platform = \"unknown\"\n\t}\n\n\tpwHash, err := HashPassword(req.Password)\n\tif err != nil {\n\t\twriteJSONError(w, \"\ube44\ubc00\ubc88\ud638 \uc554\ud638\ud654 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t\/\/ \uc804\ud654\ubc88\ud638\ub294 \uc11c\ubc84 \ube44\ubc00\ud0a4\ub97c \uc4f0\ub294 HMAC-SHA256\uc73c\ub85c \uc800\uc7a5 (\uc77c\ubc18 SHA-256 \uc0ac\uc6a9 \uae08\uc9c0)\n\tvar phoneHash *string\n\tif req.Phone != nil &amp;&amp; *req.Phone != \"\" {\n\t\thashed := HashHMAC(h.phoneKey, *req.Phone)\n\t\tphoneHash = &amp;hashed\n\t}\n\n\trawRefresh, refreshHash, refreshExp, err := h.tm.GenerateRefreshToken()\n\tif err != nil {\n\t\twriteJSONError(w, \"\ud1a0\ud070 \uc0dd\uc131 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar createdUser db.User\n\n\t\/\/ 5\ud3b8\uc5d0\uc11c \ub9cc\ub4e0 WithTx\ub85c 4\ub2e8\uacc4 DB \uc791\uc5c5\uc744 \ud558\ub098\uc758 \uc6d0\uc790\uc801 \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc74c!\n\t\/\/ \ucd08\ub300\ucf54\ub4dc \ucc28\uac10(\u2460)\ub3c4 \uc774 \ud2b8\ub79c\uc7ad\uc158 \uc548\uc5d0 \uc788\uc73c\ubbc0\ub85c, \u2461~\u2463\uac00 \uc2e4\ud328\ud558\uba74 \uc0ac\uc6a9 \ud69f\uc218\ub3c4 \ud568\uaed8 \ub864\ubc31\ub429\ub2c8\ub2e4.\n\terr = database.WithTx(r.Context(), h.pool, func(q *db.Queries) error {\n\t\t\/\/ \u2460 \ucd08\ub300\ucf54\ub4dc \uac80\uc99d \ubc0f \uc0ac\uc6a9 \ud69f\uc218 +1\n\t\t\/\/ (\ud544\uc694 \uc2dc \ubc18\ud658\ub41c \ucd08\ub300\ucf54\ub4dc\uc758 owner_id\ub85c users.invited_by\ub97c \ucc44\uc6b0\ub3c4\ub85d \ud655\uc7a5 \uac00\ub2a5)\n\t\t_, err := q.UseInviteCode(r.Context(), req.InviteCode)\n\t\tif err != nil {\n\t\t\treturn errors.New(\"\uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \ub9cc\ub8cc\ub41c \ucd08\ub300\ucf54\ub4dc\uc785\ub2c8\ub2e4\")\n\t\t}\n\n\t\t\/\/ \u2461 \uc0ac\uc6a9\uc790(users) \uc0dd\uc131\n\t\tcreatedUser, err = q.CreateUser(r.Context(), db.CreateUserParams{\n\t\t\tDisplayName:   req.DisplayName,\n\t\t\tStatusMessage: nil,\n\t\t\tPhoneHash:     phoneHash,\n\t\t\tRole:          \"user\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.New(\"\uc774\ubbf8 \ub4f1\ub85d\ub41c \uc804\ud654\ubc88\ud638\uc774\uac70\ub098 \uc0ac\uc6a9\uc790 \uc0dd\uc131\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\")\n\t\t}\n\n\t\t\/\/ \u2462 \ub85c\uadf8\uc778 \uc218\ub2e8(auth_identities) \uc0dd\uc131\n\t\t_, err = q.CreateAuthIdentity(r.Context(), db.CreateAuthIdentityParams{\n\t\t\tUserID:       createdUser.ID,\n\t\t\tProvider:     \"password\",\n\t\t\tProviderUid:  req.LoginID,\n\t\t\tPasswordHash: &amp;pwHash,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.New(\"\uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc778 \ub85c\uadf8\uc778 \uc544\uc774\ub514\uc785\ub2c8\ub2e4\")\n\t\t}\n\n\t\t\/\/ \u2463 \uae30\uae30 \uc138\uc158(devices) \ub4f1\ub85d\n\t\t_, err = q.CreateDevice(r.Context(), db.CreateDeviceParams{\n\t\t\tUserID:           createdUser.ID,\n\t\t\tPlatform:         req.Platform,\n\t\t\tName:             &amp;req.DeviceName,\n\t\t\tRefreshTokenHash: refreshHash,\n\t\t\tRefreshExpiresAt: refreshExp,\n\t\t})\n\t\tif err != nil {\n\t\t\t\/\/ DB \uc5d0\ub7ec \uc6d0\ubb38\uc774 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub178\ucd9c\ub418\uc9c0 \uc54a\ub3c4\ub85d \uba54\uc2dc\uc9c0\ub97c \ubc14\uafd4\uc11c \ubc18\ud658\n\t\t\treturn errors.New(\"\uae30\uae30 \uc138\uc158 \ub4f1\ub85d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\")\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\twriteJSONError(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\taccessToken, err := h.tm.GenerateAccessToken(createdUser.ID, createdUser.Role)\n\tif err != nil {\n\t\twriteJSONError(w, \"\uc561\uc138\uc2a4 \ud1a0\ud070 \ubc1c\uae09 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusCreated, AuthResponse{\n\t\tAccessToken:  accessToken,\n\t\tRefreshToken: rawRefresh,\n\t\tUser:         createdUser,\n\t})\n}\n\n\/\/ 2. \ub85c\uadf8\uc778 (POST \/api\/v1\/auth\/login)\nfunc (h *Handler) Login(w http.ResponseWriter, r *http.Request) {\n\tvar req LoginRequest\n\tr.Body = http.MaxBytesReader(w, r.Body, 1&lt;&lt;20)\n\tif err := json.NewDecoder(r.Body).Decode(&amp;req); err != nil {\n\t\twriteJSONError(w, \"\uc798\ubabb\ub41c \uc694\uccad \ud615\uc2dd\uc785\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tidentity, err := h.queries.GetAuthIdentityByUID(r.Context(), db.GetAuthIdentityByUIDParams{\n\t\tProvider:    \"password\",\n\t\tProviderUid: req.LoginID,\n\t})\n\tif err != nil || identity.PasswordHash == nil {\n\t\t\/\/ \uc5c6\ub294 \uc544\uc774\ub514\uc5ec\ub3c4 \ub354\ubbf8 \ud574\uc2dc \uac80\uc99d\uc744 \ub3cc\ub824\uc11c \uc751\ub2f5 \uc2dc\uac04 \ucc28\uc774\ub85c \uac00\uc785 \uc5ec\ubd80\uac00 \ub4dc\ub7ec\ub098\uc9c0 \uc54a\uac8c \ud568\n\t\t_, _ = VerifyPassword(req.Password, h.dummyHash)\n\t\twriteJSONError(w, \"\uc544\uc774\ub514 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\tmatch, err := VerifyPassword(req.Password, *identity.PasswordHash)\n\tif err != nil || !match {\n\t\twriteJSONError(w, \"\uc544\uc774\ub514 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\tuser, err := h.queries.GetUserByID(r.Context(), identity.UserID)\n\tif err != nil {\n\t\twriteJSONError(w, \"\ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc740 \uacc4\uc815\uc785\ub2c8\ub2e4\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\trawRefresh, refreshHash, refreshExp, err := h.tm.GenerateRefreshToken()\n\tif err != nil {\n\t\twriteJSONError(w, \"\ud1a0\ud070 \uc0dd\uc131 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif req.Platform == \"\" {\n\t\treq.Platform = \"unknown\"\n\t}\n\n\t_, err = h.queries.CreateDevice(r.Context(), db.CreateDeviceParams{\n\t\tUserID:           user.ID,\n\t\tPlatform:         req.Platform,\n\t\tName:             &amp;req.DeviceName,\n\t\tRefreshTokenHash: refreshHash,\n\t\tRefreshExpiresAt: refreshExp,\n\t})\n\tif err != nil {\n\t\twriteJSONError(w, \"\uae30\uae30 \uc138\uc158 \ub4f1\ub85d \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\taccessToken, err := h.tm.GenerateAccessToken(user.ID, user.Role)\n\tif err != nil {\n\t\twriteJSONError(w, \"\uc561\uc138\uc2a4 \ud1a0\ud070 \ubc1c\uae09 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, AuthResponse{\n\t\tAccessToken:  accessToken,\n\t\tRefreshToken: rawRefresh,\n\t\tUser:         user,\n\t})\n}\n\n\/\/ 3. \ud1a0\ud070 \uc7ac\ubc1c\uae09 - Refresh Token Rotation \uc801\uc6a9 (POST \/api\/v1\/auth\/refresh)\nfunc (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) {\n\tvar req RefreshRequest\n\tr.Body = http.MaxBytesReader(w, r.Body, 1&lt;&lt;20)\n\tif err := json.NewDecoder(r.Body).Decode(&amp;req); err != nil || req.RefreshToken == \"\" {\n\t\twriteJSONError(w, \"\ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tinputHash := HashSHA256(req.RefreshToken)\n\tdevice, err := h.queries.GetDeviceByRefreshToken(r.Context(), inputHash)\n\tif err != nil {\n\t\twriteJSONError(w, \"\ub9cc\ub8cc\ub418\uc5c8\uac70\ub098 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc740 \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc785\ub2c8\ub2e4 (\ub2e4\uc2dc \ub85c\uadf8\uc778\ud574 \uc8fc\uc138\uc694)\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\tuser, err := h.queries.GetUserByID(r.Context(), device.UserID)\n\tif err != nil {\n\t\twriteJSONError(w, \"\uc0ac\uc6a9\uc790\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\t\/\/ [\ubcf4\uc548 \uafc0\ud301: Refresh Token Rotation]\n\t\/\/ \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc744 \ud55c \ubc88 \uc0ac\uc6a9\ud560 \ub54c\ub9c8\ub2e4 \uc0c8 \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc73c\ub85c \uad50\uccb4\ud558\uc5ec \ud0c8\ucde8 \uc704\ud5d8\uc744 \uc904\uc785\ub2c8\ub2e4.\n\tnewRawRefresh, newRefreshHash, newRefreshExp, err := h.tm.GenerateRefreshToken()\n\tif err != nil {\n\t\twriteJSONError(w, \"\ud1a0\ud070 \uac31\uc2e0 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t_, err = h.queries.UpdateDeviceRefreshToken(r.Context(), db.UpdateDeviceRefreshTokenParams{\n\t\tID:               device.ID,\n\t\tRefreshTokenHash: newRefreshHash,\n\t\tRefreshExpiresAt: newRefreshExp,\n\t})\n\tif err != nil {\n\t\twriteJSONError(w, \"\uc138\uc158 \uac31\uc2e0 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tnewAccessToken, err := h.tm.GenerateAccessToken(user.ID, user.Role)\n\tif err != nil {\n\t\twriteJSONError(w, \"\uc561\uc138\uc2a4 \ud1a0\ud070 \ubc1c\uae09 \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, AuthResponse{\n\t\tAccessToken:  newAccessToken,\n\t\tRefreshToken: newRawRefresh,\n\t\tUser:         user,\n\t})\n}\n\n\/\/ 4. \ub0b4 \uc815\ubcf4 \uc870\ud68c - \ubcf4\ud638\ub41c API \ud14c\uc2a4\ud2b8\uc6a9 (GET \/api\/v1\/users\/me)\nfunc (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) {\n\tuserID, ok := GetUserID(r.Context())\n\tif !ok {\n\t\twriteJSONError(w, \"\uc778\uc99d \uc815\ubcf4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\tuser, err := h.queries.GetUserByID(r.Context(), userID)\n\tif err != nil {\n\t\twriteJSONError(w, \"\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \ubd88\ub7ec\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, map[string]interface{}{\n\t\t\"user\":       user,\n\t\t\"serverTime\": time.Now().Format(time.RFC3339),\n\t})\n}\n\n\/\/ \uacf5\ud1b5 JSON \uc751\ub2f5 \ud5ec\ud37c\nfunc writeJSON(w http.ResponseWriter, status int, data interface{}) {\n\tw.Header().Set(\"Content-Type\", \"application\/json\")\n\tw.WriteHeader(status)\n\tjson.NewEncoder(w).Encode(data)\n}\n\nfunc writeJSONError(w http.ResponseWriter, msg string, status int) {\n\twriteJSON(w, status, map[string]string{\"error\": msg})\n}<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. <code>main.go<\/code>\uc5d0 \uc778\uc99d \ub77c\uc6b0\ud130 \uc5f0\uacb0\ud558\uae30 (<code>server\/cmd\/api\/main.go<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\uc81c 5\ud3b8\uc5d0\uc11c \uc791\uc131\ud588\ub358 <code>cmd\/api\/main.go<\/code>\uc5d0 <code>TokenManager<\/code>\uc640 <code>auth.Handler<\/code>\ub97c \uc5f0\uacb0\ud574 \uc90d\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\ubc88 \ubc84\uc804\ubd80\ud130\ub294 <strong>\ube44\ubc00\ud0a4\ub97c \ucf54\ub4dc\uc5d0 \uc801\uc5b4\ub450\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/strong> <code>JWT_SECRET<\/code>(JWT \uc11c\uba85\uc6a9)\uacfc <code>PHONE_HASH_SECRET<\/code>(\uc804\ud654\ubc88\ud638 \ud574\uc2f1\uc6a9)\uc744 \ud658\uacbd\ubcc0\uc218\ub85c \ubc18\ub4dc\uc2dc \uc9c0\uc815\ud574\uc57c \uc11c\ubc84\uac00 \uc2dc\uc791\ub418\uba70, \ub458 \ub2e4 <strong>\ucd5c\uc18c 32\uc790 \uc774\uc0c1\uc758 \ub79c\ub364 \uac12<\/strong>\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u26a0\ufe0f <code>PHONE_HASH_SECRET<\/code>\uc744 \ub098\uc911\uc5d0 \ubc14\uafb8\uba74 \uc774\ubbf8 \uc800\uc7a5\ub41c \uc804\ud654\ubc88\ud638 \ud574\uc2dc\uac00 \uc804\ubd80 \ubb34\ud6a8\uac00 \ub429\ub2c8\ub2e4. \ud55c \ubc88 \uc815\ud55c \ub4a4\uc5d0\ub294 \uc548\uc804\ud55c \uacf3\uc5d0 \ubc31\uc5c5\ud574 \ub450\uc138\uc694.<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"golang\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">package main\n\nimport (\n\t\"context\"\n\t\"encoding\/json\"\n\t\"errors\"\n\t\"log\"\n\t\"net\/http\"\n\t\"os\"\n\t\"os\/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com\/go-chi\/chi\/v5\"\n\t\"github.com\/go-chi\/chi\/v5\/middleware\"\n\t\"github.com\/go-chi\/cors\"\n\t\"github.com\/jackc\/pgx\/v5\/pgxpool\"\n\t\"github.com\/yourname\/dingdong-server\/internal\/auth\"\n\t\"github.com\/yourname\/dingdong-server\/internal\/database\/db\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\tport := os.Getenv(\"PORT\")\n\tif port == \"\" {\n\t\tport = \"3000\"\n\t}\n\n\t\/\/ DB \uc811\uc18d \uc8fc\uc18c(\ube44\ubc00\ubc88\ud638 \ud3ec\ud568)\ub294 \ucf54\ub4dc\uc5d0 \uc801\uc9c0 \uc54a\uace0 \ud658\uacbd\ubcc0\uc218\ub85c\ub9cc \ubc1b\uc2b5\ub2c8\ub2e4. (5\ud3b8\uacfc \uac19\uc740 \ubc29\uc2dd)\n\tdbURL := os.Getenv(\"DATABASE_URL\")\n\tif dbURL == \"\" {\n\t\tlog.Fatal(\"DATABASE_URL \ud658\uacbd\ubcc0\uc218\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uc608: postgres:\/\/dingdong:\ube44\ubc00\ubc88\ud638@localhost:5432\/dingdong?sslmode=disable\")\n\t}\n\n\t\/\/ \ube44\ubc00\ud0a4\ub294 \ucf54\ub4dc\uc5d0 \uae30\ubcf8\uac12\uc744 \ub450\uc9c0 \uc54a\uace0, \uc5c6\uac70\ub098 \uc9e7\uc73c\uba74 \uc11c\ubc84\ub97c \uc2dc\uc791\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n\tjwtSecret := os.Getenv(\"JWT_SECRET\")\n\tif len(jwtSecret) &lt; 32 {\n\t\tlog.Fatal(\"JWT_SECRET \ud658\uacbd\ubcc0\uc218\uac00 \uc5c6\uac70\ub098 \ub108\ubb34 \uc9e7\uc2b5\ub2c8\ub2e4(\ucd5c\uc18c 32\uc790). \uc608: export JWT_SECRET=$(openssl rand -base64 48)\")\n\t}\n\tphoneSecret := os.Getenv(\"PHONE_HASH_SECRET\")\n\tif len(phoneSecret) &lt; 32 {\n\t\tlog.Fatal(\"PHONE_HASH_SECRET \ud658\uacbd\ubcc0\uc218\uac00 \uc5c6\uac70\ub098 \ub108\ubb34 \uc9e7\uc2b5\ub2c8\ub2e4(\ucd5c\uc18c 32\uc790). \uc608: export PHONE_HASH_SECRET=$(openssl rand -base64 48)\")\n\t}\n\n\t\/\/ 1. PostgreSQL \ucee4\ub125\uc158 \ud480 \ucd08\uae30\ud654\n\tconfig, err := pgxpool.ParseConfig(dbURL)\n\tif err != nil {\n\t\tlog.Fatalf(\"DB \uc124\uc815 \ud30c\uc2f1 \uc2e4\ud328: %v\", err)\n\t}\n\tconfig.MaxConns = 25\n\tconfig.MinConns = 5\n\tconfig.MaxConnIdleTime = 5 * time.Minute\n\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\tlog.Fatalf(\"PostgreSQL \uc5f0\uacb0 \ud480 \uc0dd\uc131 \uc2e4\ud328: %v\", err)\n\t}\n\tdefer pool.Close()\n\n\tif err := pool.Ping(ctx); err != nil {\n\t\tlog.Fatalf(\"PostgreSQL Ping \uc2e4\ud328: %v\", err)\n\t}\n\tlog.Println(\"\ud83d\udc18 PostgreSQL 16 \uc5f0\uacb0 \ud480 \ucd08\uae30\ud654 \uc131\uacf5!\")\n\n\t\/\/ 2. \uc758\uc874\uc131 \uc8fc\uc785 (sqlc Queries, TokenManager, AuthHandler)\n\tqueries := db.New(pool)\n\ttokenManager := auth.NewTokenManager(jwtSecret)\n\tauthHandler := auth.NewHandler(pool, queries, tokenManager, []byte(phoneSecret))\n\n\t\/\/ 3. Chi \ub77c\uc6b0\ud130 \uc124\uc815\n\tr := chi.NewRouter()\n\tr.Use(middleware.RequestID)\n\tr.Use(middleware.RealIP)\n\tr.Use(middleware.Logger)\n\tr.Use(middleware.Recoverer)\n\n\t\/\/ \uac1c\ubc1c \ub2e8\uacc4\uc6a9 \ub290\uc2a8\ud55c CORS \uc124\uc815\uc785\ub2c8\ub2e4. (\ub124\uc774\ud2f0\ube0c \uc571\uc740 CORS\uc640 \ubb34\uad00\ud558\uba70,\n\t\/\/ \uc6f9 \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \ub9cc\ub4e4\uac8c \ub418\uba74 \ud5c8\uc6a9\ud560 \ub3c4\uba54\uc778\ub9cc \uc9c0\uc815\ud558\ub3c4\ub85d \uc881\ud600\uc57c \ud569\ub2c8\ub2e4.)\n\tr.Use(cors.Handler(cors.Options{\n\t\tAllowedOrigins:   []string{\"https:\/\/*\", \"http:\/\/*\"},\n\t\tAllowedMethods:   []string{\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"},\n\t\tAllowedHeaders:   []string{\"Accept\", \"Authorization\", \"Content-Type\", \"X-CSRF-Token\"},\n\t\tExposedHeaders:   []string{\"Link\"},\n\t\tAllowCredentials: false,\n\t\tMaxAge:           300,\n\t}))\n\n\t\/\/ 4. \uc77c\ubc18 REST API \ub77c\uc6b0\ud130 \uadf8\ub8f9\n\tr.Group(func(api chi.Router) {\n\t\tapi.Use(middleware.Timeout(60 * time.Second))\n\n\t\t\/\/ \ud5ec\uc2a4\uccb4\ud06c\n\t\tapi.Get(\"\/health\", func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\/\/ DB \ud655\uc778\uc774 \uc624\ub798 \uac78\ub824 \ud5ec\uc2a4\uccb4\ud06c \uc804\uccb4\uac00 \uba48\ucd94\uc9c0 \uc54a\ub3c4\ub85d 2\ucd08\ub85c \uc81c\ud55c (5\ud3b8\uacfc \uac19\uc740 \ubc29\uc2dd)\n\t\t\tpingCtx, cancel := context.WithTimeout(r.Context(), 2*time.Second)\n\t\t\tdefer cancel()\n\n\t\t\tstatus, dbStatus, code := \"ok\", \"ok\", http.StatusOK\n\t\t\tif err := pool.Ping(pingCtx); err != nil {\n\t\t\t\tstatus, dbStatus, code = \"degraded\", \"unreachable\", http.StatusServiceUnavailable\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application\/json\")\n\t\t\tw.WriteHeader(code)\n\t\t\tjson.NewEncoder(w).Encode(map[string]string{\n\t\t\t\t\"status\":  status,\n\t\t\t\t\"db\":      dbStatus,\n\t\t\t\t\"version\": \"v0.3.0\",\n\t\t\t})\n\t\t})\n\n\t\t\/\/ [\uacf5\uac1c API] \ud68c\uc6d0\uac00\uc785, \ub85c\uadf8\uc778, \ud1a0\ud070 \uc7ac\ubc1c\uae09 (\uc778\uc99d \ubd88\ud544\uc694)\n\t\tapi.Route(\"\/api\/v1\/auth\", func(r chi.Router) {\n\t\t\tr.Post(\"\/register\", authHandler.Register)\n\t\t\tr.Post(\"\/login\", authHandler.Login)\n\t\t\tr.Post(\"\/refresh\", authHandler.Refresh)\n\t\t})\n\n\t\t\/\/ [\ubcf4\ud638\ub41c API] JWT \uc778\uc99d \ubbf8\ub4e4\uc6e8\uc5b4 \ud1b5\uacfc \ud544\uc218!\n\t\tapi.Group(func(protected chi.Router) {\n\t\t\tprotected.Use(auth.Authenticator(tokenManager))\n\n\t\t\tprotected.Get(\"\/api\/v1\/users\/me\", authHandler.GetMe)\n\t\t})\n\t})\n\n\t\/\/ 5. \uc11c\ubc84 \uc2e4\ud589 \ubc0f Graceful Shutdown\n\tserver := &amp;http.Server{\n\t\tAddr:              \":\" + port,\n\t\tHandler:           r,\n\t\tReadHeaderTimeout: 10 * time.Second, \/\/ \ud070 \ud30c\uc77c \uc5c5\ub85c\ub4dc\ub97c \ub9c9\uc9c0 \uc54a\ub3c4\ub85d ReadTimeout \ub300\uc2e0 \uc0ac\uc6a9 (4\ud3b8 \ucc38\uace0)\n\t\tWriteTimeout:      0,\n\t\tIdleTimeout:       60 * time.Second,\n\t}\n\n\tgo func() {\n\t\tlog.Printf(\"\ud83d\ude80 \ub529\ub3d9 Go \ubc31\uc5d4\ub4dc \uc11c\ubc84(v0.3.0)\uac00 \ud3ec\ud2b8 %s\uc5d0\uc11c \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4!\", port)\n\t\tif err := server.ListenAndServe(); err != nil &amp;&amp; !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"\uc11c\ubc84 \uc2dc\uc791 \uc2e4\ud328: %v\", err)\n\t\t}\n\t}()\n\n\tquit := make(chan os.Signal, 1)\n\tsignal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)\n\t&lt;-quit\n\n\tshutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\t_ = server.Shutdown(shutdownCtx)\n\tlog.Println(\"\uc11c\ubc84\uac00 \uc548\uc804\ud558\uac8c \uc885\ub8cc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\")\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\uc791\uc131\uc774 \uc644\ub8cc\ub418\uc5c8\uc73c\uba74 \ud130\ubbf8\ub110\uc5d0\uc11c \ud328\ud0a4\uc9c0 \uc815\ub9ac\ub97c \ud55c \ubc88 \uc2e4\ud589\ud574 \uc90d\ub2c8\ub2e4. (\uc124\uce58\ud558\uc9c0 \uc54a\uc740 chi, cors, pgx \uac19\uc740 \ud328\ud0a4\uc9c0\ub3c4 \uc774 \uba85\ub839\uc774 \uc790\ub3d9\uc73c\ub85c \ubc1b\uc544 \uc90d\ub2c8\ub2e4.)<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">go mod tidy<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">5.1 NAS \ubc30\ud3ec \ub54c <code>.env<\/code>\uc5d0 \ube44\ubc00\ud0a4 \ucd94\uac00\ud558\uae30<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ub9e5\uc5d0\uc11c \ud14c\uc2a4\ud2b8\ub97c \ub9c8\uce58\uace0 NAS\uc5d0 \ubc30\ud3ec\ud560 \ub54c\ub294 4\ud3b8\uc5d0\uc11c \ub9cc\ub4e0 <code>\/volume1\/docker\/dingdong\/.env<\/code> \ud30c\uc77c\uc5d0 \ube44\ubc00\ud0a4 \ub450 \uc904\uc744 \ucd94\uac00\ud574\uc57c \ud569\ub2c8\ub2e4. 4\ud3b8\uc758 compose\uac00 <code>.env<\/code> \ud30c\uc77c\uc744 api \ucee8\ud14c\uc774\ub108\uc5d0 \uadf8\ub300\ub85c \uc804\ub2ec\ud558\ubbc0\ub85c <code>docker-compose.yml<\/code>\uc740 \uace0\uce60 \ud544\uc694\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. (<code>DATABASE_URL<\/code>\ub3c4 compose\uac00 \uc790\ub3d9\uc73c\ub85c \ub9cc\ub4e4\uc5b4 \ub123\uc5b4 \uc90d\ub2c8\ub2e4.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uac12\uc744 \uc9c1\uc811 \uc9c0\uc5b4\ub0b4\uc9c0 \ub9d0\uace0, NAS \ud130\ubbf8\ub110(SSH)\uc5d0\uc11c \uc544\ub798 \uba85\ub839\uc73c\ub85c \ub79c\ub364 \uac12\uc744 \ub9cc\ub4e4\uc5b4 \ubc14\ub85c \ud30c\uc77c \ub05d\uc5d0 \ucd94\uac00\ud558\uc138\uc694.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">cd \/volume1\/docker\/dingdong\necho \"JWT_SECRET=$(openssl rand -base64 48)\" >> .env\necho \"PHONE_HASH_SECRET=$(openssl rand -base64 48)\" >> .env<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u26a0\ufe0f \uc11c\ubc84\ub294 \uac12\uc758 <strong>\uae38\uc774\ub9cc<\/strong> \uac80\uc0ac\ud558\ubbc0\ub85c, \uc124\uba85\uc6a9 \ubb38\uad6c(&#8220;\ucd5c\uc18c_32\uc790_\uc774\uc0c1&#8230;&#8221;)\ub97c \uadf8\ub300\ub85c \ub123\uc5b4\ub3c4 \uc2dc\uc791\uc740 \ub429\ub2c8\ub2e4. \uadf8\ub7ec\uba74 \ube44\ubc00\ud0a4\uac00 \uc0ac\uc2e4\uc0c1 \uacf5\uac1c\ub41c \ubb38\uc7a5\uc774 \ub418\ub2c8 \uaf2d \uc704 \uba85\ub839\uc73c\ub85c \ub9cc\ub4e0 \ub79c\ub364 \uac12\uc744 \uc4f0\uc138\uc694.<br><code>.env<\/code> \ud30c\uc77c \ub9c8\uc9c0\ub9c9 \uc904 \ub05d\uc5d0 \uc904\ubc14\uafc8\uc774 \uc5c6\uc73c\uba74 <code>echo<\/code>\ub85c \ucd94\uac00\ud55c \uac12\uc774 \uc717\uc904\uc5d0 \ubd99\uc5b4 \ubc84\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ucd94\uac00\ud55c \ub4a4 <code>tail -n 3 .env<\/code>\ub85c \uc904\uc774 \ub098\ub258\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uc138\uc694.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6. \uc2e4\uc804 \ud14c\uc2a4\ud2b8: \uccab \ucd08\ub300\ucf54\ub4dc \uc0dd\uc131\ubd80\ud130 \ud68c\uc6d0\uac00\uc785 &amp; \ub0b4 \uc815\ubcf4 \uc870\ud68c\uae4c\uc9c0!<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\ub2ed\uc774 \uba3c\uc800\ub0d0 \ub2ec\uac40\uc774 \uba3c\uc800\ub0d0\uc758 \ubb38\uc81c!<br>\ucd08\ub300\ucf54\ub4dc(<code>invite_codes<\/code>)\uac00 \uc788\uc5b4\uc57c \ud68c\uc6d0\uac00\uc785\uc744 \ud560 \uc218 \uc788\ub294\ub370, \uc544\uc9c1 \ud68c\uc6d0\uc774 0\uba85\uc774\ub77c \ucd08\ub300\ucf54\ub4dc\ub97c \ub9cc\ub4e0 \uc0ac\ub78c(<code>owner_id<\/code>)\ub3c4 \uc5c6\uc2b5\ub2c8\ub2e4. \uadf8\ub798\uc11c <strong>\ucd5c\ucd08 1\ud68c, \uc2dc\uc2a4\ud15c \uad00\ub9ac\uc790 \uacc4\uc815\uacfc \uccab \ubc88\uc9f8 \ub9c8\uc2a4\ud130 \ucd08\ub300\ucf54\ub4dc(<code>DING26<\/code>)\ub97c DB\uc5d0 \uc9c1\uc811 \uc2ec\uc5b4\uc8fc\ub294 \ubd80\ud2b8\uc2a4\ud2b8\ub7a9(Bootstrap) SQL<\/strong>\uc744 \uc2e4\ud589\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.1 \ub9c8\uc2a4\ud130 \ucd08\ub300\ucf54\ub4dc(<code>DING26<\/code>) \uc0dd\uc131\ud558\uae30<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\uc720\uadf8\ub9b0 NAS \ud130\ubbf8\ub110(\ub610\ub294 DBeaver)\uc5d0\uc11c \uc544\ub798 \uba85\ub839\uc5b4\ub97c \ud55c \uc904 \uc2e4\ud589\ud558\uc5ec \ub9c8\uc2a4\ud130 \ucd08\ub300\ucf54\ub4dc\ub97c \ub9cc\ub4ed\ub2c8\ub2e4:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">docker exec -i dingdong-postgres psql -U dingdong -d dingdong &lt;&lt; 'EOF'\nWITH admin_user AS (\n    INSERT INTO users (display_name, role)\n    VALUES ('\uc2dc\uc2a4\ud15c \uad00\ub9ac\uc790', 'admin')\n    RETURNING id\n)\nINSERT INTO invite_codes (code, owner_id, max_uses, expires_at)\nSELECT 'DING26', id, 100, now() + interval '365 days'\nFROM admin_user;\nEOF<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\uc81c 1\ub144 \ub3d9\uc548 \uac00\uc871 100\uba85\uc774 \uac00\uc785\ud560 \uc218 \uc788\ub294 \ud669\uae08 \ucd08\ub300\ucf54\ub4dc <strong>DING26<\/strong>\uc774 \uc900\ube44\ub418\uc5c8\uc2b5\ub2c8\ub2e4!<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 <code>DING26<\/code>\uc740 \uc5f0\uc2b5\uc6a9\uc73c\ub85c \uc678\uc6b0\uae30 \uc27d\uac8c \ub9cc\ub4e0 \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ucd08\ub300\ucf54\ub4dc\uac00 \uc9e7\uace0 \ucd94\uce21\ud558\uae30 \uc26c\uc6b0\uba74 \ubb34\ucc28\ubcc4 \ub300\uc785\uc73c\ub85c \ub6ab\ub9b4 \uc218 \uc788\uc73c\ub2c8, \uc2e4\uc81c \uc6b4\uc601\ud560 \ub54c\ub294 <strong>\uc601\ubb38 \ub300\ubb38\uc790+\uc22b\uc790\ub97c \uc11e\uc740 \ucd94\uce21\ud558\uae30 \uc5b4\ub824\uc6b4 \ucf54\ub4dc<\/strong>\ub85c \ubc14\uafb8\uace0, \uc0ac\uc6a9 \ud69f\uc218(<code>max_uses<\/code>)\ub3c4 \uac00\uc871 \uc218\uc5d0 \ub9de\uac8c \uc791\uac8c \uc7a1\uc73c\uc138\uc694.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">6.2 \ud68c\uc6d0\uac00\uc785 API \ud638\ucd9c \ud14c\uc2a4\ud2b8 (<code>curl<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\uba3c\uc800 \uc11c\ubc84\uac00 \uc0ac\uc6a9\ud560 \ube44\ubc00\ud0a4 \ub450 \uac1c\ub97c \ud658\uacbd\ubcc0\uc218\ub85c \uc124\uc815\ud569\ub2c8\ub2e4. (\ud130\ubbf8\ub110\uc744 \uc0c8\ub85c \uc5f4\uba74 \ub2e4\uc2dc \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4.)<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">export JWT_SECRET=$(openssl rand -base64 48)\nexport PHONE_HASH_SECRET=$(openssl rand -base64 48)<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">NAS\uc5d0\uc11c <code>docker compose<\/code>\ub85c \uc2e4\ud589\ud55c\ub2e4\uba74, \uc704 \ub450 \uac12(<code>JWT_SECRET<\/code>, <code>PHONE_HASH_SECRET<\/code>)\uc744 <code>\/volume1\/docker\/dingdong\/.env<\/code> \ud30c\uc77c\uc5d0 \uac19\uc740 \uc774\ub984\uc73c\ub85c \ucd94\uac00\ud574\uc57c \ud569\ub2c8\ub2e4. (4\ud3b8\uc758 compose\uac00 <code>.env<\/code>\ub97c api \ucee8\ud14c\uc774\ub108\uc5d0 \uc804\ub2ec\ud558\uace0, <code>DATABASE_URL<\/code>\uc740 compose\uac00 \uc790\ub3d9\uc73c\ub85c \ub9cc\ub4e4\uc5b4 \ub123\uc5b4 \uc90d\ub2c8\ub2e4.) \uc774 \uac12\uc774 \uc5c6\uc73c\uba74 \uc11c\ubc84\uac00 \uc2dc\uc791 \uc9c1\ud6c4 \uc885\ub8cc\ub429\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\ub9e5\uc5d0\uc11c <code>go run .\/cmd\/api<\/code>\ub85c \uc9c1\uc811 \uc2e4\ud589\ud55c\ub2e4\uba74, \uc704 \ub450 \uac12\uc5d0 \ub354\ud574 <strong>5\ud3b8 3.4\uc808\ucc98\ub7fc SSH \ud130\ub110\uc744 \uc5f4\uace0 <code>DATABASE_URL<\/code>\ub3c4 \uc9c0\uc815<\/strong>\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uadf8\ub2e4\uc74c \uc11c\ubc84\ub97c \ub744\uc6b4 \ub4a4(<code>go run .\/cmd\/api<\/code> \ub610\ub294 NAS <code>docker compose up -d --build<\/code>), \ud130\ubbf8\ub110\uc5d0\uc11c \ud68c\uc6d0\uac00\uc785 \uc694\uccad\uc744 \ubcf4\ub0b4\ubd05\ub2c8\ub2e4:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl -X POST http:\/\/localhost:3000\/api\/v1\/auth\/register \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\n    \"inviteCode\": \"DING26\",\n    \"loginId\": \"daddy\",\n    \"password\": \"my_secure_password_123!\",\n    \"displayName\": \"\uc6b0\ub9ac\uc9d1 \ub300\uc7a5\",\n    \"phone\": \"+821012345678\",\n    \"platform\": \"macos\",\n    \"deviceName\": \"Mac Studio M4\"\n  }'<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\uc751\ub2f5 \uacb0\uacfc (201 Created):<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{\n  \"accessToken\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n  \"refreshToken\": \"8fK3x9_pLmN2qR7vW1zA4bC6dE0gH5jK9lM3nP6qR8s\",\n  \"user\": {\n    \"id\": \"0192c8a4-7b12-7000-8123-456789abcdef\",\n    \"display_name\": \"\uc6b0\ub9ac\uc9d1 \ub300\uc7a5\",\n    \"status_message\": null,\n    \"avatar_file_id\": null,\n    \"phone_hash\": \"a1b2c3d4e5f6...\",\n    \"role\": \"user\",\n    \"status\": \"active\",\n    \"invited_by\": null,\n    \"created_at\": \"2026-10-28T06:15:00Z\",\n    \"updated_at\": \"2026-10-28T06:15:00Z\"\n  }\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\uc804\ud654\ubc88\ud638\ub294 \ud3c9\ubb38 \ub300\uc2e0 \uc11c\ubc84 \ube44\ubc00\ud0a4\uac00 \ub4e4\uc5b4\uac04 HMAC-SHA256 \ud574\uc2dc(<code>phone_hash<\/code>)\ub85c \uc800\uc7a5\ub418\uc5c8\uace0, <code>accessToken<\/code>\uacfc <code>refreshToken<\/code>\uc774 \ub3d9\uc2dc\uc5d0 \ubc1c\uae09\ub418\uc5c8\uc2b5\ub2c8\ub2e4!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.3 \ubc1c\uae09\ubc1b\uc740 JWT\ub85c \ubcf4\ud638\ub41c API(<code>\/api\/v1\/users\/me<\/code>) \ud638\ucd9c\ud558\uae30<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\uc704 \uc751\ub2f5\uc5d0\uc11c \ubc1b\uc740 <code>accessToken<\/code> \uac12\uc744 \ubcf5\uc0ac\ud574 <code>Bearer<\/code> \ud5e4\ub354\uc5d0 \ub123\uc5b4\uc11c \ud638\ucd9c\ud574 \ubd05\ub2c8\ub2e4:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl -X GET http:\/\/localhost:3000\/api\/v1\/users\/me \\\n  -H \"Authorization: Bearer \uc5ec\uae30\uc5d0_\ubc1c\uae09\ubc1b\uc740_accessToken_\ubd99\uc5ec\ub123\uae30\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\ubbf8\ub4e4\uc6e8\uc5b4\uac00 DB \uc870\ud68c \uc5c6\uc774 \ud1a0\ud070 \uc11c\uba85\ub9cc\uc73c\ub85c \uc989\uc2dc \uac80\uc99d\ud558\uace0, \ub0b4 \ud504\ub85c\ud544 \uc815\ubcf4\ub97c \uc815\ud655\ud788 \ubc18\ud658\ud558\ub294 \uac83\uc744 \ud655\uc778\ud560 \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\">7. \uc774\ubc88 \ud3b8 \uc694\uc57d \ubc0f \ub2e4\uc74c \ub2e8\uacc4<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\ubc88 6\ud3b8\uc744 \ud1b5\ud574 \ub529\ub3d9 \uba54\uc2e0\uc800\ub294 \ud0c4\ud0c4\ud55c \uc778\uc99d\uc758 \ubf08\ub300\ub97c \uac16\ucd94\uc5c8\uc2b5\ub2c8\ub2e4:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ud504\ub77c\uc774\ube57 \ucd08\ub300\uc81c \uc644\uc131:<\/strong> \ucd08\ub300\ucf54\ub4dc \uc5c6\uc774\ub294 \uac00\uc785\ud560 \uc218 \uc5c6\ub294 \uac00\uc871\/\uc9c0\uc778 \uc804\uc6a9 \ud3d0\uc1c4\ud615 \uad6c\uc870 \ud655\ub9bd. \ucd08\ub300\ucf54\ub4dc \ucc28\uac10\uacfc \ud68c\uc6d0 \uc0dd\uc131\uc774 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc774\ub77c \uc911\uac04\uc5d0 \uc2e4\ud328\ud574\ub3c4 \uae54\ub054\ud558\uac8c \ub864\ubc31\ub429\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>Argon2id + \uc6a9\ub3c4\ubcc4 \ud574\uc2dc \uc800\uc7a5:<\/strong> \ube44\ubc00\ubc88\ud638\ub294 \uba54\ubaa8\ub9ac \ud558\ub4dc \ud574\uc2f1(Argon2id)\uc73c\ub85c, \uc804\ud654\ubc88\ud638\ub294 \uc11c\ubc84 \ube44\ubc00\ud0a4\ub97c \uc4f0\ub294 HMAC-SHA256\uc73c\ub85c, \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc740 SHA-256 \ub2e8\ubc29\ud5a5 \ud574\uc2dc\ub85c \uc800\uc7a5\ud574 DB\uac00 \uc720\ucd9c\ub418\uc5c8\uc744 \ub54c\uc758 \ud53c\ud574\ub97c \uc904\uc600\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>Refresh Token Rotation &amp; \ub2e4\uc911 \uae30\uae30 \uc9c0\uc6d0:<\/strong> \uae30\uae30\ub9c8\ub2e4 \ub3c5\ub9bd\ub41c \uc138\uc158\uc744 \ubd80\uc5ec\ud558\uace0, \ud1a0\ud070\uc744 \uac31\uc2e0\ud560 \ub54c\ub9c8\ub2e4 \uc0c8 \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc73c\ub85c \uad50\uccb4\ud569\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">\uc544\uc9c1 \uc801\uc6a9\ud558\uc9c0 \uc54a\uc740 \uac83 (\uc678\ubd80\uc5d0 \uacf5\uac1c\ud558\uae30 \uc804\uc5d0 \uaf2d \ucc59\uae30\uc138\uc694)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uc694\uccad \ud69f\uc218 \uc81c\ud55c(Rate Limit):<\/strong> <code>\/register<\/code>\uc640 <code>\/login<\/code>\uc5d0 \uc544\uc9c1 \uc81c\ud55c\uc774 \uc5c6\uc5b4 \ucd08\ub300\ucf54\ub4dc\ub098 \ube44\ubc00\ubc88\ud638\ub97c \ubb34\ucc28\ubcc4 \ub300\uc785\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. IP\ubcc4 \uc694\uccad \uc81c\ud55c \ubbf8\ub4e4\uc6e8\uc5b4\ub97c \ucd94\uac00\ud574\uc57c \ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>Refresh Token \ub3d9\uc2dc \uc0ac\uc6a9:<\/strong> \uac19\uc740 \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc73c\ub85c \uc694\uccad \ub450 \uac1c\uac00 \ub3d9\uc2dc\uc5d0 \ub4e4\uc5b4\uc624\uba74 \ub458 \ub2e4 \uc131\uacf5\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub610\ud55c \uc774\ubbf8 \uad50\uccb4\ub41c \uc61b \ud1a0\ud070\uc774 \ub2e4\uc2dc \uc0ac\uc6a9\ub418\uc5c8\uc744 \ub54c \uc138\uc158 \uc804\uccb4\ub97c \ud3d0\uae30\ud558\ub294 \ud0c8\ucde8 \uac10\uc9c0 \ub85c\uc9c1\uc740 \uc544\uc9c1 \uc5c6\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\ub85c\uadf8\uc778\ud560 \ub54c\ub9c8\ub2e4 \uae30\uae30 \ud589\uc774 \uc0c8\ub85c \uc0dd\uae40:<\/strong> \uac19\uc740 \uae30\uae30\uc5d0\uc11c \ub85c\uadf8\uc544\uc6c3 \uc5c6\uc774 \ub2e4\uc2dc \ub85c\uadf8\uc778\ud558\uba74 <code>devices<\/code>\uc5d0 \ud589\uc774 \uacc4\uc18d \uc313\uc785\ub2c8\ub2e4. \uae30\uae30 \uc2dd\ubcc4\uc790\ub97c \ub3c4\uc785\ud574 \uc815\ub9ac\ud558\ub294 \uac83\uc744 \uace0\ub824\ud558\uc138\uc694.<\/li>\n\n\n\n<li><strong>\uc751\ub2f5 \uac1d\uccb4 \ubd84\ub9ac:<\/strong> \uc9c0\uae08\uc740 <code>db.User<\/code>\ub97c \uadf8\ub300\ub85c \uc751\ub2f5\ud558\ubbc0\ub85c <code>phone_hash<\/code> \uac19\uc740 \ub0b4\ubd80 \ud544\ub4dc\ub3c4 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub0b4\ub824\uac11\ub2c8\ub2e4. \uc751\ub2f5 \uc804\uc6a9 \uad6c\uc870\uccb4\ub97c \ub530\ub85c \ub9cc\ub4dc\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uae30\uae30 \uc6d0\uaca9 \ub85c\uadf8\uc544\uc6c3 \uc2dc WebSocket \ub04a\uae30:<\/strong> 1\uc7a5\uc5d0\uc11c \uc124\uba85\ud55c \ub300\ub85c, \uae30\uae30\ub97c \uc0ad\uc81c\ud574\ub3c4 \uc5f0\uacb0\ub41c WebSocket\uc740 \uc11c\ubc84\uac00 \uc9c1\uc811 \ub04a\uc5b4\uc57c \ud569\ub2c8\ub2e4.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\ub2e4\uc74c [<strong>\uac1c\ubc1c\uc77c\uc9c0 #07<\/strong>]\uc5d0\uc11c\ub294 \uc2a4\ub9c8\ud2b8\ud3f0(Flutter)\uc5d0\uc11c \uc774 \ub85c\uadf8\uc778 API\ub97c \uc5f0\ub3d9\ud574 <strong>\uc790\ub3d9 \ub85c\uadf8\uc778(Secure Storage)\uc744 \uad6c\ud604<\/strong>\ud558\uace0, \uba54\uc2e0\uc800\uc758 \uaf43\uc778 <strong>\uce5c\uad6c \ucd94\uac00(\uc5f0\ub77d\ucc98 \ud574\uc2dc \ub9e4\uce6d) \ubc0f 1:1 \/ \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131 API<\/strong>\ub97c \ub9cc\ub4e4\uc5b4 \ubcf4\uaca0\uc2b5\ub2c8\ub2e4!<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udccc <strong>7\ud3b8 \uc608\uace0 \ubcf4\ucda9:<\/strong> \uc5f0\ub77d\ucc98 \ub9e4\uce6d\uc740 \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc804\ud654\ubc88\ud638\ub97c \uc11c\ubc84\ub85c \ubcf4\ub0b4\uba74 <strong>\uc11c\ubc84\uac00 \uac19\uc740 \ube44\ubc00\ud0a4\ub85c HMAC \ud574\uc2dc\ub97c \uacc4\uc0b0\ud574 \ube44\uad50<\/strong>\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \uad6c\ud604\ud569\ub2c8\ub2e4. (\ud074\ub77c\uc774\uc5b8\ud2b8\ub294 \uc11c\ubc84 \ube44\ubc00\ud0a4\ub97c \uc54c \uc218 \uc5c6\uc73c\ubbc0\ub85c, \ud574\uc2dc\ub97c \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c \uacc4\uc0b0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.)<\/p>\n<\/blockquote>\n\n\n\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 1. \uae30\ud68d &amp; \uc544\ud0a4\ud14d\ucc98 \uc124\uacc4<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #01 | \uae30\ud68d] <a href=\"https:\/\/dinggo.duckdns.org\/?p=20\" data-type=\"post\" data-id=\"20\">\uc720\uadf8\ub9b0 NAS\uc640 Flutter\ub85c \ub9cc\ub4dc\ub294 \ub098\ub9cc\uc758 \ud504\ub77c\uc774\ube57 \uba54\uc2e0\uc800 &#8216;\ub529\ub3d9&#8217; (\uae30\ud68d &amp; \uc544\ud0a4\ud14d\ucc98 \ud3b8)<\/a><\/strong><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 2. \ud648\uc11c\ubc84 &amp; \uc778\ud504\ub77c \uad6c\ucd95<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #02 | \uc778\ud504\ub77c] <a href=\"https:\/\/dinggo.duckdns.org\/?p=52\" data-type=\"post\" data-id=\"52\">Mac\uc5d0\uc11c Flutter &amp; VS Code \uac1c\ubc1c \ud658\uacbd \uad6c\ucd95\ud558\uae30 (\ucd08\ubcf4\uc790 \uc644\ubcbd \uac00\uc774\ub4dc)<\/a><\/strong><\/li>\n\n\n\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #03 | \uc778\ud504\ub77c] <a href=\"https:\/\/dinggo.duckdns.org\/?p=68\" data-type=\"post\" data-id=\"68\">\uc720\uadf8\ub9b0 NAS\uc5d0 Caddy &amp; PostgreSQL \uad6c\ucd95\ud558\uae30<\/a><\/strong><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 3. \ubc31\uc5d4\ub4dc(API) &amp; \uc2e4\uc2dc\uac04 \ud1b5\ud654 \uc11c\ubc84<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #04 | \ubc31\uc5d4\ub4dc] <a href=\"https:\/\/dinggo.duckdns.org\/?p=117\" data-type=\"post\" data-id=\"117\">\uc720\uadf8\ub9b0 NAS\uc5d0 Go(Golang) \ucd08\uacbd\ub7c9 \uba54\uc2e0\uc800 \ubc31\uc5d4\ub4dc \uc11c\ubc84 \uad6c\ucd95\ud558\uae30<\/a><\/strong><\/li>\n\n\n\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #05 | \ubc31\uc5d4\ub4dc] <a href=\"https:\/\/dinggo.duckdns.org\/?p=136\" data-type=\"post\" data-id=\"136\">\uba54\uc2e0\uc800 DB \uc2a4\ud0a4\ub9c8 \uc124\uacc4 &amp; Go sqlc \uc5f0\ub3d9\ud558\uae30<\/a><\/strong><\/li>\n\n\n\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #06 | \ubc31\uc5d4\ub4dc] <a href=\"https:\/\/dinggo.duckdns.org\/?p=191\" data-type=\"post\" data-id=\"191\">\ucd08\ub300\ucf54\ub4dc \ud68c\uc6d0\uac00\uc785, Argon2id \uc554\ud638\ud654 &amp; \ub2e4\uc911 \uae30\uae30 JWT \uc778\uc99d \uad6c\ud604\ud558\uae30<\/a><\/strong><\/li>\n\n\n\n<li>\u23f3 <strong>[#08 | \ubc31\uc5d4\ub4dc]<\/strong> \uc5f0\ub77d\ucc98 HMAC \ud574\uc2dc \uce5c\uad6c \ub9e4\uce6d &amp; 1:1 \/ \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131 API <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#10 | \ubc31\uc5d4\ub4dc]<\/strong> Gorilla WebSocket \uc2e4\uc2dc\uac04 \ucc44\ud305 \ud5c8\ube0c &amp; \ub2e8\uc870 \uc99d\uac00 \uc21c\ubc88(<code>seq<\/code>) \ub3d9\uae30\ud654 <em>(\uc608\uc815)<\/em><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 4. \ubaa8\ubc14\uc77c \uc571 \uac1c\ubc1c (iOS \/ Android)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #07 | \ud074\ub77c\uc774\uc5b8\ud2b8] <a href=\"https:\/\/dinggo.duckdns.org\/?p=200\" data-type=\"post\" data-id=\"200\">Flutter \uc571 \uccab \uc5f0\ub3d9: Secure Storage \ud1a0\ud070 \uad00\ub9ac, Dio \uc790\ub3d9 \uac31\uc2e0 &amp; \ub85c\uadf8\uc778 \ud654\uba74 \uad6c\ud604<\/a><\/strong><\/li>\n\n\n\n<li>\u23f3 <strong>[#09 | \ud074\ub77c\uc774\uc5b8\ud2b8]<\/strong> \uce5c\uad6c \ubaa9\ub85d \ub3d9\uae30\ud654 &amp; \ucc44\ud305\ubc29 \ubaa9\ub85d UI \uc5f0\ub3d9\ud558\uae30 <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#11 | \ud074\ub77c\uc774\uc5b8\ud2b8]<\/strong> \uc2e4\uc2dc\uac04 WebSocket \ucc44\ud305\ubc29 \ud654\uba74 &amp; \ub04a\uae40 \uc5c6\ub294 \ub300\ud654 \ub3d9\uae30\ud654 \uad6c\ud604 <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#12 | \uc571-\uacf5\ud1b5]<\/strong> 300MB \ub300\uc6a9\ub7c9 \uc6d0\ubcf8 \uc0ac\uc9c4\/\uc601\uc0c1 \uc804\uc1a1 \ubc0f \ubbf8\ub514\uc5b4 \ucc98\ub9ac <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#13 | \uc571-\ud478\uc2dc]<\/strong> \uc571\uc774 \uaebc\uc838 \uc788\uc5b4\ub3c4 \uc6b8\ub9ac\ub294 \ud478\uc2dc \uc54c\ub9bc(FCM\/APNs) \ubc0f \ubcf4\uc774\uc2a4\ud1a1 \uc5f0\ub3d9 <em>(\uc608\uc815)<\/em><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 5. \uc815\uc2dd \ubc30\ud3ec &amp; \uc6b4\uc601<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u23f3 <strong>[#14 | \ubc30\ud3ec]<\/strong> \ubc31\uc5d4\ub4dc \uc11c\ubc84 \ub3c4\ucee4 \uc774\ubbf8\uc9c0 \ube4c\ub4dc \ubc0f \uc720\uadf8\ub9b0 NAS \uc815\uc2dd \ubc30\ud3ec <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#15 | \uc6b4\uc601]<\/strong> DB \uc790\ub3d9 \ubc31\uc5c5 \uc2a4\ud06c\ub9bd\ud2b8 \uad6c\uc131 \ubc0f \uac00\uc871 \uc2e4\uc0ac\uc6a9 \ud6c4\uae30 <em>(\uc608\uc815)<\/em><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n<\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>\uc548\ub155\ud558\uc138\uc694! \uce74\uce74\uc624\ud1a1 \uc2a4\ud0c0\uc77c\uc758 \uac00\uc871\/\uce5c\uad6c\uc6a9 \ud504\ub77c\uc774\ube57 \ud06c\ub85c\uc2a4\ud50c\ub7ab\ud3fc \uba54\uc2e0\uc800 &#8216;\ub529\ub3d9(DingDong)&#8217; \ub9cc\ub4e4\uae30 \ud504\ub85c\uc81d\ud2b8 6\ud3b8\uc785\ub2c8\ub2e4. \uc9c0\ub09c [5\ud3b8]\uc5d0\uc11c\ub294 \uba54\uc2e0\uc800\uc758 \uc2ec\uc7a5\uc778 PostgreSQL 16 \uc2a4\ud0a4\ub9c8(11\uac1c \ud575\uc2ec \ud14c\uc774\ube14)\ub97c \uad6c\ucd95\ud558\uace0, sqlc\uc640 pgxpool\uc744 \uc5f0\ub3d9\ud574 \ud0c0\uc785 \uc548\uc804\ud55c DB \ub808\uc774\uc5b4\ub97c \uc644\uc131\ud588\uc2b5\ub2c8\ub2e4.\uc774\ubc88 6\ud3b8\uc5d0\uc11c\ub294 \uadf8 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc704\uc5d0 \ud504\ub77c\uc774\ube57 \ucd08\ub300\ucf54\ub4dc \uac80\uc99d, \uc694\uc998 \uad8c\uc7a5\ub418\ub294 Argon2id \ube44\ubc00\ubc88\ud638 \ud574\uc2f1, \ub2e4\uc911 \uae30\uae30(\uc2a4\ub9c8\ud2b8\ud3f0\/\ud0dc\ube14\ub9bf\/PC) \ub3d9\uc2dc \uc811\uc18d\uc744 \uc9c0\uc6d0\ud558\ub294 JWT Access\/Refresh \ud1a0\ud070 \uc2dc\uc2a4\ud15c, \uadf8\ub9ac\uace0 Chi \uc778\uc99d \ubbf8\ub4e4\uc6e8\uc5b4\ub97c \ucc98\uc74c\ubd80\ud130 &#8230; <a title=\"[\uac1c\ubc1c\uc77c\uc9c0 #06 | \ubc31\uc5d4\ub4dc] \ucd08\ub300\ucf54\ub4dc \ud68c\uc6d0\uac00\uc785, Argon2id \uc554\ud638\ud654 &amp; \ub2e4\uc911 \uae30\uae30 JWT \uc778\uc99d \uad6c\ud604\ud558\uae30\" class=\"read-more\" href=\"https:\/\/dingdong.goodday.ing\/?p=191\" aria-label=\"[\uac1c\ubc1c\uc77c\uc9c0 #06 | \ubc31\uc5d4\ub4dc] \ucd08\ub300\ucf54\ub4dc \ud68c\uc6d0\uac00\uc785, Argon2id \uc554\ud638\ud654 &amp; \ub2e4\uc911 \uae30\uae30 JWT \uc778\uc99d \uad6c\ud604\ud558\uae30\uc5d0 \ub300\ud574 \ub354 \uc790\uc138\ud788 \uc54c\uc544\ubcf4\uc138\uc694\">\ub354 \uc77d\uae30<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[7],"class_list":["post-191","post","type-post","status-publish","format-standard","hentry","category-dingdong-","tag-7"],"_links":{"self":[{"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/191","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=191"}],"version-history":[{"count":2,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/191\/revisions"}],"predecessor-version":[{"id":212,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/191\/revisions\/212"}],"wp:attachment":[{"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=191"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=191"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=191"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}