๐๐ป ์ด์ ํ๋ก์ ํธ๋ inbox_postsํ
์ด๋ธ์ ๊ฐ๋ณ๊ธ API๋ฅผ ๋ณด๋ ค๋ฉด ํฐ๋ฏธ๋์ ์ฌ์ฉํด์ผํ์ต๋๋ค.
In the previous project, you had to use the terminal to view the API for individual posts in the inbox_posts table.
๐๐ป myapp20 ํ๋ก์ ํธ์์๋ ๋ธ๋ผ์ฐ์ ์์ posts๋ inbox_postsํ
์ด๋ธ์ API๊ฐ๋ ๋ณผ ์ ์๊ฒ ์์ ํฉ๋๋ค.
In the myapp20 project, we are modifying the setup so that API values โโfrom the posts or inbox_posts tables can also be viewed in the browser.
๐ ์ ์ฒด ํ๋ก์ ํธ ๊ตฌ์กฐ / Overall Project Structure
myapp20/
โโโ app/ (Next.js App Router)
โ โโโ .well-known/webfinger/route.ts -> webfinger
โ โโโ api/posts/route.ts -> Writing API
โ โโโ api/follow/route.ts -> Follow API(temporary)
โ โโโ api/timeline/route.ts -> Timeline API
โ โโโ 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
โ โโโ layout.tsx, page.tsx, globals.css
โ โโโ favicon.ico
โโโ lib/
โ โโโ ap.ts -> Follow Accept
โ โโโ db.ts -> DB connection
โโโ data/
โ โโโ keys/ -> private.pem, public.pem
โโโ data.sqlite -> Database
โโโ Caddyfile -> https
โโโ package.json
๐ ํ๋ก์ ํธ ์์(myapp20)
Project Start (myapp20)
npx create-next-app@latest
๐ SQLite์ค์น / Installing SQLite
cd ~/myapp20
npm install better-sqlite3
npm install -D @types/better-sqlite3
๐ DDNS,https์ค์ / DDNS,https settings
๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ํ์ธ ๋ฐ ์์
Review and modify database schema
โ๏ธ ๋ค๋ฅธ ์๋ฒ์์ ๋ฐ๋ ๊ธ(inbox_posts) ํ
์ด๋ธ์์ id๊ฐ ๋๋ฌด ๊ธธ์ด์ ์งง๊ฒ ์ค์ด๊ธฐ ์ํ ์์
์
๋๋ค.
This task aims to shorten the id in the inbox_posts table, which stores posts received from other servers, because the current ID is too long.
โ๏ธ URL๋ถ๋ถ์ ํฌํจํ ์ ์ฒด๋ฅผ id๋ฅผ original_id ํ๋์ ๋ฐ๋ก ์ ์ฅํฉ๋๋ค.
The entire string, including the URL portion, is stored separately in the original_id field.
โ๏ธ original_id๋ gotosocial์ด๋ mastodon์๋ฒ์ ํต์ ํ๊ธฐ ์ํด์ ์ฌ์ฉํ๊ณ id๋ ๊ด๋ฆฌ์ฉ์ผ๋ก ์ฌ์ฉํฉ๋๋ค.original_id is used for communication with GoToSocial or Mastodon servers, while id is used for administrative purposes.
โ๏ธ inbox_post์ ์คํค๋ง(ํ
์ด๋ธ)๊ฐ ๋ค์๊ณผ ๊ฐ์ง ์๋ค๋ฉด ์์ ํฉ๋๋ค.
Modify the schema (table) for inbox_post if it does not match the following.
CREATE TABLE inbox_posts (
id TEXT PRIMARY KEY,
actor TEXT,
content TEXT,
username TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
, original_id TEXT);
— ํ
์ด๋ธ ์์ ํ๊ธฐ(์ด์ ์ ์์ ํ์ง ์์๋ค๋ฉด…)
Modify the table (if you haven’t already…)
ALTER TABLE inbox_posts ADD COLUMN original_id TEXT;
๐ ์ฝ๋ ์์ / Code modification
โ๏ธ myapp20/app/users/[username]/inbox
if (body.type === 'Create') {
const note = body.object;
if (note && note.type === 'Note') {
console.log(`๐ [${username}] ์ ๊ธ ๋์ฐฉ / New post received from ${actorId}`);
console.log(`๋ด์ฉ/content: ${note.content?.slice(0, 100)}`);
// โ
myapp20 -inbox_posts์ฃผ์ ์ค์ด๊ธฐ / Shorten inbox_posts URL
try {
const longId = note.id || `https://remote/${Date.now()}-${Math.random()}`;
const shortId = longId.split('/').pop()!; // ๋ง์ง๋ง ๋ถ๋ถ๋ง ์๋ผ๋ด๊ธฐ / Cut off only the last part
const content = note.content || '';
db.prepare(`
INSERT OR IGNORE INTO inbox_posts (id, actor, content, username, original_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(shortId, actorId, content, username, longId, note.published || new Date().toISOString());
console.log(`โ
[${username}] inbox_posts ์ ์ฅ ์๋ฃ/inbox_posts saved successfully. : ${shortId} (์๋ณธ/original: ${longId})`);
} catch (e) {
console.error(`โ ๊ธ ์ ์ฅ ์คํจ / Failed to save post`, e);
}
}
return new Response('', { status: 202 });
}
โ๏ธ /myapp20/app/usersui/[username]/page.tsx
... ...
// โ
myapp20
function getDisplayName(actorOrUsername: string) {
if (!actorOrUsername) return 'unknown';
// https://freelifemakers.com/users/user1 -> user1@freelifemakers.com
if (actorOrUsername.startsWith('https://')) {
try {
const url = new URL(actorOrUsername);
const username = url.pathname.split('/').pop() || 'user';
return `${username}@${url.hostname}`;
} catch {
return actorOrUsername.split('/').pop() || actorOrUsername;
}
}
return actorOrUsername;
}
... ...
return (
<div style={{ padding: 20, maxWidth: 900, margin: '0 auto' }}>
<h1 style={{ fontSize: 24, marginBottom: 16 }}>{username} ํ์๋ผ์ธ / Timeline</h1>
<ul style={{ listStyle: 'none', padding: 0 }}>
{timeline.map((p:any) => (
<li key={`${p.source}-${p.id}`} style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '10px 0',
borderBottom: '1px solid #eee',
flexWrap: 'wrap'
}}>
<span style={{ background: p.source === 'mine'? '#dbeafe' : '#fef3c7', padding: '2px 6px', borderRadius: 4, fontSize: 11 }}>
{p.source}
</span>
<b>{getDisplayName(p.actor)}:</b>
<span dangerouslySetInnerHTML={{__html: p.content}} />
<span style={{ color: '#999', fontSize: 11, marginLeft: 'auto' }}>
{new Date(p.created_at).toLocaleString()}
</span>
</li>
))}
</ul>
</div>
);
๐ ํ ์คํธ / Test
โ๏ธ inbox_posts์ ๊ธฐ์กด์ ๊ธ์ด ์๋ค๋ฉด ๋ค ์ง์๋๋ค.(https:// ๋๋ฌธ์…)
Delete any existing posts in inbox_posts (due to https://โฆ)
— gotosocial์๋ฒ(freelifemakers.com)์๋ฒ์ ๊ธ๋ ์ง์๋๋ค.
Posts on the GoToSocial server (freelifemakers.com) are also being deleted.
sqlite> select * from inbox_posts;
https://freelifemakers.com/users/user1/statuses/01M0EJPP57WC47FAK0N395S7MB|https://freelifemakers.com/users/user1|<p>inbox_posts table test</p>|user1|2026-07-10 01:16:15|
https://freelifemakers.com/users/user1/statuses/01M0KMS8K1XRFW703S32MX6V25|https://freelifemakers.com/users/user1|<p>actor test</p>|user1|2026-07-19T10:20:10+02:00|https://freelifemakers.com/users/user1/statuses/01M0KMS8K1XRFW703S32MX6V25
sqlite> delete from inbox_posts;
โ๏ธ pinafore์์ ์๋ก ๊ธ์ ์ฐ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ํ์ธํฉ๋๋ค.
Write a new post in Pinafore and check the database.
— pinafore.socical

— sqlite database(aloy-horizon.duckdns.org)
sqlite> select * from inbox_posts;
01M0P8VYEQTFW28N97W6MG8PY5|https://freelifemakers.com/users/user1|<p>reduce inbox_posts id</p>|user1|2026-07-20T06:10:11+03:00|https://freelifemakers.com/users/user1/statuses/01M0P8VYEQTFW28N97W6MG8PY5
sqlite>
— gotosocial server( freelifemakers.com )

โ๏ธ ๋ธ๋ผ์ฐ์ ์์ ๋ค์ ์ฃผ์๋ฅผ ์คํํฉ๋๋ค.
Open the following address in your browser.
— inbox_posts ํ
์ด๋ธ์ ๊ธ ๋ณด๊ธฐ
View posts from the inbox_posts table
https://aloy-horizon.duckdns.org/users/user1/statuses/01M0P8VYEQTFW28N97W6MG8PY5

— posts ํ
์ด๋ธ์ ๊ธ ๋ณด๊ธฐ
View a post from the ‘posts’ table
https://aloy-horizon.duckdns.org/users/user1/statuses/e6c99652-572f-491a-9e61-05e67b0d58fc

— ํ์๋ผ์ธUI / Timeline UI
https://aloy-horizon.duckdns.org/@user1

๐ ์ง๊ธ๊น์ง ์์
ํ ๋ด์ฉ๊ณผ ๋ผ์ฐํธ ์
๋๋ค.
Here is the work done so far and the routes.
โ๏ธ ๋ธ๋ผ์ฐ์ ์์ ํ
์คํธ ํ ์ ์๋ ๋ผ์ฐํธ
Routes that can be tested in a browser
1. WebFinger
https://aloy-horizon.duckdns.org/.well-known/webfinger?resource=acct:user1@aloy-horizon.duckdns.org
2. Actor (ํ๋กํ / Profile)
https://aloy-horizon.duckdns.org/users/user1
3. Followers (๋จ์ด ๋ ํ๋ก์ฐ / Someone follows me)
https://aloy-horizon.duckdns.org/users/user1/followers
4. Following (๋ด๊ฐ ๋จ์ ํ๋ก์ฐ / I follow others)
https://aloy-horizon.duckdns.org/users/user1/following
5. Outbox (๋ด ๊ธ ๋ชฉ๋ก / My post list)
https://aloy-horizon.duckdns.org/users/user1/outbox
6. ํ์๋ผ์ธ / Timeline
- ํ์๋ผ์ธUI / Timeline UI
https://aloy-horizon.duckdns.org/usersui/user1
or
https://aloy-horizon.duckdns.org/@user1
- ํ์๋ผ์ธAPI / Timeline API
https://aloy-horizon.duckdns.org/api/timeline?username=user1
7. ๊ฐ๋ณ ๊ธ(๊ธ ์์ธ) / indivisual posts(Post Details)[posts, inbox_posts]
https://aloy-horizon.duckdns.org/users/user1/statuses/[ID]
ex)
# posts table
https://aloy-horizon.duckdns.org/users/user1/statuses/e6c99652-572f-491a-9e61-05e67b0d58fc
# inbox_posts table
https://aloy-horizon.duckdns.org/users/user1/statuses/01M0P8VYEQTFW28N97W6MG8PY5
โ๏ธ ํฐ๋ฏธ๋์์๋ง ํ
์คํธ ๊ฐ๋ฅํ ๋ผ์ฐํธ
Routes that can only be tested in the terminal
# ํ๋ก์ฐ (following 1๋ก ์ฆ๊ฐ)
# Follow (following increased to 1)
curl -X POST https://aloy-horizon.duckdns.org/api/follow \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://freelifemakers.com/users/user1"}'
# ์ธํ๋ก์ฐ (following 0์ผ๋ก ๊ฐ์)
# Unfollow (following count reduced to 0)
curl -X DELETE https://aloy-horizon.duckdns.org/api/follow \
-H "Content-Type: application/json" \
-d '{"username":"user1","target":"https://freelifemakers.com/users/user1"}'
# ๊ธ ์ฐ๊ธฐ (outbox 7๋ก ์ฆ๊ฐ + ํ๋ก์๋ค์๊ฒ ๋ฐฐ๋ฌ)
# Writing a post (added to outbox 7 + delivered to followers)
curl -X POST https://aloy-horizon.duckdns.org/api/posts \
-H "Content-Type: application/json" \
-d '{"username":"user1","content":"Hello Fediverse! #test"}'
์ํ๋ณต์ 8์ฅ 32์ / John 8:32
“๊ทธ๋ฆฌ๊ณ ๋ํฌ๋ ์ง๋ฆฌ๋ฅผ ์๊ฒ ๋ ๊ฒ์ด๋ฉฐ, ์ง๋ฆฌ๊ฐ ๋ํฌ๋ฅผ ์์ ๋กญ๊ฒ ํ ๊ฒ์ด๋ค.”
“Then you will know the truth ,and the truth will set you free”