tayo 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +38 -79
- data/docs/2025-11-29-cf-command-refactor.md +53 -0
- data/lib/tayo/cli.rb +4 -4
- data/lib/tayo/commands/cf.rb +137 -30
- data/lib/tayo/commands/gh.rb +90 -55
- data/lib/tayo/commands/init.rb +15 -2
- data/lib/tayo/commands/sqlite.rb +371 -0
- data/lib/tayo/templates/solid-cable-sqlite-setup.md +90 -0
- data/lib/tayo/version.rb +1 -1
- metadata +5 -10
- data/lib/tayo/commands/proxy.rb +0 -97
- data/lib/tayo/proxy/cloudflare_client.rb +0 -323
- data/lib/tayo/proxy/docker_manager.rb +0 -150
- data/lib/tayo/proxy/network_config.rb +0 -147
- data/lib/tayo/proxy/traefik_config.rb +0 -303
- data/lib/tayo/proxy/welcome_service.rb +0 -337
- data/lib/templates/welcome/Dockerfile +0 -14
- data/lib/templates/welcome/index.html +0 -173
data/lib/tayo/commands/gh.rb
CHANGED
|
@@ -177,44 +177,48 @@ module Tayo
|
|
|
177
177
|
def create_github_repository
|
|
178
178
|
repo_name = File.basename(Dir.pwd)
|
|
179
179
|
username = `gh api user -q .login`.strip
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
# 조직 목록 가져오기
|
|
182
182
|
orgs_json = `gh api user/orgs -q '.[].login' 2>/dev/null`
|
|
183
183
|
orgs = orgs_json.strip.split("\n").reject(&:empty?)
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
owner = username
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
if orgs.any?
|
|
188
188
|
prompt = TTY::Prompt.new
|
|
189
189
|
choices = ["#{username} (개인 계정)"] + orgs.map { |org| "#{org} (조직)" }
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
selection = prompt.select("🏢 저장소를 생성할 위치를 선택하세요:", choices)
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
if selection != "#{username} (개인 계정)"
|
|
194
194
|
owner = selection.split(" ").first
|
|
195
195
|
end
|
|
196
196
|
end
|
|
197
|
-
|
|
197
|
+
|
|
198
|
+
@repo_name = repo_name
|
|
199
|
+
@username = owner
|
|
200
|
+
|
|
198
201
|
# 저장소 존재 여부 확인
|
|
199
202
|
repo_exists = system("gh repo view #{owner}/#{repo_name}", out: File::NULL, err: File::NULL)
|
|
200
|
-
|
|
203
|
+
|
|
201
204
|
if repo_exists
|
|
202
205
|
puts "ℹ️ GitHub 저장소가 이미 존재합니다: https://github.com/#{owner}/#{repo_name}".colorize(:yellow)
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
# remote 설정 확인 및 업데이트
|
|
207
|
+
setup_git_remote(owner, repo_name)
|
|
205
208
|
else
|
|
209
|
+
# 기존 origin remote 제거 (있다면)
|
|
210
|
+
system("git remote remove origin 2>/dev/null")
|
|
211
|
+
|
|
206
212
|
create_cmd = if owner == username
|
|
207
213
|
"gh repo create #{repo_name} --private --source=. --remote=origin --push"
|
|
208
214
|
else
|
|
209
215
|
"gh repo create #{owner}/#{repo_name} --private --source=. --remote=origin --push"
|
|
210
216
|
end
|
|
211
|
-
|
|
217
|
+
|
|
212
218
|
result = system(create_cmd)
|
|
213
|
-
|
|
219
|
+
|
|
214
220
|
if result
|
|
215
221
|
puts "✅ GitHub 저장소를 생성했습니다: https://github.com/#{owner}/#{repo_name}".colorize(:green)
|
|
216
|
-
@repo_name = repo_name
|
|
217
|
-
@username = owner
|
|
218
222
|
else
|
|
219
223
|
puts "❌ GitHub 저장소 생성에 실패했습니다.".colorize(:red)
|
|
220
224
|
exit 1
|
|
@@ -222,13 +226,38 @@ module Tayo
|
|
|
222
226
|
end
|
|
223
227
|
end
|
|
224
228
|
|
|
229
|
+
def setup_git_remote(owner, repo_name)
|
|
230
|
+
remote_url = "git@github.com:#{owner}/#{repo_name}.git"
|
|
231
|
+
current_remote = `git remote get-url origin 2>/dev/null`.strip
|
|
232
|
+
|
|
233
|
+
if current_remote.empty?
|
|
234
|
+
# origin이 없으면 추가
|
|
235
|
+
system("git remote add origin #{remote_url}")
|
|
236
|
+
puts " ✅ remote origin을 추가했습니다.".colorize(:green)
|
|
237
|
+
elsif current_remote != remote_url && !current_remote.include?("#{owner}/#{repo_name}")
|
|
238
|
+
# origin이 다른 저장소를 가리키면 업데이트
|
|
239
|
+
system("git remote set-url origin #{remote_url}")
|
|
240
|
+
puts " ✅ remote origin을 업데이트했습니다.".colorize(:green)
|
|
241
|
+
else
|
|
242
|
+
puts " ✅ remote origin이 올바르게 설정되어 있습니다.".colorize(:green)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# push 되지 않은 커밋이 있으면 push
|
|
246
|
+
unpushed = `git log origin/main..HEAD 2>/dev/null`.strip
|
|
247
|
+
if !unpushed.empty? || !system("git rev-parse origin/main", out: File::NULL, err: File::NULL)
|
|
248
|
+
puts " 📤 변경사항을 push합니다...".colorize(:yellow)
|
|
249
|
+
system("git push -u origin main")
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
225
253
|
def create_container_registry
|
|
226
254
|
# Docker 이미지 태그는 소문자여야 함
|
|
227
|
-
|
|
228
|
-
@
|
|
255
|
+
# Kamal은 registry.server + image를 조합하므로 image에는 username/repo만 지정
|
|
256
|
+
@image_name = "#{@username.downcase}/#{@repo_name.downcase}"
|
|
257
|
+
@registry_url = "ghcr.io/#{@image_name}" # 전체 URL (표시용)
|
|
229
258
|
|
|
230
259
|
puts "✅ 컨테이너 레지스트리가 설정되었습니다.".colorize(:green)
|
|
231
|
-
puts " URL: #{registry_url}".colorize(:gray)
|
|
260
|
+
puts " URL: #{@registry_url}".colorize(:gray)
|
|
232
261
|
puts " ℹ️ 컨테이너 레지스트리는 첫 이미지 푸시 시 자동으로 생성됩니다.".colorize(:gray)
|
|
233
262
|
|
|
234
263
|
# Docker로 GitHub Container Registry에 로그인
|
|
@@ -273,31 +302,44 @@ module Tayo
|
|
|
273
302
|
|
|
274
303
|
def update_kamal_config
|
|
275
304
|
content = File.read("config/deploy.yml")
|
|
276
|
-
|
|
277
|
-
# 이미지 설정 업데이트
|
|
278
|
-
#
|
|
279
|
-
content.gsub!(/^image:\s+.*$/, "image: #{@
|
|
280
|
-
|
|
305
|
+
|
|
306
|
+
# 이미지 설정 업데이트
|
|
307
|
+
# Kamal은 registry.server + image를 조합하므로 image에는 username/repo만 지정
|
|
308
|
+
content.gsub!(/^image:\s+.*$/, "image: #{@image_name}")
|
|
309
|
+
|
|
281
310
|
# registry 섹션 업데이트
|
|
282
311
|
if content.include?("registry:")
|
|
283
|
-
#
|
|
284
|
-
# server 라인이 주석처리되어 있는지 확인
|
|
312
|
+
# server 설정 (주석 처리된 경우 활성화)
|
|
285
313
|
if content.match?(/^\s*#\s*server:/)
|
|
286
314
|
content.gsub!(/^\s*#\s*server:\s*.*$/, " server: ghcr.io")
|
|
287
315
|
elsif content.match?(/^\s*server:/)
|
|
288
316
|
content.gsub!(/^\s*server:\s*.*$/, " server: ghcr.io")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# username 설정 (주석 처리된 경우 활성화)
|
|
320
|
+
if content.match?(/^\s*#\s*username:/)
|
|
321
|
+
content.gsub!(/^\s*#\s*username:\s*.*$/, " username: #{@username.downcase}")
|
|
322
|
+
elsif content.match?(/^\s*username:/)
|
|
323
|
+
content.gsub!(/^\s*username:\s*.*$/, " username: #{@username.downcase}")
|
|
289
324
|
else
|
|
290
|
-
#
|
|
291
|
-
content.gsub!(/(
|
|
325
|
+
# username이 없으면 server 다음에 추가
|
|
326
|
+
content.gsub!(/(^\s*server:\s*ghcr\.io\s*$)/, "\\1\n username: #{@username.downcase}")
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# password 설정 (주석 처리된 경우 활성화)
|
|
330
|
+
# 형식: " # password:\n # - KAMAL_REGISTRY_PASSWORD"
|
|
331
|
+
if content.match?(/^(\s*)#\s*password:\s*\n\s*#\s+-\s*KAMAL_REGISTRY_PASSWORD/m)
|
|
332
|
+
content.gsub!(
|
|
333
|
+
/^(\s*)#\s*password:\s*\n\s*#\s+-\s*KAMAL_REGISTRY_PASSWORD/m,
|
|
334
|
+
"\\1password:\n\\1 - KAMAL_REGISTRY_PASSWORD"
|
|
335
|
+
)
|
|
292
336
|
end
|
|
293
|
-
# username도 소문자로 변환
|
|
294
|
-
content.gsub!(/^\s*username:\s+.*$/, " username: #{@username.downcase}")
|
|
295
337
|
else
|
|
296
338
|
# registry 섹션 추가
|
|
297
339
|
registry_config = "\n# Container registry configuration\nregistry:\n server: ghcr.io\n username: #{@username.downcase}\n password:\n - KAMAL_REGISTRY_PASSWORD\n"
|
|
298
340
|
content.gsub!(/^# Credentials for your image host\.\nregistry:.*?^$/m, registry_config)
|
|
299
341
|
end
|
|
300
|
-
|
|
342
|
+
|
|
301
343
|
File.write("config/deploy.yml", content)
|
|
302
344
|
|
|
303
345
|
# GitHub 토큰을 Kamal secrets 파일에 설정
|
|
@@ -312,35 +354,28 @@ module Tayo
|
|
|
312
354
|
def setup_kamal_secrets
|
|
313
355
|
# .kamal 디렉토리 생성
|
|
314
356
|
Dir.mkdir(".kamal") unless Dir.exist?(".kamal")
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if existing_content.include?("KAMAL_REGISTRY_PASSWORD")
|
|
328
|
-
# 기존 값 업데이트
|
|
329
|
-
updated_content = existing_content.gsub(/^KAMAL_REGISTRY_PASSWORD=.*$/, "KAMAL_REGISTRY_PASSWORD=#{token}")
|
|
330
|
-
else
|
|
331
|
-
# 새로 추가
|
|
332
|
-
updated_content = existing_content.empty? ? "KAMAL_REGISTRY_PASSWORD=#{token}\n" : "#{existing_content.chomp}\nKAMAL_REGISTRY_PASSWORD=#{token}\n"
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
File.write(secrets_file, updated_content)
|
|
336
|
-
puts "✅ GitHub 토큰이 .kamal/secrets에 설정되었습니다.".colorize(:green)
|
|
337
|
-
|
|
338
|
-
# .gitignore에 secrets 파일 추가
|
|
339
|
-
add_to_gitignore(".kamal/secrets")
|
|
357
|
+
|
|
358
|
+
secrets_file = ".kamal/secrets"
|
|
359
|
+
|
|
360
|
+
# 기존 secrets 파일 읽기 (있다면)
|
|
361
|
+
existing_content = File.exist?(secrets_file) ? File.read(secrets_file) : ""
|
|
362
|
+
|
|
363
|
+
# KAMAL_REGISTRY_PASSWORD가 실제로 설정되어 있는지 확인 (주석 제외)
|
|
364
|
+
password_line = 'KAMAL_REGISTRY_PASSWORD=$(gh auth token)'
|
|
365
|
+
|
|
366
|
+
if existing_content.match?(/^KAMAL_REGISTRY_PASSWORD=/)
|
|
367
|
+
# 기존 값 업데이트
|
|
368
|
+
updated_content = existing_content.gsub(/^KAMAL_REGISTRY_PASSWORD=.*$/, password_line)
|
|
340
369
|
else
|
|
341
|
-
|
|
342
|
-
|
|
370
|
+
# 새로 추가 (파일 끝에)
|
|
371
|
+
updated_content = "#{existing_content.chomp}\n#{password_line}\n"
|
|
343
372
|
end
|
|
373
|
+
|
|
374
|
+
File.write(secrets_file, updated_content)
|
|
375
|
+
puts "✅ KAMAL_REGISTRY_PASSWORD가 .kamal/secrets에 설정되었습니다.".colorize(:green)
|
|
376
|
+
|
|
377
|
+
# .gitignore에 secrets 파일 추가
|
|
378
|
+
add_to_gitignore(".kamal/secrets")
|
|
344
379
|
end
|
|
345
380
|
|
|
346
381
|
def add_to_gitignore(file_path)
|
data/lib/tayo/commands/init.rb
CHANGED
|
@@ -93,8 +93,21 @@ module Tayo
|
|
|
93
93
|
|
|
94
94
|
puts "🎨 Welcome 페이지를 생성합니다...".colorize(:yellow)
|
|
95
95
|
|
|
96
|
-
# Welcome 컨트롤러 생성
|
|
97
|
-
system("rails generate controller Welcome index --skip-routes --no-helper --no-assets")
|
|
96
|
+
# Welcome 컨트롤러 생성 시도
|
|
97
|
+
unless system("rails generate controller Welcome index --skip-routes --no-helper --no-assets")
|
|
98
|
+
puts " ⚠️ rails generate 실패. 수동으로 파일을 생성합니다.".colorize(:yellow)
|
|
99
|
+
# 디렉토리와 컨트롤러 파일 직접 생성
|
|
100
|
+
FileUtils.mkdir_p("app/controllers")
|
|
101
|
+
FileUtils.mkdir_p("app/views/welcome")
|
|
102
|
+
|
|
103
|
+
controller_content = <<~RUBY
|
|
104
|
+
class WelcomeController < ApplicationController
|
|
105
|
+
def index
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
RUBY
|
|
109
|
+
File.write("app/controllers/welcome_controller.rb", controller_content)
|
|
110
|
+
end
|
|
98
111
|
|
|
99
112
|
# 프로젝트 이름 가져오기
|
|
100
113
|
project_name = File.basename(Dir.pwd).gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Tayo
|
|
7
|
+
module Commands
|
|
8
|
+
class Sqlite
|
|
9
|
+
def execute
|
|
10
|
+
puts "🗄️ SQLite 최적화 설정을 시작합니다...".colorize(:green)
|
|
11
|
+
|
|
12
|
+
unless rails_project?
|
|
13
|
+
puts "❌ Rails 프로젝트가 아닙니다. Rails 프로젝트 루트에서 실행해주세요.".colorize(:red)
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless sqlite_project?
|
|
18
|
+
puts "❌ SQLite를 사용하는 프로젝트가 아닙니다.".colorize(:red)
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless rails_8_or_higher?
|
|
23
|
+
puts "❌ Rails 8 이상이 필요합니다. (현재: Rails #{detect_rails_version || '알 수 없음'})".colorize(:red)
|
|
24
|
+
puts " Solid Cable은 Rails 8에서 도입되었습니다.".colorize(:yellow)
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts " Rails #{detect_rails_version} 확인됨".colorize(:gray)
|
|
29
|
+
|
|
30
|
+
add_solid_cable_gem
|
|
31
|
+
run_bundle_install
|
|
32
|
+
install_solid_cable
|
|
33
|
+
update_database_yml
|
|
34
|
+
update_cable_yml
|
|
35
|
+
create_sqlite_initializer
|
|
36
|
+
run_migrations
|
|
37
|
+
create_documentation
|
|
38
|
+
|
|
39
|
+
puts ""
|
|
40
|
+
puts "✅ SQLite + Solid Cable 최적화 설정이 완료되었습니다!".colorize(:green)
|
|
41
|
+
puts " 📄 설정 배경 문서: docs/solid-cable-sqlite-setup.md".colorize(:gray)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def rails_project?
|
|
47
|
+
File.exist?("Gemfile") && File.exist?("config/application.rb")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sqlite_project?
|
|
51
|
+
return false unless File.exist?("config/database.yml")
|
|
52
|
+
|
|
53
|
+
database_yml = File.read("config/database.yml")
|
|
54
|
+
database_yml.include?("sqlite3")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rails_8_or_higher?
|
|
58
|
+
version = detect_rails_version
|
|
59
|
+
return false unless version
|
|
60
|
+
|
|
61
|
+
major_version = version.split(".").first.to_i
|
|
62
|
+
major_version >= 8
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detect_rails_version
|
|
66
|
+
# Gemfile.lock에서 rails 버전 확인
|
|
67
|
+
if File.exist?("Gemfile.lock")
|
|
68
|
+
lockfile = File.read("Gemfile.lock")
|
|
69
|
+
if match = lockfile.match(/^\s+rails\s+\((\d+\.\d+\.\d+)/)
|
|
70
|
+
return match[1]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Gemfile에서 확인 (edge rails 등)
|
|
75
|
+
if File.exist?("Gemfile")
|
|
76
|
+
gemfile = File.read("Gemfile")
|
|
77
|
+
# gem "rails", "~> 8.0" 형식
|
|
78
|
+
if match = gemfile.match(/gem\s+["']rails["'],\s*["']~>\s*(\d+\.\d+)["']/)
|
|
79
|
+
return "#{match[1]}.0"
|
|
80
|
+
end
|
|
81
|
+
# github: "rails/rails" (edge) - Rails 8+ 가정
|
|
82
|
+
if gemfile.match?(/gem\s+["']rails["'].*github:\s*["']rails\/rails["']/)
|
|
83
|
+
return "8.0.0 (edge)"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def add_solid_cable_gem
|
|
91
|
+
puts "📦 Gemfile에 solid_cable을 추가합니다...".colorize(:yellow)
|
|
92
|
+
|
|
93
|
+
gemfile = File.read("Gemfile")
|
|
94
|
+
|
|
95
|
+
if gemfile.include?("solid_cable")
|
|
96
|
+
puts " ℹ️ solid_cable이 이미 존재합니다.".colorize(:yellow)
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# solid_cable gem 추가
|
|
101
|
+
if gemfile.include?("solid_queue")
|
|
102
|
+
# solid_queue 다음에 추가
|
|
103
|
+
gemfile.gsub!(/^gem ["']solid_queue["'].*$/) do |match|
|
|
104
|
+
"#{match}\ngem \"solid_cable\""
|
|
105
|
+
end
|
|
106
|
+
elsif gemfile.match?(/^gem ["']rails["']/)
|
|
107
|
+
# rails gem 다음에 추가
|
|
108
|
+
gemfile.gsub!(/^gem ["']rails["'].*$/) do |match|
|
|
109
|
+
"#{match}\n\n# Solid Cable - SQLite 기반 Action Cable 어댑터\ngem \"solid_cable\""
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
# 파일 끝에 추가
|
|
113
|
+
gemfile += "\n# Solid Cable - SQLite 기반 Action Cable 어댑터\ngem \"solid_cable\"\n"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
File.write("Gemfile", gemfile)
|
|
117
|
+
puts " ✅ Gemfile에 solid_cable을 추가했습니다.".colorize(:green)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def update_database_yml
|
|
121
|
+
puts "🗄️ database.yml을 업데이트합니다...".colorize(:yellow)
|
|
122
|
+
|
|
123
|
+
database_yml_path = "config/database.yml"
|
|
124
|
+
content = File.read(database_yml_path)
|
|
125
|
+
|
|
126
|
+
# 이미 cable 설정이 있는지 확인
|
|
127
|
+
if content.include?("cable:") || content.include?("cable_production:")
|
|
128
|
+
puts " ℹ️ cable 데이터베이스가 이미 설정되어 있습니다.".colorize(:yellow)
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# 기존 database.yml 파싱
|
|
133
|
+
# production 설정에 cable DB 추가
|
|
134
|
+
new_content = generate_database_yml(content)
|
|
135
|
+
|
|
136
|
+
File.write(database_yml_path, new_content)
|
|
137
|
+
puts " ✅ database.yml에 cable 데이터베이스를 추가했습니다.".colorize(:green)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def generate_database_yml(original_content)
|
|
141
|
+
# 기존 내용 유지하면서 cable DB 추가
|
|
142
|
+
lines = original_content.lines
|
|
143
|
+
|
|
144
|
+
# production 섹션 찾기
|
|
145
|
+
production_index = lines.find_index { |line| line.match?(/^production:/) }
|
|
146
|
+
|
|
147
|
+
if production_index
|
|
148
|
+
# production 섹션 끝 찾기
|
|
149
|
+
next_section_index = lines[(production_index + 1)..].find_index { |line| line.match?(/^\w+:/) }
|
|
150
|
+
insert_index = next_section_index ? production_index + 1 + next_section_index : lines.length
|
|
151
|
+
|
|
152
|
+
cable_config = <<~YAML
|
|
153
|
+
|
|
154
|
+
# Solid Cable용 별도 데이터베이스 (WAL 모드 최적화)
|
|
155
|
+
cable_production:
|
|
156
|
+
<<: *default
|
|
157
|
+
database: storage/db/cable_production.sqlite3
|
|
158
|
+
migrations_paths: db/cable_migrate
|
|
159
|
+
YAML
|
|
160
|
+
|
|
161
|
+
lines.insert(insert_index, cable_config)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# development/test에도 추가
|
|
165
|
+
dev_index = lines.find_index { |line| line.match?(/^development:/) }
|
|
166
|
+
if dev_index
|
|
167
|
+
next_section_index = lines[(dev_index + 1)..].find_index { |line| line.match?(/^\w+:/) }
|
|
168
|
+
insert_index = next_section_index ? dev_index + 1 + next_section_index : lines.length
|
|
169
|
+
|
|
170
|
+
cable_config = <<~YAML
|
|
171
|
+
|
|
172
|
+
cable_development:
|
|
173
|
+
<<: *default
|
|
174
|
+
database: storage/db/cable_development.sqlite3
|
|
175
|
+
migrations_paths: db/cable_migrate
|
|
176
|
+
YAML
|
|
177
|
+
|
|
178
|
+
lines.insert(insert_index, cable_config)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
test_index = lines.find_index { |line| line.match?(/^test:/) }
|
|
182
|
+
if test_index
|
|
183
|
+
next_section_index = lines[(test_index + 1)..].find_index { |line| line.match?(/^\w+:/) }
|
|
184
|
+
insert_index = next_section_index ? test_index + 1 + next_section_index : lines.length
|
|
185
|
+
|
|
186
|
+
cable_config = <<~YAML
|
|
187
|
+
|
|
188
|
+
cable_test:
|
|
189
|
+
<<: *default
|
|
190
|
+
database: storage/db/cable_test.sqlite3
|
|
191
|
+
migrations_paths: db/cable_migrate
|
|
192
|
+
YAML
|
|
193
|
+
|
|
194
|
+
lines.insert(insert_index, cable_config)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
lines.join
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def update_cable_yml
|
|
201
|
+
puts "📡 cable.yml을 업데이트합니다...".colorize(:yellow)
|
|
202
|
+
|
|
203
|
+
cable_yml_path = "config/cable.yml"
|
|
204
|
+
|
|
205
|
+
# Development는 async (단일 프로세스), Production은 solid_cable
|
|
206
|
+
cable_config = <<~YAML
|
|
207
|
+
# Solid Cable 설정 (SQLite 기반 Action Cable)
|
|
208
|
+
# Development: async 어댑터 (단일 프로세스, 콘솔 디버깅 용이)
|
|
209
|
+
# Production: solid_cable (polling_interval: 25ms, Redis 수준 RTT)
|
|
210
|
+
|
|
211
|
+
development:
|
|
212
|
+
adapter: async
|
|
213
|
+
|
|
214
|
+
test:
|
|
215
|
+
adapter: test
|
|
216
|
+
|
|
217
|
+
production:
|
|
218
|
+
adapter: solid_cable
|
|
219
|
+
connects_to:
|
|
220
|
+
database:
|
|
221
|
+
writing: cable
|
|
222
|
+
polling_interval: 0.025.seconds
|
|
223
|
+
message_retention: 1.hour
|
|
224
|
+
YAML
|
|
225
|
+
|
|
226
|
+
# 기존 파일 백업
|
|
227
|
+
if File.exist?(cable_yml_path)
|
|
228
|
+
backup_path = "#{cable_yml_path}.backup"
|
|
229
|
+
FileUtils.cp(cable_yml_path, backup_path)
|
|
230
|
+
puts " 📋 기존 cable.yml을 #{backup_path}로 백업했습니다.".colorize(:gray)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
File.write(cable_yml_path, cable_config)
|
|
234
|
+
puts " ✅ cable.yml을 Solid Cable 설정으로 업데이트했습니다.".colorize(:green)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def create_sqlite_initializer
|
|
238
|
+
puts "⚡ SQLite 최적화 initializer를 생성합니다...".colorize(:yellow)
|
|
239
|
+
|
|
240
|
+
initializer_path = "config/initializers/solid_cable_sqlite.rb"
|
|
241
|
+
|
|
242
|
+
if File.exist?(initializer_path)
|
|
243
|
+
puts " ℹ️ initializer가 이미 존재합니다.".colorize(:yellow)
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
initializer_content = <<~RUBY
|
|
248
|
+
# frozen_string_literal: true
|
|
249
|
+
|
|
250
|
+
# Solid Cable SQLite 최적화 설정
|
|
251
|
+
# - WAL 모드: 읽기/쓰기 동시성 향상 (폴링과 브로드캐스트 동시 처리)
|
|
252
|
+
# - synchronous=NORMAL: 쓰기 성능 향상 (약간의 안정성 트레이드오프)
|
|
253
|
+
|
|
254
|
+
Rails.application.config.after_initialize do
|
|
255
|
+
# Cable 데이터베이스에 WAL 모드 설정
|
|
256
|
+
if defined?(SolidCable) && ActiveRecord::Base.configurations.configs_for(name: "cable")
|
|
257
|
+
ActiveRecord::Base.connected_to(role: :writing, shard: :cable) do
|
|
258
|
+
connection = ActiveRecord::Base.connection
|
|
259
|
+
|
|
260
|
+
# WAL 모드 활성화 - 읽기/쓰기 동시 처리 가능
|
|
261
|
+
connection.execute("PRAGMA journal_mode=WAL")
|
|
262
|
+
|
|
263
|
+
# synchronous=NORMAL - fsync 횟수 감소로 쓰기 성능 향상
|
|
264
|
+
connection.execute("PRAGMA synchronous=NORMAL")
|
|
265
|
+
|
|
266
|
+
# 캐시 크기 증가 (기본값의 2배)
|
|
267
|
+
connection.execute("PRAGMA cache_size=4000")
|
|
268
|
+
|
|
269
|
+
Rails.logger.info "[SolidCable] SQLite WAL 모드 최적화 적용됨"
|
|
270
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
|
271
|
+
# 마이그레이션 전에는 연결이 없을 수 있음
|
|
272
|
+
Rails.logger.debug "[SolidCable] Cable 데이터베이스 연결 대기 중..."
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
RUBY
|
|
277
|
+
|
|
278
|
+
FileUtils.mkdir_p("config/initializers")
|
|
279
|
+
File.write(initializer_path, initializer_content)
|
|
280
|
+
puts " ✅ config/initializers/solid_cable_sqlite.rb를 생성했습니다.".colorize(:green)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def run_bundle_install
|
|
284
|
+
puts "📦 bundle install을 실행합니다...".colorize(:yellow)
|
|
285
|
+
|
|
286
|
+
if system("bundle install")
|
|
287
|
+
puts " ✅ bundle install 완료".colorize(:green)
|
|
288
|
+
else
|
|
289
|
+
puts " ❌ bundle install 실패".colorize(:red)
|
|
290
|
+
puts " 수동으로 bundle install을 실행해주세요.".colorize(:yellow)
|
|
291
|
+
exit 1
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def install_solid_cable
|
|
296
|
+
puts "🔌 Solid Cable을 설치합니다...".colorize(:yellow)
|
|
297
|
+
|
|
298
|
+
# solid_cable:install 태스크가 있는지 확인
|
|
299
|
+
if system("bin/rails solid_cable:install")
|
|
300
|
+
puts " ✅ Solid Cable 설치 완료".colorize(:green)
|
|
301
|
+
else
|
|
302
|
+
puts " ⚠️ solid_cable:install 태스크를 찾을 수 없습니다.".colorize(:yellow)
|
|
303
|
+
puts " 마이그레이션 파일을 직접 생성합니다...".colorize(:yellow)
|
|
304
|
+
create_cable_migration
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def create_cable_migration
|
|
309
|
+
# db/cable_migrate 디렉토리 생성
|
|
310
|
+
FileUtils.mkdir_p("db/cable_migrate")
|
|
311
|
+
|
|
312
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
313
|
+
migration_path = "db/cable_migrate/#{timestamp}_create_solid_cable_messages.rb"
|
|
314
|
+
|
|
315
|
+
migration_content = <<~RUBY
|
|
316
|
+
class CreateSolidCableMessages < ActiveRecord::Migration[7.2]
|
|
317
|
+
def change
|
|
318
|
+
create_table :solid_cable_messages do |t|
|
|
319
|
+
t.binary :channel, null: false, limit: 1024
|
|
320
|
+
t.binary :payload, null: false, limit: 536870912
|
|
321
|
+
t.datetime :created_at, null: false
|
|
322
|
+
t.integer :channel_hash, null: false, limit: 8
|
|
323
|
+
|
|
324
|
+
t.index :channel
|
|
325
|
+
t.index :channel_hash
|
|
326
|
+
t.index :created_at
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
RUBY
|
|
331
|
+
|
|
332
|
+
File.write(migration_path, migration_content)
|
|
333
|
+
puts " ✅ 마이그레이션 파일 생성: #{migration_path}".colorize(:green)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def run_migrations
|
|
337
|
+
puts "🗄️ 데이터베이스를 준비합니다...".colorize(:yellow)
|
|
338
|
+
|
|
339
|
+
# storage 디렉토리 생성
|
|
340
|
+
FileUtils.mkdir_p("storage")
|
|
341
|
+
|
|
342
|
+
# db:prepare는 마이그레이션 + 스키마 로드를 모두 처리
|
|
343
|
+
if system("bin/rails db:prepare")
|
|
344
|
+
puts " ✅ 데이터베이스 준비 완료".colorize(:green)
|
|
345
|
+
else
|
|
346
|
+
puts " ⚠️ 데이터베이스 준비 실패".colorize(:yellow)
|
|
347
|
+
puts " 수동으로 bin/rails db:prepare를 실행해주세요.".colorize(:yellow)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def create_documentation
|
|
352
|
+
puts "📄 설정 문서를 생성합니다...".colorize(:yellow)
|
|
353
|
+
|
|
354
|
+
FileUtils.mkdir_p("docs")
|
|
355
|
+
doc_path = "docs/solid-cable-sqlite-setup.md"
|
|
356
|
+
|
|
357
|
+
if File.exist?(doc_path)
|
|
358
|
+
puts " ℹ️ 문서가 이미 존재합니다.".colorize(:yellow)
|
|
359
|
+
return
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# 템플릿 파일에서 문서 내용 읽기
|
|
363
|
+
template_path = File.expand_path("../../templates/solid-cable-sqlite-setup.md", __FILE__)
|
|
364
|
+
doc_content = File.read(template_path)
|
|
365
|
+
|
|
366
|
+
File.write(doc_path, doc_content)
|
|
367
|
+
puts " ✅ docs/solid-cable-sqlite-setup.md를 생성했습니다.".colorize(:green)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|