[{"data":1,"prerenderedAt":3457},["ShallowReactive",2],{"content-/message-mockup-migration-plan":3,"all-pages-for-dir":3435,"related-/message-mockup-migration-plan":3436,"og-image-/message-mockup-migration-plan":3456},{"id":4,"title":5,"body":6,"category":3421,"concepts":3421,"description":3422,"extension":3423,"meta":3424,"navigation":177,"ogImage":3421,"path":3425,"project_name":3421,"published":3426,"publishedAt":3427,"seo":3428,"source":3421,"stem":3429,"tags":3430,"todo":3421,"unpublished":3426,"updatedAt":3427,"__hash__":3434},"pages/2025-12/2025-12-03/message-mockup-migration-plan.md","Message Mockup Tool - Vue.js移行の必要性分析",{"type":7,"value":8,"toc":3395},"minimark",[9,13,17,25,28,32,35,39,50,54,221,227,230,233,702,705,861,865,948,952,1019,1021,1025,1029,1034,1108,1113,1129,1396,1401,1420,1425,1604,1610,1612,1616,1622,1626,1632,1637,1772,1776,1782,1787,1789,1793,1798,1803,1999,2004,2066,2070,2207,2212,2214,2218,2223,2302,2306,2317,2321,2425,2429,2656,2661,2663,2667,2672,2676,2687,2692,2836,2841,3026,3031,3033,3037,3042,3048,3053,3064,3069,3075,3080,3098,3103,3105,3108,3114,3116,3119,3123,3183,3186,3189,3222,3225,3289,3291,3294,3386,3391],[10,11,5],"h1",{"id":12},"message-mockup-tool-vuejs移行の必要性分析",[14,15,16],"h2",{"id":16},"結論",[18,19,20,24],"p",{},[21,22,23],"strong",{},"現時点では Vanilla JS のままで十分。"," ただし、以下の条件に該当する場合は Vue.js への移行を検討する価値がある。",[26,27],"hr",{},[14,29,31],{"id":30},"前提知識状態stateとは何か","前提知識：「状態（state）」とは何か",[18,33,34],{},"プログラミングにおける「状態」とは、アプリが「今どうなっているか」を表すデータのことです。",[36,37,38],"h3",{"id":38},"身近な例で理解する",[40,41,46],"pre",{"className":42,"code":44,"language":45},[43],"language-text","【ECサイトの状態の例】\n\n┌─────────────────────────────────────────────────────┐\n│ 状態（state）= アプリの「今の状況」を表すデータ      │\n├─────────────────────────────────────────────────────┤\n│                                                     │\n│  ユーザー情報:                                      │\n│    ├── ログインしてる？ → true / false             │\n│    ├── 誰がログインしてる？ → { name: \"田中\", ... }│\n│    └── カートに何が入ってる？ → [商品A, 商品B]     │\n│                                                     │\n│  UI の状態:                                         │\n│    ├── メニューは開いてる？ → true / false         │\n│    ├── ローディング中？ → true / false             │\n│    └── エラーメッセージ → \"在庫切れです\"           │\n│                                                     │\n│  フォームの状態:                                    │\n│    ├── 入力された名前 → \"山田太郎\"                 │\n│    ├── 入力されたメール → \"yamada@example.com\"     │\n│    └── バリデーションエラー → { email: \"無効\" }    │\n│                                                     │\n└─────────────────────────────────────────────────────┘\n","text",[47,48,44],"code",{"__ignoreMap":49},"",[36,51,53],{"id":52},"今回のmessage-mockupツールの状態","今回のMessage Mockupツールの「状態」",[40,55,59],{"className":56,"code":57,"language":58,"meta":49,"style":49},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// message-mockup.js の state がまさに「状態」\nconst state = {\n  // ========== データの状態 ==========\n  sender: 'You',           // 送信者の名前（今どうなってる？）\n  receiver: 'Friend',      // 受信者の名前（今どうなってる？）\n  messages: [...],         // メッセージ一覧（今何がある？）\n  nextId: 7,               // 次のメッセージID（今いくつ？）\n\n  // ========== UI の状態 ==========\n  isAnimating: false,      // アニメーション再生中？（今どうなってる？）\n  isRecording: false       // 録画中？（今どうなってる？）\n}\n","javascript",[47,60,61,70,88,94,121,141,155,172,179,185,202,215],{"__ignoreMap":49},[62,63,66],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"sxvE3","// message-mockup.js の state がまさに「状態」\n",[62,71,73,77,81,85],{"class":64,"line":72},2,[62,74,76],{"class":75},"stQ0i","const",[62,78,80],{"class":79},"s4oTP"," state",[62,82,84],{"class":83},"shFtX"," =",[62,86,87],{"class":83}," {\n",[62,89,91],{"class":64,"line":90},3,[62,92,93],{"class":68},"  // ========== データの状態 ==========\n",[62,95,97,101,104,108,112,115,118],{"class":64,"line":96},4,[62,98,100],{"class":99},"sz8Xr","  sender",[62,102,103],{"class":83},":",[62,105,107],{"class":106},"sMJiu"," '",[62,109,111],{"class":110},"sdGka","You",[62,113,114],{"class":106},"'",[62,116,117],{"class":83},",",[62,119,120],{"class":68},"           // 送信者の名前（今どうなってる？）\n",[62,122,124,127,129,131,134,136,138],{"class":64,"line":123},5,[62,125,126],{"class":99},"  receiver",[62,128,103],{"class":83},[62,130,107],{"class":106},[62,132,133],{"class":110},"Friend",[62,135,114],{"class":106},[62,137,117],{"class":83},[62,139,140],{"class":68},"      // 受信者の名前（今どうなってる？）\n",[62,142,144,147,149,152],{"class":64,"line":143},6,[62,145,146],{"class":99},"  messages",[62,148,103],{"class":83},[62,150,151],{"class":83}," [...],",[62,153,154],{"class":68},"         // メッセージ一覧（今何がある？）\n",[62,156,158,161,163,167,169],{"class":64,"line":157},7,[62,159,160],{"class":99},"  nextId",[62,162,103],{"class":83},[62,164,166],{"class":165},"sM54T"," 7",[62,168,117],{"class":83},[62,170,171],{"class":68},"               // 次のメッセージID（今いくつ？）\n",[62,173,175],{"class":64,"line":174},8,[62,176,178],{"emptyLinePlaceholder":177},true,"\n",[62,180,182],{"class":64,"line":181},9,[62,183,184],{"class":68},"  // ========== UI の状態 ==========\n",[62,186,188,191,193,197,199],{"class":64,"line":187},10,[62,189,190],{"class":99},"  isAnimating",[62,192,103],{"class":83},[62,194,196],{"class":195},"sHkkW"," false",[62,198,117],{"class":83},[62,200,201],{"class":68},"      // アニメーション再生中？（今どうなってる？）\n",[62,203,205,208,210,212],{"class":64,"line":204},11,[62,206,207],{"class":99},"  isRecording",[62,209,103],{"class":83},[62,211,196],{"class":195},[62,213,214],{"class":68},"       // 録画中？（今どうなってる？）\n",[62,216,218],{"class":64,"line":217},12,[62,219,220],{"class":83},"}\n",[18,222,223,226],{},[21,224,225],{},"現状は6個の状態"," → シンプルなので管理しやすい",[36,228,229],{"id":229},"状態が増えるとどうなるか",[18,231,232],{},"サービス化すると、こうなる：",[40,234,236],{"className":56,"code":235,"language":58,"meta":49,"style":49},"const state = {\n  // ========== 元からある状態（6個）==========\n  sender: 'You',\n  receiver: 'Friend',\n  messages: [...],\n  nextId: 7,\n  isAnimating: false,\n  isRecording: false,\n\n  // ========== 認証で追加（4個）==========\n  user: { id, email, name },    // ログイン中のユーザー\n  isAuthenticated: false,       // ログインしてる？\n  accessToken: '...',           // 認証トークン\n  tokenExpiry: Date,            // トークン有効期限\n\n  // ========== 課金で追加（5個）==========\n  plan: 'free',                 // 現在のプラン\n  usageCount: 3,                // 今月の使用回数\n  usageLimit: 5,                // 上限\n  subscriptionId: '...',        // Stripe契約ID\n  billingCycleEnd: Date,        // 次回請求日\n\n  // ========== 設定で追加（3個）==========\n  theme: 'light',               // ダークモード？\n  language: 'ja',               // 言語設定\n  defaultApp: 'x',              // デフォルトのアプリ\n\n  // ========== UIで追加（4個）==========\n  isSettingsOpen: false,        // 設定画面開いてる？\n  isUpgradeModalOpen: false,    // アップグレードモーダル開いてる？\n  currentTab: 'editor',         // 今どのタブ？\n  sidebarCollapsed: false,      // サイドバー閉じてる？\n\n  // ========== 履歴で追加（2個）==========\n  savedMockups: [...],          // 保存済みモックアップ\n  exportHistory: [...]          // エクスポート履歴\n}\n// 合計: 6 + 4 + 5 + 3 + 4 + 2 = 24個の状態！\n",[47,237,238,248,253,268,282,291,301,311,321,325,330,359,373,393,409,414,420,440,456,472,491,506,511,517,537,557,577,582,588,603,618,638,653,658,664,677,691,696],{"__ignoreMap":49},[62,239,240,242,244,246],{"class":64,"line":65},[62,241,76],{"class":75},[62,243,80],{"class":79},[62,245,84],{"class":83},[62,247,87],{"class":83},[62,249,250],{"class":64,"line":72},[62,251,252],{"class":68},"  // ========== 元からある状態（6個）==========\n",[62,254,255,257,259,261,263,265],{"class":64,"line":90},[62,256,100],{"class":99},[62,258,103],{"class":83},[62,260,107],{"class":106},[62,262,111],{"class":110},[62,264,114],{"class":106},[62,266,267],{"class":83},",\n",[62,269,270,272,274,276,278,280],{"class":64,"line":96},[62,271,126],{"class":99},[62,273,103],{"class":83},[62,275,107],{"class":106},[62,277,133],{"class":110},[62,279,114],{"class":106},[62,281,267],{"class":83},[62,283,284,286,288],{"class":64,"line":123},[62,285,146],{"class":99},[62,287,103],{"class":83},[62,289,290],{"class":83}," [...],\n",[62,292,293,295,297,299],{"class":64,"line":143},[62,294,160],{"class":99},[62,296,103],{"class":83},[62,298,166],{"class":165},[62,300,267],{"class":83},[62,302,303,305,307,309],{"class":64,"line":157},[62,304,190],{"class":99},[62,306,103],{"class":83},[62,308,196],{"class":195},[62,310,267],{"class":83},[62,312,313,315,317,319],{"class":64,"line":174},[62,314,207],{"class":99},[62,316,103],{"class":83},[62,318,196],{"class":195},[62,320,267],{"class":83},[62,322,323],{"class":64,"line":181},[62,324,178],{"emptyLinePlaceholder":177},[62,326,327],{"class":64,"line":187},[62,328,329],{"class":68},"  // ========== 認証で追加（4個）==========\n",[62,331,332,335,337,340,343,345,348,350,353,356],{"class":64,"line":204},[62,333,334],{"class":99},"  user",[62,336,103],{"class":83},[62,338,339],{"class":83}," {",[62,341,342],{"class":79}," id",[62,344,117],{"class":83},[62,346,347],{"class":79}," email",[62,349,117],{"class":83},[62,351,352],{"class":79}," name",[62,354,355],{"class":83}," },",[62,357,358],{"class":68},"    // ログイン中のユーザー\n",[62,360,361,364,366,368,370],{"class":64,"line":217},[62,362,363],{"class":99},"  isAuthenticated",[62,365,103],{"class":83},[62,367,196],{"class":195},[62,369,117],{"class":83},[62,371,372],{"class":68},"       // ログインしてる？\n",[62,374,376,379,381,383,386,388,390],{"class":64,"line":375},13,[62,377,378],{"class":99},"  accessToken",[62,380,103],{"class":83},[62,382,107],{"class":106},[62,384,385],{"class":110},"...",[62,387,114],{"class":106},[62,389,117],{"class":83},[62,391,392],{"class":68},"           // 認証トークン\n",[62,394,396,399,401,404,406],{"class":64,"line":395},14,[62,397,398],{"class":99},"  tokenExpiry",[62,400,103],{"class":83},[62,402,403],{"class":79}," Date",[62,405,117],{"class":83},[62,407,408],{"class":68},"            // トークン有効期限\n",[62,410,412],{"class":64,"line":411},15,[62,413,178],{"emptyLinePlaceholder":177},[62,415,417],{"class":64,"line":416},16,[62,418,419],{"class":68},"  // ========== 課金で追加（5個）==========\n",[62,421,423,426,428,430,433,435,437],{"class":64,"line":422},17,[62,424,425],{"class":99},"  plan",[62,427,103],{"class":83},[62,429,107],{"class":106},[62,431,432],{"class":110},"free",[62,434,114],{"class":106},[62,436,117],{"class":83},[62,438,439],{"class":68},"                 // 現在のプラン\n",[62,441,443,446,448,451,453],{"class":64,"line":442},18,[62,444,445],{"class":99},"  usageCount",[62,447,103],{"class":83},[62,449,450],{"class":165}," 3",[62,452,117],{"class":83},[62,454,455],{"class":68},"                // 今月の使用回数\n",[62,457,459,462,464,467,469],{"class":64,"line":458},19,[62,460,461],{"class":99},"  usageLimit",[62,463,103],{"class":83},[62,465,466],{"class":165}," 5",[62,468,117],{"class":83},[62,470,471],{"class":68},"                // 上限\n",[62,473,475,478,480,482,484,486,488],{"class":64,"line":474},20,[62,476,477],{"class":99},"  subscriptionId",[62,479,103],{"class":83},[62,481,107],{"class":106},[62,483,385],{"class":110},[62,485,114],{"class":106},[62,487,117],{"class":83},[62,489,490],{"class":68},"        // Stripe契約ID\n",[62,492,494,497,499,501,503],{"class":64,"line":493},21,[62,495,496],{"class":99},"  billingCycleEnd",[62,498,103],{"class":83},[62,500,403],{"class":79},[62,502,117],{"class":83},[62,504,505],{"class":68},"        // 次回請求日\n",[62,507,509],{"class":64,"line":508},22,[62,510,178],{"emptyLinePlaceholder":177},[62,512,514],{"class":64,"line":513},23,[62,515,516],{"class":68},"  // ========== 設定で追加（3個）==========\n",[62,518,520,523,525,527,530,532,534],{"class":64,"line":519},24,[62,521,522],{"class":99},"  theme",[62,524,103],{"class":83},[62,526,107],{"class":106},[62,528,529],{"class":110},"light",[62,531,114],{"class":106},[62,533,117],{"class":83},[62,535,536],{"class":68},"               // ダークモード？\n",[62,538,540,543,545,547,550,552,554],{"class":64,"line":539},25,[62,541,542],{"class":99},"  language",[62,544,103],{"class":83},[62,546,107],{"class":106},[62,548,549],{"class":110},"ja",[62,551,114],{"class":106},[62,553,117],{"class":83},[62,555,556],{"class":68},"               // 言語設定\n",[62,558,560,563,565,567,570,572,574],{"class":64,"line":559},26,[62,561,562],{"class":99},"  defaultApp",[62,564,103],{"class":83},[62,566,107],{"class":106},[62,568,569],{"class":110},"x",[62,571,114],{"class":106},[62,573,117],{"class":83},[62,575,576],{"class":68},"              // デフォルトのアプリ\n",[62,578,580],{"class":64,"line":579},27,[62,581,178],{"emptyLinePlaceholder":177},[62,583,585],{"class":64,"line":584},28,[62,586,587],{"class":68},"  // ========== UIで追加（4個）==========\n",[62,589,591,594,596,598,600],{"class":64,"line":590},29,[62,592,593],{"class":99},"  isSettingsOpen",[62,595,103],{"class":83},[62,597,196],{"class":195},[62,599,117],{"class":83},[62,601,602],{"class":68},"        // 設定画面開いてる？\n",[62,604,606,609,611,613,615],{"class":64,"line":605},30,[62,607,608],{"class":99},"  isUpgradeModalOpen",[62,610,103],{"class":83},[62,612,196],{"class":195},[62,614,117],{"class":83},[62,616,617],{"class":68},"    // アップグレードモーダル開いてる？\n",[62,619,621,624,626,628,631,633,635],{"class":64,"line":620},31,[62,622,623],{"class":99},"  currentTab",[62,625,103],{"class":83},[62,627,107],{"class":106},[62,629,630],{"class":110},"editor",[62,632,114],{"class":106},[62,634,117],{"class":83},[62,636,637],{"class":68},"         // 今どのタブ？\n",[62,639,641,644,646,648,650],{"class":64,"line":640},32,[62,642,643],{"class":99},"  sidebarCollapsed",[62,645,103],{"class":83},[62,647,196],{"class":195},[62,649,117],{"class":83},[62,651,652],{"class":68},"      // サイドバー閉じてる？\n",[62,654,656],{"class":64,"line":655},33,[62,657,178],{"emptyLinePlaceholder":177},[62,659,661],{"class":64,"line":660},34,[62,662,663],{"class":68},"  // ========== 履歴で追加（2個）==========\n",[62,665,667,670,672,674],{"class":64,"line":666},35,[62,668,669],{"class":99},"  savedMockups",[62,671,103],{"class":83},[62,673,151],{"class":83},[62,675,676],{"class":68},"          // 保存済みモックアップ\n",[62,678,680,683,685,688],{"class":64,"line":679},36,[62,681,682],{"class":99},"  exportHistory",[62,684,103],{"class":83},[62,686,687],{"class":83}," [...]",[62,689,690],{"class":68},"          // エクスポート履歴\n",[62,692,694],{"class":64,"line":693},37,[62,695,220],{"class":83},[62,697,699],{"class":64,"line":698},38,[62,700,701],{"class":68},"// 合計: 6 + 4 + 5 + 3 + 4 + 2 = 24個の状態！\n",[36,703,704],{"id":704},"状態が多いと何が問題か",[40,706,708],{"className":56,"code":707,"language":58,"meta":49,"style":49},"// 問題1: 状態を変えたら、UIを手動で更新しないといけない\nfunction login(user) {\n  state.user = user\n  state.isAuthenticated = true\n\n  // 忘れると表示がおかしくなる！\n  updateHeader()        // ヘッダーのログイン状態を更新\n  updateSidebar()       // サイドバーのユーザー名を更新\n  updateUsageDisplay()  // 使用回数表示を更新\n  checkPlanLimits()     // プラン制限をチェック\n  // ... 他にも更新が必要な場所があるかも？\n}\n\n// 問題2: どこで状態が変わったか追跡困難\n// 「なぜか isAnimating が true のままになるバグ」\n// → コード全体を検索して、どこで変更してるか探す必要がある\n\n// 問題3: 状態同士の依存関係\n// 「プランが free なら usageLimit は 5、pro なら無制限」\n// → この関係を手動で維持しないといけない\n",[47,709,710,715,735,750,764,768,773,784,794,804,814,819,823,827,832,837,842,846,851,856],{"__ignoreMap":49},[62,711,712],{"class":64,"line":65},[62,713,714],{"class":68},"// 問題1: 状態を変えたら、UIを手動で更新しないといけない\n",[62,716,717,720,724,727,730,733],{"class":64,"line":72},[62,718,719],{"class":75},"function",[62,721,723],{"class":722},"senZ8"," login",[62,725,726],{"class":83},"(",[62,728,729],{"class":79},"user",[62,731,732],{"class":83},")",[62,734,87],{"class":83},[62,736,737,740,743,745,747],{"class":64,"line":90},[62,738,739],{"class":79},"  state",[62,741,742],{"class":83},".",[62,744,729],{"class":79},[62,746,84],{"class":83},[62,748,749],{"class":79}," user\n",[62,751,752,754,756,759,761],{"class":64,"line":96},[62,753,739],{"class":79},[62,755,742],{"class":83},[62,757,758],{"class":79},"isAuthenticated",[62,760,84],{"class":83},[62,762,763],{"class":195}," true\n",[62,765,766],{"class":64,"line":123},[62,767,178],{"emptyLinePlaceholder":177},[62,769,770],{"class":64,"line":143},[62,771,772],{"class":68},"  // 忘れると表示がおかしくなる！\n",[62,774,775,778,781],{"class":64,"line":157},[62,776,777],{"class":722},"  updateHeader",[62,779,780],{"class":83},"()",[62,782,783],{"class":68},"        // ヘッダーのログイン状態を更新\n",[62,785,786,789,791],{"class":64,"line":174},[62,787,788],{"class":722},"  updateSidebar",[62,790,780],{"class":83},[62,792,793],{"class":68},"       // サイドバーのユーザー名を更新\n",[62,795,796,799,801],{"class":64,"line":181},[62,797,798],{"class":722},"  updateUsageDisplay",[62,800,780],{"class":83},[62,802,803],{"class":68},"  // 使用回数表示を更新\n",[62,805,806,809,811],{"class":64,"line":187},[62,807,808],{"class":722},"  checkPlanLimits",[62,810,780],{"class":83},[62,812,813],{"class":68},"     // プラン制限をチェック\n",[62,815,816],{"class":64,"line":204},[62,817,818],{"class":68},"  // ... 他にも更新が必要な場所があるかも？\n",[62,820,821],{"class":64,"line":217},[62,822,220],{"class":83},[62,824,825],{"class":64,"line":375},[62,826,178],{"emptyLinePlaceholder":177},[62,828,829],{"class":64,"line":395},[62,830,831],{"class":68},"// 問題2: どこで状態が変わったか追跡困難\n",[62,833,834],{"class":64,"line":411},[62,835,836],{"class":68},"// 「なぜか isAnimating が true のままになるバグ」\n",[62,838,839],{"class":64,"line":416},[62,840,841],{"class":68},"// → コード全体を検索して、どこで変更してるか探す必要がある\n",[62,843,844],{"class":64,"line":422},[62,845,178],{"emptyLinePlaceholder":177},[62,847,848],{"class":64,"line":442},[62,849,850],{"class":68},"// 問題3: 状態同士の依存関係\n",[62,852,853],{"class":64,"line":458},[62,854,855],{"class":68},"// 「プランが free なら usageLimit は 5、pro なら無制限」\n",[62,857,858],{"class":64,"line":474},[62,859,860],{"class":68},"// → この関係を手動で維持しないといけない\n",[36,862,864],{"id":863},"vuejs-なら自動で解決","Vue.js なら自動で解決",[40,866,870],{"className":867,"code":868,"language":869,"meta":49,"style":49},"language-typescript shiki shiki-themes vitesse-light vitesse-light","// Vue.js: 状態を変えると、UIが自動で更新される\nconst user = ref(null)\nconst isAuthenticated = computed(() => !!user.value)  // 自動計算\n\n// user.value = { name: '田中' } と代入するだけで\n// isAuthenticated が true になり\n// 関連するUI（ヘッダー、サイドバー等）が全部自動更新される\n","typescript",[47,871,872,877,897,929,933,938,943],{"__ignoreMap":49},[62,873,874],{"class":64,"line":65},[62,875,876],{"class":68},"// Vue.js: 状態を変えると、UIが自動で更新される\n",[62,878,879,882,884,886,889,891,894],{"class":64,"line":72},[62,880,881],{"class":75},"const ",[62,883,729],{"class":79},[62,885,84],{"class":83},[62,887,888],{"class":722}," ref",[62,890,726],{"class":83},[62,892,893],{"class":75},"null",[62,895,896],{"class":83},")\n",[62,898,899,901,903,905,908,911,914,917,919,921,924,926],{"class":64,"line":90},[62,900,881],{"class":75},[62,902,758],{"class":79},[62,904,84],{"class":83},[62,906,907],{"class":722}," computed",[62,909,910],{"class":83},"(()",[62,912,913],{"class":83}," =>",[62,915,916],{"class":75}," !!",[62,918,729],{"class":79},[62,920,742],{"class":83},[62,922,923],{"class":79},"value",[62,925,732],{"class":83},[62,927,928],{"class":68},"  // 自動計算\n",[62,930,931],{"class":64,"line":96},[62,932,178],{"emptyLinePlaceholder":177},[62,934,935],{"class":64,"line":123},[62,936,937],{"class":68},"// user.value = { name: '田中' } と代入するだけで\n",[62,939,940],{"class":64,"line":143},[62,941,942],{"class":68},"// isAuthenticated が true になり\n",[62,944,945],{"class":64,"line":157},[62,946,947],{"class":68},"// 関連するUI（ヘッダー、サイドバー等）が全部自動更新される\n",[36,949,951],{"id":950},"状態10個の目安","「状態10個」の目安",[953,954,955,971],"table",{},[956,957,958],"thead",{},[959,960,961,965,968],"tr",{},[962,963,964],"th",{},"状態の数",[962,966,967],{},"管理難易度",[962,969,970],{},"推奨",[972,973,974,986,997,1008],"tbody",{},[959,975,976,980,983],{},[977,978,979],"td",{},"1-5個",[977,981,982],{},"簡単",[977,984,985],{},"Vanilla JS で十分",[959,987,988,991,994],{},[977,989,990],{},"6-10個",[977,992,993],{},"まだ管理可能",[977,995,996],{},"Vanilla JS でギリギリ",[959,998,999,1002,1005],{},[977,1000,1001],{},"11-20個",[977,1003,1004],{},"複雑化",[977,1006,1007],{},"Vue.js 検討",[959,1009,1010,1013,1016],{},[977,1011,1012],{},"20個以上",[977,1014,1015],{},"カオス",[977,1017,1018],{},"Vue.js + 状態管理ライブラリ",[26,1020],{},[14,1022,1024],{"id":1023},"vuejs移行が必要になる条件","Vue.js移行が必要になる条件",[36,1026,1028],{"id":1027},"_1-状態管理の複雑化","1. 状態管理の複雑化",[18,1030,1031,103],{},[21,1032,1033],{},"現状（Vanilla JS）",[40,1035,1037],{"className":56,"code":1036,"language":58,"meta":49,"style":49},"const state = {\n  sender: 'You',\n  receiver: 'Friend',\n  messages: [...],\n  isAnimating: false,\n  isRecording: false\n}\n",[47,1038,1039,1049,1063,1077,1085,1095,1104],{"__ignoreMap":49},[62,1040,1041,1043,1045,1047],{"class":64,"line":65},[62,1042,76],{"class":75},[62,1044,80],{"class":79},[62,1046,84],{"class":83},[62,1048,87],{"class":83},[62,1050,1051,1053,1055,1057,1059,1061],{"class":64,"line":72},[62,1052,100],{"class":99},[62,1054,103],{"class":83},[62,1056,107],{"class":106},[62,1058,111],{"class":110},[62,1060,114],{"class":106},[62,1062,267],{"class":83},[62,1064,1065,1067,1069,1071,1073,1075],{"class":64,"line":90},[62,1066,126],{"class":99},[62,1068,103],{"class":83},[62,1070,107],{"class":106},[62,1072,133],{"class":110},[62,1074,114],{"class":106},[62,1076,267],{"class":83},[62,1078,1079,1081,1083],{"class":64,"line":96},[62,1080,146],{"class":99},[62,1082,103],{"class":83},[62,1084,290],{"class":83},[62,1086,1087,1089,1091,1093],{"class":64,"line":123},[62,1088,190],{"class":99},[62,1090,103],{"class":83},[62,1092,196],{"class":195},[62,1094,267],{"class":83},[62,1096,1097,1099,1101],{"class":64,"line":143},[62,1098,207],{"class":99},[62,1100,103],{"class":83},[62,1102,1103],{"class":195}," false\n",[62,1105,1106],{"class":64,"line":157},[62,1107,220],{"class":83},[18,1109,1110,103],{},[21,1111,1112],{},"問題が発生するケース",[1114,1115,1116,1120,1123,1126],"ul",{},[1117,1118,1119],"li",{},"ログイン状態の追加",[1117,1121,1122],{},"課金プラン・使用回数の管理",[1117,1124,1125],{},"ユーザー設定の永続化",[1117,1127,1128],{},"複数画面間での状態共有",[40,1130,1132],{"className":56,"code":1131,"language":58,"meta":49,"style":49},"// 肥大化した状態の例\nconst state = {\n  // 元の状態\n  sender: 'You',\n  messages: [...],\n\n  // 認証関連\n  user: { id, email, name, avatar },\n  isAuthenticated: false,\n\n  // 課金関連\n  plan: 'free', // free | pro | enterprise\n  usageCount: 0,\n  usageLimit: 5,\n  billingCycle: 'monthly',\n\n  // 設定関連\n  preferences: { theme, language, defaultApp },\n\n  // UI状態\n  isAnimating: false,\n  isRecording: false,\n  isSettingsOpen: false,\n  isUpgradeModalOpen: false,\n\n  // 履歴\n  savedMockups: [...],\n  exportHistory: [...]\n}\n",[47,1133,1134,1139,1149,1154,1168,1176,1180,1185,1211,1221,1225,1230,1247,1258,1268,1284,1288,1293,1317,1321,1326,1336,1346,1356,1366,1370,1375,1383,1392],{"__ignoreMap":49},[62,1135,1136],{"class":64,"line":65},[62,1137,1138],{"class":68},"// 肥大化した状態の例\n",[62,1140,1141,1143,1145,1147],{"class":64,"line":72},[62,1142,76],{"class":75},[62,1144,80],{"class":79},[62,1146,84],{"class":83},[62,1148,87],{"class":83},[62,1150,1151],{"class":64,"line":90},[62,1152,1153],{"class":68},"  // 元の状態\n",[62,1155,1156,1158,1160,1162,1164,1166],{"class":64,"line":96},[62,1157,100],{"class":99},[62,1159,103],{"class":83},[62,1161,107],{"class":106},[62,1163,111],{"class":110},[62,1165,114],{"class":106},[62,1167,267],{"class":83},[62,1169,1170,1172,1174],{"class":64,"line":123},[62,1171,146],{"class":99},[62,1173,103],{"class":83},[62,1175,290],{"class":83},[62,1177,1178],{"class":64,"line":143},[62,1179,178],{"emptyLinePlaceholder":177},[62,1181,1182],{"class":64,"line":157},[62,1183,1184],{"class":68},"  // 認証関連\n",[62,1186,1187,1189,1191,1193,1195,1197,1199,1201,1203,1205,1208],{"class":64,"line":174},[62,1188,334],{"class":99},[62,1190,103],{"class":83},[62,1192,339],{"class":83},[62,1194,342],{"class":79},[62,1196,117],{"class":83},[62,1198,347],{"class":79},[62,1200,117],{"class":83},[62,1202,352],{"class":79},[62,1204,117],{"class":83},[62,1206,1207],{"class":79}," avatar",[62,1209,1210],{"class":83}," },\n",[62,1212,1213,1215,1217,1219],{"class":64,"line":181},[62,1214,363],{"class":99},[62,1216,103],{"class":83},[62,1218,196],{"class":195},[62,1220,267],{"class":83},[62,1222,1223],{"class":64,"line":187},[62,1224,178],{"emptyLinePlaceholder":177},[62,1226,1227],{"class":64,"line":204},[62,1228,1229],{"class":68},"  // 課金関連\n",[62,1231,1232,1234,1236,1238,1240,1242,1244],{"class":64,"line":217},[62,1233,425],{"class":99},[62,1235,103],{"class":83},[62,1237,107],{"class":106},[62,1239,432],{"class":110},[62,1241,114],{"class":106},[62,1243,117],{"class":83},[62,1245,1246],{"class":68}," // free | pro | enterprise\n",[62,1248,1249,1251,1253,1256],{"class":64,"line":375},[62,1250,445],{"class":99},[62,1252,103],{"class":83},[62,1254,1255],{"class":165}," 0",[62,1257,267],{"class":83},[62,1259,1260,1262,1264,1266],{"class":64,"line":395},[62,1261,461],{"class":99},[62,1263,103],{"class":83},[62,1265,466],{"class":165},[62,1267,267],{"class":83},[62,1269,1270,1273,1275,1277,1280,1282],{"class":64,"line":411},[62,1271,1272],{"class":99},"  billingCycle",[62,1274,103],{"class":83},[62,1276,107],{"class":106},[62,1278,1279],{"class":110},"monthly",[62,1281,114],{"class":106},[62,1283,267],{"class":83},[62,1285,1286],{"class":64,"line":416},[62,1287,178],{"emptyLinePlaceholder":177},[62,1289,1290],{"class":64,"line":422},[62,1291,1292],{"class":68},"  // 設定関連\n",[62,1294,1295,1298,1300,1302,1305,1307,1310,1312,1315],{"class":64,"line":442},[62,1296,1297],{"class":99},"  preferences",[62,1299,103],{"class":83},[62,1301,339],{"class":83},[62,1303,1304],{"class":79}," theme",[62,1306,117],{"class":83},[62,1308,1309],{"class":79}," language",[62,1311,117],{"class":83},[62,1313,1314],{"class":79}," defaultApp",[62,1316,1210],{"class":83},[62,1318,1319],{"class":64,"line":458},[62,1320,178],{"emptyLinePlaceholder":177},[62,1322,1323],{"class":64,"line":474},[62,1324,1325],{"class":68},"  // UI状態\n",[62,1327,1328,1330,1332,1334],{"class":64,"line":493},[62,1329,190],{"class":99},[62,1331,103],{"class":83},[62,1333,196],{"class":195},[62,1335,267],{"class":83},[62,1337,1338,1340,1342,1344],{"class":64,"line":508},[62,1339,207],{"class":99},[62,1341,103],{"class":83},[62,1343,196],{"class":195},[62,1345,267],{"class":83},[62,1347,1348,1350,1352,1354],{"class":64,"line":513},[62,1349,593],{"class":99},[62,1351,103],{"class":83},[62,1353,196],{"class":195},[62,1355,267],{"class":83},[62,1357,1358,1360,1362,1364],{"class":64,"line":519},[62,1359,608],{"class":99},[62,1361,103],{"class":83},[62,1363,196],{"class":195},[62,1365,267],{"class":83},[62,1367,1368],{"class":64,"line":539},[62,1369,178],{"emptyLinePlaceholder":177},[62,1371,1372],{"class":64,"line":559},[62,1373,1374],{"class":68},"  // 履歴\n",[62,1376,1377,1379,1381],{"class":64,"line":579},[62,1378,669],{"class":99},[62,1380,103],{"class":83},[62,1382,290],{"class":83},[62,1384,1385,1387,1389],{"class":64,"line":584},[62,1386,682],{"class":99},[62,1388,103],{"class":83},[62,1390,1391],{"class":83}," [...]\n",[62,1393,1394],{"class":64,"line":590},[62,1395,220],{"class":83},[18,1397,1398,103],{},[21,1399,1400],{},"Vanilla JSの問題点",[1114,1402,1403,1414,1417],{},[1117,1404,1405,1406,1409,1410,1413],{},"状態変更時の UI 同期が手動（",[47,1407,1408],{},"renderMessages()",", ",[47,1411,1412],{},"renderPreview()"," を都度呼ぶ）",[1117,1415,1416],{},"状態の依存関係が見えにくい",[1117,1418,1419],{},"デバッグが困難（どこで状態が変わったか追跡しづらい）",[18,1421,1422,103],{},[21,1423,1424],{},"Vue.jsの解決策",[40,1426,1428],{"className":867,"code":1427,"language":869,"meta":49,"style":49},"// composables/useAuth.ts\nexport const useAuth = () => {\n  const user = ref(null)\n  const isAuthenticated = computed(() => !!user.value)\n  // 状態変更 → UI自動更新\n}\n\n// composables/useBilling.ts\nexport const useBilling = () => {\n  const plan = ref('free')\n  const canExport = computed(() => plan.value !== 'free' || usageCount.value \u003C 5)\n}\n",[47,1429,1430,1435,1455,1472,1496,1501,1505,1509,1514,1531,1552,1600],{"__ignoreMap":49},[62,1431,1432],{"class":64,"line":65},[62,1433,1434],{"class":68},"// composables/useAuth.ts\n",[62,1436,1437,1440,1443,1446,1448,1451,1453],{"class":64,"line":72},[62,1438,1439],{"class":195},"export",[62,1441,1442],{"class":75}," const ",[62,1444,1445],{"class":722},"useAuth",[62,1447,84],{"class":83},[62,1449,1450],{"class":83}," ()",[62,1452,913],{"class":83},[62,1454,87],{"class":83},[62,1456,1457,1460,1462,1464,1466,1468,1470],{"class":64,"line":90},[62,1458,1459],{"class":75},"  const ",[62,1461,729],{"class":79},[62,1463,84],{"class":83},[62,1465,888],{"class":722},[62,1467,726],{"class":83},[62,1469,893],{"class":75},[62,1471,896],{"class":83},[62,1473,1474,1476,1478,1480,1482,1484,1486,1488,1490,1492,1494],{"class":64,"line":96},[62,1475,1459],{"class":75},[62,1477,758],{"class":79},[62,1479,84],{"class":83},[62,1481,907],{"class":722},[62,1483,910],{"class":83},[62,1485,913],{"class":83},[62,1487,916],{"class":75},[62,1489,729],{"class":79},[62,1491,742],{"class":83},[62,1493,923],{"class":79},[62,1495,896],{"class":83},[62,1497,1498],{"class":64,"line":123},[62,1499,1500],{"class":68},"  // 状態変更 → UI自動更新\n",[62,1502,1503],{"class":64,"line":143},[62,1504,220],{"class":83},[62,1506,1507],{"class":64,"line":157},[62,1508,178],{"emptyLinePlaceholder":177},[62,1510,1511],{"class":64,"line":174},[62,1512,1513],{"class":68},"// composables/useBilling.ts\n",[62,1515,1516,1518,1520,1523,1525,1527,1529],{"class":64,"line":181},[62,1517,1439],{"class":195},[62,1519,1442],{"class":75},[62,1521,1522],{"class":722},"useBilling",[62,1524,84],{"class":83},[62,1526,1450],{"class":83},[62,1528,913],{"class":83},[62,1530,87],{"class":83},[62,1532,1533,1535,1538,1540,1542,1544,1546,1548,1550],{"class":64,"line":187},[62,1534,1459],{"class":75},[62,1536,1537],{"class":79},"plan",[62,1539,84],{"class":83},[62,1541,888],{"class":722},[62,1543,726],{"class":83},[62,1545,114],{"class":106},[62,1547,432],{"class":110},[62,1549,114],{"class":106},[62,1551,896],{"class":83},[62,1553,1554,1556,1559,1561,1563,1565,1567,1570,1572,1574,1577,1579,1581,1583,1586,1589,1591,1593,1596,1598],{"class":64,"line":204},[62,1555,1459],{"class":75},[62,1557,1558],{"class":79},"canExport",[62,1560,84],{"class":83},[62,1562,907],{"class":722},[62,1564,910],{"class":83},[62,1566,913],{"class":83},[62,1568,1569],{"class":79}," plan",[62,1571,742],{"class":83},[62,1573,923],{"class":79},[62,1575,1576],{"class":75}," !== ",[62,1578,114],{"class":106},[62,1580,432],{"class":110},[62,1582,114],{"class":106},[62,1584,1585],{"class":75}," || ",[62,1587,1588],{"class":79},"usageCount",[62,1590,742],{"class":83},[62,1592,923],{"class":79},[62,1594,1595],{"class":83}," \u003C",[62,1597,466],{"class":165},[62,1599,896],{"class":83},[62,1601,1602],{"class":64,"line":217},[62,1603,220],{"class":83},[18,1605,1606,1609],{},[21,1607,1608],{},"判断基準",": 状態が10個以上になったら移行を検討",[26,1611],{},[36,1613,1615],{"id":1614},"_2-複数画面ルーティングの必要性","2. 複数画面・ルーティングの必要性",[18,1617,1618,1621],{},[21,1619,1620],{},"現状",": 単一HTMLファイルで完結",[18,1623,1624,103],{},[21,1625,1112],{},[40,1627,1630],{"className":1628,"code":1629,"language":45},[43],"/                    → ランディングページ\n/app                 → メインツール\n/app/history         → 作成履歴\n/app/settings        → 設定\n/pricing             → 料金プラン\n/dashboard           → 使用状況ダッシュボード\n/login               → ログイン\n/success             → 決済完了\n",[47,1631,1629],{"__ignoreMap":49},[18,1633,1634,103],{},[21,1635,1636],{},"Vanilla JSでの実装",[40,1638,1640],{"className":56,"code":1639,"language":58,"meta":49,"style":49},"// 自前でルーティング実装が必要\nwindow.addEventListener('hashchange', () => {\n  const route = window.location.hash.slice(1)\n  if (route === '/history') showHistoryPage()\n  if (route === '/settings') showSettingsPage()\n  // ... どんどん増える\n})\n",[47,1641,1642,1647,1674,1709,1738,1762,1767],{"__ignoreMap":49},[62,1643,1644],{"class":64,"line":65},[62,1645,1646],{"class":68},"// 自前でルーティング実装が必要\n",[62,1648,1649,1652,1654,1657,1659,1661,1664,1666,1668,1670,1672],{"class":64,"line":72},[62,1650,1651],{"class":79},"window",[62,1653,742],{"class":83},[62,1655,1656],{"class":722},"addEventListener",[62,1658,726],{"class":83},[62,1660,114],{"class":106},[62,1662,1663],{"class":110},"hashchange",[62,1665,114],{"class":106},[62,1667,117],{"class":83},[62,1669,1450],{"class":83},[62,1671,913],{"class":83},[62,1673,87],{"class":83},[62,1675,1676,1679,1682,1684,1687,1689,1692,1694,1697,1699,1702,1704,1707],{"class":64,"line":90},[62,1677,1678],{"class":75},"  const",[62,1680,1681],{"class":79}," route",[62,1683,84],{"class":83},[62,1685,1686],{"class":79}," window",[62,1688,742],{"class":83},[62,1690,1691],{"class":79},"location",[62,1693,742],{"class":83},[62,1695,1696],{"class":79},"hash",[62,1698,742],{"class":83},[62,1700,1701],{"class":722},"slice",[62,1703,726],{"class":83},[62,1705,1706],{"class":165},"1",[62,1708,896],{"class":83},[62,1710,1711,1714,1717,1720,1723,1725,1728,1730,1732,1735],{"class":64,"line":96},[62,1712,1713],{"class":195},"  if",[62,1715,1716],{"class":83}," (",[62,1718,1719],{"class":79},"route",[62,1721,1722],{"class":75}," ===",[62,1724,107],{"class":106},[62,1726,1727],{"class":110},"/history",[62,1729,114],{"class":106},[62,1731,732],{"class":83},[62,1733,1734],{"class":722}," showHistoryPage",[62,1736,1737],{"class":83},"()\n",[62,1739,1740,1742,1744,1746,1748,1750,1753,1755,1757,1760],{"class":64,"line":123},[62,1741,1713],{"class":195},[62,1743,1716],{"class":83},[62,1745,1719],{"class":79},[62,1747,1722],{"class":75},[62,1749,107],{"class":106},[62,1751,1752],{"class":110},"/settings",[62,1754,114],{"class":106},[62,1756,732],{"class":83},[62,1758,1759],{"class":722}," showSettingsPage",[62,1761,1737],{"class":83},[62,1763,1764],{"class":64,"line":143},[62,1765,1766],{"class":68},"  // ... どんどん増える\n",[62,1768,1769],{"class":64,"line":157},[62,1770,1771],{"class":83},"})\n",[18,1773,1774,103],{},[21,1775,1424],{},[40,1777,1780],{"className":1778,"code":1779,"language":45},[43],"pages/\n├── index.vue          # ランディング\n├── app/\n│   ├── index.vue      # メインツール\n│   ├── history.vue    # 履歴\n│   └── settings.vue   # 設定\n├── pricing.vue\n└── login.vue\n",[47,1781,1779],{"__ignoreMap":49},[18,1783,1784,1786],{},[21,1785,1608],{},": 3画面以上必要になったら移行を検討",[26,1788],{},[36,1790,1792],{"id":1791},"_3-コンポーネントの再利用","3. コンポーネントの再利用",[18,1794,1795,1797],{},[21,1796,1620],{},": 1つのアプリ（X/Twitter DM）のみ",[18,1799,1800,1802],{},[21,1801,1112],{},":\nアプリ選択UIがあるが、現状はX DMのみ実装:",[40,1804,1808],{"className":1805,"code":1806,"language":1807,"meta":49,"style":49},"language-html shiki shiki-themes vitesse-light vitesse-light","\u003Cdiv class=\"app-grid\">\n  \u003Cbutton data-app=\"discord\">Discord\u003C/button>\n  \u003Cbutton data-app=\"imessage\">iMessage\u003C/button>\n  \u003Cbutton data-app=\"messenger\">Messenger\u003C/button>\n  \u003Cbutton data-app=\"whatsapp\">WhatsApp\u003C/button>\n  \u003Cbutton data-app=\"x\" class=\"active\">X\u003C/button>\n\u003C/div>\n","html",[47,1809,1810,1835,1869,1897,1925,1953,1991],{"__ignoreMap":49},[62,1811,1812,1815,1818,1821,1824,1827,1830,1832],{"class":64,"line":65},[62,1813,1814],{"class":83},"\u003C",[62,1816,1817],{"class":195},"div",[62,1819,1820],{"class":79}," class",[62,1822,1823],{"class":83},"=",[62,1825,1826],{"class":106},"\"",[62,1828,1829],{"class":110},"app-grid",[62,1831,1826],{"class":106},[62,1833,1834],{"class":83},">\n",[62,1836,1837,1840,1843,1846,1848,1850,1853,1855,1858,1862,1865,1867],{"class":64,"line":72},[62,1838,1839],{"class":83},"  \u003C",[62,1841,1842],{"class":195},"button",[62,1844,1845],{"class":79}," data-app",[62,1847,1823],{"class":83},[62,1849,1826],{"class":106},[62,1851,1852],{"class":110},"discord",[62,1854,1826],{"class":106},[62,1856,1857],{"class":83},">",[62,1859,1861],{"class":1860},"sG7-3","Discord",[62,1863,1864],{"class":83},"\u003C/",[62,1866,1842],{"class":195},[62,1868,1834],{"class":83},[62,1870,1871,1873,1875,1877,1879,1881,1884,1886,1888,1891,1893,1895],{"class":64,"line":90},[62,1872,1839],{"class":83},[62,1874,1842],{"class":195},[62,1876,1845],{"class":79},[62,1878,1823],{"class":83},[62,1880,1826],{"class":106},[62,1882,1883],{"class":110},"imessage",[62,1885,1826],{"class":106},[62,1887,1857],{"class":83},[62,1889,1890],{"class":1860},"iMessage",[62,1892,1864],{"class":83},[62,1894,1842],{"class":195},[62,1896,1834],{"class":83},[62,1898,1899,1901,1903,1905,1907,1909,1912,1914,1916,1919,1921,1923],{"class":64,"line":96},[62,1900,1839],{"class":83},[62,1902,1842],{"class":195},[62,1904,1845],{"class":79},[62,1906,1823],{"class":83},[62,1908,1826],{"class":106},[62,1910,1911],{"class":110},"messenger",[62,1913,1826],{"class":106},[62,1915,1857],{"class":83},[62,1917,1918],{"class":1860},"Messenger",[62,1920,1864],{"class":83},[62,1922,1842],{"class":195},[62,1924,1834],{"class":83},[62,1926,1927,1929,1931,1933,1935,1937,1940,1942,1944,1947,1949,1951],{"class":64,"line":123},[62,1928,1839],{"class":83},[62,1930,1842],{"class":195},[62,1932,1845],{"class":79},[62,1934,1823],{"class":83},[62,1936,1826],{"class":106},[62,1938,1939],{"class":110},"whatsapp",[62,1941,1826],{"class":106},[62,1943,1857],{"class":83},[62,1945,1946],{"class":1860},"WhatsApp",[62,1948,1864],{"class":83},[62,1950,1842],{"class":195},[62,1952,1834],{"class":83},[62,1954,1955,1957,1959,1961,1963,1965,1967,1969,1971,1973,1975,1978,1980,1982,1985,1987,1989],{"class":64,"line":143},[62,1956,1839],{"class":83},[62,1958,1842],{"class":195},[62,1960,1845],{"class":79},[62,1962,1823],{"class":83},[62,1964,1826],{"class":106},[62,1966,569],{"class":110},[62,1968,1826],{"class":106},[62,1970,1820],{"class":79},[62,1972,1823],{"class":83},[62,1974,1826],{"class":106},[62,1976,1977],{"class":110},"active",[62,1979,1826],{"class":106},[62,1981,1857],{"class":83},[62,1983,1984],{"class":1860},"X",[62,1986,1864],{"class":83},[62,1988,1842],{"class":195},[62,1990,1834],{"class":83},[62,1992,1993,1995,1997],{"class":64,"line":157},[62,1994,1864],{"class":83},[62,1996,1817],{"class":195},[62,1998,1834],{"class":83},[18,2000,2001,103],{},[21,2002,2003],{},"複数アプリ対応時のVanilla JS",[40,2005,2007],{"className":56,"code":2006,"language":58,"meta":49,"style":49},"// コードの重複が大量発生\nfunction renderXPreview() { ... }\nfunction renderDiscordPreview() { ... }\nfunction renderIMessagePreview() { ... }\n// 共通部分の抽出が難しい\n",[47,2008,2009,2014,2031,2046,2061],{"__ignoreMap":49},[62,2010,2011],{"class":64,"line":65},[62,2012,2013],{"class":68},"// コードの重複が大量発生\n",[62,2015,2016,2018,2021,2023,2025,2028],{"class":64,"line":72},[62,2017,719],{"class":75},[62,2019,2020],{"class":722}," renderXPreview",[62,2022,780],{"class":83},[62,2024,339],{"class":83},[62,2026,2027],{"class":83}," ...",[62,2029,2030],{"class":83}," }\n",[62,2032,2033,2035,2038,2040,2042,2044],{"class":64,"line":90},[62,2034,719],{"class":75},[62,2036,2037],{"class":722}," renderDiscordPreview",[62,2039,780],{"class":83},[62,2041,339],{"class":83},[62,2043,2027],{"class":83},[62,2045,2030],{"class":83},[62,2047,2048,2050,2053,2055,2057,2059],{"class":64,"line":96},[62,2049,719],{"class":75},[62,2051,2052],{"class":722}," renderIMessagePreview",[62,2054,780],{"class":83},[62,2056,339],{"class":83},[62,2058,2027],{"class":83},[62,2060,2030],{"class":83},[62,2062,2063],{"class":64,"line":123},[62,2064,2065],{"class":68},"// 共通部分の抽出が難しい\n",[18,2067,2068,103],{},[21,2069,1424],{},[40,2071,2075],{"className":2072,"code":2073,"language":2074,"meta":49,"style":49},"language-vue shiki shiki-themes vitesse-light vitesse-light","\u003C!-- 共通のMessageBubbleコンポーネント -->\n\u003CMessageBubble\n  :text=\"message.text\"\n  :sender=\"message.sender\"\n  :theme=\"currentApp\"  \u003C!-- x | discord | imessage -->\n/>\n\n\u003C!-- アプリごとのスタイルはpropsで切り替え -->\n\u003Cstyle>\n.bubble[data-theme=\"x\"] { background: #1d9bf0; }\n.bubble[data-theme=\"discord\"] { background: #5865f2; }\n.bubble[data-theme=\"imessage\"] { background: #34c759; }\n\u003C/style>\n","vue",[47,2076,2077,2082,2089,2110,2129,2168,2173,2177,2182,2187,2192,2197,2202],{"__ignoreMap":49},[62,2078,2079],{"class":64,"line":65},[62,2080,2081],{"class":68},"\u003C!-- 共通のMessageBubbleコンポーネント -->\n",[62,2083,2084,2086],{"class":64,"line":72},[62,2085,1814],{"class":83},[62,2087,2088],{"class":195},"MessageBubble\n",[62,2090,2091,2094,2096,2098,2100,2103,2105,2107],{"class":64,"line":90},[62,2092,2093],{"class":83},"  :",[62,2095,45],{"class":722},[62,2097,1823],{"class":83},[62,2099,1826],{"class":83},[62,2101,2102],{"class":79},"message",[62,2104,742],{"class":83},[62,2106,45],{"class":79},[62,2108,2109],{"class":83},"\"\n",[62,2111,2112,2114,2117,2119,2121,2123,2125,2127],{"class":64,"line":96},[62,2113,2093],{"class":83},[62,2115,2116],{"class":722},"sender",[62,2118,1823],{"class":83},[62,2120,1826],{"class":83},[62,2122,2102],{"class":79},[62,2124,742],{"class":83},[62,2126,2116],{"class":79},[62,2128,2109],{"class":83},[62,2130,2131,2133,2136,2138,2140,2143,2145,2149,2152,2155,2158,2160,2163,2166],{"class":64,"line":123},[62,2132,2093],{"class":83},[62,2134,2135],{"class":722},"theme",[62,2137,1823],{"class":83},[62,2139,1826],{"class":83},[62,2141,2142],{"class":79},"currentApp",[62,2144,1826],{"class":83},[62,2146,2148],{"class":2147},"s5V_Q","  \u003C!--",[62,2150,2151],{"class":79}," x",[62,2153,2154],{"class":79}," |",[62,2156,2157],{"class":79}," discord",[62,2159,2154],{"class":79},[62,2161,2162],{"class":79}," imessage",[62,2164,2165],{"class":79}," --",[62,2167,1834],{"class":83},[62,2169,2170],{"class":64,"line":143},[62,2171,2172],{"class":1860},"/>\n",[62,2174,2175],{"class":64,"line":157},[62,2176,178],{"emptyLinePlaceholder":177},[62,2178,2179],{"class":64,"line":174},[62,2180,2181],{"class":1860},"\u003C!-- アプリごとのスタイルはpropsで切り替え -->\n",[62,2183,2184],{"class":64,"line":181},[62,2185,2186],{"class":1860},"\u003Cstyle>\n",[62,2188,2189],{"class":64,"line":187},[62,2190,2191],{"class":1860},".bubble[data-theme=\"x\"] { background: #1d9bf0; }\n",[62,2193,2194],{"class":64,"line":204},[62,2195,2196],{"class":1860},".bubble[data-theme=\"discord\"] { background: #5865f2; }\n",[62,2198,2199],{"class":64,"line":217},[62,2200,2201],{"class":1860},".bubble[data-theme=\"imessage\"] { background: #34c759; }\n",[62,2203,2204],{"class":64,"line":375},[62,2205,2206],{"class":1860},"\u003C/style>\n",[18,2208,2209,2211],{},[21,2210,1608],{},": 2つ以上のバリエーションを作る場合は移行を検討",[26,2213],{},[36,2215,2217],{"id":2216},"_4-フォームバリデーションの複雑化","4. フォーム・バリデーションの複雑化",[18,2219,2220,2222],{},[21,2221,1620],{},": シンプルな入力のみ",[40,2224,2226],{"className":56,"code":2225,"language":58,"meta":49,"style":49},"senderInput.addEventListener('input', (e) => {\n  state.sender = e.target.value || 'You'\n  renderPreview()\n})\n",[47,2227,2228,2259,2291,2298],{"__ignoreMap":49},[62,2229,2230,2233,2235,2237,2239,2241,2244,2246,2248,2250,2253,2255,2257],{"class":64,"line":65},[62,2231,2232],{"class":79},"senderInput",[62,2234,742],{"class":83},[62,2236,1656],{"class":722},[62,2238,726],{"class":83},[62,2240,114],{"class":106},[62,2242,2243],{"class":110},"input",[62,2245,114],{"class":106},[62,2247,117],{"class":83},[62,2249,1716],{"class":83},[62,2251,2252],{"class":79},"e",[62,2254,732],{"class":83},[62,2256,913],{"class":83},[62,2258,87],{"class":83},[62,2260,2261,2263,2265,2267,2269,2272,2274,2277,2279,2281,2284,2286,2288],{"class":64,"line":72},[62,2262,739],{"class":79},[62,2264,742],{"class":83},[62,2266,2116],{"class":79},[62,2268,84],{"class":83},[62,2270,2271],{"class":79}," e",[62,2273,742],{"class":83},[62,2275,2276],{"class":79},"target",[62,2278,742],{"class":83},[62,2280,923],{"class":79},[62,2282,2283],{"class":75}," ||",[62,2285,107],{"class":106},[62,2287,111],{"class":110},[62,2289,2290],{"class":106},"'\n",[62,2292,2293,2296],{"class":64,"line":90},[62,2294,2295],{"class":722},"  renderPreview",[62,2297,1737],{"class":83},[62,2299,2300],{"class":64,"line":96},[62,2301,1771],{"class":83},[18,2303,2304,103],{},[21,2305,1112],{},[1114,2307,2308,2311,2314],{},[1117,2309,2310],{},"課金フォーム（カード情報、住所）",[1117,2312,2313],{},"プロフィール編集（画像アップロード、複数フィールド）",[1117,2315,2316],{},"バリデーション（必須チェック、形式チェック）",[18,2318,2319,103],{},[21,2320,1636],{},[40,2322,2324],{"className":56,"code":2323,"language":58,"meta":49,"style":49},"// バリデーションロジックが散乱\nfunction validateEmail(email) { ... }\nfunction validateCard(card) { ... }\nfunction showError(field, message) { ... }\nfunction clearError(field) { ... }\n// 各フィールドにイベントリスナーを手動設定\n// エラー表示の更新も手動\n",[47,2325,2326,2331,2351,2371,2396,2415,2420],{"__ignoreMap":49},[62,2327,2328],{"class":64,"line":65},[62,2329,2330],{"class":68},"// バリデーションロジックが散乱\n",[62,2332,2333,2335,2338,2340,2343,2345,2347,2349],{"class":64,"line":72},[62,2334,719],{"class":75},[62,2336,2337],{"class":722}," validateEmail",[62,2339,726],{"class":83},[62,2341,2342],{"class":79},"email",[62,2344,732],{"class":83},[62,2346,339],{"class":83},[62,2348,2027],{"class":83},[62,2350,2030],{"class":83},[62,2352,2353,2355,2358,2360,2363,2365,2367,2369],{"class":64,"line":90},[62,2354,719],{"class":75},[62,2356,2357],{"class":722}," validateCard",[62,2359,726],{"class":83},[62,2361,2362],{"class":79},"card",[62,2364,732],{"class":83},[62,2366,339],{"class":83},[62,2368,2027],{"class":83},[62,2370,2030],{"class":83},[62,2372,2373,2375,2378,2380,2383,2385,2388,2390,2392,2394],{"class":64,"line":96},[62,2374,719],{"class":75},[62,2376,2377],{"class":722}," showError",[62,2379,726],{"class":83},[62,2381,2382],{"class":79},"field",[62,2384,117],{"class":83},[62,2386,2387],{"class":79}," message",[62,2389,732],{"class":83},[62,2391,339],{"class":83},[62,2393,2027],{"class":83},[62,2395,2030],{"class":83},[62,2397,2398,2400,2403,2405,2407,2409,2411,2413],{"class":64,"line":123},[62,2399,719],{"class":75},[62,2401,2402],{"class":722}," clearError",[62,2404,726],{"class":83},[62,2406,2382],{"class":79},[62,2408,732],{"class":83},[62,2410,339],{"class":83},[62,2412,2027],{"class":83},[62,2414,2030],{"class":83},[62,2416,2417],{"class":64,"line":143},[62,2418,2419],{"class":68},"// 各フィールドにイベントリスナーを手動設定\n",[62,2421,2422],{"class":64,"line":157},[62,2423,2424],{"class":68},"// エラー表示の更新も手動\n",[18,2426,2427,103],{},[21,2428,1424],{},[40,2430,2432],{"className":2072,"code":2431,"language":2074,"meta":49,"style":49},"\u003Ctemplate>\n  \u003Cform @submit.prevent=\"handleSubmit\">\n    \u003Cinput v-model=\"form.email\" :class=\"{ error: errors.email }\" />\n    \u003Cspan v-if=\"errors.email\">{{ errors.email }}\u003C/span>\n  \u003C/form>\n\u003C/template>\n\n\u003Cscript setup>\nconst { form, errors, validate } = useForm({\n  email: { required: true, email: true },\n  name: { required: true, minLength: 2 }\n})\n\u003C/script>\n",[47,2433,2434,2443,2464,2498,2527,2536,2544,2548,2560,2590,2617,2644,2648],{"__ignoreMap":49},[62,2435,2436,2438,2441],{"class":64,"line":65},[62,2437,1814],{"class":83},[62,2439,2440],{"class":195},"template",[62,2442,1834],{"class":83},[62,2444,2445,2447,2450,2453,2455,2457,2460,2462],{"class":64,"line":72},[62,2446,1839],{"class":83},[62,2448,2449],{"class":195},"form",[62,2451,2452],{"class":79}," @submit.prevent",[62,2454,1823],{"class":83},[62,2456,1826],{"class":106},[62,2458,2459],{"class":110},"handleSubmit",[62,2461,1826],{"class":106},[62,2463,1834],{"class":83},[62,2465,2466,2469,2471,2474,2476,2478,2481,2483,2486,2488,2490,2493,2495],{"class":64,"line":90},[62,2467,2468],{"class":83},"    \u003C",[62,2470,2243],{"class":195},[62,2472,2473],{"class":79}," v-model",[62,2475,1823],{"class":83},[62,2477,1826],{"class":106},[62,2479,2480],{"class":110},"form.email",[62,2482,1826],{"class":106},[62,2484,2485],{"class":79}," :class",[62,2487,1823],{"class":83},[62,2489,1826],{"class":106},[62,2491,2492],{"class":110},"{ error: errors.email }",[62,2494,1826],{"class":106},[62,2496,2497],{"class":83}," />\n",[62,2499,2500,2502,2504,2507,2509,2511,2514,2516,2518,2521,2523,2525],{"class":64,"line":96},[62,2501,2468],{"class":83},[62,2503,62],{"class":195},[62,2505,2506],{"class":79}," v-if",[62,2508,1823],{"class":83},[62,2510,1826],{"class":106},[62,2512,2513],{"class":110},"errors.email",[62,2515,1826],{"class":106},[62,2517,1857],{"class":83},[62,2519,2520],{"class":1860},"{{ errors.email }}",[62,2522,1864],{"class":83},[62,2524,62],{"class":195},[62,2526,1834],{"class":83},[62,2528,2529,2532,2534],{"class":64,"line":123},[62,2530,2531],{"class":83},"  \u003C/",[62,2533,2449],{"class":195},[62,2535,1834],{"class":83},[62,2537,2538,2540,2542],{"class":64,"line":143},[62,2539,1864],{"class":83},[62,2541,2440],{"class":195},[62,2543,1834],{"class":83},[62,2545,2546],{"class":64,"line":157},[62,2547,178],{"emptyLinePlaceholder":177},[62,2549,2550,2552,2555,2558],{"class":64,"line":174},[62,2551,1814],{"class":83},[62,2553,2554],{"class":195},"script",[62,2556,2557],{"class":79}," setup",[62,2559,1834],{"class":83},[62,2561,2562,2564,2566,2569,2571,2574,2576,2579,2582,2584,2587],{"class":64,"line":181},[62,2563,76],{"class":75},[62,2565,339],{"class":83},[62,2567,2568],{"class":79}," form",[62,2570,117],{"class":83},[62,2572,2573],{"class":79}," errors",[62,2575,117],{"class":83},[62,2577,2578],{"class":79}," validate",[62,2580,2581],{"class":83}," }",[62,2583,84],{"class":83},[62,2585,2586],{"class":722}," useForm",[62,2588,2589],{"class":83},"({\n",[62,2591,2592,2595,2597,2599,2602,2604,2607,2609,2611,2613,2615],{"class":64,"line":187},[62,2593,2594],{"class":99},"  email",[62,2596,103],{"class":83},[62,2598,339],{"class":83},[62,2600,2601],{"class":99}," required",[62,2603,103],{"class":83},[62,2605,2606],{"class":195}," true",[62,2608,117],{"class":83},[62,2610,347],{"class":99},[62,2612,103],{"class":83},[62,2614,2606],{"class":195},[62,2616,1210],{"class":83},[62,2618,2619,2622,2624,2626,2628,2630,2632,2634,2637,2639,2642],{"class":64,"line":204},[62,2620,2621],{"class":99},"  name",[62,2623,103],{"class":83},[62,2625,339],{"class":83},[62,2627,2601],{"class":99},[62,2629,103],{"class":83},[62,2631,2606],{"class":195},[62,2633,117],{"class":83},[62,2635,2636],{"class":99}," minLength",[62,2638,103],{"class":83},[62,2640,2641],{"class":165}," 2",[62,2643,2030],{"class":83},[62,2645,2646],{"class":64,"line":217},[62,2647,1771],{"class":83},[62,2649,2650,2652,2654],{"class":64,"line":375},[62,2651,1864],{"class":83},[62,2653,2554],{"class":195},[62,2655,1834],{"class":83},[18,2657,2658,2660],{},[21,2659,1608],{},": 入力フィールドが10個以上、またはバリデーションが必要な場合",[26,2662],{},[36,2664,2666],{"id":2665},"_5-テスト容易性","5. テスト容易性",[18,2668,2669,2671],{},[21,2670,1620],{},": テストなし（小規模なら問題ない）",[18,2673,2674,103],{},[21,2675,1112],{},[1114,2677,2678,2681,2684],{},[1117,2679,2680],{},"課金機能の導入（お金が絡むのでバグは致命的）",[1117,2682,2683],{},"チーム開発（他人のコード変更で壊れないか確認）",[1117,2685,2686],{},"継続的な機能追加",[18,2688,2689,103],{},[21,2690,2691],{},"Vanilla JSのテスト",[40,2693,2695],{"className":56,"code":2694,"language":58,"meta":49,"style":49},"// DOM操作のテストは困難\ntest('メッセージ追加', () => {\n  // DOMをセットアップ\n  document.body.innerHTML = `\u003Cdiv id=\"messages-list\">\u003C/div>`\n  // グローバル変数を初期化\n  state.messages = []\n  // 関数を呼び出し\n  addMessage()\n  // DOMを検証\n  expect(document.querySelectorAll('.message-item').length).toBe(1)\n})\n",[47,2696,2697,2702,2724,2729,2755,2760,2774,2779,2786,2791,2832],{"__ignoreMap":49},[62,2698,2699],{"class":64,"line":65},[62,2700,2701],{"class":68},"// DOM操作のテストは困難\n",[62,2703,2704,2707,2709,2711,2714,2716,2718,2720,2722],{"class":64,"line":72},[62,2705,2706],{"class":722},"test",[62,2708,726],{"class":83},[62,2710,114],{"class":106},[62,2712,2713],{"class":110},"メッセージ追加",[62,2715,114],{"class":106},[62,2717,117],{"class":83},[62,2719,1450],{"class":83},[62,2721,913],{"class":83},[62,2723,87],{"class":83},[62,2725,2726],{"class":64,"line":90},[62,2727,2728],{"class":68},"  // DOMをセットアップ\n",[62,2730,2731,2734,2736,2739,2741,2744,2746,2749,2752],{"class":64,"line":96},[62,2732,2733],{"class":79},"  document",[62,2735,742],{"class":83},[62,2737,2738],{"class":79},"body",[62,2740,742],{"class":83},[62,2742,2743],{"class":79},"innerHTML",[62,2745,84],{"class":83},[62,2747,2748],{"class":106}," `",[62,2750,2751],{"class":110},"\u003Cdiv id=\"messages-list\">\u003C/div>",[62,2753,2754],{"class":106},"`\n",[62,2756,2757],{"class":64,"line":123},[62,2758,2759],{"class":68},"  // グローバル変数を初期化\n",[62,2761,2762,2764,2766,2769,2771],{"class":64,"line":143},[62,2763,739],{"class":79},[62,2765,742],{"class":83},[62,2767,2768],{"class":79},"messages",[62,2770,84],{"class":83},[62,2772,2773],{"class":83}," []\n",[62,2775,2776],{"class":64,"line":157},[62,2777,2778],{"class":68},"  // 関数を呼び出し\n",[62,2780,2781,2784],{"class":64,"line":174},[62,2782,2783],{"class":722},"  addMessage",[62,2785,1737],{"class":83},[62,2787,2788],{"class":64,"line":181},[62,2789,2790],{"class":68},"  // DOMを検証\n",[62,2792,2793,2796,2798,2801,2803,2806,2808,2810,2813,2815,2818,2821,2823,2826,2828,2830],{"class":64,"line":187},[62,2794,2795],{"class":722},"  expect",[62,2797,726],{"class":83},[62,2799,2800],{"class":79},"document",[62,2802,742],{"class":83},[62,2804,2805],{"class":722},"querySelectorAll",[62,2807,726],{"class":83},[62,2809,114],{"class":106},[62,2811,2812],{"class":110},".message-item",[62,2814,114],{"class":106},[62,2816,2817],{"class":83},").",[62,2819,2820],{"class":99},"length",[62,2822,2817],{"class":83},[62,2824,2825],{"class":722},"toBe",[62,2827,726],{"class":83},[62,2829,1706],{"class":165},[62,2831,896],{"class":83},[62,2833,2834],{"class":64,"line":204},[62,2835,1771],{"class":83},[18,2837,2838,103],{},[21,2839,2840],{},"Vue.jsのテスト",[40,2842,2844],{"className":867,"code":2843,"language":869,"meta":49,"style":49},"// コンポーネント単位でテスト可能\nimport { mount } from '@vue/test-utils'\nimport MessageEditor from './MessageEditor.vue'\n\ntest('メッセージ追加', async () => {\n  const wrapper = mount(MessageEditor, {\n    props: { messages: [] }\n  })\n  await wrapper.find('.add-btn').trigger('click')\n  expect(wrapper.emitted('add')).toBeTruthy()\n})\n",[47,2845,2846,2851,2873,2889,2893,2916,2936,2949,2954,2992,3022],{"__ignoreMap":49},[62,2847,2848],{"class":64,"line":65},[62,2849,2850],{"class":68},"// コンポーネント単位でテスト可能\n",[62,2852,2853,2856,2858,2861,2863,2866,2868,2871],{"class":64,"line":72},[62,2854,2855],{"class":195},"import",[62,2857,339],{"class":83},[62,2859,2860],{"class":79}," mount",[62,2862,2581],{"class":83},[62,2864,2865],{"class":195}," from",[62,2867,107],{"class":106},[62,2869,2870],{"class":110},"@vue/test-utils",[62,2872,2290],{"class":106},[62,2874,2875,2877,2880,2882,2884,2887],{"class":64,"line":90},[62,2876,2855],{"class":195},[62,2878,2879],{"class":79}," MessageEditor",[62,2881,2865],{"class":195},[62,2883,107],{"class":106},[62,2885,2886],{"class":110},"./MessageEditor.vue",[62,2888,2290],{"class":106},[62,2890,2891],{"class":64,"line":96},[62,2892,178],{"emptyLinePlaceholder":177},[62,2894,2895,2897,2899,2901,2903,2905,2907,2910,2912,2914],{"class":64,"line":123},[62,2896,2706],{"class":722},[62,2898,726],{"class":83},[62,2900,114],{"class":106},[62,2902,2713],{"class":110},[62,2904,114],{"class":106},[62,2906,117],{"class":83},[62,2908,2909],{"class":75}," async",[62,2911,1450],{"class":83},[62,2913,913],{"class":83},[62,2915,87],{"class":83},[62,2917,2918,2920,2923,2925,2927,2929,2932,2934],{"class":64,"line":143},[62,2919,1459],{"class":75},[62,2921,2922],{"class":79},"wrapper",[62,2924,84],{"class":83},[62,2926,2860],{"class":722},[62,2928,726],{"class":83},[62,2930,2931],{"class":79},"MessageEditor",[62,2933,117],{"class":83},[62,2935,87],{"class":83},[62,2937,2938,2941,2944,2946],{"class":64,"line":157},[62,2939,2940],{"class":99},"    props",[62,2942,2943],{"class":83},": { ",[62,2945,2768],{"class":99},[62,2947,2948],{"class":83},": [] }\n",[62,2950,2951],{"class":64,"line":174},[62,2952,2953],{"class":83},"  })\n",[62,2955,2956,2959,2962,2964,2967,2969,2971,2974,2976,2978,2981,2983,2985,2988,2990],{"class":64,"line":181},[62,2957,2958],{"class":195},"  await",[62,2960,2961],{"class":79}," wrapper",[62,2963,742],{"class":83},[62,2965,2966],{"class":722},"find",[62,2968,726],{"class":83},[62,2970,114],{"class":106},[62,2972,2973],{"class":110},".add-btn",[62,2975,114],{"class":106},[62,2977,2817],{"class":83},[62,2979,2980],{"class":722},"trigger",[62,2982,726],{"class":83},[62,2984,114],{"class":106},[62,2986,2987],{"class":110},"click",[62,2989,114],{"class":106},[62,2991,896],{"class":83},[62,2993,2994,2996,2998,3000,3002,3005,3007,3009,3012,3014,3017,3020],{"class":64,"line":187},[62,2995,2795],{"class":722},[62,2997,726],{"class":83},[62,2999,2922],{"class":79},[62,3001,742],{"class":83},[62,3003,3004],{"class":722},"emitted",[62,3006,726],{"class":83},[62,3008,114],{"class":106},[62,3010,3011],{"class":110},"add",[62,3013,114],{"class":106},[62,3015,3016],{"class":83},")).",[62,3018,3019],{"class":722},"toBeTruthy",[62,3021,1737],{"class":83},[62,3023,3024],{"class":64,"line":204},[62,3025,1771],{"class":83},[18,3027,3028,3030],{},[21,3029,1608],{},": 課金機能導入時、またはチーム開発時",[26,3032],{},[36,3034,3036],{"id":3035},"_6-既存インフラの活用","6. 既存インフラの活用",[18,3038,3039,103],{},[21,3040,3041],{},"現状のプロジェクト構成",[40,3043,3046],{"className":3044,"code":3045,"language":45},[43],"mdx-playground/\n├── apps/web/           # Nuxt 3 プロジェクト（既存）\n│   ├── nuxt.config.ts\n│   ├── wrangler.toml   # Cloudflare設定済み\n│   └── content/        # コンテンツ\n└── ...\n",[47,3047,3045],{"__ignoreMap":49},[18,3049,3050,103],{},[21,3051,3052],{},"Vanilla JS版のデプロイ",[1114,3054,3055,3058,3061],{},[1117,3056,3057],{},"別プロジェクトとして管理",[1117,3059,3060],{},"別のCloudflare Pages設定",[1117,3062,3063],{},"別のドメイン/サブドメイン",[18,3065,3066,103],{},[21,3067,3068],{},"Vue.js版（Nuxtに統合）",[40,3070,3073],{"className":3071,"code":3072,"language":45},[43],"apps/web/\n├── app/pages/\n│   ├── tools/\n│   │   └── message-mockup.vue  # ここに追加\n│   └── ...\n└── wrangler.toml  # 既存の設定を流用\n",[47,3074,3072],{"__ignoreMap":49},[18,3076,3077,103],{},[21,3078,3079],{},"メリット",[1114,3081,3082,3085,3088,3091],{},[1117,3083,3084],{},"認証システムの共有",[1117,3086,3087],{},"API Routesの共有",[1117,3089,3090],{},"デプロイパイプラインの統一",[1117,3092,3093,3094,3097],{},"ドメインの統一（",[47,3095,3096],{},"yoursite.com/tools/message-mockup","）",[18,3099,3100,3102],{},[21,3101,1608],{},": 既存のNuxtプロジェクトに他のツール/機能がある場合",[26,3104],{},[14,3106,3107],{"id":3107},"判断フローチャート",[40,3109,3112],{"className":3110,"code":3111,"language":45},[43],"サービス化したい\n    │\n    ▼\nログイン・課金機能が必要？\n    │\n    ├─ No → Vanilla JSのままCloudflare Pagesへ\n    │\n    ▼ Yes\n    │\n画面は3つ以上必要？（ダッシュボード、履歴、設定など）\n    │\n    ├─ No → Vanilla JS + Supabase/Firebase Auth\n    │        （認証SDKだけ追加）\n    │\n    ▼ Yes\n    │\n複数のモックアップ種類を作る？（Discord, iMessage等）\n    │\n    ├─ No → まだVanilla JSで頑張れる\n    │        （ただし保守コストは上がる）\n    │\n    ▼ Yes\n    │\nVue.js移行を推奨\n",[47,3113,3111],{"__ignoreMap":49},[26,3115],{},[14,3117,3118],{"id":3118},"現実的なアドバイス",[36,3120,3122],{"id":3121},"今すぐやることvanilla-js版","今すぐやること（Vanilla JS版）",[40,3124,3128],{"className":3125,"code":3126,"language":3127,"meta":49,"style":49},"language-bash shiki shiki-themes vitesse-light vitesse-light","# 1. Cloudflare Pagesにそのままデプロイ\nwrangler pages deploy ./public\n\n# 2. 認証はSupabaseを使う（SDK軽い）\nnpm install @supabase/supabase-js\n\n# 3. StripeはCloudflare Functionsで\n# functions/api/checkout.js を作成\n","bash",[47,3129,3130,3135,3149,3153,3158,3169,3173,3178],{"__ignoreMap":49},[62,3131,3132],{"class":64,"line":65},[62,3133,3134],{"class":68},"# 1. Cloudflare Pagesにそのままデプロイ\n",[62,3136,3137,3140,3143,3146],{"class":64,"line":72},[62,3138,3139],{"class":722},"wrangler",[62,3141,3142],{"class":110}," pages",[62,3144,3145],{"class":110}," deploy",[62,3147,3148],{"class":110}," ./public\n",[62,3150,3151],{"class":64,"line":90},[62,3152,178],{"emptyLinePlaceholder":177},[62,3154,3155],{"class":64,"line":96},[62,3156,3157],{"class":68},"# 2. 認証はSupabaseを使う（SDK軽い）\n",[62,3159,3160,3163,3166],{"class":64,"line":123},[62,3161,3162],{"class":722},"npm",[62,3164,3165],{"class":110}," install",[62,3167,3168],{"class":110}," @supabase/supabase-js\n",[62,3170,3171],{"class":64,"line":143},[62,3172,178],{"emptyLinePlaceholder":177},[62,3174,3175],{"class":64,"line":157},[62,3176,3177],{"class":68},"# 3. StripeはCloudflare Functionsで\n",[62,3179,3180],{"class":64,"line":174},[62,3181,3182],{"class":68},"# functions/api/checkout.js を作成\n",[36,3184,3185],{"id":3185},"移行のトリガー",[18,3187,3188],{},"以下のいずれかに該当したら、Vue.js移行を計画:",[3190,3191,3192,3198,3204,3210,3216],"ol",{},[1117,3193,3194,3197],{},[21,3195,3196],{},"状態が10個以上"," になった",[1117,3199,3200,3203],{},[21,3201,3202],{},"画面が3つ以上"," 必要になった",[1117,3205,3206,3209],{},[21,3207,3208],{},"Discord/iMessage版"," も作ることになった",[1117,3211,3212,3215],{},[21,3213,3214],{},"チームメンバー"," が増えた",[1117,3217,3218,3221],{},[21,3219,3220],{},"バグ修正が困難"," になった（どこで何が起きてるかわからない）",[36,3223,3224],{"id":3224},"移行コスト目安",[953,3226,3227,3237],{},[956,3228,3229],{},[959,3230,3231,3234],{},[962,3232,3233],{},"項目",[962,3235,3236],{},"工数",[972,3238,3239,3247,3255,3262,3269,3277],{},[959,3240,3241,3244],{},[977,3242,3243],{},"基本構造の移行",[977,3245,3246],{},"1-2日",[959,3248,3249,3252],{},[977,3250,3251],{},"日時ピッカー",[977,3253,3254],{},"0.5日",[959,3256,3257,3260],{},[977,3258,3259],{},"アニメーション",[977,3261,3254],{},[959,3263,3264,3267],{},[977,3265,3266],{},"動画エクスポート",[977,3268,3246],{},[959,3270,3271,3274],{},[977,3272,3273],{},"テスト",[977,3275,3276],{},"1日",[959,3278,3279,3284],{},[977,3280,3281],{},[21,3282,3283],{},"合計",[977,3285,3286],{},[21,3287,3288],{},"4-6日",[26,3290],{},[14,3292,3293],{"id":3293},"まとめ",[953,3295,3296,3309],{},[956,3297,3298],{},[959,3299,3300,3303,3306],{},[962,3301,3302],{},"観点",[962,3304,3305],{},"Vanilla JS",[962,3307,3308],{},"Vue.js",[972,3310,3311,3322,3333,3344,3355,3364,3375],{},[959,3312,3313,3316,3319],{},[977,3314,3315],{},"初期開発速度",[977,3317,3318],{},"✅ 速い",[977,3320,3321],{},"△ 設計必要",[959,3323,3324,3327,3330],{},[977,3325,3326],{},"保守性（小規模）",[977,3328,3329],{},"✅ 問題なし",[977,3331,3332],{},"△ オーバーキル",[959,3334,3335,3338,3341],{},[977,3336,3337],{},"保守性（大規模）",[977,3339,3340],{},"❌ 困難",[977,3342,3343],{},"✅ 良好",[959,3345,3346,3349,3352],{},[977,3347,3348],{},"状態管理",[977,3350,3351],{},"△ 手動同期",[977,3353,3354],{},"✅ 自動同期",[959,3356,3357,3359,3361],{},[977,3358,3273],{},[977,3360,3340],{},[977,3362,3363],{},"✅ 容易",[959,3365,3366,3369,3372],{},[977,3367,3368],{},"再利用性",[977,3370,3371],{},"❌ コピペ",[977,3373,3374],{},"✅ コンポーネント",[959,3376,3377,3380,3383],{},[977,3378,3379],{},"学習コスト",[977,3381,3382],{},"✅ なし",[977,3384,3385],{},"△ あり",[18,3387,3388,3390],{},[21,3389,970],{},": まずVanilla JSでリリースし、上記トリガーに該当したら移行を検討。",[3392,3393,3394],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}html pre.shiki code .s5V_Q, html code.shiki .s5V_Q{--shiki-default:#999999;--shiki-default-font-style:italic;--shiki-dark:#999999;--shiki-dark-font-style:italic}",{"title":49,"searchDepth":72,"depth":72,"links":3396},[3397,3398,3406,3414,3415,3420],{"id":16,"depth":72,"text":16},{"id":30,"depth":72,"text":31,"children":3399},[3400,3401,3402,3403,3404,3405],{"id":38,"depth":90,"text":38},{"id":52,"depth":90,"text":53},{"id":229,"depth":90,"text":229},{"id":704,"depth":90,"text":704},{"id":863,"depth":90,"text":864},{"id":950,"depth":90,"text":951},{"id":1023,"depth":72,"text":1024,"children":3407},[3408,3409,3410,3411,3412,3413],{"id":1027,"depth":90,"text":1028},{"id":1614,"depth":90,"text":1615},{"id":1791,"depth":90,"text":1792},{"id":2216,"depth":90,"text":2217},{"id":2665,"depth":90,"text":2666},{"id":3035,"depth":90,"text":3036},{"id":3107,"depth":72,"text":3107},{"id":3118,"depth":72,"text":3118,"children":3416},[3417,3418,3419],{"id":3121,"depth":90,"text":3122},{"id":3185,"depth":90,"text":3185},{"id":3224,"depth":90,"text":3224},{"id":3293,"depth":72,"text":3293},null,"Vanilla JSからVue.jsへの移行が必要になるケースと判断基準、Cloudflare Pagesでのサービス化ガイド","md",{},"/message-mockup-migration-plan",false,"2025-12-03T00:00:00.000Z",{"title":5,"description":3422},"2025-12/2025-12-03/message-mockup-migration-plan",[3308,3431,3432,3433],"Cloudflare","Architecture","Decision","ki_Vwu2n0a3oGdr80KtU4L7wxg4UEX2631KCzxYOXaU",[],[3437,3441,3445,3449,3452],{"title":3438,"path":3439,"publishedAt":3440},"Nuxt 3のapp/dataとpublicディレクトリの使い分け","/nuxt-data-directory-guide","2025-12-24T00:00:00.000Z",{"title":3442,"path":3443,"publishedAt":3444},"Nuxt Content v3：D1データベースは本当に必要なのか？","/nuxt-content-d1-not-used","2025-12-05T00:00:00.000Z",{"title":3446,"path":3447,"publishedAt":3448},"2026年7月27日の開発日記 - 画像3,810件の配信復旧とHeroku停止の実測","/2026-07-27-diary","2026-07-27T00:00:00.000Z",{"title":3450,"path":3451,"publishedAt":3448},"Notion 9,375ページの移行先を設計し直す — 原本243MBの抜けと、Markdown→Googleドキュメント変換の実測","/notion-archive-migration-google-docs",{"title":3453,"path":3454,"publishedAt":3455},"2026年7月26日の開発日記 - WordPressサイトをついに本番切替した日","/2026-07-26-diary","2026-07-26T00:00:00.000Z","https://log.eurekapu.com/og/blog/message-mockup-migration-plan.png?v=2025-12-03T00%3A00%3A00.000Z&title=Message%20Mockup%20Tool%20-%20Vue.js%E7%A7%BB%E8%A1%8C%E3%81%AE%E5%BF%85%E8%A6%81%E6%80%A7%E5%88%86%E6%9E%90&author=Kei%20Komatsu&sig=6e4c4538508d9206",1785654671325]