[nextjs]Gotosocial + CRUD(myapp10)

πŸ‘‰πŸ» κΈ°μ‘΄ ν”„λ‘œμ νŠΈμ— κΈ€μ“°κΈ° κΈ°λŠ₯κ³Ό κΈ€ μˆ˜μ •κΈ°λŠ₯을 μΆ”κ°€ν•©λ‹ˆλ‹€.
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
/feed

Leave a Reply