[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