{"id":11290,"date":"2026-09-30T08:13:53","date_gmt":"2026-09-29T23:13:53","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=11290"},"modified":"2026-09-30T13:02:03","modified_gmt":"2026-09-30T04:02:03","slug":"nextjs-sns-server-42","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/09\/30\/nextjs-sns-server-42\/","title":{"rendered":"[nextjs]SNS Server-42(myapp52)\u00a0"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb myapp52\uc5d0\uc11c\ub294 \uae00 \uc791\uc131 \ud6c4 \uc774\ubbf8\uc9c0 \uc0ad\uc81c \ubd80\ubd84\uc744 \uad6c\ud604\ud569\ub2c8\ub2e4.<br>In myapp52, we are implementing the image deletion feature for posts.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uae00 \uc791\uc131\ud558\uba74\uc11c \uc774\ubbf8\uc9c0 \ucca8\ubd80\ud558\uace0 \uc11c\ubc84 \uc624\ub958\ub098 \ube0c\ub77c\uc6b0\uc800 \uc0c8\ub85c\uace0\uce68\uc2dc \uace0\uc544\ud30c\uc77c\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.<br>Orphaned files are created when attaching images while writing a post if a server error occurs or the browser is refreshed.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb api\/clenup-orphan \ub77c\uc6b0\ud2b8\ub97c \uc774\uc6a9\ud574\uc11c \uc815\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. <br>You can clean them up using the <code>api\/cleanup-orphan<\/code> route.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc804\uccb4 \ucf54\ub4dc\ub294 \uae43\ud5c8\ube0c\uc5d0\uc11c \ud655\uc778 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br>You can find the full code on GitHub.<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/gideonslife01\/flm-nextjs\">https:\/\/github.com\/gideonslife01\/flm-nextjs<\/a><\/p>\n\n\n\n<p>\ud83d\udcc1 \uc804\uccb4 \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 \/ Overall Project Structure<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>myapp project\/  \n\u251c\u2500\u2500 app\/  (Next.js App Router)\n\u2502   \u251c\u2500\u2500 .well-known\/webfinger\/route.ts  -&gt; webfinger\n\u2502   \u251c\u2500\u2500 api\/follow\/route.ts    -&gt; Follow API(temporary)\n\u2502   \u251c\u2500\u2500 api\/announce\/route.ts  -&gt; Boost(Announcement)\n\u2502   \u251c\u2500\u2500 api\/like\/route.ts.     -&gt; Like API\n\u2502   \u251c\u2500\u2500 api\/posts\/route.ts     -&gt; Writing API(deprecated)\n\u2502   \u251c\u2500\u2500 api\/timeline\/route.ts  -&gt; Timeline API\n\u2502   \u251c\u2500\u2500 api\/followinglist\/route.ts  -&gt; followinglist API\n\u2502   \u251c\u2500\u2500 api\/followerslist\/route.ts  -&gt; followerslist API\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v1\/instance<\/span>\/route.ts -&gt; auth\n\u2502   \u251c\u2500\u2500 api\/v1\/apps\/route.ts.    -&gt; auth\n\u2502   \u251c\u2500\u2500 api\/v1\/accounts\/verify_credentials\/route.ts -&gt; auth\n\u2502   \u251c\u2500\u2500 api\/v1\/statuses\/route.ts -&gt; Write Post\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v1\/timelines\/home\/route.ts<\/span> -&gt; timeline\n\u2502   \u251c\u2500\u2500 api\/v1\/media\/route.ts -&gt; Image upload\n\u2502   \u251c\u2500\u2500 <strong>api\/v1\/media\/&#91;id]\/route.ts  -&gt; Image Delete <\/strong>\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">oauth\/authorize\/route.ts<\/span> -&gt; auth\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">oauth\/token\/route.ts<\/span> -&gt; auth\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v1\/search\/home\/route.ts<\/span> -&gt; search\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v2\/search\/home\/route.ts<\/span> -&gt; search\n\u2502   \u251c\u2500\u2500 api\/v1\/accounts\/&#91;id]\/followers\/route.ts -&gt; followers\n\u2502   \u251c\u2500\u2500 api\/v1\/accounts\/&#91;id]\/following\/route.ts -&gt; following\n\u2502   \u251c\u2500\u2500 api\/v1\/statuses\/&#91;id]\/route.ts -&gt; -&gt; Post delete(pinafore)\n\u2502   \u251c\u2500\u2500 api\/auth\/signup\/route.ts        -&gt; Signup\n\u2502   \u251c\u2500\u2500 api\/auth\/login\/route.ts         -&gt; Login\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/auth\/logout\/route.ts        -&gt; Logout\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/auth\/refresh\/route.ts       -&gt; Refresh Token\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/cleanup-orphan\/route.ts     -&gt; post,outbox clean up\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/auth\/me\/route.tsx           -&gt; Login Check<strong>\n<\/strong>\u2502   \u251c\u2500\u2500 auth\/signup\/page.tsx            -&gt; Signup UI\n\u2502   \u251c\u2500\u2500 auth\/signup\/page.tsx            -&gt; Login UI\n\u2502   \u251c\u2500\u2500 users\/&#91;username]\/\n\u2502   \u2502   \u251c\u2500\u2500 statuses\/&#91;id]\/route.ts -&gt; Indivisual Post\n\u2502   \u2502   \u251c\u2500\u2500 route.ts               -&gt; Acotr Information\n\u2502   \u2502   \u251c\u2500\u2500 followers\/route.ts     -&gt; Followers List\n\u2502   \u2502   \u251c\u2500\u2500 following\/route.ts     -&gt; Following List\n\u2502   \u2502   \u251c\u2500\u2500 inbox\/route.ts         -&gt; Inbox\n\u2502   \u2502   \u2514\u2500\u2500 outbox\/route.ts        -&gt; outbox\n\u2502   \u251c\u2500\u2500 usersui\/&#91;username]\/\n\u2502   \u2502   \u251c\u2500\u2500 page.tsx               -&gt; Timeline UI(Server, 404)\n\u2502   \u2502   \u251c\u2500\u2500 ClientPage.tsx.        -&gt; Timeline UI(Client,Call Active Themes)\n\u2502   \u2502   \u2514\u2500\u2500 _components\/themes\/\n\u2502   \u2502       \u251c\u2500\u2500 themeex\/ThemeexTheme.tsx   -&gt; Example Theme\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/PinaforeTheme.tsx -&gt; Theme 1\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/FollowersList.tsx -&gt; Theme 1, FollowersList\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/FollowingList.tsx -&gt; Theme 1, FollowingList\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/NotificationsList.tsx -&gt; Theme 1, NotificationsList\n\u2502   \u2502       \u251c\u2500\u2500 mastodon\/MastodonTheme.tsx -&gt; Theme 2\n\u2502   \u2502       \u2514\u2500\u2500 minimal\/MinimaltTheme.tsx  -&gt; Theme 3\n\u2502   \u251c\u2500\u2500 layout.tsx, page.tsx, globals.css\n\u2502   \u2514\u2500\u2500 favicon.ico\n\u251c\u2500\u2500 lib\/\n\u2502   \u251c\u2500\u2500 theme.tsx              -&gt; Theme Provider\n\u2502   \u251c\u2500\u2500 watchThemes.ts         -&gt; Check real-time theme changes\n\u2502   \u251c\u2500\u2500 auth.ts                -&gt; Authentication, User Management\n\u2502   \u251c\u2500\u2500 ap.ts                  -&gt; Follow,Undo,Create,Likes,Announce\n\u2502   \u251c\u2500\u2500 visibility.ts          -&gt; Visibility(Public,Unlisted,Private,Direct)\n\u2502   \u2514\u2500\u2500 db.ts                  -&gt; DB connection\n\u251c\u2500\u2500 data\/\n\u2502   \u251c\u2500\u2500 keys\/userIDs\/          -&gt; private.pem, public.pem(New)\n\u2502   \u2514\u2500\u2500 keys\/                  -&gt; private.pem, public.pem(legacy)\n\u251c\u2500\u2500 data.sqlite                -&gt; Database(1\/3)\n\u251c\u2500\u2500 data.sqlite-wal            -&gt; Database(2\/3)\n\u251c\u2500\u2500 data.sqlite-shm            -&gt; Database(3\/3)\n\u251c\u2500\u2500 Caddyfile                  -&gt; https \n\u251c\u2500\u2500 instrumentation.ts         -&gt; Background Server\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uc2dc\uc791 \/ Project Start<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-freelifemakers-org wp-block-embed-freelifemakers-org\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"Dlkn67m4G2\"><a href=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/09\/03\/nextjs-project-summary\/\">[nextjs]\ud504\ub85c\uc81d\ud2b8\uc694\uc57d\/project summary(myapp30+)<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; visibility: hidden;\" title=\"&#8220;[nextjs]\ud504\ub85c\uc81d\ud2b8\uc694\uc57d\/project summary(myapp30+)&#8221; &#8212; freelifemakers.org\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/09\/03\/nextjs-project-summary\/embed\/#?secret=cxLSLBmlpb#?secret=Dlkn67m4G2\" data-secret=\"Dlkn67m4G2\" width=\"560\" height=\"315\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udcc1 \ucf54\ub4dc\uc218\uc815 \/ Code Modification<\/p>\n\n\n\n<p>\u2714\ufe0f app\/api\/v1\/statuses\/[id]\/route.ts<\/p>\n\n\n\n<p>&#8212;  \uc774\ubbf8\uc9c0 \uc0ad\uc81c \uae30\ub2a5 \ucd94\uac00 \/ Added image deletion feature.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>import fs from 'fs';     \/\/ \u2705 myapp52\nimport path from 'path'; \/\/ \u2705 myapp52\n... ...\n\nconst UPLOAD_DIR = path.join(process.cwd(), 'public', 'uploads'); \/\/ \u2705 myapp52\n\nexport async function DELETE(\n  req: Request,\n  { params }: { params: Promise&lt;{ id: string }&gt; }\n) {\n... ...\n\n  if (!post) { \/\/ \u2705 myapp52\n    return NextResponse.json({ id, deleted: true, alreadyDeleted: true },\/\/{ error: 'not found' }, { status: 404 } error?\n    {\n      headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' }\n    });\n  }\n\n  if (post.username !== username) {\n    console.log(`\ud83d\udeab \uc0ad\uc81c \uac70\ubd80\/Refusal to delete: ${username}\uac00 ${post.username} \uae00 \uc0ad\uc81c \uc2dc\ub3c4`);\n    return NextResponse.json({ error: 'forbidden - not your post' }, { \n      status: 403, \n      headers: { 'Access-Control-Allow-Origin': '*' } \n    });\n  }\n  \n\/\/ \u2705 myapp52 \n  \/\/ -  \uc5f0\uacb0\ub41c \ubbf8\ub514\uc5b4(\uc774\ubbf8\uc9c0) \ucc3e\uae30 \/ Find linked media (images)\n  const medias = db.prepare(`\n      SELECT ma.* FROM media_attachments ma\n      JOIN post_media pm ON pm.media_id = ma.id\n      WHERE pm.post_id = ? AND ma.username = ?\n  `).all(id, username) as any&#91;];\n\n  \/\/ \u2705 myapp52 -  \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c DB \uc815\ub9ac \/ Clean up DB with transactions\n  \/\/ - DB\uc5d0 CASCADE \uc788\uc73c\ub098 \ud655\uc778 \uc0ad\uc81c \/ Check for and delete CASCADE settings in the DB.\n  const tx = db.transaction(() =&gt; {\n    \/\/ post_media \uba3c\uc800 \/ first post_media\n    db.prepare('DELETE FROM post_media WHERE post_id=?').run(id);\n    \/\/ media_attachments\n    if (medias.length &gt; 0) {\n      const placeholders = medias.map(() =&gt; '?').join(',');\n      db.prepare(`DELETE FROM media_attachments WHERE id IN (${placeholders}) AND username=?`)\n      .run(...medias.map(m =&gt; m.id), username);\n    }\n    \/\/ delete posts\n    db.prepare('DELETE FROM posts WHERE id=? AND username=?').run(id, username);\n    \/\/ delete outbox\n    db.prepare('DELETE FROM outbox WHERE object LIKE ?').run(`%\/${id}%`);\n    \/\/ delete inbox_posts\n    try { db.prepare('DELETE FROM inbox_posts WHERE original_id LIKE ? OR id LIKE ?').run(`%${id}%`, `%${id}%`); } catch {}\n  });\n  tx();\n\n    \/\/ \u2705 myapp52 - \uc2e4\uc81c \ud30c\uc77c \uc0ad\uc81c \/ Actual file deletion\n    for (const m of medias) {\n      try {\n        \/\/ m.url = https:\/\/aloy-horizon.duckdns.org\/uploads\/xxx.jpg\n        \/\/ or \/uploads\/xxx.jpg\n        const filename = path.basename(new URL(m.url, `https:\/\/${DOMAIN}`).pathname);\n        const filePath = path.join(UPLOAD_DIR, filename);\n        if (fs.existsSync(filePath)) {\n          fs.unlinkSync(filePath);\n          console.log(`\ud83d\uddd1 file deleted: ${filename}`);\n        }\n      } catch (e) {\n        console.error('file delete fail', m.id, e);\n      }\n    }\n... ...\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f app\/api\/v1\/media\/route.ts<\/p>\n\n\n\n<p>&#8212; \ud30c\uc77c \uc5c5\ub85c\ub4dc \uac2f\uc218 \uc81c\ud55c \uc124\uc815(4\uac1c), \ud30c\uc77c \uc5c5\ub85c\ub4dc \uc6a9\ub7c9\uc81c\ud55c \uc124\uc815(1MB),\uc774\ubbf8\uc9c0 \ud30c\uc77c\ub9cc \uc5c5\ub85c\ub4dc \uc124\uc815<br>Set file upload limit (4 files), set file upload size limit (1MB), and restrict uploads to image files only.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>\/\/ \u2705 myapp52 - \ud30c\uc77c \ud5c8\uc6a9 \ud0c0\uc785 + \ud30c\uc77c \uac2f\uc218 \uc81c\ud55c + \ud30c\uc77c \uc5c5\ub85c\ub4dc \uc6a9\ub7c9 \uc81c\ud55c\n\/\/ Allowed file types + File count limit + File upload size limit\nconst ALLOWED_MIMES = &#91;'image\/jpeg', 'image\/png', 'image\/gif', 'image\/webp'];\nconst MAX_FILE_SIZE = 1 * 1024 * 1024; \/\/ 1MB(10 * 1024 * 1024 = 10MB)\nconst MAX_FILES_PER_USER_PENDING = 4; \/\/ \uae00\uc5d0 \ucca8\ubd80 \uac00\ub2a5\ud55c \uc774\ubbf8\uc9c0\ub294 4\uac1c\uae4c\uc9c0\ub9cc \ud5c8\uc6a9 \/ A maximum of four images can be attached to the post.\n\n\nexport async function POST(req: Request) {\n  \/\/ \u2705 myapp52 - \ud30c\uc77c \uac2f\uc218 \uc81c\ud55c \/ File count limit\n  try {\n    const pending = db.prepare(`\n      SELECT COUNT(*) as c FROM media_attachments\n      WHERE username=? AND id NOT IN (SELECT media_id FROM post_media)\n    `).get(username) as any;\n    if (pending.c &gt;= MAX_FILES_PER_USER_PENDING) {\n      return NextResponse.json({error:`\uc774\ubbf8\uc9c0 ${MAX_FILES_PER_USER_PENDING}\uac1c\uae4c\uc9c0\ub9cc \uc5c5\ub85c\ub4dc \uac00\ub2a5 - \uba3c\uc800 \uae00\uc744 \uc4f0\uac70\ub098 \uc0ad\uc81c\ud558\uc138\uc694\\n`}, {status:400});\n    }\n  } catch {}\n... ...\n  \/\/ \u2705 myapp52 - \ud30c\uc77c \uc6a9\ub7c9 \uc81c\ud55c \/ File size limit\n  if (file.size === 0) return NextResponse.json({error:'empty file'}, {status:400});\n  if (file.size &gt; MAX_FILE_SIZE) return NextResponse.json({error:`${MAX_FILE_SIZE\/1024\/1024}MB \uc774\ud558\ub9cc (\ud604\uc7ac ${(file.size\/1024\/1024).toFixed(2)}MB)`}, {status:413});\n\n  \/\/ \u2705 myapp52 - \ub9c8\uc784\ud0c0\uc785 \uccb4\ud06c(\ud30c\uc77c \ud0c0\uc785\uccb4\ud06c) \/ MIME type check (file type check)\n    if (!ALLOWED_MIMES.includes(file.type)) {\n    return NextResponse.json({error:`\ud5c8\uc6a9 \uc548\ub428: ${file.type}. \ud5c8\uc6a9: ${ALLOWED_MIMES.join(', ')}`}, {status:400});\n  }\n... ...\n  \/\/ const ext = file.name.split('.').pop() || 'jpg';\n  \/\/ \u2705 myapp52 - \ud30c\uc77c \ud655\uc7a5\uc790 \uc704\uc870 \ubc29\uc5b4 \/ Protection against file extension spoofing\n  const ext = file.type === 'image\/jpeg'? 'jpg' : file.type.split('\/')&#91;1]; \/\/ png, gif, webp\n  if (!&#91;'jpg','png','gif','webp'].includes(ext)) {\n    return NextResponse.json({error:'invalid extension'}, {status:400});\n  }\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f app\/api\/v1\/media\/[id]\/route.ts &#8211; \ub77c\uc6b0\ud2b8 \ucd94\uac00 \/ Add route<\/p>\n\n\n\n<p>&#8212; \ud14c\ub9c8UI\uc5d0 \uc774\ubbf8\uc9c0 \uc120\ud0dd\ud6c4 \uc120\ud0dd \ucde8\uc18c\uc2dc \uc0ad\uc81c \ud558\ub294 \uae30\ub2a5<br>Function to delete an image when it is deselected in the theme UI.<\/p>\n\n\n\n<p>&#8212; \uae43\ud5c8\ube0c \uc804\uccb4 \ucf54\ub4dc \ucc38\uc870 \/ Refer to the full code on GitHub.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>import db from '@\/lib\/db';\nimport fs from 'fs';\nimport path from 'path';\nimport { NextResponse } from 'next\/server';\n\nconst UPLOAD_DIR = path.join(process.cwd(), 'public', 'uploads');\n\nfunction getUser(req: Request) {\n... ...\n}\nexport async function DELETE(req: Request, { params }: { params: Promise&lt;{ id: string }&gt; }) {\n... ...\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f app\/usersui\/[username]\/_components\/themes\/pinafore\/PinaforeTheme.tsx<\/p>\n\n\n\n<p>&#8212; \uc774\ubbf8\uc9c0 \uc0ad\uc81c(\ud504\ub9ac\ubdf0,\uc11c\ubc84) \/ Delete image (preview, server)<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>... ...\n\/\/ \u2705 myapp52 - \uc774\ubbf8\uc9c0 \uc0ad\uc81c(\ud504\ub9ac\ubdf0,\uc11c\ubc84) \/ Delete image (preview, server)\n  const removePreview = async (idx: number) =&gt; {\n      const mediaId = mediaIds&#91;idx];\n  \/\/ \uc11c\ubc84\uc5d0\uc11c\ub3c4 \uc0ad\uc81c \/ Delete from the server as well\n  try {\n    await fetch(`\/api\/v1\/media\/${mediaId}`, { method: 'DELETE', credentials: 'include' });\n  } catch {}\n    setMediaIds(prev =&gt; prev.filter((_, i) =&gt; i!== idx));\n    setPreviews(prev =&gt; prev.filter((_, i) =&gt; i!== idx));\n  };\n... ...<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f \ubc84\uadf8 \uc218\uc815 \/ Bug fixes<\/p>\n\n\n\n<p>&#8212; \uc774\ubbf8\uc9c0 \uc5c5\ub85c\ub4dc \ud560\uacbd\uc6b0 \uc5d0\ub7ec \uacbd\uace0\ucc3d \ud55c\ubc88\uc5d0 \ub744\uc6b0\uae30(\uae30\uc874 \ud30c\uc77c 3\uac1c \uc5c5\ub85c\ub4dc\ud558\uba74 \uacbd\uace0\ucc3d 3\uac1c \uc2e4\ud589\ub428.)<br>Display a single error alert when uploading images (previously, uploading three files would trigger three separate alert windows).<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code> async function onFiles(e: React.ChangeEvent&lt;HTMLInputElement&gt;) {\n      const failed: string&#91;] = &#91;]; \/\/ \u2705 myapp52 - \uc2e4\ud328 \ubaa8\uc74c \/ Compilation of Failures\n... ...\n        \n      \/\/ \u2705 myapp52 - \ub8e8\ud504 \ub05d\ub098\uace0 \uacbd\uace0\ucc3d \ud55c\ubc88\ub9cc \ub744\uc6b0\uae30 \/ Display a warning message only once after the loop finishes.\n      if (failed.length &gt; 0) {\n        alert(`\uc5c5\ub85c\ub4dc \uc2e4\ud328\/upload failed ${failed.length}\uac1c:\\n${failed.join('\\n')}`);\n      }\n      } catch (err: any) {\n        console.error(err);\n        alert(`\uc5c5\ub85c\ub4dc \uc2e4\ud328\/upload failed: ${err.message}`);\n      } finally {\n        setUploading(false);\n        \/\/ \uac19\uc740 \ud30c\uc77c \ub2e4\uc2dc \uc120\ud0dd \uac00\ub2a5\ud558\ub3c4\ub85d reset \/ Reset to allow re-selecting the same file.\n        e.target.value = '';\n      }\n\n... ...\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f \uace0\uc544\ud30c\uc77c \uc0ad\uc81c \uae30\ub2a5 \/ Orphaned file deletion function<\/p>\n\n\n\n<p>&#8212; \ud30c\uc77c \uc815\ub9ac\uc6a9\uc73c\ub85c \uc0ac\uc6a9\ub418\uba70 \ubbf8\ub514\uc5b4 \ud30c\uc77c\uc0ad\uc81c \ubd80\ubd84\uc774 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<br>It is used for file organization, and a media file deletion feature has been added.<\/p>\n\n\n\n<p>&#8212; \uc804\uccb4 \ucf54\ub4dc\ub294 \uae43\ud5c8\ube0c \ucf54\ub4dc\ub97c \ucc38\uc870\ud558\uc138\uc694<br>Please refer to the GitHub repository for the full code.<\/p>\n\n\n\n<p>&#8212; \uc0ac\uc6a9\ubc29\ubc95 \/ How to use<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code># \ud1a0\ud070 \uba3c\uc800 \ud655\uc778 (\ube0c\ub77c\uc6b0\uc800 \uac1c\ubc1c\uc790\ub3c4\uad6c &gt; Application &gt; LocalStorage &gt; refresh_token)\n# \nexport TOKEN=\"your_refresh_token\"\n\n# 1. \uc804\uccb4 \uc0ad\uc81c \uc815\ubcf4\ud655\uc778 \/ Check Bulk Deletion Information (GET)\ncurl \"https:\/\/aloy-horizon.duckdns.org\/api\/cleanup-orphan\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n\n# \ub2e8\uc77c \uae00 \uc0c1\ud0dc \ud655\uc778 \/ Check single post status\ncurl \"https:\/\/aloy-horizon.duckdns.org\/api\/cleanup-orphan?id=5a24536b-06ec-400c-8539-4c1350dd1d19\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n\n# 2. \uc720\ub839\uae00 + 24\uc2dc\uac04 \uc9c0\ub09c \ubbf8\ub514\uc5b4 \uace0\uc544\ub9cc \uc0ad\uc81c \/ Delete only \"ghost posts\" and media items older than 24 hours.(POST)\ncurl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/cleanup-orphan \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application\/json\"\n\n# \uc751\ub2f5 \uc608 \/ Response Example: \n{\"orphans\":3,\"deleted\":&#91;...],\"deletedFiles\":2,\"orphanMediasCleaned\":2}\n\n# 3. \ub9ac\ubaa8\ud2b8 + \ub85c\uceec + \ubbf8\ub514\uc5b4 \uc804\uccb4 \uc0ad\uc81c \/ Delete all remote, local, and media files (DELETE all)\ncurl -X DELETE \"https:\/\/aloy-horizon.duckdns.org\/api\/cleanup-orphan?all=true\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n\n# 4. \ub9ac\ubaa8\ud2b8 + \ub85c\uceec \ub2e8\uc77c \uae00 \uc0ad\uc81c \/ Delete single post (remote + local)(DELETE id)\ncurl -X DELETE \"https:\/\/aloy-horizon.duckdns.org\/api\/cleanup-orphan?id=5a24536b-06ec-400c-8539-4c1350dd1d19\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n\n# \uc751\ub2f5 \uc608 \/ Response Example: \n# {\"message\":\"\u2705 5a24536b... \ub9ac\ubaa8\ud2b8+\ub85c\uceec \uc0ad\uc81c \uc644\ub8cc \/ Remote and local deletion complete.\ub8cc!\",\"deletedId\":\"5a24536b...\",\"deletedFiles\":2,...}<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud14c\uc2a4\ud2b8 \/ Test<\/p>\n\n\n\n<p>\u2714\ufe0f \ub9ac\ubaa8\ud2b8 \uc11c\ubc84 \/ Remote Server<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"884\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myap52-p-test1-jpg.jpg\" alt=\"\" class=\"wp-image-11334\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myap52-p-test1-jpg.jpg 800w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myap52-p-test1-jpg-271x300.jpg 271w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myap52-p-test1-jpg-768x849.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myap52-p-test1-jpg-400x442.jpg 400w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">Remote Server<\/figcaption><\/figure>\n\n\n\n<p>\u2714\ufe0f \ub85c\uceec\uc11c\ubc84 \/ Local Server<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"799\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-test4-jpg.jpg\" alt=\"\" class=\"wp-image-11333\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-test4-jpg.jpg 800w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-test4-jpg-300x300.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-test4-jpg-150x150.jpg 150w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-test4-jpg-768x767.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-test4-jpg-400x400.jpg 400w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">Local Server<\/figcaption><\/figure>\n\n\n\n<p>\u2714\ufe0f \ud30c\uc77c \uc5c5\ub85c\ub4dc \uc6a9\ub7c9 \uc81c\ud55c \/ File upload size limit<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"500\" height=\"340\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/upload-failed-jpg.jpg\" alt=\"\" class=\"wp-image-11339\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/upload-failed-jpg.jpg 500w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/upload-failed-jpg-300x204.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/upload-failed-jpg-400x272.jpg 400w\" sizes=\"auto, (max-width: 500px) 100vw, 500px\" \/><\/figure>\n\n\n\n<p> File upload size limit<\/p>\n\n\n\n<p>\u2714\ufe0f \ud30c\uc77c \uc5c5\ub85c\ub4dc \uc218\ub7c9 \uc81c\ud55c \/ File upload quantity limit<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"468\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-file-limits-jpg.jpg\" alt=\"\" class=\"wp-image-11336\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-file-limits-jpg.jpg 800w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-file-limits-jpg-300x176.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-file-limits-jpg-768x449.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/09\/myapp52-file-limits-jpg-400x234.jpg 400w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/figure>\n\n\n\n<p> Quantity limit<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\uc694\ud55c\ubcf5\uc74c 8\uc7a5 32\uc808 \/ John 8:32<\/p>\n\n\n\n<p>&#8220;\uadf8\ub9ac\uace0 \ub108\ud76c\ub294 \uc9c4\ub9ac\ub97c \uc54c\uac8c \ub420 \uac83\uc774\uba70, \uc9c4\ub9ac\uac00 \ub108\ud76c\ub97c \uc790\uc720\ub86d\uac8c \ud560 \uac83\uc774\ub2e4.&#8221;<\/p>\n\n\n\n<p>&#8220;Then you will know the truth ,and the truth will set you free&#8221;<\/p>\n<\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb myapp52\uc5d0\uc11c\ub294 \uae00 \uc791\uc131 \ud6c4 \uc774\ubbf8\uc9c0 \uc0ad\uc81c \ubd80\ubd84\uc744 \uad6c\ud604\ud569\ub2c8\ub2e4.In myapp52, we are implementing the image deletion feature for posts. \ud83d\udc49\ud83c\udffb \uae00 \uc791\uc131\ud558\uba74\uc11c \uc774\ubbf8\uc9c0 \ucca8\ubd80\ud558\uace0 \uc11c\ubc84 \uc624\ub958\ub098 \ube0c\ub77c\uc6b0\uc800 \uc0c8\ub85c\uace0\uce68\uc2dc \uace0\uc544\ud30c\uc77c\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.Orphaned files are created when attaching images while writing a post if a server error occurs or the browser is refreshed. \ud83d\udc49\ud83c\udffb api\/clenup-orphan \ub77c\uc6b0\ud2b8\ub97c \uc774\uc6a9\ud574\uc11c \uc815\ub9ac\ud560 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[28,27,1,7,29],"tags":[],"class_list":["post-11290","post","type-post","status-publish","format-standard","hentry","category-news","category-nextjs","category-uncategorized","category-website","category-webui","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/11290","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=11290"}],"version-history":[{"count":36,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/11290\/revisions"}],"predecessor-version":[{"id":11342,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/11290\/revisions\/11342"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=11290"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=11290"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=11290"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}