feat: add server ordering system with session-based flow
- Add server recommendation integration (SERVER_RECOMMEND worker) - Implement KV-based session management for multi-step ordering - Add Linode/Vultr API clients for server provisioning - Add server-tool for Function Calling support refactor: major code reorganization (Phase 1-3) - Remove 443 lines of deprecated callback handlers - Extract handlers to separate files (message-handler, callback-handler) - Extract cloud-spec-service, server-recommend-service - Centralize constants (OS_IMAGES, REGION_FLAGS, NUM_EMOJIS) - webhook.ts reduced from 1,951 to 30 lines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
208
src/utils/session.ts
Normal file
208
src/utils/session.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* KV 기반 세션 관리 유틸리티
|
||||
* - 다단계 플로우 (서버 주문, 도메인 등록 등)의 임시 데이터 저장
|
||||
* - TTL 24시간 자동 만료
|
||||
*/
|
||||
|
||||
export type SessionType = 'server_order' | 'domain_register';
|
||||
|
||||
export interface SessionData<T = unknown> {
|
||||
type: SessionType;
|
||||
step: string;
|
||||
data: T;
|
||||
userId: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// 서버 주문 세션 데이터
|
||||
export interface ServerOrderSessionData {
|
||||
// 추천 목록 (선택 전까지 임시 저장)
|
||||
recommendations?: Array<{
|
||||
plan: string;
|
||||
region: string;
|
||||
provider: string;
|
||||
}>;
|
||||
|
||||
// 추천 정보
|
||||
purpose?: string;
|
||||
budget?: number;
|
||||
expectedUsers?: number;
|
||||
|
||||
// 선택된 사양
|
||||
plan?: string;
|
||||
provider?: string;
|
||||
region?: string;
|
||||
|
||||
// OS 선택
|
||||
image?: string;
|
||||
|
||||
// 가격 (캐시)
|
||||
priceKrw?: number;
|
||||
|
||||
// 주문 ID (최종 확인 단계)
|
||||
orderId?: number;
|
||||
}
|
||||
|
||||
// 서버 주문 단계 정의 (step 필드 타입 가이드)
|
||||
export type ServerOrderStep = 'recommend' | 'spec_confirm' | 'os_select' | 'final_confirm';
|
||||
|
||||
const SESSION_TTL_SECONDS = 24 * 60 * 60; // 24시간
|
||||
|
||||
/**
|
||||
* 세션 ID 생성
|
||||
* format: {type_prefix}_{userId}_{random}
|
||||
*/
|
||||
function generateSessionId(type: SessionType, userId: number): string {
|
||||
const prefix = type === 'server_order' ? 'srv' : 'dom';
|
||||
const random = crypto.randomUUID().slice(0, 8);
|
||||
return `${prefix}_${userId}_${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 생성
|
||||
*/
|
||||
export async function createSession<T>(
|
||||
kv: KVNamespace,
|
||||
userId: number,
|
||||
type: SessionType,
|
||||
initialData: T,
|
||||
step: string = 'init'
|
||||
): Promise<string> {
|
||||
const sessionId = generateSessionId(type, userId);
|
||||
const now = Date.now();
|
||||
|
||||
const session: SessionData<T> = {
|
||||
type,
|
||||
step,
|
||||
data: initialData,
|
||||
userId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await kv.put(
|
||||
`session:${sessionId}`,
|
||||
JSON.stringify(session),
|
||||
{ expirationTtl: SESSION_TTL_SECONDS }
|
||||
);
|
||||
|
||||
// 사용자의 활성 세션 참조 저장 (같은 타입의 이전 세션 덮어쓰기)
|
||||
await kv.put(
|
||||
`user_session:${userId}:${type}`,
|
||||
sessionId,
|
||||
{ expirationTtl: SESSION_TTL_SECONDS }
|
||||
);
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 조회
|
||||
*/
|
||||
export async function getSession<T>(
|
||||
kv: KVNamespace,
|
||||
sessionId: string
|
||||
): Promise<SessionData<T> | null> {
|
||||
const raw = await kv.get(`session:${sessionId}`);
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as SessionData<T>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 조회 + 권한 검증
|
||||
*/
|
||||
export async function getSessionForUser<T>(
|
||||
kv: KVNamespace,
|
||||
sessionId: string,
|
||||
userId: number
|
||||
): Promise<SessionData<T> | null> {
|
||||
const session = await getSession<T>(kv, sessionId);
|
||||
|
||||
if (!session) return null;
|
||||
if (session.userId !== userId) return null;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 활성 세션 조회
|
||||
*/
|
||||
export async function getUserActiveSession<T>(
|
||||
kv: KVNamespace,
|
||||
userId: number,
|
||||
type: SessionType
|
||||
): Promise<{ sessionId: string; session: SessionData<T> } | null> {
|
||||
const sessionId = await kv.get(`user_session:${userId}:${type}`);
|
||||
if (!sessionId) return null;
|
||||
|
||||
const session = await getSession<T>(kv, sessionId);
|
||||
if (!session) {
|
||||
// 참조는 있지만 세션이 만료됨 - 참조 정리
|
||||
await kv.delete(`user_session:${userId}:${type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return { sessionId, session };
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 업데이트
|
||||
*/
|
||||
export async function updateSession<T>(
|
||||
kv: KVNamespace,
|
||||
sessionId: string,
|
||||
updates: Partial<T> & { step?: string }
|
||||
): Promise<SessionData<T> | null> {
|
||||
const session = await getSession<T>(kv, sessionId);
|
||||
if (!session) return null;
|
||||
|
||||
const { step, ...dataUpdates } = updates;
|
||||
|
||||
const updated: SessionData<T> = {
|
||||
...session,
|
||||
step: step ?? session.step,
|
||||
data: { ...session.data, ...dataUpdates } as T,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
await kv.put(
|
||||
`session:${sessionId}`,
|
||||
JSON.stringify(updated),
|
||||
{ expirationTtl: SESSION_TTL_SECONDS }
|
||||
);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 삭제
|
||||
*/
|
||||
export async function deleteSession(
|
||||
kv: KVNamespace,
|
||||
sessionId: string
|
||||
): Promise<void> {
|
||||
const session = await getSession(kv, sessionId);
|
||||
|
||||
await kv.delete(`session:${sessionId}`);
|
||||
|
||||
// 사용자 참조도 삭제
|
||||
if (session) {
|
||||
await kv.delete(`user_session:${session.userId}:${session.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 만료 여부 확인 (UI용 메시지)
|
||||
*/
|
||||
export function isSessionExpired(session: SessionData | null): boolean {
|
||||
if (!session) return true;
|
||||
|
||||
const elapsed = Date.now() - session.createdAt;
|
||||
return elapsed > SESSION_TTL_SECONDS * 1000;
|
||||
}
|
||||
Reference in New Issue
Block a user