From 3c601295b1260462dad9a8de81ed7031244d6dfb Mon Sep 17 00:00:00 2001 From: imnyang Date: Thu, 23 Apr 2026 19:11:16 +0900 Subject: [PATCH] wow --- apps/backend/src/models/media.ts | 2 ++ apps/backend/src/routes/post.ts | 17 +++++++---- apps/frontend/src/app/add/page.tsx | 28 ++++++++++++++---- apps/frontend/src/app/detail/[id]/page.tsx | 14 ++++++++- apps/frontend/src/app/page.tsx | 34 +++++++++++++++++++++- 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/models/media.ts b/apps/backend/src/models/media.ts index 3fb00b1..64310f4 100644 --- a/apps/backend/src/models/media.ts +++ b/apps/backend/src/models/media.ts @@ -11,6 +11,8 @@ const mediaUploadSchema = new mongoose.Schema({ mediaIndex: { type: Number, required: true }, mediaUrl: { type: String, required: true }, s3Key: { type: String, required: true }, + mediaType: { type: String, enum: ["image", "video"], default: "image" }, + thumbnailUrl: { type: String }, tags: { type: [String], default: [] }, author: { type: String, required: true }, uploadedBy: { diff --git a/apps/backend/src/routes/post.ts b/apps/backend/src/routes/post.ts index b245264..d395c4d 100644 --- a/apps/backend/src/routes/post.ts +++ b/apps/backend/src/routes/post.ts @@ -256,6 +256,8 @@ export default new Elysia({ prefix: "/post" }) } : undefined, mediaUrl: post.mediaUrl, mediaIndex: post.mediaIndex, + mediaType: post.mediaType, + thumbnailUrl: post.thumbnailUrl, }; }, { params: t.Object({ @@ -558,12 +560,11 @@ export default new Elysia({ prefix: "/post" }) let savedCount = 0; let failedCount = 0; if (tweetData.tweet) { - const media = tweetData.tweet.media.photos || []; + const media = tweetData.tweet.media.all || tweetData.tweet.media.photos || []; if (media.length > 0) { - const mediaUrls = media.map((m: any) => m.url); const hasExplicitSelection = body.selected.length > 0; const savedIds: string[] = []; - for (const [index, url] of mediaUrls.entries()) { + for (const [index, mediaItem] of media.entries()) { const isSelected = hasExplicitSelection ? body.selected[index] === true : true; @@ -572,13 +573,15 @@ export default new Elysia({ prefix: "/post" }) continue; } - const fileName = makeS3FileName(tweetData.tweet.author.id, tweetData.tweet.id, url, index); + const fileName = makeS3FileName(tweetData.tweet.author.id, tweetData.tweet.id, mediaItem.url, index); const { media: _media, ...tweetWithoutMedia } = tweetData.tweet; const normalizedTags = normalizeTags(body.tag || ["미분류"]); + const mediaType = (mediaItem.type === "video" || mediaItem.type === "gif") ? "video" : "image"; + const result = await uploadAndCreateWithRetry({ fileName, - mediaUrl: url, + mediaUrl: mediaItem.url, mediaIndex: index, createDocument: async () => { return await MediaUpload.create({ @@ -587,6 +590,8 @@ export default new Elysia({ prefix: "/post" }) mediaIndex: index, mediaUrl: `${config.s3.endpoint}/${config.s3.bucket}/${fileName}`, s3Key: fileName, + mediaType, + thumbnailUrl: mediaItem.thumbnail_url, tags: normalizedTags, author: body.author ? body.author : tweetData.tweet.author.name, uploadedBy: { @@ -601,7 +606,7 @@ export default new Elysia({ prefix: "/post" }) if (!result.ok) { failedCount += 1; - console.error(`[Upload failed] index=${index} url=${url} key=${fileName}`, result.error); + console.error(`[Upload failed] index=${index} url=${mediaItem.url} key=${fileName}`, result.error); continue; } diff --git a/apps/frontend/src/app/add/page.tsx b/apps/frontend/src/app/add/page.tsx index 17066eb..9ecb000 100644 --- a/apps/frontend/src/app/add/page.tsx +++ b/apps/frontend/src/app/add/page.tsx @@ -9,12 +9,20 @@ type SourceType = "twitter" | "pixiv"; type PreviewItem = { url: string; + type?: "image" | "video"; }; type TweetApiResponse = { tweet?: { author?: { name?: string }; - media?: { photos?: Array<{ url?: string }> }; + media?: { + photos?: Array<{ url?: string }>; + all?: Array<{ + url?: string; + thumbnail_url?: string; + type?: "photo" | "video" | "gif"; + }>; + }; }; }; @@ -176,10 +184,13 @@ export default function AddPage() { } const data = (await response.json()) as TweetApiResponse; - const items = (data.tweet?.media?.photos ?? []) - .map((photo) => photo.url) - .filter((photoUrl): photoUrl is string => typeof photoUrl === "string" && photoUrl.length > 0) - .map((photoUrl) => ({ url: photoUrl })); + const mediaItems = data.tweet?.media?.all || data.tweet?.media?.photos || []; + const items: PreviewItem[] = mediaItems + .map((m) => ({ + url: (m as any).thumbnail_url || (m as any).url || "", + type: ((m as any).type === "video" || (m as any).type === "gif") ? "video" : "image" + })) + .filter((item) => item.url.length > 0); if (items.length === 0) { throw new Error("이미지를 찾지 못했습니다."); @@ -462,6 +473,13 @@ export default function AddPage() { decoding="async" className={`aspect-4/5 w-full object-cover transition ${selected[index] ? "opacity-100" : "opacity-35 grayscale-20"}`} /> + {item.type === "video" && ( +
+ + + +
+ )}
- {post.mediaUrl ? ( + {post.mediaType === "video" ? ( +