[nextjs]SNS Server-24(myapp34) 

👉🏻 회원가입 및 로그인 기능입니다.
These are the sign-up and login functions.

👉🏻 로그인 후 pinafore에서 내 서버(aloy-horizon.duckdns.org)가 검색되도록 합니다.
After logging in, ensure that my server (aloy-horizon.duckdns.org) is discoverable in Pinafore.

👉🏻 로그인 후 pinafore 타임라인에서 내 서버의 글을 볼 수 있습니다.
After logging in, you can view posts from your server on the Pinafore timeline.

👉🏻 전체 코드는 깃허브에서 확인 할 수 있습니다.
You can find the full code on GitHub.

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

📁 전체 프로젝트 구조 / Overall Project Structure

myapp project/  
├── 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
│   ├── api/v1/instance/route.ts -> auth
│   ├── api/v1/apps/route.ts.    -> auth
│   ├── api/v1/accounts/verify_credentials/route.ts -> auth
│   ├── api/v1/statuses/route.ts -> Write Post
│   ├── api/v1/timelines/home/route.ts -> timeline
│   ├── oauth/authorize/route.ts -> auth
│   ├── oauth/token/route.ts -> auth
│   ├── api/v1/search/home/route.ts -> search
│   ├── api/v2/search/home/route.ts -> search
│   ├── 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
│   ├── auth.ts                -> Authentication, User Management
│   ├── ap.ts                  -> Follow,Undo,Create,Likes,Announce
│   └── db.ts                  -> DB connection
├── data/
│   ├── keys/userIDs/          -> private.pem, public.pem(New)
│   └── keys/                  -> private.pem, public.pem(legacy)
├── data.sqlite                -> Database(1/3)
├── data.sqlite-wal            -> Database(2/3)
├── data.sqlite-shm            -> Database(3/3)
├── Caddyfile                  -> https 
├── instrumentation.ts         -> Background Server
└── package.json

📁 프로젝트 시작 / Project Start


📁 자동생성된 테이블 / Automatically generated table

CREATE TABLE outbox (
          id TEXT PRIMARY KEY,
          type TEXT,
          actor TEXT,
          object TEXT,
          created_at INTEGER
        );

📁 검색기능 라우트 추가 / Add search function route

✔️ app/api/v1/search/route.ts – 하위 호환용 / For backward compatibility

# v2 코드와 동일함 / Same as the v2 code.

✔️ app/api/v2/search/route.ts – 실제 사용되는 라우트 / Routes actually used

// ✅ myapp34
import { NextResponse } from 'next/server';
import db from '@/lib/db';

const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

export async function GET(req: Request) { ... }

✔️ 로컬 서버 테스트 / Local Server Test(aloy-horizon.duckdns.org)

— V1, V2 모두 동일한 코드 사용하면 됨
You can use the same code for both V1 and V2.

— V1

curl "https://aloy-horizon.duckdns.org/api/v1/search?q=user1" -H "Authorization: Bearer YOUR_TOKEN"
curl "https://aloy-horizon.duckdns.org/api/v1/search?q=user1" -H "4847d5c9-efdd-450f-b724-01c28676d6ea433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"
{"accounts":[{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","url":"https://aloy-horizon.duckdns.org/users/user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"}],"statuses":[],"hashtags":[]}% 

— V2

curl "https://aloy-horizon.duckdns.org/api/v2/search?q=user1" -H "Authorization: Bearer YOUR_TOKEN"
gimdaegyeong@gimdaegyeong-ui-MacBookAir ~ % curl "https://aloy-horizon.duckdns.org/api/v2/search?q=user1" -H "Authorization: Bearer 4847d5c9-efdd-450f-b724-01c28676d6ea433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"

{"accounts":[{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","url":"https://aloy-horizon.duckdns.org/users/user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"}],"statuses":[],"hashtags":[]}%  

✔️ 리모트 서버 테스트 / Remote Server Test

— pinafore.social에서 직접 검색 / Search directly on pinafore.social

Mastodon
GTS(freelifemakers.com)

✔️ app/api/v1/accounts/[id]/route.ts – 프로필 정보 / Profile information

— 리모트 서버는 pinafore.social에서 검색한 유저를 의미합니다.
A remote server refers to a user found on pinafore.social.

— 로컬서버(aloy-horizon),리모트서버(다른 서버의 유저)
Local server (aloy-horizon), remote server (user on another server)

//✅ myapp34 - Profile info.

import { NextResponse } from 'next/server';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) { ... ...

    // ✅ 리모트/remote!
    if (id.includes('mastodon.social') || id.includes('https') || id.startsWith('remote_')) { ... ...}
... ...

    // ✅ 로컬/local
    const username = rawId === '1'? 'user1' : rawId.replace('user', '')? `user${rawId}` : 'user1';
    const headerUrl = `https://picsum.photos/seed/${username}_${DOMAIN}/1200/400`;

    return NextResponse.json({ ... ... })

}

✔️ app/api/v1/accounts/[id]/statuses/route.ts – 유저가 작성한 글 / User-written post

— 리모트 서버는 pinafore.social에서 검색한 유저를 의미합니다.
A remote server refers to a user found on pinafore.social.

— 로컬서버(aloy-horizon),리모트서버(다른 서버의 유저)
Local server (aloy-horizon), remote server (user on another server)

// ✅ myapp34 - User-written post 
import { NextResponse } from 'next/server';
import db from '@/lib/db';

const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) { 
... ...

 // ✅ 리모트/remote (https:// 또는 remote_ 또는 mastodon 포함!) / 
    if (id.startsWith('https://') || id.startsWith('remote_') || id.includes('mastodon.social')) { ... ...}

  // ✅ 로컬/local
    const username = rawId === '1'? 'user1' : `user${rawId}`;
    console.log(`🏠 로컬! username=${username}`);
... ...

}

✔️ 테스트 / Test

curl "https://aloy-horizon.duckdns.org/api/v1/accounts/1/statuses" -H "Authorization: Bearer TOKEN"
# 터미널에서 실행 / Run in the terminal
curl "https://aloy-horizon.duckdns.org/api/v1/accounts/1/statuses" -H "Authorization: Bearer 4847d5c9-efdd-450f-b724-016d6eac2867a433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"

# 응답 / response
[{"id":"73a7e43b-20000-40dc-3hj4kac-1aa123fda326","uri":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","url":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","account":{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"},"content":"<p>aloy first pinafore.social post test</p>","created_at":"2026-09-06T23:06:23.998Z","visibility":"public","reblogs_count":0,"favourites_count":0},{"id":"4ace7a75-3a40-494f-8dea-bf3f688b6cfb", ... ...

# 나머지 데이터 생략
The remaining data is omitted.
local server profile

📁 코드수정 / Code Modification

✔️ app/api/v1/statuses/route.ts

— 글쓰기 라우트(DB저장 기능 추가)
Writing route (added DB save functionality)

// app/api/v1/statuses/route.ts - 글쓰기 DB저장 / Creating status and saving to DB
// ✅ myapp34-2

import { NextResponse } from 'next/server';
import db from '@/lib/db';
import { randomUUID } from 'crypto';

const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

export async function POST(req: Request) {
... ...
// posts table
      db.prepare(`INSERT INTO posts (id, username, content, created_at) VALUES (?,?,?,?)`)
        .run(id, username, content, now);
    } catch (e) { console.error('posts 저장 에러/posts save error', e); }

... ...

// outbox table
      db.prepare(`INSERT INTO outbox (id, type, actor, object, created_at) VALUES (?,?,?,?,?)`)
        .run(id, 'Create', `https://${DOMAIN}/users/${username}`, JSON.stringify(note), now);
    } catch (e) { console.error('outbox 저장 에러 / outbox save error', e); }

}

✔️ 테스트 / Test

— pinafore.social에서 글을 작성한 경우 아래 처럼 DB에 저장됩니다.

sqlite> select * from posts;
e6c99652-572f-491a-9e61-05e67b0d58fc|Hello! my SNS!!|2026-08-14 04:59:51|user1
dcece9ee-8d4a-4f63-91e1-22fc60513f62|Hello! my SNS!!|2026-08-14 05:00:18|user1
cc30330e-7040-4a1b-9e60-731066b8817e|Hello! my SNS3!!|2026-08-17 23:08:05|user1
e6d532c8-974b-4a01-89f6-6e1b2c4cff32|Hello Fediverse! #test|2026-08-20 02:16:04|user1
aea0ec89-a368-44a9-8493-b2bd82a797a8|post test! #test|2026-08-20 03:08:18|user1
9b2ead85-4b15-4df9-9de4-04c4a7fbcab4|Hello! Mastodon!!|2026-09-01 02:50:10|user1
aefc7ff8-5b38-4fb7-829d-9b14343cec6a|Hello local UI #apiposts|2026-09-05 03:39:37|user1
4ace7a75-3a40-494f-8dea-bf3f688b6cfb|Hello Activity UI #activityposts|2026-09-05 03:39:48|user1
73a7e43b-24b4-40dc-8ebc-1aa123fda326|aloy first pinafore.social post test|1788735983998|user1
sqlite> 
sqlite> select * from outbox;
73a7e43b-24b4-40dc-8ebc-1aa123fda326|Create|https://aloy-horizon.duckdns.org/users/user1|{"id":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","type":"Note","content":"<p>aloy first pinafore.social post test</p>","attributedTo":"https://aloy-horizon.duckdns.org/users/user1","published":"2026-09-06T23:06:23.998Z","to":["https://www.w3.org/ns/activitystreams#Public"]}|1788735983998
sqlite> 
local server posts

 ✔️ app/api/v1/timelines/home/route.ts

— pinafore에 보여줄 타임라인 / Timeline to display on Pinafore

// ✅ myapp34 - app/api/v1/timelines/home/route.ts 
// - pinafore 홈 타임라인 API / pinafore Home Timeline API

import { NextResponse } from 'next/server';
import db from '@/lib/db';

const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

export async function GET(req: Request) { ... }
export async function OPTIONS() { ... }

— 테스트 / Test

curl "https://aloy-horizon.duckdns.org/api/v1/timelines/home" -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# 터미널에서 실행 /  Run in the terminal
% curl "https://aloy-horizon.duckdns.org/api/v1/timelines/home" -H "Authorization: Bearer 4847d5c9-efdd-450f-b724-01c28676d6ea433c1b89-a4ac-437b-a41a-39fc2e6bdfa6"

# 응답 / response
[{"id":"73a7e43b-0000-1234-0000-1000000","uri":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","url":"https://aloy-horizon.duckdns.org/users/user1/statuses/73a7e43b-24b4-40dc-8ebc-1aa123fda326","account":{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"},"content":"<p>aloy first pinafore.social post test</p>","created_at":"2026-09-06T23:06:23.998Z","visibility":"public","reblogs_count":0,"favourites_count":0},{"id":"4ace7a75-3a40-494f-8dea-bf3f688b6cfb", ... ...

# 나머지 데이터 생략
The remaining data is omitted.
Timeline

📁 리모트 서버 상세 페이지 임시데이터
Remote Server Detail Page Placeholder Data

✔️ 지금 pinafore.social에서 다른서버(리모트서버)를 검색해서 상세페이지를 보면 다음처럼 임시데이터가 들어가 있습니다.
If you search for another server (remote server) on pinafore.social right now and view its details page, you will see placeholder data like this.

✔️ 다음 프로젝트에서 수정 예정입니다.
This is scheduled to be modified in the next project.

remote mastodon

📁 전체 과정 요약 / Summary of the entire process

1. 글쓰기 / Write Post
- Pinafore에서 글쓰면 내 DB에 저장됨.
When you write on Pinafore, it gets saved to my database.

- pinafore에서 POST /api/v1/statuses 
POST /api/v1/statuses in pinafore

- app/api/v1/statuses/route.ts


2. 홈 타임라인 / Home Timeline
- 내 글과 inbox글(받은 글을 DB에서 꺼내서 보여 줌.
My posts and inbox posts (retrieved from the database and displayed).

- Pinafore에서 GET /api/v1/timelines/home
GET /api/v1/timelines/home in pinafore

- app/api/v1/timelines/home/route.ts

3. 검색 / Searh
- pinafore에서 검색
Search in pinafore

- Pinafore에서 GET /api/v2/search?q=david@freelifemakers.com
GET /api/v2/search?q=david@freelifemakers.com in pinafore

- webfinger + actor fetch! id=remote_freelifemakers_com_david 리턴/return

- app/api/v2/search/route.ts


4. 검색한 유저 프로필 / Searched user profile
- 검색한 유저 상세 페이지 프로필
Searched User Detail Page Profile

- Pinafore예서 GET /api/v1/accounts/remote_freelifemakers_com_david

- app/api/v1/accounts/[id]/route.ts 

5. 검색한 유저 글 / Posts by the searched user

- 검색한 유저 상세 페이지 글
Searched User Detail Page Profile

- Pinafore에서 GET /api/v1/accounts/remote_freelifemakers_com_david/statuses

- app/api/v1/accounts/[id]/statuses/route.ts 

요한복음 8장 32절 / John 8:32

“그리고 너희는 진리를 알게 될 것이며, 진리가 너희를 자유롭게 할 것이다.”

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

Leave a Reply