From bae03cf918da8df9b89af2509a73ff1ab389edf8 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 9 Dec 2025 01:43:05 +0900 Subject: [PATCH] =?UTF-8?q?#64=20=E3=81=8A=E3=81=9D=E3=82=89=E3=81=8F?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/controllers/posts_controller.rb | 56 ++++++++++++++++++-- frontend/src/components/PostEditForm.tsx | 29 +++++++--- frontend/src/components/TagDetailSidebar.tsx | 25 +++++++-- frontend/src/components/TagLink.tsx | 8 +++ frontend/src/types.ts | 20 +++---- 5 files changed, 112 insertions(+), 26 deletions(-) diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 09c61b7..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 @@ -115,8 +118,9 @@ class PostsController < ApplicationController 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 @@ -145,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/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 ? (