From c1cc8ef47e4a405697cd9855667e3df5593bd557 Mon Sep 17 00:00:00 2001 From: Fushihara <1039534+fushihara@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:26:42 +0900 Subject: =?UTF-8?q?=E3=82=BF=E3=82=B0=E4=B8=80=E8=A6=A7=E3=81=AE=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=81=AE=E4=BB=95=E7=B5=84=E3=81=BF=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/anime/[animeId]/page.tsx | 2 +- src/app/article/all/[pageId]/page.tsx | 3 - src/app/article/tag/[tagName]/page.tsx | 4 +- src/app/article/tag/page.tsx | 70 ++++++++++++++--- src/util/articleLoader.ts | 132 ++++++++++++++++++++++++++++----- 5 files changed, 176 insertions(+), 35 deletions(-) diff --git a/src/app/anime/[animeId]/page.tsx b/src/app/anime/[animeId]/page.tsx index 968ce20..0f96f30 100644 --- a/src/app/anime/[animeId]/page.tsx +++ b/src/app/anime/[animeId]/page.tsx @@ -228,4 +228,4 @@ export async function generateStaticParams() { return loadedData.map(c => { return { animeId: String(c.animeId) }; }) satisfies PageType["params"][]; -} \ No newline at end of file +} diff --git a/src/app/article/all/[pageId]/page.tsx b/src/app/article/all/[pageId]/page.tsx index fd72772..0374401 100644 --- a/src/app/article/all/[pageId]/page.tsx +++ b/src/app/article/all/[pageId]/page.tsx @@ -149,9 +149,6 @@ export async function generateStaticParams() { return { pageId: `page-${index + 1}`, data: data }; }); } -// export const generateStaticParams = async () => { -// return [{ articleid: "123" }]; -// }; function chunk(list: T[], len: number) { if (len <= 0) { diff --git a/src/app/article/tag/[tagName]/page.tsx b/src/app/article/tag/[tagName]/page.tsx index 3df1f1e..b783e82 100644 --- a/src/app/article/tag/[tagName]/page.tsx +++ b/src/app/article/tag/[tagName]/page.tsx @@ -8,7 +8,7 @@ type PageType = { } export async function generateMetadata(context: PageType) { return { - title: `アキバ総研アーカイブ:ページ ${context.params.tagName}`, + title: `アキバ総研アーカイブ:タグの記事一覧 ${context.params.tagName}`, } } export default async function Page(context: PageType) { @@ -35,6 +35,6 @@ export default async function Page(context: PageType) { export async function generateStaticParams() { const tagList = await new ArticleLoader().getTagList(); return tagList.map((data, index) => { - return { tagName: data.name }; + return { tagName: encodeURIComponent(data.tag) }; }); } diff --git a/src/app/article/tag/page.tsx b/src/app/article/tag/page.tsx index 211ca94..f1d161b 100644 --- a/src/app/article/tag/page.tsx +++ b/src/app/article/tag/page.tsx @@ -14,17 +14,67 @@ export async function generateMetadata(context: PageType) { } export default async function Page(context: PageType) { const tagList = await new ArticleLoader().getTagList(); - const tagsElement: JSX.Element[] = []; - tagList.forEach(t => { - tagsElement.push({t.name}({t.count})) - }) + type TAG = { tag: string, count: number, primary?: boolean }; + const elementListPcPart: TAG[] = []; + const elementListAkiba: TAG[] = []; + const elementListAnime: TAG[] = []; + const elementListAnimeSeason: TAG[] = []; + 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 }); + } else { + elementListOther.push({ tag: tag.tag, count: tag.count }); + } + } return ( -
-

著名なタグ一覧

-

記事にセットされているタグの一覧

-
- {tagsElement} -
+
+ {createList("PCパーツ", elementListPcPart)} + {createList("アキバ", elementListAkiba)} + {createList("ゲーム", elementListGame)} + {createList("ホビー", elementListHobby)} + {createList("カテゴリなし", elementListOther)} + {createList("アニメ", elementListAnime)} + {createList("アニメ(時期別)", elementListAnimeSeason)}
); } +function createList(headerLabel: string, tagList: { tag: string, count: number, primary?: boolean }[]) { + const sortedList = tagList.toSorted((a, b) => { + if (a.primary == true) { + return -1; + } else if (b.primary == true) { + return 1; + } else { + return b.count - a.count; + }; + }) + const elementList: JSX.Element[] = []; + sortedList.forEach(t => { + elementList.push( + {t.tag}({t.count}) + ) + }) + return (<> +

{headerLabel}

+
+ {elementList} +
+ ); +} \ No newline at end of file diff --git a/src/util/articleLoader.ts b/src/util/articleLoader.ts index 787725a..c957e3a 100644 --- a/src/util/articleLoader.ts +++ b/src/util/articleLoader.ts @@ -15,7 +15,6 @@ export class ArticleLoader { constructor() { } async loadData() { const articleJsonPath = process.env["AKIBA_SOUKEN_ARTICLE_JSON"]!; - //console.log(`[${articleJsonPath}]`); const jsonStr = await readFile(articleJsonPath, { encoding: "utf-8" }).then(text => { const jsonObj = JSON.parse(text); return jsonObj; @@ -29,6 +28,7 @@ export class ArticleLoader { } return parsedObj; } + async getCategoryList() { const loadedData = await this.loadData(); // key:カテゴリ名 , val:回数 @@ -53,36 +53,130 @@ export class ArticleLoader { }); return categoryList; } + /** + * + * @returns [{tag:"タグ名",category:string,count:100}] の値。カテゴリに属していないタグはカテゴリが空文字。 + * カテゴリ自体を指す場合は {tag:"ホビー",category:"ホビー",count:100} の様に同じ値が入る事がある。 + * ソート順は未定義 + */ async getTagList() { const loadedData = await this.loadData(); + // 先にパンくずリストを全部見る + const breadcrumb = new Breadcrumb(); + for (const a of loadedData) { + breadcrumb.push(a.breadLinks); + } // key:タグ名 , val:回数 const tagCount = new Map(); + // パンくずリストに含まれないタグを調査 for (const a of loadedData) { - const tags = new Set(); - a.tags.forEach(t => { - tags.add(t); - }); - // パンくずリストの2件目以降はタグ扱いになっている - a.breadLinks.forEach((b, index) => { - if (index != 0) { - tags.add(b); + for (const tag of a.tags) { + if (breadcrumb.strExists(tag)) { + continue; } - }) - for (const tag of tags) { - if (tagCount.has(tag)) { - tagCount.set(tag, tagCount.get(tag)! + 1); + const tagData = tagCount.get(tag); + if (tagData != null) { + tagCount.set(tag, tagData + 1); } else { tagCount.set(tag, 1); } } } - const tagList: { name: string, count: number }[] = []; + const result: { tag: string, category: string, count: number }[] = []; for (const [name, count] of tagCount) { - tagList.push({ name: name, count }); + result.push({ tag: name, count: count, category: "" }); } - tagList.sort((a, b) => { - return b.count - a.count; - }); - return tagList; + 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 BreadcrumbInternal = { name: string, count: number, child: BreadcrumbInternal[] }; +/** + * パンくずリストを管理 + * ・各記事には最低2個以上のパンくずリストがある + * ・ホビー>バンダイ>S.H.Figuarts の様に親子関係がある + * ・文字は一箇所でしか使われない。例えば、バンダイという文字はホビーの下にしか存在しない。 + */ +class Breadcrumb { + private set = new Set(); + private data: BreadcrumbInternal[] = []; + strExists(str: string) { + return this.set.has(str); + } + push(breadLinks: string[]) { + if (breadLinks.length == 0) { + throw new Error(`配列が0個です`); + } + breadLinks.forEach(b => { + this.set.add(b); + }) + this.setChild( + this.data, + breadLinks, + ); + } + getFlatArray() { + function hoge(dataList: BreadcrumbInternal[], parentBreadcrumbList: string[]) { + const sortedDataList = [...dataList].toSorted((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + const result: { breadcrumb: string[], count: number }[] = []; + for (const v of sortedDataList) { + if (v.child.length == 0) { + if (v.count == 0) { + throw new Error(`子が0個で、個数も0はありえない`); + } else { + result.push({ breadcrumb: [...parentBreadcrumbList, v.name], count: v.count }); + } + } else { + if (v.count != 0) { + result.push({ breadcrumb: [...parentBreadcrumbList, v.name], count: v.count }); + } + const childResult = hoge(v.child, [...parentBreadcrumbList, v.name]); + childResult.forEach(c => { + result.push(c); + }) + } + } + return result; + } + const result = hoge(this.data, []); + return result; + } + + private setChild(pushTarget: BreadcrumbInternal[], breadLinks: string[]) { + const [top, ...nextBreadLinks] = breadLinks; + const isLast = breadLinks.length == 1; + const data = pushTarget.find(d => d.name == top); + if (data) { + if (isLast) { + data.count += 1; + } else { + this.setChild(data.child, nextBreadLinks) + } + } else { + if (isLast) { + pushTarget.push({ name: top, child: [], count: 1 }); + } else { + const newItem: BreadcrumbInternal = this.createChild(nextBreadLinks) + pushTarget.push({ name: top, child: [newItem], count: 0 }); + } + } + } + private createChild(nextBreadLinks: string[]): BreadcrumbInternal { + if (nextBreadLinks.length == 0) { + throw new Error(); + } else if (nextBreadLinks.length == 1) { + return { child: [], count: 1, name: nextBreadLinks[0] } satisfies BreadcrumbInternal; + } else { + const [nowBreadLink, ...nextNextBreadLinks] = nextBreadLinks; + const child = this.createChild(nextNextBreadLinks); + return { child: [child], count: 0, name: nowBreadLink } satisfies BreadcrumbInternal; + } + } +} \ No newline at end of file -- cgit v1.2.3