{"id":200,"date":"2026-10-29T08:06:45","date_gmt":"2026-10-29T08:06:45","guid":{"rendered":"https:\/\/dinggo.duckdns.org\/?p=200"},"modified":"2026-10-31T10:02:17","modified_gmt":"2026-10-31T10:02:17","slug":"%ea%b0%9c%eb%b0%9c%ec%9d%bc%ec%a7%80-07-%ed%81%b4%eb%9d%bc%ec%9d%b4%ec%96%b8%ed%8a%b8-flutter-%ec%95%b1-%ec%b2%ab-%ec%97%b0%eb%8f%99-secure-storage-%ed%86%a0%ed%81%b0-%ea%b4%80%eb%a6%ac-dio","status":"publish","type":"post","link":"https:\/\/dingdong.goodday.ing\/?p=200","title":{"rendered":"[\uac1c\ubc1c\uc77c\uc9c0 #07 | \ud074\ub77c\uc774\uc5b8\ud2b8] Flutter \uc571 \uccab \uc5f0\ub3d9: Secure Storage \ud1a0\ud070 \uad00\ub9ac, Dio \uc790\ub3d9 \uac31\uc2e0 &amp; \ub85c\uadf8\uc778 \ud654\uba74 \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 7\ud3b8\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc9c0\ub09c [<strong>6\ud3b8<\/strong>]\uc5d0\uc11c\ub294 Go \ubc31\uc5d4\ub4dc\uc5d0 \ucd08\ub300\ucf54\ub4dc \uae30\ubc18 \ud68c\uc6d0\uac00\uc785, Argon2id \ube44\ubc00\ubc88\ud638 \ud574\uc2f1, \ub2e4\uc911 \uae30\uae30\uc6a9 JWT Access\/Refresh \ud1a0\ud070 \ubc1c\uae09 API\ub97c \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.<br>\ubc31\uc5d4\ub4dc \uc9c4\ub3c4\ub97c \ub354 \ub098\uac00\uae30 \uc804\uc5d0, \uc774\ubc88 7\ud3b8\uc5d0\uc11c\ub294 [<strong>2\ud3b8<\/strong>]\uc5d0\uc11c \uc900\ube44\ud574 \ub454 Flutter \uac1c\ubc1c \ud658\uacbd\uc744 \uc5f4\uc5b4 <strong>\ub85c\uadf8\uc778\/\ud68c\uc6d0\uac00\uc785 \ud654\uba74\uacfc \ud1a0\ud070 \uc790\ub3d9 \uac31\uc2e0 \ub124\ud2b8\uc6cc\ud06c \uacc4\uce35<\/strong>\uc744 \uad6c\ud604\ud558\uace0 \uc2e4\uc81c \uc11c\ubc84\uc640 \ud1b5\uc2e0\ud558\ub294 \uc804\uccb4 \ud750\ub984\uc744 \uc5f0\uacb0\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. \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d \ud750\ub984 \ubc0f \ud3f4\ub354 \uad6c\uc870<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc571\uc774 \uc2dc\uc791\ub420 \ub54c \uc800\uc7a5\ub41c \ud1a0\ud070\uc744 \ud655\uc778\ud574 \uc790\ub3d9 \ub85c\uadf8\uc778\uc744 \uc2dc\ub3c4\ud558\uace0, \ud1b5\uc2e0 \uc911 Access Token(15\ubd84)\uc774 \ub9cc\ub8cc\ub418\uba74 \ubc31\uadf8\ub77c\uc6b4\ub4dc\uc5d0\uc11c Refresh Token(30\uc77c)\uc73c\ub85c \uc7ac\ubc1c\uae09\ubc1b\ub294 \uad6c\uc870\uc785\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[\"\uc571 \uc2e4\ud589\"]:::titleStyle -->|\"Secure Storage \uc870\ud68c\"| B[\"\/api\/v1\/users\/me \ud638\ucd9c\"]:::nodeStyle\n    B -->|\"200 OK\"| C[\"\ud648 \ud654\uba74 \uc774\ub3d9 (\uc790\ub3d9 \ub85c\uadf8\uc778)\"]:::titleStyle\n    B -->|\"401 \ub9cc\ub8cc\"| D[\"Dio \uc778\ud130\uc149\ud130 (\/auth\/refresh)\"]:::nodeStyle\n    D -->|\"\uac31\uc2e0 \uc131\uacf5\"| B\n    D -->|\"\uac31\uc2e0 \uc2e4\ud328 \/ \ud1a0\ud070 \uc5c6\uc74c\"| E[\"\ub85c\uadf8\uc778 \ud654\uba74 \uc774\ub3d9\"]:::titleStyle<\/pre><div class=\"wp-block-canaryblue-mermaid-diagram__output\"><\/div><\/div>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\ubcf4\uc548 \uc800\uc7a5\uc18c (<code>flutter_secure_storage<\/code>):<\/strong> \ud3c9\ubb38\uc73c\ub85c \uc800\uc7a5\ub418\ub294 <code>SharedPreferences<\/code> \ub300\uc2e0, iOS\/macOS\uc758 Keychain\uacfc Android\uc758 Keystore \uae30\ubc18 \uc554\ud638\ud654\ub97c \uc0ac\uc6a9\ud574 \ud1a0\ud070\uc744 \ubcf4\uad00\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc21c\ucc28 \uac31\uc2e0 \uc778\ud130\uc149\ud130 (<code>QueuedInterceptorsWrapper<\/code>):<\/strong> 6\ud3b8 \ubc31\uc5d4\ub4dc\uc5d0 <strong>Refresh Token Rotation<\/strong>(1\ud68c \uc0ac\uc6a9 \uc2dc \uc0c8 \ud1a0\ud070\uc73c\ub85c \uad50\uccb4)\uc774 \uc801\uc6a9\ub418\uc5b4 \uc788\uc73c\ubbc0\ub85c, \ub3d9\uc2dc\uc5d0 \uc5ec\ub7ec \uc694\uccad\uc774 <code>401<\/code> \uc5d0\ub7ec\ub97c \ubc1b\uc558\uc744 \ub54c \uac31\uc2e0 \uc694\uccad\uc774 \uc911\ubcf5\uc73c\ub85c \ub098\uac00\uc9c0 \uc54a\ub3c4\ub85d \ud050(Queue) \ubc29\uc2dd\uc73c\ub85c \uc21c\ucc28 \ucc98\ub9ac\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc0c1\ud0dc \uad00\ub9ac (<code>flutter_riverpod<\/code>):<\/strong> &#8220;\uc9c0\uae08 \ub85c\uadf8\uc778\ub41c \uc0ac\uc6a9\uc790&#8221;(<code>AsyncValue&lt;User?><\/code>)\ub9cc \uc804\uc5ed\uc73c\ub85c \uad00\ub9ac\ud558\uc5ec, \ub85c\uadf8\uc778 \uc5ec\ubd80\uc5d0 \ub530\ub77c \ud654\uba74\uc774 \uc790\ub3d9\uc73c\ub85c \uc804\ud658\ub418\ub3c4\ub85d \uad6c\uc131\ud569\ub2c8\ub2e4. (\ub85c\uadf8\uc778 \ubc84\ud2bc\uc758 \ub85c\ub529 \ud45c\uc2dc\uc640 \uc5d0\ub7ec \uba54\uc2dc\uc9c0\ub294 \ud654\uba74 \uc548\uc5d0\uc11c \ub530\ub85c \ub2e4\ub8f9\ub2c8\ub2e4.)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\uc804\uccb4 \uc791\uc5c5 \ud3f4\ub354 \uad6c\uc870 (\ubc31\uc5d4\ub4dc + Flutter \uc571)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ub0b4 \ub9e5(Mac)\uc758 \uc804\uccb4 \ud504\ub85c\uc81d\ud2b8 \ub8e8\ud2b8 \ud3f4\ub354(\uc608: <code>dingdong\/<\/code>) \uc544\ub798\uc5d0 4~6\ud3b8\uc5d0\uc11c \ub9cc\ub4e0 Go \ubc31\uc5d4\ub4dc(<code>server\/<\/code>)\uc640 2\ud3b8\uc5d0\uc11c \uc0dd\uc131\ud55c Flutter \ud504\ub85c\uc81d\ud2b8(<code>dingdong_app\/<\/code>)\uac00 \ub098\ub780\ud788 \uc704\uce58\ud558\ub294 \uad6c\uc870\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc624\ub298 \uc791\uc131\ud560 \ubaa8\ub4e0 \ucf54\ub4dc\ub294 <code>dingdong_app\/lib\/<\/code> \ud3f4\ub354 \uc544\ub798\uc5d0 \ub4e4\uc5b4\uac11\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=\"\">dingdong\/                                 # \ub0b4 \ub9e5(Mac) \uc804\uccb4 \ud504\ub85c\uc81d\ud2b8 \ub8e8\ud2b8 \ud3f4\ub354\n \u251c\u2500 server\/                               # [4~6\ud3b8] Go \ubc31\uc5d4\ub4dc \uc11c\ubc84 \ud3f4\ub354\n \u2502   \u251c\u2500 cmd\/api\/main.go\n \u2502   \u251c\u2500 internal\/\n \u2502   \u251c\u2500 migrations\/\n \u2502   \u251c\u2500 queries\/\n \u2502   \u251c\u2500 go.mod\n \u2502   \u2514\u2500 sqlc.yaml\n \u2502\n \u2514\u2500 dingdong_app\/                         # \ud83d\udc48 [2\ud3b8, 7\ud3b8] Flutter \uc571 \ud3f4\ub354 (pubspec.yaml \uc704\uce58)\n     \u251c\u2500 android\/\n     \u251c\u2500 ios\/\n     \u251c\u2500 macos\/\n     \u251c\u2500 pubspec.yaml\n     \u2514\u2500 lib\/                              # \ud83d\udc48 Flutter \uc18c\uc2a4 \ucf54\ub4dc \uae30\ubcf8 \ud3f4\ub354\n         \u251c\u2500 core\/                         # [\uc0c8\ub85c \ub9cc\ub4e4 \ud3f4\ub354] \uacf5\ud1b5 \ud575\uc2ec \ubaa8\ub4c8\n         \u2502   \u251c\u2500 storage\/\n         \u2502   \u2502   \u2514\u2500 token_storage.dart    # 1. Access \/ Refresh \ud1a0\ud070 \uc554\ud638\ud654 \uc800\uc7a5\uc18c\n         \u2502   \u2514\u2500 network\/\n         \u2502       \u2514\u2500 dio_client.dart       # 2. Dio \uc124\uc815 \ubc0f 401 \ud1a0\ud070 \uc790\ub3d9 \uac31\uc2e0 \uc778\ud130\uc149\ud130\n         \u251c\u2500 features\/                     # [\uc0c8\ub85c \ub9cc\ub4e4 \ud3f4\ub354] \uae30\ub2a5\ubcc4 \ud654\uba74 \ubc0f \uc0c1\ud0dc \uad00\ub9ac\n         \u2502   \u2514\u2500 auth\/\n         \u2502       \u251c\u2500 user_model.dart       # 3. \uc11c\ubc84 \uc751\ub2f5 JSON \ub9e4\ud551 \ubaa8\ub378\n         \u2502       \u251c\u2500 auth_provider.dart    # 4. \ub85c\uadf8\uc778 \/ \ud68c\uc6d0\uac00\uc785 \/ \uc790\ub3d9 \ub85c\uadf8\uc778 \uc0c1\ud0dc \uad00\ub9ac\n         \u2502       \u2514\u2500 login_screen.dart     # 5. \ub85c\uadf8\uc778 \ubc0f \ud68c\uc6d0\uac00\uc785 UI \ud654\uba74\n         \u2514\u2500 main.dart                     # 6. \uc571 \uc9c4\uc785\uc810 \ubc0f \ub77c\uc6b0\ud305 \ubd84\uae30 (\uae30\uc874 \ud30c\uc77c \uc218\uc815)<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. Flutter \ud504\ub85c\uc81d\ud2b8 \uc900\ube44 \ubc0f \ud328\ud0a4\uc9c0 \uc124\uce58<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\ud130\ubbf8\ub110\uc5d0\uc11c 2\ud3b8\uc5d0\uc11c \ub9cc\ub4e0 Flutter \ud504\ub85c\uc81d\ud2b8(<code>dingdong_app<\/code>) \ud3f4\ub354\ub85c \uc774\ub3d9\ud558\uc5ec \ud544\uc694\ud55c \ud328\ud0a4\uc9c0 3\uac1c\ub97c \uc124\uce58\ud558\uace0, <code>lib\/<\/code> \uc544\ub798\uc5d0 \ud558\uc704 \ud3f4\ub354\ub4e4\uc744 \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 dingdong_app\n\n# 1. HTTP \ud074\ub77c\uc774\uc5b8\ud2b8(dio), \ubcf4\uc548 \uc800\uc7a5\uc18c(flutter_secure_storage), \uc0c1\ud0dc \uad00\ub9ac(flutter_riverpod) \uc124\uce58\nflutter pub add dio flutter_secure_storage flutter_riverpod\n\n# 2. lib\/ \ud3f4\ub354 \uc544\ub798\uc5d0 \uae30\ub2a5\ubcc4 \ud558\uc704 \ud3f4\ub354 \uc77c\uad04 \uc0dd\uc131\nmkdir -p lib\/core\/storage lib\/core\/network lib\/features\/auth<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\u26a0\ufe0f macOS \ub370\uc2a4\ud06c\ud1b1 \ud0c0\uac9f\uc73c\ub85c \uc2e4\ud589\ud560 \uacbd\uc6b0 \uc8fc\uc758\uc0ac\ud56d (iOS\/Android\ub294 \ud574\ub2f9 \uc5c6\uc74c)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>flutter_secure_storage<\/code> \ub294 \ub0b4\ubd80\uc801\uc73c\ub85c \uc560\ud50c\uc758 Keychain\uc744 \uc0ac\uc6a9\ud569\ub2c8\ub2e4. iOS \uc2dc\ubbac\ub808\uc774\ud130\ub098 Android \uc5d0\ubbac\ub808\uc774\ud130\uc5d0\uc11c\ub294 \ubcc4\ub3c4 \uc124\uc815 \uc5c6\uc774 \ubc14\ub85c \ub3d9\uc791\ud558\uc9c0\ub9cc, <strong>macOS \ub370\uc2a4\ud06c\ud1b1 \uc571(<code>flutter run -d macos<\/code>)<\/strong> \uc73c\ub85c \uc2e4\ud589\ud560 \ub54c\ub294 Keychain \uc811\uadfc \uad8c\ud55c\uc774 \uc5c6\uc73c\uba74 <code>OSStatus error -34018<\/code> \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">macOS \ub370\uc2a4\ud06c\ud1b1\uc73c\ub85c \ud14c\uc2a4\ud2b8\ud558\ub824\uba74 \uc544\ub798 \ub450 \ud30c\uc77c\uc758 <code>&lt;dict&gt;<\/code> \uc548\uc5d0 \ud56d\ubaa9\uc744 \ucd94\uac00\ud558\uac70\ub098, <strong>iOS \uc2dc\ubbac\ub808\uc774\ud130<\/strong>\ub85c \uc2e4\ud589\ud558\uc138\uc694. \uae30\uc874 \ud56d\ubaa9\uc740 \uc9c0\uc6b0\uc9c0 \ub9d0\uace0, \uc5c6\ub294 \uac83\ub9cc \ucd94\uac00\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>\ub300\uc0c1 \ud30c\uc77c: <code>macos\/Runner\/DebugProfile.entitlements<\/code> \ubc0f <code>macos\/Runner\/Release.entitlements<\/code><\/em><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\t&lt;!-- \uc678\ubd80 API \uc11c\ubc84\uc640 \ud1b5\uc2e0\ud558\uae30 \uc704\ud55c \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \uad8c\ud55c -->\n\t&lt;key>com.apple.security.network.client&lt;\/key>\n\t&lt;true\/>\n\t&lt;!-- flutter_secure_storage\uac00 \ud0a4\uccb4\uc778\uc744 \uc4f0\uae30 \uc704\ud574 \ud544\uc694 -->\n\t&lt;key>keychain-access-groups&lt;\/key>\n\t&lt;array\/><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. \ud1a0\ud070 \uc800\uc7a5\uc18c \ubc0f \ub370\uc774\ud130 \ubaa8\ub378 \uad6c\ud604<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">3.1 \ubcf4\uc548 \ud1a0\ud070 \uc800\uc7a5\uc18c (<code>lib\/core\/storage\/token_storage.dart<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Access Token\uacfc Refresh Token\uc744 \uc77d\uace0 \uc4f0\ub294 \ub798\ud37c \ud074\ub798\uc2a4\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dart\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import 'package:flutter_riverpod\/flutter_riverpod.dart';\nimport 'package:flutter_secure_storage\/flutter_secure_storage.dart';\n\nfinal tokenStorageProvider = Provider&lt;TokenStorage>((ref) {\n  return TokenStorage();\n});\n\nclass TokenStorage {\n  static const _accessTokenKey = 'dingdong_access_token';\n  static const _refreshTokenKey = 'dingdong_refresh_token';\n\n  \/\/ \ud50c\ub7ab\ud3fc\ubcc4 \uae30\ubcf8 \ubcf4\uc548 \uc800\uc7a5\uc18c(Keychain \/ Android Keystore)\ub97c \uadf8\ub300\ub85c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\n  final FlutterSecureStorage _storage = const FlutterSecureStorage();\n\n  Future&lt;void> saveTokens({\n    required String accessToken,\n    required String refreshToken,\n  }) async {\n    await Future.wait([\n      _storage.write(key: _accessTokenKey, value: accessToken),\n      _storage.write(key: _refreshTokenKey, value: refreshToken),\n    ]);\n  }\n\n  Future&lt;String?> getAccessToken() => _storage.read(key: _accessTokenKey);\n\n  Future&lt;String?> getRefreshToken() => _storage.read(key: _refreshTokenKey);\n\n  Future&lt;void> clearTokens() async {\n    await Future.wait([\n      _storage.delete(key: _accessTokenKey),\n      _storage.delete(key: _refreshTokenKey),\n    ]);\n  }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3.2 \uc0ac\uc6a9\uc790 \ubc0f \uc778\uc99d \uc751\ub2f5 \ubaa8\ub378 (<code>lib\/features\/auth\/user_model.dart<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">6\ud3b8\uc758 Go \uc11c\ubc84 \ucf54\ub4dc\ub97c \ubcf4\uba74, \ud578\ub4e4\ub7ec\uac00 \uc815\uc758\ud55c <code>AuthResponse<\/code>\ub294 <code>camelCase<\/code>(<code>accessToken<\/code>, <code>refreshToken<\/code>)\ub97c \uc0ac\uc6a9\ud558\uace0, <code>sqlc<\/code>\uac00 \uc0dd\uc131\ud55c <code>db.User<\/code> \uad6c\uc870\uccb4\ub294 DB \uceec\ub7fc\uba85 \uadf8\ub300\ub85c <code>snake_case<\/code>(<code>display_name<\/code>, <code>created_at<\/code>)\ub97c JSON \ud0a4\ub85c \ub0b4\ub824\uc90d\ub2c8\ub2e4. \uc774\uc5d0 \ub9de\ucdb0 \ud30c\uc2f1 \ub85c\uc9c1\uc744 \uc791\uc131\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dart\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">class User {\n  final String id;\n  final String displayName;\n  final String? statusMessage;\n  final String role;\n  final String status;\n\n  const User({\n    required this.id,\n    required this.displayName,\n    this.statusMessage,\n    required this.role,\n    required this.status,\n  });\n\n  factory User.fromJson(Map&lt;String, dynamic> json) {\n    return User(\n      id: json['id'] as String,\n      displayName: json['display_name'] as String,\n      statusMessage: json['status_message'] as String?,\n      role: json['role'] as String,\n      status: json['status'] as String,\n    );\n  }\n}\n\nclass AuthResponse {\n  final String accessToken;\n  final String refreshToken;\n  final User user;\n\n  const AuthResponse({\n    required this.accessToken,\n    required this.refreshToken,\n    required this.user,\n  });\n\n  factory AuthResponse.fromJson(Map&lt;String, dynamic> json) {\n    return AuthResponse(\n      accessToken: json['accessToken'] as String,\n      refreshToken: json['refreshToken'] as String,\n      user: User.fromJson(json['user'] as Map&lt;String, dynamic>),\n    );\n  }\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. Dio \ud074\ub77c\uc774\uc5b8\ud2b8 \ubc0f \ud1a0\ud070 \uc790\ub3d9 \uac31\uc2e0 \uc778\ud130\uc149\ud130 (<code>lib\/core\/network\/dio_client.dart<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc778\ud130\uc149\ud130 \uc791\uc131 \uc2dc \uc8fc\uc758\ud574\uc57c \ud560 \uc810\uc740 \ub2e4\uc74c \ub124 \uac00\uc9c0\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><code>\/api\/v1\/auth\/<\/code> \ud558\uc704 \uacbd\ub85c(\ub85c\uadf8\uc778, \ud68c\uc6d0\uac00\uc785, \ub9ac\ud504\ub808\uc2dc \uc790\uccb4)\uc5d0\uc11c \ubc1c\uc0dd\ud55c <code>401<\/code> \uc5d0\ub7ec\ub294 \ud1a0\ud070 \uac31\uc2e0\uc744 \uc2dc\ub3c4\ud558\uc9c0 \uc54a\uace0 \uc989\uc2dc \uc5d0\ub7ec\ub97c \ubc18\ud658\ud574\uc57c \ubb34\ud55c \ub8e8\ud504\uc5d0 \ube60\uc9c0\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li>\ud1a0\ud070 \uac31\uc2e0 \uc694\uccad\uacfc, \uac31\uc2e0 \ub4a4 \uc6d0\ub798 \uc694\uccad\uc744 \ub2e4\uc2dc \ubcf4\ub0b4\ub294 \uc77c\uc740 <strong>\uc778\ud130\uc149\ud130\uac00 \uc5c6\ub294 \ubcc4\ub3c4\uc758 <code>Dio<\/code> \uc778\uc2a4\ud134\uc2a4(<code>plainDio<\/code>)<\/strong> \ub85c \ubcf4\ub0c5\ub2c8\ub2e4. \uc778\ud130\uc149\ud130\uac00 \uc788\ub294 <code>dio<\/code>\ub85c \ub2e4\uc2dc \ubcf4\ub0b4\uba74, \uadf8 \uc7ac\uc694\uccad\uc774 \uc2e4\ud328\ud588\uc744 \ub54c \uc5d0\ub7ec\uac00 \uc544\uc9c1 \ucc98\ub9ac \uc911\uc778 \uac19\uc740 \ud050\uc5d0 \ub4e4\uc5b4\uac00 \uc601\uc6d0\ud788 \uae30\ub2e4\ub9ac\ub294 \uad50\ucc29 \uc0c1\ud0dc(\ub370\ub4dc\ub77d)\uc5d0 \ube60\uc9c8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><code>QueuedInterceptorsWrapper<\/code>\ub97c \uc0ac\uc6a9\ud574 \uc5ec\ub7ec \uc694\uccad\uc774 \ub3d9\uc2dc\uc5d0 <code>401<\/code>\uc744 \ubc1b\uc558\uc744 \ub54c \uc21c\ucc28\uc801\uc73c\ub85c \ucc98\ub9ac\ud558\ub418, \uc55e\uc120 \uc694\uccad\uc774 \uc774\ubbf8 \ud1a0\ud070\uc744 \uac31\uc2e0\ud588\ub2e4\uba74 \ub4a4\ub530\ub77c\uc628 \uc694\uccad\uc740 <code>\/refresh<\/code>\ub97c \ub2e4\uc2dc \ud638\ucd9c\ud558\uc9c0 \uc54a\uace0 \uc0c8 \ud1a0\ud070\uc73c\ub85c \uc7ac\uc2dc\ub3c4\ub9cc \uc218\ud589\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li>\uc800\uc7a5\ub41c \ud1a0\ud070\uc744 \uc9c0\uc6b0\ub294(\ub85c\uadf8\uc544\uc6c3\uc2dc\ud0a4\ub294) \uacbd\uc6b0\ub294 <strong>\uc11c\ubc84\uac00 \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc744 \uac70\ubd80(<code>401<\/code>)\ud588\uc744 \ub54c<\/strong>\ub85c \ud55c\uc815\ud569\ub2c8\ub2e4. \uc640\uc774\ud30c\uc774\uac00 \uc7a0\uae50 \ub04a\uacbc\uac70\ub098 NAS\uac00 \uc7ac\uc2dc\uc791 \uc911\uc77c \ub54c\uae4c\uc9c0 \ub85c\uadf8\uc544\uc6c3\uc2dc\ud0a4\uba74 \uc548 \ub418\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dart\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import 'package:dio\/dio.dart';\nimport 'package:flutter_riverpod\/flutter_riverpod.dart';\nimport '..\/storage\/token_storage.dart';\n\n\/\/ \uc2e4\ud589 \uc2dc --dart-define=API_BASE_URL=https:\/\/api.myfamily.com \uc73c\ub85c \uc8fc\uc785\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\/\/ \uae30\ubcf8\uac12\uc740 \ub85c\uceec \ud14c\uc2a4\ud2b8\uc6a9 localhost:3000 (Android \uc5d0\ubbac\ub808\uc774\ud130\ub294 10.0.2.2:3000 \uc0ac\uc6a9)\nconst String kApiBaseUrl = String.fromEnvironment(\n  'API_BASE_URL',\n  defaultValue: 'http:\/\/localhost:3000',\n);\n\nfinal dioProvider = Provider&lt;Dio>((ref) {\n  final tokenStorage = ref.watch(tokenStorageProvider);\n  return createDioClient(tokenStorage);\n});\n\nDio createDioClient(TokenStorage tokenStorage) {\n  final baseOptions = BaseOptions(\n    baseUrl: kApiBaseUrl,\n    connectTimeout: const Duration(seconds: 10),\n    receiveTimeout: const Duration(seconds: 15),\n    headers: {'Content-Type': 'application\/json'},\n  );\n\n  final dio = Dio(baseOptions);\n  \/\/ \uc778\ud130\uc149\ud130\uac00 \uc5c6\ub294 \ubcc4\ub3c4 Dio: \ud1a0\ud070 \uac31\uc2e0 \uc694\uccad\uacfc, \uac31\uc2e0 \ud6c4 \uc6d0\ub798 \uc694\uccad \uc7ac\uc2dc\ub3c4\uc5d0\ub9cc \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\n  final plainDio = Dio(baseOptions);\n\n  dio.interceptors.add(\n    QueuedInterceptorsWrapper(\n      onRequest: (options, handler) async {\n        final accessToken = await tokenStorage.getAccessToken();\n        if (accessToken != null &amp;&amp; accessToken.isNotEmpty) {\n          options.headers['Authorization'] = 'Bearer $accessToken';\n        }\n        handler.next(options);\n      },\n      onError: (DioException err, ErrorInterceptorHandler handler) async {\n        final statusCode = err.response?.statusCode;\n        final requestPath = err.requestOptions.path;\n\n        \/\/ 401 \uc5d0\ub7ec\uac00 \uc544\ub2c8\uac70\ub098, \uc778\uc99d \uad00\ub828 \uc5d4\ub4dc\ud3ec\uc778\ud2b8(\/auth\/*)\uc5d0\uc11c \ub09c \uc5d0\ub7ec\uba74 \uadf8\ub300\ub85c \ud1b5\uacfc\n        if (statusCode != 401 || requestPath.contains('\/api\/v1\/auth\/')) {\n          return handler.next(err);\n        }\n\n        final storedAccess = await tokenStorage.getAccessToken();\n        final requestAuthHeader = err.requestOptions.headers['Authorization'];\n\n        \/\/ [\ub3d9\uc2dc\uc131 \ubc29\uc5b4] \ud050\uc5d0\uc11c \uae30\ub2e4\ub9ac\ub294 \ub3d9\uc548 \uc55e\uc120 \uc694\uccad\uc774 \uc774\ubbf8 \ud1a0\ud070\uc744 \uac31\uc2e0\ud588\ub2e4\uba74,\n        \/\/ \ub2e4\uc2dc \/refresh\ub97c \ud638\ucd9c\ud558\uc9c0 \uc54a\uace0 \uc0c8\ub85c \uc800\uc7a5\ub41c \ud1a0\ud070\uc73c\ub85c \uc6d0\ub798 \uc694\uccad\ub9cc \uc7ac\uc2dc\ub3c4\ud569\ub2c8\ub2e4.\n        if (storedAccess != null &amp;&amp;\n            requestAuthHeader != null &amp;&amp;\n            requestAuthHeader != 'Bearer $storedAccess') {\n          err.requestOptions.headers['Authorization'] = 'Bearer $storedAccess';\n          try {\n            return handler.resolve(await plainDio.fetch(err.requestOptions));\n          } on DioException catch (retryErr) {\n            return handler.next(retryErr);\n          }\n        }\n\n        \/\/ \uc800\uc7a5\ub41c Refresh Token \ud655\uc778\n        final refreshToken = await tokenStorage.getRefreshToken();\n        if (refreshToken == null || refreshToken.isEmpty) {\n          await tokenStorage.clearTokens();\n          return handler.next(err);\n        }\n\n        \/\/ 6\ud3b8\uc758 Refresh Token Rotation API \ud638\ucd9c\n        String newAccess;\n        try {\n          final refreshRes = await plainDio.post(\n            '\/api\/v1\/auth\/refresh',\n            data: {'refreshToken': refreshToken},\n          );\n          newAccess = refreshRes.data['accessToken'] as String;\n          final newRefresh = refreshRes.data['refreshToken'] as String;\n\n          \/\/ \uc0c8\ub85c \ubc1c\uae09\ubc1b\uc740 \ub450 \ud1a0\ud070\uc744 Secure Storage\uc5d0 \ub36e\uc5b4\uc4f0\uae30\n          await tokenStorage.saveTokens(\n            accessToken: newAccess,\n            refreshToken: newRefresh,\n          );\n        } on DioException catch (refreshErr) {\n          \/\/ \uc11c\ubc84\uac00 \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uc744 \uac70\ubd80(401)\ud588\uc744 \ub54c\ub9cc \uc800\uc7a5\uc18c\ub97c \ube44\uc6c1\ub2c8\ub2e4.\n          \/\/ \ub124\ud2b8\uc6cc\ud06c \uc624\ub958\ub098 \uc11c\ubc84 \uc7a5\uc560\uc77c \ub54c\ub294 \ud1a0\ud070\uc744 \ub0a8\uaca8 \ub450\uc5b4 \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud560 \uc218 \uc788\uac8c \ud569\ub2c8\ub2e4.\n          if (refreshErr.response?.statusCode == 401) {\n            await tokenStorage.clearTokens();\n          }\n          return handler.next(err);\n        } catch (_) {\n          \/\/ \uc751\ub2f5 \ud615\uc2dd\uc774 \uc608\uc0c1\uacfc \ub2e4\ub978 \uacbd\uc6b0 \ub4f1\n          return handler.next(err);\n        }\n\n        \/\/ \uc2e4\ud328\ud588\ub358 \uc6d0\ub798 \uc694\uccad\uc758 \ud5e4\ub354\ub97c \uc0c8 \ud1a0\ud070\uc73c\ub85c \uad50\uccb4\ud558\uc5ec \uc7ac\uc694\uccad\n        err.requestOptions.headers['Authorization'] = 'Bearer $newAccess';\n        try {\n          return handler.resolve(await plainDio.fetch(err.requestOptions));\n        } on DioException catch (retryErr) {\n          return handler.next(retryErr);\n        }\n      },\n    ),\n  );\n\n  return dio;\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. \uc778\uc99d \uc0c1\ud0dc \uad00\ub9ac \uad6c\ud604 (<code>lib\/features\/auth\/auth_provider.dart<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Riverpod\uc758 <code>AsyncNotifier<\/code>\ub97c \uc0ac\uc6a9\ud574 \uc571 \uc2dc\uc791 \uc2dc \uc790\ub3d9 \ub85c\uadf8\uc778 \uc5ec\ubd80\ub97c \ud310\ubcc4\ud558\uace0, \ub85c\uadf8\uc778\/\ud68c\uc6d0\uac00\uc785\/\ub85c\uadf8\uc544\uc6c3 \ub3d9\uc791\uc744 \uc218\ud589\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774 Provider\uc758 \uc0c1\ud0dc\ub294 <strong>&#8220;\uc9c0\uae08 \ub85c\uadf8\uc778\ub41c \uc0ac\uc6a9\uc790(<code>User?<\/code>)&#8221;<\/strong> \ud558\ub098\ub9cc \ub098\ud0c0\ub0c5\ub2c8\ub2e4. \ub85c\uadf8\uc778 \uc694\uccad\uc774 \uc2e4\ud328\ud588\uc744 \ub54c\ub294 \uc0c1\ud0dc\ub97c \ubc14\uafb8\uc9c0 \uc54a\uace0 \uc608\uc678\ub97c \ub358\uc838\uc11c, \ub85c\uadf8\uc778 \ud654\uba74\uc774 \uc9c1\uc811 \uc5d0\ub7ec \uba54\uc2dc\uc9c0\ub97c \ubcf4\uc5ec \uc8fc\ub3c4\ub85d \ud569\ub2c8\ub2e4. (\ub85c\uadf8\uc778 \ub3c4\uc911\uc5d0 \uc0c1\ud0dc\ub97c \ub85c\ub529\uc774\ub098 \uc5d0\ub7ec\ub85c \ubc14\uafb8\uba74 <code>main.dart<\/code>\uac00 \ud654\uba74\uc744 \ud1b5\uc9f8\ub85c \uac08\uc544 \ub07c\uc6cc\uc11c \uc785\ub825\ud558\ub358 \ub0b4\uc6a9\uc774 \uc0ac\ub77c\uc9c0\uace0 \uc5d0\ub7ec \uba54\uc2dc\uc9c0\ub3c4 \ub193\uce58\uac8c \ub429\ub2c8\ub2e4.)<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dart\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import 'dart:io' show Platform;\nimport 'package:dio\/dio.dart';\nimport 'package:flutter\/foundation.dart' show kIsWeb;\nimport 'package:flutter_riverpod\/flutter_riverpod.dart';\nimport '..\/..\/core\/network\/dio_client.dart';\nimport '..\/..\/core\/storage\/token_storage.dart';\nimport 'user_model.dart';\n\nfinal authProvider = AsyncNotifierProvider&lt;AuthNotifier, User?>(\n  AuthNotifier.new,\n);\n\nclass AuthNotifier extends AsyncNotifier&lt;User?> {\n  @override\n  Future&lt;User?> build() async {\n    final tokenStorage = ref.read(tokenStorageProvider);\n    final accessToken = await tokenStorage.getAccessToken();\n\n    \/\/ \uc800\uc7a5\ub41c \ud1a0\ud070\uc774 \uc5c6\uc73c\uba74 \ube44\ub85c\uadf8\uc778 \uc0c1\ud0dc(null)\ub85c \uc2dc\uc791\n    if (accessToken == null || accessToken.isEmpty) {\n      return null;\n    }\n\n    \/\/ \ud1a0\ud070\uc774 \uc788\uc73c\uba74 \/api\/v1\/users\/me\ub97c \ud638\ucd9c\ud574 \uc720\ud6a8\uc131 \uac80\uc99d \ubc0f \ub0b4 \uc815\ubcf4 \ub85c\ub4dc\n    \/\/ (Access Token\uc774 \ub9cc\ub8cc\ub418\uc5c8\ub354\ub77c\ub3c4 Dio \uc778\ud130\uc149\ud130\uac00 \uc790\ub3d9\uc73c\ub85c \uac31\uc2e0 \ud6c4 \uac00\uc838\uc635\ub2c8\ub2e4)\n    try {\n      final dio = ref.read(dioProvider);\n      final res = await dio.get('\/api\/v1\/users\/me');\n      final userMap = res.data['user'] as Map&lt;String, dynamic>;\n      return User.fromJson(userMap);\n    } on DioException catch (_) {\n      \/\/ \uc138\uc158\uc774 \ub9cc\ub8cc\ub41c \uacbd\uc6b0(401)\uc5d0\ub294 \uc778\ud130\uc149\ud130\uac00 \uc774\ubbf8 \ud1a0\ud070\uc744 \uc9c0\uc6e0\uc2b5\ub2c8\ub2e4.\n      \/\/ \ub124\ud2b8\uc6cc\ud06c \uc624\ub958\ub098 \uc11c\ubc84 \uc810\uac80\uc774\uba74 \ud1a0\ud070\uc740 \uadf8\ub300\ub85c \ub0a8\uaca8 \ub450\uace0 \ub85c\uadf8\uc778 \ud654\uba74\uc744 \ubcf4\uc5ec \uc90d\ub2c8\ub2e4.\n      \/\/ (\uc11c\ubc84\uac00 \ubcf5\uad6c\ub41c \ub4a4 \uc571\uc744 \ub2e4\uc2dc \ucf1c\uba74 \uc790\ub3d9 \ub85c\uadf8\uc778\ub429\ub2c8\ub2e4.)\n      return null;\n    }\n  }\n\n  \/\/ \ud604\uc7ac \uc2e4\ud589 \uc911\uc778 OS \ud50c\ub7ab\ud3fc \ubb38\uc790\uc5f4 \ubc18\ud658\n  String _currentPlatform() {\n    if (kIsWeb) return 'web';\n    if (Platform.isAndroid) return 'android';\n    if (Platform.isIOS) return 'ios';\n    if (Platform.isMacOS) return 'macos';\n    if (Platform.isWindows) return 'windows';\n    return 'unknown';\n  }\n\n  \/\/ \ub85c\uadf8\uc778 \uc694\uccad (\uc2e4\ud328\ud558\uba74 DioException\uc774 \uadf8\ub300\ub85c \ub358\uc838\uc9d1\ub2c8\ub2e4)\n  Future&lt;void> login({\n    required String loginId,\n    required String password,\n  }) async {\n    final res = await ref.read(dioProvider).post(\n      '\/api\/v1\/auth\/login',\n      data: {\n        'loginId': loginId.trim(),\n        'password': password,\n        'platform': _currentPlatform(),\n        'deviceName': 'DingDong-${_currentPlatform()}',\n      },\n    );\n    await _startSession(res.data as Map&lt;String, dynamic>);\n  }\n\n  \/\/ \ucd08\ub300\ucf54\ub4dc \uae30\ubc18 \ud68c\uc6d0\uac00\uc785 \uc694\uccad (\uc2e4\ud328\ud558\uba74 DioException\uc774 \uadf8\ub300\ub85c \ub358\uc838\uc9d1\ub2c8\ub2e4)\n  Future&lt;void> register({\n    required String inviteCode,\n    required String loginId,\n    required String password,\n    required String displayName,\n    String? phone,\n  }) async {\n    final res = await ref.read(dioProvider).post(\n      '\/api\/v1\/auth\/register',\n      data: {\n        'inviteCode': inviteCode.trim(),\n        'loginId': loginId.trim(),\n        'password': password,\n        'displayName': displayName.trim(),\n        if (phone != null &amp;&amp; phone.trim().isNotEmpty) 'phone': phone.trim(),\n        'platform': _currentPlatform(),\n        'deviceName': 'DingDong-${_currentPlatform()}',\n      },\n    );\n    await _startSession(res.data as Map&lt;String, dynamic>);\n  }\n\n  \/\/ \ub85c\uadf8\uc778\/\uac00\uc785 \uc131\uacf5 \uc2dc \uacf5\ud1b5 \ucc98\ub9ac: \ud1a0\ud070 \uc800\uc7a5 \ud6c4 \"\ub85c\uadf8\uc778\ub41c \uc0ac\uc6a9\uc790\"\ub85c \uc0c1\ud0dc \uc804\ud658\n  Future&lt;void> _startSession(Map&lt;String, dynamic> json) async {\n    final authRes = AuthResponse.fromJson(json);\n    await ref.read(tokenStorageProvider).saveTokens(\n      accessToken: authRes.accessToken,\n      refreshToken: authRes.refreshToken,\n    );\n    state = AsyncData(authRes.user);\n  }\n\n  \/\/ \ub85c\uadf8\uc544\uc6c3 (\ub85c\uceec \ud1a0\ud070 \uc0ad\uc81c)\n  Future&lt;void> logout() async {\n    final tokenStorage = ref.read(tokenStorageProvider);\n    await tokenStorage.clearTokens();\n    state = const AsyncData(null);\n  }\n}\n\n\/\/ DioException\uc5d0\uc11c \uc11c\ubc84\uac00 \ub0b4\ub824\uc900 {\"error\": \"\uba54\uc2dc\uc9c0\"}\ub97c \ucd94\ucd9c\ud558\ub294 \ud5ec\ud37c \ud568\uc218\nString extractErrorMessage(Object error) {\n  if (error is DioException) {\n    final data = error.response?.data;\n    if (data is Map&lt;String, dynamic> &amp;&amp; data['error'] is String) {\n      return data['error'] as String;\n    }\n    return '\uc11c\ubc84\uc640 \ud1b5\uc2e0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4 (${error.message})';\n  }\n  return error.toString();\n}<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6. \ub85c\uadf8\uc778\/\ud68c\uc6d0\uac00\uc785 \ud654\uba74 \ubc0f \uba54\uc778 \uc5d4\ud2b8\ub9ac \uad6c\ud604<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">6.1 \ub85c\uadf8\uc778 &amp; \ud68c\uc6d0\uac00\uc785 \ud3fc \ud654\uba74 (<code>lib\/features\/auth\/login_screen.dart<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ud558\ub098\uc758 \ud654\uba74\uc5d0\uc11c \ub85c\uadf8\uc778 \ubaa8\ub4dc\uc640 \ud68c\uc6d0\uac00\uc785 \ubaa8\ub4dc\ub97c \uc804\ud658\ud558\uba70 \ud14c\uc2a4\ud2b8\ud560 \uc218 \uc788\ub3c4\ub85d \uad6c\uc131\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dart\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import 'package:flutter\/material.dart';\nimport 'package:flutter_riverpod\/flutter_riverpod.dart';\nimport 'auth_provider.dart';\n\nclass LoginScreen extends ConsumerStatefulWidget {\n  const LoginScreen({super.key});\n\n  @override\n  ConsumerState&lt;LoginScreen> createState() => _LoginScreenState();\n}\n\nclass _LoginScreenState extends ConsumerState&lt;LoginScreen> {\n  bool _isRegisterMode = false;\n  bool _isLoading = false; \/\/ \uc694\uccad \uc9c4\ud589 \uc911 \uc5ec\ubd80 (\ubc84\ud2bc \ube44\ud65c\uc131\ud654\/\ub85c\ub529 \ud45c\uc2dc\uc6a9)\n\n  final _inviteCodeCtrl = TextEditingController();\n  final _loginIdCtrl = TextEditingController();\n  final _passwordCtrl = TextEditingController();\n  final _displayNameCtrl = TextEditingController();\n  final _phoneCtrl = TextEditingController();\n\n  @override\n  void dispose() {\n    _inviteCodeCtrl.dispose();\n    _loginIdCtrl.dispose();\n    _passwordCtrl.dispose();\n    _displayNameCtrl.dispose();\n    _phoneCtrl.dispose();\n    super.dispose();\n  }\n\n  Future&lt;void> _submit() async {\n    final notifier = ref.read(authProvider.notifier);\n    setState(() => _isLoading = true);\n\n    try {\n      if (_isRegisterMode) {\n        await notifier.register(\n          inviteCode: _inviteCodeCtrl.text,\n          loginId: _loginIdCtrl.text,\n          password: _passwordCtrl.text,\n          displayName: _displayNameCtrl.text,\n          phone: _phoneCtrl.text,\n        );\n      } else {\n        await notifier.login(\n          loginId: _loginIdCtrl.text,\n          password: _passwordCtrl.text,\n        );\n      }\n      \/\/ \uc131\uacf5\ud558\uba74 authProvider\uac00 User\ub85c \ubc14\ub00c\uace0, main.dart\uac00 \ud648 \ud654\uba74\uc73c\ub85c \uc804\ud658\ud569\ub2c8\ub2e4.\n    } catch (e) {\n      if (!mounted) return;\n      ScaffoldMessenger.of(context).showSnackBar(\n        SnackBar(\n          content: Text(extractErrorMessage(e)),\n          backgroundColor: Colors.redAccent,\n        ),\n      );\n    } finally {\n      \/\/ \uc131\uacf5 \uc2dc\uc5d0\ub294 \uc774 \ud654\uba74\uc774 \uc774\ubbf8 \uc0ac\ub77c\uc84c\uc744 \uc218 \uc788\uc73c\ubbc0\ub85c \ud655\uc778 \ud6c4 \uac31\uc2e0\n      if (mounted) setState(() => _isLoading = false);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isLoading = _isLoading;\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(_isRegisterMode ? '\ub529\ub3d9 \ud68c\uc6d0\uac00\uc785' : '\ub529\ub3d9 \ub85c\uadf8\uc778'),\n      ),\n      body: Center(\n        child: ConstrainedBox(\n          constraints: const BoxConstraints(maxWidth: 400),\n          child: SingleChildScrollView(\n            padding: const EdgeInsets.all(24),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.stretch,\n              children: [\n                if (_isRegisterMode) ...[\n                  TextField(\n                    controller: _inviteCodeCtrl,\n                    decoration: const InputDecoration(\n                      labelText: '\ucd08\ub300\ucf54\ub4dc',\n                      hintText: '\uc608: DING26',\n                      border: OutlineInputBorder(),\n                    ),\n                  ),\n                  const SizedBox(height: 12),\n                  TextField(\n                    controller: _displayNameCtrl,\n                    decoration: const InputDecoration(\n                      labelText: '\uc774\ub984 (\ub2c9\ub124\uc784)',\n                      border: OutlineInputBorder(),\n                    ),\n                  ),\n                  const SizedBox(height: 12),\n                  TextField(\n                    controller: _phoneCtrl,\n                    decoration: const InputDecoration(\n                      labelText: '\uc804\ud654\ubc88\ud638 (\uc120\ud0dd, E.164 \ud615\uc2dd)',\n                      hintText: '+821012345678',\n                      border: OutlineInputBorder(),\n                    ),\n                  ),\n                  const SizedBox(height: 12),\n                ],\n                TextField(\n                  controller: _loginIdCtrl,\n                  decoration: const InputDecoration(\n                    labelText: '\ub85c\uadf8\uc778 \uc544\uc774\ub514',\n                    border: OutlineInputBorder(),\n                  ),\n                ),\n                const SizedBox(height: 12),\n                TextField(\n                  controller: _passwordCtrl,\n                  obscureText: true,\n                  decoration: const InputDecoration(\n                    labelText: '\ube44\ubc00\ubc88\ud638 (\ucd5c\uc18c 8\uc790)',\n                    border: OutlineInputBorder(),\n                  ),\n                ),\n                const SizedBox(height: 24),\n                FilledButton(\n                  onPressed: isLoading ? null : _submit,\n                  style: FilledButton.styleFrom(\n                    padding: const EdgeInsets.symmetric(vertical: 16),\n                  ),\n                  child: isLoading\n                      ? const SizedBox(\n                          height: 20,\n                          width: 20,\n                          child: CircularProgressIndicator(strokeWidth: 2),\n                        )\n                      : Text(_isRegisterMode ? '\uac00\uc785\ud558\uae30' : '\ub85c\uadf8\uc778'),\n                ),\n                const SizedBox(height: 12),\n                TextButton(\n                  onPressed: isLoading\n                      ? null\n                      : () {\n                          setState(() {\n                            _isRegisterMode = !_isRegisterMode;\n                          });\n                        },\n                  child: Text(\n                    _isRegisterMode\n                        ? '\uc774\ubbf8 \uacc4\uc815\uc774 \uc788\uc73c\uc2e0\uac00\uc694? \ub85c\uadf8\uc778\uc73c\ub85c \uc804\ud658'\n                        : '\ucd08\ub300\ucf54\ub4dc\uac00 \uc788\uc73c\uc2e0\uac00\uc694? \ud68c\uc6d0\uac00\uc785\uc73c\ub85c \uc804\ud658',\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">6.2 \uc571 \uc9c4\uc785\uc810 \ubc0f \uc790\ub3d9 \ub85c\uadf8\uc778 \ubd84\uae30 (<code>lib\/main.dart<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>authProvider<\/code>\uc758 \uc0c1\ud0dc\uc5d0 \ub530\ub77c \ub85c\ub529 \ud654\uba74, \ub85c\uadf8\uc778 \ud654\uba74, \ub85c\uadf8\uc778 \uc644\ub8cc \ud648 \ud654\uba74\uc744 \ubd84\uae30\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dart\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import 'package:flutter\/material.dart';\nimport 'package:flutter_riverpod\/flutter_riverpod.dart';\nimport 'features\/auth\/auth_provider.dart';\nimport 'features\/auth\/login_screen.dart';\nimport 'features\/auth\/user_model.dart';\n\nvoid main() {\n  runApp(const ProviderScope(child: DingDongApp()));\n}\n\nclass DingDongApp extends ConsumerWidget {\n  const DingDongApp({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final authState = ref.watch(authProvider);\n\n    return MaterialApp(\n      title: '\ub529\ub3d9 \uba54\uc2e0\uc800',\n      debugShowCheckedModeBanner: false,\n      theme: ThemeData(\n        colorSchemeSeed: Colors.indigo,\n        useMaterial3: true,\n      ),\n      home: authState.when(\n        loading: () => const Scaffold(\n          body: Center(child: CircularProgressIndicator()),\n        ),\n        error: (_, __) => const LoginScreen(),\n        data: (User? user) {\n          if (user == null) {\n            return const LoginScreen();\n          }\n          return HomePlaceholderScreen(user: user);\n        },\n      ),\n    );\n  }\n}\n\nclass HomePlaceholderScreen extends ConsumerWidget {\n  final User user;\n  const HomePlaceholderScreen({super.key, required this.user});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('\ub529\ub3d9 \ud648'),\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.logout),\n            tooltip: '\ub85c\uadf8\uc544\uc6c3',\n            onPressed: () => ref.read(authProvider.notifier).logout(),\n          ),\n        ],\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Text(\n              '\ud658\uc601\ud569\ub2c8\ub2e4, ${user.displayName}\ub2d8!',\n              style: Theme.of(context).textTheme.headlineSmall,\n            ),\n            const SizedBox(height: 8),\n            Text('User ID: ${user.id}'),\n            Text('Role: ${user.role} | Status: ${user.status}'),\n          ],\n        ),\n      ),\n    );\n  }\n}<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">7. \uc2e4\ud589 \ubc0f \uc5f0\ub3d9 \ud14c\uc2a4\ud2b8<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">6\ud3b8\uc5d0\uc11c \ub9cc\ub4e0 Go \ubc31\uc5d4\ub4dc \uc11c\ubc84\uac00 \uc2e4\ud589 \uc911\uc778 \uc0c1\ud0dc\uc5d0\uc11c Flutter \uc571\uc744 \uad6c\ub3d9\ud569\ub2c8\ub2e4. (\ub9e5\uc5d0\uc11c \uc9c1\uc811 \ub744\uc6b0\ub294 \ubc29\ubc95\uc740 6\ud3b8 6.2\uc808, NAS\uc5d0\uc11c\ub294 <code>docker compose up -d --build<\/code>\uc785\ub2c8\ub2e4.)<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">7.1 \ub85c\uceec Go \uc11c\ubc84\uc640 \uc5f0\uacb0\ud560 \ub54c (iOS \uc2dc\ubbac\ub808\uc774\ud130 \/ macOS \ub370\uc2a4\ud06c\ud1b1)<\/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=\"\">flutter run<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><em>(\ucc38\uace0: Android \uc5d0\ubbac\ub808\uc774\ud130\uc5d0\uc11c \ub0b4 \ub9e5\uc758 <code>localhost:3000<\/code>\uc5d0 \uc811\uc18d\ud560 \ub54c\ub294 <code>flutter run --dart-define=API_BASE_URL=http:\/\/10.0.2.2:3000<\/code>\uc73c\ub85c \uc2e4\ud589\ud574\uc57c \ud569\ub2c8\ub2e4.)<\/em><\/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>Android\ub294 \uae30\ubcf8\uc801\uc73c\ub85c <code>http:\/\/<\/code>(\uc554\ud638\ud654\ub418\uc9c0 \uc54a\uc740) \ud1b5\uc2e0\uc744 \ub9c9\uc2b5\ub2c8\ub2e4.<\/strong> \uc5d0\ubbac\ub808\uc774\ud130\uc5d0\uc11c \uc704\ucc98\ub7fc <code>http:\/\/10.0.2.2:3000<\/code>\uc73c\ub85c \ud14c\uc2a4\ud2b8\ud558\ub824\uba74 <code>android\/app\/src\/debug\/AndroidManifest.xml<\/code>\uc758 <code>&lt;manifest&gt;<\/code> \uc548\uc5d0 \uc544\ub798 \ud55c \uc904\uc744 \ucd94\uac00\ud558\uc138\uc694. <code>debug<\/code> \ud3f4\ub354\uc758 \ud30c\uc77c\uc774\ub77c \uac1c\ubc1c \uc911\uc5d0\ub9cc \uc801\uc6a9\ub418\uace0, \ubc30\ud3ec\uc6a9 \ube4c\ub4dc\uc5d0\ub294 \ub4e4\uc5b4\uac00\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. NAS \ub3c4\uba54\uc778(<code>https:\/\/<\/code>)\uc73c\ub85c \uc811\uc18d\ud560 \ub54c\ub294 \ud544\uc694 \uc5c6\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;application android:usesCleartextTraffic=\"true\"\/><\/pre>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">7.2 \uc720\uadf8\ub9b0 NAS\uc5d0 \ubc30\ud3ec\ub41c \ub3c4\uba54\uc778\uacfc \uc5f0\uacb0\ud560 \ub54c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">3\ud3b8\uacfc 4\ud3b8\uc5d0\uc11c Caddy\ub85c \uc5f0\uacb0\ud55c \ub3c4\uba54\uc778 \uc8fc\uc18c(\ubcf8\uc778 \ub3c4\uba54\uc778\uc73c\ub85c \ubc14\uafd4 \uc785\ub825)\ub97c \uc8fc\uc785\ud558\uc5ec \uc2e4\ud589\ud569\ub2c8\ub2e4. <strong>\uc2e4\uc81c \uc2a4\ub9c8\ud2b8\ud3f0<\/strong>\uc740 <code>localhost<\/code>\uac00 \ub9e5\uc774 \uc544\ub2c8\ub77c \ud3f0 \uc790\uc2e0\uc744 \uac00\ub9ac\ud0a4\ubbc0\ub85c, \uc774 \ubc29\uc2dd\uc73c\ub85c \uc2e4\ud589\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=\"\">flutter run --dart-define=API_BASE_URL=https:\/\/api.myfamily.com<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\ud655\uc778\ud574 \ubcfc \uccb4\ud06c\ub9ac\uc2a4\ud2b8<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ub85c\uadf8\uc778 \ud14c\uc2a4\ud2b8:<\/strong> 6\ud3b8\uc5d0\uc11c \uc0dd\uc131\ud55c \uacc4\uc815(<code>daddy<\/code> \/ <code>my_secure_password_123!<\/code>)\uc73c\ub85c \ub85c\uadf8\uc778\ud588\uc744 \ub54c \ud648 \ud654\uba74\uc5d0 \uc774\ub984\uacfc UUID\uac00 \uc815\uc0c1 \ucd9c\ub825\ub418\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc790\ub3d9 \ub85c\uadf8\uc778 \ud14c\uc2a4\ud2b8:<\/strong> \uc571\uc744 \uc644\uc804\ud788 \uc885\ub8cc\ud588\ub2e4\uac00 \ub2e4\uc2dc \uc2e4\ud589\ud588\uc744 \ub54c \ub85c\uadf8\uc778 \ud654\uba74\uc744 \uac70\uce58\uc9c0 \uc54a\uace0 \uace7\ubc14\ub85c \ud648 \ud654\uba74\uc73c\ub85c \uc9c4\uc785\ud558\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc5d0\ub7ec \uba54\uc2dc\uc9c0 \ud45c\uc2dc:<\/strong> \ud2c0\ub9b0 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud588\uc744 \ub54c \ud558\ub2e8 \uc2a4\ub0b5\ubc14\uc5d0 \uc11c\ubc84\uac00 \ub0b4\ub824\uc900 <code>\"\uc544\uc774\ub514 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4\"<\/code> \ubb38\uad6c\uac00 \ud45c\uc2dc\ub418\uace0, \uc785\ub825\ud558\ub358 \uc544\uc774\ub514\uac00 \uc9c0\uc6cc\uc9c0\uc9c0 \uc54a\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">8. \uc774\ubc88 \ud3b8 \uc694\uc57d \ubc0f \ub2e4\uc74c \ub2e8\uacc4<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\ubc88 7\ud3b8\uc5d0\uc11c\ub294 \ubc31\uc5d4\ub4dc \uc778\uc99d API\uc640 \ub9de\ubb3c\ub824 \ub3cc\uc544\uac00\ub294 Flutter \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d \uad6c\uc870\ub97c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\uc554\ud638\ud654 \uc800\uc7a5\uc18c \uc5f0\ub3d9:<\/strong> <code>flutter_secure_storage<\/code>\ub97c \ud1b5\ud574 \ud1a0\ud070\uc744 OS \ubcf4\uc548 \uc601\uc5ed\uc5d0 \uc800\uc7a5\ud558\uace0 \uc571 \uc7ac\uc2e4\ud589 \uc2dc \uc790\ub3d9 \ub85c\uadf8\uc778\uc744 \uc218\ud589\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc21c\ucc28 \ud1a0\ud070 \uac31\uc2e0:<\/strong> <code>QueuedInterceptorsWrapper<\/code>\ub97c \uc0ac\uc6a9\ud574 Access Token \ub9cc\ub8cc \uc2dc Refresh Token Rotation \uaddc\uce59\uc744 \uae68\ub728\ub9ac\uc9c0 \uc54a\uace0 \ubc31\uadf8\ub77c\uc6b4\ub4dc\uc5d0\uc11c \ud1a0\ud070\uc744 \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 (\ucd94\ud6c4 \ubcf4\uc644 \uc0ac\ud56d)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\uc11c\ubc84 \uce21 \ub85c\uadf8\uc544\uc6c3 API \ud638\ucd9c:<\/strong> \ud604\uc7ac <code>logout()<\/code>\uc740 \uc2a4\ub9c8\ud2b8\ud3f0\uc5d0 \uc800\uc7a5\ub41c \ud1a0\ud070\ub9cc \uc9c0\uc6c1\ub2c8\ub2e4. \uc644\ubcbd\ud55c \uc138\uc158 \uc885\ub8cc\ub97c \uc704\ud574\uc11c\ub294 \uc11c\ubc84\uc5d0\uc11c \ud574\ub2f9 \uae30\uae30(<code>devices<\/code> \ud589)\ub3c4 \uc0ad\uc81c\ud574\uc57c \ud558\ub294\ub370, 6\ud3b8\uc5d0\ub294 <code>DeleteDevice<\/code> \ucffc\ub9ac\ub9cc \uc788\uace0 \uc774\ub97c \ud638\ucd9c\ud558\ub294 API \uacbd\ub85c(\uc608: <code>DELETE \/api\/v1\/devices\/{id}<\/code>)\ub294 \uc544\uc9c1 \uc5c6\uc2b5\ub2c8\ub2e4. \uc11c\ubc84\uc5d0 \uba3c\uc800 \ub9cc\ub4e4\uc5b4\uc57c \ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc138\uc158 \ub9cc\ub8cc \uc2dc \ud654\uba74 \uc804\ud658:<\/strong> \uc571\uc744 \uc4f0\ub294 \ub3c4\uc911 \ub9ac\ud504\ub808\uc2dc \ud1a0\ud070\uae4c\uc9c0 \uac70\ubd80\ub418\uba74 \uc778\ud130\uc149\ud130\uac00 \ud1a0\ud070\uc740 \uc9c0\uc6b0\uc9c0\ub9cc, <code>authProvider<\/code>\uc758 \uc0c1\ud0dc\ub294 \uadf8\ub300\ub85c\ub77c \ud648 \ud654\uba74\uc5d0 \uba38\ubb34\ub985\ub2c8\ub2e4. \uc778\ud130\uc149\ud130\uac00 \ub9cc\ub8cc\ub97c \uc54c\ub9ac\uba74 \ub85c\uadf8\uc544\uc6c3 \uc0c1\ud0dc\ub85c \ubc14\uafd4 \uc8fc\ub294 \uc5f0\uacb0\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc2e4\uc81c \uae30\uae30 \ubaa8\ub378\uba85 \uac00\uc838\uc624\uae30:<\/strong> \ud604\uc7ac\ub294 <code>deviceName<\/code>\uc744 <code>DingDong-ios<\/code>\ucc98\ub7fc \ub2e8\uc21c \ubb38\uc790\uc5f4\ub85c \ubcf4\ub0b4\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4 <code>device_info_plus<\/code> \ud328\ud0a4\uc9c0\ub97c \uc5f0\ub3d9\ud558\uba74 <code>\"iPhone 16 Pro\"<\/code> \uac19\uc740 \uc2e4\uc81c \uae30\uae30 \uc774\ub984\uc744 \uae30\ub85d\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong>\uc804\ud654\ubc88\ud638 \ud615\uc2dd \uac80\uc99d:<\/strong> \uc785\ub825\ub780\uc5d0\ub294 E.164 \ud615\uc2dd(<code>+821012345678<\/code>)\uc774\ub77c\uace0 \uc548\ub0b4\ud558\uc9c0\ub9cc \uc571\uc5d0\uc11c \uac80\uc0ac\ud558\uc9c0\ub294 \uc54a\uc2b5\ub2c8\ub2e4. \ud615\uc2dd\uc774 \uc81c\uac01\uac01\uc774\uba74 8\ud3b8\uc758 \uc5f0\ub77d\ucc98 \ud574\uc2dc \ub9e4\uce6d\uc5d0\uc11c \uac19\uc740 \ubc88\ud638\uac00 \ub2e4\ub978 \uac12\uc73c\ub85c \uacc4\uc0b0\ub418\ubbc0\ub85c, \uc800\uc7a5 \uc804\uc5d0 \ud615\uc2dd\uc744 \ud1b5\uc77c\ud574\uc57c \ud569\ub2c8\ub2e4.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\ub208\uc73c\ub85c \ub3d9\uc791\uc744 \ud655\uc778\ud588\uc73c\ub2c8, \ub2e4\uc74c [<strong>\uac1c\ubc1c\uc77c\uc9c0 #08<\/strong>]\uc5d0\uc11c\ub294 \ub2e4\uc2dc Go \ubc31\uc5d4\ub4dc\ub85c \ub3cc\uc544\uac00 <strong>\uc5f0\ub77d\ucc98 HMAC \ud574\uc2dc \ub9e4\uce6d\uc744 \ud1b5\ud55c \uce5c\uad6c \ucd94\uac00 API<\/strong>\uc640 <strong>\uc911\ubcf5 \uc0dd\uc131\uc774 \ubc29\uc9c0\ub418\ub294 1:1 \ubc0f \uadf8\ub8f9 \ucc44\ud305\ubc29 \uc0dd\uc131 API<\/strong>\ub97c \uad6c\ud604\ud558\uaca0\uc2b5\ub2c8\ub2e4.<\/p>\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>\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 7\ud3b8\uc785\ub2c8\ub2e4. \uc9c0\ub09c [6\ud3b8]\uc5d0\uc11c\ub294 Go \ubc31\uc5d4\ub4dc\uc5d0 \ucd08\ub300\ucf54\ub4dc \uae30\ubc18 \ud68c\uc6d0\uac00\uc785, Argon2id \ube44\ubc00\ubc88\ud638 \ud574\uc2f1, \ub2e4\uc911 \uae30\uae30\uc6a9 JWT Access\/Refresh \ud1a0\ud070 \ubc1c\uae09 API\ub97c \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.\ubc31\uc5d4\ub4dc \uc9c4\ub3c4\ub97c \ub354 \ub098\uac00\uae30 \uc804\uc5d0, \uc774\ubc88 7\ud3b8\uc5d0\uc11c\ub294 [2\ud3b8]\uc5d0\uc11c \uc900\ube44\ud574 \ub454 Flutter \uac1c\ubc1c \ud658\uacbd\uc744 \uc5f4\uc5b4 \ub85c\uadf8\uc778\/\ud68c\uc6d0\uac00\uc785 \ud654\uba74\uacfc \ud1a0\ud070 \uc790\ub3d9 \uac31\uc2e0 \ub124\ud2b8\uc6cc\ud06c \uacc4\uce35\uc744 \uad6c\ud604\ud558\uace0 \uc2e4\uc81c \uc11c\ubc84\uc640 \ud1b5\uc2e0\ud558\ub294 \uc804\uccb4 \ud750\ub984\uc744 \uc5f0\uacb0\ud569\ub2c8\ub2e4. &#8230; <a title=\"[\uac1c\ubc1c\uc77c\uc9c0 #07 | \ud074\ub77c\uc774\uc5b8\ud2b8] Flutter \uc571 \uccab \uc5f0\ub3d9: Secure Storage \ud1a0\ud070 \uad00\ub9ac, Dio \uc790\ub3d9 \uac31\uc2e0 &amp; \ub85c\uadf8\uc778 \ud654\uba74 \uad6c\ud604\" class=\"read-more\" href=\"https:\/\/dingdong.goodday.ing\/?p=200\" aria-label=\"[\uac1c\ubc1c\uc77c\uc9c0 #07 | \ud074\ub77c\uc774\uc5b8\ud2b8] Flutter \uc571 \uccab \uc5f0\ub3d9: Secure Storage \ud1a0\ud070 \uad00\ub9ac, Dio \uc790\ub3d9 \uac31\uc2e0 &amp; \ub85c\uadf8\uc778 \ud654\uba74 \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":[8],"class_list":["post-200","post","type-post","status-publish","format-standard","hentry","category-dingdong-","tag-8"],"_links":{"self":[{"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/200","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=200"}],"version-history":[{"count":4,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/200\/revisions"}],"predecessor-version":[{"id":214,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=\/wp\/v2\/posts\/200\/revisions\/214"}],"wp:attachment":[{"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=200"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=200"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dingdong.goodday.ing\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=200"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}