diff --git a/backend/app/services/post_import_google_sheets_fetcher.rb b/backend/app/services/post_import_google_sheets_fetcher.rb index af6341b..85ab331 100644 --- a/backend/app/services/post_import_google_sheets_fetcher.rb +++ b/backend/app/services/post_import_google_sheets_fetcher.rb @@ -25,7 +25,8 @@ class PostImportGoogleSheetsFetcher response.body rescue Preview::UrlSafety::UnsafeUrl, Preview::HttpFetcher::FetchFailed => e - raise ArgumentError, "スプレッドシートを取得できませんでした.ウェブ公開 URL を確認するか、ファイル保存又はコピー貼付けを使用してください: #{ e.message }" + Rails.logger.info("post_import_google_sheets_fetch_failure #{ { error: e.class.name, message: e.message }.to_json }") + raise ArgumentError, 'スプレッドシートを取得できませんでした.ウェブ公開 URL を確認するか、ファイル保存又はコピー貼付けを使用してください.' end private diff --git a/backend/app/services/post_import_previewer.rb b/backend/app/services/post_import_previewer.rb index 48d52a4..a276b0a 100644 --- a/backend/app/services/post_import_previewer.rb +++ b/backend/app/services/post_import_previewer.rb @@ -122,7 +122,8 @@ class PostImportPreviewer warnings << 'サムネールを取得できませんでした.' if data['thumbnail_base'].blank? [data.compact, warnings] rescue Preview::UrlSafety::UnsafeUrl, Preview::HttpFetcher::FetchFailed, Preview::HttpFetcher::ResponseTooLarge => e - [{ }, ["メタデータを取得できませんでした: #{ e.message }"]] + Rails.logger.info("post_import_metadata_fetch_failure #{ { error: e.class.name, message: e.message }.to_json }") + [{ }, ['メタデータを取得できませんでした.']] end def validate_preview_tags raw, errors, warnings diff --git a/backend/app/services/post_import_source_parser.rb b/backend/app/services/post_import_source_parser.rb index a36b8c5..a58621a 100644 --- a/backend/app/services/post_import_source_parser.rb +++ b/backend/app/services/post_import_source_parser.rb @@ -4,6 +4,7 @@ class PostImportSourceParser MAX_ROWS = 100 MAX_BYTES = 1.megabyte MAX_CELL_BYTES = 20.kilobytes + MAX_COLUMNS = 50 def initialize source:, format:, has_header: false, json_path: nil @source = source.to_s @@ -28,8 +29,9 @@ class PostImportSourceParser headers = @has_header ? table.shift : nil ensure_limits!(table) width = table.map(&:length).max.to_i + raise ArgumentError, "列数は #{ MAX_COLUMNS } 列までです." if width > MAX_COLUMNS { columns: (0...width).map { |index| { key: index.to_s, - label: "#{ ('A'.ord + index).chr }:#{ headers&.[](index).presence || '列' }" } }, + label: "#{ column_name(index) }:#{ headers&.[](index).presence || '列' }" } }, rows: table.each_with_index.map { |values, index| { source_row: index + (@has_header ? 2 : 1), values: } } } rescue CSV::MalformedCSVError => e raise ArgumentError, "CSV・TSV の形式が不正です: #{ e.message }" @@ -42,7 +44,8 @@ class PostImportSourceParser ensure_limits!(value) keys = value.grep(Hash).flat_map(&:keys).map(&:to_s).uniq - { columns: keys.each_with_index.map { |key, index| { key:, label: "#{ ('A'.ord + index).chr }:#{ key }" } }, + raise ArgumentError, "列数は #{ MAX_COLUMNS } 列までです." if keys.length > MAX_COLUMNS + { columns: keys.each_with_index.map { |key, index| { key:, label: "#{ column_name(index) }:#{ key }" } }, rows: value.each_with_index.map { |item, index| { source_row: index + 1, values: keys.map { json_cell(item.is_a?(Hash) ? item[_1] || item[_1.to_sym] : nil) } } } } rescue JSON::ParserError, KeyError, ArgumentError => e @@ -62,4 +65,14 @@ class PostImportSourceParser else raise ArgumentError, 'JSON のセル値が不正です.' end end + + def column_name index + name = +'' + loop do + name.prepend((65 + (index % 26)).chr) + index = (index / 26) - 1 + break if index < 0 + end + name + end end diff --git a/frontend/src/pages/posts/PostImportPage.tsx b/frontend/src/pages/posts/PostImportPage.tsx index 084150a..2d9e0fb 100644 --- a/frontend/src/pages/posts/PostImportPage.tsx +++ b/frontend/src/pages/posts/PostImportPage.tsx @@ -113,7 +113,7 @@ const PostImportPage: FC = ({ user }) => { url_column: urlColumn, mappings, existing }) - setRows (data.rows) + setRows (current => mergeValidatedRows (current, data.rows)) setPhase ('preview') } catch @@ -126,6 +126,15 @@ const PostImportPage: FC = ({ user }) => { } } + const mergeValidatedRows = (current: ImportRow[], validated: ImportRow[]): ImportRow[] => + current.filter (row => row.importStatus === 'created').concat (validated.map (row => { + const previous = current.find (_1 => _1.sourceRow === row.sourceRow) + return previous ? { ...row, + importStatus: previous.importStatus, + createdPostId: previous.createdPostId, + errors: previous.importStatus === 'failed' ? previous.errors : row.errors } : row + })) + const updateAttribute = async (index: number, field: string, value: string) => { const row = rows[index] const next = { ...row, @@ -138,10 +147,10 @@ const PostImportPage: FC = ({ user }) => { try { const validated = await apiPost<{ rows: ImportRow[] }> ('/posts/import/validate', - { rows: nextRows, existing, + { rows: nextRows.filter (row => row.importStatus !== 'created'), existing, changed_row: -1 }) if (generation === validationGeneration.current) - setRows (validated.rows) + setRows (current => mergeValidatedRows (current, validated.rows)) } catch { @@ -164,10 +173,10 @@ const PostImportPage: FC = ({ user }) => { try { const validated = await apiPost<{ rows: ImportRow[] }> ('/posts/import/validate', - { rows: nextRows, existing, + { rows: nextRows.filter (row => row.importStatus !== 'created'), existing, changed_row: row.sourceRow }) if (generation === validationGeneration.current) - setRows (validated.rows) + setRows (current => mergeValidatedRows (current, validated.rows)) } catch { @@ -187,9 +196,9 @@ const PostImportPage: FC = ({ user }) => { try { const validated = await apiPost<{ rows: ImportRow[] }> ('/posts/import/validate', - { rows, existing: value, changed_row: -1 }) + { rows: rows.filter (row => row.importStatus !== 'created'), existing: value, changed_row: -1 }) if (generation === validationGeneration.current) - setRows (validated.rows) + setRows (current => mergeValidatedRows (current, validated.rows)) } finally {