コミットを比較

...

5 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 7df51fb34b #140 ぼちぼち 2025-12-07 17:21:58 +09:00
みてるぞ 4e00ec40ab Merge remote-tracking branch 'origin/main' into feature/140 2025-12-07 13:09:49 +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個のファイルの変更80行の追加45行の削除
+2 -1
ファイルの表示
@@ -110,7 +110,8 @@ 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)
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] } }), render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
status: :ok status: :ok
+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
ファイルの表示
@@ -57,7 +57,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
生成ファイル
+20 -2
ファイルの表示
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do ActiveRecord::Schema[8.0].define(version: 2025_11_26_231500) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@@ -40,7 +40,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) 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_09_09_075500) 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
@@ -107,6 +114,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do
t.index ["tag_id"], name: "index_tag_aliases_on_tag_id" t.index ["tag_id"], name: "index_tag_aliases_on_tag_id"
end end
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "tag_id", null: false
t.bigint "parent_tag_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["parent_tag_id"], name: "index_tag_implications_on_parent_tag_id"
t.index ["tag_id"], name: "index_tag_implications_on_tag_id"
end
create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "tag_id", null: false t.bigint "tag_id", null: false
t.bigint "target_tag_id", null: false t.bigint "target_tag_id", null: false
@@ -176,6 +192,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do
add_foreign_key "posts", "users", column: "uploaded_user_id" add_foreign_key "posts", "users", column: "uploaded_user_id"
add_foreign_key "settings", "users" add_foreign_key "settings", "users"
add_foreign_key "tag_aliases", "tags" add_foreign_key "tag_aliases", "tags"
add_foreign_key "tag_implications", "tags"
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
add_foreign_key "tag_similarities", "tags" add_foreign_key "tag_similarities", "tags"
add_foreign_key "tag_similarities", "tags", column: "target_tag_id" add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
add_foreign_key "user_ips", "ip_addresses" add_foreign_key "user_ips", "ip_addresses"
-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
+8 -4
ファイルの表示
@@ -1,15 +1,16 @@
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom' import { createPath, useNavigate } from 'react-router-dom'
import { useOverlayStore } from '@/components/RouteBlockerOverlay' import { useOverlayStore } from '@/components/RouteBlockerOverlay'
import { prefetchForURL } from '@/lib/prefetchers' import { prefetchForURL } from '@/lib/prefetchers'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { AnchorHTMLAttributes, FC, MouseEvent, TouchEvent } from 'react' import type { AnchorHTMLAttributes, FC, MouseEvent, TouchEvent } from 'react'
import type { To } from 'react-router-dom'
type Props = AnchorHTMLAttributes<HTMLAnchorElement> & { type Props = AnchorHTMLAttributes<HTMLAnchorElement> & {
to: string to: To
replace?: boolean replace?: boolean
className?: string className?: string
cancelOnError?: boolean } cancelOnError?: boolean }
@@ -25,7 +26,10 @@ export default (({ to,
...rest }: Props) => { ...rest }: Props) => {
const navigate = useNavigate () const navigate = useNavigate ()
const qc = useQueryClient () const qc = useQueryClient ()
const url = useMemo (() => (new URL (to, location.origin)).toString (), [to]) const url = useMemo (() => {
const path = (typeof to === 'string') ? to : createPath (to)
return (new URL (path, location.origin)).toString ()
}, [to])
const setOverlay = useOverlayStore (s => s.setActive) const setOverlay = useOverlayStore (s => s.setActive)
const doPrefetch = async () => { const doPrefetch = async () => {
@@ -74,7 +78,7 @@ export default (({ to,
} }
return ( return (
<a href={to} <a href={typeof to === 'string' ? to : createPath (to)}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onClick={handleClick} onClick={handleClick}
+15 -4
ファイルの表示
@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink'
import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts' import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -9,7 +10,8 @@ import type { Tag } from '@/types'
type CommonProps = { tag: Tag type CommonProps = { tag: Tag
withWiki?: boolean withWiki?: boolean
withCount?: boolean } withCount?: boolean
prefetch?: boolean }
type PropsWithLink = type PropsWithLink =
CommonProps & { linkFlg?: true } & Partial<ComponentProps<typeof Link>> CommonProps & { linkFlg?: true } & Partial<ComponentProps<typeof Link>>
@@ -24,6 +26,7 @@ export default (({ tag,
linkFlg = true, linkFlg = true,
withWiki = true, withWiki = true,
withCount = true, withCount = true,
prefetch = false,
...props }: Props) => { ...props }: Props) => {
const spanClass = cn ( const spanClass = cn (
`text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`, `text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`,
@@ -44,11 +47,19 @@ export default (({ tag,
</span>)} </span>)}
{linkFlg {linkFlg
? ( ? (
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} prefetch
? <PrefetchLink
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
className={linkClass} className={linkClass}
{...props}> {...props}>
{tag.name} {tag.name}
</Link>) </PrefetchLink>
: <Link
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
className={linkClass}
{...props}>
{tag.name}
</Link>)
: ( : (
<span className={spanClass} <span className={spanClass}
{...props}> {...props}>
+7 -6
ファイルの表示
@@ -10,16 +10,17 @@ import { API_BASE_URL } from '@/config'
import { CATEGORIES } from '@/consts' import { CATEGORIES } from '@/consts'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { FC } from 'react' import type { FC, MouseEvent } from 'react'
import type { Post, Tag } from '@/types' import type { Post, Tag } from '@/types'
type TagByCategory = Record<string, Tag[]> type TagByCategory = Record<string, Tag[]>
type Props = { posts: Post[] } type Props = { posts: Post[]
onClick?: (event: MouseEvent<HTMLElement>) => void }
export default (({ posts }: Props) => { export default (({ posts, onClick }: Props) => {
const navigate = useNavigate () const navigate = useNavigate ()
const [tagsVsbl, setTagsVsbl] = useState (false) const [tagsVsbl, setTagsVsbl] = useState (false)
@@ -64,11 +65,11 @@ export default (({ posts }: Props) => {
<div className={cn (!(tagsVsbl) && 'hidden', 'md:block mt-4')}> <div className={cn (!(tagsVsbl) && 'hidden', 'md:block mt-4')}>
<SectionTitle></SectionTitle> <SectionTitle></SectionTitle>
<ul> <ul>
{CATEGORIES.flatMap (cat => cat in tags ? ( {CATEGORIES.flatMap (cat => cat in tags ?
tags[cat].map (tag => ( tags[cat].map (tag => (
<li key={tag.id} className="mb-1"> <li key={tag.id} className="mb-1">
<TagLink tag={tag}/> <TagLink tag={tag} prefetch onClick={onClick}/>
</li>))) : [])} </li>)) : [])}
</ul> </ul>
<SectionTitle></SectionTitle> <SectionTitle></SectionTitle>
{posts.length > 0 && ( {posts.length > 0 && (
+13 -8
ファイルの表示
@@ -10,6 +10,7 @@ import WikiBody from '@/components/WikiBody'
import TabGroup, { Tab } from '@/components/common/TabGroup' import TabGroup, { Tab } from '@/components/common/TabGroup'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { API_BASE_URL, SITE_TITLE } from '@/config' import { API_BASE_URL, SITE_TITLE } from '@/config'
import { fetchPosts } from '@/lib/posts'
import type { Post, WikiPage } from '@/types' import type { Post, WikiPage } from '@/types'
@@ -28,13 +29,11 @@ export default () => {
const loadMore = async (withCursor: boolean) => { const loadMore = async (withCursor: boolean) => {
setLoading (true) setLoading (true)
const res = await axios.get (`${ API_BASE_URL }/posts`, { const data = await fetchPosts ({
params: { tags: tags.join (' '), tags: tags.join (' '),
match: anyFlg ? 'any' : 'all', match: anyFlg ? 'any' : 'all',
limit: '20', limit: 20,
...(withCursor && { cursor }) } }) ...(withCursor && { cursor }) })
const data = toCamel (res.data as any, { deep: true }) as { posts: Post[]
nextCursor: string }
setPosts (posts => ( setPosts (posts => (
[...((new Map ([...(withCursor ? posts : []), ...data.posts] [...((new Map ([...(withCursor ? posts : []), ...data.posts]
.map (post => [post.id, post]))) .map (post => [post.id, post])))
@@ -111,7 +110,13 @@ export default () => {
</title> </title>
</Helmet> </Helmet>
<TagSidebar posts={posts.slice (0, 20)}/> <TagSidebar posts={posts.slice (0, 20)} onClick={() => {
const statesToSave = {
posts, cursor,
scroll: containerRef.current?.scrollTop ?? 0 }
sessionStorage.setItem (`posts:${ tagsQuery }`,
JSON.stringify (statesToSave))
}}/>
<MainArea> <MainArea>
<TabGroup> <TabGroup>