ππ» 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”