tayo 0.2.0 → 0.2.3
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/docs/2025-11-29-cf-command-refactor.md +53 -0
- data/lib/tayo/cli.rb +6 -0
- data/lib/tayo/commands/cf.rb +142 -121
- data/lib/tayo/commands/gh.rb +0 -9
- 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 +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a128e0d938a02268ef73d7c92f956d9f316923eaf1ba7c8827f416a738e2365f
|
|
4
|
+
data.tar.gz: 6e8c5fa59202380639d5f44dc05ea1723b3df5018a15ae9ec56bb5acf2765f58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a08ad8b3b192257dbbae0cf19f007210f70e303d7b64727eb388d1381b45ac36d9b3ff9e2441490951c37eb7732da2cd0e4e14fa771fd0b3b58685f36f05490
|
|
7
|
+
data.tar.gz: 53919de826cf6970b5cd96c9bbeda335c6346df981cd9f46b7790c8f5fb310baf2362b704477ee4a5d5f84d487260ec794ece693fc67bcebbe9d9278acd22943
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# tayo cf 명령어 리팩토링
|
|
2
|
+
|
|
3
|
+
**날짜**: 2025-11-29
|
|
4
|
+
|
|
5
|
+
## 변경 사항
|
|
6
|
+
|
|
7
|
+
### 문제점
|
|
8
|
+
|
|
9
|
+
1. **흐름이 어색함**: 도메인 입력을 먼저 받고 나서 Cloudflare Zone 목록을 보여주는 순서가 비직관적
|
|
10
|
+
2. **기존 레코드 확인 버그**: `check_existing_records`가 `domain_info`를 파라미터로 받지만 사용하지 않고, 항상 루트 도메인만 조회
|
|
11
|
+
3. **권한 안내 오류**: DNS 편집 권한만 있으면 읽기도 가능한데, 불필요하게 읽기 권한도 안내
|
|
12
|
+
|
|
13
|
+
### 해결
|
|
14
|
+
|
|
15
|
+
#### 새로운 흐름
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
기존: 변경 후:
|
|
19
|
+
1. 도메인 입력 1. 토큰 입력
|
|
20
|
+
2. 토큰 입력 2. Zone 선택
|
|
21
|
+
3. Zone 선택 3. 기존 레코드 목록 표시
|
|
22
|
+
4. 기존 레코드 확인 (버그) 4. 서비스 도메인 입력
|
|
23
|
+
5. DNS 레코드 설정 5. 홈서버 연결 정보 입력
|
|
24
|
+
6. deploy.yml 업데이트 6. DNS 레코드 생성/수정
|
|
25
|
+
7. Git 커밋 7. deploy.yml 업데이트
|
|
26
|
+
8. Git 커밋
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### 주요 변경 내용
|
|
30
|
+
|
|
31
|
+
1. **`show_existing_records` 메서드 추가**
|
|
32
|
+
- Zone 전체의 A/CNAME 레코드를 조회하여 표시
|
|
33
|
+
- `get_all_dns_records` 헬퍼 메서드로 name 필터 없이 조회
|
|
34
|
+
|
|
35
|
+
2. **`get_domain_input` 수정**
|
|
36
|
+
- Zone 정보를 파라미터로 받아서 활용
|
|
37
|
+
- 서브도메인 사용 여부를 먼저 질문 후 입력받음
|
|
38
|
+
|
|
39
|
+
3. **`get_server_info` 메서드 분리**
|
|
40
|
+
- 기존 `setup_dns_record`에서 서버 정보 입력 로직 분리
|
|
41
|
+
- "홈서버 연결 정보" 단계로 명명
|
|
42
|
+
|
|
43
|
+
4. **`setup_dns_record` 단순화**
|
|
44
|
+
- 인스턴스 변수 대신 파라미터로 데이터 전달
|
|
45
|
+
- `determine_final_domain` 로직 제거 (사용자가 직접 선택)
|
|
46
|
+
|
|
47
|
+
5. **권한 안내 수정**
|
|
48
|
+
- "영역 → DNS → 읽기" 제거
|
|
49
|
+
- "영역 → DNS → 편집"만 안내
|
|
50
|
+
|
|
51
|
+
## 파일 변경
|
|
52
|
+
|
|
53
|
+
- `lib/tayo/commands/cf.rb`: 143 insertions, 113 deletions
|
data/lib/tayo/cli.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "commands/init"
|
|
|
6
6
|
require_relative "commands/gh"
|
|
7
7
|
require_relative "commands/cf"
|
|
8
8
|
require_relative "commands/proxy"
|
|
9
|
+
require_relative "commands/sqlite"
|
|
9
10
|
|
|
10
11
|
module Tayo
|
|
11
12
|
class CLI < Thor
|
|
@@ -29,6 +30,11 @@ module Tayo
|
|
|
29
30
|
Commands::Proxy.new.execute
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
desc "sqlite", "SQLite + Solid Cable 최적화 설정을 적용합니다"
|
|
34
|
+
def sqlite
|
|
35
|
+
Commands::Sqlite.new.execute
|
|
36
|
+
end
|
|
37
|
+
|
|
32
38
|
desc "version", "Tayo 버전을 표시합니다"
|
|
33
39
|
def version
|
|
34
40
|
puts "Tayo #{VERSION}"
|
data/lib/tayo/commands/cf.rb
CHANGED
|
@@ -12,61 +12,78 @@ module Tayo
|
|
|
12
12
|
def execute
|
|
13
13
|
puts "☁️ Cloudflare DNS 설정을 시작합니다...".colorize(:green)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
puts "❌ Rails 프로젝트가 아닙니다. Rails 프로젝트 루트에서 실행해주세요.".colorize(:red)
|
|
17
|
-
return
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# 1. 도메인 입력받기
|
|
21
|
-
domain_info = get_domain_input
|
|
22
|
-
|
|
23
|
-
# 2. Cloudflare 토큰 생성 페이지 열기 및 권한 안내
|
|
15
|
+
# 1. Cloudflare 토큰 생성 페이지 열기 및 권한 안내
|
|
24
16
|
open_token_creation_page
|
|
25
|
-
|
|
26
|
-
#
|
|
17
|
+
|
|
18
|
+
# 2. 토큰 입력받기
|
|
27
19
|
token = get_cloudflare_token
|
|
28
|
-
|
|
29
|
-
#
|
|
20
|
+
|
|
21
|
+
# 3. Cloudflare API로 도메인 목록 조회 및 선택
|
|
30
22
|
selected_zone = select_cloudflare_zone(token)
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
23
|
+
|
|
24
|
+
# 4. 기존 레코드 목록 표시
|
|
25
|
+
show_existing_records(token, selected_zone)
|
|
26
|
+
|
|
27
|
+
# 5. 서비스 도메인 입력받기
|
|
28
|
+
domain_info = get_domain_input(selected_zone)
|
|
29
|
+
|
|
30
|
+
# 6. 홈서버 연결 정보 입력받기
|
|
31
|
+
server_info = get_server_info
|
|
32
|
+
|
|
33
|
+
# 7. DNS 레코드 추가/수정
|
|
34
|
+
setup_dns_record(token, selected_zone, domain_info, server_info)
|
|
35
|
+
|
|
36
|
+
# 8. config/deploy.yml 업데이트
|
|
37
|
+
update_deploy_config(domain_info, server_info)
|
|
40
38
|
|
|
41
39
|
puts "\n🎉 Cloudflare DNS 설정이 완료되었습니다!".colorize(:green)
|
|
42
|
-
|
|
40
|
+
|
|
43
41
|
# 변경사항 커밋
|
|
44
42
|
commit_cloudflare_changes(domain_info)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
private
|
|
48
46
|
|
|
49
|
-
def
|
|
50
|
-
|
|
47
|
+
def get_domain_input(zone)
|
|
48
|
+
prompt = TTY::Prompt.new
|
|
49
|
+
zone_name = zone['name']
|
|
50
|
+
|
|
51
|
+
puts "📝 서비스 도메인을 설정합니다.".colorize(:yellow)
|
|
52
|
+
puts " 선택된 Zone: #{zone_name}".colorize(:gray)
|
|
53
|
+
|
|
54
|
+
use_subdomain = prompt.yes?("서브도메인을 사용하시겠습니까? (예: app.#{zone_name})")
|
|
55
|
+
|
|
56
|
+
if use_subdomain
|
|
57
|
+
subdomain = prompt.ask("서브도메인을 입력하세요 (예: app, api, www):") do |q|
|
|
58
|
+
q.validate(/\A[a-zA-Z0-9-]+\z/, "올바른 서브도메인을 입력해주세요 (영문, 숫자, 하이픈만 가능)")
|
|
59
|
+
end
|
|
60
|
+
full_domain = "#{subdomain}.#{zone_name}"
|
|
61
|
+
{ type: :subdomain, domain: full_domain, zone: zone_name, subdomain: subdomain }
|
|
62
|
+
else
|
|
63
|
+
{ type: :root, domain: zone_name, zone: zone_name }
|
|
64
|
+
end
|
|
51
65
|
end
|
|
52
66
|
|
|
53
|
-
def
|
|
67
|
+
def get_server_info
|
|
54
68
|
prompt = TTY::Prompt.new
|
|
55
|
-
|
|
56
|
-
puts "\n
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
q.validate(/\A
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# 도메인이 루트인지 서브도메인인지 판단
|
|
63
|
-
parts = domain.split('.')
|
|
64
|
-
if parts.length == 2
|
|
65
|
-
{ type: :root, domain: domain, zone: domain }
|
|
66
|
-
else
|
|
67
|
-
zone = parts[-2..-1].join('.')
|
|
68
|
-
{ type: :subdomain, domain: domain, zone: zone, subdomain: parts[0..-3].join('.') }
|
|
69
|
+
|
|
70
|
+
puts "\n🖥️ 홈서버 연결 정보를 입력합니다.".colorize(:yellow)
|
|
71
|
+
|
|
72
|
+
server_address = prompt.ask("홈서버 IP 또는 도메인을 입력하세요:") do |q|
|
|
73
|
+
q.validate(/\A.+\z/, "서버 정보를 입력해주세요")
|
|
69
74
|
end
|
|
75
|
+
|
|
76
|
+
ssh_user = prompt.ask("SSH 사용자 계정을 입력하세요:", default: "root")
|
|
77
|
+
|
|
78
|
+
# IP인지 도메인인지 판단
|
|
79
|
+
is_ip = server_address.match?(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/)
|
|
80
|
+
record_type = is_ip ? 'A' : 'CNAME'
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
address: server_address,
|
|
84
|
+
ssh_user: ssh_user,
|
|
85
|
+
record_type: record_type
|
|
86
|
+
}
|
|
70
87
|
end
|
|
71
88
|
|
|
72
89
|
def open_token_creation_page
|
|
@@ -79,12 +96,10 @@ module Tayo
|
|
|
79
96
|
puts "\n다음 권한으로 토큰을 생성해주세요:".colorize(:yellow)
|
|
80
97
|
puts ""
|
|
81
98
|
puts "한국어 화면:".colorize(:gray)
|
|
82
|
-
puts "• 영역 → DNS → 읽기".colorize(:white)
|
|
83
99
|
puts "• 영역 → DNS → 편집".colorize(:white)
|
|
84
100
|
puts " (영역 리소스는 '모든 영역' 선택)".colorize(:gray)
|
|
85
101
|
puts ""
|
|
86
102
|
puts "English:".colorize(:gray)
|
|
87
|
-
puts "• Zone → DNS → Read".colorize(:white)
|
|
88
103
|
puts "• Zone → DNS → Edit".colorize(:white)
|
|
89
104
|
puts " (Zone Resources: Select 'All zones')".colorize(:gray)
|
|
90
105
|
puts ""
|
|
@@ -171,18 +186,53 @@ module Tayo
|
|
|
171
186
|
exit 1
|
|
172
187
|
end
|
|
173
188
|
|
|
174
|
-
def
|
|
189
|
+
def show_existing_records(token, zone)
|
|
175
190
|
puts "\n🔍 기존 DNS 레코드를 확인합니다...".colorize(:yellow)
|
|
176
|
-
|
|
191
|
+
|
|
177
192
|
zone_id = zone['id']
|
|
178
193
|
zone_name = zone['name']
|
|
179
|
-
|
|
180
|
-
#
|
|
181
|
-
records =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
|
|
195
|
+
# Zone의 모든 A/CNAME 레코드 조회
|
|
196
|
+
records = get_all_dns_records(token, zone_id, ['A', 'CNAME'])
|
|
197
|
+
|
|
198
|
+
if records.empty?
|
|
199
|
+
puts " 등록된 A/CNAME 레코드가 없습니다.".colorize(:gray)
|
|
200
|
+
else
|
|
201
|
+
puts " #{zone_name}의 기존 레코드:".colorize(:cyan)
|
|
202
|
+
records.each do |record|
|
|
203
|
+
puts " • #{record['name']} → #{record['content']} (#{record['type']})".colorize(:white)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
puts ""
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def get_all_dns_records(token, zone_id, types)
|
|
211
|
+
records = []
|
|
212
|
+
|
|
213
|
+
types.each do |type|
|
|
214
|
+
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
|
|
215
|
+
uri.query = URI.encode_www_form({ type: type })
|
|
216
|
+
|
|
217
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
218
|
+
http.use_ssl = true
|
|
219
|
+
|
|
220
|
+
request = Net::HTTP::Get.new(uri)
|
|
221
|
+
request['Authorization'] = "Bearer #{token}"
|
|
222
|
+
request['Content-Type'] = 'application/json'
|
|
223
|
+
|
|
224
|
+
response = http.request(request)
|
|
225
|
+
|
|
226
|
+
if response.code == '200'
|
|
227
|
+
data = JSON.parse(response.body)
|
|
228
|
+
records.concat(data['result'] || [])
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
records.sort_by { |r| r['name'] }
|
|
233
|
+
rescue => e
|
|
234
|
+
puts "⚠️ DNS 레코드 조회 중 오류: #{e.message}".colorize(:yellow)
|
|
235
|
+
[]
|
|
186
236
|
end
|
|
187
237
|
|
|
188
238
|
def get_dns_records(token, zone_id, name, types)
|
|
@@ -216,73 +266,41 @@ module Tayo
|
|
|
216
266
|
return []
|
|
217
267
|
end
|
|
218
268
|
|
|
219
|
-
def setup_dns_record(token, zone, domain_info,
|
|
269
|
+
def setup_dns_record(token, zone, domain_info, server_info)
|
|
220
270
|
puts "\n⚙️ DNS 레코드를 설정합니다...".colorize(:yellow)
|
|
221
|
-
|
|
222
|
-
# 홈서버 IP/URL 입력받기
|
|
223
|
-
prompt = TTY::Prompt.new
|
|
224
|
-
|
|
225
|
-
server_info = prompt.ask("홈서버 IP 또는 도메인을 입력하세요:") do |q|
|
|
226
|
-
q.validate(/\A.+\z/, "서버 정보를 입력해주세요")
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# SSH 사용자 계정 입력받기
|
|
230
|
-
ssh_user = prompt.ask("SSH 사용자 계정을 입력하세요:", default: "root")
|
|
231
|
-
|
|
232
|
-
# IP인지 도메인인지 판단
|
|
233
|
-
is_ip = server_info.match?(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/)
|
|
234
|
-
record_type = is_ip ? 'A' : 'CNAME'
|
|
235
|
-
|
|
271
|
+
|
|
236
272
|
zone_id = zone['id']
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
273
|
+
target_domain = domain_info[:domain]
|
|
274
|
+
server_address = server_info[:address]
|
|
275
|
+
record_type = server_info[:record_type]
|
|
276
|
+
|
|
277
|
+
# 대상 도메인의 기존 A/CNAME 레코드 확인
|
|
278
|
+
existing_records = get_dns_records(token, zone_id, target_domain, ['A', 'CNAME'])
|
|
279
|
+
|
|
280
|
+
if existing_records.any?
|
|
281
|
+
existing_record = existing_records.first
|
|
282
|
+
|
|
248
283
|
# 동일한 타입이고 같은 값이면 건너뛰기
|
|
249
|
-
if existing_record['type'] == record_type && existing_record['content'] ==
|
|
284
|
+
if existing_record['type'] == record_type && existing_record['content'] == server_address
|
|
250
285
|
puts "✅ DNS 레코드가 이미 올바르게 설정되어 있습니다.".colorize(:green)
|
|
251
|
-
puts " #{
|
|
286
|
+
puts " #{target_domain} → #{server_address} (#{record_type} 레코드)".colorize(:gray)
|
|
252
287
|
else
|
|
253
288
|
# 타입이 다르거나 값이 다른 경우 삭제 후 재생성
|
|
254
289
|
puts "⚠️ 기존 레코드를 삭제하고 새로 생성합니다.".colorize(:yellow)
|
|
255
|
-
puts " 기존: #{existing_record['content']} (#{existing_record['type']}) → 새로운: #{
|
|
256
|
-
|
|
290
|
+
puts " 기존: #{existing_record['content']} (#{existing_record['type']}) → 새로운: #{server_address} (#{record_type})".colorize(:gray)
|
|
291
|
+
|
|
257
292
|
# 기존 레코드 삭제
|
|
258
293
|
delete_dns_record(token, zone_id, existing_record['id'])
|
|
259
|
-
|
|
294
|
+
|
|
260
295
|
# 새 레코드 생성
|
|
261
|
-
create_dns_record(token, zone_id,
|
|
296
|
+
create_dns_record(token, zone_id, target_domain, record_type, server_address)
|
|
262
297
|
end
|
|
263
298
|
else
|
|
264
299
|
# DNS 레코드 생성
|
|
265
|
-
create_dns_record(token, zone_id,
|
|
300
|
+
create_dns_record(token, zone_id, target_domain, record_type, server_address)
|
|
266
301
|
end
|
|
267
|
-
|
|
268
|
-
# 최종 도메인 정보 저장
|
|
269
|
-
@final_domain = final_domain[:full_domain]
|
|
270
|
-
@server_info = server_info
|
|
271
|
-
@ssh_user = ssh_user
|
|
272
|
-
end
|
|
273
302
|
|
|
274
|
-
|
|
275
|
-
case domain_info[:type]
|
|
276
|
-
when :root
|
|
277
|
-
if existing_records.any?
|
|
278
|
-
puts "⚠️ 루트 도메인에 이미 레코드가 있습니다. app.#{zone_name}을 사용합니다.".colorize(:yellow)
|
|
279
|
-
{ name: "app.#{zone_name}", full_domain: "app.#{zone_name}" }
|
|
280
|
-
else
|
|
281
|
-
{ name: zone_name, full_domain: zone_name }
|
|
282
|
-
end
|
|
283
|
-
when :subdomain
|
|
284
|
-
{ name: domain_info[:domain], full_domain: domain_info[:domain] }
|
|
285
|
-
end
|
|
303
|
+
puts " #{target_domain} → #{server_address}".colorize(:cyan)
|
|
286
304
|
end
|
|
287
305
|
|
|
288
306
|
def create_dns_record(token, zone_id, name, type, content)
|
|
@@ -340,53 +358,56 @@ module Tayo
|
|
|
340
358
|
exit 1
|
|
341
359
|
end
|
|
342
360
|
|
|
343
|
-
def update_deploy_config(domain_info)
|
|
361
|
+
def update_deploy_config(domain_info, server_info)
|
|
344
362
|
puts "\n📝 배포 설정을 업데이트합니다...".colorize(:yellow)
|
|
345
|
-
|
|
363
|
+
|
|
346
364
|
config_file = "config/deploy.yml"
|
|
347
|
-
|
|
365
|
+
final_domain = domain_info[:domain]
|
|
366
|
+
server_address = server_info[:address]
|
|
367
|
+
ssh_user = server_info[:ssh_user]
|
|
368
|
+
|
|
348
369
|
unless File.exist?(config_file)
|
|
349
370
|
puts "⚠️ config/deploy.yml 파일이 없습니다.".colorize(:yellow)
|
|
350
371
|
return
|
|
351
372
|
end
|
|
352
|
-
|
|
373
|
+
|
|
353
374
|
content = File.read(config_file)
|
|
354
|
-
|
|
375
|
+
|
|
355
376
|
# proxy.host 설정 업데이트
|
|
356
377
|
if content.include?("proxy:")
|
|
357
|
-
content.gsub!(/(\s+host:\s+).*$/, "\\1#{
|
|
378
|
+
content.gsub!(/(\s+host:\s+).*$/, "\\1#{final_domain}")
|
|
358
379
|
else
|
|
359
380
|
# proxy 섹션이 없으면 추가
|
|
360
|
-
proxy_config = "\n# Proxy configuration\nproxy:\n ssl: true\n host: #{
|
|
381
|
+
proxy_config = "\n# Proxy configuration\nproxy:\n ssl: true\n host: #{final_domain}\n"
|
|
361
382
|
content += proxy_config
|
|
362
383
|
end
|
|
363
|
-
|
|
384
|
+
|
|
364
385
|
# servers 설정 업데이트
|
|
365
386
|
if content.match?(/servers:\s*\n\s*web:\s*\n\s*-\s*/)
|
|
366
|
-
content.gsub!(/(\s*servers:\s*\n\s*web:\s*\n\s*-\s*)[\d.]+/, "\\1#{
|
|
387
|
+
content.gsub!(/(\s*servers:\s*\n\s*web:\s*\n\s*-\s*)[\d.]+/, "\\1#{server_address}")
|
|
367
388
|
end
|
|
368
|
-
|
|
389
|
+
|
|
369
390
|
# ssh user 설정 업데이트
|
|
370
|
-
if
|
|
391
|
+
if ssh_user && ssh_user != "root"
|
|
371
392
|
if content.match?(/^ssh:/)
|
|
372
393
|
# 기존 ssh 섹션 업데이트
|
|
373
|
-
content.gsub!(/^ssh:\s*\n\s*user:\s*\w+/, "ssh:\n user: #{
|
|
394
|
+
content.gsub!(/^ssh:\s*\n\s*user:\s*\w+/, "ssh:\n user: #{ssh_user}")
|
|
374
395
|
else
|
|
375
396
|
# ssh 섹션 추가 (accessories 섹션 앞에 추가)
|
|
376
397
|
if content.match?(/^# Use accessory services/)
|
|
377
|
-
content.gsub!(/^# Use accessory services/, "# Use a different ssh user than root\nssh:\n user: #{
|
|
398
|
+
content.gsub!(/^# Use accessory services/, "# Use a different ssh user than root\nssh:\n user: #{ssh_user}\n\n# Use accessory services")
|
|
378
399
|
else
|
|
379
400
|
# 파일 끝에 추가
|
|
380
|
-
content += "\n# Use a different ssh user than root\nssh:\n user: #{
|
|
401
|
+
content += "\n# Use a different ssh user than root\nssh:\n user: #{ssh_user}\n"
|
|
381
402
|
end
|
|
382
403
|
end
|
|
383
404
|
end
|
|
384
|
-
|
|
405
|
+
|
|
385
406
|
File.write(config_file, content)
|
|
386
407
|
puts "✅ config/deploy.yml이 업데이트되었습니다.".colorize(:green)
|
|
387
|
-
puts " proxy.host: #{
|
|
388
|
-
puts " servers.web: #{
|
|
389
|
-
puts " ssh.user: #{
|
|
408
|
+
puts " proxy.host: #{final_domain}".colorize(:gray)
|
|
409
|
+
puts " servers.web: #{server_address}".colorize(:gray)
|
|
410
|
+
puts " ssh.user: #{ssh_user}".colorize(:gray) if ssh_user && ssh_user != "root"
|
|
390
411
|
end
|
|
391
412
|
|
|
392
413
|
def commit_cloudflare_changes(domain_info)
|
data/lib/tayo/commands/gh.rb
CHANGED
|
@@ -12,11 +12,6 @@ module Tayo
|
|
|
12
12
|
def execute
|
|
13
13
|
puts "🚀 GitHub 저장소 및 컨테이너 레지스트리 설정을 시작합니다...".colorize(:green)
|
|
14
14
|
|
|
15
|
-
unless rails_project?
|
|
16
|
-
puts "❌ Rails 프로젝트가 아닙니다. Rails 프로젝트 루트에서 실행해주세요.".colorize(:red)
|
|
17
|
-
return
|
|
18
|
-
end
|
|
19
|
-
|
|
20
15
|
puts "\n[1/7] GitHub CLI 설치 확인".colorize(:blue)
|
|
21
16
|
check_github_cli
|
|
22
17
|
|
|
@@ -50,10 +45,6 @@ module Tayo
|
|
|
50
45
|
|
|
51
46
|
private
|
|
52
47
|
|
|
53
|
-
def rails_project?
|
|
54
|
-
File.exist?("Gemfile") && File.exist?("config/application.rb")
|
|
55
|
-
end
|
|
56
|
-
|
|
57
48
|
def check_github_cli
|
|
58
49
|
if system("gh --version", out: File::NULL, err: File::NULL)
|
|
59
50
|
puts "✅ GitHub CLI가 이미 설치되어 있습니다.".colorize(:green)
|
|
@@ -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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Solid Cable + SQLite 최적화 설정
|
|
2
|
+
|
|
3
|
+
> 이 문서는 `tayo sqlite` 명령어로 자동 생성되었습니다.
|
|
4
|
+
|
|
5
|
+
## 개요
|
|
6
|
+
|
|
7
|
+
이 프로젝트는 Redis 없이 SQLite만으로 Action Cable 실시간 기능을 구현합니다.
|
|
8
|
+
Rails 8에서 도입된 Solid Cable을 사용하여 WebSocket 브로드캐스트를 처리합니다.
|
|
9
|
+
|
|
10
|
+
## 왜 Solid Cable인가?
|
|
11
|
+
|
|
12
|
+
### Redis vs Solid Cable
|
|
13
|
+
|
|
14
|
+
| 항목 | Redis | Solid Cable |
|
|
15
|
+
|------|-------|-------------|
|
|
16
|
+
| 의존성 | Redis 서버 필요 | DB만 사용 |
|
|
17
|
+
| 방식 | Pub/Sub (푸시) | Polling (폴링) |
|
|
18
|
+
| 지연시간 | ~1ms | ~25ms (설정값) |
|
|
19
|
+
| 운영 복잡도 | Redis 관리 필요 | 단순 |
|
|
20
|
+
|
|
21
|
+
### 체감 성능
|
|
22
|
+
|
|
23
|
+
- **25ms 폴링**: 사용자가 지연을 체감하기 어려움 (200ms 이하는 "즉각적"으로 느껴짐)
|
|
24
|
+
- **RTT 비교**: Redis ~80ms vs Solid Cable ~85ms (전체 왕복 시간 기준, 거의 동일)
|
|
25
|
+
|
|
26
|
+
## 적용된 설정
|
|
27
|
+
|
|
28
|
+
### 1. 데이터베이스 분리 (database.yml)
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
production:
|
|
32
|
+
primary:
|
|
33
|
+
database: storage/production.sqlite3
|
|
34
|
+
cable:
|
|
35
|
+
database: storage/production_cable.sqlite3 # 별도 DB
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**이유**: Cable 폴링이 메인 DB의 쓰기 작업과 락 경합하지 않도록 분리
|
|
39
|
+
|
|
40
|
+
### 2. Cable 설정 (cable.yml)
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
development:
|
|
44
|
+
adapter: async # 단일 프로세스, 콘솔 디버깅 용이
|
|
45
|
+
|
|
46
|
+
production:
|
|
47
|
+
adapter: solid_cable
|
|
48
|
+
polling_interval: 0.025.seconds # 25ms
|
|
49
|
+
message_retention: 1.hour
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**폴링 간격 선택 기준**:
|
|
53
|
+
- 100ms: 기본값, 대부분 충분
|
|
54
|
+
- 25ms: 채팅/실시간 앱에 적합
|
|
55
|
+
- 10ms: Redis 수준, 부하 증가
|
|
56
|
+
|
|
57
|
+
### 3. SQLite WAL 모드 최적화 (initializer)
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# config/initializers/solid_cable_sqlite.rb
|
|
61
|
+
PRAGMA journal_mode=WAL # 읽기/쓰기 동시 처리
|
|
62
|
+
PRAGMA synchronous=NORMAL # 쓰기 성능 향상
|
|
63
|
+
PRAGMA cache_size=4000 # 캐시 증가
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**WAL 모드 장점**:
|
|
67
|
+
- 읽기(폴링)와 쓰기(브로드캐스트)가 동시에 가능
|
|
68
|
+
- Solid Cable의 폴링 패턴에 최적화
|
|
69
|
+
|
|
70
|
+
## 성능 가이드
|
|
71
|
+
|
|
72
|
+
### 적정 사용 범위
|
|
73
|
+
|
|
74
|
+
| 동시 접속 | 메시지/초 | 예상 지연 |
|
|
75
|
+
|----------|----------|----------|
|
|
76
|
+
| ~50명 | ~10 | 거의 없음 |
|
|
77
|
+
| ~100명 | ~20 | ~50ms |
|
|
78
|
+
| ~500명 | ~100 | 검토 필요 |
|
|
79
|
+
|
|
80
|
+
### 스케일 아웃이 필요한 경우
|
|
81
|
+
|
|
82
|
+
- 초당 200개 이상 메시지
|
|
83
|
+
- 1000명 이상 동시 접속
|
|
84
|
+
- → Redis 또는 PostgreSQL NOTIFY 고려
|
|
85
|
+
|
|
86
|
+
## 참고 자료
|
|
87
|
+
|
|
88
|
+
- [Solid Cable GitHub](https://github.com/rails/solid_cable)
|
|
89
|
+
- [SQLite WAL Mode](https://sqlite.org/wal.html)
|
|
90
|
+
- [Rails 8 Release Notes](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required)
|
data/lib/tayo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tayo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 이원섭wonsup Lee/Alfonso
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- CHANGELOG.md
|
|
93
93
|
- README.md
|
|
94
94
|
- Rakefile
|
|
95
|
+
- docs/2025-11-29-cf-command-refactor.md
|
|
95
96
|
- exe/tayo
|
|
96
97
|
- lib/tayo.rb
|
|
97
98
|
- lib/tayo/cli.rb
|
|
@@ -99,12 +100,14 @@ files:
|
|
|
99
100
|
- lib/tayo/commands/gh.rb
|
|
100
101
|
- lib/tayo/commands/init.rb
|
|
101
102
|
- lib/tayo/commands/proxy.rb
|
|
103
|
+
- lib/tayo/commands/sqlite.rb
|
|
102
104
|
- lib/tayo/dockerfile_modifier.rb
|
|
103
105
|
- lib/tayo/proxy/cloudflare_client.rb
|
|
104
106
|
- lib/tayo/proxy/docker_manager.rb
|
|
105
107
|
- lib/tayo/proxy/network_config.rb
|
|
106
108
|
- lib/tayo/proxy/traefik_config.rb
|
|
107
109
|
- lib/tayo/proxy/welcome_service.rb
|
|
110
|
+
- lib/tayo/templates/solid-cable-sqlite-setup.md
|
|
108
111
|
- lib/tayo/version.rb
|
|
109
112
|
- lib/templates/welcome/Dockerfile
|
|
110
113
|
- lib/templates/welcome/index.html
|