From 914d46215fd955bc9045909d6e42bfb2f9a99331 Mon Sep 17 00:00:00 2001 From: Fushihara <1039534+fushihara@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:21:07 +0900 Subject: =?UTF-8?q?=E8=A8=98=E4=BA=8B=E3=83=87=E3=83=BC=E3=82=BF=E3=81=AE?= =?UTF-8?q?=E6=8C=81=E3=81=A1=E6=96=B9=E3=82=92=E4=BF=AE=E6=AD=A3=20?= =?UTF-8?q?=E3=83=93=E3=83=AB=E3=83=89=E6=99=82=E3=81=AB1=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=A5=E3=81=A4=E3=82=A4=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=82=B9=E3=81=8C=E7=A0=B4=E6=A3=84=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=82=8B=E3=81=A8=E6=80=9D=E3=81=84=E3=81=93=E3=82=93?= =?UTF-8?q?=E3=81=A7=E3=81=84=E3=81=A6=E6=AF=8E=E5=9B=9E=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=AE=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=E3=81=8B=E3=82=89=E3=82=84=E3=82=8A=E7=9B=B4=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/article/_components/articleListElement.tsx | 2 +- src/app/article/all/[pageId]/page.tsx | 6 +- src/app/article/tag/[tagName]/page.tsx | 12 +- src/app/article/tag/page.tsx | 38 ++--- src/util/articleLoader.ts | 179 ++++++++++++++------- 5 files changed, 155 insertions(+), 82 deletions(-) diff --git a/src/app/article/_components/articleListElement.tsx b/src/app/article/_components/articleListElement.tsx index 0fe3d70..5437c04 100644 --- a/src/app/article/_components/articleListElement.tsx +++ b/src/app/article/_components/articleListElement.tsx @@ -5,7 +5,7 @@ dateformat.i18n.dayNames = [ '日', '月', '火', '水', '木', '金', '土', '日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日' ]; -type DisplayData = Awaited>[number]; +type DisplayData = Awaited>["articles"][number]; export function ArticleListElement(displayData: DisplayData[]) { return ( diff --git a/src/app/article/all/[pageId]/page.tsx b/src/app/article/all/[pageId]/page.tsx index fda6fbb..e076aa0 100644 --- a/src/app/article/all/[pageId]/page.tsx +++ b/src/app/article/all/[pageId]/page.tsx @@ -22,12 +22,12 @@ export async function generateMetadata(context: PageType) { export default async function Page(context: PageType) { const pageId = getPageIdNumber(context.params.pageId); const loadedData = await ArticleLoader.instance.loadData(); - const chunkdData = chunk(loadedData, PPV); + const chunkdData = chunk(loadedData.articles, PPV); const displayData = chunkdData[pageId - 1]; return (
{pagenationElement(pageId, chunkdData.length)} -
全:{loadedData.length}件
+
全:{loadedData.articles.length}件
{ArticleListElement(displayData)} {pagenationElement(pageId, chunkdData.length)}
@@ -142,7 +142,7 @@ if (!Number.isInteger(PPV)) { //export const dynamicParams = true; export async function generateStaticParams() { const loadedData = await ArticleLoader.instance.loadData(); - const chunkdData = chunk(loadedData, PPV); + const chunkdData = chunk(loadedData.articles, PPV); return chunkdData.map((data, index) => { return { pageId: `page-${index + 1}`, data: data }; }); diff --git a/src/app/article/tag/[tagName]/page.tsx b/src/app/article/tag/[tagName]/page.tsx index 84c1677..991ebfc 100644 --- a/src/app/article/tag/[tagName]/page.tsx +++ b/src/app/article/tag/[tagName]/page.tsx @@ -14,7 +14,7 @@ export async function generateMetadata(context: PageType) { export default async function Page(context: PageType) { const nowPageTagName = decodeURIComponent(context.params.tagName); const loadedData = await ArticleLoader.instance.loadData().then(articles => { - const filterd = articles.filter(article => { + const filterd = articles.articles.filter(article => { if (article.tags.includes(nowPageTagName)) { return true; } @@ -34,8 +34,10 @@ export default async function Page(context: PageType) { ); } export async function generateStaticParams() { - const tagList = await ArticleLoader.instance.getTagList(); - return tagList.map((data, index) => { - return { tagName: data.tag }; - }); + const articleData = await ArticleLoader.instance.loadData(); + const result: { tagName: string }[] = []; + for (const tag of articleData.categoryTag.data.keys()) { + result.push({ tagName: tag }); + } + return result; } diff --git a/src/app/article/tag/page.tsx b/src/app/article/tag/page.tsx index 4f6ae0b..1d6efc3 100644 --- a/src/app/article/tag/page.tsx +++ b/src/app/article/tag/page.tsx @@ -13,7 +13,7 @@ export async function generateMetadata(context: PageType) { } } export default async function Page(context: PageType) { - const tagList = await ArticleLoader.instance.getTagList(); + const articleData = await ArticleLoader.instance.loadData(); type TAG = { tag: string, count: number, primary?: boolean }; const elementListPcPart: TAG[] = []; const elementListAkiba: TAG[] = []; @@ -22,25 +22,25 @@ export default async function Page(context: PageType) { const elementListGame: TAG[] = []; const elementListHobby: TAG[] = []; const elementListOther: TAG[] = []; - for (const tag of tagList) { - if (tag.category == "PCパーツ") { - elementListPcPart.push({ tag: tag.tag, count: tag.count, primary: tag.tag == "PCパーツ" }); - } else if (tag.category == "アキバ") { - elementListAkiba.push({ tag: tag.tag, count: tag.count, primary: tag.tag == "アキバ" }); - } else if (tag.category == "アニメ") { - elementListAnime.push({ tag: tag.tag, count: tag.count, primary: tag.tag == "アニメ" }); - } else if (tag.category == "ゲーム") { - elementListGame.push({ tag: tag.tag, count: tag.count, primary: tag.tag == "ゲーム" }); - } else if (tag.category == "ホビー") { - elementListHobby.push({ tag: tag.tag, count: tag.count, primary: tag.tag == "ホビー" }); - } else if (tag.tag.match(/^\d+(春|夏|秋|冬)?アニメ$/)) { - elementListAnimeSeason.push({ tag: tag.tag, count: tag.count }); - } else if (tag.tag == "G.E.M.シリーズ") { - elementListHobby.push({ tag: tag.tag, count: tag.count }); - } else if (["3DS", "PS4ゲームレビュー", "PS Vita", "PS5ゲームレビュー", "Switchインディーズ", "ポケモンGO", "Steamゲームレビュー"].includes(tag.tag)) { - elementListGame.push({ tag: tag.tag, count: tag.count }); + for (const [tag, categoryAndCount] of articleData.categoryTag.data.entries()) { + if (categoryAndCount.category == "PCパーツ") { + elementListPcPart.push({ tag: tag, count: categoryAndCount.count, primary: tag == "PCパーツ" }); + } else if (categoryAndCount.category == "アキバ") { + elementListAkiba.push({ tag: tag, count: categoryAndCount.count, primary: tag == "アキバ" }); + } else if (categoryAndCount.category == "アニメ") { + elementListAnime.push({ tag: tag, count: categoryAndCount.count, primary: tag == "アニメ" }); + } else if (categoryAndCount.category == "ゲーム") { + elementListGame.push({ tag: tag, count: categoryAndCount.count, primary: tag == "ゲーム" }); + } else if (categoryAndCount.category == "ホビー") { + elementListHobby.push({ tag: tag, count: categoryAndCount.count, primary: tag == "ホビー" }); + } else if (tag.match(/^\d+(春|夏|秋|冬)?アニメ$/)) { + elementListAnimeSeason.push({ tag: tag, count: categoryAndCount.count }); + } else if (tag == "G.E.M.シリーズ") { + elementListHobby.push({ tag: tag, count: categoryAndCount.count }); + } else if (["3DS", "PS4ゲームレビュー", "PS Vita", "PS5ゲームレビュー", "Switchインディーズ", "ポケモンGO", "Steamゲームレビュー"].includes(tag)) { + elementListGame.push({ tag: tag, count: categoryAndCount.count }); } else { - elementListOther.push({ tag: tag.tag, count: tag.count }); + elementListOther.push({ tag: tag, count: categoryAndCount.count }); } } return ( diff --git a/src/util/articleLoader.ts b/src/util/articleLoader.ts index 860593d..1ae0448 100644 --- a/src/util/articleLoader.ts +++ b/src/util/articleLoader.ts @@ -14,7 +14,10 @@ const MAX_ITEM_LIMIT = process.env["AKIBA_SOUKEN_MAX_ITEM_LIMIT"]; export class ArticleLoader { static instance = new ArticleLoader(); private constructor() { } - private dataCache: Awaited> | null = null; + private dataCache: { + articles: Article[], + categoryTag: CategoryTag, + } | null = null; async loadData() { if (this.dataCache != null) { return this.dataCache; @@ -29,82 +32,150 @@ export class ArticleLoader { const jsonObj = JSON.parse(text); return jsonObj; }); - const parsedObj = zodType.parse(jsonStr); + const parsedObj = zodType.parse(jsonStr).map(v => new Article(v)); parsedObj.sort((a, b) => { return b.timestampMs - a.timestampMs; }); if (MAX_ITEM_LIMIT != null) { parsedObj.length = Math.min(Number(MAX_ITEM_LIMIT), parsedObj.length); } - return parsedObj; - } - async getCategoryList() { - const loadedData = await this.loadData(); - // key:カテゴリ名 , val:回数 - const categoryCount = new Map(); - for (const a of loadedData) { - if (a.breadLinks.length == 0) { - continue; + const categoryTagData = new CategoryTag(); + parsedObj.forEach(v => { + for (const tag of v.tags2) { + categoryTagData.put(tag.category, tag.name); } - const category = a.breadLinks[0]; - if (categoryCount.has(category)) { - categoryCount.set(category, categoryCount.get(category)! + 1); - } else { - categoryCount.set(category, 1); - } - } - const categoryList: { name: string, count: number }[] = []; - for (const [name, count] of categoryCount) { - categoryList.push({ name: name, count }); - } - categoryList.sort((a, b) => { - return b.count - a.count; }); - return categoryList; + return { + articles: parsedObj, + categoryTag: categoryTagData, + }; } /** * * @returns [{tag:"タグ名",category:string,count:100}] の値。カテゴリに属していないタグはカテゴリが空文字。 * カテゴリ自体を指す場合は {tag:"ホビー",category:"ホビー",count:100} の様に同じ値が入る事がある。 * ソート順は未定義 + * @deprecated + */ + // async getTagList() { + // const loadedData = await this.loadData(); + // // 先にパンくずリストを全部見る + // const breadcrumb = new Breadcrumb(); + // for (const a of loadedData.articles) { + // breadcrumb.push(a.breadLinks); + // } + // // key:タグ名 , val:回数 + // const tagCount = new Map(); + // // パンくずリストに含まれないタグを調査 + // for (const a of loadedData.articles) { + // for (const tag of a.tags) { + // if (breadcrumb.strExists(tag)) { + // continue; + // } + // const tagData = tagCount.get(tag); + // if (tagData != null) { + // tagCount.set(tag, tagData + 1); + // } else { + // tagCount.set(tag, 1); + // } + // } + // } + // const result: { tag: string, category: string, count: number }[] = []; + // for (const [name, count] of tagCount) { + // result.push({ tag: name, count: count, category: "" }); + // } + // for (const b of breadcrumb.getFlatArray()) { + // const breadcrumbName = b.breadcrumb[b.breadcrumb.length - 1];//パンくずリストの最後の項目 + // const category = b.breadcrumb[0];//カテゴリ名 + // result.push({ tag: breadcrumbName, count: b.count, category: category }); + // } + // return result; + // } +} +type TagName = string; +class CategoryTag { + /** + * key:タグ名 + * val:カテゴリ名 */ - async getTagList() { - const loadedData = await this.loadData(); - // 先にパンくずリストを全部見る - const breadcrumb = new Breadcrumb(); - for (const a of loadedData) { - breadcrumb.push(a.breadLinks); + readonly data = new Map(); + put(category: string, tag: string) { + if (tag == "") { + throw new Error(`タグが空文字です`) } - // key:タグ名 , val:回数 - const tagCount = new Map(); - // パンくずリストに含まれないタグを調査 - for (const a of loadedData) { - for (const tag of a.tags) { - if (breadcrumb.strExists(tag)) { - continue; - } - const tagData = tagCount.get(tag); - if (tagData != null) { - tagCount.set(tag, tagData + 1); - } else { - tagCount.set(tag, 1); - } + const storeCategory = this.data.get(tag); + if (storeCategory == null) { + this.data.set(tag, { category: category, count: 1 }); + } else if (storeCategory.category != category) { + if (category == "" || storeCategory.category == "") { + storeCategory.count += 1; + } else if (category != "" || storeCategory.category == "") { + storeCategory.count += 1; + storeCategory.category = category; + } else if (category == "" || storeCategory.category != "") { + storeCategory.count += 1; + //} else if(category != "" || storeCategory.category != ""){ + } else { + throw new Error(`カテゴリが不一致. tag:${tag} , storeCategory:${storeCategory.category} , newCategory:${category}`); } + } else { + storeCategory.count += 1; } - const result: { tag: string, category: string, count: number }[] = []; - for (const [name, count] of tagCount) { - result.push({ tag: name, count: count, category: "" }); + } +} +class Article { + constructor( + private readonly data: z.infer[number] + ) { + this.validate(); + } + get timestampMs() { + return this.data.timestampMs; + } + get breadLinks() { + return this.data.breadLinks; + } + get tags() { + return this.data.tags; + } + get maxPageNumber() { + return this.data.maxPageNumber; + } + get articleId() { + return this.data.articleId; + } + get title() { + return this.data.title; + } + get tags2() { + const result = this.getTags(); + return result; + } + private tagsCache: { name: string, category: string | "" }[] | null = null; + private getTags() { + if (this.tagsCache != null) { + return this.tagsCache; } - for (const b of breadcrumb.getFlatArray()) { - const breadcrumbName = b.breadcrumb[b.breadcrumb.length - 1];//パンくずリストの最後の項目 - const category = b.breadcrumb[0];//カテゴリ名 - result.push({ tag: breadcrumbName, count: b.count, category: category }); + const tags: { name: string, category: string | "" }[] = []; + for (const v of this.data.tags) { + tags.push({ name: v, category: "" }) + }; + const category = this.data.breadLinks[0]; + for (const v of this.data.breadLinks) { + tags.push({ name: v, category: category }); + }; + this.tagsCache = tags; + return tags; + } + private validate() { + // パンくずリストは必ず1件以上あること + if (1 <= this.breadLinks.length) { + } else { + throw new Error(`パンくずリストの個数が不足しています`) } - return result; } } - type BreadcrumbInternal = { name: string, count: number, child: BreadcrumbInternal[] }; /** * パンくずリストを管理 -- cgit v1.2.3