$30 off During Our Annual Pro Sale. View Details »

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

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ؔ਺