Skip to content

Commit

Permalink
Merge pull request #122 from sora32127/commnet_feed
Browse files Browse the repository at this point in the history
フィードページの改修
  • Loading branch information
sora32127 authored Aug 31, 2024
2 parents d8a15bc + e12f974 commit 7f63a9b
Show file tree
Hide file tree
Showing 4 changed files with 395 additions and 303 deletions.
178 changes: 177 additions & 1 deletion app/modules/db.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export async function getRecentVotedPosts(): Promise<PostCardData[]>{
by: ["postId"],
where: {
voteDateGmt : {
gte: new Date(new Date().getTime() - 24 * 60 * 60 * 1000),
gte: new Date(new Date().getTime() - 48 * 60 * 60 * 1000),
lte: new Date(),
},
voteTypeInt : { in : [1]}
Expand Down Expand Up @@ -526,3 +526,179 @@ export async function getRandomComments(){
return randomComments;
}

type FeedPostType = "unboundedLikes" | "likes" | "timeDesc" | "timeAsc"
const PostFeedDataSchema = z.object({
meta: z.object({
totalCount: z.number(),
currentPage: z.number(),
type: z.enum(["unboundedLikes", "likes", "timeDesc", "timeAsc"]),
likeFromHour: z.optional(z.number()),
likeToHour: z.optional(z.number()),
chunkSize: z.number(),
}),
result: z.array(PostCardDataSchema),
})
type PostFeedData = z.infer<typeof PostFeedDataSchema>;

export async function getFeedPosts(pagingNumber: number, type: FeedPostType, chunkSize = 12, likeFromHour = 24, likeToHour = 0,): Promise<PostFeedData>{
const offset = (pagingNumber - 1) * chunkSize;
if (["unboundedLikes", "timeDesc", "timeAsc"].includes(type)){
const posts = await prisma.$queryRaw`
select post_id, post_title, post_date_gmt, count_likes, count_dislikes, ogp_image_url
from dim_posts
${type === "unboundedLikes" ? Prisma.raw("order by count_likes desc, post_date_gmt desc")
: type === "timeDesc" ? Prisma.raw("order by post_date_gmt desc")
: type === "timeAsc" ? Prisma.raw("order by post_date_gmt asc")
: Prisma.empty}
offset ${offset} limit ${chunkSize}
` as { post_id: number; post_title: string; post_date_gmt: Date; count_likes: number; count_dislikes: number; ogp_image_url: string | null }[]
const commentCount = await prisma.dimComments.groupBy({
by: ["postId"],
_count: { commentId: true },
where: { postId: { in: posts.map((post) => post.post_id) } },
})
const tagNames = await prisma.relPostTags.findMany({
where: { postId: { in: posts.map((post) => post.post_id) } },
select: {
postId: true,
dimTag: {
select: {
tagId: true,
tagName: true,
}
}
},
})
const totalCount = await prisma.dimPosts.count();
const postData = posts.map((post) => {
return {
postId: post.post_id,
postTitle: post.post_title,
postDateGmt: post.post_date_gmt,
countLikes: post.count_likes,
countDislikes: post.count_dislikes,
ogpImageUrl: post.ogp_image_url,
tags: tagNames.filter((tag) => tag.postId === post.post_id).map((tag) => tag.dimTag),
countComments: commentCount.find((c) => c.postId === post.post_id)?._count.commentId || 0,
}
})
return {
meta: {
totalCount: totalCount,
currentPage: pagingNumber,
type: type,
chunkSize: chunkSize,
},
result: postData,
}
}
if (type === "likes"){
const voteCount = await prisma.fctPostVoteHistory.groupBy({
by: ["postId"],
_count: { voteUserIpHash: true },
where: {
voteTypeInt: { in: [1] },
voteDateGmt: {
gte: new Date(new Date().getTime() - likeFromHour * 60 * 60 * 1000),
lte: new Date(new Date().getTime() - likeToHour * 60 * 60 * 1000)
}
},
orderBy: { _count: { voteUserIpHash: "desc" } },
take: chunkSize,
skip: offset,
})
const totalCountRaw = await prisma.fctPostVoteHistory.groupBy({
by: ["postId"],
_count: { voteUserIpHash: true },
where: {
voteTypeInt: { in: [1] },
voteDateGmt: {
gte: new Date(new Date().getTime() - likeFromHour * 60 * 60 * 1000),
lte: new Date(new Date().getTime() - likeToHour * 60 * 60 * 1000)
}
}
})
const totalCount = totalCountRaw.length;
const postIds = voteCount.map((vote) => vote.postId)
const posts = await prisma.dimPosts.findMany({
where: { postId: { in: postIds } },
select: {
postId: true,
postTitle: true,
postDateGmt: true,
countLikes: true,
countDislikes: true,
ogpImageUrl: true,
rel_post_tags: {
select: {
dimTag: {
select: {
tagId: true,
tagName: true,
}
}
}
}
}
})
const countComments = await prisma.dimComments.groupBy({
by: ["postId"],
_count: { commentId: true },
where: { postId: { in: posts.map((post) => post.postId) } },
})
const postData = posts.map((post) => {
return {
...post,
countComments: countComments.find((c) => c.postId === post.postId)?._count.commentId || 0,
tags: post.rel_post_tags.map((tag) => tag.dimTag),
}
})
return {
meta: {
totalCount: totalCount,
currentPage: pagingNumber,
type: type,
likeFromHour: likeFromHour,
likeToHour: likeToHour,
chunkSize: chunkSize,
},
result: postData,
}
}
return {
meta: {
totalCount: 0,
currentPage: 0,
type: type,
chunkSize: chunkSize,
},
result: [],
}
}

export async function getOldestPostIdsForTest(chunkSize: number){
const posts = await prisma.dimPosts.findMany({
orderBy: { postDateGmt: "asc" },
take: chunkSize * 2,
})
return posts.map((post) => post.postId);
}

export async function getNewestPostIdsForTest(chunkSize: number){
const posts = await prisma.dimPosts.findMany({
orderBy: { postDateGmt: "desc" },
take: chunkSize * 2,
})
return posts.map((post) => post.postId);
}

export async function getUnboundedLikesPostIdsForTest(chunkSize: number){
const posts = await prisma.$queryRaw`
select post_id
from dim_posts
order by count_likes desc, post_date_gmt desc
limit ${chunkSize * 2}
` as { post_id: number }[]
return posts.map((post) => post.post_id);
}

122 changes: 107 additions & 15 deletions app/modules/db.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,111 @@
import { expect, test } from "vitest";
import { ArchiveDataEntry } from "./db.server";
import { ArchiveDataEntry, getFeedPosts, getNewestPostIdsForTest, getOldestPostIdsForTest, getUnboundedLikesPostIdsForTest, PostCardDataSchema } from "./db.server";
import { describe } from "node:test";


test("記事ID23576の正しいデータを返すこと", async () => {
const archiveDataEntry = await ArchiveDataEntry.getData(23576);
expect(archiveDataEntry.postId).toBe(23576);
expect(archiveDataEntry.postTitle).toBe('無神論者の火');
expect(archiveDataEntry.tags).toContainEqual({ tagName: 'クリスマス', tagId: 381 });
expect(archiveDataEntry.tags).toContainEqual({ tagName: '学生', tagId: 21 });
expect(archiveDataEntry.tags).toContainEqual({ tagName: '小学生', tagId: 35 });
expect(archiveDataEntry.countLikes).toBeGreaterThan(30);
expect(archiveDataEntry.countDislikes).toBeGreaterThan(5);
expect(archiveDataEntry.postDateGmt).toEqual(new Date('2023-02-11T05:57:26.000Z'));
expect(archiveDataEntry.postContent).not.toBe('');
expect(archiveDataEntry.similarPosts).toHaveLength(15);
expect(archiveDataEntry.previousPost.postTitle).toBe('無知識でアナルにローターを入れるべきでは無い');
expect(archiveDataEntry.nextPost.postTitle).toBe('無能が消去法で大学を決めるべきではない');
describe("記事ID23576の正しいデータを返すこと", async () => {
test("記事ID23576の正しいデータを返すこと", async () => {
const archiveDataEntry = await ArchiveDataEntry.getData(23576);
expect(archiveDataEntry.postId).toBe(23576);
expect(archiveDataEntry.postTitle).toBe('無神論者の火');
expect(archiveDataEntry.tags).toContainEqual({ tagName: 'クリスマス', tagId: 381 });
expect(archiveDataEntry.tags).toContainEqual({ tagName: '学生', tagId: 21 });
expect(archiveDataEntry.tags).toContainEqual({ tagName: '小学生', tagId: 35 });
expect(archiveDataEntry.countLikes).toBeGreaterThan(30);
expect(archiveDataEntry.countDislikes).toBeGreaterThan(5);
expect(archiveDataEntry.postDateGmt).toEqual(new Date('2023-02-11T05:57:26.000Z'));
expect(archiveDataEntry.postContent).not.toBe('');
expect(archiveDataEntry.similarPosts).toHaveLength(15);
expect(archiveDataEntry.previousPost.postTitle).toBe('無知識でアナルにローターを入れるべきでは無い');
expect(archiveDataEntry.nextPost.postTitle).toBe('無能が消去法で大学を決めるべきではない');
})
});

describe("getFeedPostsが正しいデータを返すこと", async () => {
const likeFromHour = 0;
const likeToHour = 24;
const chunkSize = 12;
describe("古い順の場合", async () => {
test("古い順, 1ページ目", async () => {
const oldestPostIds = await getOldestPostIdsForTest(chunkSize);
const pagingNumber = 1;
const type = "timeAsc";
const feedPosts = await getFeedPosts(pagingNumber, type, chunkSize);
expect(feedPosts.meta.totalCount).toBeGreaterThan(9000);
expect(feedPosts.result).toHaveLength(chunkSize);
for (let i = 0; i < chunkSize; i++) {
const post = PostCardDataSchema.parse(feedPosts.result[i]);
expect(post.postId).toBe(oldestPostIds[i]);
}
})
test("古い順, 2ページ目", async () => {
const oldestPostIds = await getOldestPostIdsForTest(chunkSize);
const pagingNumber = 2;
const type = "timeAsc";
const feedPosts = await getFeedPosts(pagingNumber, type, chunkSize);
expect(feedPosts.result).toHaveLength(chunkSize);
for (let i = 0; i < chunkSize; i++) {
const post = PostCardDataSchema.parse(feedPosts.result[i]);
expect(post.postId).toBe(oldestPostIds[i+chunkSize]);
}
})
})

describe("新着順の場合", async () => {
test("新着順, 1ページ目", async () => {
const newestPostIds = await getNewestPostIdsForTest(chunkSize);
const pagingNumber = 1;
const type = "timeDesc";
const feedPosts = await getFeedPosts(pagingNumber, type, chunkSize);
expect(feedPosts.result).toHaveLength(chunkSize);
for (let i = 0; i < chunkSize; i++) {
const post = PostCardDataSchema.parse(feedPosts.result[i]);
expect(post.postId).toBe(newestPostIds[i]);
}
})
test("新着順, 2ページ目", async () => {
const newestPostIds = await getNewestPostIdsForTest(chunkSize);
const pagingNumber = 2;
const type = "timeDesc";
const feedPosts = await getFeedPosts(pagingNumber, type, chunkSize);
expect(feedPosts.result).toHaveLength(chunkSize);
for (let i = 0; i < chunkSize; i++) {
const post = PostCardDataSchema.parse(feedPosts.result[i]);
expect(post.postId).toBe(newestPostIds[i+chunkSize]);
}
})
})
describe("いいね順の場合", async () => {
test("いいね順, 1ページ目", async () => {
const pagingNumber = 1;
const type = "likes";
const feedPosts = await getFeedPosts(pagingNumber, type, chunkSize);
expect(feedPosts.result.length).toBeGreaterThan(0);
// 時間によって違うのでテストが難しい
})
})
describe("無期限いいね順の場合", async () => {
test("無期限いいね順, 1ページ目", async () => {
const unboundedLikesPostIds = await getUnboundedLikesPostIdsForTest(chunkSize);
const pagingNumber = 1;
const type = "unboundedLikes";
const feedPosts = await getFeedPosts(pagingNumber, type, chunkSize);
expect(feedPosts.result).toHaveLength(chunkSize);
for (let i = 0; i < chunkSize; i++) {
const post = PostCardDataSchema.parse(feedPosts.result[i]);
expect(post.postId).toBe(unboundedLikesPostIds[i]);
}
})
test("無期限いいね順, 2ページ目", async () => {
const unboundedLikesPostIds = await getUnboundedLikesPostIdsForTest(chunkSize);
const pagingNumber = 2;
const type = "unboundedLikes";
const feedPosts = await getFeedPosts(pagingNumber, type, chunkSize);
expect(feedPosts.result).toHaveLength(chunkSize);
for (let i = 0; i < chunkSize; i++) {
const post = PostCardDataSchema.parse(feedPosts.result[i]);
expect(post.postId).toBe(unboundedLikesPostIds[i+chunkSize]);
}
})
})
});
2 changes: 1 addition & 1 deletion app/routes/_layout._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export default function Feed() {
<div role="tabpanel" className="tab-content" style={{ display: tab === "trend" ? "block" : "none" }}>
<PostSection title="最近いいねされた投稿" posts={recentVotedPosts} identifier="voted">
<button className="rounded-md block w-full max-w-[400px] px-4 py-2 text-center my-4 bg-base-200 mx-auto hover:bg-base-300" type="button">
<NavLink to="/feed?p=2&likeFrom=24&likeTo=0&type=like" className="block w-full h-full">
<NavLink to="/feed?p=2&likeFrom=48&likeTo=0&type=likes" className="block w-full h-full">
最近いいねされた投稿を見る
</NavLink>
</button>
Expand Down
Loading

1 comment on commit 7f63a9b

@vercel
Copy link

@vercel vercel bot commented on 7f63a9b Aug 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.