> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Context 가져오기

> Sonamu.getContext()로 요청 컨텍스트 접근하기

API 메서드 내에서 `Sonamu.getContext()`를 호출하여 현재 요청의 Context를 가져올 수 있습니다.

## 기본 사용법

### Context 가져오기

```typescript theme={null}
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;
  }
}
```

<Info>
  `Sonamu.getContext()`는 현재 실행 중인 API 요청의 Context를 반환합니다. 비동기 작업 중에도 올바른
  Context를 반환합니다.
</Info>

## 언제 사용하나?

### 1. 인증된 사용자 확인

```typescript theme={null}
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");

    return { postId: post.id };
  }
}
```

### 2. 권한 확인

```typescript theme={null}
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. 소유권 확인

```typescript theme={null}
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 정보 접근

```typescript theme={null}
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 헤더 설정

```typescript theme={null}
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);
  }
}
```

## 헬퍼 함수 패턴

### 재사용 가능한 인증 체크

```typescript theme={null}
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();
  }
}
```

### 소유권 확인 헬퍼

```typescript theme={null}
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 메서드에서

```typescript theme={null}
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에서

```typescript theme={null}
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(),
    });
  }
}
```

### 비동기 작업에서

```typescript theme={null}
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)

```typescript theme={null}
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();
  }
}
```

### 요청 속도 제한 체크

```typescript theme={null}
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("*");
  }
}
```

### 다국어 지원

```typescript theme={null}
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>,
    );
  }
}
```

## 주의사항

<Warning>
  **Context 사용 시 주의사항**: 1. API 메서드 내에서만 호출 가능 2. 생성자나 클래스 필드에서 호출
  불가 3. 비동기 경계를 넘어도 같은 Context 유지 4. 여러 번 호출해도 같은 객체 반환 5. Context는
  읽기 전용으로 사용 권장
</Warning>

### 흔한 실수

```typescript theme={null}
// ❌ 잘못됨: 생성자에서 호출
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(); // ← 정상
    // ...
  }
}
```

## 다음 단계

<CardGroup cols={2}>
  <Card title="SonamuContext" icon="cube" href="/ko/api-development/context/sonamu-context">
    Context 구조 이해하기
  </Card>

  <Card title="커스텀 Context" icon="puzzle-piece" href="/ko/api-development/context/custom-context">
    Context 확장하기
  </Card>

  <Card title="@api 데코레이터" icon="at" href="/ko/api-development/creating-apis/api-decorator">
    API 기본 사용법
  </Card>

  <Card title="에러 처리" icon="triangle-exclamation" href="/ko/api-development/error-handling">
    API 에러 핸들링
  </Card>
</CardGroup>
