From 9be4bb15328d72dde42bfc00eb1646eec10806bf Mon Sep 17 00:00:00 2001 From: miteruzo Date: Wed, 9 Jul 2025 06:54:19 +0900 Subject: [PATCH] #60 --- backend/app/controllers/posts_controller.rb | 47 +++- backend/app/models/post.rb | 2 + frontend/src/components/PostEditForm.tsx | 99 ++++---- frontend/src/components/TopNav.tsx | 13 +- frontend/src/components/common/TextArea.tsx | 10 + frontend/src/pages/posts/PostDetailPage.tsx | 2 +- frontend/src/pages/posts/PostNewPage.tsx | 244 +++++++++----------- 7 files changed, 208 insertions(+), 209 deletions(-) create mode 100644 frontend/src/components/common/TextArea.tsx diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 00618a1..82af00b 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -53,19 +53,30 @@ class PostsController < ApplicationController # POST /posts def create return head :unauthorized unless current_user - return head :forbidden unless ['admin', 'member'].include?(current_user.role) + return head :forbidden unless current_user.member? # TODO: URL が正規のものがチェック,不正ならエラー + # TODO: title、URL は必須にする. + # TODO: サイトに応じて thumbnail_base 設定 title = params[:title] - post = Post.new(title:, url: params[:url], thumbnail_base: '', uploaded_user: current_user) - post.thumbnail.attach(params[:thumbnail]) + url = params[:url] + thumbnail = params[:thumbnail] + tag_names = params[:tags].to_s.split(' ') + if tag_names.size < 20 && tag_names.none?('タグ希望') + tag_names << 'タグ希望' + end + + post = Post.new(title:, url:, thumbnail_base: '', uploaded_user: current_user) + post.thumbnail.attach(thumbnail) if post.save post.resized_thumbnail! - if params[:tags].present? - tag_ids = JSON.parse(params[:tags]) - post.tags = Tag.where(id: tag_ids) - end - render json: post, status: :created + # TODO: 接頭辞に応じて category 変へる + post.tags = tag_names.map { |name| Tag.find_or_initialize_by(name:) { |tag| + tag.category = 'general' + } } + + render json: post.as_json(include: { tags: { only: [:id, :name, :category] } }), + status: :created else render json: { errors: post.errors.full_messages }, status: :unprocessable_entity end @@ -88,11 +99,23 @@ class PostsController < ApplicationController # PATCH/PUT /posts/1 def update return head :unauthorized unless current_user - return head :forbidden unless ['admin', 'member'].include?(current_user.role) + return head :forbidden unless current_user.member? - post = Post.find(params[:id]) - tag_ids = JSON.parse(params[:tags]) - if post.update(title: params[:title], tags: Tag.where(id: tag_ids)) + title = params[:title] + tag_names = params[:tags].to_s.split(' ') + if tag_names.size < 20 && tag_names.none?('タグ希望') + tag_names << 'タグ希望' + end + + post = Post.find(params[:id].to_i) + tags = post.tags.where(category: 'nico').to_a + tag_names.each do |name| + # TODO: 接頭辞に応じて category 変へる + tags << Tag.find_or_initialize_by(name:) { |tag| + tag.category = 'general' + } + end + if post.update(title:, tags:) render({ json: (post .as_json(include: { tags: { only: [:id, :name, :category] } })), status: :created }) diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index 89d49d2..6934025 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -14,6 +14,8 @@ class Post < ApplicationRecord Rails.application.routes.url_helpers.rails_blob_url( thumbnail, only_path: false) : nil }) + rescue + super(options).merge(thumbnail: nil) end def resized_thumbnail! diff --git a/frontend/src/components/PostEditForm.tsx b/frontend/src/components/PostEditForm.tsx index c3e06f5..2de0d7e 100644 --- a/frontend/src/components/PostEditForm.tsx +++ b/frontend/src/components/PostEditForm.tsx @@ -1,73 +1,56 @@ -import React, { useEffect, useState } from 'react' import axios from 'axios' -import { API_BASE_URL } from '@/config' +import React, { useEffect, useState } from 'react' + +import TextArea from '@/components/common/TextArea' import { Button } from '@/components/ui/button' +import { API_BASE_URL } from '@/config' import type { Post, Tag } from '@/types' -type Props = { post: Post - onSave: (newPost: Post) => void } +type Props = { post: Post + onSave: (newPost: Post) => void } export default ({ post, onSave }: Props) => { const [title, setTitle] = useState (post.title) - const [tags, setTags] = useState ([]) - const [tagIds, setTagIds] = useState (post.tags.map (t => t.id)) - - const handleSubmit = () => { - void (axios.put (`${ API_BASE_URL }/posts/${ post.id }`, - { title, - tags: JSON.stringify (tagIds) }, - { headers: { 'Content-Type': 'multipart/form-data', - 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } } ) - .then (res => { - const newPost: Post = res.data - onSave ({ ...post, - title: newPost.title, - tags: newPost.tags } as Post) - })) + const [tags, setTags] = useState (post.tags + .filter (t => t.category !== 'nico') + .map (t => t.name) + .join (' ')) + + const handleSubmit = async () => { + const { data } = await axios.put (`${ API_BASE_URL }/posts/${ post.id }`, { title, tags }, + { headers: { 'Content-Type': 'multipart/form-data', + 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } } ) + onSave ({ ...post, + title: data.title, + tags: data.tags } as Post) } - useEffect (() => { - void (axios.get ('/api/tags') - .then (res => setTags (res.data)) - .catch (() => toast ({ title: 'タグ一覧の取得失敗' }))) - }, []) - return (
- {/* タイトル */} -
-
- -
- setTitle (e.target.value)} /> -
- - {/* タグ */} -
- - -
- - {/* 送信 */} - + {/* タイトル */} +
+
+ +
+ setTitle (e.target.value)} /> +
+ + {/* タグ */} +
+ +