From ef3d428a06f1d6f99a0e77a457dbc569496f649b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Fri, 23 Jan 2026 01:08:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Wiki=20=E6=9C=89=E7=84=A1=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E8=BB=BD=E9=87=8F=E5=8C=96=EF=BC=88#233=EF=BC=89=20(#?= =?UTF-8?q?234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #233 Backend が対応できてなかったのでそっちも対応させた. Co-authored-by: miteruzo Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/234 --- backend/app/controllers/posts_controller.rb | 14 +++++++------- backend/spec/requests/posts_spec.rb | 18 +++++++++++++----- frontend/src/components/TagLink.tsx | 12 ++++++++++-- frontend/src/types.ts | 1 + 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index e4dbb04..648d42e 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -36,7 +36,7 @@ class PostsController < ApplicationController render json: { posts: posts.map { |post| post.as_json(include: { tags: { only: [:id, :category, :post_count], - methods: [:name] } }).tap do |json| + methods: [:name, :has_wiki] } }).tap do |json| json['thumbnail'] = if post.thumbnail.attached? rails_storage_proxy_url(post.thumbnail, only_path: false) @@ -59,7 +59,7 @@ class PostsController < ApplicationController render json: (post .as_json(include: { tags: { only: [:id, :category, :post_count], - methods: [:name] } }) + methods: [:name, :has_wiki] } }) .merge(viewed:)) end @@ -100,7 +100,7 @@ class PostsController < ApplicationController tags = Tag.expand_parent_tags(tags) sync_post_tags!(post, tags) render json: post.as_json(include: { tags: { only: [:id, :category, :post_count], - methods: [:name] } }), + methods: [:name, :has_wiki] } }), status: :created else render json: { errors: post.errors.full_messages }, status: :unprocessable_entity @@ -162,7 +162,7 @@ class PostsController < ApplicationController pts.each do |pt| events << Event.new( post: pt.post, - tag: pt.tag.as_json(only: [:id, :category], methods: [:name]), + tag: pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]), user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name }, change_type: 'add', timestamp: pt.created_at) @@ -170,7 +170,7 @@ class PostsController < ApplicationController if pt.discarded_at events << Event.new( post: pt.post, - tag: pt.tag.as_json(only: [:id, :category], methods: [:name]), + tag: pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]), user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name }, change_type: 'remove', timestamp: pt.discarded_at) @@ -255,7 +255,7 @@ class PostsController < ApplicationController if path.include?(tag_id) return tag.as_json(only: [:id, :category, :post_count], - methods: [:name]).merge(children: []) + methods: [:name, :has_wiki]).merge(children: []) end if memo.key?(tag_id) @@ -268,7 +268,7 @@ class PostsController < ApplicationController children = child_ids.filter_map { |cid| build_node.(cid, new_path) } memo[tag_id] = tag.as_json(only: [:id, :category, :post_count], - methods: [:name]).merge(children:) + methods: [:name, :has_wiki]).merge(children:) end root_ids.filter_map { |id| build_node.call(id, []) } diff --git a/backend/spec/requests/posts_spec.rb b/backend/spec/requests/posts_spec.rb index 817eec0..eb7ce9f 100644 --- a/backend/spec/requests/posts_spec.rb +++ b/backend/spec/requests/posts_spec.rb @@ -54,9 +54,9 @@ RSpec.describe 'Posts API', type: :request do # 全postの全tagが name を含むこと expect(posts).not_to be_empty posts.each do |p| - expect(p["tags"]).to be_an(Array) - p["tags"].each do |t| - expect(t).to include("name", "category") + expect(p['tags']).to be_an(Array) + p['tags'].each do |t| + expect(t).to include('name', 'category', 'has_wiki') end end expect(json['count']).to be_an(Integer) @@ -71,11 +71,19 @@ RSpec.describe 'Posts API', type: :request do get "/posts", params: { tags: "spec_tag" } expect(response).to have_http_status(:ok) - ids = json.fetch("posts").map { |p| p["id"] } + posts = json.fetch('posts') + ids = posts.map { |p| p['id'] } expect(ids).to include(hit_post.id) expect(ids).not_to include(miss_post.id) expect(json['count']).to be_an(Integer) + + posts.each do |p| + expect(p['tags']).to be_an(Array) + p['tags'].each do |t| + expect(t).to include('name', 'category', 'has_wiki') + end + end end it "returns empty posts when nothing matches" do @@ -104,7 +112,7 @@ RSpec.describe 'Posts API', type: :request do # show は build_tag_tree_for を使うので、tags はツリー形式(children 付き) node = json['tags'][0] - expect(node).to include('id', 'name', 'category', 'post_count', 'children') + expect(node).to include('id', 'name', 'category', 'post_count', 'children', 'has_wiki') expect(node['name']).to eq('spec_tag') expect(json).to have_key('related') diff --git a/frontend/src/components/TagLink.tsx b/frontend/src/components/TagLink.tsx index ee673cc..a9a1c11 100644 --- a/frontend/src/components/TagLink.tsx +++ b/frontend/src/components/TagLink.tsx @@ -32,7 +32,15 @@ export default (({ tag, ...props }: Props) => { const [havingWiki, setHavingWiki] = useState (true) - const wikiExists = async (tagName: string) => { + const wikiExists = async (tag: Tag) => { + if ('hasWiki' in tag) + { + setHavingWiki (tag.hasWiki) + return + } + + const tagName = (tag as Tag).name + try { await axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (tagName) }/exists`) @@ -48,7 +56,7 @@ export default (({ tag, if (!(linkFlg) || !(withWiki)) return - wikiExists (tag.name) + wikiExists (tag) }, [tag.name, linkFlg, withWiki]) const spanClass = cn ( diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 0dccb59..a2f57b7 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -48,6 +48,7 @@ export type Tag = { name: string category: Category postCount: number + hasWiki: boolean children?: Tag[] } export type User = {