๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
API ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ Sonamu.getContext()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ˜„์žฌ ์š”์ฒญ์˜ Context๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

Context ๊ฐ€์ ธ์˜ค๊ธฐ

import { BaseModelClass, api, Sonamu } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }
  
  @api({ httpMethod: "GET" })
  async getCurrentUser(): Promise<User> {
    // Context ๊ฐ€์ ธ์˜ค๊ธฐ
    const context = Sonamu.getContext();
    
    // user ์ •๋ณด ์ ‘๊ทผ
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    const rdb = this.getPuri("r");
    
    const user = await rdb
      .table("users")
      .where("id", context.user.id)
      .first();
    
    if (!user) {
      throw new Error("User not found");
    }
    
    return user;
  }
}
Sonamu.getContext()๋Š” ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ API ์š”์ฒญ์˜ Context๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ์ž‘์—… ์ค‘์—๋„ ์˜ฌ๋ฐ”๋ฅธ Context๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์–ธ์ œ ์‚ฌ์šฉํ•˜๋‚˜?

1. ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ํ™•์ธ

class PostModelClass extends BaseModelClass<
  PostSubsetKey,
  PostSubsetMapping,
  typeof postSubsetQueries,
  typeof postLoaderQueries
> {
  constructor() {
    super("Post", postSubsetQueries, postLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async create(params: PostSaveParams): Promise<{ postId: number }> {
    const context = Sonamu.getContext();
    
    // ๋กœ๊ทธ์ธ ํ™•์ธ
    if (!context.user) {
      throw new Error("Login required");
    }
    
    // ์ž‘์„ฑ์ž๋กœ ํ˜„์žฌ ์‚ฌ์šฉ์ž ์„ค์ •
    const wdb = this.getPuri("w");
    const [post] = await wdb
      .table("posts")
      .insert({
        ...params,
        user_id: context.user.id,
        author: context.user.username,
      })
      .returning({ id: "id" });
    
    return { postId: post.id };
  }
}

2. ๊ถŒํ•œ ํ™•์ธ

class AdminModelClass extends BaseModelClass<
  AdminSubsetKey,
  AdminSubsetMapping,
  typeof adminSubsetQueries,
  typeof adminLoaderQueries
> {
  constructor() {
    super("Admin", adminSubsetQueries, adminLoaderQueries);
  }

  @api({ httpMethod: "DELETE" })
  async deleteUser(userId: number): Promise<void> {
    const context = Sonamu.getContext();
    
    // ์ธ์ฆ ํ™•์ธ
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    // ๊ถŒํ•œ ํ™•์ธ
    if (context.user.role !== "admin") {
      throw new Error("Admin permission required");
    }
    
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
}

3. ์†Œ์œ ๊ถŒ ํ™•์ธ

class PostModelClass extends BaseModelClass<
  PostSubsetKey,
  PostSubsetMapping,
  typeof postSubsetQueries,
  typeof postLoaderQueries
> {
  constructor() {
    super("Post", postSubsetQueries, postLoaderQueries);
  }

  @api({ httpMethod: "PUT" })
  async update(
    postId: number,
    params: PostSaveParams
  ): Promise<void> {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    const rdb = this.getPuri("r");
    
    // ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
    const post = await rdb
      .table("posts")
      .where("id", postId)
      .first();
    
    if (!post) {
      throw new Error("Post not found");
    }
    
    // ์†Œ์œ ๊ถŒ ํ™•์ธ (์ž‘์„ฑ์ž์ด๊ฑฐ๋‚˜ ๊ด€๋ฆฌ์ž๋งŒ)
    const isOwner = post.user_id === context.user.id;
    const isAdmin = context.user.role === "admin";
    
    if (!isOwner && !isAdmin) {
      throw new Error("Permission denied");
    }
    
    // ์ˆ˜์ •
    const wdb = this.getPuri("w");
    await wdb.table("posts").where("id", postId).update(params);
  }
}

4. Request ์ •๋ณด ์ ‘๊ทผ

class AnalyticsModelClass extends BaseModelClass<
  AnalyticsSubsetKey,
  AnalyticsSubsetMapping,
  typeof analyticsSubsetQueries,
  typeof analyticsLoaderQueries
> {
  constructor() {
    super("Analytics", analyticsSubsetQueries, analyticsLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async trackEvent(params: EventParams): Promise<void> {
    const context = Sonamu.getContext();
    
    const wdb = this.getPuri("w");
    
    // Request ์ •๋ณด ๊ธฐ๋ก
    await wdb.table("analytics").insert({
      ...params,
      user_id: context.user?.id,
      ip_address: context.request.ip,
      user_agent: context.request.headers["user-agent"],
      referer: context.request.headers.referer,
      timestamp: new Date(),
    });
  }
}

5. Response ํ—ค๋” ์„ค์ •

class FileModelClass extends BaseModelClass<
  FileSubsetKey,
  FileSubsetMapping,
  typeof fileSubsetQueries,
  typeof fileLoaderQueries
> {
  constructor() {
    super("File", fileSubsetQueries, fileLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async download(fileId: number): Promise<Buffer> {
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    const file = await rdb
      .table("files")
      .where("id", fileId)
      .first();
    
    if (!file) {
      throw new Error("File not found");
    }
    
    // ๋‹ค์šด๋กœ๋“œ ํ—ค๋” ์„ค์ •
    context.reply.header("Content-Type", file.mime_type);
    context.reply.header(
      "Content-Disposition",
      `attachment; filename="${file.filename}"`
    );
    
    return Buffer.from(file.content);
  }
}

ํ—ฌํผ ํ•จ์ˆ˜ ํŒจํ„ด

์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ธ์ฆ ์ฒดํฌ

class BaseModelClass {
  // ์ธ์ฆ ํ™•์ธ ํ—ฌํผ
  protected requireAuth(): ContextUser {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      context.reply.status(401);
      throw new Error("Authentication required");
    }
    
    return context.user;
  }
  
  // ๊ถŒํ•œ ํ™•์ธ ํ—ฌํผ
  protected requireRole(role: UserRole): ContextUser {
    const user = this.requireAuth();
    
    if (user.role !== role) {
      const context = Sonamu.getContext();
      context.reply.status(403);
      throw new Error(`${role} permission required`);
    }
    
    return user;
  }
  
  // ๊ด€๋ฆฌ์ž ํ™•์ธ ํ—ฌํผ
  protected requireAdmin(): ContextUser {
    return this.requireRole("admin");
  }
}

// ์‚ฌ์šฉ
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "DELETE" })
  async remove(userId: number): Promise<void> {
    // ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ถŒํ•œ ํ™•์ธ
    this.requireAdmin();
    
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
  
  @api({ httpMethod: "GET" })
  async getMyProfile(): Promise<User> {
    // ๋กœ๊ทธ์ธ๋งŒ ํ™•์ธ
    const user = this.requireAuth();
    
    const rdb = this.getPuri("r");
    return rdb.table("users").where("id", user.id).first();
  }
}

์†Œ์œ ๊ถŒ ํ™•์ธ ํ—ฌํผ

class BaseModelClass {
  protected async requireOwnership(
    tableName: string,
    itemId: number,
    userIdField: string = "user_id"
  ): Promise<void> {
    const user = this.requireAuth();
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    const item = await rdb
      .table(tableName)
      .where("id", itemId)
      .first();
    
    if (!item) {
      context.reply.status(404);
      throw new Error("Item not found");
    }
    
    // ์†Œ์œ ์ž์ด๊ฑฐ๋‚˜ ๊ด€๋ฆฌ์ž๋งŒ ํ—ˆ์šฉ
    const isOwner = item[userIdField] === user.id;
    const isAdmin = user.role === "admin";
    
    if (!isOwner && !isAdmin) {
      context.reply.status(403);
      throw new Error("Permission denied");
    }
  }
}

// ์‚ฌ์šฉ
class PostModelClass extends BaseModelClass<
  PostSubsetKey,
  PostSubsetMapping,
  typeof postSubsetQueries,
  typeof postLoaderQueries
> {
  constructor() {
    super("Post", postSubsetQueries, postLoaderQueries);
  }

  @api({ httpMethod: "DELETE" })
  async remove(postId: number): Promise<void> {
    // ์†Œ์œ ๊ถŒ ์ž๋™ ํ™•์ธ
    await this.requireOwnership("posts", postId);
    
    const wdb = this.getPuri("w");
    await wdb.table("posts").where("id", postId).delete();
  }
}

์—ฌ๋Ÿฌ ์œ„์น˜์—์„œ ์‚ฌ์šฉ

Model ๋ฉ”์„œ๋“œ์—์„œ

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async getCurrentUser(): Promise<User> {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    return this.getUserById(context.user.id);
  }
  
  // private ๋ฉ”์„œ๋“œ์—์„œ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  private async getUserById(userId: number): Promise<User> {
    const context = Sonamu.getContext();
    
    // ์š”์ฒญ ๋กœ๊น…
    console.log("getUserById called from IP:", context.request.ip);
    
    const rdb = this.getPuri("r");
    const user = await rdb.table("users").where("id", userId).first();
    
    if (!user) {
      throw new Error("User not found");
    }
    
    return user;
  }
}

Frame์—์„œ

class AnalyticsFrame extends BaseFrameClass {
  frameName = "Analytics";
  
  @api({ httpMethod: "POST" })
  async track(params: TrackParams): Promise<void> {
    const context = Sonamu.getContext();
    
    const wdb = this.getPuri("w");
    
    await wdb.table("analytics").insert({
      event_type: params.eventType,
      event_data: JSON.stringify(params.data),
      user_id: context.user?.id,
      ip_address: context.request.ip,
      user_agent: context.request.headers["user-agent"],
      timestamp: new Date(),
    });
  }
}

๋น„๋™๊ธฐ ์ž‘์—…์—์„œ

class NotificationModelClass extends BaseModelClass<
  NotificationSubsetKey,
  NotificationSubsetMapping,
  typeof notificationSubsetQueries,
  typeof notificationLoaderQueries
> {
  constructor() {
    super("Notification", notificationSubsetQueries, notificationLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async sendBulk(params: {
    userIds: number[];
    message: string;
  }): Promise<void> {
    const context = Sonamu.getContext();
    
    // ๋น„๋™๊ธฐ ์ž‘์—…์—์„œ๋„ Context ์ ‘๊ทผ ๊ฐ€๋Šฅ
    const promises = params.userIds.map(async (userId) => {
      const innerContext = Sonamu.getContext();
      
      // ๋™์ผํ•œ Context
      console.log(innerContext.user?.id === context.user?.id); // true
      
      return this.sendNotification(userId, params.message);
    });
    
    await Promise.all(promises);
  }
  
  private async sendNotification(
    userId: number,
    message: string
  ): Promise<void> {
    const context = Sonamu.getContext();
    
    // ์—ฌ๊ธฐ์„œ๋„ ๋™์ผํ•œ Context
    const wdb = this.getPuri("w");
    
    await wdb.table("notifications").insert({
      user_id: userId,
      message,
      sender_id: context.user?.id,
      created_at: new Date(),
    });
  }
}

์‹ค์ „ ํŒจํ„ด

๊ฐ์‚ฌ ๋กœ๊ทธ (Audit Log)

class BaseModelClass {
  protected async logAudit(action: string, details: any): Promise<void> {
    const context = Sonamu.getContext();
    const wdb = this.getPuri("w");
    
    await wdb.table("audit_logs").insert({
      user_id: context.user?.id,
      action,
      details: JSON.stringify(details),
      ip_address: context.request.ip,
      user_agent: context.request.headers["user-agent"],
      timestamp: new Date(),
    });
  }
}

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "DELETE" })
  async remove(userId: number): Promise<void> {
    this.requireAdmin();
    
    const rdb = this.getPuri("r");
    const user = await rdb.table("users").where("id", userId).first();
    
    // ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก
    await this.logAudit("USER_DELETE", {
      targetUserId: userId,
      targetUsername: user.username,
    });
    
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
}

์š”์ฒญ ์†๋„ ์ œํ•œ ์ฒดํฌ

class ApiModelClass extends BaseModelClass<
  ApiSubsetKey,
  ApiSubsetMapping,
  typeof apiSubsetQueries,
  typeof apiLoaderQueries
> {
  constructor() {
    super("Api", apiSubsetQueries, apiLoaderQueries);
  }

  private async checkRateLimit(): Promise<void> {
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    const key = context.user
      ? `user:${context.user.id}`
      : `ip:${context.request.ip}`;
    
    const oneMinuteAgo = new Date(Date.now() - 60000);
    
    const [{ count }] = await rdb
      .table("request_logs")
      .where("key", key)
      .where("timestamp", ">=", oneMinuteAgo)
      .count({ count: "*" });
    
    if (count >= 100) {
      context.reply.status(429);
      throw new Error("Too many requests");
    }
    
    // ์š”์ฒญ ๊ธฐ๋ก
    const wdb = this.getPuri("w");
    await wdb.table("request_logs").insert({
      key,
      timestamp: new Date(),
    });
  }
  
  @api({ httpMethod: "GET" })
  async list(): Promise<any[]> {
    await this.checkRateLimit();
    
    const rdb = this.getPuri("r");
    return rdb.table("items").select("*");
  }
}

๋‹ค๊ตญ์–ด ์ง€์›

class I18nModelClass extends BaseModelClass<
  I18nSubsetKey,
  I18nSubsetMapping,
  typeof i18nSubsetQueries,
  typeof i18nLoaderQueries
> {
  constructor() {
    super("I18n", i18nSubsetQueries, i18nLoaderQueries);
  }

  private getLanguage(): string {
    const context = Sonamu.getContext();
    
    // Accept-Language ํ—ค๋”์—์„œ ์–ธ์–ด ์ถ”์ถœ
    const acceptLanguage = context.request.headers["accept-language"];
    
    if (acceptLanguage) {
      const lang = acceptLanguage.split(",")[0].split("-")[0];
      return lang;
    }
    
    return "en"; // ๊ธฐ๋ณธ๊ฐ’
  }
  
  @api({ httpMethod: "GET" })
  async getMessages(): Promise<Record<string, string>> {
    const language = this.getLanguage();
    const rdb = this.getPuri("r");
    
    const messages = await rdb
      .table("translations")
      .where("language", language)
      .select("*");
    
    return messages.reduce((acc, msg) => {
      acc[msg.key] = msg.value;
      return acc;
    }, {} as Record<string, string>);
  }
}

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

Context ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. API ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ๋งŒ ํ˜ธ์ถœ ๊ฐ€๋Šฅ
  2. ์ƒ์„ฑ์ž๋‚˜ ํด๋ž˜์Šค ํ•„๋“œ์—์„œ ํ˜ธ์ถœ ๋ถˆ๊ฐ€
  3. ๋น„๋™๊ธฐ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด๋„ ๊ฐ™์€ Context ์œ ์ง€
  4. ์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœํ•ด๋„ ๊ฐ™์€ ๊ฐ์ฒด ๋ฐ˜ํ™˜
  5. Context๋Š” ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์‚ฌ์šฉ ๊ถŒ์žฅ

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

// โŒ ์ž˜๋ชป๋จ: ์ƒ์„ฑ์ž์—์„œ ํ˜ธ์ถœ
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  private currentUser?: ContextUser;
  
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
    // ์ƒ์„ฑ์ž์—์„œ๋Š” Context ์—†์Œ
    const context = Sonamu.getContext(); // โ† ์—๋Ÿฌ
    this.currentUser = context.user;
  }
}

// โŒ ์ž˜๋ชป๋จ: ํด๋ž˜์Šค ํ•„๋“œ์—์„œ ํ˜ธ์ถœ
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  // ํด๋ž˜์Šค ํ•„๋“œ ์ดˆ๊ธฐํ™” ์‹œ์ ์—๋Š” Context ์—†์Œ
  private context = Sonamu.getContext(); // โ† ์—๋Ÿฌ
}

// โŒ ์ž˜๋ชป๋จ: API ๋ฉ”์„œ๋“œ ๋ฐ–์—์„œ ํ˜ธ์ถœ
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  private getContext() {
    // private ๋ฉ”์„œ๋“œ๋Š” ๊ฐ€๋Šฅํ•˜์ง€๋งŒ,
    // API ์š”์ฒญ ์ปจํ…์ŠคํŠธ ๋ฐ–์—์„œ ํ˜ธ์ถœํ•˜๋ฉด ์—๋Ÿฌ
    return Sonamu.getContext();
  }
}

// โœ… ์˜ฌ๋ฐ”๋ฆ„: API ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ ํ˜ธ์ถœ
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async getCurrentUser(): Promise<User> {
    const context = Sonamu.getContext(); // โ† ์ •์ƒ
    // ...
  }
  
  private async helperMethod(): Promise<void> {
    // API ๋ฉ”์„œ๋“œ์—์„œ ํ˜ธ์ถœ๋œ ๊ฒฝ์šฐ ์ •์ƒ
    const context = Sonamu.getContext(); // โ† ์ •์ƒ
    // ...
  }
}

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