From 5cc47e42e11fdfe50a669c184c7f32a7db9f33f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Wed, 26 Nov 2025 22:33:50 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E3=82=BF=E3=82=B0=E5=B8=8C?= =?UTF-8?q?=E6=9C=9B=E3=82=BF=E3=82=B0=E3=82=92=E6=96=B0=E8=A6=8F=E6=99=82?= =?UTF-8?q?=E3=81=AE=E3=81=BF=E3=81=AB=E3=81=99=E3=82=8B=EF=BC=88#128?= =?UTF-8?q?=EF=BC=89=20(#145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #128 完了 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/145 --- backend/app/controllers/posts_controller.rb | 3 ++- backend/app/models/tag.rb | 2 +- backend/lib/tasks/sync_nico.rake | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 6877c70..b6e7d5d 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -110,7 +110,8 @@ class PostsController < ApplicationController original_created_before = params[:original_created_before] post = Post.find(params[:id].to_i) - tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names) + tags = post.tags.where(category: 'nico').to_a + + Tag.normalise_tags(tag_names, with_tagme: false) if post.update(title:, tags:, original_created_from:, original_created_before:) render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), status: :ok diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index 137afff..e80baff 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -57,7 +57,7 @@ class Tag < ApplicationRecord end end end - tags << Tag.tagme if with_tagme && tags.size < 20 && tags.none?(Tag.tagme) + tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme) tags.uniq end diff --git a/backend/lib/tasks/sync_nico.rake b/backend/lib/tasks/sync_nico.rake index a8601c5..9fe28fb 100644 --- a/backend/lib/tasks/sync_nico.rake +++ b/backend/lib/tasks/sync_nico.rake @@ -51,7 +51,7 @@ namespace :nico do end tags_to_add.concat([tag] + tag.linked_tags) end - tags_to_add << Tag.tagme if post.tags.size < 20 + tags_to_add << Tag.tagme if post.tags.size < 10 tags_to_add << Tag.bot post.tags = (post.tags + tags_to_add).uniq end From 8a126e2e92037fcb6575f6934df44629784c2c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Wed, 26 Nov 2025 23:48:45 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6?= =?UTF-8?q?=E3=81=AB=20IP=20=E3=82=A2=E3=83=89=E3=83=AC=E3=82=B9=E3=82=92?= =?UTF-8?q?=E7=B4=90=E3=81=A5=E3=81=91=EF=BC=88#29=EF=BC=89=20(#165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #29 対応完了 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/165 --- backend/app/controllers/users_controller.rb | 13 ++++++----- backend/app/models/user.rb | 1 - ...rename_ip_adress_column_to_ip_addresses.rb | 5 +++++ backend/db/schema.rb | 22 +++++++++++++++++-- 4 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 backend/db/migrate/20251126231500_rename_ip_adress_column_to_ip_addresses.rb diff --git a/backend/app/controllers/users_controller.rb b/backend/app/controllers/users_controller.rb index 8658a5f..4ee4836 100644 --- a/backend/app/controllers/users_controller.rb +++ b/backend/app/controllers/users_controller.rb @@ -6,12 +6,15 @@ class UsersController < ApplicationController end def verify + ip_bin = IPAddr.new(request.remote_ip).hton + ip_address = IpAddress.find_or_create_by!(ip_address: ip_bin) + user = User.find_by(inheritance_code: params[:code]) - render json: if user - { valid: true, user: user.slice(:id, :name, :inheritance_code, :role) } - else - { valid: false } - end + return render json: { valid: false } unless user + + UserIp.find_or_create_by!(user:, ip_address:) + + render json: { valid: true, user: user.slice(:id, :name, :inheritance_code, :role) } end def renew diff --git a/backend/app/models/user.rb b/backend/app/models/user.rb index 830d383..ede464a 100644 --- a/backend/app/models/user.rb +++ b/backend/app/models/user.rb @@ -8,7 +8,6 @@ class User < ApplicationRecord has_many :posts has_many :settings - has_many :ip_addresses has_many :user_ips, dependent: :destroy has_many :ip_addresses, through: :user_ips has_many :user_post_views, dependent: :destroy diff --git a/backend/db/migrate/20251126231500_rename_ip_adress_column_to_ip_addresses.rb b/backend/db/migrate/20251126231500_rename_ip_adress_column_to_ip_addresses.rb new file mode 100644 index 0000000..60f78e6 --- /dev/null +++ b/backend/db/migrate/20251126231500_rename_ip_adress_column_to_ip_addresses.rb @@ -0,0 +1,5 @@ +class RenameIpAdressColumnToIpAddresses < ActiveRecord::Migration[8.0] + def change + rename_column :ip_addresses, :ip_adress, :ip_address + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 4d5cfd6..b8210d8 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do +ActiveRecord::Schema[8.0].define(version: 2025_11_26_231500) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -40,7 +40,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do end create_table "ip_addresses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.binary "ip_adress", limit: 16, null: false + t.binary "ip_address", limit: 16, null: false t.boolean "banned", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -70,9 +70,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do t.bigint "deleted_user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "discarded_at" + t.virtual "is_active", type: :boolean, as: "(`discarded_at` is null)", stored: true + t.virtual "active_unique_key", type: :string, as: "(case when (`discarded_at` is null) then concat(`post_id`,_utf8mb4':',`tag_id`) else NULL end)", stored: true + t.index ["active_unique_key"], name: "idx_post_tags_active_unique", unique: true t.index ["created_user_id"], name: "index_post_tags_on_created_user_id" t.index ["deleted_user_id"], name: "index_post_tags_on_deleted_user_id" + t.index ["discarded_at"], name: "index_post_tags_on_discarded_at" + t.index ["post_id", "discarded_at"], name: "index_post_tags_on_post_id_and_discarded_at" t.index ["post_id"], name: "index_post_tags_on_post_id" + t.index ["tag_id", "discarded_at"], name: "index_post_tags_on_tag_id_and_discarded_at" t.index ["tag_id"], name: "index_post_tags_on_tag_id" end @@ -107,6 +114,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do t.index ["tag_id"], name: "index_tag_aliases_on_tag_id" end + create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "tag_id", null: false + t.bigint "parent_tag_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["parent_tag_id"], name: "index_tag_implications_on_parent_tag_id" + t.index ["tag_id"], name: "index_tag_implications_on_tag_id" + end + create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "tag_id", null: false t.bigint "target_tag_id", null: false @@ -176,6 +192,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do add_foreign_key "posts", "users", column: "uploaded_user_id" add_foreign_key "settings", "users" add_foreign_key "tag_aliases", "tags" + add_foreign_key "tag_implications", "tags" + add_foreign_key "tag_implications", "tags", column: "parent_tag_id" add_foreign_key "tag_similarities", "tags" add_foreign_key "tag_similarities", "tags", column: "target_tag_id" add_foreign_key "user_ips", "ip_addresses" From 06cd569fc58cebda10b2649f942e53b85cbe055d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sun, 7 Dec 2025 12:28:43 +0900 Subject: [PATCH 03/12] =?UTF-8?q?=E3=83=8B=E3=82=B3=E3=82=BF=E3=82=B0?= =?UTF-8?q?=E4=B8=80=E6=8B=AC=E9=80=A3=E6=90=BA=E3=81=AE=E5=89=8A=E9=99=A4?= =?UTF-8?q?=EF=BC=88#166=EF=BC=89=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'backend/lib/tasks/link_nico.rake' を削除 Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/167 --- backend/lib/tasks/link_nico.rake | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 backend/lib/tasks/link_nico.rake diff --git a/backend/lib/tasks/link_nico.rake b/backend/lib/tasks/link_nico.rake deleted file mode 100644 index 0c02f48..0000000 --- a/backend/lib/tasks/link_nico.rake +++ /dev/null @@ -1,12 +0,0 @@ -namespace :nico do - desc 'ニコタグ連携' - task link: :environment do - Post.find_each do |post| - tags = post.tags.where(category: 'nico') - tags.each do |tag| - post.tags.concat(tag.linked_tags) if tag.linked_tags.present? - end - post.tags = post.tags.to_a.uniq - end - end -end From d50a302d26e80f7ba06ce5e7366baf11e7b46451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Tue, 9 Dec 2025 12:34:26 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BD=8D=E3=82=BF?= =?UTF-8?q?=E3=82=B0=EF=BC=88#64=EF=BC=89=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #64 おそらく完成 Merge remote-tracking branch 'origin/main' into feature/064 #64 バックエンドぼちぼち Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/173 --- backend/app/controllers/posts_controller.rb | 58 +++++++++++++++++-- backend/app/models/tag.rb | 31 ++++++++++ backend/app/models/tag_implication.rb | 17 ++++++ .../20251009222200_create_tag_implications.rb | 9 +++ backend/db/schema.rb | 2 +- frontend/src/components/PostEditForm.tsx | 29 ++++++++-- frontend/src/components/TagDetailSidebar.tsx | 25 ++++++-- frontend/src/components/TagLink.tsx | 8 +++ frontend/src/types.ts | 20 +++---- 9 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 backend/app/models/tag_implication.rb create mode 100644 backend/db/migrate/20251009222200_create_tag_implications.rb diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index b6e7d5d..81a1c0e 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -52,9 +52,12 @@ class PostsController < ApplicationController viewed = current_user&.viewed?(post) || false - render json: (post - .as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }) - .merge(related: post.related(limit: 20), viewed:)) + json = post.as_json + json['tags'] = build_tag_tree_for(post.tags) + json['related'] = post.related(limit: 20) + json['viewed'] = viewed + + render json: end # POST /posts @@ -78,6 +81,7 @@ class PostsController < ApplicationController if post.save post.resized_thumbnail! post.tags = Tag.normalise_tags(tag_names) + post.tags = Tag.expand_parent_tags(post.tags) render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), status: :created else @@ -112,9 +116,11 @@ class PostsController < ApplicationController post = Post.find(params[:id].to_i) tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names, with_tagme: false) + tags = Tag.expand_parent_tags(tags) if post.update(title:, tags:, original_created_from:, original_created_before:) - render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), - status: :ok + json = post.as_json + json['tags'] = build_tag_tree_for(post.tags) + render json:, status: :ok else render json: post.errors, status: :unprocessable_entity end @@ -143,4 +149,46 @@ class PostsController < ApplicationController end posts.distinct end + + def build_tag_tree_for tags + tags = tags.to_a + tag_ids = tags.map(&:id) + + implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids) + + children_ids_by_parent = Hash.new { |h, k| h[k] = [] } + implications.each do |imp| + children_ids_by_parent[imp.parent_tag_id] << imp.tag_id + end + + child_ids = children_ids_by_parent.values.flatten.uniq + + root_ids = tag_ids - child_ids + + tags_by_id = tags.index_by(&:id) + + memo = { } + + build_node = -> tag_id, path do + tag = tags_by_id[tag_id] + return nil unless tag + + if path.include?(tag_id) + return tag.as_json(only: [:id, :name, :category, :post_count]).merge(children: []) + end + + if memo.key?(tag_id) + return memo[tag_id] + end + + new_path = path + [tag_id] + child_ids = children_ids_by_parent[tag_id] || [] + + children = child_ids.filter_map { |cid| build_node.(cid, new_path) } + + memo[tag_id] = tag.as_json(only: [:id, :name, :category, :post_count]).merge(children:) + end + + root_ids.filter_map { |id| build_node.call(id, []) } + end end diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index e80baff..8a505e2 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -11,6 +11,14 @@ class Tag < ApplicationRecord dependent: :destroy has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag + has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy + has_many :children, through: :tag_implications, source: :tag + + has_many :reversed_tag_implications, class_name: 'TagImplication', + foreign_key: :tag_id, + dependent: :destroy + has_many :parents, through: :reversed_tag_implications, source: :parent_tag + enum :category, { deerjikist: 'deerjikist', meme: 'meme', character: 'character', @@ -61,6 +69,29 @@ class Tag < ApplicationRecord tags.uniq end + def self.expand_parent_tags tags + return [] if tags.blank? + + seen = Set.new + result = [] + stack = tags.compact.dup + + until stack.empty? + tag = stack.pop + next unless tag + + tag.parents.each do |parent| + next if seen.include?(parent.id) + + seen << parent.id + result << parent + stack << parent + end + end + + (result + tags).uniq { |t| t.id } + end + private def nico_tag_name_must_start_with_nico diff --git a/backend/app/models/tag_implication.rb b/backend/app/models/tag_implication.rb new file mode 100644 index 0000000..68f4083 --- /dev/null +++ b/backend/app/models/tag_implication.rb @@ -0,0 +1,17 @@ +class TagImplication < ApplicationRecord + belongs_to :tag, class_name: 'Tag' + belongs_to :parent_tag, class_name: 'Tag' + + validates :tag_id, presence: true + validates :parent_tag_id, presence: true + + validate :parent_tag_mustnt_be_itself + + private + + def parent_tag_mustnt_be_itself + if parent_tag == tag + errors.add :parent_tag_id, '親タグは子タグと同一であってはなりません.' + end + end +end diff --git a/backend/db/migrate/20251009222200_create_tag_implications.rb b/backend/db/migrate/20251009222200_create_tag_implications.rb new file mode 100644 index 0000000..ea8df1a --- /dev/null +++ b/backend/db/migrate/20251009222200_create_tag_implications.rb @@ -0,0 +1,9 @@ +class CreateTagImplications < ActiveRecord::Migration[8.0] + def change + create_table :tag_implications do |t| + t.references :tag, null: false, foreign_key: { to_table: :tags } + t.references :parent_tag, null: false, foreign_key: { to_table: :tags } + t.timestamps + end + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index b8210d8..19ec4f6 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_11_26_231500) do +ActiveRecord::Schema[8.0].define(version: 2025_10_09_222200) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false diff --git a/frontend/src/components/PostEditForm.tsx b/frontend/src/components/PostEditForm.tsx index 60a4318..38b03b4 100644 --- a/frontend/src/components/PostEditForm.tsx +++ b/frontend/src/components/PostEditForm.tsx @@ -1,6 +1,6 @@ import axios from 'axios' import toCamel from 'camelcase-keys' -import { useState } from 'react' +import { useEffect, useState } from 'react' import PostFormTagsArea from '@/components/PostFormTagsArea' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' @@ -10,7 +10,23 @@ import { API_BASE_URL } from '@/config' import type { FC } from 'react' -import type { Post } from '@/types' +import type { Post, Tag } from '@/types' + + +const tagsToStr = (tags: Tag[]): string => { + const result: Tag[] = [] + + const walk = (tag: Tag) => { + const { children, ...rest } = tag + result.push (rest) + children?.forEach (walk) + } + + tags.filter (t => t.category !== 'nico').forEach (walk) + + return [...(new Set (result.map (t => t.name)))].join (' ') +} + type Props = { post: Post onSave: (newPost: Post) => void } @@ -22,10 +38,7 @@ export default (({ post, onSave }: Props) => { const [originalCreatedFrom, setOriginalCreatedFrom] = useState (post.originalCreatedFrom) const [title, setTitle] = useState (post.title) - const [tags, setTags] = useState (post.tags - .filter (t => t.category !== 'nico') - .map (t => t.name) - .join (' ')) + const [tags, setTags] = useState ('') const handleSubmit = async () => { const res = await axios.put ( @@ -43,6 +56,10 @@ export default (({ post, onSave }: Props) => { originalCreatedBefore: data.originalCreatedBefore } as Post) } + useEffect (() => { + setTags(tagsToStr (post.tags)) + }, [post]) + return (
{/* タイトル */} diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 6e2e2dd..fcbfa97 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -7,12 +7,30 @@ import SubsectionTitle from '@/components/common/SubsectionTitle' import SidebarComponent from '@/components/layout/SidebarComponent' import { CATEGORIES } from '@/consts' -import type { FC } from 'react' +import type { FC, ReactNode } from 'react' import type { Category, Post, Tag } from '@/types' type TagByCategory = { [key in Category]: Tag[] } + +const renderTagTree = ( + tag: Tag, + nestLevel: number, + path: string, + ): ReactNode[] => { + const key = `${ path }-${ tag.id }` + + const self = ( +
  • + +
  • ) + + return [self, + ...(tag.children?.flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])] +} + + type Props = { post: Post | null } @@ -54,10 +72,7 @@ export default (({ post }: Props) => {
    {categoryNames[cat]}
      - {tags[cat].map (tag => ( -
    • - -
    • ))} + {tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))}
    ))} {post && ( diff --git a/frontend/src/components/TagLink.tsx b/frontend/src/components/TagLink.tsx index 0734d4d..a77ca5f 100644 --- a/frontend/src/components/TagLink.tsx +++ b/frontend/src/components/TagLink.tsx @@ -8,6 +8,7 @@ import type { ComponentProps, FC, HTMLAttributes } from 'react' import type { Tag } from '@/types' type CommonProps = { tag: Tag + nestLevel?: number withWiki?: boolean withCount?: boolean } @@ -21,6 +22,7 @@ type Props = PropsWithLink | PropsWithoutLink export default (({ tag, + nestLevel = 0, linkFlg = true, withWiki = true, withCount = true, @@ -42,6 +44,12 @@ export default (({ tag, ? )} + {nestLevel > 0 && ( + + ↳ + )} {linkFlg ? ( Date: Wed, 10 Dec 2025 20:18:48 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BD=8D=E3=82=BF?= =?UTF-8?q?=E3=82=B0=E3=81=AE=E3=83=90=E3=82=B0=E4=BF=AE=E6=AD=A3=EF=BC=88?= =?UTF-8?q?#174=EF=BC=89=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #174 エラー回避と誤記修正 #174 完了 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/175 --- backend/app/models/tag_implication.rb | 2 +- ...00_add_unique_index_to_tag_implications.rb | 27 +++++++++++++++++++ backend/db/schema.rb | 3 ++- frontend/src/components/TagDetailSidebar.tsx | 4 ++- 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 backend/db/migrate/20251210123200_add_unique_index_to_tag_implications.rb diff --git a/backend/app/models/tag_implication.rb b/backend/app/models/tag_implication.rb index 68f4083..a629764 100644 --- a/backend/app/models/tag_implication.rb +++ b/backend/app/models/tag_implication.rb @@ -2,7 +2,7 @@ class TagImplication < ApplicationRecord belongs_to :tag, class_name: 'Tag' belongs_to :parent_tag, class_name: 'Tag' - validates :tag_id, presence: true + validates :tag_id, presence: true, uniqueness: { scope: :parent_tag_id } validates :parent_tag_id, presence: true validate :parent_tag_mustnt_be_itself diff --git a/backend/db/migrate/20251210123200_add_unique_index_to_tag_implications.rb b/backend/db/migrate/20251210123200_add_unique_index_to_tag_implications.rb new file mode 100644 index 0000000..681c1b5 --- /dev/null +++ b/backend/db/migrate/20251210123200_add_unique_index_to_tag_implications.rb @@ -0,0 +1,27 @@ +class AddUniqueIndexToTagImplications < ActiveRecord::Migration[8.0] + def up + execute <<~SQL + DELETE + ti1 + FROM + tag_implications ti1 + INNER JOIN + tag_implications ti2 + ON + ti1.tag_id = ti2.tag_id + AND ti1.parent_tag_id = ti2.parent_tag_id + AND ti1.id > ti2.id + ; + SQL + + add_index :tag_implications, [:tag_id, :parent_tag_id], + unique: true, + name: 'index_tag_implications_on_tag_id_and_parent_tag_id' + end + + def down + # NOTE: 重複削除は復元されなぃ. + remove_index :tag_implications, + name: 'index_tag_implications_on_tag_id_and_parent_tag_id' + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 19ec4f6..6a26dfd 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_10_09_222200) do +ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -120,6 +120,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_09_222200) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["parent_tag_id"], name: "index_tag_implications_on_parent_tag_id" + t.index ["tag_id", "parent_tag_id"], name: "index_tag_implications_on_tag_id_and_parent_tag_id", unique: true t.index ["tag_id"], name: "index_tag_implications_on_tag_id" end diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index fcbfa97..da2c3b8 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -27,7 +27,9 @@ const renderTagTree = ( ) return [self, - ...(tag.children?.flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])] + ...(tag.children + ?.sort ((a, b) => a.name < b.name ? -1 : 1) + .flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])] } From 9a656a9e6ee17d37067f1f6f3f12f417595cdd50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sat, 13 Dec 2025 16:34:45 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E3=81=A8?= =?UTF-8?q?=E3=82=BF=E3=82=B0=E3=81=AE=E3=83=AA=E3=83=AC=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=83=BB=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB?= =?UTF-8?q?=E3=81=AB=E3=81=A4=E3=81=84=E3=81=A6=E8=AB=96=E7=90=86=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E3=81=A8=E5=B1=A5=E6=AD=B4=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=EF=BC=88#84=EF=BC=89=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #84 マイグレ修正 Merge remote-tracking branch 'origin/main' into feature/084 #84 構文エラー修正 #84 #84 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/148 --- backend/Gemfile | 2 + backend/Gemfile.lock | 3 + backend/app/controllers/posts_controller.rb | 48 ++++++--- backend/app/models/post.rb | 12 ++- backend/app/models/post_tag.rb | 18 ++++ backend/app/models/tag.rb | 10 +- ...20251011200300_add_discard_to_post_tags.rb | 36 +++++++ backend/lib/tasks/sync_nico.rake | 98 ++++++++++++------- 8 files changed, 172 insertions(+), 55 deletions(-) create mode 100644 backend/db/migrate/20251011200300_add_discard_to_post_tags.rb diff --git a/backend/Gemfile b/backend/Gemfile index bb5460b..303b937 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -63,3 +63,5 @@ gem 'diff-lcs' gem 'dotenv-rails' gem 'whenever', require: false + +gem 'discard' diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index 8494a53..2c08f92 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -90,6 +90,8 @@ GEM crass (1.0.6) date (3.4.1) diff-lcs (1.6.2) + discard (1.4.0) + activerecord (>= 4.2, < 9.0) dotenv (3.1.8) dotenv-rails (3.1.8) dotenv (= 3.1.8) @@ -420,6 +422,7 @@ DEPENDENCIES bootsnap brakeman diff-lcs + discard dotenv-rails gollum image_processing (~> 1.14) diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 81a1c0e..a09028a 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -1,7 +1,3 @@ -require 'open-uri' -require 'nokogiri' - - class PostsController < ApplicationController # GET /posts def index @@ -80,8 +76,9 @@ class PostsController < ApplicationController post.thumbnail.attach(thumbnail) if post.save post.resized_thumbnail! - post.tags = Tag.normalise_tags(tag_names) - post.tags = Tag.expand_parent_tags(post.tags) + tags = Tag.normalise_tags(tag_names) + tags = Tag.expand_parent_tags(tags) + sync_post_tags!(post, tags) render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), status: :created else @@ -114,10 +111,11 @@ class PostsController < ApplicationController original_created_before = params[:original_created_before] post = Post.find(params[:id].to_i) - tags = post.tags.where(category: 'nico').to_a + - Tag.normalise_tags(tag_names, with_tagme: false) - tags = Tag.expand_parent_tags(tags) - if post.update(title:, tags:, original_created_from:, original_created_before:) + if post.update(title:, original_created_from:, original_created_before:) + tags = post.tags.where(category: 'nico').to_a + + Tag.normalise_tags(tag_names, with_tagme: false) + tags = Tag.expand_parent_tags(tags) + sync_post_tags!(post, tags) json = post.as_json json['tags'] = build_tag_tree_for(post.tags) render json:, status: :ok @@ -135,7 +133,11 @@ class PostsController < ApplicationController def filtered_posts tag_names = params[:tags]&.split(' ') match_type = params[:match] - tag_names.present? ? filter_posts_by_tags(tag_names, match_type) : Post.all + if tag_names.present? + filter_posts_by_tags(tag_names, match_type) + else + Post.all + end end def filter_posts_by_tags tag_names, match_type @@ -150,6 +152,30 @@ class PostsController < ApplicationController posts.distinct end + def sync_post_tags! post, desired_tags + desired_tags.each do |t| + t.save! if t.new_record? + end + + desired_ids = desired_tags.map(&:id).to_set + current_ids = post.tags.pluck(:id).to_set + + to_add = desired_ids - current_ids + to_remove = current_ids - desired_ids + + Tag.where(id: to_add).find_each do |tag| + begin + PostTag.create!(post:, tag:, created_user: current_user) + rescue ActiveRecord::RecordNotUnique + ; + end + end + + PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt| + pt.discard_by!(current_user) + end + end + def build_tag_tree_for tags tags = tags.to_a tag_ids = tags.map(&:id) diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index f32f753..6dd565b 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -1,11 +1,13 @@ -require 'mini_magick' - - class Post < ApplicationRecord + require 'mini_magick' + belongs_to :parent, class_name: 'Post', optional: true, foreign_key: 'parent_id' belongs_to :uploaded_user, class_name: 'User', optional: true - has_many :post_tags, dependent: :destroy - has_many :tags, through: :post_tags + + has_many :post_tags, dependent: :destroy, inverse_of: :post + has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :post + has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag' + has_many :tags, through: :active_post_tags has_many :user_post_views, dependent: :destroy has_many :post_similarities_as_post, class_name: 'PostSimilarity', diff --git a/backend/app/models/post_tag.rb b/backend/app/models/post_tag.rb index 9dbd756..91a739d 100644 --- a/backend/app/models/post_tag.rb +++ b/backend/app/models/post_tag.rb @@ -1,7 +1,25 @@ class PostTag < ApplicationRecord + include Discard::Model + belongs_to :post belongs_to :tag, counter_cache: :post_count + belongs_to :created_user, class_name: 'User', optional: true + belongs_to :deleted_user, class_name: 'User', optional: true validates :post_id, presence: true validates :tag_id, presence: true + validates :post_id, uniqueness: { + scope: :tag_id, + conditions: -> { where(discarded_at: nil) } } + + def discard_by! deleted_user + return self if discarded? + + transaction do + update!(discarded_at: Time.current, deleted_user:) + Tag.where(id: tag_id).update_all('post_count = GREATEST(post_count - 1, 0)') + end + + self + end end diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index 8a505e2..d496802 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -1,6 +1,8 @@ class Tag < ApplicationRecord - has_many :post_tags, dependent: :destroy - has_many :posts, through: :post_tags + has_many :post_tags, dependent: :delete_all, inverse_of: :tag + has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag + has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag' + has_many :posts, through: :active_post_tags has_many :tag_aliases, dependent: :destroy has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy @@ -43,13 +45,13 @@ class Tag < ApplicationRecord 'meta:' => 'meta' }.freeze def self.tagme - @tagme ||= Tag.find_or_initialize_by(name: 'タグ希望') do |tag| + @tagme ||= Tag.find_or_create_by!(name: 'タグ希望') do |tag| tag.category = 'meta' end end def self.bot - @bot ||= Tag.find_or_initialize_by(name: 'bot操作') do |tag| + @bot ||= Tag.find_or_create_by!(name: 'bot操作') do |tag| tag.category = 'meta' end end diff --git a/backend/db/migrate/20251011200300_add_discard_to_post_tags.rb b/backend/db/migrate/20251011200300_add_discard_to_post_tags.rb new file mode 100644 index 0000000..f825a37 --- /dev/null +++ b/backend/db/migrate/20251011200300_add_discard_to_post_tags.rb @@ -0,0 +1,36 @@ +class AddDiscardToPostTags < ActiveRecord::Migration[8.0] + def up + execute <<~SQL + DELETE + pt1 + FROM + post_tags pt1 + INNER JOIN + post_tags pt2 + ON + pt1.post_id = pt2.post_id + AND pt1.tag_id = pt2.tag_id + AND pt1.id > pt2.id + ; + SQL + + add_column :post_tags, :discarded_at, :datetime + add_index :post_tags, :discarded_at + + add_column :post_tags, :is_active, :boolean, + as: 'discarded_at IS NULL', stored: true + + add_column :post_tags, :active_unique_key, :string, + as: "CASE WHEN discarded_at IS NULL THEN CONCAT(post_id, ':', tag_id) ELSE NULL END", + stored: true + + add_index :post_tags, :active_unique_key, unique: true, name: 'idx_post_tags_active_unique' + + add_index :post_tags, [:post_id, :discarded_at] + add_index :post_tags, [:tag_id, :discarded_at] + end + + def down + raise ActiveRecord::IrreversibleMigration, '戻せません.' + end +end diff --git a/backend/lib/tasks/sync_nico.rake b/backend/lib/tasks/sync_nico.rake index 9fe28fb..00e309f 100644 --- a/backend/lib/tasks/sync_nico.rake +++ b/backend/lib/tasks/sync_nico.rake @@ -5,12 +5,30 @@ namespace :nico do require 'open-uri' require 'nokogiri' - fetch_thumbnail = -> url { + fetch_thumbnail = -> url do html = URI.open(url, read_timeout: 60, 'User-Agent' => 'Mozilla/5.0').read doc = Nokogiri::HTML(html) doc.at('meta[name="thumbnail"]')&.[]('content').presence - } + end + + def sync_post_tags! post, desired_tag_ids + desired_ids = desired_tag_ids.compact.to_set + current_ids = post.tags.pluck(:id).to_set + + to_add = desired_ids - current_ids + to_remove = current_ids - desired_ids + + Tag.where(id: to_add.to_a).find_each do |tag| + begin + PostTag.create!(post:, tag:) + rescue ActiveRecord::RecordNotUnique + ; + end + end + + PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each(&:discard!) + end mysql_user = ENV['MYSQL_USER'] mysql_pass = ENV['MYSQL_PASS'] @@ -19,43 +37,53 @@ namespace :nico do { 'MYSQL_USER' => mysql_user, 'MYSQL_PASS' => mysql_pass }, 'python3', "#{ nizika_nico_path }/get_videos.py") - if status.success? - data = JSON.parse(stdout) - data.each do |datum| - post = Post.where('url LIKE ?', '%nicovideo.jp%').find { |post| - post.url =~ %r{#{ Regexp.escape(datum['code']) }(?!\d)} - } - unless post - title = datum['title'] - url = "https://www.nicovideo.jp/watch/#{ datum['code'] }" - thumbnail_base = fetch_thumbnail.(url) || '' rescue '' - post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil) - if thumbnail_base.present? - post.thumbnail.attach( - io: URI.open(thumbnail_base), - filename: File.basename(URI.parse(thumbnail_base).path), - content_type: 'image/jpeg') - end - post.save! - post.resized_thumbnail! + abort unless status.success? + + data = JSON.parse(stdout) + data.each do |datum| + post = Post.where('url LIKE ?', '%nicovideo.jp%').find { |post| + post.url =~ %r{#{ Regexp.escape(datum['code']) }(?!\d)} + } + unless post + title = datum['title'] + url = "https://www.nicovideo.jp/watch/#{ datum['code'] }" + thumbnail_base = fetch_thumbnail.(url) || '' rescue '' + post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil) + if thumbnail_base.present? + post.thumbnail.attach( + io: URI.open(thumbnail_base), + filename: File.basename(URI.parse(thumbnail_base).path), + content_type: 'image/jpeg') end + post.save! + post.resized_thumbnail! + end + + kept_tags = post.tags.reload + kept_non_nico_ids = kept_tags.where.not(category: 'nico').pluck(:id).to_set - current_tags = post.tags.where(category: 'nico').pluck(:name).sort - new_tags = datum['tags'].map { |tag| "nico:#{ tag }" }.sort - if current_tags != new_tags - post.tags.destroy(post.tags.where(name: current_tags)) - tags_to_add = [] - new_tags.each do |name| - tag = Tag.find_or_initialize_by(name:) do |t| - t.category = 'nico' - end - tags_to_add.concat([tag] + tag.linked_tags) - end - tags_to_add << Tag.tagme if post.tags.size < 10 - tags_to_add << Tag.bot - post.tags = (post.tags + tags_to_add).uniq + desired_nico_ids = [] + datum['tags'].each do |raw| + name = "nico:#{ raw }" + tag = Tag.find_or_initialize_by(name:) do |t| + t.category = 'nico' end + tag.save! if tag.new_record? + desired_nico_ids << tag.id + desired_nico_ids.concat(tag.linked_tags.pluck(:id)) end + desired_nico_ids.uniq! + + desired_extra_ids = [] + desired_extra_ids << Tag.tagme.id if kept_tags.size < 10 + desired_extra_ids << Tag.bot.id + desired_extra_ids.compact! + desired_extra_ids.uniq! + + desired_all_ids = kept_non_nico_ids.to_a + desired_nico_ids + desired_extra_ids + desired_all_ids.uniq! + + sync_post_tags!(post, desired_all_ids) end end end From f36837f0d83332d74039cc4ef3d43eafc8d03bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sun, 14 Dec 2025 03:49:03 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=E8=80=95=E4=BD=9C=E5=B1=A5?= =?UTF-8?q?=E6=AD=B4=E3=83=9A=E3=83=BC=E3=82=B8=E4=BD=9C=E6=88=90=EF=BC=88?= =?UTF-8?q?#112=EF=BC=89=20(#177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #112 現在ページの表示を太く #112 完了 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/177 --- backend/app/controllers/posts_controller.rb | 40 +++++++++ backend/config/routes.rb | 1 + frontend/src/App.tsx | 2 + frontend/src/components/TopNav.tsx | 1 + frontend/src/components/common/Pagination.tsx | 79 ++++++++++++++++ frontend/src/pages/posts/PostHistoryPage.tsx | 90 +++++++++++++++++++ frontend/src/types.ts | 7 ++ 7 files changed, 220 insertions(+) create mode 100644 frontend/src/components/common/Pagination.tsx create mode 100644 frontend/src/pages/posts/PostHistoryPage.tsx diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index a09028a..96af1c1 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -1,4 +1,6 @@ class PostsController < ApplicationController + Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true) + # GET /posts def index limit = params[:limit].presence&.to_i @@ -128,6 +130,44 @@ class PostsController < ApplicationController def destroy end + def changes + id = params[:id] + page = (params[:page].presence || 1).to_i + limit = (params[:limit].presence || 20).to_i + + page = 1 if page < 1 + limit = 1 if limit < 1 + + offset = (page - 1) * limit + + pts = PostTag.with_discarded + pts = pts.where(post_id: id) if id.present? + pts = pts.includes(:post, :tag, :created_user, :deleted_user) + + events = [] + pts.each do |pt| + events << Event.new( + post: pt.post, + tag: pt.tag, + user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name }, + change_type: 'add', + timestamp: pt.created_at) + + if pt.discarded_at + events << Event.new( + post: pt.post, + tag: pt.tag, + user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name }, + change_type: 'remove', + timestamp: pt.discarded_at) + end + end + events.sort_by!(&:timestamp) + events.reverse! + + render json: { changes: events.slice(offset, limit).as_json, count: events.size } + end + private def filtered_posts diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 709f1d0..0733991 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -4,6 +4,7 @@ Rails.application.routes.draw do get 'tags/autocomplete', to: 'tags#autocomplete' get 'tags/name/:name', to: 'tags#show_by_name' get 'posts/random', to: 'posts#random' + get 'posts/changes', to: 'posts#changes' post 'posts/:id/viewed', to: 'posts#viewed' delete 'posts/:id/viewed', to: 'posts#unviewed' get 'preview/title', to: 'preview#title' diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d1b1e28..2195ca8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import { API_BASE_URL } from '@/config' import NicoTagListPage from '@/pages/tags/NicoTagListPage' import NotFound from '@/pages/NotFound' import PostDetailPage from '@/pages/posts/PostDetailPage' +import PostHistoryPage from '@/pages/posts/PostHistoryPage' import PostListPage from '@/pages/posts/PostListPage' import PostNewPage from '@/pages/posts/PostNewPage' import ServiceUnavailable from '@/pages/ServiceUnavailable' @@ -79,6 +80,7 @@ export default (() => { }/> }/> }/> + }/> }/> }/> }/> diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 104f40e..e62be09 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -30,6 +30,7 @@ export default (({ user }: Props) => { { name: '広場', to: '/posts', subMenu: [ { name: '一覧', to: '/posts' }, { name: '投稿追加', to: '/posts/new' }, + { name: '耕作履歴', to: '/posts/changes' }, { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] }, { name: 'タグ', to: '/tags', subMenu: [ { name: 'タグ一覧', to: '/tags', visible: false }, diff --git a/frontend/src/components/common/Pagination.tsx b/frontend/src/components/common/Pagination.tsx new file mode 100644 index 0000000..5fb527e --- /dev/null +++ b/frontend/src/components/common/Pagination.tsx @@ -0,0 +1,79 @@ +import { Link, useLocation } from 'react-router-dom' + +import type { FC } from 'react' + +type Props = { page: number + totalPages: number + siblingCount?: number } + + +const range = (start: number, end: number): number[] => + [...Array (end - start + 1).keys ()].map (i => start + i) + + +const getPages = ( + page: number, + total: number, + siblingCount: number, +): (number | '…')[] => { + if (total <= 1) + return [1] + + const first = 1 + const last = total + + const left = Math.max (page - siblingCount, first) + const right = Math.min (page + siblingCount, last) + + const pages: (number | '…')[] = [] + + pages.push (first) + + if (left > first + 1) + pages.push ('…') + + const midStart = Math.max (left, first + 1) + const midEnd = Math.min (right, last - 1) + pages.push (...range (midStart, midEnd)) + + if (right < last - 1) + pages.push ('…') + + if (last !== first) + pages.push (last) + + return pages.filter ((v, i, arr) => i === 0 || v !== arr[i - 1]) +} + + +export default (({ page, totalPages, siblingCount = 4 }) => { + const location = useLocation () + + const buildTo = (p: number) => { + const qs = new URLSearchParams (location.search) + qs.set ('page', String (p)) + return `${ location.pathname }?${ qs.toString () }` + } + + const pages = getPages (page, totalPages, siblingCount) + + return ( + ) +}) satisfies FC diff --git a/frontend/src/pages/posts/PostHistoryPage.tsx b/frontend/src/pages/posts/PostHistoryPage.tsx new file mode 100644 index 0000000..e401b09 --- /dev/null +++ b/frontend/src/pages/posts/PostHistoryPage.tsx @@ -0,0 +1,90 @@ +import axios from 'axios' +import toCamel from 'camelcase-keys' +import { useEffect, useState } from 'react' +import { Helmet } from 'react-helmet-async' +import { Link, useLocation } from 'react-router-dom' + +import TagLink from '@/components/TagLink' +import PageTitle from '@/components/common/PageTitle' +import Pagination from '@/components/common/Pagination' +import MainArea from '@/components/layout/MainArea' +import { API_BASE_URL, SITE_TITLE } from '@/config' + +import type { FC } from 'react' + +import type { PostTagChange } from '@/types' + + +export default (() => { + const [changes, setChanges] = useState ([]) + const [totalPages, setTotalPages] = useState (0) + + const location = useLocation () + const query = new URLSearchParams (location.search) + const id = query.get ('id') + const page = Number (query.get ('page') ?? 1) + const limit = Number (query.get ('limit') ?? 20) + + useEffect (() => { + void (async () => { + const res = await axios.get (`${ API_BASE_URL }/posts/changes`, + { params: { ...(id && { id }), + ...(page && { page }), + ...(limit && { limit }) } }) + const data = toCamel (res.data as any, { deep: true }) as { + changes: PostTagChange[] + count: number } + setChanges (data.changes) + setTotalPages (Math.trunc ((data.count - 1) / limit)) + }) () + }, [location.search]) + + return ( + + + {`耕作履歴 | ${ SITE_TITLE }`} + + + + 耕作履歴 + {Boolean (id) && <>: 投稿 {#{id}}} + + + + + + + + + + + + {changes.map (change => ( + + + + + ))} + +
    投稿変更日時
    + + {change.post.title + + + + {`を${ change.changeType === 'add' ? '追加' : '削除' }`} + + {change.user ? ( + + {change.user.name} + ) : 'bot 操作'} +
    + {change.timestamp} +
    + + +
    ) +}) satisfies FC diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 5904f19..f78c85b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -29,6 +29,13 @@ export type Post = { originalCreatedFrom: string | null originalCreatedBefore: string | null } +export type PostTagChange = { + post: Post + tag: Tag + user?: User + changeType: 'add' | 'remove' + timestamp: string } + export type SubMenuItem = | { component: ReactNode visible: boolean } From 92d9fe7733f10cd8e692a83ce6afd9c51814f0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sun, 14 Dec 2025 16:24:04 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=E3=82=A2=E3=83=8B=E3=83=A1?= =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E4=B8=80=E9=83=A8?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88#176=EF=BC=89=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #176 完了 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/178 --- frontend/package-lock.json | 43 +++++++ frontend/package.json | 1 + frontend/src/components/TagDetailSidebar.tsx | 123 ++++++++++--------- frontend/src/components/TagSidebar.tsx | 79 +++++++----- frontend/src/components/TopNav.tsx | 78 +++++++----- 5 files changed, 204 insertions(+), 120 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b241081..43fbc44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "camelcase-keys": "^9.1.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.26", "humps": "^2.0.1", "lucide-react": "^0.511.0", "markdown-it": "^14.1.0", @@ -3573,6 +3574,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5216,6 +5244,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 747f7ed..cbe44ff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "camelcase-keys": "^9.1.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.26", "humps": "^2.0.1", "lucide-react": "^0.511.0", "markdown-it": "^14.1.0", diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index da2c3b8..0006738 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -1,3 +1,4 @@ +import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useState } from 'react' import TagLink from '@/components/TagLink' @@ -18,18 +19,23 @@ const renderTagTree = ( tag: Tag, nestLevel: number, path: string, - ): ReactNode[] => { +): ReactNode[] => { const key = `${ path }-${ tag.id }` const self = ( -
  • + -
  • ) + ) return [self, - ...(tag.children - ?.sort ((a, b) => a.name < b.name ? -1 : 1) - .flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])] + ...((tag.children + ?.sort ((a, b) => a.name < b.name ? -1 : 1) + .flatMap (child => renderTagTree (child, nestLevel + 1, key))) + ?? [])] } @@ -70,55 +76,60 @@ export default (({ post }: Props) => { return ( - {CATEGORIES.map ((cat: Category) => cat in tags && ( -
    - {categoryNames[cat]} -
      - {tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))} -
    -
    ))} - {post && ( -
    - 情報 -
      -
    • Id.: {post.id}
    • - {/* TODO: uploadedUser の取得を対応したらコメント外す */} - {/* -
    • - <>耕作者: - {post.uploadedUser - ? ( - - {post.uploadedUser.name || '名もなきニジラー'} - ) - : 'bot操作'} -
    • - */} -
    • 耕作日時: {(new Date (post.createdAt)).toLocaleString ()}
    • -
    • - <>リンク: - - {post.url} - -
    • -
    • - {/* TODO: 表示形式きしょすぎるので何とかする */} - <>オリジナルの投稿日時: - {!(post.originalCreatedFrom) && !(post.originalCreatedBefore) - ? '不明' - : ( - <> - {post.originalCreatedFrom - && `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} - {post.originalCreatedBefore - && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} - )} -
    • -
    -
    )} + + {CATEGORIES.map ((cat: Category) => cat in tags && ( + + {categoryNames[cat]} + + + + {tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))} + + + ))} + {post && ( +
    + 情報 +
      +
    • Id.: {post.id}
    • + {/* TODO: uploadedUser の取得を対応したらコメント外す */} + {/* +
    • + <>耕作者: + {post.uploadedUser + ? ( + + {post.uploadedUser.name || '名もなきニジラー'} + ) + : 'bot操作'} +
    • + */} +
    • 耕作日時: {(new Date (post.createdAt)).toLocaleString ()}
    • +
    • + <>リンク: + + {post.url} + +
    • +
    • + {/* TODO: 表示形式きしょすぎるので何とかする */} + <>オリジナルの投稿日時: + {!(post.originalCreatedFrom) && !(post.originalCreatedBefore) + ? '不明' + : ( + <> + {post.originalCreatedFrom + && `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} + {post.originalCreatedBefore + && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} + )} +
    • +
    +
    )} +
    ) }) satisfies FC diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx index a2e00b2..735490d 100644 --- a/frontend/src/components/TagSidebar.tsx +++ b/frontend/src/components/TagSidebar.tsx @@ -1,4 +1,5 @@ import axios from 'axios' +import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' @@ -8,7 +9,6 @@ import SectionTitle from '@/components/common/SectionTitle' import SidebarComponent from '@/components/layout/SidebarComponent' import { API_BASE_URL } from '@/config' import { CATEGORIES } from '@/consts' -import { cn } from '@/lib/utils' import type { FC } from 'react' @@ -61,37 +61,52 @@ export default (({ posts }: Props) => { return ( - + + + {tagsVsbl && ( + + タグ +
      + {CATEGORIES.flatMap (cat => cat in tags ? ( + tags[cat].map (tag => ( +
    • + +
    • ))) : [])} +
    + 関聯 + {posts.length > 0 && ( + { + ev.preventDefault () + void ((async () => { + try + { + const { data } = await axios.get (`${ API_BASE_URL }/posts/random`, + { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), + match: (anyFlg ? 'any' : 'all') } }) + navigate (`/posts/${ (data as Post).id }`) + } + catch + { + ; + } + }) ()) + }}> + ランダム + )} +
    )} +
    + - {subItem.name} - )))} - ))} - - -
    + + {menuOpen && ( + + + {menu.map ((item, i) => ( + + { + if (i !== openItemIdx) + { + ev.preventDefault () + setOpenItemIdx (i) + } + }}> + {item.name} + + {i === openItemIdx && ( + item.subMenu + .filter (subItem => subItem.visible ?? true) + .map ((subItem, j) => 'component' in subItem ? subItem.component : ( + + {subItem.name} + )))} + ))} + + + )} + ) }) satisfies FC From 43a55dbfcfa687cd0065cfe657059a3ff5a4d279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sun, 14 Dec 2025 16:51:57 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20#176=20=E3=81=AE=E3=83=90?= =?UTF-8?q?=E3=82=B0=E5=AF=BE=E5=BF=9C=EF=BC=88#179=EF=BC=89=20(#180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #179 完了 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/180 --- frontend/src/components/TagSidebar.tsx | 71 +++++++++++++++----------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx index 735490d..b8f5f07 100644 --- a/frontend/src/components/TagSidebar.tsx +++ b/frontend/src/components/TagSidebar.tsx @@ -58,15 +58,52 @@ export default (({ posts }: Props) => { setTags (tagsTmp) }, [posts]) + const TagBlock = ( + <> + タグ +
      + {CATEGORIES.flatMap (cat => cat in tags ? ( + tags[cat].map (tag => ( +
    • + +
    • ))) : [])} +
    + 関聯 + {posts.length > 0 && ( +
    { + ev.preventDefault () + void ((async () => { + try + { + const { data } = await axios.get (`${ API_BASE_URL }/posts/random`, + { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), + match: (anyFlg ? 'any' : 'all') } }) + navigate (`/posts/${ (data as Post).id }`) + } + catch + { + ; + } + }) ()) + }}> + ランダム + )} + ) + return ( +
    + {TagBlock} +
    + {tagsVsbl && ( { animate="visible" exit="hidden" transition={{ duration: .2, ease: 'easeOut' }}> - タグ -
      - {CATEGORIES.flatMap (cat => cat in tags ? ( - tags[cat].map (tag => ( -
    • - -
    • ))) : [])} -
    - 関聯 - {posts.length > 0 && ( - { - ev.preventDefault () - void ((async () => { - try - { - const { data } = await axios.get (`${ API_BASE_URL }/posts/random`, - { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), - match: (anyFlg ? 'any' : 'all') } }) - navigate (`/posts/${ (data as Post).id }`) - } - catch - { - ; - } - }) ()) - }}> - ランダム - )} + {TagBlock}
    )}
    @@ -113,7 +122,7 @@ export default (({ posts }: Props) => { dark:text-gray-300 dark:hover:text-gray-100" onClick={ev => { ev.preventDefault () - setTagsVsbl (!(tagsVsbl)) + setTagsVsbl (v => !(v)) }}> {tagsVsbl ? '▲▲▲ タグ一覧を閉じる ▲▲▲' : '▼▼▼ タグ一覧を表示 ▼▼▼'} From 468ef44bf9023459d50f392a6d71b8f7a17b067b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Mon, 15 Dec 2025 01:13:52 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20#84=20=E3=81=AE=E3=83=90=E3=82=B0?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=EF=BC=88#181=EF=BC=89=20(#182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #181 バグ #181 #181 #181 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/182 --- backend/lib/tasks/sync_nico.rake | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/lib/tasks/sync_nico.rake b/backend/lib/tasks/sync_nico.rake index 00e309f..d20150c 100644 --- a/backend/lib/tasks/sync_nico.rake +++ b/backend/lib/tasks/sync_nico.rake @@ -27,7 +27,9 @@ namespace :nico do end end - PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each(&:discard!) + PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt| + pt.discard_by!(nil) + end end mysql_user = ENV['MYSQL_USER'] @@ -57,12 +59,14 @@ namespace :nico do end post.save! post.resized_thumbnail! + sync_post_tags!(post, [Tag.tagme.id]) end kept_tags = post.tags.reload kept_non_nico_ids = kept_tags.where.not(category: 'nico').pluck(:id).to_set desired_nico_ids = [] + desired_non_nico_ids = [] datum['tags'].each do |raw| name = "nico:#{ raw }" tag = Tag.find_or_initialize_by(name:) do |t| @@ -70,17 +74,19 @@ namespace :nico do end tag.save! if tag.new_record? desired_nico_ids << tag.id - desired_nico_ids.concat(tag.linked_tags.pluck(:id)) + unless tag.in?(kept_tags) + desired_non_nico_ids.concat(tag.linked_tags.pluck(:id)) + desired_nico_ids.concat(tag.linked_tags.pluck(:id)) + end end desired_nico_ids.uniq! - desired_extra_ids = [] - desired_extra_ids << Tag.tagme.id if kept_tags.size < 10 - desired_extra_ids << Tag.bot.id - desired_extra_ids.compact! - desired_extra_ids.uniq! - - desired_all_ids = kept_non_nico_ids.to_a + desired_nico_ids + desired_extra_ids + desired_all_ids = kept_non_nico_ids.to_a + desired_nico_ids + desired_non_nico_ids.concat(kept_non_nico_ids.to_a) + desired_non_nico_ids.uniq! + if kept_non_nico_ids.to_set != desired_non_nico_ids.to_set + desired_all_ids << Tag.bot.id + end desired_all_ids.uniq! sync_post_tags!(post, desired_all_ids) From 34c227f326287d5f8105499a9b75470e1d2e111b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sat, 20 Dec 2025 02:36:32 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20=E3=82=A2=E3=83=8B=E3=83=A1?= =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E4=BF=AE=E6=AD=A3=EF=BC=88?= =?UTF-8?q?#183=EF=BC=89=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #183 #183 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/185 --- frontend/src/components/TagDetailSidebar.tsx | 2 +- frontend/src/components/TopNav.tsx | 159 +++++++++++++++---- 2 files changed, 129 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 0006738..5d02358 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -76,7 +76,7 @@ export default (({ post }: Props) => { return ( - + {CATEGORIES.map ((cat: Category) => cat in tags && ( {categoryNames[cat]} diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 35a1a57..9762a3d 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -1,7 +1,7 @@ import axios from 'axios' import toCamel from 'camelcase-keys' import { AnimatePresence, motion } from 'framer-motion' -import { Fragment, useState, useEffect } from 'react' +import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react' import { Link, useLocation } from 'react-router-dom' import Separator from '@/components/MenuSeparator' @@ -20,6 +20,28 @@ type Props = { user: User | null } export default (({ user }: Props) => { const location = useLocation () + const dirRef = useRef<(-1) | 1> (1) + const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) + const navRef = useRef (null) + + const measure = () => { + const nav = navRef.current + const el = itemsRef.current[activeIdx] + if (!(nav) || !(el) || activeIdx < 0) + return + + const navRect = nav.getBoundingClientRect () + const elRect = el.getBoundingClientRect () + + setHl ({ left: elRect.left - navRect.left, + width: elRect.width, + visible: true }) + } + + const [hl, setHl] = useState<{ left: number; width: number; visible: boolean }> ({ + left: 0, + width: 0, + visible: false }) const [menuOpen, setMenuOpen] = useState (false) const [openItemIdx, setOpenItemIdx] = useState (-1) const [postCount, setPostCount] = useState (null) @@ -54,6 +76,32 @@ export default (({ user }: Props) => { { name: 'お前', to: `/users/${ user?.id }`, visible: false }, { name: '設定', to: '/users/settings', visible: Boolean (user) }] }] + const activeIdx = menu.findIndex (item => location.pathname.startsWith (item.base || item.to)) + + const prevActiveIdxRef = useRef (activeIdx) + + if (activeIdx !== prevActiveIdxRef.current) + { + dirRef.current = activeIdx > prevActiveIdxRef.current ? 1 : -1 + prevActiveIdxRef.current = activeIdx + } + + const dir = dirRef.current + + useLayoutEffect (() => { + if (activeIdx < 0) + return + + const raf = requestAnimationFrame (measure) + const onResize = () => requestAnimationFrame (measure) + + addEventListener ('resize', onResize) + return () => { + cancelAnimationFrame (raf) + removeEventListener ('resize', onResize) + } + }, [activeIdx]) + useEffect (() => { const unsubscribe = WikiIdBus.subscribe (setWikiId) return () => unsubscribe () @@ -99,16 +147,26 @@ export default (({ user }: Props) => { ぼざクリ タグ広場 - {menu.map ((item, i) => ( - - {item.name} - - ))} +
    +
    + + {menu.map ((item, i) => ( + { + itemsRef.current[i] = el + }} + className={cn ('relative z-10 flex h-full items-center px-5', + (i === openItemIdx) && 'font-bold')}> + {item.name} + ))} +
    @@ -125,16 +183,33 @@ export default (({ user }: Props) => { -
    - {menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu - .filter (item => item.visible ?? true) - .map ((item, i) => 'component' in item ? item.component : ( - - {item.name} - ))} +
    + + ({ y: d * 24, opacity: 0 }), + centre: { y: 0, opacity: 1 }, + exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }} + className="absolute inset-0 flex items-center px-3" + initial="enter" + animate="centre" + exit="exit" + transition={{ duration: .2, ease: 'easeOut' }}> + {(menu[activeIdx]?.subMenu ?? []) + .filter (item => item.visible ?? true) + .map ((item, i) => ( + 'component' in item + ? {item.component} + : ( + + {item.name} + )))} + +
    @@ -167,16 +242,38 @@ export default (({ user }: Props) => { }}> {item.name} - {i === openItemIdx && ( - item.subMenu - .filter (subItem => subItem.visible ?? true) - .map ((subItem, j) => 'component' in subItem ? subItem.component : ( - - {subItem.name} - )))} + + + {i === openItemIdx && ( + + {item.subMenu + .filter (subItem => subItem.visible ?? true) + .map ((subItem, j) => ( + 'component' in subItem + ? ( + + {subItem.component} + ) + : ( + + {subItem.name} + )))} + )} + ))} From 0353961a721d39f5fd8b189d8d288013ebcfcb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Tue, 23 Dec 2025 23:37:58 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20403=20=E8=A7=A3=E6=B6=88=EF=BC=88?= =?UTF-8?q?#168=EF=BC=89=20(#187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #168 #168 Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/187 --- frontend/scripts/generate-sitemap.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/scripts/generate-sitemap.js b/frontend/scripts/generate-sitemap.js index f30786c..bf6c9fe 100644 --- a/frontend/scripts/generate-sitemap.js +++ b/frontend/scripts/generate-sitemap.js @@ -19,7 +19,6 @@ const fetchPosts = async tagName => (await axios.get (`${ API_BASE_URL }/posts`, { params: { ...(tagName && { tags: tagName, match: 'all', limit: '20' }) } })).data.posts -const fetchPostIds = async () => (await fetchPosts ()).map (post => post.id) const fetchTags = async () => (await axios.get (`${ API_BASE_URL }/tags`)).data const fetchTagNames = async () => (await fetchTags ()).map (tag => tag.name) @@ -33,7 +32,7 @@ const createPostListOutlet = async tagName => `
    - ${ (await fetchPosts (tagName)).map (post => ` + ${ (await fetchPosts (tagName)).slice (0, 20).map (post => ` ${ post.title } ` fetchpriority="high" decoding="async" class="object-none w-full h-full" - src="${ post.url }" /> + src="${ post.thumbnail }" /> `).join ('') }