{"id":7683,"date":"2026-08-10T10:28:43","date_gmt":"2026-08-10T01:28:43","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=7683"},"modified":"2026-08-11T10:25:06","modified_gmt":"2026-08-11T01:25:06","slug":"nextjs-gotosocial-crud","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/08\/10\/nextjs-gotosocial-crud\/","title":{"rendered":"[nextjs]Gotosocial + CRUD"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb \uae30\uc874 \ud504\ub85c\uc81d\ud2b8\uc5d0 \uae00\uc4f0\uae30 \uae30\ub2a5\uacfc \uae00 \uc218\uc815\uae30\ub2a5\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.<br>Add post creation and editing features to the existing project.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ud0c0\uc784\ub77c\uc778\uc744 \ubd88\ub7ec\uc624\ub294 gotosocial.ts\ud30c\uc77c\uc740 \uc0ad\uc81c\ub410\uc2b5\ub2c8\ub2e4.<br>The <code>gotosocial.ts<\/code> file that fetches the timeline has been deleted.<\/p>\n\n\n\n<p>\ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 \/ Project Structure<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>myapp10\/\n\u251c\u2500\u2500 app\/\n\u2502 \u251c\u2500\u2500 api\/\n\u2502 \u2502 \u251c\u2500\u2500 auth\/ # NextAuth \n\u2502 \u2502 \u2514\u2500\u2500 gts\/\n\u2502 \u2502      \u2514\u2500\u2500 statuses\/\n\u2502 \u2502       \u251c\u2500\u2500 <strong>route.ts<\/strong> # POST Create\n\u2502 \u2502       \u2514\u2500\u2500 &#91;id]\/\n\u2502 \u2502            \u2514\u2500\u2500 <strong>route.ts<\/strong> # DELETE, PUT\n\u2502 \u251c\u2500\u2500 feed\/\n\u2502 \u2502    \u251c\u2500\u2500 page.tsx # frontend(main page,post read)\n\u2502 \u2502    \u251c\u2500\u2500 <strong>PostCard.tsx<\/strong> # frontend(post content,delete,update)\n\u2502 \u2502    \u2514\u2500\u2500 PostComposer.tsx # frontend(post create)\n\u2502 \u251c\u2500\u2500 layout.tsx\n\u2502 \u251c\u2500\u2500 page.tsx # Home\n\u2502 \u2514\u2500\u2500 globals.css\n\u251c\u2500\u2500 auth.ts.   # NextAuth \n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 Nextjs \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131<br>Create a Next.js project<\/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 next-auth \uc124\uce58<br>Install next-auth<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install next-auth@beta<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \uae00\uc4f0\uae30 \/ Writing(Create)<\/p>\n\n\n\n<p>\u2714\ufe0f myapp10\/app\/feed\/PostComposer.tsx<\/p>\n\n\n\n<p>&#8212; \uae00 \uc791\uc131 \ud3fc \/ Post Creation Form<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use client'\nimport { useState } from 'react'\n\nexport function PostComposer() {\n  const &#91;text, setText] = useState('')\n  \n  const handlePost = async () =&gt; {\n    await fetch('\/api\/gts\/statuses', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/json' },\n      body: JSON.stringify({ status: text })\n    })\n    location.reload()\n  }\n\n  return (\n    &lt;div className=\"border p-4 rounded-xl flex gap-2\"&gt;\n      &lt;input value={text} onChange={e =&gt; setText(e.target.value)} className=\"flex-1 border p-2 rounded\" placeholder=\"\ubb34\uc2a8 \uc77c\uc774 \uc77c\uc5b4\ub098\uace0 \uc788\ub098\uc694?\" \/&gt;\n      &lt;button onClick={handlePost} className=\"bg-black text-white px-4 rounded\"&gt;\uac8c\uc2dc&lt;\/button&gt;\n    &lt;\/div&gt;\n  )\n}<\/code><\/pre>\n\n\n\n<p>1)\uc11c\ubc84\uc5d0\uc11c CORS\uc5d0\ub7ec\ub97c \ub9c9\uae30\uc704\ud574\uc11c \/api\/gts\/statuses \ub77c\uc6b0\ud130\ub85c fetch\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4.<br>To prevent CORS errors on the server, a fetch request is made to the <code>\/api\/gts\/statuses<\/code> route.<\/p>\n\n\n\n<p>\u2714\ufe0f myapp10\/app\/feed\/page.tsx<\/p>\n\n\n\n<p>&#8212; \uba54\uc778\ud398\uc774\uc9c0,\uae00 \uc791\uc131 \ud3fc\uc744 \ubd88\ub7ec\uc635\ub2c8\ub2e4. \/ Loads the main page and post creation form. (PostComposer.tsx)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { PostComposer } from '.\/PostComposer' \/\/ Create Component\n... ...\n    return (\n      &lt;div className=\"max-w-xl mx-auto p-4 space-y-4\"&gt;\n        &lt;h1 className=\"text-xl font-bold border-b pb-2\"&gt;\uc5f0\ud569 \uc6b0\uc8fc \ud0c0\uc784\ub77c\uc778&lt;\/h1&gt;\n        &lt;h1 className=\"text-sm text-gray-500\"&gt; Federated Universe Timeline &lt;\/h1&gt;\n        \n        {\/* Post Composer *\/}\n        <strong>&lt;PostComposer token={session.accessToken} \/&gt;<\/strong>\n        \n        {\/* Post Cards *\/}\n        {posts.length === 0 ? (\n          &lt;p className=\"text-gray-500 py-10 text-center\"&gt;\uc544\uc9c1 \ud0c0\uc784\ub77c\uc778\uc5d0 \ud45c\uc2dc\ud560 \uae00\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \/ No posts to display yet.&lt;\/p&gt;\n        ) : (\n          posts.map((p: any) =&gt; &lt;PostCard key={p.id} post={p} token={session.accessToken} \/&gt;)\n        )}\n      &lt;\/div&gt;\n    )<\/code><\/pre>\n\n\n\n<p>1)\ud074\ub77c\uc774\uc5b8\ud2b8\uc640 \uc11c\ubc84\uc758 \uae30\ub2a5\uc744 \ub3d9\uc77c\ud55c\ud398\uc774\uc9c0\uc5d0 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \ud30c\uc77c\uc744 \ubd84\ub9ac\ud569\ub2c8\ub2e4.<br>Since client-side and server-side functions cannot be used on the same page, the files are separated.<\/p>\n\n\n\n<p>\u2714\ufe0f myapp10\/app\/api\/gts\/router.ts<\/p>\n\n\n\n<p>&#8212; gotosocial\uc11c\ubc84\uc5d0 \uae00\uc744 \uc800\uc7a5\ud569\ub2c8\ub2e4.<br>Saves the post to the GoToSocial server.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { auth } from \"@\/auth\"\n\nexport async function POST(req: Request) {\n  const session = await auth()\n  if (!session?.accessToken) return new Response(\"Unauthorized\", { status: 401 })\n  \n  const { status } = await req.json()\n  const baseUrl = process.env.GTS_URL || \"https:\/\/freelifemakers.com\"\n  \n  const res = await fetch(`${baseUrl}\/api\/v1\/statuses`, {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${session.accessToken}`,\n      'Content-Type': 'application\/json'\n    },\n    body: JSON.stringify({ status,visibility : \"public\" })\n  })\n  \n  const data = await res.json()\n  return Response.json(data)\n}<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \uae00 \uc218\uc815 \/ Edit Post (Update)<\/p>\n\n\n\n<p>\u2714\ufe0f myapp10\/app\/feed\/PostCard.tsx<\/p>\n\n\n\n<p>&#8212; \uae00 \uc0ad\uc81c \uc544\ub798 \ubd80\ubd84\uc5d0 \uae00 \uc218\uc815 \ubc84\ud2bc\uc744 \ubd99\uc785\ub2c8\ub2e4.<br>Place the &#8220;Edit Post&#8221; button below the &#8220;Delete Post&#8221; button.<\/p>\n\n\n\n<p>&#8212; \uc815\uaddc \ud45c\ud604\uc2dd\uc740 html\uc744 \uc81c\uac70\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.<br>The regular expression is the part that removes HTML.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>        &lt;button \n          onClick={async () => {\n            const newText = prompt('\uc218\uc815\ud560 \ub0b4\uc6a9\uc218\uc815\ud560 \ub0b4\uc6a9 \/ Edit content:', post.content.replace(\/&lt;&#91;^>]*>\/g, '')) \/\/ HTML \ud0dc\uadf8 \uc81c\uac70\n            if(!newText) return\n            await fetch(`\/api\/gts\/statuses\/${post.id}`, { \n              method: 'PUT', \n              headers: { 'Content-Type': 'application\/json' },\n              body: JSON.stringify({ status: newText }) \n            })\n            location.reload()\n          }}\n          className=\"text-blue-500 text-sm\"\n        >\n          \uc218\uc815\/Edit\n        &lt;\/button><\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f myapp10\/app\/api\/gts\/statuses\/[id]\/route.ts<\/p>\n\n\n\n<p>&#8212; \uae00 \uc218\uc815 \ub77c\uc6b0\ud2b8 \uc785\ub2c8\ub2e4.<br>This is the route for editing a post.(Update)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \uc218\uc815 \/ Update\nexport async function PUT(req: Request, { params }: { params: Promise&lt;{ id: string }> }) {\n  const { id } = await params\n  const session = await auth()\n  const { status } = await req.json()\n  const baseUrl = process.env.GTS_URL || \"https:\/\/freelifemakers.com\"\n  \n  const res = await fetch(`${baseUrl}\/api\/v1\/statuses\/${id}`, {\n    method: 'PUT',\n    headers: {\n      Authorization: `Bearer ${session.accessToken}`,\n      'Content-Type': 'application\/json'\n    },\n    body: JSON.stringify({ status, visibility : \"public\" })\n  })\n  \n  return Response.json(await res.json())\n}<\/code><\/pre>\n\n\n\n<p><br>\ud83d\udcc1 \uc11c\ubc84\uc2e4\ud589 \/ Start Server<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm run dev<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ube0c\ub77c\uc6b0\uc800 \uc811\uc18d \/ Access Browser<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>http:&#47;&#47;localhost:3000\/feed<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"571\" height=\"1024\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp10-jpg-571x1024.jpg\" alt=\"\" class=\"wp-image-7754\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp10-jpg-571x1024.jpg 571w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp10-jpg-167x300.jpg 167w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp10-jpg-400x718.jpg 400w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/08\/myapp10-jpg.jpg 700w\" sizes=\"auto, (max-width: 571px) 100vw, 571px\" \/><figcaption class=\"wp-element-caption\">\/feed<\/figcaption><\/figure>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb \uae30\uc874 \ud504\ub85c\uc81d\ud2b8\uc5d0 \uae00\uc4f0\uae30 \uae30\ub2a5\uacfc \uae00 \uc218\uc815\uae30\ub2a5\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.Add post creation and editing features to the existing project. \ud83d\udc49\ud83c\udffb \ud0c0\uc784\ub77c\uc778\uc744 \ubd88\ub7ec\uc624\ub294 gotosocial.ts\ud30c\uc77c\uc740 \uc0ad\uc81c\ub410\uc2b5\ub2c8\ub2e4.The gotosocial.ts file that fetches the timeline has been deleted. \ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 \/ Project Structure \ud83d\udcc1 Nextjs \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131Create a Next.js project \ud83d\udcc1 next-auth \uc124\uce58Install next-auth \ud83d\udcc1 \uae00\uc4f0\uae30 \/ Writing(Create) \u2714\ufe0f myapp10\/app\/feed\/PostComposer.tsx &#8212; [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[27,1,7],"tags":[],"class_list":["post-7683","post","type-post","status-publish","format-standard","hentry","category-nextjs","category-uncategorized","category-website","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/7683","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=7683"}],"version-history":[{"count":42,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/7683\/revisions"}],"predecessor-version":[{"id":7756,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/7683\/revisions\/7756"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=7683"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=7683"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=7683"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}