## 변경사항 - app.js (1370줄) → 7개 모듈 (1427줄) - ES6 import/export 문법 사용 - Alpine.js 호환성 유지 (window 전역 노출) ## 모듈 구조 - js/config.js: 상수/설정 (WIZARD_CONFIG, PRICING_DATA, MOCK_*) - js/api.js: ApiService - js/utils.js: formatPrice, switchTab, ping 시뮬레이션 - js/wizard.js: 서버 추천 마법사 로직 - js/pricing.js: 가격표 컴포넌트 - js/dashboard.js: 대시보드 및 텔레그램 연동 - js/app.js: 메인 통합 (모든 모듈 import) ## HTML 변경 - <script type="module" src="js/app.js">로 변경 - 기존 기능 모두 정상 작동 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
209 lines
6.0 KiB
TypeScript
209 lines
6.0 KiB
TypeScript
/**
|
|
* Pricing endpoint - Direct D1 query (no rate limiting)
|
|
* GET /api/pricing → D1 cloud-instances-db
|
|
*
|
|
* Supported regions:
|
|
* - Tokyo, Osaka, Singapore: Linode
|
|
* - Seoul: Vultr
|
|
*/
|
|
|
|
import { type PagesFunction } from '@cloudflare/workers-types';
|
|
|
|
interface Env {
|
|
DB: D1Database;
|
|
}
|
|
|
|
interface InstanceRow {
|
|
instance_id: string;
|
|
instance_name: string;
|
|
vcpu: number;
|
|
memory_mb: number;
|
|
storage_gb: number | null;
|
|
transfer_tb: number | null;
|
|
provider_name: string;
|
|
region_name: string;
|
|
region_code: string;
|
|
monthly_price: number;
|
|
hourly_price: number | null;
|
|
monthly_price_krw: number | null;
|
|
hourly_price_krw: number | null;
|
|
// GPU fields
|
|
has_gpu?: number;
|
|
gpu_count?: number;
|
|
gpu_type?: string;
|
|
}
|
|
|
|
const CORS_HEADERS = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
};
|
|
|
|
export const onRequestGet: PagesFunction<Env> = async ({ env }) => {
|
|
try {
|
|
// 1. 일반 인스턴스 쿼리 - Linode(Tokyo, Osaka, Singapore) + Vultr(Seoul)
|
|
const regularSql = `
|
|
SELECT
|
|
it.instance_id,
|
|
it.instance_name,
|
|
it.vcpu,
|
|
it.memory_mb,
|
|
it.storage_gb,
|
|
it.transfer_tb,
|
|
p.name as provider_name,
|
|
r.region_name,
|
|
r.region_code,
|
|
pr.monthly_price,
|
|
pr.hourly_price,
|
|
pr.monthly_price_krw,
|
|
pr.hourly_price_krw,
|
|
0 as has_gpu,
|
|
0 as gpu_count,
|
|
NULL as gpu_type
|
|
FROM instance_types it
|
|
JOIN providers p ON it.provider_id = p.id
|
|
JOIN pricing pr ON it.id = pr.instance_type_id
|
|
JOIN regions r ON pr.region_id = r.id
|
|
WHERE pr.available = 1
|
|
AND it.instance_id NOT LIKE '%-sc1'
|
|
AND (
|
|
(p.name = 'linode' AND (
|
|
r.region_name LIKE '%Tokyo%' OR
|
|
r.region_name LIKE '%Osaka%' OR
|
|
r.region_code LIKE '%jp-osa%' OR
|
|
r.region_name LIKE '%Singapore%'
|
|
))
|
|
OR
|
|
(p.name = 'vultr' AND (
|
|
r.region_name LIKE '%Seoul%' OR
|
|
r.region_code LIKE '%icn%'
|
|
))
|
|
)
|
|
ORDER BY pr.monthly_price ASC
|
|
`;
|
|
|
|
// 2. GPU 인스턴스 쿼리 - Linode(Tokyo) + Vultr(Seoul)
|
|
const gpuSql = `
|
|
SELECT
|
|
gi.instance_id,
|
|
gi.instance_name,
|
|
gi.vcpu,
|
|
gi.memory_mb,
|
|
gi.storage_gb,
|
|
gi.transfer_tb,
|
|
p.name as provider_name,
|
|
r.region_name,
|
|
r.region_code,
|
|
gp.monthly_price,
|
|
gp.hourly_price,
|
|
gp.monthly_price_krw,
|
|
gp.hourly_price_krw,
|
|
1 as has_gpu,
|
|
gi.gpu_count,
|
|
gi.gpu_type
|
|
FROM gpu_instances gi
|
|
JOIN providers p ON gi.provider_id = p.id
|
|
JOIN gpu_pricing gp ON gi.id = gp.gpu_instance_id
|
|
JOIN regions r ON gp.region_id = r.id
|
|
WHERE gp.available = 1
|
|
AND (
|
|
(p.name = 'linode' AND (
|
|
r.region_name LIKE '%Tokyo%' OR
|
|
r.region_name LIKE '%Osaka%' OR
|
|
r.region_code LIKE '%jp-osa%'
|
|
))
|
|
OR
|
|
(p.name = 'vultr' AND (
|
|
r.region_name LIKE '%Seoul%' OR
|
|
r.region_code LIKE '%icn%'
|
|
))
|
|
)
|
|
ORDER BY gp.monthly_price ASC
|
|
`;
|
|
|
|
const [regularResult, gpuResult] = await Promise.all([
|
|
env.DB.prepare(regularSql).all<InstanceRow>(),
|
|
env.DB.prepare(gpuSql).all<InstanceRow>(),
|
|
]);
|
|
|
|
// Transform to frontend format
|
|
const transformRow = (row: InstanceRow) => ({
|
|
id: row.instance_id,
|
|
instance_name: row.instance_name,
|
|
vcpu: row.vcpu,
|
|
memory_mb: row.memory_mb,
|
|
storage_gb: row.storage_gb,
|
|
transfer_tb: row.transfer_tb,
|
|
provider: { name: row.provider_name },
|
|
region: {
|
|
region_name: row.region_name,
|
|
region_code: row.region_code
|
|
},
|
|
pricing: {
|
|
monthly_price: row.monthly_price,
|
|
hourly_price: row.hourly_price,
|
|
monthly_price_krw: row.monthly_price_krw,
|
|
hourly_price_krw: row.hourly_price_krw,
|
|
},
|
|
has_gpu: row.has_gpu === 1,
|
|
gpu_count: row.gpu_count || 0,
|
|
gpu_type: row.gpu_type || null,
|
|
});
|
|
|
|
const regularInstances = regularResult.results.map(transformRow);
|
|
const gpuInstances = gpuResult.results.map(transformRow);
|
|
const instances = [...regularInstances, ...gpuInstances];
|
|
|
|
// Count by region
|
|
const regionCounts: Record<string, number> = {};
|
|
for (const inst of instances) {
|
|
const name = inst.region.region_name.toLowerCase();
|
|
if (name.includes('tokyo')) regionCounts['tokyo'] = (regionCounts['tokyo'] || 0) + 1;
|
|
else if (name.includes('osaka')) regionCounts['osaka'] = (regionCounts['osaka'] || 0) + 1;
|
|
else if (name.includes('singapore')) regionCounts['singapore'] = (regionCounts['singapore'] || 0) + 1;
|
|
else if (name.includes('seoul')) regionCounts['seoul'] = (regionCounts['seoul'] || 0) + 1;
|
|
}
|
|
|
|
// GPU counts
|
|
const gpuCounts = {
|
|
'gpu-japan': gpuInstances.filter(i =>
|
|
i.region.region_name.toLowerCase().includes('tokyo') ||
|
|
i.region.region_name.toLowerCase().includes('osaka')
|
|
).length,
|
|
'gpu-korea': gpuInstances.filter(i =>
|
|
i.region.region_name.toLowerCase().includes('seoul')
|
|
).length,
|
|
};
|
|
|
|
return new Response(JSON.stringify({
|
|
success: true,
|
|
total: instances.length,
|
|
region_counts: regionCounts,
|
|
gpu_counts: gpuCounts,
|
|
instances,
|
|
}), {
|
|
headers: {
|
|
...CORS_HEADERS,
|
|
'Content-Type': 'application/json',
|
|
'Cache-Control': 'public, max-age=300', // 5분 캐시
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('[Pricing] D1 query error:', error);
|
|
return new Response(JSON.stringify({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Database query failed',
|
|
}), {
|
|
status: 500,
|
|
headers: {
|
|
...CORS_HEADERS,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
export const onRequestOptions: PagesFunction<Env> = async () => {
|
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
};
|