[nextjs]SNS Server-10(myapp20)

๐Ÿ‘‰๐Ÿป ์ด์ „ ํ”„๋กœ์ ํŠธ๋Š” inbox_postsํ…Œ์ด๋ธ”์˜ ๊ฐœ๋ณ„๊ธ€ API๋ฅผ ๋ณด๋ ค๋ฉด ํ„ฐ๋ฏธ๋„์„ ์‚ฌ์šฉํ•ด์•ผํ–ˆ์Šต๋‹ˆ๋‹ค.
In the previous project, you had to use the terminal to view the API for individual posts in the inbox_posts table.

๐Ÿ‘‰๐Ÿป myapp20 ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ posts๋‚˜ inbox_postsํ…Œ์ด๋ธ”์˜ API๊ฐ’๋„ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
In the myapp20 project, we are modifying the setup so that API values โ€‹โ€‹from the posts or inbox_posts tables can also be viewed in the browser.

๐Ÿ“ ์ „์ฒด ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ / Overall Project Structure

myapp20/  
โ”œโ”€โ”€ 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

๐Ÿ“ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘(myapp20)
Project Start (myapp20)

npx create-next-app@latest

๐Ÿ“ SQLite์„ค์น˜ / Installing SQLite

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

๐Ÿ“ DDNS,https์„ค์ • / DDNS,https settings


๐Ÿ“ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ํ™•์ธ ๋ฐ ์ˆ˜์ •
Review and modify database schema

โœ”๏ธ ๋‹ค๋ฅธ ์„œ๋ฒ„์—์„œ ๋ฐ›๋Š” ๊ธ€(inbox_posts) ํ…Œ์ด๋ธ”์—์„œ id๊ฐ€ ๋„ˆ๋ฌด ๊ธธ์–ด์„œ ์งง๊ฒŒ ์ค„์ด๊ธฐ ์œ„ํ•œ ์ž‘์—…์ž…๋‹ˆ๋‹ค.
This task aims to shorten the id in the inbox_posts table, which stores posts received from other servers, because the current ID is too long.

โœ”๏ธ URL๋ถ€๋ถ„์„ ํฌํ•จํ•œ ์ „์ฒด๋ฅผ id๋ฅผ original_id ํ•„๋“œ์— ๋”ฐ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
The entire string, including the URL portion, is stored separately in the original_id field.

โœ”๏ธ original_id๋Š” gotosocial์ด๋‚˜ mastodon์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  id๋Š” ๊ด€๋ฆฌ์šฉ์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
original_id is used for communication with GoToSocial or Mastodon servers, while id is used for administrative purposes.

โœ”๏ธ inbox_post์˜ ์Šคํ‚ค๋งˆ(ํ…Œ์ด๋ธ”)๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์ง€ ์•Š๋‹ค๋ฉด ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
Modify the schema (table) for inbox_post if it does not match the following.

CREATE TABLE inbox_posts (
  id TEXT PRIMARY KEY,
  actor TEXT,
  content TEXT,
  username TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
, original_id TEXT);

— ํ…Œ์ด๋ธ” ์ˆ˜์ •ํ•˜๊ธฐ(์ด์ „์— ์ˆ˜์ •ํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด…)
Modify the table (if you haven’t already…)

ALTER TABLE inbox_posts ADD COLUMN original_id TEXT;

๐Ÿ“ ์ฝ”๋“œ ์ˆ˜์ • / Code modification

โœ”๏ธ myapp20/app/users/[username]/inbox

    if (body.type === 'Create') {
      const note = body.object;
      if (note && note.type === 'Note') {
        console.log(`๐Ÿ“ [${username}] ์ƒˆ ๊ธ€ ๋„์ฐฉ / New post received from ${actorId}`);
        console.log(`๋‚ด์šฉ/content: ${note.content?.slice(0, 100)}`);

        // โœ… myapp20 -inbox_posts์ฃผ์†Œ ์ค„์ด๊ธฐ / Shorten inbox_posts URL
        try {
          const longId = note.id || `https://remote/${Date.now()}-${Math.random()}`;
          const shortId = longId.split('/').pop()!; // ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„๋งŒ ์ž˜๋ผ๋‚ด๊ธฐ / Cut off only the last part
          const content = note.content || '';

          db.prepare(`
            INSERT OR IGNORE INTO inbox_posts (id, actor, content, username, original_id, created_at) 
            VALUES (?, ?, ?, ?, ?, ?)
          `).run(shortId, actorId, content, username, longId, note.published || new Date().toISOString());
          console.log(`โœ… [${username}] inbox_posts ์ €์žฅ ์™„๋ฃŒ/inbox_posts saved successfully. : ${shortId} (์›๋ณธ/original: ${longId})`);

        } catch (e) {
          console.error(`โŒ ๊ธ€ ์ €์žฅ ์‹คํŒจ / Failed to save post`, e);
        }

      }
      return new Response('', { status: 202 });
    }

โœ”๏ธ /myapp20/app/usersui/[username]/page.tsx

... ...

// โœ… myapp20
function getDisplayName(actorOrUsername: string) {
  if (!actorOrUsername) return 'unknown';
  // https://freelifemakers.com/users/user1 -> user1@freelifemakers.com
  if (actorOrUsername.startsWith('https://')) {
    try {
      const url = new URL(actorOrUsername);
      const username = url.pathname.split('/').pop() || 'user';
      return `${username}@${url.hostname}`;
    } catch {
      return actorOrUsername.split('/').pop() || actorOrUsername;
    }
  }
  return actorOrUsername;
}

... ...

  return (
    <div style={{ padding: 20, maxWidth: 900, margin: '0 auto' }}>
      <h1 style={{ fontSize: 24, marginBottom: 16 }}>{username} ํƒ€์ž„๋ผ์ธ / Timeline</h1>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {timeline.map((p:any) => (
          <li key={`${p.source}-${p.id}`} style={{
            display: 'flex',
            alignItems: 'center',
            gap: 6,
            padding: '10px 0',
            borderBottom: '1px solid #eee',
            flexWrap: 'wrap'
          }}>
            <span style={{ background: p.source === 'mine'? '#dbeafe' : '#fef3c7', padding: '2px 6px', borderRadius: 4, fontSize: 11 }}>
              {p.source}
            </span>
            <b>{getDisplayName(p.actor)}:</b>
            <span dangerouslySetInnerHTML={{__html: p.content}} />
            <span style={{ color: '#999', fontSize: 11, marginLeft: 'auto' }}>
              {new Date(p.created_at).toLocaleString()}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );

๐Ÿ“ ํ…Œ์ŠคํŠธ / Test

โœ”๏ธ inbox_posts์— ๊ธฐ์กด์˜ ๊ธ€์ด ์žˆ๋‹ค๋ฉด ๋‹ค ์ง€์›๋‹ˆ๋‹ค.(https:// ๋•Œ๋ฌธ์—…)
Delete any existing posts in inbox_posts (due to https://โ€ฆ)

— gotosocial์„œ๋ฒ„(freelifemakers.com)์„œ๋ฒ„์˜ ๊ธ€๋„ ์ง€์›๋‹ˆ๋‹ค.
Posts on the GoToSocial server (freelifemakers.com) are also being deleted.

sqlite> select * from inbox_posts;
https://freelifemakers.com/users/user1/statuses/01M0EJPP57WC47FAK0N395S7MB|https://freelifemakers.com/users/user1|<p>inbox_posts table test</p>|user1|2026-07-10 01:16:15|
https://freelifemakers.com/users/user1/statuses/01M0KMS8K1XRFW703S32MX6V25|https://freelifemakers.com/users/user1|<p>actor test</p>|user1|2026-07-19T10:20:10+02:00|https://freelifemakers.com/users/user1/statuses/01M0KMS8K1XRFW703S32MX6V25
sqlite> delete from inbox_posts;

โœ”๏ธ pinafore์—์„œ ์ƒˆ๋กœ ๊ธ€์„ ์“ฐ๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
Write a new post in Pinafore and check the database.

— pinafore.socical

pinafore.social

— sqlite database(aloy-horizon.duckdns.org)

sqlite> select * from inbox_posts;
01M0P8VYEQTFW28N97W6MG8PY5|https://freelifemakers.com/users/user1|<p>reduce inbox_posts id</p>|user1|2026-07-20T06:10:11+03:00|https://freelifemakers.com/users/user1/statuses/01M0P8VYEQTFW28N97W6MG8PY5
sqlite> 

— gotosocial server( freelifemakers.com )

gotosocial

โœ”๏ธ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋‹ค์Œ ์ฃผ์†Œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
Open the following address in your browser.

— inbox_posts ํ…Œ์ด๋ธ”์˜ ๊ธ€ ๋ณด๊ธฐ
View posts from the inbox_posts table

https://aloy-horizon.duckdns.org/users/user1/statuses/01M0P8VYEQTFW28N97W6MG8PY5
inbox_posts table

— posts ํ…Œ์ด๋ธ”์˜ ๊ธ€ ๋ณด๊ธฐ
View a post from the ‘posts’ table

https://aloy-horizon.duckdns.org/users/user1/statuses/e6c99652-572f-491a-9e61-05e67b0d58fc
posts table

— ํƒ€์ž„๋ผ์ธUI / Timeline UI

https://aloy-horizon.duckdns.org/@user1
TImeline UI

๐Ÿ“ ์ง€๊ธˆ๊นŒ์ง€ ์ž‘์—…ํ•œ ๋‚ด์šฉ๊ณผ ๋ผ์šฐํŠธ ์ž…๋‹ˆ๋‹ค.
Here is the work done so far and the routes.

โœ”๏ธ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ผ์šฐํŠธ
Routes that can be tested in a browser

1. WebFinger
https://aloy-horizon.duckdns.org/.well-known/webfinger?resource=acct:user1@aloy-horizon.duckdns.org

2. Actor (ํ”„๋กœํ•„ / Profile)
https://aloy-horizon.duckdns.org/users/user1

3. Followers (๋‚จ์ด ๋‚˜ ํŒ”๋กœ์šฐ / Someone follows me)
https://aloy-horizon.duckdns.org/users/user1/followers

4. Following (๋‚ด๊ฐ€ ๋‚จ์„ ํŒ”๋กœ์šฐ / I follow others)
https://aloy-horizon.duckdns.org/users/user1/following

5. Outbox (๋‚ด ๊ธ€ ๋ชฉ๋ก / My post list)
https://aloy-horizon.duckdns.org/users/user1/outbox

6. ํƒ€์ž„๋ผ์ธ / Timeline

- ํƒ€์ž„๋ผ์ธUI / Timeline UI
https://aloy-horizon.duckdns.org/usersui/user1
or
https://aloy-horizon.duckdns.org/@user1

- ํƒ€์ž„๋ผ์ธAPI / Timeline API
https://aloy-horizon.duckdns.org/api/timeline?username=user1

7. ๊ฐœ๋ณ„ ๊ธ€(๊ธ€ ์ƒ์„ธ) / indivisual posts(Post Details)[posts, inbox_posts]
https://aloy-horizon.duckdns.org/users/user1/statuses/[ID]

ex)
# posts table
https://aloy-horizon.duckdns.org/users/user1/statuses/e6c99652-572f-491a-9e61-05e67b0d58fc

# inbox_posts table
https://aloy-horizon.duckdns.org/users/user1/statuses/01M0P8VYEQTFW28N97W6MG8PY5

โœ”๏ธ ํ„ฐ๋ฏธ๋„์—์„œ๋งŒ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๋ผ์šฐํŠธ
Routes that can only be tested 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 Fediverse! #test"}'

์š”ํ•œ๋ณต์Œ 8์žฅ 32์ ˆ / John 8:32

“๊ทธ๋ฆฌ๊ณ  ๋„ˆํฌ๋Š” ์ง„๋ฆฌ๋ฅผ ์•Œ๊ฒŒ ๋  ๊ฒƒ์ด๋ฉฐ, ์ง„๋ฆฌ๊ฐ€ ๋„ˆํฌ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ํ•  ๊ฒƒ์ด๋‹ค.”

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

Leave a Reply