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.
@@ -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
- @repo_name = repo_name
204
- @username = owner
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
- registry_url = "ghcr.io/#{@username.downcase}/#{@repo_name.downcase}"
228
- @registry_url = registry_url
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
- # 이미지 설정 업데이트 (ghcr.io 중복 제거)
278
- # @registry_url이미 ghcr.io포함하고 있으므로, 그대로 사용
279
- content.gsub!(/^image:\s+.*$/, "image: #{@registry_url}")
280
-
305
+
306
+ # 이미지 설정 업데이트
307
+ # Kamalregistry.server + image조합하므로 image에는 username/repo만 지정
308
+ content.gsub!(/^image:\s+.*$/, "image: #{@image_name}")
309
+
281
310
  # registry 섹션 업데이트
282
311
  if content.include?("registry:")
283
- # 기존 registry 섹션 수정
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
- # server 라인이 없으면 username 위에 추가
291
- content.gsub!(/(\s*username:\s+)/, " server: ghcr.io\n\\1")
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
- # 현재 GitHub 토큰 가져오기
317
- token_output = `gh auth token 2>/dev/null`
318
-
319
- if $?.success? && !token_output.strip.empty?
320
- token = token_output.strip
321
- secrets_file = ".kamal/secrets"
322
-
323
- # 기존 secrets 파일 읽기 (있다면)
324
- existing_content = File.exist?(secrets_file) ? File.read(secrets_file) : ""
325
-
326
- # KAMAL_REGISTRY_PASSWORD 이미 있는지 확인
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
- puts "⚠️ GitHub 토큰을 가져올 수 없습니다. 수동으로 설정해주세요:".colorize(:yellow)
342
- puts " echo 'KAMAL_REGISTRY_PASSWORD=your_github_token' >> .kamal/secrets".colorize(:cyan)
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)
@@ -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