ππ» κΈ°μ‘΄ νλ‘μ νΈμ κΈμ°κΈ° κΈ°λ₯κ³Ό κΈ μμ κΈ°λ₯μ μΆκ°ν©λλ€.
Add post creation and editing features to the existing project.
ππ» νμλΌμΈμ λΆλ¬μ€λ gotosocial.tsνμΌμ μμ λμ΅λλ€.
The gotosocial.ts file that fetches the timeline has been deleted.
π νλ‘μ νΈ κ΅¬μ‘° / Project Structure
myapp10/
βββ app/
β βββ api/
β β βββ auth/ # NextAuth
β β βββ gts/
β β βββ statuses/
β β βββ route.ts # POST Create
β β βββ [id]/
β β βββ route.ts # DELETE, PUT
β βββ feed/
β β βββ page.tsx # frontend(main page,post read)
β β βββ PostCard.tsx # frontend(post content,delete,update)
β β βββ PostComposer.tsx # frontend(post create)
β βββ layout.tsx
β βββ page.tsx # Home
β βββ globals.css
βββ auth.ts. # NextAuth
βββ package.json
π Nextjs νλ‘μ νΈ μμ±
Create a Next.js project
npx create-next-app@latest
π next-auth μ€μΉ
Install next-auth
npm install next-auth@beta
π κΈμ°κΈ° / Writing(Create)
βοΈ myapp10/app/feed/PostComposer.tsx
— κΈ μμ± νΌ / Post Creation Form
'use client'
import { useState } from 'react'
export function PostComposer() {
const [text, setText] = useState('')
const handlePost = async () => {
await fetch('/api/gts/statuses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: text })
})
location.reload()
}
return (
<div className="border p-4 rounded-xl flex gap-2">
<input value={text} onChange={e => setText(e.target.value)} className="flex-1 border p-2 rounded" placeholder="λ¬΄μ¨ μΌμ΄ μΌμ΄λκ³ μλμ?" />
<button onClick={handlePost} className="bg-black text-white px-4 rounded">κ²μ</button>
</div>
)
}
1)μλ²μμ CORSμλ¬λ₯Ό λ§κΈ°μν΄μ /api/gts/statuses λΌμ°ν°λ‘ fetchλ₯Ό μ€νν©λλ€.
To prevent CORS errors on the server, a fetch request is made to the /api/gts/statuses route.
βοΈ myapp10/app/feed/page.tsx
— λ©μΈνμ΄μ§,κΈ μμ± νΌμ λΆλ¬μ΅λλ€. / Loads the main page and post creation form. (PostComposer.tsx)
import { PostComposer } from './PostComposer' // Create Component
... ...
return (
<div className="max-w-xl mx-auto p-4 space-y-4">
<h1 className="text-xl font-bold border-b pb-2">μ°ν© μ°μ£Ό νμλΌμΈ</h1>
<h1 className="text-sm text-gray-500"> Federated Universe Timeline </h1>
{/* Post Composer */}
<PostComposer token={session.accessToken} />
{/* Post Cards */}
{posts.length === 0 ? (
<p className="text-gray-500 py-10 text-center">μμ§ νμλΌμΈμ νμν κΈμ΄ μμ΅λλ€. / No posts to display yet.</p>
) : (
posts.map((p: any) => <PostCard key={p.id} post={p} token={session.accessToken} />)
)}
</div>
)
1)ν΄λΌμ΄μΈνΈμ μλ²μ κΈ°λ₯μ λμΌννμ΄μ§μ μ¬μ©ν μ μκΈ° λλ¬Έμ νμΌμ λΆλ¦¬ν©λλ€.
Since client-side and server-side functions cannot be used on the same page, the files are separated.
βοΈ myapp10/app/api/gts/router.ts
— gotosocialμλ²μ κΈμ μ μ₯ν©λλ€.
Saves the post to the GoToSocial server.
import { auth } from "@/auth"
export async function POST(req: Request) {
const session = await auth()
if (!session?.accessToken) return new Response("Unauthorized", { status: 401 })
const { status } = await req.json()
const baseUrl = process.env.GTS_URL || "https://freelifemakers.com"
const res = await fetch(`${baseUrl}/api/v1/statuses`, {
method: 'POST',
headers: {
Authorization: `Bearer ${session.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status,visibility : "public" })
})
const data = await res.json()
return Response.json(data)
}
π κΈ μμ / Edit Post (Update)
βοΈ myapp10/app/feed/PostCard.tsx
— κΈ μμ μλ λΆλΆμ κΈ μμ λ²νΌμ λΆμ
λλ€.
Place the “Edit Post” button below the “Delete Post” button.
— μ κ· ννμμ htmlμ μ κ±°νλ λΆλΆμ
λλ€.
The regular expression is the part that removes HTML.
<button
onClick={async () => {
const newText = prompt('μμ ν λ΄μ©μμ ν λ΄μ© / Edit content:', post.content.replace(/<[^>]*>/g, '')) // HTML νκ·Έ μ κ±°
if(!newText) return
await fetch(`/api/gts/statuses/${post.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newText })
})
location.reload()
}}
className="text-blue-500 text-sm"
>
μμ /Edit
</button>
βοΈ myapp10/app/api/gts/statuses/[id]/route.ts
— κΈ μμ λΌμ°νΈ μ
λλ€.
This is the route for editing a post.(Update)
// μμ / Update
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const session = await auth()
const { status } = await req.json()
const baseUrl = process.env.GTS_URL || "https://freelifemakers.com"
const res = await fetch(`${baseUrl}/api/v1/statuses/${id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${session.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status, visibility : "public" })
})
return Response.json(await res.json())
}
π μλ²μ€ν / Start Server
npm run dev
π λΈλΌμ°μ μ μ / Access Browser
http://localhost:3000/feed
