[nextjs]SNS Server-39(myapp49)Β 

πŸ‘‰πŸ» myapp49μ—μ„œλŠ” myapp48κΉŒμ§€ κΈ°λŠ₯ μΆ”κ°€ λ˜λ©΄μ„œ λ°œμƒν•œ 였λ₯˜λ‚˜ 버그λ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€.
myapp49 fixes errors and bugs that arose during the addition of features up to myapp48.

πŸ‘‰πŸ» λΆ€μŠ€νŠΈ ,μ’‹μ•„μš” 배달 λΆ€λΆ„ 였λ₯˜λ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€.
Fixed errors related to Boosts, Likes, and delivery.

πŸ‘‰πŸ» μ™ΈλΆ€μ•±μ—μ„œ κΈ€μ“°κ±°λ‚˜ μ‚­μ œν•˜λŠ” APIλ₯Ό ν„°λ―Έλ„μ—μ„œ μ‚¬μš©ν•  경우 토큰을 μž…λ ₯ν•΄μ•Όν•©λ‹ˆλ‹€.
You must enter a token when using the API to create or delete posts from an external app via the terminal.

πŸ‘‰πŸ» 전체 μ½”λ“œλŠ” κΉƒν—ˆλΈŒμ—μ„œ 확인 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
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/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/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/v1/statuses/[id]/route.ts -> -> Post delete(pinafore)
β”‚   β”œβ”€β”€ 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/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
β”‚   β”‚       β”œβ”€β”€ pinafore/PinaforeTheme.tsx -> Theme 1
β”‚   β”‚       β”œβ”€β”€ pinafore/FollowersList.tsx -> Theme 1, FollowersList
β”‚   β”‚       β”œβ”€β”€ pinafore/FollowingList.tsx -> Theme 1, FollowingList
β”‚   β”‚       β”œβ”€β”€ pinafore/NotificationsList.tsx -> Theme 1, NotificationsList
β”‚   β”‚       β”œβ”€β”€ 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
└── package.json

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


πŸ“ μ½”λ“œ μˆ˜μ • / Code Modification

βœ”οΈ λ‚΄ μ„œλ²„μ—μ„œ 원본글 μ‚­μ œμ‹œ 리λͺ¨νŠΈ μ„œλ²„μ—μ„œ μ‚­μ œλ˜μ§€ μ•ŠλŠ” 문제 μˆ˜μ •
Fixed an issue where deleting the original post on my server did not delete it from the remote server.

— app/api/posts/route.ts

  //const noteId = `https://${DOMAIN}/users/${username}/posts/${id}`; // βœ… 전체 URL λ§Œλ“€κΈ° / Create full URL

  // βœ… myapp49 - bug fix
  const noteId = `https://${DOMAIN}/users/${username}/statuses/${id}`;

βœ”οΈ λΆ€μŠ€νŠΈ 및 μ’‹μ•„μš” 였λ₯˜ μˆ˜μ •
Fixed Boost and Like errors.

— κ³ νˆ¬μ†Œμ…œ 원본 글에 μ’‹μ•„μš”,λΆ€μŠ€νŠΈλ₯Ό λ‚΄ μ„œλ²„μ˜ λ°°λ‹¬λœ 글에 μ‹€ν–‰ν•œκ²½μš° 원본 κΈ€ μ‚­μ œμ‹œ λ‚΄μ„œλ²„μ˜ λ°°λ‹¬λœκΈ€κ³Ό μ’‹μ•„μš”,λΆ€μŠ€νŠΈλ„ 같이 μ‚­μ œ
If you “like” or “boost” an original GoToSocial post and that action is applied to the copy delivered to your server, deleting the original post will also delete the delivered copy along with the associated likes and boosts.

if (body.type === 'Delete') {
... ...
if (objectId) {
... ...
              // βœ… myapp49 - λ°°λ‹¬λœ κΈ€μ˜ 원본 μ‚­μ œμ‹œ λΆ€μŠ€νŠΈ μ’‹μ•„μš” μ‚­μ œ 
              // Deleting the original delivered post removes the 'Boost Like'.

              // - μ’‹μ•„μš” 정리 / delete likes
              const likeDel = db.prepare(`DELETE FROM likes WHERE object LIKE? OR object LIKE?`).run(`%${shortId}%`, `%${canonicalId}%`);
              if (likeDel.changes > 0) {
                console.log(` -> likes 정리: ${likeDel.changes}개 μ‚­μ œ (원본 ${shortId})`);
              }

              // - λΆ€μŠ€νŠΈ 정리 / delete boost(announce)
              const announceDel = db.prepare(`DELETE FROM announces WHERE object LIKE? OR object LIKE?`).run(`%${shortId}%`, `%${canonicalId}%`);
              if (announceDel.changes > 0) {
                console.log(` -> announces 정리: ${announceDel.changes}개 μ‚­μ œ (원본 ${shortId})`);
              }
... ...
}
... ...
}

βœ”οΈ λΆ€μŠ€νŠΈ 라벨 μˆ˜μ • / Edit Boost Label

— inbox_posts둜 λ°°λ‹¬λ˜λŠ” λͺ¨λ“ κΈ€μ— boosted 라벨이 λΆ™λŠ” 문제 μˆ˜μ •
Fixed an issue where all posts delivered to inbox_posts were labeled as “boosted.”

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

    // βœ… myapp49- announces.object에 원본 있으면 true (λ„€κ°€ λ§ν•œ 둜직)
    const isBoostedPost = boostCount > 0 ? true : false;

1)announcesν…Œμ΄λΈ”μ—μ„œ idλŠ” λΆ€μŠ€νŠΈν•œμ‚¬λžŒ 전체 URL,objectλŠ” 원본글URL, actorλŠ” λΆ€μŠ€νŠΈν•œ μ‚¬μš©μž 계정 μ£Όμ†Œ
In the announces table, id is the full URL of the boost, object is the URL of the original post, and actor is the account address of the user who performed the boost.

2) announcesν…Œμ΄λΈ”μ˜ objectν•„λ“œ, inbox_posts ν…Œμ΄λΈ”μ˜ id, original_idλŠ” 원본 글을 가리킴
The object field in the announces table, as well as the id and original_id fields in the inbox_posts table, refer to the original post.

βœ”οΈ μ’‹μ•„μš” μ‹€ν–‰ 및 μ’‹μ•„μš”μ·¨μ†Œ
Like and Unlike

— μ’‹μ•„μš” λΆ€λΆ„μ—μ„œ λ‚΄ 글인지 μ²΄ν¬ν•˜λŠ” λΆ€λΆ„ 도메인좔가
Added the domain check to verify if the post belongs to the user in the “Like” section.

— λ°°μ†‘λœ μ’‹μ•„μš” μ‚­μ œ 둜직 μˆ˜μ •
Fixed logic for deleting delivered ‘likes’

— app/api/like/route.ts

export async function POST(req: NextRequest) {
... ...
const isOwnPost = target.includes(`${DOMAIN}/users/${username}/`); // βœ… myapp49 도메인좔가 / add DOMAIN
... ...
}

export async function DELETE(req: NextRequest) {
  const { username, target } = await req.json();
  const myActor = `https://${DOMAIN}/users/${username}`;
  const shortId = target.replace('/posts/', '/statuses/').split('/').pop()!.split('?')[0];

  const likeRow = db.prepare(`SELECT id, object FROM likes WHERE actor =? AND object LIKE '%' ||? || '%'`).get(myActor, shortId) as any;

  if (!likeRow) {
    const likeCount = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`).get(shortId) as any).c;
    return NextResponse.json({ ok: true, alreadyUnliked: true, isMyLike: false, likeCount });
  }

  const objectForCheck = likeRow.object || target;

  const isOwnPost = objectForCheck.includes(`${DOMAIN}/users/${username}/`);

 // βœ… myapp49 - μ’‹μ•„μš” 배달 / Send a Like
  if (!isOwnPost) {
    try {
      const u = new URL(objectForCheck);
      const parts = u.pathname.split('/').filter(Boolean);
      const idx = parts.indexOf('users');
      const inbox = idx!== -1? `${u.origin}/users/${parts[idx+1]}/inbox` : null;
      if (inbox) {
        await sendUndoLike(inbox, likeRow.id, objectForCheck, username);
      }
    } catch (e) {
      console.error('Undo Like 배달 μ‹€νŒ¨', e);
    }
  }

  db.prepare('DELETE FROM likes WHERE id =?').run(likeRow.id);
  const likeCount = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`).get(shortId) as any).c;
  return NextResponse.json({ ok: true, isMyLike: false, likeCount });
}

βœ”οΈ λΆ€μŠ€νŠΈ μ‹€ν–‰ 및 λΆ€μŠ€νŠΈ μ·¨μ†Œ
Start Boost and Cancel Boost

— μ’‹μ•„μš” 뢀뢄에 λ‚΄ κΈ€ 인지 μ²΄ν¬ν•˜λŠ” λΆ€λΆ„ 도메인좔가
Added a domain check to the “Like” section to verify if the post belongs to the user.

— λ°°μ†‘λœ λΆ€μŠ€νŠΈ μ‚­μ œ 둜직 μˆ˜μ •
Modified logic for deleting delivered boosts

— app/api/announces/route.ts

export async function POST(req: NextRequest) {
... ...
const isOwnPost = target.includes(`${DOMAIN}//users/${username}/`); // βœ… myapp48 도메인좔가  / Add DOMAIN
... ...
}

export async function DELETE(req: NextRequest) {
  try {
    const { username, target } = await req.json();
    const myActor = `https://${DOMAIN}/users/${username}`;
    const shortId = target.replace('/posts/', '/statuses/').split('?')[0].split('#')[0].split('/').filter(Boolean).pop() || target;

    const row = db.prepare(
      `SELECT id, object, actor FROM announces WHERE username =? AND object LIKE '%' ||? || '%'`
    ).get(username, shortId) as any;

    if (!row) {
      const boostCount = (db.prepare(
        `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`
      ).get(shortId) as any).c;
      return NextResponse.json({ ok: true, alreadyUnboosted: true, isMyBoost: false, boostCount });
    }

    // λΆ€μŠ€νŠΈ μ·¨μ†Œ 배달 / Undo Boost Delivery
    const objectForCheck = row.object || target;
    const isOwnPost = objectForCheck.includes(`${DOMAIN}/users/${username}/`);

    if (!isOwnPost) {
      try {
        const u = new URL(objectForCheck);
        const parts = u.pathname.split('/').filter(Boolean);
        const idx = parts.indexOf('users');
        const inbox = idx!== -1? `${u.origin}/users/${parts[idx+1]}/inbox` : null;
        if (inbox) {
          await sendUndoAnnounce(inbox, row.id, objectForCheck, username);
        }
      } catch (e) {
        console.error('Undo Announce 배달 μ‹€νŒ¨', e);
      }
    }

    db.prepare('DELETE FROM announces WHERE id =?').run(row.id);

    const boostCount = (db.prepare(
      `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`
    ).get(shortId) as any).c;

    return NextResponse.json({ ok: true, isMyBoost: false, boostCount });

  } catch (e: any) {
    console.error('[Boost DELETE]', e);
    return NextResponse.json({ ok: false, error: e.message }, { status: 500 });
  }
}

βœ”οΈ 라우트 λ³΄μ•ˆκ°•ν™” / Route Security Enhancement

— ν„°λ―Έλ„μ—μ„œ ν† ν°μ΄λ‚˜ μΏ ν‚€ 없이 κΈ€μ“°κ±°λ‚˜ μ‚­μ œλ₯Ό ν•  수 μ—†μŠ΅λ‹ˆλ‹€.
You cannot write or delete posts from the terminal without a token or cookie.

— app/api/v1/statuses/route.ts — Pinafore

export async function POST(req: Request) {
... ...
    // βœ… myapp49 - pinafore,local server login 
    let username: string | null = null;
    // βœ… myapp49 - 1. Bearer 토큰 체크 / Bearer token check (Pinafore)
    if (token) {
      try {
        const oauthToken = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
        if (oauthToken?.username) username = oauthToken.username;
      } catch {}
    }

    // 2. μΏ ν‚€ 체크 (둜컬 μ„œλ²„) / Cookie Check (Local Server)
    if (!username) {
      try {
        const { getViewerFromRequest } = await import('@/lib/visibility');
        username = getViewerFromRequest(req);
      } catch {}
    }

    // βœ… 3. λ‘˜ λ‹€ μ—†μœΌλ©΄ 401 - 'user1' 폴백 μ ˆλŒ€ κΈˆμ§€
    // If neither exists, return 401β€”falling back to 'user1' is strictly prohibited.
    if (!username) {
      return NextResponse.json({ error: 'unauthorized - Bearer token or login required' }, { status: 401 });
    }
... ...
}

— app/api/v1/statuses/[id]/route.ts – Pinafore


export async function DELETE(
  req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
... ...  
// βœ… myapp49 - 1. 인증 - Bearer + μΏ ν‚€ λ‘˜ λ‹€ 지원 / Authentication - Supports both Bearer and cookies.

  const auth = req.headers.get('Authorization') || '';
  const token = auth.replace('Bearer ', '');
  let username: string | null = null;


  if (token) {
    try {
      const oauthToken = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
      if (oauthToken?.username) username = oauthToken.username;
    } catch {}
  }

  if (!username) {
    try {
      const { getViewerFromRequest } = await import('@/lib/visibility');
      username = getViewerFromRequest(req as any);
    } catch {}
  }

  // let oauthToken: any;
  // try { oauthToken = db.prepare('SELECT * FROM oauth_tokens WHERE access_token=?').get(token) as any; } catch {}
  // const username = oauthToken?.username || 'user1';

  // βœ… myapp49 - 폴백 κΈˆμ§€ - 인증 μ—†μœΌλ©΄ 401 / Disable fallback – 401 if unauthenticated
  if (!username) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401, headers: { 'Access-Control-Allow-Origin': '*' } });
  }

  console.log(`πŸ—‘ Pinafore μ‚­μ œ μš”μ²­ / Request to delete Pinafore : ${username} -> ${id}`);

  // βœ… myapp49 - μ†Œμœ κΆŒ 체크 - λ‚΄ κΈ€ λ§žλŠ”μ§€ 확인 / Ownership Check – Verify if it is my post
  let post: any;
  try {
    post = db.prepare('SELECT id, username, visibility FROM posts WHERE id=?').get(id) as any;
  } catch {}

  if (!post) {
    // 이미 μ§€μ›Œμ‘ŒμœΌλ©΄ PinaforeλŠ” 404보닀 200을 원함 - λ©±λ“±μ„±
    return NextResponse.json({ id, deleted: true, alreadyDeleted: true }, {
      headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' }
    });
  }

  if (post.username !== username) {
    console.log(`🚫 μ‚­μ œ κ±°λΆ€: ${username}κ°€ ${post.username} κΈ€ μ‚­μ œ μ‹œλ„`);
    return NextResponse.json({ error: 'forbidden - not your post' }, { 
      status: 403, 
      headers: { 'Access-Control-Allow-Origin': '*' } 
    });
  }
... ...
}

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

βœ”οΈ μ’‹μ•„μš” / Likes

local
GTS

βœ”οΈ λΆ€μŠ€νŠΈ / Boost

Local
GTS

βœ”οΈ api/v1/statuses 라우트 터미널 ν…ŒμŠ€νŠΈ
Terminal test for the api/v1/statuses route

# 1.κΈ€μ“°κΈ° ν…ŒμŠ€νŠΈ - ν† ν°μ΄λ‚˜ μΏ ν‚€ μ—†λŠ” 경우
Writing Test - No tokens or cookies

 curl -s -X POST https://aloy-horizon.duckdns.org/api/v1/statuses \
 -H "Content-Type: application/json" \
 -d '{"status":"write post test-1","visibility":"public"}'

# 응닡 / Response
{"error":"unauthorized - Bearer token or login required"}%    

# 2.κΈ€μ“°κΈ° ν…ŒμŠ€νŠΈ / Test - refresh token
curl -s -X POST https://aloy-horizon.duckdns.org/api/v1/statuses \
 -H "Cookie: refresh_token=099b61b8616edcf48b ... ..." \
 -H "Content-Type: application/json" \
 -d '{"status":"refresh token","visibility":"public"}'

# 응닡 / Response

{"id":"c3b1cefe-853c-476e-b978-3b2e28506a92","uri":"https://aloy-horizon.duckdns.org/users/user1/statuses/c3b1cefe-853c-476e-b978-3b2e28506a92","url":"https://aloy-horizon.duckdns.org/users/user1/statuses/c3b1cefe-853c-476e-b978-3b2e28506a92","account":{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"},"content":"<p>refresh token</p>","created_at":"2026-09-26T04:26:48.469Z","visibility":"public","reblogs_count":0,"favourites_count":0,"replies_count":0}%   

   
# 3. κΈ€ μ‚­μ œ ν…ŒμŠ€νŠΈ - ν† ν°μ΄λ‚˜ μΏ ν‚€κ°€ μ—†λŠ”κ²½μš°
myapp49 % curl -s -X DELETE https://aloy-horizon.duckdns.org/api/v1/statuses/9a9f4c12-265c-46ed-b956-f51a5e61fdb2
# 응닡 / Response
{"error":"unauthorized"}%                         

# 4. κΈ€ μ‚­μ œ ν…ŒμŠ€νŠΈ - refresh token
curl -s -X DELETE https://aloy-horizon.duckdns.org/api/v1/statuses/9a9f4c12-265c-46ed-b956-f51a5e61fdb2 \
 -H "Authorization: Bearer 099b61b8616edcf48b4cf7260ef818b7a451... ..."

# 응닡 / Response   
{"id":"9a9f4c12-265c-46ed-b956-f51a5e61fdb2","deleted":true}%   
                                                 
                                                          

πŸ’‘ ν„°λ―Έλ„μ—μ„œ 직접 κΈ€μ“°κΈ° / Writing directly in the terminal

  curl -X POST https://aloy-horizon.duckdns.org/users/user1/outbox \
  -H "Content-Type: application/activity+json" \
  -d '{"type":"Create","object":{"content":"Hello Activity UI #activityposts "}}'

πŸ’‘ ν„°λ―Έλ„μ—μ„œ 직접 κΈ€ μ‚­μ œ / Delete posts directly from the terminal.

curl -X DELETE "https://aloy-horizon.duckdns.org/api/cleanup-orphan?id=c429765e-1ffd-4168-b487-e4b36a7a162a&username=user1"

πŸ’‘ μ’‹μ•„μš” 및 μ’‹μ•„μš” μ·¨μ†Œμ‹€ν–‰ / Perform Like and Unlike actions

# μ’‹μ•„μš” μ‹€ν–‰ / Likes
curl -X POST https://aloy-horizon.duckdns.org/api/like \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}'

# μ’‹μ•„μš” μ·¨μ†Œν•˜κΈ° / Undo Likes
curl -X DELETE https://aloy-horizon.duckdns.org/api/like \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}'

μš”ν•œλ³΅μŒ 8μž₯ 32절 / John 8:32

“그리고 λ„ˆν¬λŠ” 진리λ₯Ό μ•Œκ²Œ 될 것이며, 진리가 λ„ˆν¬λ₯Ό 자유둭게 ν•  것이닀.”

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

Leave a Reply