[nextjs]SNS Server-45(myapp55)ย 

๐Ÿ‘‰๐Ÿป myapp45์—์„œ๋Š” Tursky๋กœ๊ทธ์ธ ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
In myapp45, the Tursky login functionality is implemented.

๐Ÿ‘‰๐Ÿป ์›น์ด๋‚˜ pinafore์—์„œ ์ •์ƒ์ ์œผ๋กœ ๋ชจ๋“ ๋ฐ์ดํ„ฐ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
All data is displayed correctly on the web or in Pinafore.

๐Ÿ‘‰๐Ÿป ํ•˜์ง€๋งŒ Tusky์•ฑ ํƒ€์ž„๋ผ์ธ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
However, there is an issue where data is not displaying correctly on the Tusky app’s timeline.

๐Ÿ‘‰๐Ÿป ์ถ”ํ›„ ์‚ดํŽด ๋ณด๋„๋ก ํ•˜๊ณ  ์—ฌ๊ธฐ์„œ๋Š” ์•ฑ์— ๋กœ๊ทธ์ธ ์™„๋ฃŒํ•˜๊ณ  404์˜ค๋ฅ˜ ์—†์• ๋Š” ๋ถ€๋ถ„๊นŒ์ง€๋งŒ ์™„๋ฃŒํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
We will look into that later; for now, we will just complete the steps of logging into the app and resolving the 404 error.

๐Ÿ‘‰๐Ÿป ์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
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(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/route.ts -> timeline
โ”‚   โ”œโ”€โ”€ api/v1/media/route.ts -> Image upload
โ”‚   โ”œโ”€โ”€ api/v1/media/[id]/route.ts  -> Image Delete 
โ”‚   โ”œโ”€โ”€ api/v1/instance/rules/route.ts
โ”‚   โ”œโ”€โ”€ api/v1/instance/domain_blocks/route.ts 
โ”‚   โ”œโ”€โ”€ api/v1/filters/route.ts
โ”‚   โ”œโ”€โ”€ api/v1/lists/route.ts 
โ”‚   โ”œโ”€โ”€ api/v1/announcements/route.ts
โ”‚   โ”œโ”€โ”€ api/v1/timelines/public/route.ts 
โ”‚   โ”œโ”€โ”€ oauth/authorize/route.ts -> auth
โ”‚   โ”œโ”€โ”€ oauth/token/route.ts -> auth
โ”‚   โ”œโ”€โ”€ oauth/login-and-authorize/route.ts -> Tursky,Pinafore login processing
โ”‚   โ”œโ”€โ”€ oauth/revoke/route.ts    
โ”‚   โ”œโ”€โ”€ api/v1/search/home/route.ts -> search
โ”‚   โ”œโ”€โ”€ api/v2/search/home/route.ts -> search
โ”‚   โ”œโ”€โ”€ api/v2/notifications/policy/route.ts
โ”‚   โ”œโ”€โ”€ 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        -> 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
โ”‚   โ”‚       โ”œโ”€โ”€ 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
โ””โ”€โ”€ package.json

๐Ÿ“ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ / Project Start


๐Ÿ“ ์ฝ”๋“œ์ˆ˜์ • / Code Modification

โœ”๏ธ Tusky์•ฑ์— ๋กœ๊ทธ์ธ ๊ตฌํ˜„ / Implementing login for the Tusky app

1) app/oauth/authorize/route.ts

A. ํ˜„์žฌ ๋ชจ๋ฐ”์ผ์•ฑ๊ณผ ์›น์‚ฌ์ดํŠธ์ธ๊ฒฝ์šฐ ๋™์ผํ•œ ์ฝ”๋“œ๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ถ„๋ฆฌ์ ์šฉ
Currently, the mobile app and the website do not operate on the same code, so they are implemented separately.

B. fetch๋ฐฉ์‹์ด ์•„๋‹Œ methd=POST ๋ฐฉ์‹ ์ ์šฉ
Applied the POST method instead of the fetch method.

— ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ(/auth/login/page.tsx)์ด๋™ํ•˜์ง€ ์•Š๊ณ  ์ด ๋ผ์šฐํŠธ์—์„œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ณด์—ฌ์ฃผ๊ธฐ
Display the login page on this route without navigating to the login page (/auth/login/page.tsx).


  // โœ… for CORS error
  // if (!username) {
  //     const loginUrl = new URL('/auth/login', ORIGIN); // โœ… ๋„๋ฉ”์ธ, not loacalhost / Domain, not localhost
  //     const realNextUrl = `${ORIGIN}${url.pathname}${url.search}`; // โœ… Use the domain for Next.js, not localhost.
  //     loginUrl.searchParams.set('next', realNextUrl);
  //     return NextResponse.redirect(loginUrl.toString());
  // }

  // โœ… myapp55 ์ˆ˜์ • - ๋กœ๊ทธ์ธ ์•ˆ ๋์œผ๋ฉด /auth/login์œผ๋กœ ์ด๋™ํ•˜์ง€ ๋ง๊ณ  ์—ฌ๊ธฐ์„œ ๋กœ๊ทธ์ธ ํผ ๋ณด์—ฌ์ฃผ๊ธฐ
  //  If not logged in, do not redirect to /auth/login, but show the login form here.
  if (!username) {
    const html = `
    <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Login</title>
    <style>body{font-family:sans-serif;max-width:400px;margin:50px auto;padding:20px}input{width:90%;padding:10px;margin:8px 0;font-size:16px}button{width:100%;padding:12px;background:#6364ff;color:white;border:0;border-radius:8px;font-size:16px}</style>
    </head><body>
      <h2>๐Ÿ” ๋กœ๊ทธ์ธ ํ•„์š”(OAuth)</h2>
      <p>${clientId} ์•ฑ ์Šน์ธ ์œ„ํ•ด ๋กœ๊ทธ์ธ</p>
      <form method="POST" action="/oauth/login-and-authorize">
        <input type="hidden" name="client_id" value="${clientId}">
        <input type="hidden" name="redirect_uri" value="${redirectUri}">
        <input type="hidden" name="scope" value="${scope}">
        <input name="username" placeholder="username" required>
        <input name="password" type="password" placeholder="password" required>
        <button type="submit">๋กœ๊ทธ์ธ & ์Šน์ธ</button>
      </form>
    </body></html>`;
    return new NextResponse(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
  }

โœ”๏ธapp/oauth/login-and-authorize/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

— Tursky์•ฑ์— ๋กœ๊ทธ์ธ์„ ์œ„ํ•œ ๋ผ์šฐํŠธ,๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
Route and login handling for the Tursky app

— api/auth/login/route.ts๋Š” ์›น์‚ฌ์ดํŠธ์šฉ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
api/auth/login/route.ts handles login for the website.

... ...
export async function POST(req: Request) {
... ...
  // - Tusky๋Š” HTML+JS๋กœ, ๊ทธ ์™ธ๋Š” 302 / Tusky uses HTML+JS, others use 302
  let html: string;
  if (finalUrl.startsWith('oauth2redirect://') || finalUrl.includes('tusky')) {
    html = `<!DOCTYPE html><html><body style="text-align:center;padding:50px"><h2>โœ… ${user.username} ์Šน์ธ๋จ</h2><p><a href="${finalUrl}">${finalUrl}</a></p><script>setTimeout(()=>{window.location.href="${finalUrl}"},300)</script></body></html>`;
  } else {
    html = `<!DOCTYPE html><html><body><script>window.location.href="${finalUrl}"</script><a href="${finalUrl}">${finalUrl}</a></body></html>`;
  }

  const res = new NextResponse(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
  res.cookies.set('token', accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60*15, path: '/' });
  res.cookies.set('refresh_token', refreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60*60*24*7, path: '/' });
  return res;
}

โœ”๏ธ 404 ์˜ค๋ฅ˜ ๋ฉ”์„ธ์ง€ ์ฒ˜๋ฆฌ

1)app/api/v1/instance/rules/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

import { NextResponse } from 'next/server';
export async function GET() {
  return NextResponse.json([], { headers: { 'Access-Control-Allow-Origin': '*' } });
}

2)app/api/v1/instance/domain_blocks/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

app/api/v1/instance/rules/route.ts ์ฝ”๋“œ์™€ ๋™์ผ
Identical to the code in app/api/v1/instance/rules/route.ts

3)app/oauth/revoke/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

A.Tusky ๋กœ๊ทธ์•„์›ƒ ๋•Œ ํ˜ธ์ถœ / Add route (called upon Tusky logout)

import { NextResponse } from 'next/server';
import db from '@/lib/db';

export async function POST(req: Request) {
  try {
    const body = await req.json().catch(() => ({})) as any;
    const token = body.token;
    if (token) {
      try {
        db.prepare(`DELETE FROM oauth_tokens WHERE access_token=?`).run(token);
        db.prepare(`DELETE FROM sessions WHERE access_token=?`).run(token);
      } catch {}
    }
  } catch {}
  return NextResponse.json({});
}
export async function GET() { return NextResponse.json({}); }

4)app/api/v2/notifications/policy/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

import { NextResponse } from 'next/server';
export async function GET() {
  return NextResponse.json({ for_not_following: 'accept', for_not_followers: 'accept', for_new_accounts: 'accept', for_private_mentions: 'accept', for_limited_accounts: 'accept' });
}

5)public/icon.png – ํŒŒ์ผ์ถ”๊ฐ€ / add file

6) app/api/v1/filters/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

import { NextResponse } from 'next/server';
export async function GET() {
  return NextResponse.json([], { headers: { 'Access-Control-Allow-Origin': '*' } });
}

7)app/api/v1/lists/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

app/api/v1/instance/rules/route.ts ์ฝ”๋“œ์™€ ๋™์ผ
Identical to the code in app/api/v1/instance/rules/route.ts

8)app/api/v1/announcements/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

app/api/v1/instance/rules/route.ts ์ฝ”๋“œ์™€ ๋™์ผ
Identical to the code in app/api/v1/instance/rules/route.ts

9)app/api/v1/timelines/public/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

app/api/v1/instance/rules/route.ts ์ฝ”๋“œ์™€ ๋™์ผ
Identical to the code in app/api/v1/instance/rules/route.ts

10)app/api/v1/notifications/route.ts – ์ฝ”๋“œ์ˆ˜์ • / Code Modification

A.๋ผ์šฐํŠธ ๋ฌดํ•œ ํ˜ธ์ถœ ๋ฉˆ์ถ”๊ธฐ / Stop infinite route calls

B. /api/v1/notifications ๋ฌดํ•œ ํ˜ธ์ถœ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€

likes table rows=0 for user1
๐Ÿ”” notifications return=2 follow=2 fav=0 reblog=0
 GET /api/v1/notifications?max_id=follow_1_https%3A%2F%2Ffreelifemakers.com%2Fusers%2Fuser1%2Ffollow%2F01HW187GFP316GG213ASB2DG3T&limit=30 200 in 16ms (next.js: 13ms, proxy.ts: 0.8ms, application-code: 3ms)
 GET /icon.png 404 in 25ms (next.js: 16ms, application-code: 10ms)
โค likes table rows=0 for user1
๐Ÿ”” notifications return=2 follow=2 fav=0 reblog=0
 GET /api/v1/notifications?limit=30 200 in 4ms (next.js: 0.9ms, proxy.ts: 1.0ms, application-code: 1.7ms)
โค likes table rows=0 for user1

C. ์ฝ”๋“œ / Code

... ...
export async function GET(req: Request) {
... ...
    // โœ… myapp55 - Tusky๊ฐ€ max_id๋กœ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋‹ฌ๋ผ๊ณ  ํ•˜๋ฉด ๋นˆ ๋ฐฐ์—ด ๋ฆฌํ„ด = ๋
    // Tusky๊ฐ€ max_id๋กœ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋‹ฌ๋ผ๊ณ  ํ•˜๋ฉด ๋นˆ ๋ฐฐ์—ด ๋ฆฌํ„ด = ๋
    // Tursky requests the next page with max_id, return an empty array = end
    const max_id = searchParams.get('max_id'); 
    if (max_id) {
      return NextResponse.json([], {
        headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }
      });
    }
... ...
}
... ...

11) app/api/v1/markers/route.ts – ๋ผ์šฐํŠธ ์ถ”๊ฐ€ / Add route

import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({
    notifications: { last_read_id: "0", version: 1, updated_at: new Date().toISOString() },
    home: { last_read_id: "0", version: 1, updated_at: new Date().toISOString() }
  }, { headers: { 'Access-Control-Allow-Origin': '*' } });
}

export async function POST(req: Request) {
  const body = await req.json().catch(()=>({}));
  return NextResponse.json(body, { headers: { 'Access-Control-Allow-Origin': '*' } });
}

12)app/api/v1/timelines/home/route.ts – ์ฝ”๋“œ์ˆ˜์ • / code modification

    // โœ… myapp55 - ๊ณ„์ • ์ •๋ณด ๊ตฌ์„ฑ / Account information configuration
    const baseAccount = isMy
    ? { 
        id: '1', username, acct: `${username}@${DOMAIN}`, display_name: username,
        avatar: `https://${DOMAIN}/icon.png`,
        avatar_static: `https://${DOMAIN}/icon.png`,
        header: `https://${DOMAIN}/header.png`,
        header_static: `https://${DOMAIN}/header.png`,
      }
    : (() => {
        let uname = 'user1', dom = 'freelifemakers.com', display = 'user1';
        if (row.actor?.includes('mastodon.social')) { uname = 'freelifemakers'; dom = 'mastodon.social'; display = 'freelifemakers'; }
        return { 
          id: `remote_${dom.replace(/\./g,'_')}_${uname}`, 
          username: uname, acct: `${uname}@${dom}`, display_name: display,
          avatar: `https://${DOMAIN}/icon.png`,
          avatar_static: `https://${DOMAIN}/icon.png`,
          header: `https://${DOMAIN}/header.png`,
          header_static: `https://${DOMAIN}/header.png`,
        };
      })();

    return {
      id: String(row.id),
      uri: longId,
      url: longId,
      account: baseAccount,
      content: content.startsWith('<')? content : `<p>${content}</p>`,
      created_at: new Date(parseDate(row.created_at)).toISOString(),
      visibility: 'public',
      reblogs_count,      // ์ „์ฒด ๋ถ€์ŠคํŠธ ์นด์šดํŠธ / Full boost count
      favourites_count,   // ์ „์ฒด ์ข‹์•„์š” ์นด์šดํŠธ / Full like count 
      replies_count: 0,
      favourited: !!favourited, // ์ „์ฒด ์ข‹์•„์š” / Full like
      reblogged: !!reblogged,   // ์ „์ฒด ๋ถ€์ŠคํŠธ / Full Boost
      muted: false, bookmarked: false, pinned: false,
      // โœ… myapp55 - Tusky ํ•„์ˆ˜ / Required for Tusky 
      sensitive: false, spoiler_text: '',
      media_attachments: [],
      mentions: [],
      tags: [],
      emojis: [],
      card: null,
      poll: null,
      reblog: null,
    };

๐Ÿ“ ํ…Œ์ŠคํŠธ / Test

โœ”๏ธ ์„œ๋ฒ„๋กœ๊ทธ์—์„œ 404 ์—๋Ÿฌ ํ™•์ธ / Check for 404 errors in the server logs.

Server log

โœ”๏ธ ์™ธ๋ถ€ ์•ฑ ๋กœ๊ทธ์ธํผ / External App Login Form

โœ”๏ธ ์™ธ๋ถ€ ์•ฑ ๋กœ๊ทธ์ธ ํผ – ๋น„๋ฐ€๋ฒˆํ˜ธ ํ‹€๋ฆฐ๊ฒฝ์šฐ / External App Login Form โ€“ Wrong Password

โœ”๏ธ Tusky์•ฑ ๋กœ๊ทธ์ธ ๋ฐ ํ”„๋กœํ•„ ํ™•์ธ / Logging into the Tusky app and checking your profile

์š”ํ•œ๋ณต์Œ 8์žฅ 32์ ˆ / John 8:32

“๊ทธ๋ฆฌ๊ณ  ๋„ˆํฌ๋Š” ์ง„๋ฆฌ๋ฅผ ์•Œ๊ฒŒ ๋  ๊ฒƒ์ด๋ฉฐ, ์ง„๋ฆฌ๊ฐ€ ๋„ˆํฌ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ํ•  ๊ฒƒ์ด๋‹ค.”

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

Leave a Reply