コミットを比較

...

5 コミット

作成者 SHA1 メッセージ 日付
みてるぞ bae03cf918 #64 おそらく完成 2025-12-09 01:43:05 +09:00
みてるぞ ff05d6e213 Merge remote-tracking branch 'origin/main' into feature/064 2025-12-08 12:24:46 +09:00
みてるぞ 06cd569fc5 ニコタグ一括連携の削除(#166) (#167)
'backend/lib/tasks/link_nico.rake' を削除

Reviewed-on: #167
2025-12-07 12:28:43 +09:00
みてるぞ 8a126e2e92 feat: ユーザに IP アドレスを紐づけ(#29) (#165)
#29 対応完了

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #165
2025-11-26 23:48:45 +09:00
みてるぞ 5cc47e42e1 feat: タグ希望タグを新規時のみにする(#128) (#145)
#128 完了

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #145
2025-11-26 22:33:50 +09:00
12個のファイルの変更137行の追加48行の削除
+53 -6
ファイルの表示
@@ -52,9 +52,12 @@ class PostsController < ApplicationController
viewed = current_user&.viewed?(post) || false viewed = current_user&.viewed?(post) || false
render json: (post json = post.as_json
.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }) json['tags'] = build_tag_tree_for(post.tags)
.merge(related: post.related(limit: 20), viewed:)) json['related'] = post.related(limit: 20)
json['viewed'] = viewed
render json:
end end
# POST /posts # POST /posts
@@ -111,11 +114,13 @@ class PostsController < ApplicationController
original_created_before = params[:original_created_before] original_created_before = params[:original_created_before]
post = Post.find(params[:id].to_i) 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)
tags = Tag.expand_parent_tags(tags) tags = Tag.expand_parent_tags(tags)
if post.update(title:, tags:, original_created_from:, original_created_before:) if post.update(title:, tags:, original_created_from:, original_created_before:)
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), json = post.as_json
status: :ok json['tags'] = build_tag_tree_for(post.tags)
render json:, status: :ok
else else
render json: post.errors, status: :unprocessable_entity render json: post.errors, status: :unprocessable_entity
end end
@@ -144,4 +149,46 @@ class PostsController < ApplicationController
end end
posts.distinct posts.distinct
end 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 end
+8 -5
ファイルの表示
@@ -6,12 +6,15 @@ class UsersController < ApplicationController
end end
def verify 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]) user = User.find_by(inheritance_code: params[:code])
render json: if user return render json: { valid: false } unless user
{ valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
else UserIp.find_or_create_by!(user:, ip_address:)
{ valid: false }
end render json: { valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
end end
def renew def renew
+1 -1
ファイルの表示
@@ -65,7 +65,7 @@ class Tag < ApplicationRecord
end end
end 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 tags.uniq
end end
-1
ファイルの表示
@@ -8,7 +8,6 @@ class User < ApplicationRecord
has_many :posts has_many :posts
has_many :settings has_many :settings
has_many :ip_addresses
has_many :user_ips, dependent: :destroy has_many :user_ips, dependent: :destroy
has_many :ip_addresses, through: :user_ips has_many :ip_addresses, through: :user_ips
has_many :user_post_views, dependent: :destroy has_many :user_post_views, dependent: :destroy
+5
ファイルの表示
@@ -0,0 +1,5 @@
class RenameIpAdressColumnToIpAddresses < ActiveRecord::Migration[8.0]
def change
rename_column :ip_addresses, :ip_adress, :ip_address
end
end
生成ファイル
+8 -1
ファイルの表示
@@ -40,7 +40,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_09_222200) do
end end
create_table "ip_addresses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| 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.boolean "banned", default: false, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@@ -70,9 +70,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_09_222200) do
t.bigint "deleted_user_id" t.bigint "deleted_user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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 ["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 ["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 ["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" t.index ["tag_id"], name: "index_post_tags_on_tag_id"
end end
-12
ファイルの表示
@@ -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
+1 -1
ファイルの表示
@@ -51,7 +51,7 @@ namespace :nico do
end end
tags_to_add.concat([tag] + tag.linked_tags) tags_to_add.concat([tag] + tag.linked_tags)
end 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 tags_to_add << Tag.bot
post.tags = (post.tags + tags_to_add).uniq post.tags = (post.tags + tags_to_add).uniq
end end
+23 -6
ファイルの表示
@@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
import toCamel from 'camelcase-keys' import toCamel from 'camelcase-keys'
import { useState } from 'react' import { useEffect, useState } from 'react'
import PostFormTagsArea from '@/components/PostFormTagsArea' import PostFormTagsArea from '@/components/PostFormTagsArea'
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
@@ -10,7 +10,23 @@ import { API_BASE_URL } from '@/config'
import type { FC } from 'react' 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 type Props = { post: Post
onSave: (newPost: Post) => void } onSave: (newPost: Post) => void }
@@ -22,10 +38,7 @@ export default (({ post, onSave }: Props) => {
const [originalCreatedFrom, setOriginalCreatedFrom] = const [originalCreatedFrom, setOriginalCreatedFrom] =
useState<string | null> (post.originalCreatedFrom) useState<string | null> (post.originalCreatedFrom)
const [title, setTitle] = useState (post.title) const [title, setTitle] = useState (post.title)
const [tags, setTags] = useState<string> (post.tags const [tags, setTags] = useState<string> ('')
.filter (t => t.category !== 'nico')
.map (t => t.name)
.join (' '))
const handleSubmit = async () => { const handleSubmit = async () => {
const res = await axios.put ( const res = await axios.put (
@@ -43,6 +56,10 @@ export default (({ post, onSave }: Props) => {
originalCreatedBefore: data.originalCreatedBefore } as Post) originalCreatedBefore: data.originalCreatedBefore } as Post)
} }
useEffect (() => {
setTags(tagsToStr (post.tags))
}, [post])
return ( return (
<div className="max-w-xl pt-2 space-y-4"> <div className="max-w-xl pt-2 space-y-4">
{/* タイトル */} {/* タイトル */}
+20 -5
ファイルの表示
@@ -7,12 +7,30 @@ import SubsectionTitle from '@/components/common/SubsectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent' import SidebarComponent from '@/components/layout/SidebarComponent'
import { CATEGORIES } from '@/consts' import { CATEGORIES } from '@/consts'
import type { FC } from 'react' import type { FC, ReactNode } from 'react'
import type { Category, Post, Tag } from '@/types' import type { Category, Post, Tag } from '@/types'
type TagByCategory = { [key in Category]: Tag[] } type TagByCategory = { [key in Category]: Tag[] }
const renderTagTree = (
tag: Tag,
nestLevel: number,
path: string,
): ReactNode[] => {
const key = `${ path }-${ tag.id }`
const self = (
<li key={key} className="mb-1">
<TagLink tag={tag} nestLevel={nestLevel}/>
</li>)
return [self,
...(tag.children?.flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])]
}
type Props = { post: Post | null } type Props = { post: Post | null }
@@ -54,10 +72,7 @@ export default (({ post }: Props) => {
<div className="my-3" key={cat}> <div className="my-3" key={cat}>
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> <SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
<ul> <ul>
{tags[cat].map (tag => ( {tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))}
<li key={tag.id} className="mb-1">
<TagLink tag={tag}/>
</li>))}
</ul> </ul>
</div>))} </div>))}
{post && ( {post && (
+8
ファイルの表示
@@ -8,6 +8,7 @@ import type { ComponentProps, FC, HTMLAttributes } from 'react'
import type { Tag } from '@/types' import type { Tag } from '@/types'
type CommonProps = { tag: Tag type CommonProps = { tag: Tag
nestLevel?: number
withWiki?: boolean withWiki?: boolean
withCount?: boolean } withCount?: boolean }
@@ -21,6 +22,7 @@ type Props = PropsWithLink | PropsWithoutLink
export default (({ tag, export default (({ tag,
nestLevel = 0,
linkFlg = true, linkFlg = true,
withWiki = true, withWiki = true,
withCount = true, withCount = true,
@@ -42,6 +44,12 @@ export default (({ tag,
? ?
</Link> </Link>
</span>)} </span>)}
{nestLevel > 0 && (
<span
className="ml-1 mr-1"
style={{ paddingLeft: `${ (nestLevel - 1) }rem` }}>
</span>)}
{linkFlg {linkFlg
? ( ? (
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} <Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
+10 -10
ファイルの表示
@@ -1,7 +1,7 @@
import React from 'react'
import { CATEGORIES, USER_ROLES, ViewFlagBehavior } from '@/consts' import { CATEGORIES, USER_ROLES, ViewFlagBehavior } from '@/consts'
import type { ReactNode } from 'react'
export type Category = typeof CATEGORIES[number] export type Category = typeof CATEGORIES[number]
export type Menu = MenuItem[] export type Menu = MenuItem[]
@@ -29,19 +29,19 @@ export type Post = {
originalCreatedFrom: string | null originalCreatedFrom: string | null
originalCreatedBefore: string | null } originalCreatedBefore: string | null }
export type SubMenuItem = { export type SubMenuItem =
component: React.ReactNode | { component: ReactNode
visible: boolean visible: boolean }
} | { | { name: string
name: string to: string
to: string visible?: boolean }
visible?: boolean }
export type Tag = { export type Tag = {
id: number id: number
name: string name: string
category: Category category: Category
postCount: number } postCount: number
children?: Tag[] }
export type User = { export type User = {
id: number id: number