概要
バックエンドで保持しているテンプレートエクセルファイルをいい感じに編集してクライアントへ返送するAPIの話。
ChatGPT 4oに聞いてもいまいち情報を得られなかったので自力調査した内容を纏めておく。
サンプルコードのエクセル編集内容は以下。
- 書式設定された行をコピー
- 入力規則の設定
- 値入力
動作環境
Gemfile.lock
RUBY VERSION
ruby 3.1.1p18
GEM
specs:
rails (7.0.4)
rubyXL (3.4.27)
ソースコード
バックエンド
一部コード抜粋
template_generate_service.rb
class TemplateGenerateService
INPUT_FILE_NAME = 'フォーマット.xlsx'.freeze
SOURCE_ROW_INDEX = 5 # データ行書式コピー元の行インデックス
START_ROW_INDEX = 5 # データ行の開始インデックス
def initialize(header_data:, rows_data:)
@header_data = header_data
@rows_data = rows_data
end
def file_extension
File.extname(INPUT_FILE_NAME)
end
def generate
template_path = fetch_template_path
workbook = RubyXL::Parser.parse(template_path)
worksheet = workbook[0]
# 特定セルへの値入力
update_template_headers(worksheet)
end_row_index = START_ROW_INDEX + @rows_data.length - 1
# データ行生成と書式設定
styling_rows(worksheet:, start_row_index: START_ROW_INDEX, end_row_index:)
# データ行の一部に入力規則設定
set_data_validations(worksheet:, start_row_index: START_ROW_INDEX, end_row_index:)
# データ入力
put_rows_data(worksheet:, start_row_index: START_ROW_INDEX)
# 編集されたエクセルデータ返却
workbook.stream.string
end
private
def fetch_template_path
base_path = Rails.public_path.join('templates')
file_path = base_path.join(INPUT_FILE_NAME)
raise "Template file not found: #{file_path}" unless File.exist?(file_path)
file_path
end
def update_template_headers(worksheet)
worksheet[1][1].change_contents(@header_data.label) # B2
worksheet[2][1].change_contents(@header_data.note) # B3
end
# 元ファイルに予め書式設定された行を他の行へコピー
def styling_rows(worksheet:, start_row_index:, end_row_index:)
source_row = worksheet[SOURCE_ROW_INDEX]
(start_row_index..end_row_index).each do |row_index|
next if row_index == SOURCE_ROW_INDEX
source_row.cells.each_with_index do |source_cell, col_index|
next unless source_cell
target_cell = worksheet.add_cell(row_index, col_index)
target_cell.style_index = source_cell.style_index
end
end
end
# 元ファイルの別シートに予め用意された入力規則用データを特定セルに適用
def set_data_validations(worksheet:, start_row_index:, end_row_index:)
validations_config = [
{ column: 6, formula: '=Sheet2!$A$1:$A$6' }, # G列
{ column: 7, formula: '=Sheet2!$B$1:$B$99' }, # H列
{ column: 8, formula: '=Sheet2!$C$1:$C$99' } # I列
]
validations = RubyXL::DataValidations.new
validations_config.each do |config|
validations << RubyXL::DataValidation.new(
sqref: RubyXL::Reference.new(start_row_index, end_row_index, config[:column], config[:column]),
formula1: RubyXL::Formula.new(expression: config[:formula]),
type: 'list',
error_style: 'stop',
allow_blank: true,
show_error_message: true
)
end
worksheet.data_validations = validations
end
def put_rows_data(worksheet:, start_row_index:)
@rows_data.each_with_index do |row_data, index|
row_index = start_row_index + index
worksheet[row_index][1].change_contents(row_data.nullable_value) if row_data.nullable_value
worksheet[row_index][2].change_contents(row_data.id)
worksheet[row_index][3].change_contents("#{row_data.name}さん")
worksheet[row_index][4].change_contents("#{row_data.age}歳")
end
end
end
xx_controller.rb
def download_template
template_generator = TemplateGenerateService.new(
header_data: fetch_header_data(params),
rows_data: fetch_rows_data(params),
)
template_data = template_generator.generate
file_ext = template_generator.file_extension
out_file_name = "編集済みファイル#{file_ext}"
mime_type = Rack::Mime.mime_type(file_ext)
send_data template_data, type: mime_type, filename: out_file_name, disposition: 'attachment'
end
フロントエンド
一部コード抜粋
front.vue
<script setup lang="ts">
const onClickDownload = async () => {
downloading.value = true;
const res = await aspida(initAxios(), { timeout: 60000, baseURL: process.env.VUE_APP_API_URL }).template.get({ query });
const contentDisposition = res.headers["content-disposition"];
const contentType = res.headers["content-type"];
const contentFileName = contentDisposition?.match(/filename\*=UTF-8''(.+)/);
if (!contentFileName) throw new Error("ファイル名が取得できないのは想定外");
const encodedFileName = contentFileName[1];
if (!encodedFileName) throw new Error("ファイル名が取得できないのは想定外");
const fileName = decodeURIComponent(encodedFileName);
const blob = new Blob([res.body], { type: contentType });
saveAs(blob, fileName);
downloading.value = false;
};
</script>