๊ธฐ๋ณธ ๊ตฌ์กฐ
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 ์ฌ์ฉ ์ ์ฃผ์์ฌํญ:
frameName์์ฑ ํ์- ๋ฉ์๋๋
asyncํจ์์ฌ์ผ ํจ - ์ธ์คํด์ค๋ฅผ export ํด์ผ ํจ
- ๋ณต์กํ ๋ก์ง์ Model ์ฌ์ฉ ๊ณ ๋ ค
- 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();