[nextjs]SNS Server-12(myapp22)

πŸ‘‰πŸ» νŒ”λ‘œμž‰κ³Ό νŒ”λ‘œμ›Œκ°€ ν…Œμ΄λΈ”μ— μ €μž₯λ˜μ§€ μ•ŠλŠ” 문제λ₯Ό μˆ˜μ •ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
Fixed an issue where following and follower data were not being saved to the table.

πŸ‘‰πŸ» IGNORE INTOλ₯Ό μΆ”κ°€ν•΄μ„œ 데이터 μ€‘λ³΅μž…λ ₯ λ°œμƒμ‹œ μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚€μ§€ μ•Šκ³  μ§€λ‚˜κ°‘λ‹ˆλ‹€.
By adding IGNORE INTO, the operation proceeds without throwing an error when duplicate data is encountered.

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

myapp22/  
β”œβ”€β”€ app/  (Next.js App Router)
β”‚   β”œβ”€β”€ .well-known/webfinger/route.ts  -> webfinger
β”‚   β”œβ”€β”€ api/posts/route.ts     -> Writing API
β”‚   β”œβ”€β”€ api/follow/route.ts    -> Follow API(temporary)
β”‚   β”œβ”€β”€ 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
β”‚   β”œβ”€β”€ layout.tsx, page.tsx, globals.css
β”‚   └── favicon.ico
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ ap.ts                  -> Follow Accept
β”‚   └── db.ts                  -> DB connection
β”œβ”€β”€ data/
β”‚   └── keys/                  -> private.pem, public.pem
β”œβ”€β”€ data.sqlite                -> Database
β”œβ”€β”€ Caddyfile                  -> https 
└── package.json

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

npx create-next-app@latest

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

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

πŸ“ 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;

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

βœ”οΈ myapp22/app/users/[username]/inbox/route.ts

      // βœ… myapp22 - insert or ignore
      try {
        db.prepare('INSERT OR IGNORE followers (id, actor, inbox, username) VALUES (?,?,?,?)')
          .run(body.id, actorId, inboxUrl, username);
        console.log(`βœ… [${username}] νŒ”λ‘œμš° μ €μž₯ / follow save : ${actorId}`);
      } catch (e: any) {
        if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') {
          console.log(`ℹ️ 쀑볡 νŒ”λ‘œμ›Œ 차단 / Block Duplicate Followers (UNIQUE): ${actorId}`);
          return new Response('', { status: 202 });
        }
        throw e;
      }

βœ”οΈ myapp22/api/follow/route.ts

    // myapp22 βœ… 
    // λ°μ΄ν„°λ² μ΄μŠ€μ— 쀑볡 μž…λ ₯ λ°©μ§€ / Preventing duplicate entries in the database
    if (result.ok) {
      try {
        db.prepare('INSERT OR IGNORE INTO following (id, actor, username) VALUES (?, ?, ?)')
          .run(result.followDoc.id, target, username);
        console.log(`βœ… DB μ €μž₯ / DB Save: ${username} -> ${target}`);
      } catch (e: any) {
        if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') {
          console.log(`ℹ️ 이미 μ €μž₯됨  / Already saved: ${target}`);
        } else {
          console.error('DB μ €μž₯ μ‹€νŒ¨ / DB save failed:', e);
        }
      }
    }

βœ”οΈ myapp22/lib/db.ts

— DBμŠ€ν‚€λ§ˆ 재반영(ν”„λ‘œμ νŠΈ μ½”λ“œ μ°Έμ‘°)
Re-apply DB schema (refer to project code)

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

βœ”οΈ ν„°λ―Έλ„μ—μ„œ μ•„λž˜μ˜ 라우트둜 ν…ŒμŠ€νŠΈ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
You can test using the route below in the terminal.

# νŒ”λ‘œμš° (following 1둜 증가)
# Follow (following increased to 1)

curl -X POST https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://freelifemakers.com/users/user1"}'

# μ–ΈνŒ”λ‘œμš° (following 0으둜 κ°μ†Œ)
# Unfollow (following count reduced to 0)

curl -X DELETE https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://freelifemakers.com/users/user1"}'

# κΈ€ μ“°κΈ° (outbox 7둜 증가 + νŒ”λ‘œμ›Œλ“€μ—κ²Œ 배달)
# Writing a post (added to outbox 7 + delivered to followers)

curl -X POST https://aloy-horizon.duckdns.org/api/posts \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","content":"Hello Follow! #test"}'

βœ”οΈ https://freelifemakers.com/users/user1 μ—κ²Œ νŒ”λ‘œμš°λ₯Ό μš”μ²­ν•˜λ©΄ following ν…Œμ΄λΈ”μ— 데이터 μ €μž₯λ˜λŠ”μ§€ν™•μΈν•©λ‹ˆλ‹€.
Check if data is saved to the following table when a follow request is sent to https://freelifemakers.com/users/user1.

# νŒ”λ‘œμš° μš”μ²­ / Follow request
myapp22 % curl -X POST https://aloy-horizon.duckdns.org/api/follow \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","target":"https://freelifemakers.com/users/user1"}'

# 응닡 / response
{"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/1787615594327","type":"Follow","actor":"https://aloy-horizon.duckdns.org/users/user1","object":"https://freelifemakers.com/users/user1"}}%                                

# λ°μ΄ν„°λ² μ΄μŠ€μ— 데이터 μ €μž₯ μƒνƒœ 확인 / Checking the data storage status in the database
myapp22 % sqlite3 data.sqlite "SELECT * FROM following;"
https://aloy-horizon.duckdns.org/users/user1/follows/1787615594327|https://freelifemakers.com/users/user1|user1|2026-08-24 23:53:14
gimdaegyeong@gimdaegyeong-ui-MacBookAir myapp22 % 

βœ”οΈ freelifemakers.com/@user1(pinafore)이 aloy-horizon.ducdns.orgμ—κ²Œ νŒ”λ‘œμš°λ₯Ό μš”μ²­ν•˜λ©΄ followerν…Œμ΄λΈ”μ— μ €μž₯λ©λ‹ˆλ‹€.
When freelifemakers.com/@user1(pinafore) requests to follow aloy-horizon.ducdns.org, the request is stored in the follower table.

# sqlite db 접속 / Connect to SQLite database
myapp22 % sqlite3 data.sqlite
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.
sqlite> select * from followers;
https://freelifemakers.com/users/user1/follow/01RR6E0D7QZA98S06VJYC99FNH|https://freelifemakers.com/users/user1|https://freelifemakers.com/users/user1/inbox|user1
sqlite> 

βœ”οΈ API 확인 / Check API

— 배열에 μ €μž₯된 값이 좜λ ₯λ˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
Check whether the values ​​stored in the array are output.

— following

https://aloy-horizon.duckdns.org/users/user1/following
following API

— followers

https://aloy-horizon.duckdns.org/users/user1/followers
followers API

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

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

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

Leave a Reply