tayo 0.1.13 → 0.2.2

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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "colorize"
4
- # require_relative "../dockerfile_modifier"
4
+ require_relative "../dockerfile_modifier"
5
5
 
6
6
  module Tayo
7
7
  module Commands
@@ -15,9 +15,9 @@ module Tayo
15
15
  end
16
16
  commit_initial_state
17
17
  check_orbstack
18
- create_welcome_page
18
+ create_welcome_page
19
19
  clear_docker_cache
20
- ensure_dockerfile_exists
20
+ ensure_dockerfile_exists
21
21
  commit_changes
22
22
  puts "✅ Tayo가 성공적으로 설정되었습니다!".colorize(:green)
23
23
  end
@@ -30,19 +30,19 @@ module Tayo
30
30
 
31
31
  def check_orbstack
32
32
  puts "🐳 OrbStack 상태를 확인합니다...".colorize(:yellow)
33
-
33
+
34
34
  # OrbStack 실행 상태 확인
35
35
  orbstack_running = system("pgrep -x OrbStack > /dev/null 2>&1")
36
-
36
+
37
37
  if orbstack_running
38
38
  puts "✅ OrbStack이 실행 중입니다.".colorize(:green)
39
39
  else
40
40
  puts "🚀 OrbStack을 시작합니다...".colorize(:yellow)
41
-
41
+
42
42
  # OrbStack 실행
43
43
  if system("open -a OrbStack")
44
44
  puts "✅ OrbStack이 시작되었습니다.".colorize(:green)
45
-
45
+
46
46
  # OrbStack이 완전히 시작될 때까지 잠시 대기
47
47
  print "Docker 서비스가 준비될 때까지 대기 중".colorize(:yellow)
48
48
  5.times do
@@ -50,7 +50,7 @@ module Tayo
50
50
  print ".".colorize(:yellow)
51
51
  end
52
52
  puts ""
53
-
53
+
54
54
  # Docker가 준비되었는지 확인
55
55
  if system("docker ps > /dev/null 2>&1")
56
56
  puts "✅ Docker가 준비되었습니다.".colorize(:green)
@@ -64,11 +64,11 @@ module Tayo
64
64
  end
65
65
  end
66
66
  end
67
-
67
+
68
68
  def ensure_dockerfile_exists
69
69
  unless File.exist?("Dockerfile")
70
70
  puts "🐳 Dockerfile이 없습니다. 기본 Dockerfile을 생성합니다...".colorize(:yellow)
71
-
71
+
72
72
  # Rails 7의 기본 Dockerfile 생성
73
73
  if system("rails app:update:bin")
74
74
  system("./bin/rails generate dockerfile")
@@ -80,7 +80,7 @@ module Tayo
80
80
  end
81
81
  else
82
82
  puts "✅ Dockerfile이 이미 존재합니다.".colorize(:green)
83
- end
83
+ end
84
84
  end
85
85
 
86
86
  def create_welcome_page
@@ -90,15 +90,15 @@ module Tayo
90
90
  @welcome_page_created = false
91
91
  return
92
92
  end
93
-
93
+
94
94
  puts "🎨 Welcome 페이지를 생성합니다...".colorize(:yellow)
95
-
95
+
96
96
  # Welcome 컨트롤러 생성
97
97
  system("rails generate controller Welcome index --skip-routes --no-helper --no-assets")
98
-
98
+
99
99
  # 프로젝트 이름 가져오기
100
100
  project_name = File.basename(Dir.pwd).gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
101
-
101
+
102
102
  # Welcome 페이지 HTML 생성
103
103
  welcome_html = <<~HTML
104
104
  <!DOCTYPE html>
@@ -188,7 +188,7 @@ module Tayo
188
188
  <div class="container">
189
189
  <h1>🏠 #{project_name}</h1>
190
190
  <p class="subtitle">Welcome to your Tayo-powered Rails application!</p>
191
-
191
+
192
192
  <div class="info-grid">
193
193
  <div class="info-card">
194
194
  <h3>📦 Container Ready</h3>
@@ -203,7 +203,7 @@ module Tayo
203
203
  <p>Domain management simplified</p>
204
204
  </div>
205
205
  </div>
206
-
206
+
207
207
  <div class="deploy-badge">
208
208
  Deployed with Tayo 🎉
209
209
  </div>
@@ -211,15 +211,15 @@ module Tayo
211
211
  </body>
212
212
  </html>
213
213
  HTML
214
-
214
+
215
215
  # Welcome 뷰 파일에 저장
216
216
  welcome_view_path = "app/views/welcome/index.html.erb"
217
217
  File.write(welcome_view_path, welcome_html)
218
-
218
+
219
219
  # routes.rb 업데이트
220
220
  routes_file = "config/routes.rb"
221
221
  routes_content = File.read(routes_file)
222
-
222
+
223
223
  # root 경로 설정 - welcome#index가 이미 있는지 확인
224
224
  unless routes_content.include?("welcome#index")
225
225
  if routes_content.match?(/^\s*root\s+/)
@@ -229,18 +229,18 @@ module Tayo
229
229
  # root 설정이 없으면 추가
230
230
  routes_content.gsub!(/Rails\.application\.routes\.draw do\s*\n/, "Rails.application.routes.draw do\n root 'welcome#index'\n")
231
231
  end
232
-
232
+
233
233
  File.write(routes_file, routes_content)
234
234
  puts " ✅ routes.rb에 root 경로를 설정했습니다.".colorize(:green)
235
235
  else
236
236
  puts " ℹ️ routes.rb에 welcome#index가 이미 설정되어 있습니다.".colorize(:yellow)
237
237
  end
238
-
238
+
239
239
  puts "✅ Welcome 페이지가 생성되었습니다!".colorize(:green)
240
240
  puts " 경로: /".colorize(:gray)
241
241
  puts " 컨트롤러: app/controllers/welcome_controller.rb".colorize(:gray)
242
242
  puts " 뷰: app/views/welcome/index.html.erb".colorize(:gray)
243
-
243
+
244
244
  @welcome_page_created = true
245
245
  end
246
246
 
@@ -250,20 +250,20 @@ module Tayo
250
250
  puts "⚠️ Git 저장소가 아닙니다. 커밋을 건너뜁니다.".colorize(:yellow)
251
251
  return
252
252
  end
253
-
253
+
254
254
  puts "📝 초기 상태를 Git에 커밋합니다...".colorize(:yellow)
255
-
255
+
256
256
  # Git 상태 확인
257
257
  git_status = `git status --porcelain`
258
-
258
+
259
259
  if git_status.strip.empty?
260
260
  puts "ℹ️ 커밋할 변경사항이 없습니다.".colorize(:yellow)
261
261
  return
262
262
  end
263
-
263
+
264
264
  # 변경사항 스테이징
265
265
  system("git add .")
266
-
266
+
267
267
  # 커밋
268
268
  commit_message = "Save current state before Tayo initialization"
269
269
  if system("git commit -m '#{commit_message}'")
@@ -280,20 +280,20 @@ module Tayo
280
280
  puts "⚠️ Git 저장소가 아닙니다. 커밋을 건너뜁니다.".colorize(:yellow)
281
281
  return
282
282
  end
283
-
283
+
284
284
  puts "📝 Tayo 설정 완료 상태를 Git에 커밋합니다...".colorize(:yellow)
285
-
285
+
286
286
  # Git 상태 확인
287
287
  git_status = `git status --porcelain`
288
-
288
+
289
289
  if git_status.strip.empty?
290
290
  puts "ℹ️ 커밋할 변경사항이 없습니다.".colorize(:yellow)
291
291
  return
292
292
  end
293
-
293
+
294
294
  # 변경사항 스테이징
295
295
  system("git add .")
296
-
296
+
297
297
  # 커밋
298
298
  commit_message = "Complete Tayo initialization with Welcome page and Docker setup"
299
299
  if system("git commit -m '#{commit_message}'")
@@ -306,14 +306,14 @@ module Tayo
306
306
 
307
307
  def clear_docker_cache
308
308
  puts "🧹 Docker 캐시를 정리합니다...".colorize(:yellow)
309
-
309
+
310
310
  # Docker system prune
311
311
  if system("docker system prune -f > /dev/null 2>&1")
312
312
  puts "✅ Docker 시스템 캐시가 정리되었습니다.".colorize(:green)
313
313
  else
314
314
  puts "⚠️ Docker 시스템 정리에 실패했습니다.".colorize(:yellow)
315
315
  end
316
-
316
+
317
317
  # Kamal build cache clear
318
318
  if File.exist?("config/deploy.yml")
319
319
  puts "🚢 Kamal 빌드 캐시를 정리합니다...".colorize(:yellow)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+ require "tty-prompt"
5
+ require_relative "../proxy/cloudflare_client"
6
+ require_relative "../proxy/docker_manager"
7
+ require_relative "../proxy/network_config"
8
+ require_relative "../proxy/traefik_config"
9
+ require_relative "../proxy/welcome_service"
10
+
11
+ module Tayo
12
+ module Commands
13
+ class Proxy
14
+ def execute
15
+ puts "🚀 Kamal Proxy와 Caddy 설정을 시작합니다...".colorize(:green)
16
+ puts ""
17
+
18
+ # 1. Cloudflare 설정
19
+ cloudflare = Tayo::Proxy::CloudflareClient.new
20
+ cloudflare.ensure_token
21
+
22
+ # 2. 네트워크 설정
23
+ network = Tayo::Proxy::NetworkConfig.new
24
+ network.detect_ips
25
+ network.configure_ports
26
+
27
+ # 3. Docker 확인
28
+ docker = Tayo::Proxy::DockerManager.new
29
+ docker.check_containers
30
+
31
+ # 4. 도메인 선택
32
+ selected_domains = cloudflare.select_domains
33
+
34
+ if selected_domains.empty?
35
+ puts "❌ 도메인이 선택되지 않았습니다. 종료합니다.".colorize(:red)
36
+ return
37
+ end
38
+
39
+ # 5. DNS 설정
40
+ puts "\n📝 DNS 레코드를 설정합니다...".colorize(:yellow)
41
+ cloudflare.setup_dns_records(selected_domains, network.public_ip)
42
+
43
+ # 6. Welcome 서비스 확인
44
+ welcome = Tayo::Proxy::WelcomeService.new
45
+ welcome.ensure_running
46
+
47
+ # 7. Traefik 설정
48
+ traefik = Tayo::Proxy::TraefikConfig.new
49
+ traefik.setup(selected_domains)
50
+
51
+ # 8. 최종 안내
52
+ show_summary(selected_domains, network)
53
+
54
+ rescue => e
55
+ puts "❌ 오류가 발생했습니다: #{e.message}".colorize(:red)
56
+ puts e.backtrace.join("\n") if ENV["DEBUG"]
57
+ exit 1
58
+ end
59
+
60
+ private
61
+
62
+ def show_summary(domains, network)
63
+ puts "\n" + "="*60
64
+ puts "✅ Proxy 설정이 완료되었습니다!".colorize(:green)
65
+ puts "="*60
66
+
67
+ puts "\n📋 설정 요약:".colorize(:yellow)
68
+ puts "━"*40
69
+ puts "공인 IP: #{network.public_ip}".colorize(:white)
70
+ puts "내부 IP: #{network.internal_ip}".colorize(:white)
71
+ puts "Traefik: 80, 443 포트 사용 중".colorize(:white)
72
+ puts "대시보드: http://localhost:8080".colorize(:white)
73
+ puts "━"*40
74
+
75
+ puts "\n🌐 활성화된 도메인:".colorize(:yellow)
76
+ domains.each do |domain|
77
+ if network.use_custom_ports?
78
+ puts "• #{domain}".colorize(:cyan)
79
+ puts " HTTP: http://#{domain}:#{network.external_http}".colorize(:gray)
80
+ puts " HTTPS: https://#{domain}:#{network.external_https}".colorize(:gray)
81
+ else
82
+ puts "• #{domain}".colorize(:cyan)
83
+ puts " HTTP: http://#{domain}".colorize(:gray)
84
+ puts " HTTPS: https://#{domain}".colorize(:gray)
85
+ end
86
+ end
87
+
88
+ if network.use_custom_ports?
89
+ puts "\n💡 공유기 포트포워딩을 설정하세요!".colorize(:yellow)
90
+ network.show_port_forwarding_guide
91
+ end
92
+
93
+ puts "\n🎉 모든 설정이 완료되었습니다!".colorize(:green)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+ require "tty-prompt"
5
+ require "net/http"
6
+ require "json"
7
+ require "uri"
8
+ require "fileutils"
9
+
10
+ module Tayo
11
+ module Proxy
12
+ class CloudflareClient
13
+ TOKEN_FILE = File.expand_path("~/.tayo/cloudflare_token")
14
+
15
+ def initialize
16
+ @prompt = TTY::Prompt.new
17
+ end
18
+
19
+ def ensure_token
20
+ @token = load_saved_token
21
+
22
+ if @token
23
+ puts "🔑 저장된 Cloudflare 토큰을 확인합니다...".colorize(:yellow)
24
+
25
+ if test_token(@token)
26
+ puts "✅ 저장된 Cloudflare 토큰이 유효합니다.".colorize(:green)
27
+ else
28
+ puts "⚠️ 저장된 토큰이 만료되었거나 유효하지 않습니다.".colorize(:yellow)
29
+ puts "새 토큰을 입력해주세요.".colorize(:cyan)
30
+ open_token_creation_page
31
+ @token = get_cloudflare_token
32
+ save_token(@token)
33
+ end
34
+ else
35
+ puts "\n🔑 Cloudflare API 토큰이 필요합니다.".colorize(:yellow)
36
+ open_token_creation_page
37
+ @token = get_cloudflare_token
38
+ save_token(@token)
39
+ end
40
+ end
41
+
42
+ def select_domains
43
+ puts "\n🌐 Cloudflare 도메인 목록을 조회합니다...".colorize(:yellow)
44
+
45
+ zones = get_cloudflare_zones
46
+
47
+ if zones.empty?
48
+ puts "❌ Cloudflare에 등록된 도메인이 없습니다.".colorize(:red)
49
+ puts "먼저 https://dash.cloudflare.com 에서 도메인을 추가해주세요.".colorize(:cyan)
50
+ return []
51
+ end
52
+
53
+ zone_choices = zones.map { |zone| "#{zone['name']} (#{zone['status']})" }
54
+
55
+ selected_zones = @prompt.multi_select(
56
+ "Caddy로 라우팅할 도메인을 선택하세요 (Space로 선택, Enter로 완료):",
57
+ zone_choices,
58
+ per_page: 10,
59
+ min: 1
60
+ )
61
+
62
+ # 선택된 항목이 없으면 빈 배열 반환
63
+ return [] if selected_zones.nil? || selected_zones.empty?
64
+
65
+ puts "DEBUG: selected_zones = #{selected_zones.inspect}" if ENV["DEBUG"]
66
+
67
+ selected_domains = selected_zones.map do |selection|
68
+ zone_name = selection.split(' ').first
69
+ zone = zones.find { |z| z['name'] == zone_name }
70
+
71
+ # 서브도메인 추가 여부 확인
72
+ if @prompt.yes?("#{zone_name}에 서브도메인을 추가하시겠습니까?")
73
+ subdomain = @prompt.ask("서브도메인을 입력하세요 (예: app, api):")
74
+ if subdomain && !subdomain.empty?
75
+ {
76
+ domain: "#{subdomain}.#{zone_name}",
77
+ zone_id: zone['id'],
78
+ zone_name: zone_name
79
+ }
80
+ else
81
+ {
82
+ domain: zone_name,
83
+ zone_id: zone['id'],
84
+ zone_name: zone_name
85
+ }
86
+ end
87
+ else
88
+ {
89
+ domain: zone_name,
90
+ zone_id: zone['id'],
91
+ zone_name: zone_name
92
+ }
93
+ end
94
+ end
95
+
96
+ puts "\n✅ 선택된 도메인:".colorize(:green)
97
+ selected_domains.each do |d|
98
+ puts " • #{d[:domain]}".colorize(:cyan)
99
+ end
100
+
101
+ puts "DEBUG: returning selected_domains = #{selected_domains.inspect}" if ENV["DEBUG"]
102
+
103
+ selected_domains
104
+ end
105
+
106
+ def setup_dns_records(domains, public_ip)
107
+ domains.each do |domain_info|
108
+ domain = domain_info[:domain]
109
+ zone_id = domain_info[:zone_id]
110
+
111
+ puts "\n⚙️ #{domain}의 DNS 레코드를 설정합니다...".colorize(:yellow)
112
+
113
+ # 기존 A 레코드 확인
114
+ existing_records = get_dns_records(zone_id, domain, ['A'])
115
+
116
+ if existing_records.any?
117
+ existing_record = existing_records.first
118
+ if existing_record['content'] == public_ip
119
+ puts "✅ #{domain} → #{public_ip} (이미 설정됨)".colorize(:green)
120
+ else
121
+ puts "⚠️ 기존 레코드를 업데이트합니다.".colorize(:yellow)
122
+ update_dns_record(zone_id, existing_record['id'], public_ip)
123
+ end
124
+ else
125
+ # 새 A 레코드 생성
126
+ create_dns_record(zone_id, domain, 'A', public_ip)
127
+ end
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def load_saved_token
134
+ return nil unless File.exist?(TOKEN_FILE)
135
+ File.read(TOKEN_FILE).strip
136
+ rescue
137
+ nil
138
+ end
139
+
140
+ def save_token(token)
141
+ dir = File.dirname(TOKEN_FILE)
142
+
143
+ # 디렉토리가 파일로 존재하는 경우 삭제
144
+ if File.exist?(dir) && !File.directory?(dir)
145
+ FileUtils.rm(dir)
146
+ end
147
+
148
+ # 디렉토리가 없을 때만 생성
149
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
150
+
151
+ File.write(TOKEN_FILE, token)
152
+ File.chmod(0600, TOKEN_FILE)
153
+ puts "✅ API 토큰이 저장되었습니다.".colorize(:green)
154
+ end
155
+
156
+ def open_token_creation_page
157
+ puts "토큰 생성 페이지를 엽니다...".colorize(:cyan)
158
+ system("open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null || xdg-open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null")
159
+
160
+ puts "\n다음 권한으로 토큰을 생성해주세요:".colorize(:yellow)
161
+ puts ""
162
+ puts "한국어 화면:".colorize(:gray)
163
+ puts "• 영역 → 영역 → 읽기".colorize(:white)
164
+ puts "• 영역 → DNS → 편집".colorize(:white)
165
+ puts " (영역 리소스는 '모든 영역' 선택)".colorize(:gray)
166
+ puts ""
167
+ puts "English:".colorize(:gray)
168
+ puts "• Zone → Zone → Read".colorize(:white)
169
+ puts "• Zone → DNS → Edit".colorize(:white)
170
+ puts " (Zone Resources: Select 'All zones')".colorize(:gray)
171
+ puts ""
172
+ end
173
+
174
+ def get_cloudflare_token
175
+ token = @prompt.mask("생성된 Cloudflare API 토큰을 붙여넣으세요:")
176
+
177
+ if token.nil? || token.strip.empty?
178
+ puts "❌ 토큰이 입력되지 않았습니다.".colorize(:red)
179
+ exit 1
180
+ end
181
+
182
+ if test_token(token.strip)
183
+ puts "✅ 토큰이 확인되었습니다.".colorize(:green)
184
+ return token.strip
185
+ else
186
+ puts "❌ 토큰이 올바르지 않거나 권한이 부족합니다.".colorize(:red)
187
+ exit 1
188
+ end
189
+ end
190
+
191
+ def test_token(token)
192
+ uri = URI('https://api.cloudflare.com/client/v4/user/tokens/verify')
193
+ http = Net::HTTP.new(uri.host, uri.port)
194
+ http.use_ssl = true
195
+
196
+ request = Net::HTTP::Get.new(uri)
197
+ request['Authorization'] = "Bearer #{token}"
198
+ request['Content-Type'] = 'application/json'
199
+
200
+ response = http.request(request)
201
+
202
+ if response.code == '200'
203
+ data = JSON.parse(response.body)
204
+ return data['success'] == true
205
+ else
206
+ return false
207
+ end
208
+ rescue => e
209
+ puts "⚠️ 토큰 검증 중 오류: #{e.message}".colorize(:yellow) if ENV["DEBUG"]
210
+ false
211
+ end
212
+
213
+ def get_cloudflare_zones
214
+ uri = URI('https://api.cloudflare.com/client/v4/zones')
215
+ http = Net::HTTP.new(uri.host, uri.port)
216
+ http.use_ssl = true
217
+
218
+ request = Net::HTTP::Get.new(uri)
219
+ request['Authorization'] = "Bearer #{@token}"
220
+ request['Content-Type'] = 'application/json'
221
+
222
+ response = http.request(request)
223
+
224
+ if response.code == '200'
225
+ data = JSON.parse(response.body)
226
+ return data['result'] || []
227
+ else
228
+ puts "❌ 도메인 목록 조회에 실패했습니다: #{response.code}".colorize(:red)
229
+ []
230
+ end
231
+ rescue => e
232
+ puts "❌ API 요청 중 오류가 발생했습니다: #{e.message}".colorize(:red)
233
+ []
234
+ end
235
+
236
+ def get_dns_records(zone_id, name, types)
237
+ records = []
238
+
239
+ types.each do |type|
240
+ uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
241
+ uri.query = URI.encode_www_form({
242
+ type: type,
243
+ name: name
244
+ })
245
+
246
+ http = Net::HTTP.new(uri.host, uri.port)
247
+ http.use_ssl = true
248
+
249
+ request = Net::HTTP::Get.new(uri)
250
+ request['Authorization'] = "Bearer #{@token}"
251
+ request['Content-Type'] = 'application/json'
252
+
253
+ response = http.request(request)
254
+
255
+ if response.code == '200'
256
+ data = JSON.parse(response.body)
257
+ records.concat(data['result'] || [])
258
+ end
259
+ end
260
+
261
+ records
262
+ rescue => e
263
+ puts "⚠️ DNS 레코드 조회 중 오류: #{e.message}".colorize(:yellow)
264
+ []
265
+ end
266
+
267
+ def create_dns_record(zone_id, name, type, content)
268
+ uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
269
+ http = Net::HTTP.new(uri.host, uri.port)
270
+ http.use_ssl = true
271
+
272
+ request = Net::HTTP::Post.new(uri)
273
+ request['Authorization'] = "Bearer #{@token}"
274
+ request['Content-Type'] = 'application/json'
275
+
276
+ data = {
277
+ type: type,
278
+ name: name,
279
+ content: content,
280
+ ttl: 300,
281
+ proxied: false
282
+ }
283
+
284
+ request.body = data.to_json
285
+ response = http.request(request)
286
+
287
+ if response.code == '200'
288
+ puts "✅ #{name} → #{content} (A 레코드 생성됨)".colorize(:green)
289
+ else
290
+ puts "❌ DNS 레코드 생성 실패: #{response.code}".colorize(:red)
291
+ puts response.body if ENV["DEBUG"]
292
+ end
293
+ rescue => e
294
+ puts "❌ DNS 레코드 생성 중 오류: #{e.message}".colorize(:red)
295
+ end
296
+
297
+ def update_dns_record(zone_id, record_id, new_content)
298
+ uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records/#{record_id}")
299
+ http = Net::HTTP.new(uri.host, uri.port)
300
+ http.use_ssl = true
301
+
302
+ request = Net::HTTP::Patch.new(uri)
303
+ request['Authorization'] = "Bearer #{@token}"
304
+ request['Content-Type'] = 'application/json'
305
+
306
+ data = {
307
+ content: new_content
308
+ }
309
+
310
+ request.body = data.to_json
311
+ response = http.request(request)
312
+
313
+ if response.code == '200'
314
+ puts "✅ DNS 레코드가 업데이트되었습니다.".colorize(:green)
315
+ else
316
+ puts "❌ DNS 레코드 업데이트 실패: #{response.code}".colorize(:red)
317
+ end
318
+ rescue => e
319
+ puts "❌ DNS 레코드 업데이트 중 오류: #{e.message}".colorize(:red)
320
+ end
321
+ end
322
+ end
323
+ end