From 3c89d14636a869207163b2a21a869df7dba3593e Mon Sep 17 00:00:00 2001 From: miteruzo Date: Fri, 11 Jul 2025 02:05:16 +0900 Subject: [PATCH] #77 --- .../app/controllers/nico_tags_controller.rb | 24 ++++ backend/app/controllers/posts_controller.rb | 8 +- backend/app/models/nico_tag_relation.rb | 10 +- backend/app/models/tag.rb | 14 ++- backend/config/routes.rb | 2 + frontend/src/App.tsx | 2 + frontend/src/pages/tags/NicoTagListPage.tsx | 81 +++++++++++++ frontend/src/pages/wiki/WikiSearchPage.tsx | 111 +++++++++--------- frontend/src/types.ts | 5 + 9 files changed, 191 insertions(+), 66 deletions(-) create mode 100644 backend/app/controllers/nico_tags_controller.rb create mode 100644 frontend/src/pages/tags/NicoTagListPage.tsx diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb new file mode 100644 index 0000000..a3c611d --- /dev/null +++ b/backend/app/controllers/nico_tags_controller.rb @@ -0,0 +1,24 @@ +class NicoTagsController < ApplicationController + def index + limit = (params[:limit] || 20).to_i + cursor = params[:cursor].presence + + q = Tag.nico_tags.includes(:linked_tags) + q = q.where('tags.updated_at < ?', Time.iso8601(cursor)) if cursor + + tags = q.limit(limit + 1) + + next_cursor = nil + if tags.size > limit + next_cursor = tags.last.updated_at.iso8601(6) + tags = tags.first(limit) + end + + render json: { tags: tags.map { |tag| + tag.as_json(include: :linked_tags) + }, next_cursor: } + end + + def upload + end +end diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 2deaf0f..f401882 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -6,14 +6,10 @@ class PostsController < ApplicationController # GET /posts def index limit = (params[:limit] || 20).to_i - cursor = params[:cursor] + cursor = params[:cursor].presence q = filtered_posts.order(created_at: :desc) - - next_cursor = nil - if cursor.present? - q = q.where('posts.created_at < ?', Time.iso8601(cursor)) - end + q = q.where('posts.created_at < ?', Time.iso8601(cursor)) if cursor posts = q.limit(limit + 1) diff --git a/backend/app/models/nico_tag_relation.rb b/backend/app/models/nico_tag_relation.rb index 7156dec..ff4f3a6 100644 --- a/backend/app/models/nico_tag_relation.rb +++ b/backend/app/models/nico_tag_relation.rb @@ -1,6 +1,6 @@ class NicoTagRelation < ApplicationRecord - belongs_to :nico_tag, class_name: 'Tag', foreign_key: 'nico_tag_id' - belongs_to :tag, class_name: 'Tag', foreign_key: 'tag_id' + belongs_to :nico_tag, class_name: 'Tag' + belongs_to :tag, class_name: 'Tag' validates :nico_tag_id, presence: true validates :tag_id, presence: true @@ -14,4 +14,10 @@ class NicoTagRelation < ApplicationRecord errors.add :nico_tag_id, 'タグのカテゴリがニコニコである必要があります.' end end + + def tag_mustnt_be_nico + if tag && tag.category == 'nico' + errors.add :tag_id, '連携先タグのカテゴリはニコニコであってはなりません.' + end + end end diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index f848d58..a8239f6 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -2,7 +2,14 @@ class Tag < ApplicationRecord has_many :post_tags, dependent: :destroy has_many :posts, through: :post_tags has_many :tag_aliases, dependent: :destroy - has_many :wiki_pages, dependent: :nullify + + has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy + has_many :linked_tags, through: :nico_tag_relations, source: :tag + + has_many :reversed_nico_tag_relations, class_name: 'NicoTagRelation', + foreign_key: :tag_id, + dependent: :destroy + has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag enum :category, { deerjikist: 'deerjikist', meme: 'meme', @@ -17,7 +24,7 @@ class Tag < ApplicationRecord validate :nico_tag_name_must_start_with_nico - scope :nico_tags, -> { where category: :nico } + scope :nico_tags, -> { where(category: :nico) } def self.tagme @tagme ||= Tag.find_or_initialize_by(name: 'タグ希望') do |tag| @@ -34,7 +41,8 @@ class Tag < ApplicationRecord private def nico_tag_name_must_start_with_nico - if category == 'nico' && name&.[](0, 5) != 'nico:' + if ((category == 'nico' && name&.[](0, 5) != 'nico:') || + (category != 'nico' && name&.[](0, 5) == 'nico:')) errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.' end end diff --git a/backend/config/routes.rb b/backend/config/routes.rb index e65020c..922b63c 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -1,4 +1,6 @@ Rails.application.routes.draw do + get 'tags/nico', to: 'nico_tags#index' + put 'tags/nico/:id', to: 'nico_tags#update' get 'tags/autocomplete', to: 'tags#autocomplete' get 'tags/name/:name', to: 'tags#show_by_name' get 'posts/random', to: 'posts#random' diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8394017..76a130d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import TagSidebar from '@/components/TagSidebar' import TopNav from '@/components/TopNav' import { Toaster } from '@/components/ui/toaster' import { API_BASE_URL } from '@/config' +import NicoTagListPage from '@/pages/tags/NicoTagListPage' import NotFound from '@/pages/NotFound' import PostDetailPage from '@/pages/posts/PostDetailPage' import PostListPage from '@/pages/posts/PostListPage' @@ -62,6 +63,7 @@ export default () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/tags/NicoTagListPage.tsx b/frontend/src/pages/tags/NicoTagListPage.tsx new file mode 100644 index 0000000..44f2e46 --- /dev/null +++ b/frontend/src/pages/tags/NicoTagListPage.tsx @@ -0,0 +1,81 @@ +import axios from 'axios' +import toCamel from 'camelcase-keys' +import { useEffect, useState } from 'react' +import { Helmet } from 'react-helmet-async' +import { Link } from 'react-router-dom' + +import SectionTitle from '@/components/common/SectionTitle' +import TextArea from '@/components/common/TextArea' +import MainArea from '@/components/layout/MainArea' +import { API_BASE_URL, SITE_TITLE } from '@/config' + +import type { NicoTag } from '@/types' + + +export default () => { + const [nicoTags, setNicoTags] = useState ([]) + const [editing, setEditing] = useState<{ [key: number]: boolean }> ({ }) + const [rawTags, setRawTags] = useState<{ [key: number]: string }> ({ }) + + useEffect (() => { + void (async () => { + const res = await axios.get (`${ API_BASE_URL }/tags/nico`) + const data = toCamel (res.data, { deep: true }) + + setNicoTags (data.tags) + + const newEditing = Object.fromEntries (data.tags.map (t => [t.id, false])) + setEditing (editing => ({ ...editing, ...newEditing })) + + const newRawTags = Object.fromEntries ( + data.tags.map (t => [t.id, t.linkedTags.map (lt => lt.name).join (' ')])) + setRawTags (rawTags => ({ ...rawTags, ...newRawTags })) + }) () + }, []) + + return ( + + + ニコニコ連携 | {SITE_TITLE} + + +
+ ニコニコ連携 +
+ +
+ + + + + + + + + + {nicoTags.map (tag => ( + + +
ニコニコタグ連携タグ
+ {tag.name} + + {editing[tag.id] + ? ( +