{"id":8253,"date":"2026-08-20T08:51:20","date_gmt":"2026-08-19T23:51:20","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=8253"},"modified":"2026-08-20T13:51:30","modified_gmt":"2026-08-20T04:51:30","slug":"nextjs-sns-server-7","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/08\/20\/nextjs-sns-server-7\/","title":{"rendered":"[nextjs]SNS Server-7(myapp17)"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb \uc774\ubc88 \ud3ec\uc2a4\ud2b8\ub294 \ud314\ub85c\uc789 \ub9ac\uc2a4\ud2b8\ub97c \uad6c\ud604\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.<br>This post covers the implementation of the following list feature.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ud314\ub85c\uc789\uacfc \ud314\ub85c\uc6b0 \uac1c\ub150\uc740 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4.<br>The concepts of &#8220;following&#8221; and &#8220;being followed&#8221; are as follows.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1. followers\n\n- \ub0a8(user1@freelifemakers.com)\uc774 \ub098\ub97c \ud314\ub85c\uc6b0\ud558\ub294 \uacbd\uc6b0 \uc785\ub2c8\ub2e4.\nThis is the case where someone (user1@freelifemakers.com) follows me.\n\n- \ub2e4\uc74c\uacfc \uac19\uc740 \uacfc\uc815\uc774 \uc218\ud589\ub418\ub294 \uacbd\uc6b0\uc785\ub2c8\ub2e4.\nThis applies when the following process is carried out.\n\n  1)pinafore.social\uc5d0 \ub85c\uadf8\uc778(user1@freelifemakers.com)\n   Log in to pinafore.social (user1@freelifemakers.com)\n\n  2)user1@aloy-horizon.duckdns.org\uc744 \uac80\uc0c9\n   Search for user1@aloy-horizon.duckdns.org\n\n  3)\ud314\ub85c\uc6b0 \ubc84\ud2bc \ud074\ub9ad\ud558\uae30\n   Click the Follow button.\n\n  4)\ub098\uc758 \uc11c\ubc84 inbox\ub85c Follow \ub4e4\uc5b4\uc624\uace0 followers \ud14c\uc774\ube14\uc5d0 \uc800\uc7a5\n   A \"follow\" request arrives in my server's inbox and is stored in the `followers` table.\n\n2. following \n\n- \ub0b4(user1@aloy-horizon.duckdns.org)\uac00 \ub0a8(user1@freelifemakers.com)\uc744 \ud314\ub85c\uc6b0\ud558\ub294 \uacbd\uc6b0 \uc785\ub2c8\ub2e4.\nThis is the case where I (user1@aloy-horizon.duckdns.org) follow another person (user1@freelifemakers.com).\n\n- \uc774\uac74 pinafore\uc5d0\uc11c \ud14c\uc2a4\ud2b8 \ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\nThis is not tested in Pinafore.\n\n- \ub0b4 \uc11c\ubc84\uac00 user1@freelifemakers.com\uac19\uc740 \uc678\ubd80 \uc0ac\ub78c\uc744 \ud314\ub85c\uc6b0\ud558\ub294 \uacbd\uc6b0 \uc785\ub2c8\ub2e4.\nThis is the case where my server follows an external user, such as user1@freelifemakers.com.\n\n- \ub0b4\uac00\ud314\ub85c\uc6b0 \ud558\ub294 \uc0ac\ub78c\uc740 following \ud14c\uc774\ube14\uc5d0 \uc800\uc7a5 \ub429\ub2c8\ub2e4.\nThe people I follow are stored in the 'following' table.\n<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uc124\uce58,\ub77c\uc774\ube0c\ub7ec\ub9ac \uc124\uce58,HTTPS\uc124\uc815<br>Project setup, library installation, HTTPS configuration<\/p>\n\n\n\n<p>\u2714\ufe0f \uc544\ub798\uc758 \uc774\uc804 \ud3ec\uc2a4\ud2b8\ub97c \ucc38\uc870\ud558\uc138\uc694<br>Please refer to the previous post below.<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-freelifemakers-org wp-block-embed-freelifemakers-org\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"c8ux9ydtUJ\"><a href=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/08\/10\/nextjs-dns-caddy\/\">[nextjs]nextjs+domain(ddns)+https(caddy)<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; visibility: hidden;\" title=\"&#8220;[nextjs]nextjs+domain(ddns)+https(caddy)&#8221; &#8212; freelifemakers.org\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/08\/10\/nextjs-dns-caddy\/embed\/#?secret=4QF2Qjs85v#?secret=c8ux9ydtUJ\" data-secret=\"c8ux9ydtUJ\" width=\"560\" height=\"315\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n\n\n\n<p>\ud83d\udcc1 \uc804\uccb4 \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 \/ Overall Project Structure<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>myapp16\/  \n\u251c\u2500\u2500 app\/  (Next.js App Router)\n\u2502   \u251c\u2500\u2500 .well-known\/webfinger\/route.ts  -&gt; webfinger\n\u2502   \u251c\u2500\u2500 api\/posts\/route.ts     -&gt; \uae00 \uc4f0\uae30 API \/ Writing API\n\u2502   \u251c\u2500\u2500 api\/follow\/route.ts    -&gt; \ud314\ub85c\uc6b0 API(\uc784\uc2dc) \/ Follow API(temporary)\n\u2502   \u251c\u2500\u2500 users\/&#91;username]\/\n\u2502   \u2502   \u251c\u2500\u2500 route.ts           -&gt; Actor\uc815\ubcf4 \/ Acotr Information\n\u2502   \u2502   \u251c\u2500\u2500 followers\/route.ts -&gt; Followers List\n\u2502   \u2502   \u251c\u2500\u2500 <strong>folloing\/route.ts  -&gt; Following List<\/strong>\n\u2502   \u2502   \u251c\u2500\u2500 inbox\/route.ts     -&gt; Inbox\n\u2502   \u2502   \u2514\u2500\u2500 outbox\/route.ts    -&gt; outbox\n\u2502   \u251c\u2500\u2500 layout.tsx, page.tsx, globals.css\n\u2502   \u2514\u2500\u2500 favicon.ico\n\u251c\u2500\u2500 lib\/\n\u2502   \u251c\u2500\u2500 ap.ts                  -&gt; Follow Accept\n\u2502   \u2514\u2500\u2500 db.ts                  -&gt; DB connection\n\u251c\u2500\u2500 data\/\n\u2502   \u2514\u2500\u2500 keys\/                  -&gt; private.pem, public.pem\n\u251c\u2500\u2500 data.sqlite                -&gt; Database\n\u251c\u2500\u2500 Caddyfile                  -&gt; https \n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uc2dc\uc791(myapp17)<br>Project Start (myapp17)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npx create-next-app@latest<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 SQLite\uc124\uce58 \/ Installing SQLite<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd ~\/myapp17\nnpm install better-sqlite3\nnpm install -D @types\/better-sqlite3<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 following \ud14c\uc774\ube14 \ubc0f \uc778\ub371\uc2a4 \uc0dd\uc131<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE TABLE following (\n  id TEXT PRIMARY KEY,\n  actor TEXT NOT NULL,\n  username TEXT NOT NULL,\n  created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\nCREATE INDEX idx_following_username ON following(username);\nCREATE INDEX idx_following_actor ON following(actor);<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ucf54\ub4dc \uc218\uc815 \ubc0f  \ub77c\uc6b0\ud2b8 \ucd94\uac00 \/ Code modifications and route additions<\/p>\n\n\n\n<p>\u2714\ufe0f \ub77c\uc6b0\ud2b8 \ucd94\uac00 \/ Add Route<\/p>\n\n\n\n<p>&#8212; <code>app\/users\/[username]\/following\/route.ts<\/code>&nbsp;<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import db from '@\/lib\/db';\n\nexport async function GET(\n  req: Request, \n  { params }: { params: Promise&lt;{ username: string }&gt; }\n) {\n  const { username } = await params; \/\/ \u2190 await \ud574\uc57c\ud568 \/ must await\n  const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';\n\n  let following: any&#91;] = &#91;];\n  try {\n    following = db.prepare('SELECT actor FROM following WHERE username = ?').all(username) as any&#91;];\n  } catch (e) {\n    console.log('following \ud14c\uc774\ube14 \uc5c6\uc74c \ub610\ub294 \uc5d0\ub7ec \/ Following table not found or error:', e);\n  }\n\n  return Response.json({\n    \"@context\": \"https:\/\/www.w3.org\/ns\/activitystreams\",\n    \"id\": `https:\/\/${DOMAIN}\/users\/${username}\/following`,\n    \"type\": \"OrderedCollection\",\n    \"totalItems\": following.length,\n    \"orderedItems\": following.map((f: any) =&gt; f.actor)\n  }, {\n    headers: {\n      'Content-Type': 'application\/activity+json; charset=utf-8',\n      'Access-Control-Allow-Origin': '*'\n    }\n  });\n}<\/code><\/pre>\n\n\n\n<p>1) \ub0b4\uac00 \ud314\ub85c\uc6b0\ud55c \uc0ac\uc6a9\uc790 \ub9ac\uc2a4\ud2b8\ub97c \ucd9c\ub825\ud569\ub2c8\ub2e4.<br>Displays the list of users I follow.<\/p>\n\n\n\n<p>\u2714\ufe0f \ucf54\ub4dc \uc218\uc815 \/ Code modification<\/p>\n\n\n\n<p>&#8212; app\/api\/follow\/route.ts<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { sendFollow, signedFetch } from '@\/lib\/ap';\nimport db from '@\/lib\/db'; \n\n\/\/ \ud314\ub85c\uc6b0 API \/ Follow API\nexport async function POST(req: Request) {\n\n... ...\n\n    \/\/ myapp17\u2705 \n    \/\/  4.\uc131\uacf5\ud558\uba74 following \ud14c\uc774\ube14\uc5d0 \uc800\uc7a5! \/ If successful, save to the following table!\n    if (result.ok) {\n      try {\n<strong>        db.prepare('INSERT OR IGNORE INTO following (id, actor, username) VALUES (?, ?, ?)')\n          .run(result.followDoc.id, target, username);<\/strong>\n        console.log(`\u2705 following DB \uc800\uc7a5 \/ If successful, save to the following table!: ${username} -&gt; ${target}`);\n      } catch (e) {\n        console.error('DB \uc800\uc7a5 \uc2e4\ud328 \/ DB save failed:', e);\n      }\n    }\n\n... ...\n\n} \n\n\/\/ \uc5b8\ud314\ub85c\uc6b0 \ucd94\uac00 \/ Unfollow added\nexport async function DELETE(req: Request) {\n  try {\n    const { username, target } = await req.json();\n    if (!username || !target) return Response.json({ error: 'username, target \ud544\uc694 \/ Username and target required' }, { status: 400 });\n\n    const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';\n    \n    \/\/ Undo Follow \ub9cc\ub4e4\uae30 (sendFollow \ucc38\uace0\ud574\uc11c) \/ Create Undo Follow (refer to sendFollow)\n<strong>    const { sendUndoFollow } = await import('@\/lib\/ap');\n    const result = await sendUndoFollow(target, username);<\/strong>\n\n    \/\/ DB\uc5d0\uc11c \uc0ad\uc81c \/ Delete from DB\n<strong>    db.prepare('DELETE FROM following WHERE actor = ? AND username = ?').run(target, username);<\/strong>\n    console.log(`\ud83d\uddd1\ufe0f following \uc0ad\uc81c \/ Delete from DB: ${username} -X-&gt; ${target}`);\n\n    return Response.json({ ok: true, result });\n  } catch (e: any) {\n    return Response.json({ error: e.message }, { status: 500 });\n  }\n}<\/code><\/pre>\n\n\n\n<p>1)\ud314\ub85c\uc6b0,\uc5b8\ud314\ub85c\uc6b0 \ud560\ub54c following\ud14c\uc774\ube14\uc5d0\uc11c \uc0ac\uc6a9\uc790\ub97c \ucd94\uac00\ud558\uace0 \uc0ad\uc81c \ud569\ub2c8\ub2e4.<br>Users are added to and removed from the <code>following<\/code> table when following or unfollowing.<\/p>\n\n\n\n<p>&#8212; lib\/ap.ts<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>port async function sendUndoFollow(targetActor: string, username: string) {\n  const actorId = `https:\/\/${DOMAIN}\/users\/${username}`;\n\n  \/\/ let\uc73c\ub85c \ubc16\uc5d0 \uc120\uc5b8(\ud0c0\uc785\uc5d0\ub7ec \uc218\uc815) \/ Declare outside with let (type error fix)\n  let followId = `${actorId}\/follows\/${targetActor}`; \n\n  try {\n    const db = (await import('@\/lib\/db')).default;\n    const row = db.prepare('SELECT id FROM following WHERE actor = ? AND username = ?').get(targetActor, username) as any;\n    if (row?.id) {\n      followId = row.id;\n    }\n  } catch {}\n\n  const actorRes = await signedFetch(targetActor, username);\n  if (!actorRes.ok) {\n    const t = await actorRes.text();\n    throw new Error(`\ub300\uc0c1 \uc870\ud68c \uc2e4\ud328 ${actorRes.status}: ${t}`);\n  }\n  const actorData = await actorRes.json();\n  const inbox = actorData.inbox;\n\n  const undoId = `${actorId}#undo\/${Date.now()}`;\n  const undoDoc = {\n    '@context': 'https:\/\/www.w3.org\/ns\/activitystreams',\n    id: undoId,\n    type: 'Undo',\n    actor: actorId,\n    object: {\n      id: followId,\n      type: 'Follow',\n      actor: actorId,\n      object: targetActor\n    }\n  };\n\n  const body = JSON.stringify(undoDoc);\n  const url = new URL(inbox);\n  const digest = `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;\n  const date = new Date().toUTCString();\n  const signingString = `(request-target): post ${url.pathname}\\nhost: ${url.host}\\ndate: ${date}\\ndigest: ${digest}`;\n  \n  const signer = crypto.createSign('sha256');\n  signer.update(signingString);\n  const signature = signer.sign(PRIVATE_KEY, 'base64');\n\n  const keyId = `${actorId}#main-key`;\n  const sigHeader = `keyId=\"${keyId}\",headers=\"(request-target) host date digest\",signature=\"${signature}\"`;\n\n  console.log(`\u21a9\ufe0f &#91;${username}] Undo Follow \uc804\uc1a1 -&gt; ${inbox}`);\n\n  const res = await fetch(inbox, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application\/activity+json',\n      'Date': date,\n      'Digest': digest,\n      'Signature': sigHeader,\n      'Host': url.host\n    },\n    body\n  });\n\n  const text = await res.text();\n  console.log(`\ud83d\udcec Undo \uacb0\uacfc \/ Undo result: ${res.status}`, text);\n  return { ok: res.ok, status: res.status, text, undoDoc };\n}<\/code><\/pre>\n\n\n\n<p>1)app\/api\/follow\/route.ts\uc5d0\uc11c \ud638\ucd9c\ub418\ub294 \ud314\ub85c\uc6b0 \ucde8\uc18c \ud568\uc218\uc785\ub2c8\ub2e4.<br>This is the unfollow function called from <code>app\/api\/follow\/route.ts<\/code>.<\/p>\n\n\n\n<p>\ud83d\udcc1 \ud14c\uc2a4\ud2b8 \ud558\uae30 \/ Run a test<\/p>\n\n\n\n<p>\u2714\ufe0f \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c folllowing \ub9ac\uc2a4\ud2b8 \ubcf4\uae30<br>View the following list in the browser.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;aloy-horizon.duckdns.org\/users\/user1\/following<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"433\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-following-jpg.jpg\" alt=\"\" class=\"wp-image-8354\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-following-jpg.jpg 800w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-following-jpg-300x162.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-following-jpg-768x416.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-following-jpg-400x217.jpg 400w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">following<\/figcaption><\/figure>\n\n\n\n<p>\u2714\ufe0f \ud130\ubbf8\ub110\uc5d0\uc11c \ud314\ub85c\uc6b0,\uc5b8\ud314\ub85c\uc6b0 ,\ud314\ub85c\uc789 \ub9ac\uc2a4\ud2b8\ubcf4\uae30<br>Follow, unfollow, and view following list in the terminal.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># 1. user1\uc774 freelifemakers\ub97c \ud314\ub85c\uc6b0 (following\uc5d0 \uc800\uc7a5)\n# user1 follows freelifemakers (saved to 'following')\n\ncurl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/follow \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"user1\", \"target\":\"https:\/\/freelifemakers.com\/users\/user1\"}'\n\n# \ud655\uc778 \/ check\ncurl https:\/\/aloy-horizon.duckdns.org\/users\/user1\/following\n# \u2192 totalItems: 1\n\n# 2. \uc5b8\ud314\ub85c\uc6b0 (following\uc5d0\uc11c \uc0ad\uc81c)\n# Unfollow (remove from 'following' list)\n\ncurl -X DELETE https:\/\/aloy-horizon.duckdns.org\/api\/follow \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"user1\", \"target\":\"https:\/\/freelifemakers.com\/users\/user1\"}'\n\n# \ud655\uc778 \/ check\ncurl https:\/\/aloy-horizon.duckdns.org\/users\/user1\/following\n# \u2192 totalItems: 0<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f \ud130\ubbf8\ub110\uc5d0\uc11c \uc2e4\ud589\uacb0\uacfc (\ud314\ub85c\uc6b0)<br>Execution results in the terminal (follow)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>following % <strong>curl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/follow \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"user1\", \"target\":\"https:\/\/freelifemakers.com\/users\/user1\"}'<\/strong>\n{\"ok\":true,\"inbox\":\"https:\/\/freelifemakers.com\/users\/user1\/inbox\",\"target\":\"https:\/\/freelifemakers.com\/users\/user1\",\"username\":\"user1\",\"result\":\"{\\\"status\\\":\\\"Accepted\\\"}\",\"follow\":{\"@context\":\"https:\/\/www.w3.org\/ns\/activitystreams\",\"id\":\"https:\/\/aloy-horizon.duckdns.org\/users\/user1\/follows\/1787190758589\",\"type\":\"Follow\",\"actor\":\"https:\/\/aloy-horizon.duckdns.org\/users\/user1\",\"object\":\"https:\/\/freelifemakers.com\/users\/user1\"}}%                                                          \nfollowing % <\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \uc9c0\uae08\uae4c\uc9c0 \uc644\ub8cc\ud55c \ub0b4\uc6a9 \/ Work completed so far<\/p>\n\n\n\n<p>&#8212;&nbsp;WebFinger&nbsp; (<code>users<\/code>&nbsp;\ud14c\uc774\ube14 \uc0dd\uc131 + DB \uac80\uc0c9)<br>WebFinger (create &#8216;users&#8217; table + DB search)<\/p>\n\n\n\n<p>&#8212;  Follow \ubcf4\ub0b4\uae30 \/ \ubc1b\uae30<br>Follow Send\/Receive<\/p>\n\n\n\n<p>&#8212; \uae00 \ubc30\ub2ec <br>Article(Post) Delivery<\/p>\n\n\n\n<p>&#8212; \ub0b4\uac00 \ud314\ub85c\uc6b0\ud55c \ub9ac\uc2a4\ud2b8 \ub9cc\ub4e4\uae30 <br>Create a list of people I follow<\/p>\n\n\n\n<p>&#8212; \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c \ud14c\uc2a4\ud2b8 \ud560 \uc218 \uc788\ub294 \ub77c\uc6b0\ud2b8<br>Routes that can be tested in a browser<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1. WebFinger\nhttps:&#47;&#47;aloy-horizon.duckdns.org\/.well-known\/webfinger?resource=acct:user1@aloy-horizon.duckdns.org\n\n2. Actor (\ud504\ub85c\ud544 \/ Profile)\nhttps:\/\/aloy-horizon.duckdns.org\/users\/user1\n\n3. Followers (\ub0a8\uc774 \ub098 \ud314\ub85c\uc6b0 \/ Someone follows me)\nhttps:\/\/aloy-horizon.duckdns.org\/users\/user1\/followers\n\n4. Following (\ub0b4\uac00 \ub0a8\uc744 \ud314\ub85c\uc6b0 \/ I follow others)\nhttps:\/\/aloy-horizon.duckdns.org\/users\/user1\/following\n\n5. Outbox (\ub0b4 \uae00 \/ My Post)\nhttps:\/\/aloy-horizon.duckdns.org\/users\/user1\/outbox\n\n6. \uac1c\ubcc4 \uae00 \/ indivisual posts \nmyapp18\uc5d0\uc11c \uad6c\ud604 \uc608\uc815\nScheduled for implementation in myapp18.<\/code><\/pre>\n\n\n\n<p>&#8212; \ud130\ubbf8\ub110\uc5d0\uc11c\ub9cc \ud14c\uc2a4\ud2b8 \uac00\ub2a5\ud55c \ub77c\uc6b0\ud2b8<br>Routes that can only be tested in the terminal<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \ud314\ub85c\uc6b0 (following 1\ub85c \uc99d\uac00)\n# Follow (following increased to 1)\n\ncurl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/follow \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"user1\",\"target\":\"https:\/\/freelifemakers.com\/users\/user1\"}'\n\n# \uc5b8\ud314\ub85c\uc6b0 (following 0\uc73c\ub85c \uac10\uc18c)\n# Unfollow (following count reduced to 0)\n\ncurl -X DELETE https:\/\/aloy-horizon.duckdns.org\/api\/follow \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"user1\",\"target\":\"https:\/\/freelifemakers.com\/users\/user1\"}'\n\n# \uae00 \uc4f0\uae30 (outbox 7\ub85c \uc99d\uac00 + \ud314\ub85c\uc6cc\ub4e4\uc5d0\uac8c \ubc30\ub2ec)\n# Writing a post (added to outbox 7 + delivered to followers)\n\ncurl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/posts \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"user1\",\"content\":\"Hello Fediverse! #test\"}'<\/code><\/pre>\n\n\n\n<p>1)\ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c \uc0ac\uc6a9\uc2dc POST\uc804\uc1a1\uc774\ub098 DELETE\uc804\uc1a1\uc740 \ud3fc submit\ub4f1\uc758 \ud615\uc2dd\uc73c\ub85c \uc804\uc1a1\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4.<br>When using a browser, POST or DELETE requests must be sent using a format such as a &#8220;form submit.&#8221;<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc5ec\uae30\uc11c\ub294 \ub0b4\uac00 \uc4f4\uae00\uc740 posts\ud14c\uc774\ube14\uc5d0 \uc800\uc7a5\ud558\uace0 \ub0b4\uac00 \ud314\ub85c\uc789\ud558\ub294 \uc11c\ubc84\uc5d0\uc11c \ubc1b\uc740 \uae00\uc740 inbox_posts\uc5d0 \uc800\uc7a5\ub418\ub3c4\ub85d \uc218\uc815\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.<br>Here, we are modifying the system so that posts I write are stored in the <code>posts<\/code> table, while posts received from servers I follow are stored in <code>inbox_posts<\/code>.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb post\ud14c\uc774\ube14\uc5d0 \uc678\ubd80 \uae00\uc774 \uc11e\uc774\ub294 \ubc84\uadf8\ub97c \uc218\uc815\ud55c \ubd80\ubd84\uc785\ub2c8\ub2e4.<br>This change fixes a bug where external posts were getting mixed into the <code>post<\/code> table.<\/p>\n\n\n\n<p>\ud83d\udcc1 DB \ub9c8\uc774\uadf8\ub808\uc774\uc158 \/ DB Migration<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sqlite3 data.db\n\n# posts\uc5d0 username \uceec\ub7fc \ucd94\uac00\n# Add username column to posts\nALTER TABLE posts ADD COLUMN username TEXT DEFAULT 'user1';\n\n# \uc798\ubabb \ub4e4\uc5b4\uac04 remote \uae00 \uc0ad\uc81c\n# Deleted accidentally posted remote-related entry\nDELETE FROM posts WHERE id LIKE 'https:\/\/%';\n\n# inbox_posts \ud14c\uc774\ube14 \ub9cc\ub4e4\uae30\n# Create the inbox_posts table\nCREATE TABLE IF NOT EXISTS inbox_posts (\n  id TEXT PRIMARY KEY,\n  actor TEXT,\n  content TEXT,\n  username TEXT,\n  created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ucf54\ub4dc \uc218\uc815 \/ Code Modification<\/p>\n\n\n\n<p>\u2714\ufe0f api\/posts\/route.ts<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/api\/posts\/route.ts\n\n\/\/ \u2705 myapp17\nimport db from '@\/lib\/db';\nimport { randomUUID } from 'crypto';\nimport { sendNote } from '@\/lib\/ap';\n\nconst DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const username = searchParams.get('username') || 'user1';\n\n  \/\/ \u2705 \ud574\ub2f9 \uc720\uc800 \uae00\ub9cc \/ Only that user's posts\n  const posts = db.prepare('SELECT * FROM posts WHERE username = ? ORDER BY created_at DESC').all(username);\n  return Response.json(posts);\n}\n\nexport async function POST(req: Request) {\n  const { content, username = 'user1' } = await req.json();\n  if (!content) return Response.json({ error: '\ub0b4\uc6a9 \uc5c6\uc74c \/ Content required' }, { status: 400 });\n\n  const id = randomUUID();\n  \n  \/\/ \u2705 username \uc800\uc7a5! \/ Save username!\n  db.prepare('INSERT INTO posts (id, content, username) VALUES (?, ?, ?)').run(id, content, username);\n\n  const noteId = `https:\/\/${DOMAIN}\/users\/${username}\/posts\/${id}`;\n  const note = {\n    id: noteId,\n    type: 'Note',\n    attributedTo: `https:\/\/${DOMAIN}\/users\/${username}`,\n    content: content,\n    to: &#91;'https:\/\/www.w3.org\/ns\/activitystreams#Public'],\n    cc: &#91;`https:\/\/${DOMAIN}\/users\/${username}\/followers`]\n  };\n\n  \/\/ \u2705 \ud574\ub2f9 \uc720\uc800\uc758 \ud314\ub85c\uc6cc\ub9cc!\n  const followers = db.prepare('SELECT * FROM followers WHERE username = ?').all(username) as any&#91;];\n  console.log(`\ud83d\udce4 &#91;${username}] ${followers.length}\uba85\uc5d0\uac8c \ubc30\ub2ec`);\n\n  for (const follower of followers) {\n    try {\n      await sendNote(follower.inbox, note, username, id, content);\n      console.log(`\u2705 \ubc30\ub2ec \uc131\uacf5 \/ Delivery successful -&gt; ${follower.actor}`);\n    } catch (e) {\n      console.error(`\u274c \ubc30\ub2ec \uc2e4\ud328 \/ Delivery failed -&gt; ${follower.actor}`, e);\n    }\n  }\n\n  const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);\n  return Response.json(post);\n}\n\nexport async function PUT(req: Request) {\n  const { id, content, username = 'user1' } = await req.json();\n  if (!id || !content) {\n    return Response.json({ error: 'id\uc640 content \ud544\uc694' }, { status: 400 });\n  }\n  \/\/ \u2705 \ub0b4 \uae00\ub9cc \uc218\uc815 \/ Only edit my own posts\n  const result = db.prepare('UPDATE posts SET content = ? WHERE id = ? AND username = ?').run(content, id, username);\n  if (result.changes === 0) {\n    return Response.json({ error: '\ud574\ub2f9 \uae00 \uc5c6\uc74c \/ Post not found' }, { status: 404 });\n  }\n  const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);\n  return Response.json(post);\n}\n\nexport async function DELETE(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const id = searchParams.get('id');\n  const username = searchParams.get('username') || 'user1';\n  if (!id) return Response.json({ error: 'id \ud544\uc694 \/ id required' }, { status: 400 });\n  \/\/ \u2705 \ub0b4 \uae00\ub9cc \uc0ad\uc81c \/ Only delete my own posts\n  db.prepare('DELETE FROM posts WHERE id = ? AND username = ?').run(id, username);\n  return Response.json({ ok: true });\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f app\/inbox\/route.ts<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    if (body.type === 'Create') {\n      const note = body.object;\n      if (note &amp;&amp; note.type === 'Note') {\n        console.log(`\ud83d\udcdd &#91;${username}] \uc0c8 \uae00 \ub3c4\ucc29 \/ New post received from ${actorId}`);\n        console.log(`\ub0b4\uc6a9\/content: ${note.content?.slice(0, 100)}`);\n\n        try {\n          const postId = note.id || `remote-${Date.now()}-${Math.random()}`;\n          const content = note.content || '';\n\n          \/\/ author \uceec\ub7fc \uc5c6\uc73c\uba74 \uc5d0\ub7ec\ub098\ub2c8\uae4c content\uc5d0 \uc791\uc131\uc790 \ud3ec\ud568\ud574\uc11c \uc800\uc7a5\n          \/\/ If there's no author column, include the author in the content to avoid errors\n          const fullContent = `&#91;from: ${actorId}] ${content}`;\n\n          \/\/ db.prepare('INSERT OR IGNORE INTO posts (id, content) VALUES (?,?)')\n          \/\/  .run(postId, fullContent);\n          \/\/console.log(` &#91;${username}] \uae00 \uc800\uc7a5 \uc644\ub8cc \/ Post saved successfully : ${postId}`);\n\n           \/\/ \u2705 myapp17 - inbox_posts\uc5d0 \uc800\uc7a5! posts \uc544\ub2d8! \/ Save to inbox_posts, not posts!\n<strong>          db.prepare(`\n            INSERT OR IGNORE INTO inbox_posts (id, actor, content, username) \n            VALUES (?, ?, ?, ?)\n          `).run(postId, actorId, content, username);<\/strong>\n\n          console.log(`\u2705 &#91;${username}] inbox_posts \uc800\uc7a5 \uc644\ub8cc : ${postId}`);\n\n        } catch (e) {\n          console.error(`\u274c \uae00 \uc800\uc7a5 \uc2e4\ud328 \/ Failed to save post`, e);\n        }\n      }\n      return new Response('', { status: 202 });\n    }<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud14c\uc2a4\ud2b8 \/ Test<\/p>\n\n\n\n<p>\u2714\ufe0f \ub0b4 \uc11c\ubc84\uc5d0 \uae00 \uc4f0\uae30 \/ write post to my server<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \uae00 \uc4f0\uae30 (outbox 7\ub85c \uc99d\uac00 + \ud314\ub85c\uc6cc\ub4e4\uc5d0\uac8c \ubc30\ub2ec)\n# Writing a post (added to outbox 7 + delivered to followers)\n\ncurl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/posts \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"user1\",\"content\":\"Hello Fediverse! #test\"}'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>myapp17 %<strong> sqlite3 data.sqlite<\/strong>\nSQLite version 3.51.0 2025-06-12 13:14:41\nEnter \".help\" for usage hints.\nsqlite&gt; <strong>select * from posts;<\/strong>\ne6c99652-572f-491a-9e61-05e67b0d58fc|Hello! my SNS!!|2026-08-12 04:59:51|user1\ndcece9ee-8d4a-4f63-91e1-22fc60513f62|Hello! my SNS!!|2026-08-12 05:00:18|user1\ncc30330e-7040-4a1b-9e60-731066b8817e|Hello! my SNS3!!|2026-08-12 23:08:05|user1\ne6d532c8-974b-4a01-89f6-6e1b2c4cff32|Hello Fediverse! #test|2026-08-22 02:16:04|user1\naea0ec89-a368-44a9-8493-b2bd82a797a8|post test! #test|2026-08-12 03:08:18|user1\nsqlite&gt; <strong>select * from inbox_posts;<\/strong>\nsqlite&gt; \n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f  \uae00\ubc1b\uae30(inbox_posts \ud14c\uc774\ube14\uc5d0 \uc800\uc7a5)<br>Receive posts (stored in the inbox_posts table)<\/p>\n\n\n\n<p>&#8212; \uae00 \uc791\uc131\uc740 pinafore(website)\ub098 Tusky(android),Feditex(IOS)\uc5d0\uc11c \uc791\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br>You can create posts using Pinafore(website), Tusky (Android), or Feditex (iOS).<\/p>\n\n\n\n<p>&#8212; \uc11c\ubc84 \ub85c\uadf8 \/ Server Log<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># server log\n\n\ud83d\udce9 &#91;user1] INBOX: Create https:\/\/freelifemakers.com\/users\/user1\n\ud83d\udcdd &#91;user1] \uc0c8 \uae00 \ub3c4\ucc29 \/ New post received from https:\/\/freelifemakers.com\/users\/user1\n\ub0b4\uc6a9\/content: &lt;p&gt;inbox_posts table test&lt;\/p&gt;\n\u2705 &#91;user1] inbox_posts \uc800\uc7a5 \uc644\ub8cc : https:\/\/freelifemakers.com\/users\/user1\/statuses\/01M0EJPP57WC47FAK0N395S7MB<\/code><\/pre>\n\n\n\n<p>&#8212; index_posts\uc5d0 \uae00 \uc800\uc7a5\ud655\uc778 \ud558\uae30<br>Check if posts are saved in index_posts<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sqlite&gt; <strong>select * from inbox_posts;<\/strong>\nhttps:\/\/freelifemakers.com\/users\/user1\/statuses\/0--------7FAK0N395----|https:\/\/freelifemakers.com\/users\/user1|&lt;p&gt;inbox_posts table test&lt;\/p&gt;|user1|2026-08-20 03:16:15\nsqlite&gt; <\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"151\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-inbox_posts_sqlite-jpg.jpg\" alt=\"\" class=\"wp-image-8357\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-inbox_posts_sqlite-jpg.jpg 800w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-inbox_posts_sqlite-jpg-300x57.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-inbox_posts_sqlite-jpg-768x145.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp17-inbox_posts_sqlite-jpg-400x76.jpg 400w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">Terminal<\/figcaption><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb \uc774\ubc88 \ud3ec\uc2a4\ud2b8\ub294 \ud314\ub85c\uc789 \ub9ac\uc2a4\ud2b8\ub97c \uad6c\ud604\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.This post covers the implementation of the following list feature. \ud83d\udc49\ud83c\udffb \ud314\ub85c\uc789\uacfc \ud314\ub85c\uc6b0 \uac1c\ub150\uc740 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4.The concepts of &#8220;following&#8221; and &#8220;being followed&#8221; are as follows. \ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uc124\uce58,\ub77c\uc774\ube0c\ub7ec\ub9ac \uc124\uce58,HTTPS\uc124\uc815Project setup, library installation, HTTPS configuration \u2714\ufe0f \uc544\ub798\uc758 \uc774\uc804 \ud3ec\uc2a4\ud2b8\ub97c \ucc38\uc870\ud558\uc138\uc694Please refer to the previous post below. \ud83d\udcc1 \uc804\uccb4 \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 [&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-8253","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\/8253","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=8253"}],"version-history":[{"count":88,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/8253\/revisions"}],"predecessor-version":[{"id":8366,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/8253\/revisions\/8366"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=8253"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=8253"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=8253"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}