feat: implement post existence check and detail page
This commit is contained in:
parent
55af0549e7
commit
b18cff8b1a
10 changed files with 646 additions and 43 deletions
|
|
@ -6,12 +6,56 @@ import { Tag } from "@/models/tag";
|
|||
import { User } from "@/models/user";
|
||||
import { createAuditLog } from "@/lib/audit";
|
||||
import { normalizeQueryTags, normalizeTags } from "@/lib/tag";
|
||||
import { checkTweetData, fetchTweetData } from "@/lib/tweet";
|
||||
import { fetchTweetData } from "@/lib/tweet";
|
||||
import { fetchPixivData } from "@/lib/pixiv";
|
||||
import { checkExistingPostByUrl } from "@/lib/post";
|
||||
import { makeS3FileName, s3Client, uploadToS3 } from "@/lib/s3";
|
||||
|
||||
const inFlightUploads = new Set<string>();
|
||||
|
||||
type UploadResponse = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
savedCount?: number;
|
||||
failedCount?: number;
|
||||
};
|
||||
|
||||
type ExistsResponse = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
exists: boolean;
|
||||
source: "twitter" | "pixiv" | null;
|
||||
postId: string | null;
|
||||
documentId: string | null;
|
||||
};
|
||||
|
||||
function uploadOk(message: string, extra?: Omit<UploadResponse, "success" | "message">): UploadResponse {
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function uploadError(message: string, extra?: Omit<UploadResponse, "success" | "message">): UploadResponse {
|
||||
return {
|
||||
success: false,
|
||||
message,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function existsResponse(message: string, exists: boolean, source: ExistsResponse["source"], postId: string | null, documentId: string | null): ExistsResponse {
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
exists,
|
||||
source,
|
||||
postId,
|
||||
documentId,
|
||||
};
|
||||
}
|
||||
|
||||
function buildUploadKey(url: string, selected: boolean[]) {
|
||||
const match = url.match(/\/status\/(\d+)/);
|
||||
const tweetId = match?.[1] ?? url;
|
||||
|
|
@ -169,6 +213,22 @@ export default new Elysia({ prefix: "/post" })
|
|||
return tags;
|
||||
})
|
||||
|
||||
.get("/exists", async ({ query }) => {
|
||||
const result = await checkExistingPostByUrl(query.url);
|
||||
|
||||
return existsResponse(
|
||||
result.exists ? "이미 저장된 게시물입니다." : "저장된 게시물이 없습니다.",
|
||||
result.exists,
|
||||
result.source,
|
||||
result.postId,
|
||||
result.documentId,
|
||||
);
|
||||
}, {
|
||||
query: t.Object({
|
||||
url: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
.get("/detail/:id", async ({ params, status }) => {
|
||||
const post = await MediaUpload.findById(params.id);
|
||||
if (!post) {
|
||||
|
|
@ -323,13 +383,18 @@ export default new Elysia({ prefix: "/post" })
|
|||
return status(403, "Forbidden");
|
||||
}
|
||||
|
||||
const existingPost = await checkExistingPostByUrl(body.url);
|
||||
if (existingPost.exists) {
|
||||
return uploadOk("이미 저장된 게시물입니다.", { savedCount: 0, failedCount: 0 });
|
||||
}
|
||||
|
||||
if (body.url.startsWith("https://www.pixiv.net/")) {
|
||||
const requestId = crypto.randomUUID();
|
||||
const uploadKey = buildUploadKey(body.url, body.selected);
|
||||
|
||||
if (inFlightUploads.has(uploadKey)) {
|
||||
console.warn(`[Upload skipped-duplicate] requestId=${requestId} key=${uploadKey}`);
|
||||
return status(202, "이미 처리 중인 업로드입니다.");
|
||||
return status(202, uploadError("이미 처리 중인 업로드입니다."));
|
||||
}
|
||||
|
||||
inFlightUploads.add(uploadKey);
|
||||
|
|
@ -342,7 +407,7 @@ export default new Elysia({ prefix: "/post" })
|
|||
: [];
|
||||
|
||||
if (mediaUrls.length === 0) {
|
||||
return status(400, "No media found in the Pixiv artwork.");
|
||||
return status(400, uploadError("No media found in the Pixiv artwork."));
|
||||
}
|
||||
|
||||
const normalizedTags = normalizeTags(body.tag ?? ["미분류"]);
|
||||
|
|
@ -430,20 +495,26 @@ export default new Elysia({ prefix: "/post" })
|
|||
});
|
||||
console.log(`[Pixiv upload finished] requestId=${requestId} key=${uploadKey}`);
|
||||
if (savedCount === 0 && failedCount > 0) {
|
||||
return status(502, "업로드에 실패했습니다. 잠시 후 다시 시도해 주세요.");
|
||||
return status(502, uploadError("업로드에 실패했습니다. 잠시 후 다시 시도해 주세요.", {
|
||||
savedCount,
|
||||
failedCount,
|
||||
}));
|
||||
}
|
||||
|
||||
if (savedCount === 0) {
|
||||
return status(400, "선택된 이미지가 없습니다.");
|
||||
return status(400, uploadError("선택된 이미지가 없습니다.", {
|
||||
savedCount,
|
||||
failedCount,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
return uploadOk("업로드가 완료되었습니다.", {
|
||||
savedCount,
|
||||
failedCount,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[Pixiv upload aborted] requestId=${requestId} key=${uploadKey}`, error);
|
||||
return status(500, "Failed to fetch Pixiv data");
|
||||
return status(500, uploadError("Failed to fetch Pixiv data"));
|
||||
} finally {
|
||||
inFlightUploads.delete(uploadKey);
|
||||
}
|
||||
|
|
@ -453,17 +524,12 @@ export default new Elysia({ prefix: "/post" })
|
|||
|
||||
if (inFlightUploads.has(uploadKey)) {
|
||||
console.warn(`[Upload skipped-duplicate] requestId=${requestId} key=${uploadKey}`);
|
||||
return status(202, "이미 처리 중인 업로드입니다.");
|
||||
return status(202, uploadError("이미 처리 중인 업로드입니다."));
|
||||
}
|
||||
|
||||
inFlightUploads.add(uploadKey);
|
||||
console.log(`[Upload started] requestId=${requestId} key=${uploadKey}`);
|
||||
|
||||
if (await checkTweetData(body.url, body.selected)) {
|
||||
inFlightUploads.delete(uploadKey);
|
||||
console.log(`[Upload skipped-existing] requestId=${requestId} key=${uploadKey}`);
|
||||
return "이미 저장된 트윗입니다.";
|
||||
}
|
||||
try {
|
||||
const tweetData = await fetchTweetData(body.url);
|
||||
let savedCount = 0;
|
||||
|
|
@ -544,31 +610,37 @@ export default new Elysia({ prefix: "/post" })
|
|||
},
|
||||
});
|
||||
} else {
|
||||
return status(400, "트윗에서 이미지를 찾지 못했습니다.");
|
||||
return status(400, uploadError("트윗에서 이미지를 찾지 못했습니다."));
|
||||
}
|
||||
}
|
||||
console.log(`[Upload finished] requestId=${requestId} key=${uploadKey}`);
|
||||
if (savedCount === 0 && failedCount > 0) {
|
||||
return status(502, "업로드에 실패했습니다. 잠시 후 다시 시도해 주세요.");
|
||||
return status(502, uploadError("업로드에 실패했습니다. 잠시 후 다시 시도해 주세요.", {
|
||||
savedCount,
|
||||
failedCount,
|
||||
}));
|
||||
}
|
||||
|
||||
if (savedCount === 0) {
|
||||
return status(400, "선택된 이미지가 없습니다.");
|
||||
return status(400, uploadError("선택된 이미지가 없습니다.", {
|
||||
savedCount,
|
||||
failedCount,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
return uploadOk("업로드가 완료되었습니다.", {
|
||||
savedCount,
|
||||
failedCount,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[Upload aborted] requestId=${requestId} key=${uploadKey}`, error);
|
||||
console.error(error);
|
||||
return status(500, "Failed to fetch tweet data");
|
||||
return status(500, uploadError("Failed to fetch tweet data"));
|
||||
} finally {
|
||||
inFlightUploads.delete(uploadKey);
|
||||
}
|
||||
} else {
|
||||
return status(400, "어...");
|
||||
return status(400, uploadError("어..."));
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue