> ## 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 메서드

> Context 접근 메서드 레퍼런스

Sonamu는 Context에 접근할 수 있는 여러 메서드를 제공합니다. 이 메서드들은 API 메서드 외부에서 Context가 필요한 경우에 유용합니다.

## Sonamu.getContext()

현재 실행 중인 요청의 Context를 반환합니다. AsyncLocalStorage를 통해 관리되므로, 같은 요청 스택 내 어디서든 호출할 수 있습니다.

### 시그니처

```typescript theme={null}
Sonamu.getContext(): Context
```

### 반환값

* **Context**: 현재 요청의 Context 객체
* **테스트 환경**: 빈 Context 객체 (request와 reply는 null)

### 예외

* **Error**: Context를 찾을 수 없는 경우 (프로덕션 환경에서만)

### 사용 예시

#### 헬퍼 함수에서 사용

```typescript theme={null}
// src/utils/auth-helper.ts
import { Sonamu } from "sonamu";

export function getCurrentUser() {
  const ctx = Sonamu.getContext();
  return ctx.user;
}

export function requireAuth() {
  const user = getCurrentUser();
  if (!user) {
    throw new UnauthorizedException("로그인이 필요합니다");
  }
  return user;
}
```

```typescript theme={null}
// src/models/post.model.ts
import { requireAuth } from "../utils/auth-helper";

class PostModelClass extends BaseModelClass {
  @api()
  async createPost(title: string, content: string) {
    // Context를 직접 받지 않아도 헬퍼 함수를 통해 사용자 정보 접근
    const user = requireAuth();

    return this.create({
      title,
      content,
      authorId: user.id,
    });
  }
}
```

#### 서비스 레이어에서 사용

```typescript theme={null}
// src/services/notification.service.ts
import { Sonamu } from "sonamu";

export class NotificationService {
  async sendNotification(userId: number, message: string) {
    const ctx = Sonamu.getContext();

    // Context의 locale 정보를 사용하여 다국어 메시지 전송
    const localizedMessage = this.localize(message, ctx.locale);

    // 알림 전송 로직
    await this.send(userId, localizedMessage);
  }

  private localize(message: string, locale?: string) {
    // 다국어 처리 로직
    return message;
  }
}
```

### 테스트 환경 동작

테스트 환경에서는 Context가 주입되지 않아도 에러를 발생시키지 않고 빈 Context를 반환합니다:

```typescript theme={null}
// test/post.test.ts
import { Sonamu } from "sonamu";

describe("PostModel", () => {
  it("should create post", async () => {
    // 테스트 환경에서는 Context 없이도 동작
    const ctx = Sonamu.getContext();
    console.log(ctx.request); // null (에러 없음)
  });
});
```

실제 Context가 필요한 테스트에서는 `runWithMockContext`를, 사용자 정보까지 지정하려면 `testAs`를 사용하세요:

```typescript theme={null}
import { runWithMockContext, testAs } from "sonamu/test";

it("should use mock context in test", async () => {
  await runWithMockContext(async () => {
    const ctx = Sonamu.getContext();
    expect(ctx.user).toBeNull();
  });
});

testAs(
  { id: 1, email: "test@example.com" },
  "should use context with user",
  async () => {
    const ctx = Sonamu.getContext();
    expect(ctx.user?.id).toBe(1);
  },
);
```

## 파일 업로드 컨텍스트

파일 업로드 요청에서 업로드된 파일 정보는 `Sonamu.getContext()`를 통해 접근합니다. `@upload` 데코레이터가 적용된 메서드에서만 사용할 수 있습니다.

업로드 모드에 따라 두 가지 속성을 사용합니다:

* **Buffer 모드 (기본)**: `bufferedFiles` - 메모리에 로드된 파일, MD5 계산/이미지 처리에 적합
* **Stream 모드**: `uploadedFiles` - 저장소로 직접 스트리밍된 파일, 대용량 파일에 적합

### 시그니처

```typescript theme={null}
// Buffer 모드 (기본)
const { bufferedFiles } = Sonamu.getContext();
// bufferedFiles: BufferedFile[]

// Stream 모드
const { uploadedFiles } = Sonamu.getContext();
// uploadedFiles: UploadedFile[]
```

### 타입

```typescript theme={null}
// Context의 파일 업로드 속성
bufferedFiles?: BufferedFile[];  // buffer 모드
uploadedFiles?: UploadedFile[];  // stream 모드
```

### 주의사항

* 파일 속성은 `@upload` 데코레이터를 사용한 경우에만 설정됩니다
* Buffer 모드(기본)에서는 `bufferedFiles`를, Stream 모드에서는 `uploadedFiles`를 사용합니다
* 단일 파일 업로드의 경우 `bufferedFiles?.[0]` 또는 `uploadedFiles?.[0]`으로 접근합니다

### 사용 예시

#### 단일 파일 업로드 (Buffer 모드)

```typescript theme={null}
class FileModelClass extends BaseModelClass {
  @upload()
  async uploadAvatar(ctx: Context) {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0]; // 첫 번째 파일 사용

    if (!file) {
      throw new BadRequestException("파일이 필요합니다");
    }

    // 파일 정보 확인
    console.log(file.filename); // "avatar.jpg"
    console.log(file.mimetype); // "image/jpeg"
    console.log(file.size); // 102400 (bytes)
    console.log(file.extname); // "jpg"

    // 파일 저장 (diskName, key 순서)
    const key = `avatars/${Date.now()}_${file.filename}`;
    const url = await file.saveToDisk("fs", key);

    return { url, filename: file.filename, size: file.size };
  }
}
```

#### 다중 파일 업로드 (Buffer 모드)

```typescript theme={null}
class FileModelClass extends BaseModelClass {
  @upload()
  async uploadDocuments(ctx: Context, folderId: number) {
    const { bufferedFiles } = Sonamu.getContext();

    if (!bufferedFiles || bufferedFiles.length === 0) {
      throw new BadRequestException("최소 1개 이상의 파일이 필요합니다");
    }

    const savedFiles = await Promise.all(
      bufferedFiles.map(async (file, i) => {
        const key = `documents/${Date.now()}_${i}_${file.filename}`;
        const url = await file.saveToDisk("fs", key);
        return { url, filename: file.filename, size: file.size };
      }),
    );

    // 파일 메타데이터 DB 저장
    await this.createMany(
      savedFiles.map((file) => ({
        folderId,
        filename: file.filename,
        url: file.url,
        size: file.size,
      })),
    );

    return { count: savedFiles.length, files: savedFiles };
  }
}
```

#### 대용량 파일 업로드 (Stream 모드)

```typescript theme={null}
class FileModelClass extends BaseModelClass {
  @upload({ consume: "stream", destination: "s3" })
  async uploadLargeFiles(ctx: Context) {
    const { uploadedFiles } = Sonamu.getContext();

    if (!uploadedFiles || uploadedFiles.length === 0) {
      throw new BadRequestException("파일이 필요합니다");
    }

    // 이미 저장소에 업로드 완료된 상태
    return {
      files: uploadedFiles.map((file) => ({
        filename: file.filename,
        url: file.url,
        size: file.size,
      })),
    };
  }
}
```

#### 헬퍼 함수에서 사용

```typescript theme={null}
// src/utils/file-helper.ts
import { Sonamu } from "sonamu";

export function getBufferedFiles() {
  const { bufferedFiles } = Sonamu.getContext();
  return bufferedFiles;
}

export function validateFileTypes(allowedTypes: string[]) {
  const { bufferedFiles } = Sonamu.getContext();

  if (!bufferedFiles) return;

  for (const file of bufferedFiles) {
    if (!allowedTypes.includes(file.mimetype)) {
      throw new BadRequestException(`허용되지 않는 파일 형식입니다: ${file.mimetype}`);
    }
  }
}
```

```typescript theme={null}
class FileModelClass extends BaseModelClass {
  @api()
  @upload()
  async uploadImages(ctx: Context) {
    // 이미지 파일만 허용
    validateFileTypes(["image/jpeg", "image/png", "image/gif", "image/webp"]);

    const files = getBufferedFiles();

    // 파일 처리 로직
    // ...
  }
}
```

## 관련 타입

### BufferedFile (Buffer 모드)

```typescript theme={null}
class BufferedFile {
  /** 원본 파일명 */
  get filename(): string;

  /** MIME 타입 */
  get mimetype(): string;

  /** 파일 크기 (bytes) */
  get size(): number;

  /** 확장자 (점 제외) */
  get extname(): string | false;

  /** 파일 Buffer (getter) */
  get buffer(): Buffer;

  /** 원본 MultipartFile 접근 */
  get raw(): MultipartFile;

  /** MD5 해시 계산 */
  async md5(): Promise<string>;

  /**
   * 파일을 디스크에 저장
   * @param diskName 디스크 이름 (예: 'fs', 's3')
   * @param key 저장 경로 (예: 'uploads/avatar.png')
   * @returns 저장된 파일의 URL
   */
  async saveToDisk(diskName: DriverKey, key: string): Promise<string>;
}
```

### UploadedFile (Stream 모드)

```typescript theme={null}
class UploadedFile {
  /** 원본 파일명 */
  get filename(): string;

  /** MIME 타입 */
  get mimetype(): string;

  /** 파일 크기 (bytes) */
  get size(): number;

  /** 확장자 (점 제외) */
  get extname(): string | false;

  /** 저장된 파일의 URL (Unsigned) */
  get url(): string;

  /** 저장된 파일의 URL (Signed, 만료 시간 있음) */
  get signedUrl(): string;

  /** 저장소 내 키 */
  get key(): string;

  /** 저장된 디스크 이름 */
  get diskName(): DriverKey;

  /** 저장소에서 파일 다운로드 */
  async download(): Promise<Buffer>;
}
```

## 주의사항

### AsyncLocalStorage 스택

`getContext()`는 AsyncLocalStorage를 통해 동작하므로, 반드시 같은 비동기 실행 스택 내에서 호출해야 합니다:

```typescript theme={null}
// ✅ 올바른 사용
class PostModelClass extends BaseModelClass {
  @api()
  async createPost(title: string) {
    const user = this.getCurrentUser(); // 같은 스택 내
    return this.create({ title, authorId: user.id });
  }

  private getCurrentUser() {
    return Sonamu.getContext().user;
  }
}

// ❌ 잘못된 사용
class PostModelClass extends BaseModelClass {
  @api()
  async createPost(title: string) {
    // setTimeout은 새로운 실행 스택을 생성
    setTimeout(() => {
      const ctx = Sonamu.getContext(); // Error!
    }, 1000);
  }
}
```

### @upload 데코레이터 필수

`bufferedFiles`/`uploadedFiles` 속성은 반드시 `@upload` 데코레이터가 적용된 메서드에서만 사용할 수 있습니다:

```typescript theme={null}
// ✅ 올바른 사용 (Buffer 모드)
@api()
@upload()
async uploadFile() {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0]; // OK
}

// ✅ 올바른 사용 (Stream 모드)
@api()
@upload({ consume: "stream", destination: "s3" })
async uploadFile() {
  const { uploadedFiles } = Sonamu.getContext();
  const file = uploadedFiles?.[0]; // OK
}

// ❌ 잘못된 사용
@api()
async uploadFile() {
  const { bufferedFiles } = Sonamu.getContext();
  // bufferedFiles는 undefined
}
```

## 관련 문서

* [Context](/ko/api-reference/context/sonamu-context)
* [AuthContext](/ko/api-reference/context/auth-context)
* [파일 업로드](/ko/api-development/file-upload)
* [테스트에서 Context 사용](/ko/testing/mocking/run-with-mock-context)
