{"id":12349,"date":"2026-10-17T09:50:37","date_gmt":"2026-10-17T00:50:37","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=12349"},"modified":"2026-10-17T18:31:04","modified_gmt":"2026-10-17T09:31:04","slug":"nextjs-sns-server-51-6","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/10\/17\/nextjs-sns-server-51-6\/","title":{"rendered":"[nextjs]SNS Server-51(myapp61+)[boost(announce)]\u00a0"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb \uc774\ubc88\uc5d0\ub294 \ubd80\uc2a4\ud2b8\uc758 \uad6c\uc870\uc5d0 \ub300\ud574\uc11c \uc0b4\ud3b4\ubd05\ub2c8\ub2e4.<br>This time, we will take a look at the structure of Boost.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc774\uc804\uc758 \uc88b\uc544\uc694 \ubd80\ubd84\uc758 \ub85c\uc9c1\uacfc \ub9e4\uc6b0 \uc720\uc0ac\ud558\uace0 \ucf54\ub4dc\uc758 \uc0ac\uc6a9\uc704\uce58\ub3c4 \uac70\uc758 \ub3d9\uc77c\ud569\ub2c8\ub2e4.<br>The logic is very similar to that of the previous &#8216;like&#8217; feature, and the code is used in almost the same place.<\/p>\n\n\n\n<p>\ud83d\udcc1 ClientPage.tsx<\/p>\n\n\n\n<p>\u2714\ufe0f app\/usersui\/[username]\/ClientPage.tsx<\/p>\n\n\n\n<p>\u2714\ufe0f \ubd80\uc2a4\ud2b8 \ubd80\ubd84\uc740 \uc88b\uc544\uc694 \ubd80\ubd84\uc758 \uae30\ub2a5\uacfc \ub9e4\uc6b0 \uc720\uc0ac\ud569\ub2c8\ub2e4.<br>The &#8220;Boost&#8221; feature is very similar in functionality to the &#8220;Like&#8221; feature.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>... ...\n  const handleBoost = async (post: any) =&gt; {\n    const latest = timelineRef.current.find((p: any) =&gt; p.id === post.id) || post;\n    let target = latest.original_id || latest.id;\n\n    if (!target.startsWith('http')) {\n      if (latest.actor &amp;&amp; latest.actor.startsWith('http')) {\n        try {\n          const actorUrl = new URL(latest.actor);\n          const parts = actorUrl.pathname.split('\/');\n          const usersIdx = parts.indexOf('users');\n          const actorUsername = usersIdx!== -1? parts&#91;usersIdx + 1] : latest.username;\n          target = `${actorUrl.origin}\/users\/${actorUsername}\/statuses\/${target}`;\n        } catch {\n          target = `https:\/\/${process.env.NEXT_PUBLIC_DOMAIN || 'aloy-horizon.duckdns.org'}\/users\/${latest.username}\/statuses\/${target}`;\n        }\n      } else {\n        target = `https:\/\/${process.env.NEXT_PUBLIC_DOMAIN || 'aloy-horizon.duckdns.org'}\/users\/${latest.username}\/statuses\/${target}`;\n      }\n    }\n\n    const currentlyBoosted = latest.isMyBoost;\n    console.log('\ud83d\udd01 boost click', { target, isMyBoost: currentlyBoosted });\n\n    \/\/ Optimistic\n    setTimeline(prev =&gt; prev.map((p: any) =&gt;\n      p.id === latest.id\n      ? {...p, isMyBoost:!currentlyBoosted, boostCount: (p.boostCount || 0) + (currentlyBoosted? -1 : 1) }\n        : p\n    ));\n\n    try {\n      const res = await fetch(`\/api\/announce`, {\n        method: currentlyBoosted? 'DELETE' : 'POST',\n        headers: { 'Content-Type': 'application\/json' },\n        body: JSON.stringify({ username, target }),\n        cache: 'no-store'\n      });\n      const data = await res.json();\n      console.log('\u2705 announce res', data);\n\n      \/\/ \u2705 \uc11c\ubc84 \uc9c4\uc2e4\uac12\uc73c\ub85c \ub36e\uc5b4\uc4f0\uae30\n      \/\/ Overwrite with server truth values\n      setTimeline(prev =&gt; prev.map((p: any) =&gt;\n        p.id === latest.id\n        ? {...p, isMyBoost: data.isMyBoost, boostCount: data.boostCount }\n          : p\n      ));\n\n      if (data.alreadyBoosted || data.alreadyUnboosted) {\n        await refreshTimeline();\n      }\n\n    } catch (e) {\n      console.error('\u274c boost \uc2e4\ud328\/boost failed', e);\n      await refreshTimeline();\n    }\n  };\n... ...<\/code><\/pre>\n\n\n\n<p>1)\uc774 handleboost\ud568\uc218\uac00 TimelineList.tsx\uc5d0 \uc804\ub2ec\ub418\uace0 TimelineList.tsx\uc5d0\uc11c \ubd80\uc2a4\ud2b8 \ubc84\ud2bc\uc744 \ub204\ub974\uba74 \uc774 handleboost\ud568\uc218\uac00 \uc2e4\ud589\ub429\ub2c8\ub2e4.<br>The <code>handleboost<\/code> function is passed to <code>TimelineList.tsx<\/code>, and when the boost button is pressed within <code>TimelineList.tsx<\/code>, this <code>handleboost<\/code> function is executed.<\/p>\n\n\n\n<p>2)target\uc740 API\uc5d0 \uc694\uccad\ud560 \uc804\uccb4 URL\uc744 \ub9cc\ub4ed\ub2c8\ub2e4.<br>&#8216;target&#8217; constructs the full URL for the API request.<\/p>\n\n\n\n<p>3)\ucf54\ub4dc\uc544\ub798\uc5d0 \ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\uc740 \ubc29\uc2dd\uc73c\ub85c target\uacfc username\uc744 API\uc5d0 \uc804\uc1a1\ud569\ub2c8\ub2e4.<br>Below the code, you can see how the <code>target<\/code> and <code>username<\/code> are sent to the API.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>      const res = await <strong>fetch(`\/api\/announce<\/strong>`, {\n        method: currentlyBoosted? 'DELETE' : 'POST',\n        headers: { 'Content-Type': 'application\/json' },\n        <strong>body: JSON.stringify({ username, target }),<\/strong>\n        cache: 'no-store'\n      });<\/code><\/pre>\n\n\n\n<p>4)actorUrl\uc740 \ub2e4\uc74c\uacfc \uac19\uc740 \uacfc\uc815\uc744 \uac70\uce69\ub2c8\ub2e4.<br>actorUrl goes through the following process:<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>\n1.actorURL\uc5d0\uc11c pathname\ub9cc \uc120\ud0dd\ud569\ub2c8\ub2e4.\nSelect only the pathname from actorURL.\n\nconst actorUrl = new URL(latest.actor); \n\n\/\/ actorUrl.origin = \"https:\/\/mastodon.social\"\n\/\/ actorUrl.pathname = \"\/users\/bob\" \n\n2.\uadf8\ub798\uc11c \uc544\ub798\ucc98\ub7fc \/ \uae30\uc900\uc73c\ub85c \uac12\uc744 \ubd84\ub9ac\ud558\uace0\nSo, as shown below, split the values \u200b\u200bbased on the \/ character.\n\nconst parts = actorUrl.pathname.split('\/');\n\/\/ parts = &#91;\"\", \"users\", \"bob\"]\n\/\/          0    1       2\n\n3.\ubc30\uc5f4\uc5d0\uc11c users\ub77c\ub294 \ubb38\uc790\uac00 \ud3ec\ud568\ub41c \uc778\ub371\uc2a4\ub97c \ubf51\uace0\nExtract the indices from the array that contain the string \"users\"\n\nconst usersIdx = parts.indexOf('users');\n\/\/ usersIdx = 1\n\n4. \ubc30\uc5f4\uc774 -1\uc774 \uc544\ub2c8\uba74 actorUsername\uc5d0 \ubc30\uc5f4\uac12\uc911&#91;usersIdx + 1]\ub97c \ub300\uc785\ud569\ub2c8\ub2e4.\nIf the array is not -1, assign the value at index `usersIdx + 1` from the array to `actorUsername`.\n\nconst actorUsername = usersIdx !== -1 ? parts&#91;usersIdx + 1]\n\/\/ parts&#91;1 + 1] = parts&#91;2] = \"bob\"\n\/\/ actorUsername = \"bob\"\n\n5.URL\uac1d\uccb4 \uc815\ubcf4 \/ URL object information\nactorUrl.href     = \"https:\/\/mastodon.social\/users\/bob\"  \/\/ \uc804\uccb4 \/ entire\nactorUrl.protocol = \"https:\"                              \/\/ https: \ub9cc \ub530\ub85c \/ 'https:' only\nactorUrl.host     = \"mastodon.social\"                     \/\/ \ub3c4\uba54\uc778\ub9cc \ub530\ub85c \/ Domain only\nactorUrl.origin   = \"https:\/\/mastodon.social\"             \/\/ https + \ub3c4\uba54\uc778 \/ https + domain\nactorUrl.pathname = \"\/users\/bob\"                          \/\/ \uacbd\ub85c\ub9cc \ub530\ub85c \/ The path only, separately<\/code><\/pre>\n\n\n\n<p>5) \uc704\uc758 \uacfc\uc815\uc73c\ub85c \uc544\ub798\uc640 \uac19\uc740 fullURL\uc744 \ub9cc\ub4ed\ub2c8\ub2e4.<br>Through the process above, a fullURL like the one below is created.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>target = `${actorUrl.origin}\/users\/${actorUsername}\/statuses\/${target}`;<\/code><\/pre>\n\n\n\n<p>6) timeline\ub370\uc774\ud130\uc5d0\uc11c \ud574\ub2f9 \uae00\uc758 isMyBoost\uac12\uacfc boostCount\uac12\uc744 \ubc14\uafd4\uc11c TimelineList.tsx\uc5d0\uc11c \ubcf4\uc5ec\uc8fc\ub294 \uac12\ub4e4\uc744  \uba3c\uc800 \ubc14\uafc9\ub2c8\ub2c8\ub2e4.  \uc774 \ubd80\ubd84\uc740 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uac12\uc774 \ubc14\ub00c\uae30 \uc804\uc5d0 UI\ub97c \uba3c\uc800 \ubc14\uafd4\uc8fc\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.<br>\uc774\uac83\uc744 \ub099\uad00\uc801 \uc5c5\ub370\uc774\ud2b8\ub77c\uace0\ud569\ub2c8\ub2e4. prev\ub294 \uae30\uc874\uc758 timeline\uc5d0 \ub370\uc774\ud130\uc785\ub2c8\ub2e4.<br>First, update the <code>isMyBoost<\/code> and <code>boostCount<\/code> values \u200b\u200bfor the relevant post within the timeline data, which will in turn update the values \u200b\u200bdisplayed in <code>TimelineList.tsx<\/code>.<br>In this part, the UI is updated before the database value changes.<br>This is called an optimistic update.<br>&#8216;prev&#8217; represents the data from the existing timeline.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    setTimeline(prev =&gt; prev.map((p: any) =&gt;\n      p.id === latest.id\n      ? {...p, isMyBoost:!currentlyBoosted, boostCount: (p.boostCount || 0) + (currentlyBoosted? -1 : 1) }\n        : p\n    ));<\/code><\/pre>\n\n\n\n<p>7)\uc774 \ubd80\ubd84\uc5d0\uc11c \uc2e4\uc81c\ub85c API\uc640 \ud1b5\uc2e0\ud574\uc11c \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uac12\uc744 \ubc14\uafc9\ub2c8\ub2e4 \uadf8\ub9ac\uace0 \ud55c\ubc88\ub354 setTimeline()\uc73c\ub85c \ud0c0\uc784\ub77c\uc778 \ub370\uc774\ud130\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4. \ub099\uad00\uc801 \uc5c5\ub370\uc774\ud2b8 \ud6c4 \ud55c\ubc88\ub354 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \uc9c4\uc9dc \ub370\uc774\ud130\ub85c \uc5c5\ub370\uc774\ud2b8 \ud569\ub2c8\ub2e4.<br>At this stage, the application communicates with the API to modify values \u200b\u200bin the database, and then calls <code>setTimeline()<\/code> once more to update the timeline data. In other words, following an &#8220;optimistic update,&#8221; the data is refreshed again using the actual, authoritative data from the database.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>      const res = await fetch(`\/api\/announce`, {\n        method: currentlyBoosted? 'DELETE' : 'POST',\n        headers: { 'Content-Type': 'application\/json' },\n        body: JSON.stringify({ username, target }),\n        cache: 'no-store'\n      });\n      const data = await res.json();\n      console.log('\u2705 announce res', data);\n\n      \/\/ \u2705 \uc11c\ubc84 \uc9c4\uc2e4\uac12\uc73c\ub85c \ub36e\uc5b4\uc4f0\uae30\n      \/\/ Overwrite with server truth values\n      setTimeline(prev =&gt; prev.map((p: any) =&gt;\n        p.id === latest.id\n        ? {...p, isMyBoost: data.isMyBoost, boostCount: data.boostCount }\n          : p\n      ));<\/code><\/pre>\n\n\n\n<p>8)\uc704\uc758 \ubd80\uc2a4\ud2b8 \uacfc\uc815\uc774 \ub05d\ub0ac\uc73c\uba74 \ud0c0\uc784\ub77c\uc778 \uc804\uccb4\ub97c \ud55c\ubc88 \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.<br>Once the boost process described above is complete, update the entire timeline.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>      if (data.alreadyBoosted || data.alreadyUnboosted) {\n        await refreshTimeline();\n      }<\/code><\/pre>\n\n\n\n<p>9)TimelineList.tsx\uc758 \ubd80\uc2a4\ud2b8 \ubd80\ubd84 \ucf54\ub4dc\ub294 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4.<br>The code for the &#8216;boost&#8217; section in TimelineList.tsx is as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>              &lt;div className=\"flex gap-4 mt-2\"&gt;\n                &lt;button <strong>onClick={() =&gt; onBoost(p)<\/strong>} className={`bg-none border-none cursor-pointer text-&#91;#999] px-2 py-1 rounded-xl text-sm hover:bg-&#91;#f3f4f6] hover:text-&#91;#333] ${p.isMyBoost? '!text-&#91;#1b9c59] font-bold bg-&#91;#dcfce7]' : ''}`}&gt;\ud83d\udd01 {<strong>p.boostCount<\/strong> || ''}&lt;\/button&gt;\n... ...\n              &lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 API<\/p>\n\n\n\n<p>\u2714\ufe0f POST<\/p>\n\n\n\n<p>&#8212; \ubd80\uc2a4\ud2b8 \ud560 \uae00\uc774 \ub0b4 \uc11c\ubc84 \uae00\uc774 \uc544\ub2cc \uacbd\uc6b0<br>When the post you want to boost is not from your own server<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>... ... \n   if (!isOwnPost) {\n      try {\n        const pr = await signedFetch(target, username);\n        if (pr.ok) {\n          const d = await pr.json();\n          apObjectId = (d.id || target).replace('\/posts\/', '\/statuses\/');\n          const info = await getActorData((d.attributedTo || d.actor) as any, username);\n          inbox = info.inbox;\n        }\n      } catch {}\n    }\n... ...<\/code><\/pre>\n\n\n\n<p>1)\ub0b4 \uae00\uc774 \uc544\ub2c8\uba74 target \uc704\uce58\uc758 \uc0c1\ub300\ubc29 \uae00\uc758 \uc874\uc7ac\ud655\uc778(await signedFetch(target, username);)<br>If it is not my own post, verify the existence of the other person&#8217;s post at the target location (await signedFetch(target, username);)<\/p>\n\n\n\n<p>2)\uae00\uc774 \uc874\uc7ac\ud558\uba74(pr.ok)getActorData\ub85c \uc0c1\ub300\ubc29 inbox\uc815\ubcf4 \uac00\uc838\uc624\uae30<br>If the post exists (pr.ok), retrieve the other party&#8217;s inbox information using getActorData.<\/p>\n\n\n\n<p>&#8212; \uc774\uc804\uc5d0 \ubd80\uc2a4\ud2b8 \ud55c \uc801\uc774 \uc788\ub294\uc9c0 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uac80\uc0c9<br>Search the database to see if there has been a previous boost<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    const existing = db.prepare(\n      `SELECT id FROM announces WHERE username =? AND object LIKE '%' ||? || '%'`\n    ).get(username, shortId) as any;<\/code><\/pre>\n\n\n\n<p>1)shortId\uac00 \ud3ec\ud568\ub41c \uae00\uacfc username\uacfc \uc77c\uce58\ud558\ub294 id \uac80\uc0c9<br>Search for a post containing a <code>shortId<\/code> and an ID that matches the <code>username<\/code>.<\/p>\n\n\n\n<p>2)LIKE &#8216;%&#8217; ||? || &#8216;%&#8217;`<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>'%' = SQL \uc640\uc77c\ub4dc\uce74\ub4dc. \"\uc544\ubb34\uac70\ub098\"\n|| = \ubb38\uc790\uc5f4 \ubd99\uc774\uae30\n? = shortId\n\n'%' + shortId + '%' \ub294 shortId\uac00 \ud3ec\ud568\ub41c \uae00\uc774\ub77c\ub294 \uc758\ubbf8<\/code><\/pre>\n\n\n\n<p>&#8212; \ubd80\uc2a4\ud2b8\ud55c \uae00\uc774 \uc874\uc7ac\ud558\uba74 json\uac12 \ub9ac\ud134<br>If a boosted post exists, return a JSON value.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    if (existing) {\n      const boostCount = (db.prepare(\n        `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`\n      ).get(shortId) as any).c;\n      return NextResponse.json({ ok: true, alreadyBoosted: true, isMyBoost: true, boostCount });\n    }<\/code><\/pre>\n\n\n\n<p>1) \uc774\uc804\uc5d0 \ubd80\uc2a4\ud2b8 \ud55c\uc801\uc774 \uc788\ub2e4\uba74 \ub2e4\uc2dc \ubd80\uc2a4\ud2b8\ud558\uc9c0 \ubabb\ud558\uac8c \ud568.<br>Prevent a user from boosting again if they have already boosted previously.<\/p>\n\n\n\n<p>&#8212; \ubd80\uc2a4\ud2b8 \ubc30\ub2ec \/ Boost Delivery<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    let announceDoc;\n    if (isOwnPost) {\n      announceDoc = { id: `${myActor}\/announces\/${crypto.randomUUID()}`, actor: myActor, object: apObjectId };\n    } else {\n      if (!inbox) return NextResponse.json({ ok: false, error: 'no inbox' }, { status: 400 });\n      const r = await <strong>sendAnnounce(inbox, apObjectId, username);<\/strong>\n      announceDoc = r.announceDoc;\n    }\n\n\/\/ const myActor = `https:\/\/${DOMAIN}\/users\/${username}`;\n\/\/ let apObjectId = target.replace('\/posts\/', '\/statuses\/');<\/code><\/pre>\n\n\n\n<p>1)\ub0b4 \uc11c\ubc84 \uae00\uc774 \uc544\ub2c8\uba74 sendAnnounce\ud568\uc218\ub85c \ubd80\uc2a4\ud2b8 \ubc30\ub2ec<br>If the post is not from my own server, deliver a boost using the <code>sendAnnounce<\/code> function.<\/p>\n\n\n\n<p>2)announceDoc = {id: &#8230; }\uc5d0\uc11c id \ubd80\ubd84\uc758 \uacbd\ub85c\ub294 \uc2e4\uc81c\ub85c \uc874\uc7ac\ud558\uc9c0 \uc54a\uc74c \uadf8\ub0e5 \ubd80\uc2a4\ud2b8 \uc2dd\ubcc4\uc6a9 id\uc784.<br>In <code>announceDoc = {id: ... }<\/code>, the path represented by the <code>id<\/code> part does not actually exist; it serves merely as an identifier for the boost.<\/p>\n\n\n\n<p>3)\uc9c0\uae08\uc740 \uc804\ud600 \ubb38\uc81c \uc5c6\uc73c\ub098 \uc2e4\uc81c\ub85c \uc81c\ub300\ub85c \ud65c\uc6a9\ud558\ub824\uba74 \ub77c\uc6b0\ud2b8 \uc788\uc5b4\uc57c\ud568.<br>There are no issues at the moment, but to make proper use of it, you will need a router.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    db.prepare('INSERT OR IGNORE INTO announces (id, actor, object, username) VALUES (?,?,?,?)')\n     .run(announceDoc.id, myActor, announceDoc.object.replace('\/posts\/', '\/statuses\/'), username);\n\n    const boostCount = (db.prepare(\n      `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`\n    ).get(shortId) as any).c;\n\n    return NextResponse.json({ ok: true, isMyBoost: true, boostCount });<\/code><\/pre>\n\n\n\n<p>1) \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \ubd80\uc2a4\ud2b8 \uc800\uc7a5 \ubd80\uc2a4\ud2b8 \uce74\uc6b4\ud130 \uac80\uc0c9<br>Retrieve boost count from the database<\/p>\n\n\n\n<p>2)\uacb0\uacfc\uac12 json\ub370\uc774\ud130 \ub9ac\ud134<br>Return the result as JSON data<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{ ok: true, isMyBoost: true, boostCount }<\/code><\/pre>\n\n\n\n<ol class=\"wp-block-list\">\n<li><\/li>\n<\/ol>\n\n\n\n<p>\u2714\ufe0f DELETE<\/p>\n\n\n\n<p>&#8212; \ubd80\uc2a4\ud2b8 \uac80\uc0c9 \/ Boost Search<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    const myActor = `https:\/\/${DOMAIN}\/users\/${username}`;\n    const shortId = target.replace('\/posts\/', '\/statuses\/').split('?')&#91;0].split('#')&#91;0].split('\/').filter(Boolean).pop() || target;\n\n    const row = db.prepare(\n      `SELECT id, object, actor FROM announces WHERE username =? AND object LIKE '%' ||? || '%'`\n    ).get(username, shortId) as any;\n\n    if (!row) {\n      const boostCount = (db.prepare(\n        `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`\n      ).get(shortId) as any).c;\n      return NextResponse.json({ ok: true, alreadyUnboosted: true, isMyBoost: false, boostCount });\n    }<\/code><\/pre>\n\n\n\n<p>1)username\uacfc shortId\uac00 \uc77c\uce58\ud558\ub294 \ubd80\uc2a4\ud2b8\uac00 \uc874\uc7ac\ud558\ub294\uc9c0 \uac80\uc0c9<br>Search for the existence of a boost that matches both the username and shortId.<\/p>\n\n\n\n<p>2)\uc874\uc7ac\ud558\uc9c0\uc54a\uc73c\uba74 \ub2e4\uc74c\uacfc \uac19\uc740 json\ub370\uc774\ud130 \ub9ac\ud134<br>If it does not exist, return the following JSON data.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>{ ok: true, alreadyUnboosted: true, isMyBoost: false, boostCount }<\/code><\/pre>\n\n\n\n<p>&#8212; \ubd80\uc2a4\ud2b8 \ucde8\uc18c \ubc30\ub2ec \/ Boost-cancelled delivery<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    \/\/ \ubd80\uc2a4\ud2b8 \ucde8\uc18c \ubc30\ub2ec \/ Undo Boost Delivery\n    const objectForCheck = row.object || target;\n    const isOwnPost = objectForCheck.includes(`${DOMAIN}\/users\/${username}\/`);\n\n    if (!isOwnPost) {\n      try {\n        const u = new URL(objectForCheck);\n        const parts = u.pathname.split('\/').filter(Boolean);\n        const idx = parts.indexOf('users');\n        const inbox = idx!== -1? `${u.origin}\/users\/${parts&#91;idx+1]}\/inbox` : null;\n        if (inbox) {\n          await <strong>sendUndoAnnounce(inbox, row.id, objectForCheck, username);<\/strong>\n        }\n      } catch (e) {\n        console.error('Undo Announce \ubc30\ub2ec \uc2e4\ud328', e);\n      }\n    }<\/code><\/pre>\n\n\n\n<p>1)\ub0b4\uc6a9\uc740 POST\ubd80\ubd84\uacfc \uac19\uace0 !isOwnPost\uc778\uacbd\uc6b0 sendUndoAnnounce\uac00 \uc2e4\ud589\ub418\uc5b4 \ubd80\uc2a4\ud2b8 \ucde8\uc18c \ubc30\ub2ec\ub428<br>The content is the same as in the POST section; if !isOwnPost is true, sendUndoAnnounce is executed, and a boost cancellation is delivered.<\/p>\n\n\n\n<p>&#8212; \ubd80\uc2a4\ud2b8 \uc0ad\uc81c \/ Delete Boost<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>    db.prepare('DELETE FROM announces WHERE id =?').run(row.id);\n\n    const boostCount = (db.prepare(\n      `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`\n    ).get(shortId) as any).c;\n\n    return NextResponse.json({ ok: true, isMyBoost: false, boostCount });<\/code><\/pre>\n\n\n\n<p>1)\uae00\uc758 id\uc640 \uc77c\uce58\ud558\ub294 \ubd80\uc2a4\ud2b8 \uc0ad\uc81c<br>Delete the boost that matches the post ID<\/p>\n\n\n\n<p>2)\ubd80\uc2a4\ud2b8 \uac2f\uc218 \uac80\uc0c9 \ud6c4 json\ub370\uc774\ud130 \ub9ac\ud134<br>Return JSON data after retrieving the boost count<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>{ ok: true, isMyBoost: false, boostCount }<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\ub9c8\ud0dc\ubcf5\uc74c 21\uc7a5 22\uc808 \/ Matthew 21: 22<\/p>\n\n\n\n<p>&#8220;\ub108\ud76c\uac00 \uae30\ub3c4\ud560 \ub54c\uc5d0 \ubb34\uc5c7\uc774\ub4e0\uc9c0 \ubbff\uace0 \uad6c\ud558\ub294 \uac83\uc740 \ub2e4 \ubc1b\uc73c\ub9ac\ub77c \ud558\uc2dc\ub2c8\ub77c&#8221;<\/p>\n\n\n\n<p>&#8220;And everything you ask in prayer, believing, you will receive.&#8221;<\/p>\n<\/blockquote>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb \uc774\ubc88\uc5d0\ub294 \ubd80\uc2a4\ud2b8\uc758 \uad6c\uc870\uc5d0 \ub300\ud574\uc11c \uc0b4\ud3b4\ubd05\ub2c8\ub2e4.This time, we will take a look at the structure of Boost. \ud83d\udc49\ud83c\udffb \uc774\uc804\uc758 \uc88b\uc544\uc694 \ubd80\ubd84\uc758 \ub85c\uc9c1\uacfc \ub9e4\uc6b0 \uc720\uc0ac\ud558\uace0 \ucf54\ub4dc\uc758 \uc0ac\uc6a9\uc704\uce58\ub3c4 \uac70\uc758 \ub3d9\uc77c\ud569\ub2c8\ub2e4.The logic is very similar to that of the previous &#8216;like&#8217; feature, and the code is used in almost the same place. \ud83d\udcc1 ClientPage.tsx \u2714\ufe0f app\/usersui\/[username]\/ClientPage.tsx \u2714\ufe0f \ubd80\uc2a4\ud2b8 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[28,27,1,7],"tags":[],"class_list":["post-12349","post","type-post","status-publish","format-standard","hentry","category-news","category-nextjs","category-uncategorized","category-website","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/12349","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=12349"}],"version-history":[{"count":50,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/12349\/revisions"}],"predecessor-version":[{"id":12400,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/12349\/revisions\/12400"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=12349"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=12349"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=12349"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}