diff --git a/AGENTS.md b/AGENTS.md index 728181c..876b13f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -529,6 +529,11 @@ and layout reuse, follow `frontend/AGENTS.md`. - Mobile UI must be checked as a first-class layout. Avoid wide fixed content, make dense controls wrap or scroll intentionally, and keep tag/filter controls usable without horizontal page overflow. +- Frontend のスマホ/PC表示境界は原則 `md` とする。 +- button stack、footer action、dialogue action は `md` 未満で縦並び、 + `md` 以上で横並びとする。 +- 同じ画面内で `sm` と `md` を混在させて中間 layout を作らない。 +- 明確に別の responsive 要件がある component だけを例外とする。 - For mobile horizontal scrollers, make the scroll direction and item sizing explicit, and ensure chip text remains readable in both light and dark modes. - In TypeScript and TSX, prefer direct comparison operators such as `===` and diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 69bbc41..4bb4b3f 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -137,6 +137,7 @@ class PostsController < ApplicationController video_ms: nil, field_warnings: { }, base_warnings: [], + existing_post_id: existing_post.id, existing_post: compact_post(existing_post.id) } end @@ -158,6 +159,7 @@ class PostsController < ApplicationController video_ms: metadata[:video_ms], field_warnings: field_warnings, base_warnings: [], + existing_post_id: nil, existing_post: nil } rescue ArgumentError => e render_bad_request e.message @@ -177,6 +179,7 @@ class PostsController < ApplicationController video_ms: nil, field_warnings: { url: ['自動取得に失敗しました.'] }, base_warnings: [], + existing_post_id: nil, existing_post: nil } end @@ -212,7 +215,7 @@ class PostsController < ApplicationController attributes: post_create_attributes, thumbnail: params[:thumbnail]).run return render json: dry_run_json(preflight) if bool?(:dry) - if preflight[:existing_post].present? + if preflight[:existing_post_id].present? post = Post.new(url: preflight[:url]) post.errors.add :url, :taken return render_post_form_record_invalid post @@ -637,6 +640,7 @@ class PostsController < ApplicationController :video_ms, :field_warnings, :base_warnings, + :existing_post_id, :existing_post) end diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index 9597c40..3f6f708 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -20,13 +20,19 @@ class Post < ApplicationRecord def self.resized_thumbnail_attachment(upload, content_type: nil) upload.rewind - image = image_for_thumbnail_upload(upload.read, content_type:) - image.resize '180x180' - image.format 'jpg' + bytes = upload.read + blob = Timeout.timeout(THUMBNAIL_PROCESS_TIMEOUT) do + image = image_for_thumbnail_upload(bytes, content_type:) + image.resize '180x180' + image.format 'jpg' + image.to_blob + end - { io: StringIO.new(image.to_blob), + { io: StringIO.new(blob), filename: 'resized_thumbnail.jpg', content_type: 'image/jpeg' } + rescue Timeout::Error + raise MiniMagick::Error, 'サムネイル画像の変換に失敗しました.' ensure upload.rewind end @@ -41,6 +47,7 @@ class Post < ApplicationRecord Preview::HttpFetcher::FetchFailed, Preview::HttpFetcher::FetchTimeout, Preview::HttpFetcher::ResponseTooLarge, + Timeout::Error, MiniMagick::Error => e raise RemoteThumbnailFetchFailed, e.message end @@ -240,13 +247,11 @@ class Post < ApplicationRecord end def self.decode_raster_thumbnail(bytes) - Timeout.timeout(THUMBNAIL_PROCESS_TIMEOUT) { MiniMagick::Image.read(bytes) } + MiniMagick::Image.read(bytes) end def self.decode_svg_thumbnail(bytes) - Timeout.timeout(THUMBNAIL_PROCESS_TIMEOUT) do - MiniMagick::Image.read(sanitised_svg_bytes(bytes)) - end + MiniMagick::Image.read(sanitised_svg_bytes(bytes)) end def self.sanitised_svg_bytes(bytes) @@ -303,7 +308,7 @@ class Post < ApplicationRecord stripped = value.to_s.strip return false if stripped.blank? || stripped.start_with?('#') - stripped.match?(/\A(?:https?:|data:|\/\/)/i) + true end def self.style_contains_disallowed_urls?(value) @@ -325,6 +330,8 @@ class Post < ApplicationRecord view_box = root['viewBox'].to_s.strip.split(/\s+/).map { Float(_1) rescue nil } return [nil, nil] if view_box.length != 4 || view_box.any?(&:nil?) + return [nil, nil] unless view_box[2].finite? && view_box[2].positive? + return [nil, nil] unless view_box[3].finite? && view_box[3].positive? [view_box[2], view_box[3]] end diff --git a/backend/app/services/post_bulk_creator.rb b/backend/app/services/post_bulk_creator.rb index 9865506..8bb6eca 100644 --- a/backend/app/services/post_bulk_creator.rb +++ b/backend/app/services/post_bulk_creator.rb @@ -65,9 +65,10 @@ class PostBulkCreator PostCreatePreflight.new( attributes: attributes, thumbnail: thumbnail_for(index, attributes)).run - if preflight[:existing_post].present? + if preflight[:existing_post_id].present? return { status: 'skipped', + existing_post_id: preflight[:existing_post_id], existing_post: preflight[:existing_post] } end @@ -91,6 +92,7 @@ class PostBulkCreator if existing_post.present? return { status: 'skipped', + existing_post_id: existing_post[:id], existing_post: existing_post } end @@ -104,6 +106,7 @@ class PostBulkCreator existing_post = existing_post_for_race(attributes) return { status: 'skipped', + existing_post_id: existing_post[:id], existing_post: existing_post } if existing_post.present? end diff --git a/backend/app/services/post_create_preflight.rb b/backend/app/services/post_create_preflight.rb index 1ef7f4e..234483f 100644 --- a/backend/app/services/post_create_preflight.rb +++ b/backend/app/services/post_create_preflight.rb @@ -31,6 +31,7 @@ class PostCreatePreflight video_ms: preview[:attributes]['video_ms'], field_warnings: final_field_warnings(preview[:field_warnings] || { }), base_warnings: preview[:base_warnings], + existing_post_id: preview[:existing_post_id], existing_post: existing_post_compact(preview[:existing_post_id]) } end @@ -70,6 +71,7 @@ class PostCreatePreflight normalised_parent_post_ids: plan[:normalised_parent_post_ids], field_warnings: final_field_warnings(preview[:field_warnings] || { }), base_warnings: preview[:base_warnings], + existing_post_id: preview[:existing_post_id], existing_post: existing_post_compact(preview[:existing_post_id]) } end diff --git a/backend/app/services/post_thumbnail_upload_validator.rb b/backend/app/services/post_thumbnail_upload_validator.rb index 37e752a..b5b44e6 100644 --- a/backend/app/services/post_thumbnail_upload_validator.rb +++ b/backend/app/services/post_thumbnail_upload_validator.rb @@ -16,7 +16,7 @@ class PostThumbnailUploadValidator attachment = Post.resized_thumbnail_attachment(thumbnail, content_type: thumbnail.content_type) attachment[:io].close if attachment[:io].respond_to?(:close) - rescue MiniMagick::Error + rescue MiniMagick::Error, Timeout::Error raise InvalidUpload, 'サムネイル画像の変換に失敗しました.' ensure thumbnail&.rewind if thumbnail.respond_to?(:rewind) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index bd9e5a6..ae68412 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -602,6 +602,12 @@ offsets, or footer offsets, inspect existing layout components such as Do not create a second layout shell before checking whether the current layout can be reused or minimally extended. +- Frontend のスマホ/PC表示境界は原則 `md` とする。 +- button stack、footer action、dialogue action は `md` 未満で縦並び、 + `md` 以上で横並びとする。 +- 同じ画面内で `sm` と `md` を混在させて中間 layout を作らない。 +- 明確に別の responsive 要件がある component だけを例外とする。 + ### Delimiter decision table Use this table before accepting any edited TypeScript or TSX hunk. The table is diff --git a/frontend/src/components/dialogues/DialogueProvider.tsx b/frontend/src/components/dialogues/DialogueProvider.tsx index 3ab04f3..01ff21e 100644 --- a/frontend/src/components/dialogues/DialogueProvider.tsx +++ b/frontend/src/components/dialogues/DialogueProvider.tsx @@ -215,12 +215,12 @@ const DialogueProvider: FC = ({ children }) => { -
+ md:flex-row md:justify-between md:gap-0 md:space-x-0"> +
{startActions.map (action => ( ))}
-
+
{showActions && (editVisible || retryAllowed) && ( -
+
{editVisible && (