Upgrade to Pro — share decks privately, control downloads, hide ads and more …

【1ヶ月後にタイ進出】約300ファイルを修正して複数タイムゾーンに対応した話

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for gogutan gogutan
December 08, 2025

 【1ヶ月後にタイ進出】約300ファイルを修正して複数タイムゾーンに対応した話

Avatar for gogutan

gogutan

December 08, 2025
Tweet

More Decks by gogutan

Other Decks in Programming

Transcript

  1. ࣗݾ঺հ • @gogutan • झຯ : ӳޠɺڝϓϩ(AtCoder)ɺ ໺ٿ؍ઓɺຑ੃ • FJORD

    BOOT CAMPग़਎ • Tech Lead @ גࣜձࣾΩϟλϧ Rails 8(Solid Cable, Kamal)
  2. λΠਐग़લ: ୯ҰλΠϜκʔϯ(ڭࣨ͸ࠃ಺ͷΈ) • όοΫΤϯυ͸ con fi g.time_zone = ‘Asia/Tokyo’ •

    ϑϩϯτΤϯυ(dayjs)Ͱ͸໌ࣔతͳઃఆ͕ແ͘ɺաڈʹ1౓ෆ۩߹༗Γ (ւ֎ʹ଺ࡏதͷڭࢣ͕ϩάΠϯ͢Δͱɺݱ஍࣌ؒͰදࣔ͞Εͯ͠·͏) γϑτ͕ޕલ࣌։࢝ͱ දࣔ͞Ε͍ͯ·͢💦 👩🎓
  3. λΠਐग़લ: ୯ҰλΠϜκʔϯ(ڭࣨ͸ࠃ಺ͷΈ) • όοΫΤϯυ͸ con fi g.time_zone = ‘Asia/Tokyo’ •

    ϑϩϯτΤϯυ(dayjs)Ͱ͸໌ࣔతͳઃఆ͕ແ͘ɺաڈʹ1౓ෆ۩߹༗Γ (ւ֎ʹ଺ࡏதͷڭࢣ͕ϩάΠϯ͢Δͱɺݱ஍࣌ؒͰදࣔ͞Εͯ͠·͏) γϑτ͕ޕલ࣌։࢝ͱ දࣔ͞Ε͍ͯ·͢💦 👩🎓 ↓ ϑϩϯτΤϯυ΋ ’Asia/Tokyo’ ʹݻఆͯ͠ղܾ
  4. ෳ਺λΠϜκʔϯʹରԠ (ϩάΠϯϢʔβʔͰ੾ସ) Time.current => Tue, 09 Dec 2025 20:30:00.000000000 JST

    +09:00 Time.use_zone('Asia/Bangkok') { Time.current } => Tue, 09 Dec 2025 18:30:00.000000000 +07 +07:00 Time.use_zone('America/Chicago') { Time.current } => Tue, 09 Dec 2025 05:30:00.000000000 CST -06:00 • Time.use_zoneΛ࢖ͬͯɺ ೚ҙͷλΠϜκʔϯͰ࣮ߦՄೳ
  5. ෳ਺λΠϜκʔϯʹରԠ (ϩάΠϯϢʔβʔͰ੾ସ) Time.current => Tue, 09 Dec 2025 20:30:00.000000000 JST

    +09:00 Time.use_zone('Asia/Bangkok') { Time.current } => Tue, 09 Dec 2025 18:30:00.000000000 +07 +07:00 Time.use_zone('America/Chicago') { Time.current } => Tue, 09 Dec 2025 05:30:00.000000000 CST -06:00 # https://api.rubyonrails.org/classes/Time.html#method-c-zone # Allows override of Time.zone locally inside supplied block; # resets Time.zone to existing value when done. class ApplicationController < ActionController::Base around_action :set_time_zone private def set_time_zone Time.use_zone(current_user.timezone) { yield } end end • Time.use_zoneΛ࢖ͬͯɺ ೚ҙͷλΠϜκʔϯͰ࣮ߦՄೳ • around_actionͰ׆༻ͨ͠ Rails APIͷίʔυྫˠ
  6. ෳ਺λΠϜκʔϯʹରԠ (ϩάΠϯϢʔβʔͰ੾ସ) Time.current => Tue, 09 Dec 2025 20:30:00.000000000 JST

    +09:00 Time.use_zone('Asia/Bangkok') { Time.current } => Tue, 09 Dec 2025 18:30:00.000000000 +07 +07:00 Time.use_zone('America/Chicago') { Time.current } => Tue, 09 Dec 2025 05:30:00.000000000 CST -06:00 # https://api.rubyonrails.org/classes/Time.html#method-c-zone # Allows override of Time.zone locally inside supplied block; # resets Time.zone to existing value when done. class ApplicationController < ActionController::Base around_action :set_time_zone private def set_time_zone Time.use_zone(current_user.timezone) { yield } end end # app/models/user.rb class User < ApplicationRecord has_many :branch_users, dependent: :destroy has_many :branches, through: :branch_users end • Time.use_zoneΛ࢖ͬͯɺ ೚ҙͷλΠϜκʔϯͰ࣮ߦՄೳ • around_actionͰ׆༻ͨ͠ Rails APIͷίʔυྫˠ • ͔͠͠ɺUser͸ෳ਺ͷBranch (ߍࣷ)ʹॴଐͰ͖Δ🤔
  7. ෳ਺λΠϜκʔϯʹରԠ (ߍࣷ͝ͱʹ੾ସ) # https://api.rubyonrails.org/classes/Time.html#method-c-zone # Allows override of Time.zone locally

    inside supplied block; # resets Time.zone to existing value when done. class ApplicationController < ActionController::Base around_action :set_time_zone private def set_time_zone Time.use_zone(current_user.timezone) { yield } end end # app/models/user.rb class User < ApplicationRecord has_many :branch_users, dependent: :destroy has_many :branches, through: :branch_users end • ίϯτϩʔϥʔͷ around_actionͰׅΔํ਑͸🙆
  8. ෳ਺λΠϜκʔϯʹରԠ (ߍࣷ͝ͱʹ੾ସ) # https://api.rubyonrails.org/classes/Time.html#method-c-zone # Allows override of Time.zone locally

    inside supplied block; # resets Time.zone to existing value when done. class ApplicationController < ActionController::Base around_action :set_time_zone private def set_time_zone Time.use_zone(current_user.timezone) { yield } end end # app/models/user.rb class User < ApplicationRecord has_many :branch_users, dependent: :destroy has_many :branches, through: :branch_users end # app/models/branch.rb class Branch < ApplicationRecord def time_zone # 'Asia/Tokyo'͔'Asia/Bangkok'Λฦ͢ end end • ίϯτϩʔϥʔͷ around_actionͰׅΔํ਑͸🙆 • ͨͩ͠λΠϜκʔϯΛ࣋ͭͷ͸ UserͰ͸ͳ͘Branch
  9. ෳ਺λΠϜκʔϯʹରԠ (ߍࣷ͝ͱʹ੾ସ) # https://api.rubyonrails.org/classes/Time.html#method-c-zone # Allows override of Time.zone locally

    inside supplied block; # resets Time.zone to existing value when done. class ApplicationController < ActionController::Base around_action :set_time_zone private def set_time_zone Time.use_zone(current_user.timezone) { yield } end end # app/models/user.rb class User < ApplicationRecord has_many :branch_users, dependent: :destroy has_many :branches, through: :branch_users end # app/models/branch.rb class Branch < ApplicationRecord def time_zone # 'Asia/Tokyo'͔'Asia/Bangkok'Λฦ͢ end end • ίϯτϩʔϥʔͷ around_actionͰׅΔํ਑͸🙆 • ͨͩ͠λΠϜκʔϯΛ࣋ͭͷ͸ UserͰ͸ͳ͘Branch • ֤ίϯτϩʔϥʔͰద੾ͳ time_zoneΛ஍ಓʹ౰͍ͯͯ͘
  10. # app/controllers/concerns/branch_time_zone_applicable.rb module BranchTimeZoneApplicable extend ActiveSupport::Concern included do around_action :set_time_zone

    end private def set_time_zone(&block) if branch_id Time.use_zone(Branch.find_by(id: branch_id).time_zone, &block) else yield end end # Override this method in the including controller if necessary def branch_id @branch&.id end end
  11. # app/controllers/concerns/branch_time_zone_applicable.rb module BranchTimeZoneApplicable extend ActiveSupport::Concern included do around_action :set_time_zone

    end private def set_time_zone(&block) if branch_id Time.use_zone(Branch.find_by(id: branch_id).time_zone, &block) else yield end end # Override this method in the including controller if necessary def branch_id @branch&.id end end
  12. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController def new

    @appointment = Appointment.new end def create @appointment = Appointment.new(appointment_params) if @appointment.save redirect_to staff_appointments_path end end private def appointment_params permitted_params = %i[title starts_at ends_at branch_id memo] params.fetch(:appointment, {}).permit(permitted_params) end end end
  13. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController def new

    @appointment = Appointment.new end def create @appointment = Appointment.new(appointment_params) if @appointment.save redirect_to staff_appointments_path end end private def appointment_params permitted_params = %i[title starts_at ends_at branch_id memo] params.fetch(:appointment, {}).permit(permitted_params) end end end
  14. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController def new

    @appointment = Appointment.new end def create @appointment = Appointment.new(appointment_params) if @appointment.save redirect_to staff_appointments_path end end private def appointment_params permitted_params = %i[title starts_at ends_at branch_id memo] params.fetch(:appointment, {}).permit(permitted_params) end end end λΠߍࣷͳͷʹ ೔ຊ࣌ؒͷ12࣌Ͱ อଘ͞ΕΔ😥
  15. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    def new @appointment = Appointment.new end def create @appointment = Appointment.new(appointment_params) if @appointment.save redirect_to staff_appointments_path end end private def branch_id appointment_params[:branch_id] end def appointment_params permitted_params = %i[title starts_at ends_at branch_id memo] params.fetch(:appointment, {}).permit(permitted_params) end end end
  16. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    def new @appointment = Appointment.new end def create @appointment = Appointment.new(appointment_params) if @appointment.save redirect_to staff_appointments_path end end private def branch_id appointment_params[:branch_id] end def appointment_params permitted_params = %i[title starts_at ends_at branch_id memo] params.fetch(:appointment, {}).permit(permitted_params) end end end ೔ຊ࣌ؒͷ14࣌ (λΠ࣌ؒͷ12࣌👍)
  17. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    before_action :set_appointment, only: %i[edit] > def new; end > def create; end > def edit; end private def branch_id appointment_params[:branch_id] end def set_appointment @appointment ||= Appointment.find(params[:id]) end > def appointment_params; end end end
  18. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    before_action :set_appointment, only: %i[edit] > def new; end > def create; end > def edit; end private def branch_id appointment_params[:branch_id] end def set_appointment @appointment ||= Appointment.find(params[:id]) end > def appointment_params; end end end
  19. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    before_action :set_appointment, only: %i[edit] > def new; end > def create; end > def edit; end private def branch_id appointment_params[:branch_id] end def set_appointment @appointment ||= Appointment.find(params[:id]) end > def appointment_params; end end end editΞΫγϣϯͰ͸nil😢
  20. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    before_action :set_appointment, only: %i[edit] > def new; end > def create; end > def edit; end private def branch_id case action_name when ‘edit' set_appointment @appointment.branch_id when 'create' appointment_params[:branch_id] end end def set_appointment @appointment ||= Appointment.find(params[:id]) end > def appointment_params; end end end
  21. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    before_action :set_appointment, only: %i[edit] > def new; end > def create; end > def edit; end private def branch_id case action_name when ‘edit' set_appointment @appointment.branch_id when 'create' appointment_params[:branch_id] end end def set_appointment @appointment ||= Appointment.find(params[:id]) end > def appointment_params; end end end 🎉
  22. # app/controllers/staff/appointments_controller.rb module Staff class AppointmentsController < Staff::BaseController include BranchTimeZoneApplicable

    before_action :set_appointment, only: %I[edit update] > def new; end > def create; end > def edit; end > def update; end private def branch_id case action_name when ‘edit' set_appointment @appointment.branch_id when 'create', 'update' appointment_params[:branch_id] end end def set_appointment @appointment ||= Appointment.find(params[:id]) end > def appointment_params; end end end ΞΫγϣϯ͝ͱʹ ஍ಓʹbranch_idΛ ઃఆ͍ͯ͘͠
  23. # app/views/staff/appointments/index.html.haml - @appointments.each do |appointment| - Time.use_zone(appointment.branch.time_zone) do %tr

    %td.col-sm-1 = l(appointment.starts_at, format: :year_month_date_wday) %td.col-sm-1 = l(appointment.starts_at.to_time, format: :hour) \~ = l(appointment.ends_at.to_time, format: :hour) %td.col-sm-1 = appointment.branch.name λΠϜκʔϯ͕ҟͳΔ🤔 ϏϡʔͰϨίʔυ͝ͱʹ λΠϜκʔϯΛద༻͢Δ
  24. ϑϩϯτΤϯυଆͷରԠ (งғؾ͚ͩ঺հ) { "id": 743999, "user_id": 4681, "starts_at": "2025-12-10T18:00:00.000+09:00", "ends_at":

    “2025-12-10T21:00:00.000+09:00", "student_full_name": "test ܑ", "branch_id": 13, "branch_name": “Phrom Phong", ... } • طʹඥ෇͘branchId͸ ෼͔͍ͬͯΔ λΠ࣌ؒ16࣌ʙ
  25. ϑϩϯτΤϯυଆͷରԠ (งғؾ͚ͩ঺հ) { "id": 743999, "user_id": 4681, "starts_at": "2025-12-10T18:00:00.000+09:00", "ends_at":

    “2025-12-10T21:00:00.000+09:00", "student_full_name": "test ܑ", "branch_id": 13, "branch_name": “Phrom Phong", ... } "time_zone": { "13": "Asia/Bangkok", "default": "Asia/Tokyo" } • طʹඥ෇͘branchId͸ ෼͔͍ͬͯΔ • branchIdͱλΠϜκʔϯͷ ඥ෇͚͚ͩ౉͢ λΠ࣌ؒ16࣌ʙ
  26. ϑϩϯτΤϯυଆͷରԠ (งғؾ͚ͩ঺հ) { "id": 743999, "user_id": 4681, "starts_at": "2025-12-10T18:00:00.000+09:00", "ends_at":

    “2025-12-10T21:00:00.000+09:00", "student_full_name": "test ܑ", "branch_id": 13, "branch_name": “Phrom Phong", ... } "time_zone": { "13": "Asia/Bangkok", "default": "Asia/Tokyo" } • طʹඥ෇͘branchId͸ ෼͔͍ͬͯΔ • branchIdͱλΠϜκʔϯͷ ඥ෇͚͚ͩ౉͢ λΠ࣌ؒ16࣌ʙ 👍
  27. ϑϩϯτΤϯυଆͷରԠ (งғؾ͚ͩ঺հ) { "id": 743999, "user_id": 4681, "starts_at": "2025-12-10T18:00:00.000+09:00", "ends_at":

    “2025-12-10T21:00:00.000+09:00", "student_full_name": "test ܑ", "branch_id": 13, "branch_name": “Phrom Phong", ... } "time_zone": { "13": "Asia/Bangkok", "default": "Asia/Tokyo" } • طʹඥ෇͘branchId͸ ෼͔͍ͬͯΔ • branchIdͱλΠϜκʔϯͷ ඥ෇͚͚ͩ౉͢ • ͍ͭͰʹɺλΠϜκʔϯซه ΋ಋೖ(ݱ஍ɺϢʔβʔҐஔ) λΠ࣌ؒ16࣌ʙ
  28. import dayjs from 'dayjs' ... const timeZoneOf = (branchId: number):

    string => object.branch.time_zone[branchId.toString()] || object.branch.time_zone['default'] const timeZoneNames = { 'Asia/Tokyo': 'Japan', 'Asia/Bangkok': 'Thailand' } dayjs.prototype.tzOf = function (branchId: number) { return this.tz(timeZoneOf(branchId)) } // ྫ: 🇯🇵 2024/04/26 (🇹🇭 2024/04/25) dayjs.prototype.formatWithBranchTimeZone = function ( format: string, branchId: number ) { const userTimeZone = dayjs.tz.guess() const userFormat = this.format(format) const branchTimeZone = branchId ? timeZoneOf(branchId) : userTimeZone const branchFormat = this.tz(branchTimeZone).format(format) // ͕࣌ࠩ͋ͬͯ΋ɺformat('YYYY/MM/DD')ͳͲ͸දه͕ಉҰʹՄೳੑ͕͋ΔͨΊɺformatಉ࢜Ͱൺֱ͢Δ if (userFormat === branchFormat) { return this.format(format) } else { const branchTimeZoneName = timeZoneNames[branchTimeZone] || branchTimeZone const userTimeZoneName = timeZoneNames[userTimeZone] || userTimeZone const branchFlag = getTimeZoneFlag(branchTimeZoneName) const userFlag = getTimeZoneFlag(userTimeZoneName) || '📍' return `${branchFlag} ${branchFormat} (${userFlag} ${userFormat})` } } export default dayjs λΠϜκʔϯม׵ޙͷ dayjsΠϯελϯεΛฦؔ͢਺ λΠϜκʔϯซه༻ͷ formatؔ਺