@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
527 lines (447 loc) • 13.2 kB
text/typescript
import {
CacheEntry,
CacheLevel,
CacheProvider,
ContentType,
CacheStatus,
} from "./types";
export abstract class BaseCacheProvider {
protected level: CacheLevel;
protected provider: CacheProvider;
constructor(level: CacheLevel, provider: CacheProvider) {
this.level = level;
this.provider = provider;
}
abstract get<T>(key: string): Promise<CacheEntry<T> | null>;
abstract set<T>(entry: CacheEntry<T>): Promise<void>;
abstract delete(key: string): Promise<boolean>;
abstract clear(): Promise<void>;
abstract keys(pattern?: string): Promise<string[]>;
abstract exists(key: string): Promise<boolean>;
abstract size(): Promise<number>;
abstract healthCheck(): Promise<boolean>;
}
/**
* 브라우저 캐시 프로바이더 (localStorage/sessionStorage)
*/
export class BrowserCacheProvider extends BaseCacheProvider {
private storage: Storage;
constructor(useSessionStorage = false) {
super("browser", "browser");
this.storage = useSessionStorage ? sessionStorage : localStorage;
}
async get<T>(key: string): Promise<CacheEntry<T> | null> {
try {
const item = this.storage.getItem(`cache:${key}`);
if (!item) return null;
const entry = JSON.parse(item);
entry.createdAt = new Date(entry.createdAt);
entry.expiresAt = new Date(entry.expiresAt);
entry.lastAccessed = new Date(entry.lastAccessed);
// 만료 확인
if (entry.expiresAt < new Date()) {
await this.delete(key);
return null;
}
// 액세스 업데이트
entry.hits++;
entry.lastAccessed = new Date();
this.storage.setItem(`cache:${key}`, JSON.stringify(entry));
return entry;
} catch (error) {
console.error("브라우저 캐시 조회 오류:", error);
return null;
}
}
async set<T>(entry: CacheEntry<T>): Promise<void> {
try {
this.storage.setItem(`cache:${entry.key}`, JSON.stringify(entry));
} catch (error) {
// 저장 공간 부족 시 오래된 항목 정리
if (error.name === "QuotaExceededError") {
await this.evictOldEntries();
this.storage.setItem(`cache:${entry.key}`, JSON.stringify(entry));
} else {
throw error;
}
}
}
async delete(key: string): Promise<boolean> {
this.storage.removeItem(`cache:${key}`);
return true;
}
async clear(): Promise<void> {
const keys = await this.keys();
keys.forEach((key) => this.storage.removeItem(key));
}
async keys(pattern?: string): Promise<string[]> {
const keys: string[] = [];
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i);
if (key?.startsWith("cache:")) {
const cacheKey = key.replace("cache:", "");
if (!pattern || cacheKey.includes(pattern)) {
keys.push(key);
}
}
}
return keys;
}
async exists(key: string): Promise<boolean> {
return this.storage.getItem(`cache:${key}`) !== null;
}
async size(): Promise<number> {
return (await this.keys()).length;
}
async healthCheck(): Promise<boolean> {
try {
const testKey = "health-check";
this.storage.setItem(testKey, "test");
this.storage.removeItem(testKey);
return true;
} catch {
return false;
}
}
private async evictOldEntries(): Promise<void> {
const keys = await this.keys();
const entries: Array<{ key: string; lastAccessed: Date }> = [];
// 모든 엔트리의 최종 액세스 시간 수집
for (const key of keys) {
try {
const item = this.storage.getItem(key);
if (item) {
const entry = JSON.parse(item);
entries.push({
key,
lastAccessed: new Date(entry.lastAccessed),
});
}
} catch {
// 파싱 오류 시 해당 키 삭제
this.storage.removeItem(key);
}
}
// 오래된 순으로 정렬하여 25% 제거
entries.sort((a, b) => a.lastAccessed.getTime() - b.lastAccessed.getTime());
const toEvict = Math.ceil(entries.length * 0.25);
for (let i = 0; i < toEvict; i++) {
this.storage.removeItem(entries[i].key);
}
}
}
/**
* 메모리 캐시 프로바이더 (인메모리)
*/
export class MemoryCacheProvider extends BaseCacheProvider {
private cache = new Map<string, CacheEntry<any>>();
private maxSize: number;
constructor(maxSize = 1000) {
super("memory", "memory");
this.maxSize = maxSize;
this.startCleanupTimer();
}
async get<T>(key: string): Promise<CacheEntry<T> | null> {
const entry = this.cache.get(key);
if (!entry) return null;
// 만료 확인
if (entry.expiresAt < new Date()) {
this.cache.delete(key);
return null;
}
// 액세스 업데이트
entry.hits++;
entry.lastAccessed = new Date();
return entry;
}
async set<T>(entry: CacheEntry<T>): Promise<void> {
// 크기 제한 확인
if (this.cache.size >= this.maxSize) {
await this.evictLRU();
}
this.cache.set(entry.key, entry);
}
async delete(key: string): Promise<boolean> {
return this.cache.delete(key);
}
async clear(): Promise<void> {
this.cache.clear();
}
async keys(pattern?: string): Promise<string[]> {
const keys = Array.from(this.cache.keys());
return pattern ? keys.filter((key) => key.includes(pattern)) : keys;
}
async exists(key: string): Promise<boolean> {
return this.cache.has(key);
}
async size(): Promise<number> {
return this.cache.size;
}
async healthCheck(): Promise<boolean> {
return true; // 메모리 캐시는 항상 건강함
}
private async evictLRU(): Promise<void> {
// LRU 방식으로 오래된 항목 제거
let oldestKey = "";
let oldestTime = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (entry.lastAccessed.getTime() < oldestTime) {
oldestTime = entry.lastAccessed.getTime();
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
private startCleanupTimer(): void {
// 5분마다 만료된 항목 정리
setInterval(() => {
const now = new Date();
for (const [key, entry] of this.cache.entries()) {
if (entry.expiresAt < now) {
this.cache.delete(key);
}
}
}, 5 * 60 * 1000);
}
}
/**
* Redis 캐시 프로바이더 (서버 사이드)
*/
export class RedisCacheProvider extends BaseCacheProvider {
private client: any; // Redis 클라이언트
private connected = false;
constructor(redisClient?: any) {
super("server", "redis");
this.client = redisClient;
}
async connect(config?: {
host: string;
port: number;
password?: string;
db?: number;
}): Promise<void> {
if (!this.client && config) {
// Redis 클라이언트 생성 (실제 구현에서는 redis 라이브러리 사용)
console.log("Redis 연결 설정:", config);
this.connected = true;
}
}
async get<T>(key: string): Promise<CacheEntry<T> | null> {
if (!this.connected) return null;
try {
const data = await this.client?.get(`cache:${key}`);
if (!data) return null;
const entry = JSON.parse(data);
entry.createdAt = new Date(entry.createdAt);
entry.expiresAt = new Date(entry.expiresAt);
entry.lastAccessed = new Date(entry.lastAccessed);
// 만료 확인
if (entry.expiresAt < new Date()) {
await this.delete(key);
return null;
}
// 액세스 업데이트
entry.hits++;
entry.lastAccessed = new Date();
await this.client?.set(`cache:${key}`, JSON.stringify(entry));
return entry;
} catch (error) {
console.error("Redis 캐시 조회 오류:", error);
return null;
}
}
async set<T>(entry: CacheEntry<T>): Promise<void> {
if (!this.connected) return;
try {
const ttl = Math.ceil((entry.expiresAt.getTime() - Date.now()) / 1000);
await this.client?.setex(
`cache:${entry.key}`,
ttl,
JSON.stringify(entry)
);
} catch (error) {
console.error("Redis 캐시 저장 오류:", error);
throw error;
}
}
async delete(key: string): Promise<boolean> {
if (!this.connected) return false;
try {
const result = await this.client?.del(`cache:${key}`);
return result > 0;
} catch (error) {
console.error("Redis 캐시 삭제 오류:", error);
return false;
}
}
async clear(): Promise<void> {
if (!this.connected) return;
try {
const keys = await this.keys();
if (keys.length > 0) {
await this.client?.del(...keys);
}
} catch (error) {
console.error("Redis 캐시 클리어 오류:", error);
}
}
async keys(pattern = "*"): Promise<string[]> {
if (!this.connected) return [];
try {
return (await this.client?.keys(`cache:${pattern}`)) || [];
} catch (error) {
console.error("Redis 키 조회 오류:", error);
return [];
}
}
async exists(key: string): Promise<boolean> {
if (!this.connected) return false;
try {
const result = await this.client?.exists(`cache:${key}`);
return result > 0;
} catch (error) {
console.error("Redis 존재 확인 오류:", error);
return false;
}
}
async size(): Promise<number> {
if (!this.connected) return 0;
try {
const keys = await this.keys();
return keys.length;
} catch (error) {
console.error("Redis 크기 조회 오류:", error);
return 0;
}
}
async healthCheck(): Promise<boolean> {
if (!this.connected) return false;
try {
await this.client?.ping();
return true;
} catch {
return false;
}
}
}
/**
* CDN 캐시 프로바이더 (Cloudflare API)
*/
export class CDNCacheProvider extends BaseCacheProvider {
private apiToken: string;
private zoneId: string;
constructor(apiToken: string, zoneId: string) {
super("cdn", "cloudflare");
this.apiToken = apiToken;
this.zoneId = zoneId;
}
async get<T>(_key: string): Promise<CacheEntry<T> | null> {
// CDN 캐시는 직접 조회할 수 없음
return null;
}
async set<T>(_entry: CacheEntry<T>): Promise<void> {
// CDN 캐시는 요청에 따라 자동으로 설정됨
}
async delete(key: string): Promise<boolean> {
try {
// Cloudflare API를 사용하여 캐시 무효화
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${this.zoneId}/purge_cache`,
{
method: "POST",
headers: {
Authorization: `Bearer ${this.apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
files: [key],
}),
}
);
return response.ok;
} catch (error) {
console.error("CDN 캐시 무효화 오류:", error);
return false;
}
}
async clear(): Promise<void> {
try {
// 전체 캐시 무효화
await fetch(
`https://api.cloudflare.com/client/v4/zones/${this.zoneId}/purge_cache`,
{
method: "POST",
headers: {
Authorization: `Bearer ${this.apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
purge_everything: true,
}),
}
);
} catch (error) {
console.error("CDN 전체 캐시 클리어 오류:", error);
}
}
async keys(_pattern?: string): Promise<string[]> {
// CDN에서는 키 목록을 조회할 수 없음
return [];
}
async exists(_key: string): Promise<boolean> {
// CDN에서는 존재 여부를 직접 확인할 수 없음
return false;
}
async size(): Promise<number> {
// CDN에서는 크기를 직접 측정할 수 없음
return 0;
}
async healthCheck(): Promise<boolean> {
try {
// Cloudflare API 상태 확인
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${this.zoneId}`,
{
headers: {
Authorization: `Bearer ${this.apiToken}`,
},
}
);
return response.ok;
} catch {
return false;
}
}
}
/**
* 프로바이더 팩토리
*/
export class CacheProviderFactory {
static createProvider(
level: CacheLevel,
provider: CacheProvider,
options?: any
): BaseCacheProvider {
switch (provider) {
case "browser":
return new BrowserCacheProvider(options?.useSessionStorage);
case "memory":
return new MemoryCacheProvider(options?.maxSize);
case "redis":
return new RedisCacheProvider(options?.client);
case "cloudflare":
return new CDNCacheProvider(options?.apiToken, options?.zoneId);
default:
throw new Error(`지원하지 않는 캐시 프로바이더: ${provider}`);
}
}
}
export {
BaseCacheProvider,
BrowserCacheProvider,
MemoryCacheProvider,
RedisCacheProvider,
CDNCacheProvider,
};