{"id":215,"date":"2026-10-31T10:13:44","date_gmt":"2026-10-31T10:13:44","guid":{"rendered":"https:\/\/dinggo.duckdns.org\/?p=215"},"modified":"2026-10-31T10:29:44","modified_gmt":"2026-10-31T10:29:44","slug":"%ea%b0%9c%eb%b0%9c%ec%9d%bc%ec%a7%80-08-%eb%b0%b1%ec%97%94%eb%93%9c-%ec%97%b0%eb%9d%bd%ec%b2%98-hmac-%ed%95%b4%ec%8b%9c-%ec%b9%9c%ea%b5%ac-%eb%a7%a4%ec%b9%ad-%ec%a4%91%eb%b3%b5-%ec%97%86","status":"publish","type":"post","link":"https:\/\/dingdong.goodday.ing\/?p=215","title":{"rendered":"[\uac1c\ubc1c\uc77c\uc9c0 #08 | \ubc31\uc5d4\ub4dc] \uc5f0\ub77d\ucc98 HMAC \ud574\uc2dc \uce5c\uad6c \ub9e4\uce6d &amp; \uc911\ubcf5 \uc5c6\ub294 1:1 \/ \uadf8\ub8f9 \ucc44\ud305\ubc29 API \uad6c\ud604"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">\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 8\ud3b8\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc9c0\ub09c [<strong>7\ud3b8<\/strong>]\uc5d0\uc11c\ub294 Flutter \uc571\uc5d0\uc11c \ub85c\uadf8\uc778 \ubc0f \ud68c\uc6d0\uac00\uc785\uc744 \uc218\ud589\ud558\uace0 \ud1a0\ud070\uc744 \uc790\ub3d9\uc73c\ub85c \uac31\uc2e0\ud558\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d \ud750\ub984\uc744 \uc5f0\uacb0\ud588\uc2b5\ub2c8\ub2e4.<br>\uc774\ubc88 8\ud3b8\uc5d0\uc11c\ub294 \ub2e4\uc2dc Go \ubc31\uc5d4\ub4dc\ub85c \ub3cc\uc544\uc640, \ub85c\uadf8\uc778\ud55c \uc774\ud6c4 \uc571 \ud654\uba74\uc744 \ucc44\uc6b0\ub294 \ub370 \ud544\uc694\ud55c <strong>\uc5f0\ub77d\ucc98 \uae30\ubc18 \uce5c\uad6c \ub3d9\uae30\ud654 API<\/strong> \uc640 <strong>1:1 \ubc0f \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131\u00b7\ubaa9\ub85d \uc870\ud68c API<\/strong> \ub97c \uad6c\ud604\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\">1. \ud575\uc2ec \uc124\uacc4 \uc6d0\uce59 \ubc0f \ucd94\uac00 \ud3f4\ub354 \uad6c\uc870<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uba54\uc2e0\uc800\uc5d0\uc11c \uce5c\uad6c\ub97c \ucc3e\uace0 \ub300\ud654\ubc29\uc744 \uc5f4 \ub54c \ubc1c\uc0dd\ud560 \uc218 \uc788\ub294 \uac1c\uc778\uc815\ubcf4 \ub178\ucd9c\uacfc \ub3d9\uc2dc\uc131(Race Condition) \ubb38\uc81c\ub97c \ub2e4\uc74c\uacfc \uac19\uc774 \ubc29\uc9c0\ud569\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[\"\uc5f0\ub77d\ucc98 \uc804\ud654\ubc88\ud638 \ubc30\uc5f4\"]:::titleStyle -->|\"1. E.164 \uc815\uaddc\ud654 &amp; HMAC \ud574\uc2f1\"| B[\"users \ud14c\uc774\ube14 \ub9e4\uce6d\"]:::nodeStyle\n    B -->|\"2. friendships \ub4f1\ub85d (WithTx)\"| C[\"\uce5c\uad6c \ubaa9\ub85d \ubc18\ud658\"]:::nodeStyle\n    C -->|\"3. direct_key \uc815\ub82c (\uc791\uc740UUID:\ud070UUID)\"| D[\"1:1 \ucc44\ud305\ubc29 \uc0dd\uc131 \/ \uc870\ud68c\"]:::titleStyle<\/pre><div class=\"wp-block-canaryblue-mermaid-diagram__output\"><\/div><\/div>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uc11c\ubc84 \ube44\ubc00\ud0a4 \uae30\ubc18 \uc5f0\ub77d\ucc98 \ub9e4\uce6d (<code>HMAC-SHA256<\/code>):<\/strong> \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc8fc\uc18c\ub85d\uc758 \uc804\ud654\ubc88\ud638 \ubaa9\ub85d\uc744 \ubcf4\ub0b4\uba74, \uc11c\ubc84\uac00 \ubc88\ud638\ub97c E.164 \ud3ec\ub9f7(<code>+8210...<\/code>)\uc73c\ub85c \uc815\uaddc\ud654\ud55c \ub4a4 [<strong>6\ud3b8<\/strong>]\uc5d0\uc11c \uc124\uc815\ud55c <code>PHONE_HASH_SECRET<\/code> \uc73c\ub85c HMAC \ud574\uc2dc\ub97c \uacc4\uc0b0\ud574 <code>users.phone_hash<\/code> \uc640 \ube44\uad50\ud569\ub2c8\ub2e4. \ud55c \ubc88\uc5d0 \ubcf4\ub0bc \uc218 \uc788\ub294 \uc804\ud654\ubc88\ud638 \uc218\ub294 \ucd5c\ub300 500\uac1c\ub85c \uc81c\ud55c\ud569\ub2c8\ub2e4. (\ud638\ucd9c \ube48\ub3c4 \uc81c\ud55c\uc740 \uc544\ub798 &#8220;\uc544\uc9c1 \uc801\uc6a9\ud558\uc9c0 \uc54a\uc740 \uac83&#8221;\uc5d0\uc11c \ub2e4\ub8f9\ub2c8\ub2e4.)<\/li>\n\n\n\n<li><strong>\ubbfc\uac10 \uc815\ubcf4 \ubd84\ub9ac (\ud504\ub85c\ud544 \uc804\uc6a9 \ud504\ub85c\uc81d\uc158):<\/strong> 6\ud3b8\uc758 \ub85c\uadf8\uc778\u00b7\ub0b4 \uc815\ubcf4 \uc751\ub2f5\uc740 <code>db.User<\/code> \uc804\uccb4\ub97c \ubc18\ud658\ud574 <code>phone_hash<\/code>\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \uc774\ubc88\uc5d0 \ub9cc\ub4dc\ub294 \uce5c\uad6c \ubaa9\ub85d \ubc0f \ucc44\ud305\ubc29 API\uc5d0\uc11c\ub294 SQL \uc870\ud68c \ub2e8\uacc4\ubd80\ud130 \uacf5\uac1c \ud504\ub85c\ud544 \uceec\ub7fc(<code>id<\/code>, <code>display_name<\/code>, <code>status_message<\/code>, <code>avatar_file_id<\/code>)\ub9cc \uc120\ud0dd\ud558\uc5ec \ubc18\ud658\ud569\ub2c8\ub2e4. (6\ud3b8 \uc751\ub2f5\uc758 \uc815\ub9ac\ub294 \uc544\uc9c1 \ub0a8\uc544 \uc788\uc2b5\ub2c8\ub2e4.)<\/li>\n\n\n\n<li><strong>1:1 \ucc44\ud305\ubc29 \uc911\ubcf5 \uc0dd\uc131 \uc6d0\ucc9c \ucc28\ub2e8 (<code>direct_key<\/code> + <code>ON CONFLICT<\/code>):<\/strong> \ub450 \uc0ac\uc6a9\uc790 A, B\uac00 \ub3d9\uc2dc\uc5d0 \uc11c\ub85c\uc5d0\uac8c 1:1 \ub300\ud654\ubc29\uc744 \uc5f4\ub354\ub77c\ub3c4 \ubc29\uc774 2\uac1c \uc0dd\uae30\uc9c0 \uc54a\ub3c4\ub85d, \ub450 UUID\ub97c \uc0ac\uc804\uc21c\uc73c\ub85c \uc815\ub82c\ud574 <code>\"\uc791\uc740UUID:\ud070UUID\"<\/code> \ud615\ud0dc\uc758 <code>direct_key<\/code> \ub97c \ub9cc\ub4e4\uace0 PostgreSQL\uc758 <code>ON CONFLICT (direct_key) DO UPDATE<\/code> \uad6c\ubb38\uc73c\ub85c \uc6d0\uc790\uc801(Atomic)\uc73c\ub85c \ucc98\ub9ac\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong><code>last_seq<\/code> \uc778\ub371\uc2a4\ub97c \ud65c\uc6a9\ud55c \uace0\uc18d \ubc29 \ubaa9\ub85d \uc870\ud68c:<\/strong> 5\ud3b8\uc5d0\uc11c \uc124\uacc4\ud55c <code>rooms.last_seq<\/code> \uc640 <code>messages<\/code> \ud14c\uc774\ube14\uc758 <code>UNIQUE (room_id, seq)<\/code> \uc778\ub371\uc2a4\ub97c \uc870\uc778\ud558\uc5ec, \ubcc4\ub3c4 \uc11c\ube0c\ucffc\ub9ac \uc5c6\uc774 \uac01 \ubc29\uc758 \ub9c8\uc9c0\ub9c9 \uba54\uc2dc\uc9c0\uc640 \uc548 \uc77d\uc740 \uba54\uc2dc\uc9c0 \uc218(<code>last_seq - last_read_seq<\/code>)\ub97c \ud55c \ubc88\uc5d0 \uc870\ud68c\ud569\ub2c8\ub2e4.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ubc31\uc5d4\ub4dc \ucd94\uac00 \ud30c\uc77c \uad6c\uc870 (<code>server\/<\/code>)<\/h3>\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\/\n \u251c\u2500 queries\/\n \u2502   \u251c\u2500 users.sql\n \u2502   \u251c\u2500 messages.sql\n \u2502   \u251c\u2500 auth.sql\n \u2502   \u251c\u2500 friends.sql                  # \ud83d\udc48 [\uc2e0\uaddc] \uce5c\uad6c \ub9e4\uce6d \ubc0f \ubaa9\ub85d \uc870\ud68c SQL\n \u2502   \u2514\u2500 rooms.sql                    # \ud83d\udc48 [\uc2e0\uaddc] 1:1 \/ \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131 \ubc0f \ubaa9\ub85d \uc870\ud68c SQL\n \u251c\u2500 internal\/\n \u2502   \u251c\u2500 auth\/\n \u2502   \u2502   \u2514\u2500 handler.go               # [\uc218\uc815] \uac00\uc785 \uc2dc \uc804\ud654\ubc88\ud638\ub3c4 \uac19\uc740 \ubc29\uc2dd\uc73c\ub85c \uc815\uaddc\ud654\n \u2502   \u251c\u2500 database\/\n \u2502   \u251c\u2500 phone\/\n \u2502   \u2502   \u2514\u2500 phone.go                 # \ud83d\udc48 [\uc2e0\uaddc] \uc804\ud654\ubc88\ud638 E.164 \uc815\uaddc\ud654 (auth\uc640 friend\uac00 \ud568\uaed8 \uc0ac\uc6a9)\n \u2502   \u251c\u2500 friend\/\n \u2502   \u2502   \u2514\u2500 handler.go               # \ud83d\udc48 [\uc2e0\uaddc] \uce5c\uad6c \ub3d9\uae30\ud654 \ud578\ub4e4\ub7ec\n \u2502   \u2514\u2500 room\/\n \u2502       \u2514\u2500 handler.go               # \ud83d\udc48 [\uc2e0\uaddc] 1:1 \/ \uadf8\ub8f9 \ucc44\ud305\ubc29 \ud578\ub4e4\ub7ec\n \u2514\u2500 cmd\/api\/main.go                  # [\uc218\uc815] \uce5c\uad6c \ubc0f \ucc44\ud305\ubc29 \ub77c\uc6b0\ud130 \uc5f0\uacb0<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. \uce5c\uad6c \ubc0f \ucc44\ud305\ubc29 SQL \ucffc\ub9ac \uc791\uc131 (<code>sqlc<\/code>)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">2.1 \uce5c\uad6c \uad00\ub828 \ucffc\ub9ac (<code>server\/queries\/friends.sql<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>server\/queries\/friends.sql<\/code> \ud30c\uc77c\uc744 \uc0dd\uc131\ud558\uace0 \uc544\ub798 \ucffc\ub9ac\ub97c \uc791\uc131\ud569\ub2c8\ub2e4. \ubc30\uc5f4 \ud30c\ub77c\ubbf8\ud130\uc5d0 <code>sqlc.arg('phone_hashes')::text[]<\/code> \ubb38\ubc95\uc744 \uc0ac\uc6a9\ud558\uba74 Go\uc5d0\uc11c \uae54\ub054\ud55c <code>[]string<\/code> \uc2ac\ub77c\uc774\uc2a4\ub97c \uc778\uc790\ub85c \ubc1b\uc744 \uc218 \uc788\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: FindUsersByPhoneHashes :many\n-- \uc804\ud654\ubc88\ud638 \ud574\uc2dc \ubc30\uc5f4\uacfc \uc77c\uce58\ud558\ub294 \ud65c\uc131 \uc0ac\uc6a9\uc790\ub97c \ucc3e\ub418, \uc790\uae30 \uc790\uc2e0\uc740 \uc81c\uc678\ud558\uace0 \uacf5\uac1c \uceec\ub7fc\ub9cc \ubc18\ud658\nSELECT\n    id,\n    display_name,\n    status_message,\n    avatar_file_id\nFROM users\nWHERE phone_hash = ANY(sqlc.arg('phone_hashes')::text[])\n  AND id != sqlc.arg('user_id')\n  AND status = 'active';\n\n-- name: UpsertFriendship :exec\n-- \uce5c\uad6c \uad00\uacc4 \ub4f1\ub85d (\uc774\ubbf8 \ub4f1\ub85d\ub41c \uce5c\uad6c\uba74 \ubb34\uc2dc)\nINSERT INTO friendships (user_id, friend_id, tag)\nVALUES ($1, $2, $3)\nON CONFLICT (user_id, friend_id) DO NOTHING;\n\n-- name: ListFriends :many\n-- \ub0b4 \uce5c\uad6c \ubaa9\ub85d \uc870\ud68c (\ucc28\ub2e8\ud558\uc9c0 \uc54a\uc740 \ud65c\uc131 \uc0c1\ud0dc\uc758 \uce5c\uad6c\ub9cc \uc774\ub984\uc21c \uc815\ub82c)\nSELECT\n    u.id,\n    u.display_name,\n    u.status_message,\n    u.avatar_file_id,\n    f.tag,\n    f.nickname,\n    f.blocked,\n    f.created_at\nFROM friendships f\nJOIN users u ON u.id = f.friend_id\nWHERE f.user_id = $1\n  AND f.blocked = false\n  AND u.status = 'active'\nORDER BY COALESCE(f.nickname, u.display_name) ASC;\n\n-- name: IsBlockedByUser :one\n-- \uc0c1\ub300\ubc29(target_id)\uc774 \ub098(user_id)\ub97c \ucc28\ub2e8\ud588\ub294\uc9c0 \uc5ec\ubd80 \ud655\uc778\nSELECT EXISTS (\n    SELECT 1 FROM friendships\n    WHERE user_id = sqlc.arg('target_id')\n      AND friend_id = sqlc.arg('user_id')\n      AND blocked = true\n) AS is_blocked;<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2.2 \ucc44\ud305\ubc29 \uad00\ub828 \ucffc\ub9ac (<code>server\/queries\/rooms.sql<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>server\/queries\/rooms.sql<\/code> \ud30c\uc77c\uc744 \uc0dd\uc131\ud558\uace0 \uc544\ub798 \ucffc\ub9ac\ub97c \uc791\uc131\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\">\ud83d\udca1 <strong>\ud301:<\/strong> PostgreSQL\uc758 <code>COALESCE<\/code> \ub098 <code>GREATEST<\/code> \uac19\uc740 \ud568\uc218 \uacb0\uacfc\uc5d0\ub294 <code>::varchar<\/code>, <code>::bigint<\/code> \ucc98\ub7fc \uba85\uc2dc\uc801 \ud0c0\uc785 \uce90\uc2a4\ud305\uc744 \ubd99\uc5ec\uc8fc\uc5b4\uc57c <code>sqlc<\/code>\uac00 \ubaa8\ud638\ud55c <code>interface{}<\/code> \ub300\uc2e0 \uc815\ud655\ud55c <code>string<\/code>, <code>int64<\/code> \ud0c0\uc785\uc73c\ub85c Go \uad6c\uc870\uccb4\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.<\/p>\n<\/blockquote>\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: UpsertDirectRoom :one\n-- 1:1 \ucc44\ud305\ubc29 \uc0dd\uc131 \ub610\ub294 \uae30\uc874 \ubc29 \ubc18\ud658 (\ub3d9\uc2dc \uc694\uccad \uc2dc\uc5d0\ub3c4 \uc911\ubcf5 \uc0dd\uc131 \uc5c6\uc774 \uae30\uc874 \ubc29\uc744 \ubc18\ud658)\nINSERT INTO rooms (type, direct_key, created_by)\nVALUES ('direct', $1, $2)\nON CONFLICT (direct_key)\nDO UPDATE SET updated_at = rooms.updated_at\nRETURNING *;\n\n-- name: CreateGroupRoom :one\n-- \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc2e0\uaddc \uc0dd\uc131\nINSERT INTO rooms (type, name, created_by)\nVALUES ('group', $1, $2)\nRETURNING *;\n\n-- name: AddRoomMember :exec\n-- \ucc44\ud305\ubc29 \uba64\ubc84 \ucd94\uac00 (\uc774\ubbf8 \ucc38\uc5ec \uc911\uc774\uba74 \ubb34\uc2dc)\nINSERT INTO room_members (room_id, user_id, role, last_read_seq, joined_seq)\nVALUES ($1, $2, $3, $4, $5)\nON CONFLICT (room_id, user_id) DO NOTHING;\n\n-- name: ListMyRooms :many\n-- \ub0b4\uac00 \ucc38\uc5ec \uc911\uc778 \ucc44\ud305\ubc29 \ubaa9\ub85d + \uc0c1\ub300\ubc29 \uc774\ub984(1:1 \ubc29) + \uc548 \uc77d\uc740 \uac1c\uc218 + \ub9c8\uc9c0\ub9c9 \uba54\uc2dc\uc9c0 \uc870\ud68c\nSELECT\n    r.id,\n    r.type,\n    COALESCE(r.name, du.display_name, '\uc54c \uc218 \uc5c6\ub294 \ub300\ud654\uc0c1\ub300')::varchar AS display_title,\n    COALESCE(r.avatar_file_id, du.avatar_file_id) AS display_avatar_id,\n    r.last_seq,\n    rm.last_read_seq,\n    GREATEST(r.last_seq - rm.last_read_seq, 0)::bigint AS unread_count,\n    rm.pinned,\n    rm.muted,\n    m.type AS last_message_type,\n    m.text AS last_message_text,\n    m.created_at AS last_message_at,\n    r.updated_at\nFROM room_members rm\nJOIN rooms r ON r.id = rm.room_id\nLEFT JOIN room_members dm\n    ON dm.room_id = r.id AND r.type = 'direct' AND dm.user_id != $1\nLEFT JOIN users du\n    ON du.id = dm.user_id\nLEFT JOIN messages m\n    ON m.room_id = r.id AND m.seq = r.last_seq AND m.deleted_at IS NULL\nWHERE rm.user_id = $1\nORDER BY rm.pinned DESC, r.updated_at DESC;<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2.3 <code>sqlc generate<\/code> \uc2e4\ud589<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ud130\ubbf8\ub110\uc758 <code>server\/<\/code> \uacbd\ub85c\uc5d0\uc11c Go \ucf54\ub4dc\ub97c \uc0dd\uc131\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\nsqlc generate<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>internal\/database\/db\/<\/code> \ud3f4\ub354 \uc548\uc5d0 <code>friends.sql.go<\/code>\uc640 <code>rooms.sql.go<\/code> \ud30c\uc77c\uc774 \uc0dd\uc131\ub429\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 \uc2dc\uac01(<code>created_at<\/code> \ub4f1)\uc744 \uc704\ud55c <code>sqlc.yaml<\/code> \uc124\uc815\uc740 \ub530\ub85c \ubc14\uafc0 \ud544\uc694\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. 5\ud3b8 \uc124\uc815 \uadf8\ub300\ub85c \ub450\uba74 sqlc\uac00 pgx \uc804\uc6a9 \ud0c0\uc785\uc73c\ub85c \ub9cc\ub4e4\uc5b4 \uc8fc\uace0, JSON\uc73c\ub85c \ub0b4\ub824\uac08 \ub54c\ub294 <code>\"2026-10-31T09:30:00Z\"<\/code> \uac19\uc740 \ubb38\uc790\uc5f4\uc774 \ub429\ub2c8\ub2e4.<\/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\">3. \uc804\ud654\ubc88\ud638 \uc815\uaddc\ud654 \ubc0f \uce5c\uad6c \ub9e4\uce6d \uad6c\ud604<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc2a4\ub9c8\ud2b8\ud3f0 \uc8fc\uc18c\ub85d\uc5d0\ub294 \uc804\ud654\ubc88\ud638\uac00 <code>010-1234-5678<\/code>, <code>01012345678<\/code>, <code>+82 10-1234-5678<\/code>, <code>+82 010-1234-5678<\/code> \ub4f1 \uc81c\uac01\uac01\uc758 \ud615\ud0dc\ub85c \uc800\uc7a5\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \ud574\uc2dc\uac12\uc740 \uae00\uc790 \ud558\ub098\ub9cc \ub2ec\ub77c\ub3c4 \uc644\uc804\ud788 \ub2ec\ub77c\uc9c0\ubbc0\ub85c, <strong>\ud68c\uc6d0\uac00\uc785 \uc2dc\uc810\uacfc \uc8fc\uc18c\ub85d \ub3d9\uae30\ud654 \uc2dc\uc810 \ubaa8\ub450 \ub3d9\uc77c\ud55c \uc815\uaddc\ud654 \ud568\uc218<\/strong>\ub97c \uac70\uccd0\uc57c \ub9e4\uce6d\uc774 \uc131\ub9bd\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc815\uaddc\ud654 \ud568\uc218\ub294 <code>auth<\/code>(\ud68c\uc6d0\uac00\uc785)\uc640 <code>friend<\/code>(\ub3d9\uae30\ud654) \ub450 \uacf3\uc5d0\uc11c \uc4f0\ubbc0\ub85c \ubcc4\ub3c4 \ud328\ud0a4\uc9c0\ub85c \ub461\ub2c8\ub2e4. (<code>friend<\/code> \ud328\ud0a4\uc9c0\uac00 <code>auth<\/code>\ub97c \uc774\ubbf8 \uac00\uc838\ub2e4 \uc4f0\uace0 \uc788\uc5b4\uc11c, \uc815\uaddc\ud654 \ud568\uc218\ub97c <code>friend<\/code>\uc5d0 \ub450\uace0 <code>auth<\/code>\uac00 \ub2e4\uc2dc \uac00\uc838\uc624\uba74 \uc11c\ub85c \ucc38\uc870\ud558\ub294 \uc21c\ud658 import \uc5d0\ub7ec\uac00 \ub0a9\ub2c8\ub2e4.)<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.1 \uc804\ud654\ubc88\ud638 \uc815\uaddc\ud654 (<code>server\/internal\/phone\/phone.go<\/code>)<\/h3>\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 phone\n\nimport \"strings\"\n\n\/\/ NormalizeE164: \uc804\ud654\ubc88\ud638\ub97c E.164(+821012345678) \ud615\ud0dc\ub85c \uc815\uaddc\ud654\ud569\ub2c8\ub2e4.\n\/\/ \ubcc0\ud658\ud560 \uc218 \uc5c6\ub294 \uac12\uc774\uba74 \ube48 \ubb38\uc790\uc5f4(\"\")\uc744 \ubc18\ud658\ud569\ub2c8\ub2e4.\n\/\/\n\/\/ \uc9c0\uc6d0\ud558\ub294 \ud615\ud0dc\n\/\/   - \ud55c\uad6d \ud734\ub300\ud3f0 \ub85c\uceec \ubc88\ud638: 010-1234-5678, 01012345678\n\/\/   - \uad6d\uac00\ubc88\ud638\uac00 \ubd99\uc740 \ubc88\ud638: +82 10-1234-5678, +82 010-1234-5678, 0082 10 1234 5678\nfunc NormalizeE164(raw string) string {\n\traw = strings.TrimSpace(raw)\n\thasPlus := strings.HasPrefix(raw, \"+\")\n\n\t\/\/ \uc22b\uc790(0-9)\ub9cc \ub0a8\uae41\ub2c8\ub2e4. (\ud558\uc774\ud508, \uacf5\ubc31, \uad04\ud638, \uc804\uac01 \uc22b\uc790 \ub4f1\uc740 \ubaa8\ub450 \ubc84\ub9bc)\n\tvar b strings.Builder\n\tfor _, r := range raw {\n\t\tif r >= '0' &amp;&amp; r &lt;= '9' {\n\t\t\tb.WriteRune(r)\n\t\t}\n\t}\n\tdigits := b.String()\n\n\t\/\/ \uad6d\uc81c\uc804\ud654 \uc811\ub450\uc5b4 \"00\"\uc740 \"+\"\uc640 \uac19\uc740 \ub73b\uc73c\ub85c \ucc98\ub9ac\ud569\ub2c8\ub2e4. (\uc608: 0082...)\n\tif !hasPlus &amp;&amp; strings.HasPrefix(digits, \"00\") {\n\t\thasPlus = true\n\t\tdigits = digits[2:]\n\t}\n\n\tif hasPlus {\n\t\t\/\/ \ud55c\uad6d \ubc88\ud638\ub97c \"+82 010-1234-5678\"\ucc98\ub7fc 0\uc744 \ubd99\uc5ec \uc801\ub294 \uacbd\uc6b0\uac00 \ub9ce\uc544 \uadf8 0\uc744 \uc81c\uac70\ud569\ub2c8\ub2e4.\n\t\tif strings.HasPrefix(digits, \"820\") {\n\t\t\tdigits = \"82\" + digits[3:]\n\t\t}\n\t\tif len(digits) &lt; 9 || len(digits) > 15 {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn \"+\" + digits\n\t}\n\n\t\/\/ \ud55c\uad6d \ub85c\uceec \ud734\ub300\ud3f0 \ubc88\ud638 (010, 011 ...) \u2192 +82\n\tif strings.HasPrefix(digits, \"01\") &amp;&amp; len(digits) >= 10 &amp;&amp; len(digits) &lt;= 11 {\n\t\treturn \"+82\" + digits[1:]\n\t}\n\treturn \"\"\n}<\/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 \ud55c\uad6d \ud734\ub300\ud3f0 \ubc88\ud638(010 \ub4f1)\uc640 \uad6d\uac00\ubc88\ud638\uac00 \ubd99\uc740 \ubc88\ud638\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4. <code>02-123-4567<\/code> \uac19\uc740 \uc9c0\uc5ed \uc720\uc120\ubc88\ud638\ub294 \ube48 \ubb38\uc790\uc5f4\uc774 \ub418\uc5b4 \ub9e4\uce6d\uc5d0\uc11c \uc81c\uc678\ub429\ub2c8\ub2e4. \uac00\uc871 \uba54\uc2e0\uc800\uc5d0\uc11c\ub294 \ud734\ub300\ud3f0 \ubc88\ud638\uba74 \ucda9\ubd84\ud558\ub2e4\uace0 \ubcf4\uace0 \ub2e8\uc21c\ud558\uac8c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">3.2 6\ud3b8 \ud68c\uc6d0\uac00\uc785 \ud578\ub4e4\ub7ec \ubcf4\uc644 (<code>server\/internal\/auth\/handler.go<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\uac00\uc785\ud560 \ub54c \uc800\uc7a5\ud558\ub294 \uc804\ud654\ubc88\ud638 \ud574\uc2dc\ub3c4 \uac19\uc740 \ud568\uc218\ub97c \uac70\uce58\ub3c4\ub85d \uace0\uce69\ub2c8\ub2e4. \uc548 \uace0\uce58\uba74 <code>010-9876-5432<\/code>\ub85c \uac00\uc785\ud55c \uc0ac\ub78c\uc740 \uc8fc\uc18c\ub85d \ub3d9\uae30\ud654\uc5d0\uc11c \uc601\uc6d0\ud788 \ub9e4\uce6d\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uba3c\uc800 <code>import<\/code> \ubaa9\ub85d\uc5d0 <code>strings<\/code>\uc640 <code>phone<\/code> \ud328\ud0a4\uc9c0\ub97c \ucd94\uac00\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=\"\">import (\n\t\"encoding\/json\"\n\t\"errors\"\n\t\"net\/http\"\n\t\"strings\" \/\/ \ud83d\udc48 \ucd94\uac00\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\t\"github.com\/yourname\/dingdong-server\/internal\/phone\" \/\/ \ud83d\udc48 \ucd94\uac00\n)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\uadf8\ub2e4\uc74c <code>Register<\/code> \ud568\uc218 \uc548\uc758 \uc804\ud654\ubc88\ud638 \ud574\uc2f1 \ubd80\ubd84\uc744 \uc544\ub798\ucc98\ub7fc \ubc14\uafc9\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=\"\">\t\/\/ \uc804\ud654\ubc88\ud638\ub294 E.164\ub85c \uc815\uaddc\ud654\ud55c \ub4a4, \uc11c\ubc84 \ube44\ubc00\ud0a4\ub97c \uc4f0\ub294 HMAC-SHA256\uc73c\ub85c \uc800\uc7a5\n\tvar phoneHash *string\n\tif req.Phone != nil &amp;&amp; strings.TrimSpace(*req.Phone) != \"\" {\n\t\tnormalized := phone.NormalizeE164(*req.Phone)\n\t\tif normalized == \"\" {\n\t\t\twriteJSONError(w, \"\uc804\ud654\ubc88\ud638 \ud615\uc2dd\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (\uc608: 010-1234-5678)\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\thashed := HashHMAC(h.phoneKey, normalized)\n\t\tphoneHash = &amp;hashed\n\t}<\/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 6\ud3b8\uc5d0\uc11c \uc774\ubbf8 \uac00\uc785\uc2dc\ucf1c \ub454 \ud14c\uc2a4\ud2b8 \uacc4\uc815\uc774 <code>+821012345678<\/code>\ucc98\ub7fc \ucc98\uc74c\ubd80\ud130 E.164 \ud615\uc2dd\uc774\uc5c8\ub2e4\uba74 \uadf8\ub300\ub85c \uc368\ub3c4 \ub429\ub2c8\ub2e4. \ud558\uc774\ud508\uc774 \uc11e\uc778 \ubc88\ud638\ub85c \uac00\uc785\ud588\ub2e4\uba74 \ud574\uc2dc\uac00 \ub2ec\ub77c\uc838 \uc788\uc73c\ub2c8, \uacc4\uc815\uc744 \uc0c8\ub85c \ub9cc\ub4dc\uc138\uc694.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">3.3 \uce5c\uad6c \ub3d9\uae30\ud654 \ud578\ub4e4\ub7ec (<code>server\/internal\/friend\/handler.go<\/code>)<\/h3>\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> <code>import<\/code> \uacbd\ub85c\uc758 <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\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 friend\n\nimport (\n\t\"encoding\/json\"\n\t\"net\/http\"\n\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\"\n\t\"github.com\/yourname\/dingdong-server\/internal\/database\/db\"\n\t\"github.com\/yourname\/dingdong-server\/internal\/phone\"\n)\n\nconst maxSyncPhones = 500 \/\/ 1\ud68c \ub3d9\uae30\ud654 \uc2dc \ud5c8\uc6a9\ud560 \ucd5c\ub300 \uc804\ud654\ubc88\ud638 \uac1c\uc218\n\ntype Handler struct {\n\tpool     *pgxpool.Pool\n\tqueries  *db.Queries\n\tphoneKey []byte\n}\n\nfunc NewHandler(pool *pgxpool.Pool, queries *db.Queries, phoneKey []byte) *Handler {\n\treturn &amp;Handler{pool: pool, queries: queries, phoneKey: phoneKey}\n}\n\ntype SyncContactsRequest struct {\n\tPhones []string `json:\"phones\"`\n}\n\n\/\/ 1. \uc5f0\ub77d\ucc98 \ub3d9\uae30\ud654 \ubc0f \uce5c\uad6c \uc790\ub3d9 \ucd94\uac00 (POST \/api\/v1\/friends\/sync)\nfunc (h *Handler) SyncContacts(w http.ResponseWriter, r *http.Request) {\n\tuserID, ok := auth.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\tr.Body = http.MaxBytesReader(w, r.Body, 256&lt;&lt;10) \/\/ \uc694\uccad \ubcf8\ubb38 256KB \uc81c\ud55c\n\tvar req SyncContactsRequest\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 len(req.Phones) > maxSyncPhones {\n\t\twriteJSONError(w, \"\ud55c \ubc88\uc5d0 \ub3d9\uae30\ud654\ud560 \uc218 \uc788\ub294 \uc5f0\ub77d\ucc98\ub294 \ucd5c\ub300 500\uac1c\uc785\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t\/\/ \uc804\ud654\ubc88\ud638 \uc815\uaddc\ud654 \ubc0f \uc911\ubcf5 \uc81c\uac70 \ud6c4 HMAC-SHA256 \ud574\uc2dc \uacc4\uc0b0\n\tseen := make(map[string]struct{})\n\thashes := make([]string, 0, len(req.Phones))\n\tfor _, rawPhone := range req.Phones {\n\t\tnormalized := phone.NormalizeE164(rawPhone)\n\t\tif normalized == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, exists := seen[normalized]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[normalized] = struct{}{}\n\t\thashes = append(hashes, auth.HashHMAC(h.phoneKey, normalized))\n\t}\n\n\t\/\/ \ub9e4\uce6d\ud560 \uc720\ud6a8 \ubc88\ud638\uac00 \uc788\uc73c\uba74 DB\uc5d0\uc11c \uc870\ud68c \ud6c4 friendships \ud14c\uc774\ube14\uc5d0 \ub4f1\ub85d\n\tif len(hashes) > 0 {\n\t\terr := database.WithTx(r.Context(), h.pool, func(q *db.Queries) error {\n\t\t\tmatchedUsers, err := q.FindUsersByPhoneHashes(r.Context(), db.FindUsersByPhoneHashesParams{\n\t\t\t\tPhoneHashes: hashes,\n\t\t\t\tUserID:      userID,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, u := range matchedUsers {\n\t\t\t\terr := q.UpsertFriendship(r.Context(), db.UpsertFriendshipParams{\n\t\t\t\t\tUserID:   userID,\n\t\t\t\t\tFriendID: u.ID,\n\t\t\t\t\tTag:      \"friend\",\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\twriteJSONError(w, \"\uc5f0\ub77d\ucc98 \ub3d9\uae30\ud654 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n\n\t\/\/ \ub3d9\uae30\ud654 \uc644\ub8cc \ud6c4 \ucd5c\uc2e0 \uce5c\uad6c \ubaa9\ub85d \ubc18\ud658\n\tfriends, err := h.queries.ListFriends(r.Context(), userID)\n\tif err != nil {\n\t\twriteJSONError(w, \"\uce5c\uad6c \ubaa9\ub85d \uc870\ud68c \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, map[string]interface{}{\n\t\t\"friends\": friends,\n\t})\n}\n\n\/\/ 2. \ub0b4 \uce5c\uad6c \ubaa9\ub85d \uc870\ud68c (GET \/api\/v1\/friends)\nfunc (h *Handler) ListFriends(w http.ResponseWriter, r *http.Request) {\n\tuserID, ok := auth.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\tfriends, err := h.queries.ListFriends(r.Context(), userID)\n\tif err != nil {\n\t\twriteJSONError(w, \"\uce5c\uad6c \ubaa9\ub85d \uc870\ud68c \uc2e4\ud328\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, map[string]interface{}{\n\t\t\"friends\": friends,\n\t})\n}\n\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\">4. 1:1 \ubc0f \uadf8\ub8f9 \ucc44\ud305\ubc29 \ud578\ub4e4\ub7ec \uad6c\ud604 (<code>internal\/room\/handler.go<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>server\/internal\/room\/handler.go<\/code> \ud30c\uc77c\uc744 \uc0dd\uc131\ud558\uace0 \ucc44\ud305\ubc29 \uc0dd\uc131 \ubc0f \uc870\ud68c \ub85c\uc9c1\uc744 \uc791\uc131\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">1:1 \ucc44\ud305\ubc29 \uc0dd\uc131 \uc2dc\uc5d0\ub294 <strong>\u2460 \uc790\uae30 \uc790\uc2e0\uacfc\uc758 \ub300\ud654 \uc694\uccad\uc778\uc9c0<\/strong>, <strong>\u2461 \uc0c1\ub300\ubc29 \uacc4\uc815\uc774 \uc720\ud6a8\ud558\uace0 \ud65c\uc131 \uc0c1\ud0dc\uc778\uc9c0<\/strong>, <strong>\u2462 \uc0c1\ub300\ubc29\uc774 \ub098\ub97c \ucc28\ub2e8\ud558\uc9c0 \uc54a\uc558\ub294\uc9c0<\/strong>\ub97c \uba3c\uc800 \uac80\uc99d\ud55c \ub4a4 \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubc29\uacfc \uba64\ubc84\ub97c \ub4f1\ub85d\ud569\ub2c8\ub2e4. \uc0c1\ub300\ubc29\uc774 \ub098\ub97c \ucc28\ub2e8\ud588\uc744 \ub54c\ub3c4 &#8220;\uc0c1\ub300\ub97c \ucc3e\uc744 \uc218 \uc5c6\ub2e4&#8221;\ub294 \uac19\uc740 \uc751\ub2f5\uc744 \uc8fc\uc5b4, \ucc28\ub2e8\ub2f9\ud55c \uc0ac\uc2e4\uc774 \ub4dc\ub7ec\ub098\uc9c0 \uc54a\uac8c \ud588\uc2b5\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 room\n\nimport (\n\t\"encoding\/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\/http\"\n\t\"strings\"\n\t\"unicode\/utf8\"\n\n\t\"github.com\/google\/uuid\"\n\t\"github.com\/jackc\/pgx\/v5\"\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\"\n\t\"github.com\/yourname\/dingdong-server\/internal\/database\/db\"\n)\n\n\/\/ \ucd08\ub300 \ub300\uc0c1 \uc911 \uc874\uc7ac\ud558\uc9c0 \uc54a\uac70\ub098 \ube44\ud65c\uc131\uc778 \uc0ac\uc6a9\uc790\uac00 \uc788\uc744 \ub54c \uc4f0\ub294 \uc5d0\ub7ec (\uc0ac\uc6a9\uc790\uc5d0\uac8c \uadf8\ub300\ub85c \ubcf4\uc5ec \uc918\ub3c4 \ub418\ub294 \uba54\uc2dc\uc9c0)\nvar errInvalidMember = errors.New(\"\uc874\uc7ac\ud558\uc9c0 \uc54a\uac70\ub098 \ube44\ud65c\uc131\ud654\ub41c \uc0ac\uc6a9\uc790\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4\")\n\ntype Handler struct {\n\tpool    *pgxpool.Pool\n\tqueries *db.Queries\n}\n\nfunc NewHandler(pool *pgxpool.Pool, queries *db.Queries) *Handler {\n\treturn &amp;Handler{pool: pool, queries: queries}\n}\n\ntype CreateDirectRoomRequest struct {\n\tTargetUserID uuid.UUID `json:\"targetUserId\"`\n}\n\ntype CreateGroupRoomRequest struct {\n\tName      string      `json:\"name\"`\n\tMemberIDs []uuid.UUID `json:\"memberIds\"`\n}\n\n\/\/ makeDirectKey: \ub450 UUID\ub97c \uc0ac\uc804\uc21c\uc73c\ub85c \uc815\ub82c\ud574 \"\uc791\uc740UUID:\ud070UUID\" \ud0a4\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.\nfunc makeDirectKey(a, b uuid.UUID) string {\n\ts1, s2 := a.String(), b.String()\n\tif s1 &lt; s2 {\n\t\treturn fmt.Sprintf(\"%s:%s\", s1, s2)\n\t}\n\treturn fmt.Sprintf(\"%s:%s\", s2, s1)\n}\n\n\/\/ 1. 1:1 \ucc44\ud305\ubc29 \uc0dd\uc131 \ub610\ub294 \uae30\uc874 \ubc29 \uc5f4\uae30 (POST \/api\/v1\/rooms\/direct)\nfunc (h *Handler) CreateDirectRoom(w http.ResponseWriter, r *http.Request) {\n\tuserID, ok := auth.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\tr.Body = http.MaxBytesReader(w, r.Body, 1&lt;&lt;20)\n\tvar req CreateDirectRoomRequest\n\tif err := json.NewDecoder(r.Body).Decode(&amp;req); err != nil || req.TargetUserID == uuid.Nil {\n\t\twriteJSONError(w, \"\uc720\ud6a8\ud55c \ub300\ud654 \uc0c1\ub300 ID(targetUserId)\uac00 \ud544\uc694\ud569\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif req.TargetUserID == userID {\n\t\twriteJSONError(w, \"\uc790\uae30 \uc790\uc2e0\uacfc\ub294 1:1 \ucc44\ud305\ubc29\uc744 \ub9cc\ub4e4 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t\/\/ \uc0c1\ub300\ubc29 \uacc4\uc815 \uc874\uc7ac \uc5ec\ubd80 \ud655\uc778\n\ttargetUser, err := h.queries.GetUserByID(r.Context(), req.TargetUserID)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\twriteJSONError(w, \"\ub300\ud654 \uc0c1\ub300\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4\", http.StatusNotFound)\n\t\treturn\n\t}\n\tif err != nil {\n\t\twriteJSONError(w, \"\ub300\ud654 \uc0c1\ub300\ub97c \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tif targetUser.Status != \"active\" {\n\t\twriteJSONError(w, \"\ub300\ud654 \uc0c1\ub300\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\t\/\/ \uc0c1\ub300\ubc29\uc774 \ub098\ub97c \ucc28\ub2e8\ud588\ub294\uc9c0 \ud655\uc778 (\ucc28\ub2e8 \uc0ac\uc2e4\uc744 \uc54c\ub824\uc8fc\uc9c0 \uc54a\uae30 \uc704\ud574 \uc704\uc640 \uac19\uc740 \uc751\ub2f5\uc744 \uc0ac\uc6a9)\n\tblocked, err := h.queries.IsBlockedByUser(r.Context(), db.IsBlockedByUserParams{\n\t\tTargetID: req.TargetUserID,\n\t\tUserID:   userID,\n\t})\n\tif err != nil {\n\t\twriteJSONError(w, \"\ub300\ud654 \uc0c1\ub300\ub97c \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tif blocked {\n\t\twriteJSONError(w, \"\ub300\ud654 \uc0c1\ub300\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tdirectKey := makeDirectKey(userID, req.TargetUserID)\n\tvar room db.Room\n\n\terr = database.WithTx(r.Context(), h.pool, func(q *db.Queries) error {\n\t\t\/\/ \u2460 direct_key \uae30\uc900\uc73c\ub85c \ubc29 \uc0dd\uc131 (\uc774\ubbf8 \uc788\uc73c\uba74 \uae30\uc874 \ubc29 \ubc18\ud658)\n\t\trCreated, err := q.UpsertDirectRoom(r.Context(), db.UpsertDirectRoomParams{\n\t\t\tDirectKey: &amp;directKey,\n\t\t\tCreatedBy: &amp;userID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\troom = rCreated\n\n\t\t\/\/ \u2461 \ub098 \uc790\uc2e0\uacfc \uc0c1\ub300\ubc29\uc744 \uba64\ubc84\ub85c \ub4f1\ub85d (\uc774\ubbf8 \ub4f1\ub85d\ub418\uc5b4 \uc788\uc73c\uba74 \ubb34\uc2dc)\n\t\tif err := q.AddRoomMember(r.Context(), db.AddRoomMemberParams{\n\t\t\tRoomID:      room.ID,\n\t\t\tUserID:      userID,\n\t\t\tRole:        \"member\",\n\t\t\tLastReadSeq: room.LastSeq,\n\t\t\tJoinedSeq:   0,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn q.AddRoomMember(r.Context(), db.AddRoomMemberParams{\n\t\t\tRoomID:      room.ID,\n\t\t\tUserID:      req.TargetUserID,\n\t\t\tRole:        \"member\",\n\t\t\tLastReadSeq: 0,\n\t\t\tJoinedSeq:   0,\n\t\t})\n\t})\n\n\tif err != nil {\n\t\twriteJSONError(w, \"1:1 \ucc44\ud305\ubc29 \uc0dd\uc131\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, map[string]interface{}{\n\t\t\"room\": room,\n\t})\n}\n\n\/\/ 2. \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131 (POST \/api\/v1\/rooms\/group)\nfunc (h *Handler) CreateGroupRoom(w http.ResponseWriter, r *http.Request) {\n\tuserID, ok := auth.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\tr.Body = http.MaxBytesReader(w, r.Body, 1&lt;&lt;20)\n\tvar req CreateGroupRoomRequest\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\t\/\/ \ud55c\uae00\uc740 \uae00\uc790\ub2f9 3\ubc14\uc774\ud2b8\uc774\ubbc0\ub85c, len()\uc774 \uc544\ub2c8\ub77c \uae00\uc790 \uc218(rune)\ub85c \uae38\uc774\ub97c \uc149\ub2c8\ub2e4.\n\troomName := strings.TrimSpace(req.Name)\n\tif n := utf8.RuneCountInString(roomName); n == 0 || n > 50 {\n\t\twriteJSONError(w, \"\uadf8\ub8f9 \ucc44\ud305\ubc29 \uc774\ub984\uc740 1~50\uc790 \uc0ac\uc774\uc5ec\uc57c \ud569\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif len(req.MemberIDs) == 0 || len(req.MemberIDs) > 100 {\n\t\twriteJSONError(w, \"\ucd08\ub300\ud560 \uba64\ubc84\ub294 1\uba85 \uc774\uc0c1 100\uba85 \uc774\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t\/\/ \ube48 ID, \ub098 \uc790\uc2e0, \uc911\ubcf5\ub41c ID\ub97c \uac78\ub7ec \ub0c5\ub2c8\ub2e4.\n\tseen := map[uuid.UUID]struct{}{userID: {}}\n\tmembers := make([]uuid.UUID, 0, len(req.MemberIDs))\n\tfor _, id := range req.MemberIDs {\n\t\tif id == uuid.Nil {\n\t\t\tcontinue\n\t\t}\n\t\tif _, dup := seen[id]; dup {\n\t\t\tcontinue\n\t\t}\n\t\tseen[id] = struct{}{}\n\t\tmembers = append(members, id)\n\t}\n\tif len(members) == 0 {\n\t\twriteJSONError(w, \"\ub098 \uc790\uc2e0 \uc678\uc5d0 \ucd08\ub300\ud560 \uba64\ubc84\uac00 \ud544\uc694\ud569\ub2c8\ub2e4\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar room db.Room\n\n\terr := database.WithTx(r.Context(), h.pool, func(q *db.Queries) error {\n\t\trCreated, err := q.CreateGroupRoom(r.Context(), db.CreateGroupRoomParams{\n\t\t\tName:      &amp;roomName,\n\t\t\tCreatedBy: &amp;userID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\troom = rCreated\n\n\t\t\/\/ \ubc29\uc7a5(\uc0dd\uc131\uc790) \ub4f1\ub85d\n\t\tif err := q.AddRoomMember(r.Context(), db.AddRoomMemberParams{\n\t\t\tRoomID:      room.ID,\n\t\t\tUserID:      userID,\n\t\t\tRole:        \"owner\",\n\t\t\tLastReadSeq: 0,\n\t\t\tJoinedSeq:   0,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t\/\/ \ucd08\ub300\ub41c \uba64\ubc84\ub4e4 \ub4f1\ub85d\n\t\tfor _, memberID := range members {\n\t\t\t\/\/ \uc2e4\uc81c \uc874\uc7ac\ud558\ub294 \ud65c\uc131 \uc720\uc800\uc778\uc9c0 \ud655\uc778\n\t\t\tu, err := q.GetUserByID(r.Context(), memberID)\n\t\t\tif errors.Is(err, pgx.ErrNoRows) {\n\t\t\t\treturn errInvalidMember\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif u.Status != \"active\" {\n\t\t\t\treturn errInvalidMember\n\t\t\t}\n\n\t\t\tif err := q.AddRoomMember(r.Context(), db.AddRoomMemberParams{\n\t\t\t\tRoomID:      room.ID,\n\t\t\t\tUserID:      memberID,\n\t\t\t\tRole:        \"member\",\n\t\t\t\tLastReadSeq: 0,\n\t\t\t\tJoinedSeq:   0,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tif errors.Is(err, errInvalidMember) {\n\t\t\twriteJSONError(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\t\/\/ \uadf8 \ubc16\uc758 \uc5d0\ub7ec\ub294 DB \uc5d0\ub7ec \uc6d0\ubb38\uc774 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub178\ucd9c\ub418\uc9c0 \uc54a\ub3c4\ub85d \uc77c\ubc18 \uba54\uc2dc\uc9c0\ub85c \ubc14\uafc9\ub2c8\ub2e4.\n\t\twriteJSONError(w, \"\uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusCreated, map[string]interface{}{\n\t\t\"room\": room,\n\t})\n}\n\n\/\/ 3. \ub0b4 \ucc44\ud305\ubc29 \ubaa9\ub85d \uc870\ud68c (GET \/api\/v1\/rooms)\nfunc (h *Handler) ListMyRooms(w http.ResponseWriter, r *http.Request) {\n\tuserID, ok := auth.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\trooms, err := h.queries.ListMyRooms(r.Context(), userID)\n\tif err != nil {\n\t\twriteJSONError(w, \"\ucc44\ud305\ubc29 \ubaa9\ub85d\uc744 \ubd88\ub7ec\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, map[string]interface{}{\n\t\t\"rooms\": rooms,\n\t})\n}\n\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 \uce5c\uad6c \ubc0f \ucc44\ud305\ubc29 \ub77c\uc6b0\ud130 \uc5f0\uacb0\ud558\uae30 (<code>server\/cmd\/api\/main.go<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">6\ud3b8\uc5d0\uc11c \uc791\uc131\ud55c <code>server\/cmd\/api\/main.go<\/code>\ub97c \uc138 \uad70\ub370\ub9cc \uace0\uce69\ub2c8\ub2e4. \ub098\uba38\uc9c0(DB \uc5f0\uacb0, <code>\/health<\/code>, CORS, \uc11c\ubc84 \uc885\ub8cc \ucc98\ub9ac \ub4f1)\ub294 6\ud3b8 \ucf54\ub4dc \uadf8\ub300\ub85c \ub461\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u2460 <code>import<\/code>\uc5d0 \ub450 \ud328\ud0a4\uc9c0 \ucd94\uac00<\/strong><\/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=\"\">\t\"github.com\/yourname\/dingdong-server\/internal\/friend\" \/\/ \ud83d\udc48 \ucd94\uac00\n\t\"github.com\/yourname\/dingdong-server\/internal\/room\"   \/\/ \ud83d\udc48 \ucd94\uac00<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u2461 \ud578\ub4e4\ub7ec \ucd08\uae30\ud654<\/strong> \u2014 <code>authHandler := ...<\/code> \uc904 \ubc14\ub85c \uc544\ub798\uc5d0 \ub450 \uc904 \ucd94\uac00<\/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=\"\">\tfriendHandler := friend.NewHandler(pool, queries, []byte(phoneSecret))\n\troomHandler := room.NewHandler(pool, queries)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u2462 \ub77c\uc6b0\ud130 \ub4f1\ub85d<\/strong> \u2014 6\ud3b8\uc758 <code>protected<\/code> \uadf8\ub8f9 \uc548, <code>\/api\/v1\/users\/me<\/code> \uc544\ub798\uc5d0 \ucd94\uac00<\/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=\"\">\t\t\tprotected.Get(\"\/api\/v1\/users\/me\", authHandler.GetMe)\n\n\t\t\t\/\/ \uce5c\uad6c \ub3d9\uae30\ud654 \ubc0f \ubaa9\ub85d \uc870\ud68c \ud83d\udc48 \ucd94\uac00\n\t\t\tprotected.Post(\"\/api\/v1\/friends\/sync\", friendHandler.SyncContacts)\n\t\t\tprotected.Get(\"\/api\/v1\/friends\", friendHandler.ListFriends)\n\n\t\t\t\/\/ \ucc44\ud305\ubc29 \uc0dd\uc131 \ubc0f \ubaa9\ub85d \uc870\ud68c \ud83d\udc48 \ucd94\uac00\n\t\t\tprotected.Get(\"\/api\/v1\/rooms\", roomHandler.ListMyRooms)\n\t\t\tprotected.Post(\"\/api\/v1\/rooms\/direct\", roomHandler.CreateDirectRoom)\n\t\t\tprotected.Post(\"\/api\/v1\/rooms\/group\", roomHandler.CreateGroupRoom)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\ubc84\uc804 \ud45c\uc2dc\ub97c \uc62c\ub9ac\uace0 \uc2f6\ub2e4\uba74 <code>\/health<\/code> \uc751\ub2f5\uacfc \uc2dc\uc791 \ub85c\uadf8\uc758 <code>v0.3.0<\/code>\uc744 <code>v0.4.0<\/code>\uc73c\ub85c \ubc14\uafb8\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\">6. \uc2e4\uc804 \uc5f0\ub3d9 \ud14c\uc2a4\ud2b8 (<code>curl<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc11c\ubc84\ub97c \uc7ac\uc2dc\uc791\ud55c \ub4a4(<code>go run .\/cmd\/api<\/code> \ub610\ub294 NAS <code>docker compose up -d --build<\/code>), \ub450 \ubc88\uc9f8 \uac00\uc871 \uacc4\uc815(<code>mommy<\/code>)\uc744 \uac00\uc785\uc2dc\ud0a4\uace0 \uccab \ubc88\uc9f8 \uacc4\uc815(<code>daddy<\/code>)\uc5d0\uc11c \uc5f0\ub77d\ucc98 \ub3d9\uae30\ud654 \ubc0f 1:1 \ucc44\ud305\ubc29 \uc0dd\uc131\uc744 \ud14c\uc2a4\ud2b8\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>daddy<\/code>\uc758 <code>accessToken<\/code>\uc774 \ud544\uc694\ud558\uba74 6\ud3b8\uc5d0\uc11c \ubc1b\uc544 \ub454 \uac12\uc744 \uc4f0\uac70\ub098, \uc544\ub798\ucc98\ub7fc \ub2e4\uc2dc \ub85c\uadf8\uc778\ud574\uc11c \ubc1b\uc2b5\ub2c8\ub2e4. (\uc720\ud6a8\uae30\uac04\uc774 15\ubd84\uc774\ub77c \ub9cc\ub8cc\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\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\/login \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"loginId\": \"daddy\", \"password\": \"my_secure_password_123!\", \"platform\": \"macos\", \"deviceName\": \"Mac Studio M4\"}'<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">6.1 \ub450 \ubc88\uc9f8 \uac00\uc871 \uacc4\uc815(<code>mommy<\/code>) \ud68c\uc6d0\uac00\uc785<\/h3>\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\": \"mommy\",\n    \"password\": \"mom_secure_password_123!\",\n    \"displayName\": \"\uc548\ubc29\ub9c8\ub2d8\",\n    \"phone\": \"+821098765432\",\n    \"platform\": \"ios\",\n    \"deviceName\": \"iPhone 16 Pro\"\n  }'<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">6.2 \uccab \ubc88\uc9f8 \uacc4\uc815(<code>daddy<\/code>)\uc73c\ub85c \uc5f0\ub77d\ucc98 \ub3d9\uae30\ud654 \ud638\ucd9c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>daddy<\/code> \uacc4\uc815\uc758 <code>accessToken<\/code>\uc744 \ud5e4\ub354\uc5d0 \ub123\uace0, \ud558\uc774\ud508\uc774 \ud3ec\ud568\ub41c \ub85c\uceec \uc804\ud654\ubc88\ud638(<code>010-9876-5432<\/code>)\uc640 \uac19\uc740 \ubc88\ud638\ub97c \ub2e4\ub978 \ud615\uc2dd(<code>+82 10-9876-5432<\/code>)\uc73c\ub85c \uc801\uc740 \uac12, \uac00\uc785\uc790\uac00 \uc544\ub2cc \ubc88\ud638\ub97c \ud568\uaed8 \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\/friends\/sync \\\n  -H \"Authorization: Bearer DADDY_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\n    \"phones\": [\"010-9876-5432\", \"+82 10-9876-5432\", \"010-0000-0000\"]\n  }'<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\uc751\ub2f5 \uacb0\uacfc (200 OK):<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{\n  \"friends\": [\n    {\n      \"id\": \"0192c9b1-3d44-7000-9abc-1234567890ab\",\n      \"display_name\": \"\uc548\ubc29\ub9c8\ub2d8\",\n      \"status_message\": null,\n      \"avatar_file_id\": null,\n      \"tag\": \"friend\",\n      \"nickname\": null,\n      \"blocked\": false,\n      \"created_at\": \"2026-10-31T09:30:00Z\"\n    }\n  ]\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\ub450 \uac00\uc9c0 \ud45c\uae30\uac00 \ubaa8\ub450 \uc11c\ubc84\uc5d0\uc11c <code>+821098765432<\/code>\ub85c \uc815\uaddc\ud654\ub418\uc5b4 \ud558\ub098\ub85c \ud569\uccd0\uc9c0\uace0, HMAC \ud574\uc2dc\uac00 \uc77c\uce58\ud558\ub294 <code>\uc548\ubc29\ub9c8\ub2d8<\/code> \uacc4\uc815\uc774 \uce5c\uad6c\ub85c \ub4f1\ub85d\ub429\ub2c8\ub2e4. \uac00\uc785\uc790\uac00 \uc544\ub2cc \ubc88\ud638\ub294 \uc870\uc6a9\ud788 \ubb34\uc2dc\ub418\uba70, \uc751\ub2f5\uc5d0\ub294 <code>phone_hash<\/code>\uac00 \ub4e4\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.3 1:1 \ucc44\ud305\ubc29 \uc0dd\uc131 \ubc0f \ucc44\ud305\ubc29 \ubaa9\ub85d \uc870\ud68c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\uc704\uc5d0\uc11c \ucc3e\uc740 \uce5c\uad6c\uc758 <code>id<\/code> \ub97c <code>targetUserId<\/code> \uc5d0 \ub123\uc5b4 1:1 \ubc29\uc744 \uc0dd\uc131\ud55c \ub4a4 \ubaa9\ub85d\uc744 \uc870\ud68c\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=\"\"># 1:1 \ucc44\ud305\ubc29 \uc0dd\uc131 (\uc5ec\ub7ec \ubc88 \ud638\ucd9c\ud574\ub3c4 \ub3d9\uc77c\ud55c \ubc29 ID\uac00 \ubc18\ud658\ub428)\ncurl -X POST http:\/\/localhost:3000\/api\/v1\/rooms\/direct \\\n  -H \"Authorization: Bearer DADDY_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\n    \"targetUserId\": \"0192c9b1-3d44-7000-9abc-1234567890ab\"\n  }'\n\n# \ub0b4 \ucc44\ud305\ubc29 \ubaa9\ub85d \uc870\ud68c\ncurl -X GET http:\/\/localhost:3000\/api\/v1\/rooms \\\n  -H \"Authorization: Bearer DADDY_ACCESS_TOKEN\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\ubaa9\ub85d \uc870\ud68c \uc751\ub2f5 \uacb0\uacfc (200 OK):<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{\n  \"rooms\": [\n    {\n      \"id\": \"a7f8d9e0-1234-4567-89ab-cdef01234567\",\n      \"type\": \"direct\",\n      \"display_title\": \"\uc548\ubc29\ub9c8\ub2d8\",\n      \"display_avatar_id\": null,\n      \"last_seq\": 0,\n      \"last_read_seq\": 0,\n      \"unread_count\": 0,\n      \"pinned\": false,\n      \"muted\": false,\n      \"last_message_type\": null,\n      \"last_message_text\": null,\n      \"last_message_at\": null,\n      \"updated_at\": \"2026-10-31T09:32:00Z\"\n    }\n  ]\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">1:1 \ucc44\ud305\ubc29\uc740 \ubcc4\ub3c4\uc758 \ubc29 \uc774\ub984(<code>r.name<\/code>)\uc774 \uc5c6\ub354\ub77c\ub3c4 \uc0c1\ub300\ubc29\uc758 \uc774\ub984(<code>\uc548\ubc29\ub9c8\ub2d8<\/code>)\uc774 <code>display_title<\/code>\ub85c \ub0b4\ub824\uc635\ub2c8\ub2e4. <code>mommy<\/code>\uc758 \ud1a0\ud070\uc73c\ub85c \uac19\uc740 \ubaa9\ub85d\uc744 \uc870\ud68c\ud558\uba74 \uac19\uc740 \ubc29\uc774 <code>\uc6b0\ub9ac\uc9d1 \ub300\uc7a5<\/code>\uc774\ub77c\ub294 \uc81c\ubaa9\uc73c\ub85c \ubcf4\uc785\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 8\ud3b8\uc5d0\uc11c\ub294 \uba54\uc2e0\uc800\uc758 \uc18c\uc15c \uadf8\ub798\ud504(\uce5c\uad6c \uad00\uacc4)\uc640 \ub300\ud654\ubc29 \uad00\ub9ac \ubc31\uc5d4\ub4dc API\ub97c \uc644\uc131\ud588\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>E.164 \uc815\uaddc\ud654 \ubc0f HMAC \uc5f0\ub77d\ucc98 \ub9e4\uce6d:<\/strong> \ud558\uc774\ud508\uc774\ub098 \uacf5\ubc31\uc774 \uc11e\uc778 \uc804\ud654\ubc88\ud638\ub97c \ud45c\uc900 \uaddc\uaca9\uc73c\ub85c \ud1b5\uc77c(\uac00\uc785 \ub54c\uc640 \ub3d9\uae30\ud654 \ub54c \uac19\uc740 \ud568\uc218 \uc0ac\uc6a9)\ud55c \ub4a4 \uc11c\ubc84 \ube44\ubc00\ud0a4 \uae30\ubc18 HMAC \ud574\uc2dc\ub85c \ub9e4\uce6d\ud558\uace0, \uacf5\uac1c \ud504\ub85c\ud544 \uceec\ub7fc\ub9cc \ubc18\ud658\ud558\ub3c4\ub85d \ubd84\ub9ac\ud588\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc6d0\uc790\uc801 1:1 \ubc29 \uc0dd\uc131:<\/strong> <code>direct_key<\/code> \uc815\ub82c\uacfc <code>ON CONFLICT<\/code> \uad6c\ubb38\uc744 \uacb0\ud569\ud574 \uc911\ubcf5 \ubc29 \uc0dd\uc131\uc744 DB \ub808\ubca8\uc5d0\uc11c \ucc28\ub2e8\ud588\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uace0\uc18d \ucc44\ud305\ubc29 \ubaa9\ub85d \ucffc\ub9ac:<\/strong> <code>rooms.last_seq<\/code> \uc640 <code>messages<\/code> \uc778\ub371\uc2a4\ub97c \uc870\uc778\ud574 \uc548 \uc77d\uc740 \uba54\uc2dc\uc9c0 \uac1c\uc218\uc640 \ub9c8\uc9c0\ub9c9 \uba54\uc2dc\uc9c0 \ubbf8\ub9ac\ubcf4\uae30\ub97c \ub2e8\uc77c \ucffc\ub9ac\ub85c \uac00\uc838\uc654\uc2b5\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">\uc544\uc9c1 \uc801\uc6a9\ud558\uc9c0 \uc54a\uc740 \uac83 (\ucd94\ud6c4 \ubcf4\uc644 \uc0ac\ud56d)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uc5f0\ub77d\ucc98 \ub3d9\uae30\ud654 \ud638\ucd9c \ube48\ub3c4 \uc81c\ud55c (Rate Limit):<\/strong> 1\ud68c \uc694\uccad\ub2f9 500\uac1c\ub85c \uc81c\ud55c\ud588\uc73c\ub098, \uc9e7\uc740 \uc2dc\uac04\uc5d0 \uc218\ub9cc \ubc88 \ud638\ucd9c\ud558\uba74 \uc5ec\uc804\ud788 \ubc88\ud638 \ub300\uc5ed\uc744 \ud0d0\uc0c9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790\ubcc4 \uc77c\uc77c \ud638\ucd9c \ud69f\uc218 \uc81c\ud55c(\uc608: \ud558\ub8e8 10\ud68c)\uc744 \ucd94\uac00\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\ub2e8\ubc29\ud5a5 \uce5c\uad6c \ub4f1\ub85d \uad6c\uc870:<\/strong> \ud604\uc7ac <code>\/api\/v1\/friends\/sync<\/code> \ub294 \uc694\uccad\ud55c \uc0ac\ub78c\uc758 <code>friendships<\/code> \uc5d0\ub9cc \uc0c1\ub300\ubc29\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4(\uce74\uce74\uc624\ud1a1\uacfc \ub3d9\uc77c\ud55c \ub2e8\ubc29\ud5a5 \uc8fc\uc18c\ub85d \ubaa8\ub378). \ub530\ub77c\uc11c \uadf8\ub8f9 \ucc44\ud305\ubc29\uc5d0 \ucd08\ub300\ud560 \ub54c \uc0c1\ub300\ubc29\ub3c4 \ub098\ub97c \uce5c\uad6c\ub85c \ub4f1\ub85d\ud588\ub294\uc9c0 \ud655\uc778\ud558\ub294 \uc635\uc158\uc740 \uc544\uc9c1 \uc5c6\uc73c\uba70, \ud65c\uc131 \uc0ac\uc6a9\uc790\uba74 \ucd08\ub300\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc804\ud654\ubc88\ud638 \uc18c\uc720 \uc778\uc99d \uc5c6\uc74c:<\/strong> \uac00\uc785\ud560 \ub54c \uc785\ub825\ud55c \ubc88\ud638\uac00 \uc815\ub9d0 \ubcf8\uc778 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4(SMS \uc778\uc99d \ub4f1). \ucd08\ub300\ubc1b\uc740 \uac00\uc871\ub07c\ub9ac\ub9cc \uc4f0\ub294 \uc9c0\uae08\uc740 \uad1c\ucc2e\uc9c0\ub9cc, \ub0a8\uc758 \ubc88\ud638\ub97c \uc801\uc5b4 \uac00\uc785\ud558\uba74 \uadf8 \uc0ac\ub78c\uc758 \uc5f0\ub77d\ucc98\uc5d0 \ub0b4 \uc774\ub984\uc774 \ub739\ub2c8\ub2e4. \uc678\ubd80\uc5d0 \uacf5\uac1c\ud558\uae30 \uc804\uc5d0 \ubc18\ub4dc\uc2dc \ubcf4\uc644\ud574\uc57c \ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\ucc28\ub2e8 \uae30\ub2a5\uc758 API\uac00 \uc544\uc9c1 \uc5c6\uc74c:<\/strong> <code>friendships.blocked<\/code>\ub97c \ubc14\uafb8\ub294 API\uac00 \uc5c6\uc5b4\uc11c \uc9c0\uae08\uc740 \ud56d\uc0c1 <code>false<\/code>\uc785\ub2c8\ub2e4. 1:1 \ubc29\uc744 \ub9cc\ub4e4 \ub54c\uc758 \ucc28\ub2e8 \ud655\uc778(<code>IsBlockedByUser<\/code>)\uc740 \ucc28\ub2e8 API\ub97c \ub9cc\ub4e0 \ub4a4\uc5d0\uc57c \uc2e4\uc81c\ub85c \ub3d9\uc791\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\ub85c\uadf8\uc778\u00b7\ub0b4 \uc815\ubcf4 \uc751\ub2f5\uc758 <code>phone_hash<\/code>:<\/strong> 6\ud3b8\uc758 <code>\/auth\/*<\/code>\uc640 <code>\/users\/me<\/code> \uc751\ub2f5\uc740 \uc544\uc9c1 <code>db.User<\/code> \uc804\uccb4\ub97c \ub0b4\ub824\ubcf4\ub0b4\ubbc0\ub85c \uc751\ub2f5 \uc804\uc6a9 \uad6c\uc870\uccb4\ub85c \ubd84\ub9ac\ud574\uc57c \ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\ucc44\ud305\ubc29 \ub098\uac00\uae30 \ubc0f \uc7ac\uc785\uc7a5 \ucc98\ub9ac:<\/strong> 1:1 \ubc29\uc5d0\uc11c \ud55c\ucabd\uc774 \ub098\uac14\ub2e4\uac00 \ub2e4\uc2dc \uba54\uc2dc\uc9c0\uac00 \uc624\uba74 \ubc29\uc774 \uc7ac\ud65c\uc131\ud654\ub418\uace0 <code>joined_seq<\/code> \uc774\ud6c4 \uba54\uc2dc\uc9c0\ub9cc \ubcf4\uc774\uac8c \ud558\ub294 \uc138\ubc00\ud55c \uc0c1\ud0dc \uad00\ub9ac\ub294 \ucd94\ud6c4 \uba54\uc2dc\uc9c0 \ud3b8\uc5d0\uc11c \ub2e4\ub8f9\ub2c8\ub2e4.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\ubc31\uc5d4\ub4dc\uc5d0 \uce5c\uad6c\uc640 \ucc44\ud305\ubc29 API\uac00 \uc900\ube44\ub418\uc5c8\uc73c\ub2c8, \ub2e4\uc74c [<strong>\uac1c\ubc1c\uc77c\uc9c0 #09 | \ud074\ub77c\uc774\uc5b8\ud2b8<\/strong>]\uc5d0\uc11c\ub294 \ub2e4\uc2dc Flutter \uc571\uc73c\ub85c \ub118\uc5b4\uac00 <strong>\ud558\ub2e8 \ud0ed \ub124\ube44\uac8c\uc774\uc158(\uce5c\uad6c \ud0ed \/ \ucc44\ud305 \ud0ed), \uc5f0\ub77d\ucc98 \ub3d9\uae30\ud654 \uc5f0\ub3d9, \uadf8\ub9ac\uace0 \ucc44\ud305\ubc29 \uc0dd\uc131 \ubc0f \ubaa9\ub85d \ud654\uba74<\/strong> \uc744 \uad6c\ud604\ud558\uaca0\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\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 8\ud3b8\uc785\ub2c8\ub2e4. \uc9c0\ub09c [7\ud3b8]\uc5d0\uc11c\ub294 Flutter \uc571\uc5d0\uc11c \ub85c\uadf8\uc778 \ubc0f \ud68c\uc6d0\uac00\uc785\uc744 \uc218\ud589\ud558\uace0 \ud1a0\ud070\uc744 \uc790\ub3d9\uc73c\ub85c \uac31\uc2e0\ud558\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d \ud750\ub984\uc744 \uc5f0\uacb0\ud588\uc2b5\ub2c8\ub2e4.\uc774\ubc88 8\ud3b8\uc5d0\uc11c\ub294 \ub2e4\uc2dc Go \ubc31\uc5d4\ub4dc\ub85c \ub3cc\uc544\uc640, \ub85c\uadf8\uc778\ud55c \uc774\ud6c4 \uc571 \ud654\uba74\uc744 \ucc44\uc6b0\ub294 \ub370 \ud544\uc694\ud55c \uc5f0\ub77d\ucc98 \uae30\ubc18 \uce5c\uad6c \ub3d9\uae30\ud654 API \uc640 1:1 \ubc0f \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131\u00b7\ubaa9\ub85d \uc870\ud68c API \ub97c \uad6c\ud604\ud569\ub2c8\ub2e4. 1. \ud575\uc2ec \uc124\uacc4 &#8230; <a title=\"[\uac1c\ubc1c\uc77c\uc9c0 #08 | \ubc31\uc5d4\ub4dc] \uc5f0\ub77d\ucc98 HMAC \ud574\uc2dc \uce5c\uad6c \ub9e4\uce6d &amp; \uc911\ubcf5 \uc5c6\ub294 1:1 \/ \uadf8\ub8f9 \ucc44\ud305\ubc29 API \uad6c\ud604\" class=\"read-more\" href=\"https:\/\/dingdong.goodday.ing\/?p=215\" aria-label=\"[\uac1c\ubc1c\uc77c\uc9c0 #08 | \ubc31\uc5d4\ub4dc] \uc5f0\ub77d\ucc98 HMAC \ud574\uc2dc \uce5c\uad6c \ub9e4\uce6d &amp; \uc911\ubcf5 \uc5c6\ub294 1:1 \/ \uadf8\ub8f9 \ucc44\ud305\ubc29 API \uad6c\ud604\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-215","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\/215","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=215"}],"version-history":[{"count":2,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/215\/revisions"}],"predecessor-version":[{"id":218,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/215\/revisions\/218"}],"wp:attachment":[{"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=215"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=215"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=215"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}