[nextjs]SNS Server-19(myapp29)

πŸ‘‰πŸ» myapp29μ—λŠ” μ’‹μ•„μš” λ²„νŠΌμ„ μΆ”κ°€ν•©λ‹ˆλ‹€.
Add a “Like” button to myapp29.

πŸ‘‰πŸ» 기쑴의 λΆ€μŠ€νŠΈμ™€ μ’‹μ•„μš” λΆ€λΆ„μ˜ λ²„νŠΌ 였λ₯˜λ‘œ λ²„νŠΌ 싀행정보λ₯Ό DB검색정보λ₯Ό ν™œμš©ν•˜λ„λ‘ λ°”κΏ¨μŠ΅λ‹ˆλ‹€.
To resolve button errors in the existing “Boost” and “Like” features, I modified the implementation to utilize database search information for button actions.

πŸ‘‰πŸ» 전체 μ½”λ“œλŠ” κΉƒν—ˆλΈŒμ—μ„œ 확인 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
You can find the full code on GitHub.

https://github.com/gideonslife01/flm-nextjs

πŸ“ 전체 ν”„λ‘œμ νŠΈ ꡬ쑰 / Overall Project Structure

myapp29/  
β”œβ”€β”€ app/  (Next.js App Router)
β”‚   β”œβ”€β”€ .well-known/webfinger/route.ts  -> webfinger
β”‚   β”œβ”€β”€ api/follow/route.ts    -> Follow API(temporary)
β”‚   β”œβ”€β”€ api/announce/route.ts  -> Boost(Announcement)
β”‚   β”œβ”€β”€ api/like/route.ts.     -> Like API
β”‚   β”œβ”€β”€ api/posts/route.ts     -> Writing API
β”‚   β”œβ”€β”€ api/timeline/route.ts  -> Timeline API
β”‚   β”œβ”€β”€ users/[username]/
β”‚   β”‚   β”œβ”€β”€ statuses/[id]/route.ts -> Indivisual Post
β”‚   β”‚   β”œβ”€β”€ route.ts               -> Acotr Information
β”‚   β”‚   β”œβ”€β”€ followers/route.ts     -> Followers List
β”‚   β”‚   β”œβ”€β”€ following/route.ts     -> Following List
β”‚   β”‚   β”œβ”€β”€ inbox/route.ts         -> Inbox
β”‚   β”‚   └── outbox/route.ts        -> outbox
β”‚   β”œβ”€β”€ usersui/[username]/
β”‚   β”‚   β”œβ”€β”€ page.tsx               -> Timeline UI
β”‚   β”‚   └── _components/themes/
β”‚   β”‚       β”œβ”€β”€ themeex/ThemeexTheme.tsx   -> Example Theme
β”‚   β”‚       β”œβ”€β”€ pinafore/PinaforeTheme.tsx -> Theme 1
β”‚   β”‚       β”œβ”€β”€ mastodon/MastodonTheme.tsx -> Theme 2
β”‚   β”‚       └── minimal/MinimaltTheme.tsx  -> Theme 3
β”‚   β”œβ”€β”€ layout.tsx, page.tsx, globals.css
β”‚   └── favicon.ico
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ theme.tsx              -> Theme Provider
β”‚   β”œβ”€β”€ watchThemes.ts         -> Check real-time theme changes
β”‚   β”œβ”€β”€ ap.ts                  -> Follow Accept
β”‚   └── db.ts                  -> DB connection
β”œβ”€β”€ data/
β”‚   └── keys/                  -> private.pem, public.pem
β”œβ”€β”€ data.sqlite                -> Database
β”œβ”€β”€ Caddyfile                  -> https 
β”œβ”€β”€ instrumentation.ts         -> Background Server
└── package.json

πŸ“ ν”„λ‘œμ νŠΈ μ‹œμž‘(myapp29)
Project Start (myapp29)

npx create-next-app@latest

πŸ“ SQLiteμ„€μΉ˜ / Installing SQLite

cd ~/myapp29
npm install better-sqlite3
npm install -D @types/better-sqlite3

πŸ“ chokidar μ„€μΉ˜ / Installing chokidar

— μ‹€μ‹œκ°„ 파일 및 폴더변경 κ°μ‹œκΈ°λŠ₯
Real-time file and folder change monitoring function

npm install -D chokidar

πŸ“ DDNS,httpsμ„€μ • / DDNS,https settings

πŸ“ λ„λ©”μΈν—ˆμš©, @userid μ‚¬μš©ν•˜κΈ°
Allowing domains, using @userid

— next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* allow domain*/
  allowedDevOrigins: ['aloy-horizon.duckdns.org', '*.duckdns.org'],

  /* host.domain.org/@userid */
  async rewrites() {
    return [
      {
        source: '/@:username',
        destination: '/usersui/:username',
      },
    ]
  }
};


export default nextConfig;

πŸ“ 이전 ν”„λ‘œμ νŠΈμ—μ„œ μ‹ κ·œ ν”„λ‘œμ νŠΈλ‘œ 볡사할 파일
Files to copy from the previous project to the new project

myapp project/  
β”œβ”€β”€ app/  (Next.js App Router)
β”‚   β”œβ”€β”€ .well-known/webfinger/route.ts  -> webfinger
β”‚   β”œβ”€β”€ api/                  -> API
β”‚   β”œβ”€β”€ users/                -> Inbox,Outbox,Post,Actor
β”‚   β”œβ”€β”€ usersui/              -> Timeline UI,Theme
β”‚   β”œβ”€β”€ layout.tsx            -> Privoder
β”œβ”€β”€ lib/
β”œβ”€β”€ data/                     -> Key
β”œβ”€β”€ data.sqlite               -> Database
β”œβ”€β”€ Caddyfile                 -> https 
β”œβ”€β”€ next.config.ts            -> DDNS,@
└── instrumentation.ts        -> Background Server

πŸ“ ν…Œμ΄λΈ” μˆ˜μ • / Modify Table

βœ”οΈ announces ν…Œμ΄λΈ”μ— actorν•„λ“œλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.
Add an ‘actor’ field to the ‘announces’ table.

ALTER TABLE announces ADD COLUMN actor TEXT;
CREATE INDEX IF NOT EXISTS idx_announces_actor ON announces(actor);
CREATE UNIQUE INDEX IF NOT EXISTS idx_announces_actor_object ON announces(actor, object);

πŸ“ μ½”λ“œ μˆ˜μ • / Code Modification

βœ”οΈ lib/db.ts

... ...
db.exec(`
... ...
  CREATE TABLE IF NOT EXISTS users (  
-- myapp29 βœ…  Boost(Announcement)
  CREATE TABLE IF NOT EXISTS announces (
    id TEXT PRIMARY KEY, 
    username TEXT, 
    object TEXT, 
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    actor TEXT
  );
`);
db.exec(`
  ... ...
    -- myapp29 βœ…
  CREATE INDEX IF NOT EXISTS idx_announces_actor ON announces(actor);
  CREATE UNIQUE INDEX IF NOT EXISTS idx_announces_actor_object ON announces(actor, object);
`);

βœ”οΈ app/api/timeline/route.ts

— boostλŠ”κΈ°μ‘΄ μ½”λ“œμœ μ§€, μ•„λž˜μ½”λ“œλŠ” μ’‹μ•„μš” λΆ€λΆ„ μ½”λ“œ μˆ˜μ •
The ‘boost’ option preserves the existing code, while the code below modifies the ‘like’ section.

export async function GET(req: Request) {
... ...  
// λ‚΄ Like - actor μ •ν™•νžˆ / Accurate actor for my Like
  const myActor = `https://${DOMAIN}/users/${username}`;
  const myLikes = db.prepare(`SELECT object FROM likes WHERE actor =?`).all(myActor) as any[];
... ...
    // βœ… shortId μ•ˆμ „ν•˜κ²Œ! / shortId safely!
    const shortId = getShortId(fullId);
    if (!shortId) {
      return {...p, fullId, isMyBoost: false, boostCount: 0, isMyLike: false, likeCount: 0 };
    }
... ...
   // βœ… μ’‹μ•„μš” - shortId 기반 / Like - based on shortId
    const isMyLike = likedSet.has(shortId);

    const likeCount = (db.prepare(
      `SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`
    ).get(shortId) as any)?.c || 0;
}

βœ”οΈ app/api/like/route.ts

— μ’‹μ•„μš” λ²„νŠΌ λˆ„λ₯Όλ•Œ λ°μ΄ν„°λ² μ΄μŠ€μ— μ €μž₯된 값을 기쑴으둜 μ„€μ •
Set the initial value based on the data stored in the database when the “Like” button is clicked.

  // βœ… DB 체크: λ‚΄κ°€ 이미 λˆŒλ €μœΌλ©΄ 1 λ”ν•˜μ§€ μ•ŠμŒ! / DB check: If I already liked, do not add 1!
  const existing = db.prepare(`SELECT id FROM likes WHERE actor =? AND object LIKE '%' ||? || '%'`).get(myActor, shortId) as any;
  if (existing) {
    const likeCount = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`).get(shortId) as any).c;
    return NextResponse.json({ ok: true, alreadyLiked: true, isMyLike: true, likeCount });
  }

βœ”οΈ usersui/[username]/page.tsx

— 전체 DB κΈ°μ€€μœΌλ‘œ μˆ˜μ •
Modified based on the entire database

const handleBoost = async (post: any) => { ... ... }  
const handleLike = async (post: any) => { ... ...}

βœ”οΈapp/api/inbox/route.ts

— actν•„λ“œ 빠진것 μΆ”κ°€, Undo λ‘κ°œ μžˆλŠ”κ²ƒ μˆ˜μ •,actor + shortId둜 μ‚­μ œ ν•˜λ„λ‘ μˆ˜μ •
Added the missing ‘act’ field, fixed the duplicate ‘Undo’ entry, and modified the deletion logic to use ‘actor’ + ‘shortId’.

export async function POST(req: Request, { params }: { params: Promise<{ username: string }> }) {

... ...
if (body.type === 'Undo') { ... ...}
if (body.type === 'Create') { ... ... }
if (body.type === 'Like') { ... ... }
if (body.type === 'Announce') { ... ...

            db.prepare(`INSERT OR IGNORE INTO inbox_posts (id, actor, content, username, original_id, created_at) VALUES (?,?,?,?,?,?)`)
              .run(shortId, actorId, content, username, longId, body.published || new Date().toISOString());
          }
if (body.type === 'Follow') { ... ... }
... ...
 }

}

βœ”οΈ usersui/[username]/theme/pinafore/PinaforeTheme.tsx

— λ²„νŠΌ 및 μΉ΄μš΄ν„° μΆ”κ°€ / Add buttons and counters

<button onClick={() => onBoost(p)} className={p.isMyBoost? 'boosted' : ''}>πŸ” {p.boostCount || ''}</button>
<button onClick={() => onLike(p)} className={p.isMyLike? 'liked' : ''}>⭐ {p.likeCount || ''}</button>

πŸ“ ν…ŒμŠ€νŠΈ / Test

βœ”οΈ ν…ŒμŠ€νŠΈλŠ” λ§ˆμŠ€ν† λˆ μ„œλ²„μ—μ„œ κ³„μ •μƒμ„±ν•΄μ„œ ν…ŒμŠ€νŠΈν–ˆμŠ΅λ‹ˆλ‹€.
The testing was conducted by creating an account on a Mastodon server.

βœ”οΈ 라우트 / route

βœ”οΈ λ§ˆμŠ€ν† λˆ 계정 μ£Όμ†Œ

https://mastodon.social/@freelifemakers

βœ”οΈ pinafore

likes

βœ”οΈ mastodon

Mastodon

βœ”οΈ aloy-horizon.duckdns.org

@user1@aloy-horizon.duckdns.org
@user1@aloy-horizon.duckdns.org

μš”ν•œλ³΅μŒ 8μž₯ 32절 / John 8:32

“그리고 λ„ˆν¬λŠ” 진리λ₯Ό μ•Œκ²Œ 될 것이며, 진리가 λ„ˆν¬λ₯Ό 자유둭게 ν•  것이닀.”

“Then you will know the truth ,and the truth will set you free”

Leave a Reply