feat: add terminal-style pricing table with region filtering
- Add dynamic pricing section with kubectl-style table UI - Implement region tabs (Tokyo 1-3, Osaka 1-2, Seoul 1, Singapore 1) - Integrate with existing /api/pricing endpoint - Add localStorage caching with 1-hour TTL - Apply terminal theme (Fira Code, GitHub Dark colors) - Sort by price, filter by region Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
228
js/pricing.js
228
js/pricing.js
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Pricing Table Component
|
||||
* API 연동 가격표 (아시아 전용, 캐싱 적용)
|
||||
* Anvil 가격표 (D1 직접 조회)
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -8,32 +8,27 @@
|
||||
*/
|
||||
export function pricingTable() {
|
||||
// 캐시 키 및 유효 시간 (1시간)
|
||||
const CACHE_KEY = 'anvil_pricing_cache_v3'; // v3: Global 탭 통합 (도쿄/오사카/싱가폴)
|
||||
const CACHE_KEY = 'anvil_pricing_cache_v6'; // v6: D1 API 형식 호환
|
||||
const CACHE_TTL = 60 * 60 * 1000; // 1시간
|
||||
|
||||
// Fallback 데이터 (API 실패 시 사용) - Linode: Tokyo R1/R2/Osaka/Singapore, Vultr: Seoul
|
||||
// Fallback 데이터 (API 실패 시 사용)
|
||||
const FALLBACK_DATA = [
|
||||
{ id: 'f1', vcpu: 1, memory_mb: 1024, storage_gb: 25, transfer_tb: 1, provider: { name: 'linode' }, region: { region_name: 'Tokyo 2, JP' }, pricing: { monthly_price: 5 } },
|
||||
{ id: 'f2', vcpu: 2, memory_mb: 4096, storage_gb: 80, transfer_tb: 4, provider: { name: 'linode' }, region: { region_name: 'Tokyo 2, JP' }, pricing: { monthly_price: 24 } },
|
||||
{ id: 'f3', vcpu: 1, memory_mb: 1024, storage_gb: 25, transfer_tb: 1, provider: { name: 'linode' }, region: { region_name: 'Tokyo 3, JP' }, pricing: { monthly_price: 5 } },
|
||||
{ id: 'f4', vcpu: 2, memory_mb: 4096, storage_gb: 80, transfer_tb: 4, provider: { name: 'linode' }, region: { region_name: 'Tokyo 3, JP' }, pricing: { monthly_price: 24 } },
|
||||
{ id: 'f5', vcpu: 1, memory_mb: 1024, storage_gb: 25, transfer_tb: 1, provider: { name: 'linode' }, region: { region_name: 'Osaka, JP', region_code: 'jp-osa' }, pricing: { monthly_price: 5 } },
|
||||
{ id: 'f6', vcpu: 2, memory_mb: 4096, storage_gb: 80, transfer_tb: 4, provider: { name: 'linode' }, region: { region_name: 'Osaka, JP', region_code: 'jp-osa' }, pricing: { monthly_price: 24 } },
|
||||
{ id: 'f7', vcpu: 1, memory_mb: 1024, storage_gb: 25, transfer_tb: 1, provider: { name: 'linode' }, region: { region_name: 'Singapore, SG' }, pricing: { monthly_price: 5 } },
|
||||
{ id: 'f8', vcpu: 2, memory_mb: 4096, storage_gb: 80, transfer_tb: 4, provider: { name: 'linode' }, region: { region_name: 'Singapore, SG' }, pricing: { monthly_price: 24 } },
|
||||
{ id: 'f9', vcpu: 1, memory_mb: 1024, storage_gb: 25, transfer_tb: 1, provider: { name: 'vultr' }, region: { region_name: 'Seoul, KR' }, pricing: { monthly_price: 6 } },
|
||||
{ id: 'f10', vcpu: 2, memory_mb: 4096, storage_gb: 80, transfer_tb: 4, provider: { name: 'vultr' }, region: { region_name: 'Seoul, KR' }, pricing: { monthly_price: 24 } },
|
||||
{ id: 'anvil-1g-1c', vcpu: 1, memory_mb: 1024, storage_gb: 25, transfer_tb: 1, category: 'vm', provider: 'linode', region: { display_name: 'Tokyo 1', country_code: 'JP' }, pricing: { monthly_price: 6, monthly_price_krw: 8500 } },
|
||||
{ id: 'anvil-2g-1c', vcpu: 1, memory_mb: 2048, storage_gb: 50, transfer_tb: 2, category: 'vm', provider: 'linode', region: { display_name: 'Tokyo 1', country_code: 'JP' }, pricing: { monthly_price: 12, monthly_price_krw: 17000 } },
|
||||
{ id: 'anvil-4g-2c', vcpu: 2, memory_mb: 4096, storage_gb: 80, transfer_tb: 4, category: 'vm', provider: 'linode', region: { display_name: 'Tokyo 1', country_code: 'JP' }, pricing: { monthly_price: 24, monthly_price_krw: 34000 } },
|
||||
{ id: 'anvil-1g-1c', vcpu: 1, memory_mb: 1024, storage_gb: 25, transfer_tb: 1, category: 'vm', provider: 'vultr', region: { display_name: 'Seoul 1', country_code: 'KR' }, pricing: { monthly_price: 6, monthly_price_krw: 8500 } },
|
||||
{ id: 'anvil-2g-1c', vcpu: 1, memory_mb: 2048, storage_gb: 50, transfer_tb: 2, category: 'vm', provider: 'vultr', region: { display_name: 'Seoul 1', country_code: 'KR' }, pricing: { monthly_price: 12, monthly_price_krw: 17000 } },
|
||||
];
|
||||
|
||||
return {
|
||||
// 필터 상태
|
||||
sortBy: 'vcpu', // 'vcpu' | 'memory' | 'price'
|
||||
sortBy: 'price', // 'vcpu' | 'memory' | 'price'
|
||||
sortOrder: 'asc', // 'asc' | 'desc'
|
||||
selectedCity: 'global', // 'global' | 'seoul' | 'gpu-japan' | 'gpu-korea'
|
||||
selectedSeoulType: 'vc2', // 'vc2' | 'vhf' (서울 서브탭)
|
||||
selectedGlobalType: 'standard', // 'standard' | 'dedicated' | 'premium' (글로벌 서브탭)
|
||||
selectedSeoulType: 'all', // 'all' (서브탭 단순화)
|
||||
selectedGlobalType: 'all', // 'all' (서브탭 단순화)
|
||||
|
||||
// 도시 목록: Linode(도쿄/오사카/싱가폴 동일가격), Seoul(Vultr), GPU
|
||||
// 도시 목록
|
||||
cities: [
|
||||
{ id: 'global', name: '도쿄/오사카/싱가폴', flag: '🌏', provider: 'linode' },
|
||||
{ id: 'seoul', name: 'Seoul', flag: '🇰🇷', provider: 'vultr' },
|
||||
@@ -41,21 +36,14 @@ export function pricingTable() {
|
||||
{ id: 'gpu-korea', name: 'GPU Korea', flag: '🇰🇷', provider: 'vultr', isGpu: true },
|
||||
],
|
||||
|
||||
// 글로벌(Linode) 서브탭 (인스턴스 타입별)
|
||||
// 글로벌 서브탭 (단순화)
|
||||
globalTypes: [
|
||||
{ id: 'standard', name: 'Standard', tooltip: '공유 CPU · 1-32 vCPU · 1-192GB RAM\n버스트 가능한 CPU로 비용 효율적\n웹서버, 개발환경에 적합' },
|
||||
{ id: 'dedicated', name: 'Dedicated', tooltip: '전용 CPU · 4-64 vCPU · 8-512GB RAM\n100% CPU 자원 보장\nCI/CD, 게임서버, 고부하 작업에 적합' },
|
||||
{ id: 'premium', name: 'Premium', tooltip: 'AMD EPYC 9004 · 1-64 vCPU · 2-512GB RAM\nDDR5 메모리 · NVMe 스토리지\n최신 세대 고성능 컴퓨팅' },
|
||||
{ id: 'highmem', name: 'High Memory', tooltip: '고밀도 메모리 · 2-16 vCPU · 24-300GB RAM\nvCPU당 RAM 비율 최대화\n데이터베이스, 캐싱, 분석 워크로드' },
|
||||
{ id: 'all', name: 'All', tooltip: '모든 인스턴스 타입\n도쿄, 오사카, 싱가폴 리전' },
|
||||
],
|
||||
|
||||
// 서울 서브탭 (인스턴스 타입별)
|
||||
// 서울 서브탭 (단순화)
|
||||
seoulTypes: [
|
||||
{ id: 'vc2', name: 'Cloud Compute', tooltip: '일반 SSD · 1-24 vCPU · 1-96GB RAM\n가성비 좋은 범용 인스턴스\n웹호스팅, 소규모 앱에 적합' },
|
||||
{ id: 'vhf', name: 'High Frequency', tooltip: 'NVMe SSD · 3GHz+ CPU · 1-12 vCPU\n고클럭 프로세서로 단일 스레드 성능 극대화\n게임서버, 실시간 처리에 적합' },
|
||||
{ id: 'vhp', name: 'High Performance', tooltip: 'AMD EPYC · 1-16 vCPU · 1-64GB RAM\n전용 CPU 코어 · NVMe 스토리지\n고성능 컴퓨팅, ML 추론에 적합' },
|
||||
{ id: 'voc', name: 'Optimized', tooltip: '특화 인스턴스 · 다양한 구성\nCPU/메모리/스토리지 최적화 버전\n특정 워크로드에 맞춤 선택' },
|
||||
{ id: 'vx1', name: 'Extreme', tooltip: '초고밀도 · 4-24 vCPU · 96-384GB RAM\n고메모리 비율 인스턴스\n대규모 DB, 인메모리 캐시에 적합' },
|
||||
{ id: 'all', name: 'All', tooltip: '모든 인스턴스 타입\n서울 리전' },
|
||||
],
|
||||
|
||||
// 데이터 상태
|
||||
@@ -66,9 +54,6 @@ export function pricingTable() {
|
||||
lastUpdate: null,
|
||||
fromCache: false,
|
||||
|
||||
// 지원 리전 패턴 (Tokyo, Osaka, Singapore = Linode / Seoul = Vultr)
|
||||
supportedRegions: ['Tokyo', 'Osaka', 'Singapore', 'Seoul', 'ap-northeast', 'ap-southeast', 'jp-osa'],
|
||||
|
||||
// 초기화
|
||||
async init() {
|
||||
await this.loadInstances();
|
||||
@@ -127,14 +112,13 @@ export function pricingTable() {
|
||||
}
|
||||
},
|
||||
|
||||
// API에서 인스턴스 가져오기 (D1 직접 조회 - rate limit 없음)
|
||||
// API에서 인스턴스 가져오기
|
||||
async fetchFromApi() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.fromCache = false;
|
||||
|
||||
try {
|
||||
// D1 직접 조회 엔드포인트 사용 (rate limit 없음)
|
||||
const response = await fetch('/api/pricing');
|
||||
const data = await response.json();
|
||||
|
||||
@@ -147,14 +131,11 @@ export function pricingTable() {
|
||||
console.log('[PricingTable] Region counts:', data.region_counts);
|
||||
|
||||
if (instances.length > 0) {
|
||||
// 캐시에 저장
|
||||
this.setCache(instances);
|
||||
|
||||
this.instances = this.filterAndSort(instances);
|
||||
this.filteredInstances = this.applyFilters(this.instances);
|
||||
this.lastUpdate = new Date();
|
||||
|
||||
console.log('[PricingTable] Loaded', this.instances.length, 'instances (Linode: JP/SG, Vultr: KR)');
|
||||
console.log('[PricingTable] Loaded', this.instances.length, 'instances');
|
||||
} else {
|
||||
throw new Error('인스턴스 데이터가 없습니다.');
|
||||
}
|
||||
@@ -174,7 +155,7 @@ export function pricingTable() {
|
||||
console.log('[PricingTable] Using expired cache as fallback');
|
||||
return;
|
||||
} catch (e) {
|
||||
// 캐시 파싱 실패, fallback 사용
|
||||
// 캐시 파싱 실패
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,11 +170,10 @@ export function pricingTable() {
|
||||
}
|
||||
},
|
||||
|
||||
// 필터 및 정렬 적용 (모든 인스턴스 표시)
|
||||
// 필터 및 정렬 적용
|
||||
filterAndSort(instances) {
|
||||
let filtered = [...instances];
|
||||
|
||||
// 정렬 적용
|
||||
filtered.sort((a, b) => {
|
||||
switch (this.sortBy) {
|
||||
case 'price':
|
||||
@@ -210,30 +190,27 @@ export function pricingTable() {
|
||||
return filtered;
|
||||
},
|
||||
|
||||
// 리전 이름 정규화 (Tokyo 2, JP -> Tokyo)
|
||||
normalizeRegion(regionName) {
|
||||
const name = regionName.toLowerCase();
|
||||
if (name.includes('osaka') || name.includes('jp-osa')) return 'Osaka';
|
||||
if (name.includes('tokyo')) return 'Tokyo';
|
||||
if (name.includes('singapore')) return 'Singapore';
|
||||
if (name.includes('seoul')) return 'Seoul';
|
||||
return regionName.split(',')[0].trim();
|
||||
// 리전 이름에서 도시 추출
|
||||
getRegionCity(displayName) {
|
||||
const name = (displayName || '').toLowerCase();
|
||||
if (name.includes('tokyo')) return 'tokyo';
|
||||
if (name.includes('osaka')) return 'osaka';
|
||||
if (name.includes('singapore')) return 'singapore';
|
||||
if (name.includes('seoul')) return 'seoul';
|
||||
return name;
|
||||
},
|
||||
|
||||
// 필터 변경 핸들러 (도시 필터 + 정렬 적용)
|
||||
// 필터 변경 핸들러
|
||||
onFilterChange() {
|
||||
this.filteredInstances = this.applyFilters(this.instances);
|
||||
},
|
||||
|
||||
// 정렬 토글 (같은 컬럼 클릭 시 오름/내림 전환)
|
||||
// 정렬 토글
|
||||
toggleSort(column) {
|
||||
if (this.sortBy === column) {
|
||||
// 같은 컬럼: 방향 토글
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
// 다른 컬럼: 기본 방향 설정
|
||||
this.sortBy = column;
|
||||
// vcpu, memory는 내림차순이 기본, price는 오름차순이 기본
|
||||
this.sortOrder = column === 'price' ? 'asc' : 'desc';
|
||||
}
|
||||
this.onFilterChange();
|
||||
@@ -243,41 +220,33 @@ export function pricingTable() {
|
||||
applyFilters(instances) {
|
||||
let filtered = [...instances];
|
||||
|
||||
// 도시 필터 (항상 적용)
|
||||
// 도시 필터
|
||||
filtered = filtered.filter(inst => {
|
||||
const regionName = (inst.region?.region_name || '').toLowerCase();
|
||||
const regionCode = (inst.region?.region_code || '').toLowerCase();
|
||||
const instId = (inst.id || '').toLowerCase();
|
||||
const hasGpu = inst.has_gpu || inst.gpu_count > 0;
|
||||
const displayName = (inst.region?.display_name || '').toLowerCase();
|
||||
const hasGpu = inst.has_gpu || inst.category === 'gpu';
|
||||
|
||||
switch (this.selectedCity) {
|
||||
case 'global':
|
||||
// Global: Tokyo 2 + 서브탭(Standard/Dedicated/Premium/HighMem) 필터링, GPU 제외
|
||||
if (!regionName.includes('tokyo 2') || hasGpu) return false;
|
||||
switch (this.selectedGlobalType) {
|
||||
case 'standard': return instId.startsWith('g6-nanode') || instId.startsWith('g6-standard');
|
||||
case 'dedicated': return instId.startsWith('g6-dedicated');
|
||||
case 'premium': return instId.startsWith('g7-premium');
|
||||
case 'highmem': return instId.startsWith('g7-highmem');
|
||||
default: return false;
|
||||
}
|
||||
// Global: 도쿄/오사카/싱가폴 (GPU 제외)
|
||||
const isGlobal = displayName.includes('tokyo') ||
|
||||
displayName.includes('osaka') ||
|
||||
displayName.includes('singapore');
|
||||
return isGlobal && !hasGpu;
|
||||
case 'seoul':
|
||||
// 서울은 서브탭(selectedSeoulType)으로 필터링, GPU 제외
|
||||
const isSeoul = regionName.includes('seoul');
|
||||
if (!isSeoul || !instId.startsWith(this.selectedSeoulType + '-') || hasGpu) return false;
|
||||
// vhp: AMD/Intel 중복 방지 → AMD만 표시 (Intel은 레거시)
|
||||
if (this.selectedSeoulType === 'vhp' && instId.endsWith('-intel')) return false;
|
||||
return true;
|
||||
// 서울 (GPU 제외)
|
||||
return displayName.includes('seoul') && !hasGpu;
|
||||
case 'gpu-japan':
|
||||
// 일본 GPU (도쿄만, 중복 방지)
|
||||
return regionName.includes('tokyo 2') && hasGpu;
|
||||
// 일본 GPU
|
||||
return (displayName.includes('tokyo') || displayName.includes('osaka')) && hasGpu;
|
||||
case 'gpu-korea':
|
||||
// 한국 GPU
|
||||
return regionName.includes('seoul') && hasGpu;
|
||||
default: return false;
|
||||
return displayName.includes('seoul') && hasGpu;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 정렬 (sortOrder 반영, 보조 정렬: 가격)
|
||||
// 정렬
|
||||
const order = this.sortOrder === 'asc' ? 1 : -1;
|
||||
filtered.sort((a, b) => {
|
||||
let diff = 0;
|
||||
@@ -287,15 +256,11 @@ export function pricingTable() {
|
||||
break;
|
||||
case 'vcpu':
|
||||
diff = (a.vcpu || 0) - (b.vcpu || 0);
|
||||
if (diff === 0) {
|
||||
diff = (a.pricing?.monthly_price || 0) - (b.pricing?.monthly_price || 0);
|
||||
}
|
||||
if (diff === 0) diff = (a.pricing?.monthly_price || 0) - (b.pricing?.monthly_price || 0);
|
||||
break;
|
||||
case 'memory':
|
||||
diff = (a.memory_mb || 0) - (b.memory_mb || 0);
|
||||
if (diff === 0) {
|
||||
diff = (a.pricing?.monthly_price || 0) - (b.pricing?.monthly_price || 0);
|
||||
}
|
||||
if (diff === 0) diff = (a.pricing?.monthly_price || 0) - (b.pricing?.monthly_price || 0);
|
||||
break;
|
||||
default:
|
||||
diff = (a.vcpu || 0) - (b.vcpu || 0);
|
||||
@@ -309,49 +274,55 @@ export function pricingTable() {
|
||||
// 도시별 인스턴스 수 조회
|
||||
getInstanceCountByCity(cityId) {
|
||||
return this.instances.filter(inst => {
|
||||
const regionName = (inst.region?.region_name || '').toLowerCase();
|
||||
const hasGpu = inst.has_gpu || inst.gpu_count > 0;
|
||||
const displayName = (inst.region?.display_name || '').toLowerCase();
|
||||
const hasGpu = inst.has_gpu || inst.category === 'gpu';
|
||||
|
||||
switch (cityId) {
|
||||
case 'global':
|
||||
return regionName.includes('tokyo 2') && !hasGpu;
|
||||
const isGlobal = displayName.includes('tokyo') ||
|
||||
displayName.includes('osaka') ||
|
||||
displayName.includes('singapore');
|
||||
return isGlobal && !hasGpu;
|
||||
case 'seoul':
|
||||
return regionName.includes('seoul') && !hasGpu;
|
||||
return displayName.includes('seoul') && !hasGpu;
|
||||
case 'gpu-japan':
|
||||
return regionName.includes('tokyo 2') && hasGpu;
|
||||
return (displayName.includes('tokyo') || displayName.includes('osaka')) && hasGpu;
|
||||
case 'gpu-korea':
|
||||
return regionName.includes('seoul') && hasGpu;
|
||||
default: return false;
|
||||
return displayName.includes('seoul') && hasGpu;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}).length;
|
||||
},
|
||||
|
||||
// 서울 서브탭별 인스턴스 수 조회
|
||||
getSeoulTypeCount(typeId) {
|
||||
return this.instances.filter(inst => {
|
||||
const regionName = (inst.region?.region_name || '').toLowerCase();
|
||||
const instId = (inst.id || '').toLowerCase();
|
||||
return regionName.includes('seoul') && instId.startsWith(typeId + '-');
|
||||
}).length;
|
||||
if (typeId === 'all') {
|
||||
return this.instances.filter(inst => {
|
||||
const displayName = (inst.region?.display_name || '').toLowerCase();
|
||||
const hasGpu = inst.has_gpu || inst.category === 'gpu';
|
||||
return displayName.includes('seoul') && !hasGpu;
|
||||
}).length;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
// 글로벌 서브탭별 인스턴스 수 조회
|
||||
getGlobalTypeCount(typeId) {
|
||||
return this.instances.filter(inst => {
|
||||
const regionName = (inst.region?.region_name || '').toLowerCase();
|
||||
const instId = (inst.id || '').toLowerCase();
|
||||
const hasGpu = inst.has_gpu || inst.gpu_count > 0;
|
||||
if (!regionName.includes('tokyo 2') || hasGpu) return false;
|
||||
switch (typeId) {
|
||||
case 'standard': return instId.startsWith('g6-nanode') || instId.startsWith('g6-standard');
|
||||
case 'dedicated': return instId.startsWith('g6-dedicated');
|
||||
case 'premium': return instId.startsWith('g7-premium');
|
||||
case 'highmem': return instId.startsWith('g7-highmem');
|
||||
default: return false;
|
||||
}
|
||||
}).length;
|
||||
if (typeId === 'all') {
|
||||
return this.instances.filter(inst => {
|
||||
const displayName = (inst.region?.display_name || '').toLowerCase();
|
||||
const hasGpu = inst.has_gpu || inst.category === 'gpu';
|
||||
const isGlobal = displayName.includes('tokyo') ||
|
||||
displayName.includes('osaka') ||
|
||||
displayName.includes('singapore');
|
||||
return isGlobal && !hasGpu;
|
||||
}).length;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
// 강제 새로고침 (캐시 삭제 후 API 호출)
|
||||
// 강제 새로고침
|
||||
async forceRefresh() {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
await this.fetchFromApi();
|
||||
@@ -363,13 +334,13 @@ export function pricingTable() {
|
||||
return '$' + price.toFixed(2);
|
||||
},
|
||||
|
||||
// 가격 포맷 (KRW) - DB에서 직접 가져온 한화 금액
|
||||
// 가격 포맷 (KRW)
|
||||
formatKrw(krwPrice) {
|
||||
if (krwPrice == null) return '-';
|
||||
return '₩' + Math.round(krwPrice).toLocaleString('ko-KR');
|
||||
},
|
||||
|
||||
// 시간당 가격 포맷 (KRW) - DB에서 직접 가져온 한화 금액
|
||||
// 시간당 가격 포맷 (KRW)
|
||||
formatKrwHourly(krwPrice) {
|
||||
if (krwPrice == null) return '-';
|
||||
return '₩' + Math.round(krwPrice).toLocaleString('ko-KR');
|
||||
@@ -377,6 +348,7 @@ export function pricingTable() {
|
||||
|
||||
// 메모리 포맷
|
||||
formatMemory(mb) {
|
||||
if (mb == null) return '-';
|
||||
if (mb >= 1024) {
|
||||
return (mb / 1024).toFixed(mb % 1024 === 0 ? 0 : 1) + ' GB';
|
||||
}
|
||||
@@ -401,32 +373,12 @@ export function pricingTable() {
|
||||
return Math.round(tb * 1024) + ' GB';
|
||||
},
|
||||
|
||||
// 프로바이더 색상
|
||||
getProviderColor(provider) {
|
||||
const colors = {
|
||||
'linode': 'bg-green-500/20 text-green-400',
|
||||
'vultr': 'bg-blue-500/20 text-blue-400',
|
||||
'aws': 'bg-orange-500/20 text-orange-400',
|
||||
};
|
||||
return colors[provider?.toLowerCase()] || 'bg-slate-500/20 text-slate-400';
|
||||
},
|
||||
|
||||
// 리전 플래그 이모지
|
||||
getRegionFlag(regionName) {
|
||||
const flags = {
|
||||
'tokyo': '🇯🇵', 'osaka': '🇯🇵', 'japan': '🇯🇵',
|
||||
'singapore': '🇸🇬',
|
||||
'hong kong': '🇭🇰',
|
||||
'seoul': '🇰🇷', 'korea': '🇰🇷',
|
||||
'mumbai': '🇮🇳', 'bangalore': '🇮🇳', 'india': '🇮🇳',
|
||||
'sydney': '🇦🇺', 'australia': '🇦🇺',
|
||||
'amsterdam': '🇳🇱',
|
||||
'frankfurt': '🇩🇪', 'germany': '🇩🇪',
|
||||
'london': '🇬🇧', 'uk': '🇬🇧',
|
||||
'paris': '🇫🇷', 'france': '🇫🇷',
|
||||
'us': '🇺🇸', 'america': '🇺🇸', 'atlanta': '🇺🇸', 'dallas': '🇺🇸',
|
||||
'chicago': '🇺🇸', 'miami': '🇺🇸', 'new york': '🇺🇸', 'seattle': '🇺🇸',
|
||||
'los angeles': '🇺🇸', 'silicon valley': '🇺🇸'
|
||||
};
|
||||
|
||||
const lower = (regionName || '').toLowerCase();
|
||||
@@ -448,19 +400,14 @@ export function pricingTable() {
|
||||
|
||||
// 인스턴스 선택 상태
|
||||
selectedInstance: null,
|
||||
|
||||
// 모달 상태
|
||||
showInstanceModal: false,
|
||||
selectedInstanceDetail: null,
|
||||
|
||||
// 클립보드 복사 상태
|
||||
copiedToClipboard: false,
|
||||
|
||||
// 인스턴스 선택 핸들러
|
||||
selectInstance(inst) {
|
||||
// 같은 인스턴스 다시 클릭하면 선택 해제 및 모달 닫기
|
||||
if (this.selectedInstance?.id === inst.id &&
|
||||
this.selectedInstance?.region?.region_code === inst.region?.region_code) {
|
||||
this.selectedInstance?.region?.display_name === inst.region?.display_name) {
|
||||
this.selectedInstance = null;
|
||||
this.showInstanceModal = false;
|
||||
this.selectedInstanceDetail = null;
|
||||
@@ -493,10 +440,11 @@ export function pricingTable() {
|
||||
});
|
||||
},
|
||||
|
||||
// 텔레그램 링크 가져오기
|
||||
// 텔레그램 링크
|
||||
getInstanceTelegramLink() {
|
||||
if (!this.selectedInstanceDetail) return '#';
|
||||
return `https://t.me/AnvilForgeBot?start=order_${encodeURIComponent(this.selectedInstanceDetail.id)}`;
|
||||
const region = this.selectedInstanceDetail.region?.display_name || '';
|
||||
return `https://t.me/AnvilForgeBot?start=order_${encodeURIComponent(this.selectedInstanceDetail.id)}_${encodeURIComponent(region.toLowerCase().replace(/\s+/g, '-'))}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user