๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Frame์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๊ตฌ์กฐ๋ฅผ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ

Frame ํŒŒ์ผ ์ƒ์„ฑ

// api/src/frames/health/health.frame.ts
import { BaseFrameClass, api } from "sonamu";

class HealthFrame extends BaseFrameClass {
  // Frame ์ด๋ฆ„ (URL ๊ฒฝ๋กœ์— ์‚ฌ์šฉ๋จ)
  frameName = "Health";
  
  @api({ httpMethod: "GET" })
  async check(): Promise<{
    status: string;
    timestamp: Date;
  }> {
    return {
      status: "ok",
      timestamp: new Date(),
    };
  }
}

export const HealthFrameInstance = new HealthFrame();
์ƒ์„ฑ๋˜๋Š” ์—”๋“œํฌ์ธํŠธ:
  • GET /api/health/check
Frame์€ frameName ์†์„ฑ์œผ๋กœ URL ๊ฒฝ๋กœ๊ฐ€ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค. ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜๋˜์–ด /api/{frameName}/{methodName} ํ˜•ํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

ํ•„์ˆ˜ ์š”์†Œ

1. BaseFrameClass ์ƒ์†

import { BaseFrameClass } from "sonamu";

class MyFrame extends BaseFrameClass {
  // Frame ๊ตฌํ˜„
}

2. frameName ์ •์˜

class HealthFrame extends BaseFrameClass {
  frameName = "Health"; // โ† ํ•„์ˆ˜!
}

// URL: /api/health/...

3. @api ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ

class HealthFrame extends BaseFrameClass {
  frameName = "Health";
  
  // @api ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ํ•„์ˆ˜
  @api({ httpMethod: "GET" })
  async check(): Promise<{ status: string }> {
    return { status: "ok" };
  }
}

4. ์ธ์Šคํ„ด์Šค export

export const HealthFrameInstance = new HealthFrame();

์‹ค์ „ ์˜ˆ์ œ

ํ—ฌ์Šค์ฒดํฌ Frame

// health.frame.ts
import { BaseFrameClass, api } from "sonamu";

interface HealthCheckResponse {
  status: "ok" | "error";
  timestamp: Date;
  uptime: number;
  memory: {
    total: number;
    used: number;
    free: number;
  };
  database: "connected" | "disconnected";
}

class HealthFrame extends BaseFrameClass {
  frameName = "Health";
  
  @api({ httpMethod: "GET" })
  async check(): Promise<HealthCheckResponse> {
    // DB ์—ฐ๊ฒฐ ์ฒดํฌ
    let dbStatus: "connected" | "disconnected" = "disconnected";
    
    try {
      const rdb = this.getPuri("r");
      await rdb.raw("SELECT 1");
      dbStatus = "connected";
    } catch (error) {
      console.error("Database connection failed:", error);
    }
    
    // ๋ฉ”๋ชจ๋ฆฌ ์ •๋ณด
    const memoryUsage = process.memoryUsage();
    
    return {
      status: dbStatus === "connected" ? "ok" : "error",
      timestamp: new Date(),
      uptime: process.uptime(),
      memory: {
        total: memoryUsage.heapTotal,
        used: memoryUsage.heapUsed,
        free: memoryUsage.heapTotal - memoryUsage.heapUsed,
      },
      database: dbStatus,
    };
  }
  
  @api({ httpMethod: "GET" })
  async ping(): Promise<{ message: string; timestamp: Date }> {
    return {
      message: "pong",
      timestamp: new Date(),
    };
  }
  
  @api({ httpMethod: "GET" })
  async version(): Promise<{
    version: string;
    buildDate: string;
    nodeVersion: string;
  }> {
    return {
      version: process.env.APP_VERSION || "1.0.0",
      buildDate: process.env.BUILD_DATE || new Date().toISOString(),
      nodeVersion: process.version,
    };
  }
}

export const HealthFrameInstance = new HealthFrame();

์œ ํ‹ธ๋ฆฌํ‹ฐ Frame

// utils.frame.ts
import { BaseFrameClass, api } from "sonamu";
import crypto from "crypto";

interface HashParams {
  text: string;
  algorithm: "md5" | "sha256" | "sha512";
}

interface EncryptParams {
  text: string;
  key: string;
}

interface DecryptParams {
  encrypted: string;
  key: string;
}

class UtilsFrame extends BaseFrameClass {
  frameName = "Utils";
  
  @api({ httpMethod: "POST" })
  async hash(params: HashParams): Promise<{ hash: string }> {
    const hash = crypto
      .createHash(params.algorithm)
      .update(params.text)
      .digest("hex");
    
    return { hash };
  }
  
  @api({ httpMethod: "POST" })
  async uuid(): Promise<{ uuid: string }> {
    return {
      uuid: crypto.randomUUID(),
    };
  }
  
  @api({ httpMethod: "POST" })
  async encrypt(params: EncryptParams): Promise<{ encrypted: string }> {
    const algorithm = "aes-256-cbc";
    const key = crypto.scryptSync(params.key, "salt", 32);
    const iv = crypto.randomBytes(16);
    
    const cipher = crypto.createCipheriv(algorithm, key, iv);
    
    let encrypted = cipher.update(params.text, "utf8", "hex");
    encrypted += cipher.final("hex");
    
    // IV๋ฅผ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ (๋ณตํ˜ธํ™”์— ํ•„์š”)
    return {
      encrypted: iv.toString("hex") + ":" + encrypted,
    };
  }
  
  @api({ httpMethod: "POST" })
  async decrypt(params: DecryptParams): Promise<{ decrypted: string }> {
    const algorithm = "aes-256-cbc";
    const key = crypto.scryptSync(params.key, "salt", 32);
    
    // IV์™€ ์•”ํ˜ธ๋ฌธ ๋ถ„๋ฆฌ
    const [ivHex, encrypted] = params.encrypted.split(":");
    const iv = Buffer.from(ivHex, "hex");
    
    const decipher = crypto.createDecipheriv(algorithm, key, iv);
    
    let decrypted = decipher.update(encrypted, "hex", "utf8");
    decrypted += decipher.final("utf8");
    
    return { decrypted };
  }
  
  @api({ httpMethod: "POST" })
  async randomString(params: {
    length: number;
    charset?: "alphanumeric" | "hex" | "base64";
  }): Promise<{ random: string }> {
    const length = Math.min(params.length, 1024); // ์ตœ๋Œ€ 1024์ž
    const charset = params.charset || "alphanumeric";
    
    let random: string;
    
    switch (charset) {
      case "hex":
        random = crypto.randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
        break;
      case "base64":
        random = crypto.randomBytes(Math.ceil(length * 3 / 4)).toString("base64").slice(0, length);
        break;
      case "alphanumeric":
      default:
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        random = Array.from(crypto.randomBytes(length))
          .map((byte) => chars[byte % chars.length])
          .join("");
        break;
    }
    
    return { random };
  }
}

export const UtilsFrameInstance = new UtilsFrame();

์™ธ๋ถ€ API ํ”„๋ก์‹œ Frame

// weather.frame.ts
import { BaseFrameClass, api } from "sonamu";
import axios from "axios";

interface WeatherParams {
  city: string;
  country?: string;
}

interface WeatherResponse {
  city: string;
  temperature: number;
  condition: string;
  humidity: number;
  windSpeed: number;
  timestamp: Date;
}

class WeatherFrame extends BaseFrameClass {
  frameName = "Weather";
  
  private apiKey = process.env.WEATHER_API_KEY || "";
  private baseUrl = "https://api.openweathermap.org/data/2.5";
  
  @api({ httpMethod: "GET" })
  async current(params: WeatherParams): Promise<WeatherResponse> {
    const query = params.country
      ? `${params.city},${params.country}`
      : params.city;
    
    try {
      const response = await axios.get(`${this.baseUrl}/weather`, {
        params: {
          q: query,
          appid: this.apiKey,
          units: "metric",
        },
      });
      
      const data = response.data;
      
      return {
        city: data.name,
        temperature: data.main.temp,
        condition: data.weather[0].description,
        humidity: data.main.humidity,
        windSpeed: data.wind.speed,
        timestamp: new Date(),
      };
    } catch (error) {
      throw new Error("Failed to fetch weather data");
    }
  }
  
  @api({ httpMethod: "GET" })
  async forecast(params: WeatherParams): Promise<{
    city: string;
    forecast: Array<{
      date: string;
      temperature: number;
      condition: string;
    }>;
  }> {
    const query = params.country
      ? `${params.city},${params.country}`
      : params.city;
    
    try {
      const response = await axios.get(`${this.baseUrl}/forecast`, {
        params: {
          q: query,
          appid: this.apiKey,
          units: "metric",
          cnt: 5, // 5์ผ ์˜ˆ๋ณด
        },
      });
      
      const data = response.data;
      
      return {
        city: data.city.name,
        forecast: data.list.map((item: any) => ({
          date: item.dt_txt,
          temperature: item.main.temp,
          condition: item.weather[0].description,
        })),
      };
    } catch (error) {
      throw new Error("Failed to fetch forecast data");
    }
  }
}

export const WeatherFrameInstance = new WeatherFrame();

DB ์ ‘๊ทผ

Frame์—์„œ๋„ DB์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฝ๊ธฐ ์ž‘์—…

class StatsFrame extends BaseFrameClass {
  frameName = "Stats";
  
  @api({ httpMethod: "GET" })
  async dashboard(): Promise<{
    totalUsers: number;
    totalPosts: number;
    totalComments: number;
  }> {
    const rdb = this.getPuri("r");
    
    const [{ userCount }] = await rdb
      .table("users")
      .count({ userCount: "*" });
    
    const [{ postCount }] = await rdb
      .table("posts")
      .count({ postCount: "*" });
    
    const [{ commentCount }] = await rdb
      .table("comments")
      .count({ commentCount: "*" });
    
    return {
      totalUsers: userCount,
      totalPosts: postCount,
      totalComments: commentCount,
    };
  }
}

์“ฐ๊ธฐ ์ž‘์—… (@transactional)

class AdminFrame extends BaseFrameClass {
  frameName = "Admin";
  
  @api({ httpMethod: "POST" })
  @transactional()
  async clearOldLogs(params: {
    beforeDate: Date;
  }): Promise<{
    deletedCount: number;
  }> {
    const wdb = this.getPuri("w");
    
    const deletedCount = await wdb
      .table("logs")
      .where("created_at", "<", params.beforeDate)
      .delete();
    
    return { deletedCount };
  }
}

์—๋Ÿฌ ์ฒ˜๋ฆฌ

try-catch ํŒจํ„ด

class ExternalApiFrame extends BaseFrameClass {
  frameName = "External";
  
  @api({ httpMethod: "GET" })
  async fetchData(params: { url: string }): Promise<any> {
    try {
      const response = await axios.get(params.url, {
        timeout: 5000,
      });
      
      return {
        success: true,
        data: response.data,
      };
    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw new Error(`External API error: ${error.message}`);
      }
      
      throw new Error("Unknown error occurred");
    }
  }
}

๊ฒ€์ฆ ๋กœ์ง

class ValidationFrame extends BaseFrameClass {
  frameName = "Validation";
  
  @api({ httpMethod: "POST" })
  async validateEmail(params: {
    email: string;
  }): Promise<{
    valid: boolean;
    message?: string;
  }> {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
    if (!emailRegex.test(params.email)) {
      return {
        valid: false,
        message: "Invalid email format",
      };
    }
    
    // ์ถ”๊ฐ€ ๊ฒ€์ฆ (๋„๋ฉ”์ธ ์ฒดํฌ ๋“ฑ)
    const domain = params.email.split("@")[1];
    const blockedDomains = ["tempmail.com", "throwaway.com"];
    
    if (blockedDomains.includes(domain)) {
      return {
        valid: false,
        message: "Domain not allowed",
      };
    }
    
    return { valid: true };
  }
}

ํŒŒ์ผ ๊ตฌ์กฐ

๊ถŒ์žฅ ๊ตฌ์กฐ

api/src/frames/
โ”œโ”€โ”€ health/
โ”‚   โ””โ”€โ”€ health.frame.ts
โ”œโ”€โ”€ utils/
โ”‚   โ””โ”€โ”€ utils.frame.ts
โ”œโ”€โ”€ weather/
โ”‚   โ””โ”€โ”€ weather.frame.ts
โ””โ”€โ”€ stats/
    โ””โ”€โ”€ stats.frame.ts

ํƒ€์ž… ํŒŒ์ผ ๋ถ„๋ฆฌ (์„ ํƒ)

api/src/frames/
โ”œโ”€โ”€ health/
โ”‚   โ”œโ”€โ”€ health.frame.ts
โ”‚   โ””โ”€โ”€ health.types.ts      # โ† ํƒ€์ž…๋งŒ ๋ถ„๋ฆฌ
โ”œโ”€โ”€ utils/
โ”‚   โ”œโ”€โ”€ utils.frame.ts
โ”‚   โ””โ”€โ”€ utils.types.ts
// utils.types.ts
export interface HashParams {
  text: string;
  algorithm: "md5" | "sha256" | "sha512";
}

export interface HashResponse {
  hash: string;
}

// utils.frame.ts
import { HashParams, HashResponse } from "./utils.types";

class UtilsFrame extends BaseFrameClass {
  @api({ httpMethod: "POST" })
  async hash(params: HashParams): Promise<HashResponse> {
    // ...
  }
}

์ฃผ์˜์‚ฌํ•ญ

Frame ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. frameName ์†์„ฑ ํ•„์ˆ˜
  2. ๋ฉ”์„œ๋“œ๋Š” async ํ•จ์ˆ˜์—ฌ์•ผ ํ•จ
  3. ์ธ์Šคํ„ด์Šค๋ฅผ export ํ•ด์•ผ ํ•จ
  4. ๋ณต์žกํ•œ ๋กœ์ง์€ Model ์‚ฌ์šฉ ๊ณ ๋ ค
  5. Entity๊ฐ€ ํ•„์š”ํ•˜๋ฉด Model๋กœ ์ „ํ™˜

ํ”ํ•œ ์‹ค์ˆ˜

// โŒ ์ž˜๋ชป๋จ: frameName ์—†์Œ
class HealthFrame extends BaseFrameClass {
  // frameName = "Health"; // โ† ํ•„์ˆ˜!
  
  @api({ httpMethod: "GET" })
  async check() { /* ... */ }
}

// โŒ ์ž˜๋ชป๋จ: ์ธ์Šคํ„ด์Šค export ์—†์Œ
class HealthFrame extends BaseFrameClass {
  frameName = "Health";
  // ...
}
// export const HealthFrameInstance = new HealthFrame(); // โ† ํ•„์ˆ˜!

// โŒ ์ž˜๋ชป๋จ: async ์•„๋‹˜
class HealthFrame extends BaseFrameClass {
  frameName = "Health";
  
  @api({ httpMethod: "GET" })
  check(): { status: string } { // โ† async ํ•„์š”
    return { status: "ok" };
  }
}

// โœ… ์˜ฌ๋ฐ”๋ฆ„
class HealthFrame extends BaseFrameClass {
  frameName = "Health";
  
  @api({ httpMethod: "GET" })
  async check(): Promise<{ status: string }> {
    return { status: "ok" };
  }
}

export const HealthFrameInstance = new HealthFrame();

๋‹ค์Œ ๋‹จ๊ณ„