[nextjs]SNS Server-50(myapp60)Β 

πŸ‘‰πŸ» myapp60μ—μ„œλŠ” νŽ˜μ΄μ§€λ„€μ΄μ…˜ κΈ°λŠ₯ μΆ”κ°€λ‘œ 인해 λ°œμƒν•œ μ’‹μ•„μš”,λΆ€μŠ€νŠΈ λΆ€λΆ„ 였λ₯˜λ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€.
In myapp60, we are fixing errors related to the “Like” and “Boost” features that arose from the addition of pagination.

πŸ‘‰πŸ» νŽ˜μ΄μ§€λ„€μ΄μ…˜ 뢀뢄을 μ™„λ£Œ ν•©λ‹ˆλ‹€.
I will complete the pagination section.

πŸ‘‰πŸ» μ—‘ν‹°λΈŒν…Œλ§ˆ μžλ™μ „ν™˜μ΄ λ˜μ§€ μ•ŠλŠ” 문제λ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€.
Fixes an issue where the active theme does not switch automatically.

πŸ‘‰πŸ» 전체 μ½”λ“œλŠ” κΉƒν—ˆλΈŒμ—μ„œ 확인 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
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
β”‚   β”œβ”€β”€ hooks/useCursorPagination.ts    -> Pagination Hook
β”‚   β”œβ”€β”€ 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(deprecated)
β”‚   β”œβ”€β”€ api/timeline/route.ts  -> Timeline API
β”‚   β”œβ”€β”€ api/followinglist/route.ts  -> followinglist API
β”‚   β”œβ”€β”€ api/followerslist/route.ts  -> followerslist 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_back/route.ts -> timeline(Depreacted)
β”‚   β”œβ”€β”€ api/v1/media/route.ts -> Image upload
β”‚   β”œβ”€β”€ api/v1/media/[id]/route.ts  -> Image Delete 
β”‚   β”œβ”€β”€ api/v1/instance/rules/route.ts -> Feature not implemented.
β”‚   β”œβ”€β”€ api/v1/instance/domain_blocks/route.ts -> Feature not implemented.
β”‚   β”œβ”€β”€ api/v1/filters/route.ts -> Feature not implemented.
β”‚   β”œβ”€β”€ api/v1/lists/route.ts -> Feature not implemented.
β”‚   β”œβ”€β”€ api/v1/announcements/route.ts -> Feature not implemented.
β”‚   β”œβ”€β”€ api/v1/timelines/_public_back/route.ts -> Feature not implemented.(Deprecated)
β”‚   β”œβ”€β”€ api/v1/timelines/[type]/route.ts -> Timeline API
β”‚   β”œβ”€β”€ api/v1/statuses/[id]/context/route.ts -> Post Check,Delete
β”‚   β”œβ”€β”€ oauth/authorize/route.ts -> auth
β”‚   β”œβ”€β”€ oauth/token/route.ts -> auth
β”‚   β”œβ”€β”€ oauth/login-and-authorize/route.ts -> Tursky,Pinafore login processing
β”‚   β”œβ”€β”€ oauth/revoke/route.ts -> Feature not implemented.
β”‚   β”œβ”€β”€ api/v1/search/home/route.ts -> search
β”‚   β”œβ”€β”€ api/v2/search/home/route.ts -> search
β”‚   β”œβ”€β”€ api/v2/notifications/policy/route.ts-> Feature not implemented.
β”‚   β”œβ”€β”€ api/v1/accounts/[id]/followers/route.ts -> followers
β”‚   β”œβ”€β”€ api/v1/accounts/[id]/following/route.ts -> following
β”‚   β”œβ”€β”€ api/v1/statuses/[id]/route.ts  -> Post delete(pinafore)
β”‚   β”œβ”€β”€ api/v1/statuses/[id]/favourite/route.ts -> favourite(for pinafore)
β”‚   β”œβ”€β”€ api/v1/statuses/[id]/unfavourite/route.ts -> unfavourite(for pinafore)
β”‚   β”œβ”€β”€ api/v1/statuses/[id]/reblog/route.ts  -> reblog[boost](for pinafore)
β”‚   β”œβ”€β”€ api/v1/statuses/[id]/unreblog/route.ts  -> unreblog[unboost](for pinafore)
β”‚   β”œβ”€β”€ api/auth/signup/route.ts        -> Website Signup
β”‚   β”œβ”€β”€ api/auth/login/route.ts         -> Website Login
β”‚   β”œβ”€β”€ api/auth/logout/route.ts        -> Website Logout
β”‚   β”œβ”€β”€ api/auth/refresh/route.ts       -> Refresh Token
β”‚   β”œβ”€β”€ api/cleanup-orphan/route.ts     -> post,outbox clean up
β”‚   β”œβ”€β”€ 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(Server, 404)
β”‚   β”‚   β”œβ”€β”€ ClientPage.tsx.        -> Timeline UI(Client,Call Active Themes)
β”‚   β”‚   └── _components/themes/
β”‚   β”‚       β”œβ”€β”€ themeex/ThemeexTheme.tsx      -> Example Theme
β”‚   β”‚       β”œβ”€β”€ themeex/FollowersList.tsx     -> Theme Example, FollowersList
β”‚   β”‚       β”œβ”€β”€ themeex/FollowingList.tsx     -> Theme Example, FollowingList
β”‚   β”‚       β”œβ”€β”€ themeex/NotificationsList.tsx -> Theme Example, NotificationsList
β”‚   β”‚       β”œβ”€β”€ themeex/TimelineList.tsx      -> Theme Example, TimelineList
β”‚   β”‚       β”œβ”€β”€ pinafore/PinaforeTheme.tsx -> Theme 1
β”‚   β”‚       β”œβ”€β”€ pinafore/FollowersList.tsx -> Theme 1, FollowersList
β”‚   β”‚       β”œβ”€β”€ pinafore/FollowingList.tsx -> Theme 1, FollowingList
β”‚   β”‚       β”œβ”€β”€ pinafore/NotificationsList.tsx -> Theme 1, NotificationsList
β”‚   β”‚       β”œβ”€β”€ pinafore/TimelineList.tsx  -> Theme 1, TimelineList
β”‚   β”‚       β”œβ”€β”€ 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
β”‚   β”œβ”€β”€ visibility.ts          -> Visibility(Public,Unlisted,Private,Direct)
β”‚   └── 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
β”œβ”€β”€ proxy.ts                   -> middleware.ts - >proxy.ts(for Nextjs16)
β”œβ”€β”€ postcss.config.mjs         -> for Nextjs16
└── package.json

πŸ“ ν”„λ‘œμ νŠΈ μ‹œμž‘ / Project Start


πŸ“ μ’‹μ•„μš” + λΆ€μŠ€νŠΈ μˆ˜μ • / Modify Like + Boost

βœ”οΈ 라우트 μΆ”κ°€ / Add route


app/api/v1/statuses/[id]/favourite/route.ts

app/api/v1/statuses/[id]/unfavourite/route.ts 

app/api/v1/statuses/[id]/reblog/route.ts 

app/api/v1/statuses/[id]/unreblog/route.ts

βœ”οΈ app/api/v1/statuses/[id]/favourite/route.ts

// βœ… myapp60 - favorite for pinafore

export const dynamic = 'force-dynamic';
import { NextRequest, NextResponse } from 'next/server';
import db from '@/lib/db';
import { getActorData, sendLike, signedFetch } from '@/lib/ap';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

function getUser(req: Request) {
  const auth = req.headers.get('Authorization')?.replace('Bearer ','').trim();
  if (auth) {
    const o = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(auth) as any;
    if (o?.username) return o.username;
  }
  const cookie = req.headers.get('cookie') || '';
  const m = cookie.match(/refresh_token=([^;]+)/);
  if (m) {
    const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(m[1]) as any;
    if (s?.username) return s.username;
  }
  return null;
}

export async function POST(req: NextRequest, { params }: { params: Promise<{id: string}> }) {
  const authUser = getUser(req);
  if (!authUser) return NextResponse.json({ error: 'Unauthorize' }, { status: 401 });
  const username = authUser;
  const { id } = await params;

  let target = id;
  let post: any = db.prepare(`SELECT * FROM posts WHERE id=?`).get(id) as any;
  if (post) {
    target = `https://${DOMAIN}/users/${post.username}/statuses/${post.id}`;
  } else {
    post = db.prepare(`SELECT * FROM inbox_posts WHERE id=? OR original_id=? OR original_id LIKE '%' ||?|| '%'`).get(id, id, id) as any;
    if (post?.original_id) target = post.original_id;
    else if (post) target = post.original_id || id;
    else target = id.startsWith('http')? id : `https://${DOMAIN}/users/${username}/statuses/${id}`;
  }

  const myActor = `https://${DOMAIN}/users/${username}`;
  let apObjectId = target.replace('/posts/', '/statuses/');
  let inbox: string | null = null;
  const isOwn = apObjectId.includes(`${DOMAIN}/users/${username}/`);

  if (!isOwn) {
    try {
      const pr = await signedFetch(apObjectId, username);
      if (pr.ok) {
        const d = await pr.json();
        apObjectId = (d.id || apObjectId).replace('/posts/', '/statuses/');
        const info = await getActorData((d.attributedTo || d.actor) as any, username);
        inbox = info.inbox;
      }
    } catch {}
  }

  const shortId = apObjectId.split('/').pop()!.split('?')[0].split('#')[0];
  const existing = db.prepare(`SELECT id FROM likes WHERE actor=? AND object LIKE '%' ||?|| '%'`).get(myActor, shortId) as any;
  if (existing) {
    const c = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||?|| '%'`).get(shortId) as any).c;
    return NextResponse.json({ id: id, favourited: true, favourites_count: c, reblogged: false });
  }

  let likeDoc: any;
  try {
    if (isOwn ||!inbox) {
      likeDoc = { id: `${myActor}/likes/${crypto.randomUUID()}`, actor: myActor, object: apObjectId };
    } else {
      likeDoc = (await sendLike(inbox!, apObjectId, username)).likeDoc;
    }
  } catch (e: any) {
    console.log(`⚠ sendLike μ‹€νŒ¨/sendLike failed, λ‘œμ»¬μ—λ§Œ μ €μž₯/Save locally only: ${e.message}`);
    likeDoc = { id: `${myActor}/likes/${crypto.randomUUID()}`, actor: myActor, object: apObjectId };
  }

  db.prepare('INSERT OR IGNORE INTO likes (id, actor, object, username) VALUES (?,?,?,?)')
  .run(likeDoc.id, myActor, likeDoc.object.replace('/posts/', '/statuses/'), username);

  const c = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||?|| '%'`).get(shortId) as any).c;
 console.log(`πŸ’œ v1-favourite`);
  return NextResponse.json({
    id: id,
    favourited: true,
    favourites_count: c,
    reblogged: false,
    account: { id: username },
  });
}

βœ”οΈ app/api/v1/statuses/[id]/unfavourite/route.ts

// βœ… myapp60 - for pinafore - unfavourite

export const dynamic = 'force-dynamic';
import { NextRequest, NextResponse } from 'next/server';
import db from '@/lib/db';
import { getActorData, sendUndoLike, signedFetch } from '@/lib/ap';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';

function getUser(req: Request) {
  const auth = req.headers.get('Authorization')?.replace('Bearer ','').trim();
  if (auth) {
    const o = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(auth) as any;
    if (o?.username) return o.username;
  }
  const cookie = req.headers.get('cookie') || '';
  const m = cookie.match(/refresh_token=([^;]+)/);
  if (m) {
    const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(m[1]) as any;
    if (s?.username) return s.username;
  }
  return null;
}

export async function POST(req: NextRequest, { params }: { params: Promise<{id: string}> }) {
  const authUser = getUser(req);
  if (!authUser) return NextResponse.json({ error: 'Unauthorize' }, { status: 401 });
  const username = authUser;
  const { id } = await params;

  let target = id;
  let post: any = db.prepare(`SELECT * FROM posts WHERE id=?`).get(id) as any;
  if (post) {
    target = `https://${DOMAIN}/users/${post.username}/statuses/${post.id}`;
  } else {
    post = db.prepare(`SELECT * FROM inbox_posts WHERE id=? OR original_id=? OR original_id LIKE '%' ||?|| '%'`).get(id, id, id) as any;
    if (post?.original_id) target = post.original_id;
    else target = id.startsWith('http')? id : `https://${DOMAIN}/users/${username}/statuses/${id}`;
  }

  const myActor = `https://${DOMAIN}/users/${username}`;
  let apObjectId = target.replace('/posts/', '/statuses/');
  let inbox: string | null = null;
  const isOwn = apObjectId.includes(`${DOMAIN}/users/${username}/`);

  if (!isOwn) {
    try {
      const pr = await signedFetch(apObjectId, username);
      if (pr.ok) {
        const d = await pr.json();
        apObjectId = (d.id || apObjectId).replace('/posts/', '/statuses/');
        const info = await getActorData((d.attributedTo || d.actor) as any, username);
        inbox = info.inbox;
      }
    } catch {}
  }

  const shortId = apObjectId.split('/').pop()!.split('?')[0].split('#')[0];
  const likeRow = db.prepare(`SELECT id, object FROM likes WHERE actor=? AND object LIKE '%' ||?|| '%'`).get(myActor, shortId) as any;

  if (likeRow &&!isOwn && inbox) {
    try {
      await sendUndoLike(inbox, likeRow.id, likeRow.object, username);
      console.log(`πŸ’” [${username}] Undo Like 전솑 -> ${inbox}`);
    } catch (e: any) {
      console.log(`⚠ Undo Like μ‹€νŒ¨, 둜컬만 μ‚­μ œ: ${e.message}`);
    }
  }

  if (likeRow) {
    db.prepare(`DELETE FROM likes WHERE id=?`).run(likeRow.id);
  } else {
    // id둜 직접 λ§€μΉ­ μ•ˆλ˜λ©΄ shortId LIKE둜 ν•œλ²ˆ 더 μ‚­μ œ
    // If direct matching by ID fails, perform an additional deletion using `shortId LIKE`.
    db.prepare(`DELETE FROM likes WHERE actor=? AND object LIKE '%' ||?|| '%'`).run(myActor, shortId);
  }

  const c = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||?|| '%'`).get(shortId) as any).c;
 console.log(`πŸ’” v1-unfavourite`);
  return NextResponse.json({
    id: id,
    favourited: false,
    favourites_count: c,
    reblogged: false,
  });
}

βœ”οΈ app/api/v1/statuses/[id]/reblog/route.ts – κΉƒν—ˆλΈŒ μ°Έμ‘° / Refer to GitHub.

βœ”οΈ app/api/v1/statuses/[id]/unreblog/route.ts – κΉƒν—ˆλΈŒ μ°Έμ‘° / Refer to GitHub.


πŸ“ ν…Œλ§ˆ λ³€κ²½ μˆ˜μ • / Theme Change Modification

βœ”οΈ ν…Œλ§ˆκ°€ λ³€κ²½λ˜μ–΄λ„ Pinafore ν…Œλ§ˆλ‘œ κ³ μ •λ˜λŠ” 문제λ₯Ό μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.
Fixed an issue where the theme remained locked to “Pinafore” even after the theme was changed.

βœ”οΈ μƒˆλ‘œμš΄ ν…Œλ§ˆλ₯Ό λ§Œλ“€μ–΄μ„œ μ—…λ‘œλ“œν•˜λ©΄ ν…Œλ§ˆνŒŒμΌμ΄ μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•˜κ³  index.tsνŒŒμΌμ„ μƒμ„±ν•©λ‹ˆλ‹€.
When you create and upload a new theme, the system checks for the existence of the theme file and generates an index.ts file.

βœ”οΈ ν…Œλ§ˆλ””λ ‰ν† λ¦¬ λͺ…은 자유둭게 μˆ˜μ •κ°€λŠ₯ν•˜κ³  ν…Œλ§ˆλ””λ ‰ν† λ¦¬ λ‚΄μ˜ νŒŒμΌμ΄λ¦„μ€ λͺ¨λ‘ λ™μΌν•˜κ²Œ μ μš©ν•΄μ•Όν•©λ‹ˆλ‹€.
You are free to rename the theme directory, but the filenames within the directory must remain exactly the same.

βœ”οΈ ν…Œλ§ˆλ₯Ό μ‚­μ œν•˜λŠ” κ²½μš°λŠ” κ΄€λ¦¬μž λͺ¨λ“œμ—μ„œ μ‚­μ œν•˜λ„λ‘ μ²˜λ¦¬ν•  μ˜ˆμ •μž…λ‹ˆλ‹€.
We plan to handle theme deletion via the administrator mode.

— lib/watchThemas.ts

for (const folderName of folders) {
... ...
      // write code to index.ts themeName.ts files
      const indexContent = `'use client';\n ${imports}
export const themeRegistry = {
${registry}} as const;
export type ThemeName = keyof typeof themeRegistry;
export function getThemeComponent(name: ThemeName) {
  return themeRegistry[name]?.component;
}
`;
      const namesContent = `export const themeNames = ${JSON.stringify(validFolders)} as const;
export type ThemeName = typeof themeNames[number];
`;
}
... ...  

// βœ… myapp60
  if (process.env.NODE_ENV!== 'production') {
    let t: NodeJS.Timeout;
    chokidar.watch(themesDir, {
      depth: 1,
      ignoreInitial: true,
      ignored: (p: string) => {
        const b = path.basename(p);
        return b === 'index.ts' || b === 'themeNames.ts';
      }
    }).on('all', (event) => {
      // Check addDir, unlinkDir, add, unlink
      clearTimeout(t);
      t = setTimeout(generate, 1000); // βœ… 1초 λŒ€κΈ° / Wait 1 second
    });
  }

πŸ“ νŽ˜μ΄μ§€ λ„€μ΄μ…˜ ν…ŒμŠ€νŠΈ / Pagination Test

 βœ”️ μž„μ‹œλΌμš°νŠΈ 생성 / Create temporary route

— ν•œλ²ˆμ— λ§Žμ€ 글을 μ“°κ³  λ™μ‹œμ‚­μ œν•˜λŠ” 라우트 μž…λ‹ˆλ‹€.
This is a route for writing multiple posts at once and deleting them simultaneously.

1)api/test/bulk/route.ts

# 1. κΈ€ 50개 생성 (둜그인 μƒνƒœμ—μ„œ) / Generate 50 posts (while logged in)
curl -X POST https://aloy-horizon.duckdns.org/api/test/bulk \
  -H "Content-Type: application/json" \
  -b "refresh_token=YOUR_TOKEN" \
  -d '{"count":50}'

# 2. μ‚­μ œ / delete
curl -X DELETE https://aloy-horizon.duckdns.org/api/test/bulk \
  -H "Content-Type: application/json" \
  -b "refresh_token=YOUR_TOKEN" \

— 리λͺ¨νŠΈ μ„œλ²„κΈ€ κ°•μ œ μ‚­μ œ 라우트
Route for forced deletion of remote server posts

1) api/test/nuke-remote/route.ts

2) 기본적으둜 λ‘œμ»¬μ„œλ²„μ™€ 리λͺ¨νŠΈ μ„œλ²„μ˜ 글이 μΌμΉ˜ν•˜μ§€ μ•ŠμœΌλ©΄ μ‚­μ œ ν•  수 μ—†μŠ΅λ‹ˆλ‹€.
By default, you cannot delete the content if the versions on the local server and the remote server do not match.

3) μ•„λž˜λŠ” λ‘œμ»¬μ„œλ²„ 글은 μ—†λŠ”λ° 리λͺ¨νŠΈ μ„œλ²„λ§Œ 글이 남아 μžˆλŠ”κ²½μš° μ‚­μ œ ν•  수 μžˆλŠ” 라우트 μž…λ‹ˆλ‹€.
The following is a route that allows you to delete posts that exist only on the remote server but not on the local server.

4) μž„μ‹œλ‘œ μ‚¬μš©ν•˜κ³  ν”„λ‘œλ•μ…˜μ—μ„œλŠ” μ‚­μ œ ν•΄μ•Όν•©λ‹ˆλ‹€.
It should be used temporarily and removed in the production environment.

# 1. μƒνƒœ 확인 / Check Status
curl -b "refresh_token=YOUR_TOKEN" https://aloy-horizon.duckdns.org/api/test/nuke-remote

# 2. κΈ€ μ‚­μ œ / post delete
curl -X DELETE -b "refresh_token=YOUR_TOKEN" \
  "https://aloy-horizon.duckdns.org/api/test/nuke-remote?id=01913d88-51bd-4564-9f0a-820ec7e9fb0e"

βœ”οΈ μ½”λ“œ μˆ˜μ • / Code Modification

— app/usersui/[username]/ClientPage.tsx

  // βœ… myapp60 
  // - νŽ˜μ΄μ§€λ„€μ΄μ…˜μ€ TimelineListκ°€ λ‹΄λ‹Ή, μ—¬κΈ°μ„  빈 λ°°μ—΄λ‘œ μ‹œμž‘
  // - Pagination is handled by TimelineList; start with an empty array here.
  useEffect(() => {
    if (!username) return;
    setTimeline([]);
    setLoading(false);
  }, [username]);

  const refreshTimeline = async () => {
    if (!username) return;
    const json = await fetch(`/api/v1/timelines/home?limit=20&format=cursor&t=${Date.now()}`, { cache: 'no-store', credentials: 'include' }).then(r => r.json());
    const updated = Array.isArray(json)? json : json.data;
    setTimeline(updated || []);
  };

— app/usersui/[username]/_components/themes/themeex/MainTheme.tsx

# λͺ¨λ“  ν…Œλ§ˆνŒŒμΌμ΄λ¦„μ„ MainTheme.tsx둜 λ³€κ²½
# Rename all theme files to MainTheme.tsx.

— app/usersui/[username]/ClientPage.tsx

  const {
    flatData,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useCursorPagination({
    key: ['timeline', tab],
    initialData: initialTimeline.slice(0, 20), // βœ… myapp60
    fetcher: async (cursor) => {
      const params = new URLSearchParams();
      params.set('limit', '20');
      if (cursor) params.set('max_id', cursor);
      const res = await fetch(`/api/v1/timelines/${tab}?${params.toString()}`, { credentials: 'include' });
      if (!res.ok) throw new Error('fetch fail');
      const json = await res.json();
      const arr = Array.isArray(json)? json : (json.data || json.statuses || []);
      const withMine = arr.map((p:any) => ({
      ...p,
        isMine: p.username === currentUser || p.account?.username === currentUser || p.acct === currentUser
      }));
      return {
        data: withMine,
        nextCursor: withMine.length? withMine[withMine.length-1].id : null,
        hasMore: withMine.length >= 20
      };
    },
  });

  // βœ… myapp60 - flatData만 μ‚¬μš©, initialTimeline 쀑볡 μ‚¬μš© μ•ˆ 함
  const flat = (flatData || []).flat().filter(Boolean) as any[];
  const data = flat.length > 0? flat : initialTimeline.filter((p:any) => p && p.id);
     
... ... 
     {data.filter((p:any) => p && p.id).map((p: any, idx: number) => {
        const uniqueKey = `${p.id}-${idx}`;
        const isMine = !!(currentUser && (
          p.isMine || p.username === currentUser || p.acct === currentUser || p.account?.username === currentUser
        ));
        return (
... ...
                {currentUser && isMine && (
                  <button onClick={() => onDelete(p.id)} className="text-[#e0245e] ml-auto text-xs hover:underline">
                    πŸ—‘ Delete
                  </button>
                )}
... ...
        );
      })}

πŸ“ ν…ŒμŠ€νŠΈ / Test

βœ”οΈ νŽ˜μ΄μ§€λ„€μ΄μ…˜ +ν…Œλ§ˆ / Pagination + Theme

pagenation + theme

βœ”οΈμ’‹μ•„μš” + λΆ€μŠ€νŠΈ(둜컬) / Like + Boost (Local)

Local

βœ”οΈμ’‹μ•„μš” + λΆ€μŠ€νŠΈ / Like + Boost(GTS)

GTS

λ§ˆνƒœλ³΅μŒ 21μž₯ 22절 / Matthew 21: 22

“λ„ˆν¬κ°€ 기도할 λ•Œμ— 무엇이든지 λ―Ώκ³  κ΅¬ν•˜λŠ” 것은 λ‹€ λ°›μœΌλ¦¬λΌ ν•˜μ‹œλ‹ˆλΌ”

“And everything you ask in prayer, believing, you will receive.”

Leave a Reply