feat: KRW 가격 지원 및 GPU/G8/VPU 인스턴스 추가

## KRW 가격 기능
- pricing 테이블에 hourly_price_krw, monthly_price_krw 컬럼 추가
- 부가세 10% + 영업이익 10% + 환율 적용 (기본 1450원)
- 시간당: 1원 단위 반올림 (최소 1원)
- 월간: 100원 단위 반올림 (최소 100원)
- 환율/부가세/영업이익률 환경변수로 분리 (배포 없이 변경 가능)

## GPU/G8/VPU 인스턴스 지원
- gpu_instances, gpu_pricing 테이블 추가
- g8_instances, g8_pricing 테이블 추가
- vpu_instances, vpu_pricing 테이블 추가
- Linode/Vultr 커넥터에 GPU 동기화 로직 추가

## 환경변수 추가
- KRW_EXCHANGE_RATE: 환율 (기본 1450)
- KRW_VAT_RATE: 부가세율 (기본 1.1)
- KRW_MARKUP_RATE: 영업이익률 (기본 1.1)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-22 18:57:51 +09:00
parent b1cb844c05
commit a2133ae5c9
20 changed files with 3517 additions and 690 deletions

View File

@@ -0,0 +1,223 @@
/**
* GPU Pricing Repository
* Handles CRUD operations for GPU instance pricing data
*/
import { BaseRepository } from './base';
import { GpuPricing, GpuPricingInput, RepositoryError, ErrorCodes, Env } from '../types';
import { createLogger } from '../utils/logger';
import { calculateKRWHourly, calculateKRWMonthly } from '../constants';
export class GpuPricingRepository extends BaseRepository<GpuPricing> {
protected tableName = 'gpu_pricing';
protected logger = createLogger('[GpuPricingRepository]');
protected allowedColumns = [
'gpu_instance_id',
'region_id',
'hourly_price',
'monthly_price',
'hourly_price_krw',
'monthly_price_krw',
'currency',
'available',
];
constructor(db: D1Database, private env?: Env) {
super(db);
}
/**
* Find pricing for a specific GPU instance
*/
async findByGpuInstance(gpuInstanceId: number): Promise<GpuPricing[]> {
try {
const result = await this.db
.prepare('SELECT * FROM gpu_pricing WHERE gpu_instance_id = ?')
.bind(gpuInstanceId)
.all<GpuPricing>();
return result.results;
} catch (error) {
this.logger.error('findByGpuInstance failed', {
gpuInstanceId,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw new RepositoryError(
`Failed to find pricing for GPU instance: ${gpuInstanceId}`,
ErrorCodes.DATABASE_ERROR,
error
);
}
}
/**
* Find pricing for a specific region
*/
async findByRegion(regionId: number): Promise<GpuPricing[]> {
try {
const result = await this.db
.prepare('SELECT * FROM gpu_pricing WHERE region_id = ?')
.bind(regionId)
.all<GpuPricing>();
return result.results;
} catch (error) {
this.logger.error('findByRegion failed', {
regionId,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw new RepositoryError(
`Failed to find GPU pricing for region: ${regionId}`,
ErrorCodes.DATABASE_ERROR,
error
);
}
}
/**
* Find specific pricing record by GPU instance and region
*/
async findByGpuInstanceAndRegion(
gpuInstanceId: number,
regionId: number
): Promise<GpuPricing | null> {
try {
const result = await this.db
.prepare('SELECT * FROM gpu_pricing WHERE gpu_instance_id = ? AND region_id = ?')
.bind(gpuInstanceId, regionId)
.first<GpuPricing>();
return result || null;
} catch (error) {
this.logger.error('findByGpuInstanceAndRegion failed', {
gpuInstanceId,
regionId,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw new RepositoryError(
`Failed to find GPU pricing for instance ${gpuInstanceId} in region ${regionId}`,
ErrorCodes.DATABASE_ERROR,
error
);
}
}
/**
* Bulk upsert GPU pricing records
* Uses batch operations for efficiency
*/
async upsertMany(pricingData: GpuPricingInput[]): Promise<number> {
if (pricingData.length === 0) {
return 0;
}
try {
const statements = pricingData.map((pricing) => {
const hourlyKrw = calculateKRWHourly(pricing.hourly_price, this.env);
const monthlyKrw = calculateKRWMonthly(pricing.monthly_price, this.env);
return this.db.prepare(
`INSERT INTO gpu_pricing (
gpu_instance_id, region_id, hourly_price, monthly_price,
hourly_price_krw, monthly_price_krw, currency, available
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(gpu_instance_id, region_id)
DO UPDATE SET
hourly_price = excluded.hourly_price,
monthly_price = excluded.monthly_price,
hourly_price_krw = excluded.hourly_price_krw,
monthly_price_krw = excluded.monthly_price_krw,
currency = excluded.currency,
available = excluded.available`
).bind(
pricing.gpu_instance_id,
pricing.region_id,
pricing.hourly_price,
pricing.monthly_price,
hourlyKrw,
monthlyKrw,
pricing.currency,
pricing.available
);
});
const results = await this.executeBatch(statements);
const successCount = results.reduce(
(sum, result) => sum + (result.meta.changes ?? 0),
0
);
this.logger.info('Upserted GPU pricing records', { count: successCount });
return successCount;
} catch (error) {
this.logger.error('upsertMany failed', {
count: pricingData.length,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw new RepositoryError(
'Failed to upsert GPU pricing records',
ErrorCodes.TRANSACTION_FAILED,
error
);
}
}
/**
* Search GPU pricing by price range
*/
async searchByPriceRange(
minHourly?: number,
maxHourly?: number,
minMonthly?: number,
maxMonthly?: number
): Promise<GpuPricing[]> {
try {
const conditions: string[] = [];
const params: (string | number | boolean | null)[] = [];
if (minHourly !== undefined) {
conditions.push('hourly_price >= ?');
params.push(minHourly);
}
if (maxHourly !== undefined) {
conditions.push('hourly_price <= ?');
params.push(maxHourly);
}
if (minMonthly !== undefined) {
conditions.push('monthly_price >= ?');
params.push(minMonthly);
}
if (maxMonthly !== undefined) {
conditions.push('monthly_price <= ?');
params.push(maxMonthly);
}
const whereClause = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
const query = 'SELECT * FROM gpu_pricing' + whereClause + ' ORDER BY hourly_price';
const result = await this.db
.prepare(query)
.bind(...params)
.all<GpuPricing>();
return result.results;
} catch (error) {
this.logger.error('searchByPriceRange failed', {
minHourly,
maxHourly,
minMonthly,
maxMonthly,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw new RepositoryError(
'Failed to search GPU pricing by price range',
ErrorCodes.DATABASE_ERROR,
error
);
}
}
}