diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 41e0520..de2c9fd 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -36,37 +36,16 @@ class PostsController < ApplicationController # POST /posts def create + logger.info ">>> thumbnail: #{params[:thumbnail]&.content_type}" +logger.info ">>> filename: #{params[:thumbnail]&.original_filename}" + return head :unauthorized unless current_user return head :forbidden unless ['admin', 'member'].include?(current_user.role) # TODO: URL が正規のものがチェック,不正ならエラー title = params[:title] - unless title.present? - # TODO: # 既知サイトなら決まったフォーマットで title 取得するやぅに. - begin - html = URI.open(params[:url], open_timeout: 5, read_timeout: 5).read - doc = Nokogiri::HTML.parse(html) - title = doc.at('title')&.text&.strip || '' - rescue - title = '' - end - end post = Post.new(title: title, url: params[:url], thumbnail_base: '', uploaded_user: current_user) - if params[:thumbnail].present? - post.thumbnail.attach(params[:thumbnail]) - else - # TODO: 既知ドメインであれば指定のアドレスからサムネールを取得するやぅにする. - path = Rails.root.join('tmp', "thumb_#{ SecureRandom.hex }.png") - system("node #{ Rails.root }/lib/screenshot.js #{ Shellwords.escape(params[:url]) } #{ path }") - if File.exist?(path) - image = MiniMagick::Image.open(path) - image.resize '180x180' - post.thumbnail.attach(io: File.open(image.path), - filename: 'thumbnail.png', - content_type: 'image/png') - File.delete(path) rescue nil - end - end + post.thumbnail.attach(params[:thumbnail]) if post.save post.resized_thumbnail! if params[:tags].present? diff --git a/backend/app/controllers/preview_controller.rb b/backend/app/controllers/preview_controller.rb new file mode 100644 index 0000000..0c80354 --- /dev/null +++ b/backend/app/controllers/preview_controller.rb @@ -0,0 +1,37 @@ +class PreviewController < ApplicationController + def title + # TODO: # 既知サイトなら決まったフォーマットで title 取得するやぅに. + return head :unauthorized unless current_user + + url = params[:url] + return head :bad_request unless url.present? + + html = URI.open(url, open_timeout: 5, read_timeout: 5).read + doc = Nokogiri::HTML.parse(html) + title = doc.at('title')&.text&.strip + + render json: { title: title } + rescue => e + render json: { error: e.message }, status: :bad_request + end + + def thumbnail + # TODO: 既知ドメインであれば指定のアドレスからサムネールを取得するやぅにする. + return head :unauthorized unless current_user + + url = params[:url] + return head :bad_request unless url.present? + + path = Rails.root.join('tmp', "thumb_#{ SecureRandom.hex }.png") + system("node #{ Rails.root }/lib/screenshot.js #{ Shellwords.escape(url) } #{ path }") + + if File.exist?(path) + image = MiniMagick::Image.open(path) + image.resize '180x180' + File.delete(path) rescue nil + send_file image.path, type: 'image/png', disposition: 'inline' + else + render json: { error: 'Failed to generate thumbnail' }, status: :internal_server_error + end + end +end diff --git a/backend/config/routes.rb b/backend/config/routes.rb index a689b21..3bfc4da 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -52,6 +52,8 @@ Rails.application.routes.draw do get "ip_addresses/create" get "ip_addresses/update" get "ip_addresses/destroy" + get 'preview/title', to: 'preview#title' + get 'preview/thumbnail', to: 'preview#thumbnail' root 'home#index' resources :posts diff --git a/backend/lib/screenshot.js b/backend/lib/screenshot.js index dc976ba..cef1da0 100644 --- a/backend/lib/screenshot.js +++ b/backend/lib/screenshot.js @@ -11,7 +11,7 @@ void (async () => { const page = await browser.newPage () await page.setViewport ({ width: 960, height: 960 }) - await page.goto (url, { waitUntil: 'networkidle2', timeout: 10000 }) + await page.goto (url, { waitUntil: 'networkidle2', timeout: 15000 }) await page.screenshot ({ path: output }) await browser.close () diff --git a/backend/test/controllers/preview_controller_test.rb b/backend/test/controllers/preview_controller_test.rb new file mode 100644 index 0000000..5cc632d --- /dev/null +++ b/backend/test/controllers/preview_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class PreviewControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/frontend/src/pages/PostNewPage.tsx b/frontend/src/pages/PostNewPage.tsx index 24b2599..9aba795 100644 --- a/frontend/src/pages/PostNewPage.tsx +++ b/frontend/src/pages/PostNewPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useRef } from 'react' import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' import axios from 'axios' import { API_BASE_URL, SITE_TITLE } from '../config' @@ -28,21 +28,22 @@ const PostNewPage = () => { const [title, setTitle] = useState ('') const [titleAutoFlg, setTitleAutoFlg] = useState (true) + const [titleLoading, setTitleLoading] = useState (false) const [url, setURL] = useState ('') const [thumbnailFile, setThumbnailFile] = useState (null) const [thumbnailPreview, setThumbnailPreview] = useState ('') const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true) + const [thumbnailLoading, setThumbnailLoading] = useState (false) const [tags, setTags] = useState ([]) const [tagIds, setTagIds] = useState ([]) + const previousURLRef = useRef ('') const handleSubmit = () => { const formData = new FormData () - if (title) - formData.append ('title', title) + formData.append ('title', title) formData.append ('url', url) formData.append ('tags', JSON.stringify (tagIds)) - if (!(thumbnailAutoFlg) && thumbnailFile) - formData.append ('thumbnail', thumbnailFile) + formData.append ('thumbnail', thumbnailFile) void (axios.post (`${ API_BASE_URL }/posts`, formData, { headers: { 'Content-Type': 'multipart/form-data', @@ -63,6 +64,61 @@ const PostNewPage = () => { .catch (() => toast ({ title: 'タグ一覧の取得失敗' }))) }, []) + useEffect (() => { + if (titleAutoFlg && url) + fetchTitle () + }, [titleAutoFlg]) + + useEffect (() => { + if (thumbnailAutoFlg && url) + fetchThumbnail () + }, [thumbnailAutoFlg]) + + const handleURLBlur = () => { + if (!(url) || url === previousURLRef.current) + return + + if (titleAutoFlg) + fetchTitle () + if (thumbnailAutoFlg) + fetchThumbnail () + previousURLRef.current = url + } + + const fetchTitle = () => { + setTitle ('') + setTitleLoading (true) + void (axios.get (`${ API_BASE_URL }/preview/title`, { + params: { url }, + headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) + .then (res => { + setTitle (res.data.title || '') + setTitleLoading (false) + }) + .finally (() => setTitleLoading (false))) + } + + const fetchThumbnail = () => { + setThumbnailPreview ('') + setThumbnailFile (null) + setThumbnailLoading (true) + if (thumbnailPreview) + URL.revokeObjectURL (thumbnailPreview) + void (axios.get (`${ API_BASE_URL }/preview/thumbnail`, { + params: { url }, + headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' }, + responseType: 'blob' }) + .then (res => { + const imageURL = URL.createObjectURL (res.data) + setThumbnailPreview (imageURL) + setThumbnailFile (new File ([res.data], + 'thumbnail.png', + { type: res.data.type || 'image/png' })) + setThumbnailLoading (false) + }) + .finally (() => setThumbnailLoading (false))) + } + return (

広場に投稿を追加する

@@ -74,7 +130,8 @@ const PostNewPage = () => { placeholder="例:https://www.nicovideo.jp/watch/..." value={url} onChange={e => setURL (e.target.value)} - className="w-full border p-2 rounded" /> + className="w-full border p-2 rounded" + onBlur={handleURLBlur} />
{/* タイトル */} @@ -91,6 +148,7 @@ const PostNewPage = () => { setTitle (e.target.value)} disabled={titleAutoFlg} /> @@ -107,27 +165,27 @@ const PostNewPage = () => { {thumbnailAutoFlg - ? ( -

- URL から自動取得されます。 -

) + ? (thumbnailLoading + ?

Loading...

+ : !(thumbnailPreview) && ( +

+ URL から自動取得されます。 +

)) : ( - <> - { - const file = e.target.files?.[0] - if (file) - { - setThumbnailFile (file) - setThumbnailPreview (URL.createObjectURL (file)) - } - }} /> - {thumbnailPreview && ( - preview)} - )} + { + const file = e.target.files?.[0] + if (file) + { + setThumbnailFile (file) + setThumbnailPreview (URL.createObjectURL (file)) + } + }} />)} + {thumbnailPreview && ( + preview)} {/* タグ */} @@ -149,7 +207,8 @@ const PostNewPage = () => { {/* 送信 */} )