[nextjs]SNS Server-31(myapp41) 

👉🏻 myapp41에서는 SNS메인페이지에 헤더부분과 팔로우버튼을 추가합니다.
In myapp41, a header section and a “Follow” button are added to the social media main page.

👉🏻 전체 코드는 깃허브에서 확인 할 수 있습니다.
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
│   ├── api/v1/accounts/[id]/followers/route.ts -> followers
│   ├── api/v1/accounts/[id]/following/route.ts -> following
│   ├── api/auth/signup/route.ts        -> Signup
│   ├── api/auth/login/route.ts         -> Login
│   ├── api/auth/logout/route.ts        -> Logout
│   ├── api/auth/refresh/route.ts       -> Refresh Token
│   ├── api/auth/me/route.tsx           -> Login Check
│   ├── auth/signup/page.tsx            -> Signup UI
│   ├── auth/signup/page.tsx            -> Login UI
│   ├── 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


📁 코드 수정 / Code Modification

✔️ 프로필 헤더 추가,실시간 카운트 반영
Added profile header; implemented real-time count updates.

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

... ...
function FollowButton({ username, initialFollowing, currentUser, onToggle }: {
... ...
  return (
    <button
      onClick={toggle}
      disabled={loading}
      className={following? 'profile-edit-btn' : 'profile-follow-btn'}
    >
      {following? 'Following' : 'Follow'}
    </button>
  );
 }

export function PinaforeTheme({ timeline, username, onBoost, onLike }: any) {

  return (
    <>
     <style>{``}</style>
    </>
... ...
         {/* ✅ myapp41 - Timeline Header */}
         <div className="profile-header"> ... </div>
... ...
                <div>
                  {/* - 로그인하면 프로필 편집 로그인 후 팔로우버튼 / Log in to edit profile; follow button appears after logging in. */ }
                  {isMe? (
                    <>
                    {/* 팔로우,언팔로우 버튼 / follow, unfollow button */ }
                    {/* <FollowButton username={username} initialFollowing={profile?.following || false} currentUser={currentUser} /> */}
                    <button className="profile-edit-btn">프로필 편집/Edit Profile</button>
                    </>
                  ) : ( 
                      <FollowButton username={username} 
                        initialFollowing={profile?.following || false} 
                        currentUser={currentUser} 
                        onToggle={handleFollowToggle} 
                      />)}
                </div>
 )
}

✔️ user1으로 아이디 고정 수정, 팔로우 확인 json필드 추가
Fixed the user ID to ‘user1’ and added a JSON field to check follow status.

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

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

    // parameter User - ✅ myapp41
    const me = getUsernameFromRequest(req);
    // const tempUserid = req.headers.get('x-user') || 'user1';  // ✅ 로그인 이전 임시 아이피 변수 / Temporary ID variable before login


// 팔로우 여부 / Follow status
    let isFollowing = false;
    if (me && me!== targetUsername) {
      const f = db.prepare(`SELECT 1 FROM following WHERE username =? AND actor LIKE ?`).get(me, `%${targetUsername}%`) as any;
      console.log(`✅ accounts-username:${me},actor:${targetUsername}`);
      if (f) isFollowing = true;
    }
    console.log(`me:${me},targetUsername:${targetUsername}`);
    return NextResponse.json({
      id: rawId,
      username: targetUsername,
      acct: `${targetUsername}@${DOMAIN}`,
      display_name: userRow?.display_name || targetUsername,
      avatar: `https://${DOMAIN}/icon.png`,
      avatar_static: `https://${DOMAIN}/icon.png`,
      header: `https://picsum.photos/seed/${DOMAIN}_${targetUsername}/1200/400`,
      header_static: `https://picsum.photos/seed/${DOMAIN}_${targetUsername}/1200/400`,
      followers_count: cntFollowers?.cnt || 0,
      following_count: cntFollowing?.cnt || 0,
      statuses_count: cntPosts?.cnt || 0,
      note: userRow?.summary? `<p>${userRow.summary}</p>` : `<p>${targetUsername}</p>`,
      url: `https://${DOMAIN}/users/${targetUsername}`,
      emojis: [], fields: [], bot: false,
      created_at: userRow?.created_at || new Date().toISOString(),
      following: isFollowing, // ✅ FollowButton용!
      is_me: me === targetUsername
    }, { headers: { 'Access-Control-Allow-Origin': '*' } });

  } catch (e) {
    console.error(e);
    return NextResponse.json({ error: 'failed' }, { status: 500, headers: { 'Access-Control-Allow-Origin': '*' , 'Cache-Control': 'no-store, no-cache, must-revalidate',} });
  }
... ...
}

✔️ 쿠키 디코딩 실패 수정 / Fixed cookie decoding failure.

— lib/auth.ts

// ✅ myapp39 + myapp40 + myapp41 - username return from request
export function getUsernameFromRequest(req: Request | NextRequest): string | null { ... 
    // token 쿠키/Cookie
    const tokenCookie = cookies['token'];
    if (tokenCookie) {
      const decoded = verifyToken(tokenCookie);
      if (decoded?.username) {
        console.log(`✅ me from token (verify): ${decoded.username}`);
        return decoded.username;
      }
      const payload = decodeJwtPayloadNoVerify(tokenCookie);
      if (payload?.username) {
        console.log(`✅ me from token (no-verify): ${payload.username}`);
        return payload.username;
      }
    }
...
}

// ✅ myapp41 - verify 없이 payload만 읽는 함수
// A function that reads only the payload without verification.
function decodeJwtPayloadNoVerify(token: string): any {
  try {
    const parts = token.split('.');
    if (parts.length!== 3) return null;
    let payload = parts[1];
    payload = payload.replace(/-/g, '+').replace(/_/g, '/');
    while (payload.length % 4) payload += '=';
    const json = Buffer.from(payload, 'base64').toString('utf-8');
    return JSON.parse(json);
  } catch {
    return null;
  }
}

✔️ 팔로우나 언팔로우시 한쪽 테이블만 정보 추가 삭제되는것 수정
Fixed the issue where information was added or deleted in only one table when following or unfollowing.

— /api/follow/route.ts

...
// 팔로우 API / follow API
export async function POST(req: Request) {
    // ===== 1. 로컬 팔로우 / Local Follow =====
    if (localTarget) {
      // following: 내가 팔로우하는 목록
      // Following: The list of people I follow
      try {
        db.prepare(`INSERT OR IGNORE INTO following (id, actor, inbox, username) VALUES (?,?,?,?)`)
         .run(id, targetActor, `${targetActor}/inbox`, username);
      } catch {
        // 구 스키마 호환 (inbox 컬럼 없는 버전)
        // Legacy schema compatibility (version without the 'inbox' column)
        db.prepare(`INSERT OR IGNORE INTO following (id, actor, username) VALUES (?,?,?)`)
         .run(id, targetActor, username);
      }

      // followers: 상대방의 팔로워 목록
      // followers: The other party's list of followers
      try {
        db.prepare(`INSERT OR IGNORE INTO followers (id, actor, inbox, username) VALUES (?,?,?,?)`)
         .run(`${localTarget}-${username}-${Date.now()}`, myActor, `${myActor}/inbox`, localTarget);
      } catch {
        db.prepare(`INSERT OR IGNORE INTO followers (id, actor, username) VALUES (?,?,?)`)
         .run(`${localTarget}-${username}-${Date.now()}`, myActor, localTarget);
      }
    }
    // ===== 2. 리모트 팔로우 / Remote Follow =====
    const actorRes = await signedFetch(target, username);
}

// 언팔로우 / Unfollow
export async function DELETE(req: Request) {
... ...
    if (localTarget) {
      // 로컬은 DB에서만 삭제 / Delete from the database only (local).
      db.prepare(`DELETE FROM following WHERE username=? AND (actor LIKE? OR actor=?)`).run(username, `%/users/${localTarget}%`, localTarget);
      db.prepare(`DELETE FROM followers WHERE username=? AND (actor LIKE? OR actor=?)`).run(localTarget, `%/users/${username}%`, username);
      console.log(`🗑 로컬 언팔로우: ${username} -X-> ${localTarget}`);
      return Response.json({ ok: true, local: true });
    }
... ...
    // 리모트 언팔로우 / remote unfollow
... ...
    const { sendUndoFollow } = await import('@/lib/ap');
    const result = await sendUndoFollow(target, username);

    db.prepare('DELETE FROM following WHERE username=? AND (actor=? OR actor LIKE?)').run(username, target, `%${target}%`);

}

📁 테스트 / Test

✔️ user1로그인하고 user2 팔로우
Log in as user1 and follow user2.

/@user2
/@user1

✔️ user1로그인하고 user2 언팔로우
Log in as user1 and unfollow user2.

/@user1
@user2

✔️ 언팔로우 서버로그 / Unfollow server log

Server Log

✔️ Database

sqlite> SELECT username, actor FROM following;
user1|https://aloy-horizon.duckdns.org/users/user2
user1|https://freelifemakers.com/users/user1
user1|https://mastodon.social/users/freelifemakers
sqlite> 
sqlite> 
sqlite> SELECT username, actor FROM followers;
user2|https://aloy-horizon.duckdns.org/users/user1
user1|https://freelifemakers.com/users/user1
user1|https://mastodon.social/ap/users/117193449750714993
sqlite> 

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

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

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

Leave a Reply