From 2cd9c603688e0dc92f21a236454a82a0dff64880 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Mon, 22 Jun 2026 06:18:10 +0900 Subject: [PATCH] #378 --- backend/app/controllers/tags_controller.rb | 124 ++++++++++++++++----- 1 file changed, 94 insertions(+), 30 deletions(-) diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index b9fadb9..c9e585e 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -1,5 +1,6 @@ require 'net/http' require 'uri' +require 'set' class TagsController < ApplicationController @@ -82,51 +83,27 @@ class TagsController < ApplicationController parent_tag_id = params[:parent].to_i parent_tag_id = nil if parent_tag_id <= 0 + graph = build_with_depth_graph + tag_ids = if parent_tag_id - TagImplication.joins(:tag) - .where(parent_tag_id:) - .where(tags: { deprecated_at: nil }) - .select(:tag_id) + visible_child_tag_ids(parent_tag_id, graph) else - Tag.where(deprecated_at: nil) - .where.not(id: TagImplication - .joins(<<~SQL.squish) - INNER JOIN - tags parent_tags - ON parent_tags.id = tag_implications.parent_tag_id - SQL - .where('parent_tags.deprecated_at IS NULL') - .select(:tag_id)) - .select(:id) + visible_root_tag_ids(graph) end tags = Tag .joins(:tag_name) .includes(:tag_name, :materials, tag_name: :wiki_page) - .where(category: [:meme, :character, :material]) - .where(deprecated_at: nil) .where(id: tag_ids) .order('tag_names.name') .distinct .to_a - has_children_tag_ids = - if tags.empty? - [] - else - TagImplication - .joins(:tag) - .where(parent_tag_id: tags.map(&:id), - tags: { category: [:meme, :character, :material], - deprecated_at: nil }) - .distinct - .pluck(:parent_tag_id) - end - render json: tags.map { |tag| - TagRepr.base(tag).merge(has_children: has_children_tag_ids.include?(tag.id), children: []) + TagRepr.base(tag).merge(has_children: visible_child_tag_ids(tag.id, graph).present?, + children: []) } end @@ -371,6 +348,93 @@ class TagsController < ApplicationController private + def build_with_depth_graph + children_by_parent_id = Hash.new { |h, k| h[k] = [] } + parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] } + + TagImplication.pluck(:parent_tag_id, :tag_id).each do |parent_id, child_id| + children_by_parent_id[parent_id] << child_id + parent_ids_by_child_id[child_id] << parent_id + end + + tag_ids = (children_by_parent_id.keys + + parent_ids_by_child_id.keys + + Tag.where(category: ['meme', 'character', 'material']).pluck(:id)).uniq + + tags_by_id = Tag.where(id: tag_ids) + .pluck(:id, :category, :deprecated_at) + .each_with_object({ }) do |(id, category, deprecated_at), h| + h[id] = { category:, deprecated: deprecated_at.present? } + end + + { children_by_parent_id:, parent_ids_by_child_id:, tags_by_id:, + visible_child_tag_ids_by_parent_id: { } } + end + + def visible_root_tag_ids graph + graph[:tags_by_id].filter_map do |tag_id, attrs| + next unless with_depth_visible_tag?(attrs) + next unless visible_root_tag?(tag_id, graph) + + tag_id + end + end + + def visible_root_tag? tag_id, graph + seen = Set.new([tag_id]) + stack = graph[:parent_ids_by_child_id][tag_id].dup + + until stack.empty? + parent_id = stack.pop + next if seen.include?(parent_id) + + seen << parent_id + + parent = graph[:tags_by_id][parent_id] + next unless parent + + return false unless parent[:deprecated] + + stack.concat(graph[:parent_ids_by_child_id][parent_id]) + end + + true + end + + def visible_child_tag_ids parent_tag_id, graph + cache = graph[:visible_child_tag_ids_by_parent_id] + return cache[parent_tag_id] if cache.key?(parent_tag_id) + + visible_ids = Set.new + + graph[:children_by_parent_id][parent_tag_id].each do |child_tag_id| + collect_visible_child_tag_ids(child_tag_id, graph, visible_ids, Set.new([parent_tag_id])) + end + + cache[parent_tag_id] = visible_ids.to_a + end + + def collect_visible_child_tag_ids tag_id, graph, visible_ids, seen + return if seen.include?(tag_id) + + seen = seen.dup << tag_id + tag = graph[:tags_by_id][tag_id] + return unless tag + + if tag[:deprecated] + graph[:children_by_parent_id][tag_id].each do |child_tag_id| + collect_visible_child_tag_ids(child_tag_id, graph, visible_ids, seen) + end + return + end + + visible_ids << tag_id if with_depth_visible_tag?(tag) + end + + def with_depth_visible_tag? tag + tag[:category].in?(['meme', 'character', 'material']) && !tag[:deprecated] + end + def build_tag_children tag material = tag.materials.first file = nil