👉🏻 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.


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


✔️ 언팔로우 서버로그 / Unfollow 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”