[nextjs]SNS Server-18(myapp28)

👉🏻 myapp28에는 테마파일에 부스트버튼을 추가합니다.
In myapp28, a boost button is added to the theme file.

👉🏻 내가 쓴 글에는 부스트 할 수 없습니다.
You cannot boost your own posts.

👉🏻 내가 부스트 버튼을 누르고 다시 한번 더 누르면 부스트 취소가 됩니다.
If I press the boost button and then press it again, the boost is cancelled.

👉🏻 내가 부스트 한 글을 다른 사람이 부스트하면 카운터가 증가합니다.
When someone else boosts a post I have boosted, the counter increases.

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

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

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

myapp28/  
├── 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

📁 프로젝트 시작(myapp28)
Project Start (myapp28)

npx create-next-app@latest

📁 SQLite설치 / Installing SQLite

cd ~/myapp28
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

📁 코드 수정 / Code Modification


💡 app/api/announce/route.ts

— 자기자신의 글은 부스트하지 못해게 기본적으로 막혀 있습니다.
By default, you are prevented from boosting your own posts.

— target은 데이터베이스가 가리키는 주소이고 DOMAIN은 내서버의 주소입니다.
‘target’ is the address pointed to by the database, while ‘DOMAIN’ is the address of my server.

— 두개의 값이 일치하면 오류를 발생시킵니다.
An error is raised if the two values ​​match.

    if (target.startsWith(`https://${DOMAIN}/users/${username}`)) {
      return NextResponse.json({ ok: false, error: 'cannot boost own post' }, { status: 400 });
    }

✔️ app/usersui/[username]/page.tsx

— 기존 코드는 inbox_posts에 대한 부분을 고려하지 않아서 “cannot boost own post”오류를 발생시킵니다.
The existing code does not account for inbox_posts, causing a “cannot boost own post” error.

  const handleBoost = async (post: any) => {
    const targetId = post.original_id || post.id;
    const fullTarget = targetId.startsWith('http')
     ? targetId
      : `https://${process.env.NEXT_PUBLIC_DOMAIN || 'aloy-horizon.duckdns.org'}/users/${post.username}/statuses/${targetId}`;

    await fetch(`/api/announce`, {
      method: post.isMyBoost? 'DELETE' : 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, target: fullTarget })
    });
    await refreshTimeline();
  };

— myapp28에서 배달된글(inbox_posts)에대한 부분을 수정했습니다.
I modified the section regarding posts delivered to myapp28 (inbox_posts).

 ... ... 
 useEffect(() => {
    params.then(p => {
      setUsername(p.username);
      fetch(`/api/timeline?username=${p.username}`)
       .then(r => r.json())
       .then(data => {
          console.log('📦 timeline', data[0]); // ✅ myapp28
          setTimeline(data);
          setLoading(false);
        });
    });
  }, [params]);
... ...
 // ✅ myapp28
 const handleBoost = async (post: any) => {
  let target = post.original_id || post.id;
  console.log('🔍 원본 post 데이터/original data', post); 

  if (!target.startsWith('http')) {
    // inbox 글이면 actor가 진짜 주인!
    // actor = https://freelifemakers.com/users/user1 같은 형태일 수 있음
    // If it's an inbox message, the actor is the actual owner!
    // The actor might be in a format like https://freelifemakers.com/users/user1

    if (post.actor && post.actor.startsWith('http')) {
      // actor URL에서 username 추출 / Extract username from actor URL
      try {
        const actorUrl = new URL(post.actor);
        const parts = actorUrl.pathname.split('/');
        const usersIdx = parts.indexOf('users');

        if (usersIdx !== -1) {
        // inbox_posts
          const actorUsername = parts[usersIdx + 1];
          target = `${actorUrl.origin}/users/${actorUsername}/statuses/${target}`;
        } else {
        // posts
          target = `https://${process.env.NEXT_PUBLIC_DOMAIN || 'aloy-horizon.duckdns.org'}/users/${post.username}/statuses/${target}`;
        }
      } catch {
        target = `https://${process.env.NEXT_PUBLIC_DOMAIN || 'aloy-horizon.duckdns.org'}/users/${post.username}/statuses/${target}`;
      }
    } else {
      target = `https://${process.env.NEXT_PUBLIC_DOMAIN || 'aloy-horizon.duckdns.org'}/users/${post.username}/statuses/${target}`;
    }
  }
  
  console.log('🔁 boost click', { target, isMyBoost: post.isMyBoost });

  // Optimistic UI
  setTimeline(prev => prev.map(p =>
    p.id === post.id
      ? {...p, isMyBoost:!p.isMyBoost, boostCount: (p.boostCount || 0) + (p.isMyBoost? -1 : 1) }
      : p
  ));

  try {   
    const res = await fetch(`/api/announce`, {
      method: post.isMyBoost? 'DELETE' : 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, target })
    });

    const data = await res.json();
    console.log('✅ announce res', data);
    if (!res.ok) throw new Error(data.error);
    await refreshTimeline();
  } catch (e) {
    console.error('❌ boost 실패', e);
    await refreshTimeline();
  }
};

✔️ app/usersui/[username]/inbox

— 부스트 취소할 때 원본 글 삭제 금지처리
Prevent deletion of the original post when cancelling a boost.

      else if (objType === 'Announce') {
         const announceId = typeof obj === 'object' ? obj.id : null;
         const objectId = typeof obj.object === 'string' ? obj.object : obj.object?.id;
         if (announceId) {
           db.prepare('DELETE FROM announces WHERE id = ?').run(announceId);
           
           // ✅ myapp28 - Announce Undo시 원본 글 삭제 금지 처리 / Prevent deletion of the original post when performing "Announce Undo."
           //db.prepare('DELETE FROM inbox_posts WHERE original_id = ? AND actor = ?').run(objectId, body.actor);
           console.log(`🗑 [${username}] Announce 취소: ${announceId}`);
         }
      }

✔️ app/api/timeline/route.ts

— isMyBoost , boostCount 추가해서 부스트 했는지 여부를 체크하고 부스트 된 횟수를 추가했습니다.
Added isMyBoost and boostCount to track whether a boost has been applied and to record the number of boosts.

  // ✅ myapp28
  const enriched = timeline.map((p: any) => {
    // 풀 URL 만들기 / Create full URL
    let fullId = p.original_id || p.id;
    if (!fullId.startsWith('http')) {
      fullId = `https://${DOMAIN}/users/${p.username}/statuses/${fullId}`;
    }
    
    // inbox 글은 original_id가 이미 풀 URL일 수 있음
    // The original_id of an inbox post might already be a full URL.
    let fullOriginalId = p.original_id || p.id;
    if (p.original_id && !p.original_id.startsWith('http')) {
      fullOriginalId = `https://${DOMAIN}/users/${p.username}/statuses/${p.original_id}`;
    }

    const isMyBoost = boostedSet.has(p.original_id) || 
                      boostedSet.has(p.id) || 
                      boostedSet.has(fullId) ||
                      boostedSet.has(fullOriginalId) ||
                      (p.original_id && boostedSet.has(p.original_id));

    const boostCount = (db.prepare(`SELECT COUNT(*) as c FROM announces WHERE object = ? OR object = ? OR object = ?`)
      .get(fullId, p.original_id || '', p.id || '') as any)?.c || 0;

    return {
      ...p,
      fullId, // 디버깅용 / For debugging purposes
      isMyBoost,
      boostCount
    };
  });

  return NextResponse.json(enriched); // ✅ myapp28 - 버그 수정 / Bug fixes

✔️app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx

— import 오류 수정 / Fix import error

import type { ThemeName } from '../themeNames'; // ✅ myapp28
import { themeNames } from '../themeNames';

— 부스트 카운터 / Boost Counter

<button onClick={() => onBoost(p)} className={p.isMyBoost? 'boosted' : ''}>🔁 {p.boostCount || ''}</button>

📁 테스트 / Test

✔️ pinafore에서 쓴 글 내 서버로 배달되는지 확인 및 부스트 하기, 카운터 확인
Check if posts from Pinafore are being delivered to my server, boost them, and check the counter.

Boost1

✔️ 내가 부스트 한글이 pinafore에 도착하는지 확인
Check if my Boost Hangul arrives at Pinafore.

Boost Message

✔️ pinafore에서 부스트하기 / Boosting in Pinafore

Pinafore Boost

✔️ 카운터 증가 확인 / Verify counter increment

Boost Counter

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

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

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

Leave a Reply