refactor: app.js를 ES6 모듈로 분리

## 변경사항
- 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>
This commit is contained in:
kappa
2026-01-23 12:59:54 +09:00
parent 347a5cc598
commit 758266d8cb
21 changed files with 2193 additions and 194 deletions

View File

@@ -1,102 +0,0 @@
/**
* Shared proxy utilities for Cloudflare Pages Functions
* Handles CORS, error responses, and Worker API forwarding
*/
export interface Env {
WORKER_API_KEY: string;
WORKER_API_URL: string;
DB: D1Database;
}
export interface ErrorResponse {
success: false;
error: string;
details?: any;
}
const CORS_HEADERS = {
'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
} as const;
/**
* Create CORS preflight response
*/
export function createCorsPreflightResponse(): Response {
return new Response(null, {
status: 204,
headers: CORS_HEADERS,
});
}
/**
* Create error response with CORS headers
*/
export function createErrorResponse(
error: string,
status: number = 500,
details?: any
): Response {
const body: ErrorResponse = {
success: false,
error,
...(details && { details }),
};
return new Response(JSON.stringify(body), {
status,
headers: {
...CORS_HEADERS,
'Content-Type': 'application/json',
},
});
}
/**
* Proxy request to Worker API with authentication
*/
export async function proxyToWorker(
env: Env,
path: string,
options: RequestInit = {}
): Promise<Response> {
const workerUrl = `${env.WORKER_API_URL}${path}`;
try {
const response = await fetch(workerUrl, {
...options,
headers: {
...options.headers,
'X-API-Key': env.WORKER_API_KEY,
},
});
// Clone response to add CORS headers
const body = await response.text();
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: {
...CORS_HEADERS,
'Content-Type': response.headers.get('Content-Type') || 'application/json',
},
});
} catch (error) {
console.error(`[Proxy] Failed to fetch ${workerUrl}:`, error);
return createErrorResponse(
'Failed to connect to API server',
503,
error instanceof Error ? error.message : String(error)
);
}
}
/**
* Build query string from URL search params
*/
export function buildQueryString(searchParams: URLSearchParams): string {
const params = searchParams.toString();
return params ? `?${params}` : '';
}

View File

@@ -1,17 +0,0 @@
/**
* Health check endpoint
* GET /api/health → Worker GET /health
*/
import { type PagesFunction } from '@cloudflare/workers-types';
import { Env, createCorsPreflightResponse, proxyToWorker } from '../_shared/proxy';
export const onRequestGet: PagesFunction<Env> = async ({ env }) => {
return proxyToWorker(env, '/health', {
method: 'GET',
});
};
export const onRequestOptions: PagesFunction<Env> = async () => {
return createCorsPreflightResponse();
};

View File

@@ -1,25 +0,0 @@
/**
* Instances query endpoint
* GET /api/instances → Worker GET /instances
*/
import { type PagesFunction } from '@cloudflare/workers-types';
import {
Env,
createCorsPreflightResponse,
proxyToWorker,
buildQueryString,
} from '../_shared/proxy';
export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => {
const url = new URL(request.url);
const queryString = buildQueryString(url.searchParams);
return proxyToWorker(env, `/instances${queryString}`, {
method: 'GET',
});
};
export const onRequestOptions: PagesFunction<Env> = async () => {
return createCorsPreflightResponse();
};

View File

@@ -8,7 +8,10 @@
*/
import { type PagesFunction } from '@cloudflare/workers-types';
import { Env, createCorsPreflightResponse } from '../_shared/proxy';
interface Env {
DB: D1Database;
}
interface InstanceRow {
instance_id: string;
@@ -201,5 +204,5 @@ export const onRequestGet: PagesFunction<Env> = async ({ env }) => {
};
export const onRequestOptions: PagesFunction<Env> = async () => {
return createCorsPreflightResponse();
return new Response(null, { status: 204, headers: CORS_HEADERS });
};

View File

@@ -1,47 +0,0 @@
/**
* Recommendation endpoint
* POST /api/recommend → Worker POST /recommend
*/
import { type PagesFunction } from '@cloudflare/workers-types';
import {
Env,
createCorsPreflightResponse,
createErrorResponse,
proxyToWorker,
} from '../_shared/proxy';
export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
try {
// Read request body
const body = await request.text();
// Validate JSON
if (body) {
try {
JSON.parse(body);
} catch {
return createErrorResponse('Invalid JSON in request body', 400);
}
}
return proxyToWorker(env, '/recommend', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
});
} catch (error) {
console.error('[Recommend] Failed to process request:', error);
return createErrorResponse(
'Failed to process recommendation request',
500,
error instanceof Error ? error.message : String(error)
);
}
};
export const onRequestOptions: PagesFunction<Env> = async () => {
return createCorsPreflightResponse();
};