Sonamu.getContext()를 호출하여 현재 요청의 Context를 가져올 수 있습니다.
기본 사용법
Context 가져오기
복사
import { BaseModelClass, api, Sonamu } from "sonamu";
class UserModel extends BaseModelClass {
modelName = "User";
@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 PostModel extends BaseModelClass {
@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 AdminModel extends BaseModelClass {
@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 PostModel extends BaseModelClass {
@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 AnalyticsModel extends BaseModelClass {
@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 FileModel extends BaseModelClass {
@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 UserModel extends BaseModelClass {
@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 PostModel extends BaseModelClass {
@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 UserModel extends BaseModelClass {
@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 NotificationModel extends BaseModelClass {
@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 UserModel extends BaseModelClass {
@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 ApiModel extends BaseModelClass {
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 I18nModel extends BaseModelClass {
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 사용 시 주의사항:
- API 메서드 내에서만 호출 가능
- 생성자나 클래스 필드에서 호출 불가
- 비동기 경계를 넘어도 같은 Context 유지
- 여러 번 호출해도 같은 객체 반환
- Context는 읽기 전용으로 사용 권장
흔한 실수
복사
// ❌ 잘못됨: 생성자에서 호출
class UserModel extends BaseModelClass {
private currentUser?: ContextUser;
constructor() {
super();
// 생성자에서는 Context 없음
const context = Sonamu.getContext(); // ← 에러
this.currentUser = context.user;
}
}
// ❌ 잘못됨: 클래스 필드에서 호출
class UserModel extends BaseModelClass {
// 클래스 필드 초기화 시점에는 Context 없음
private context = Sonamu.getContext(); // ← 에러
}
// ❌ 잘못됨: API 메서드 밖에서 호출
class UserModel extends BaseModelClass {
private getContext() {
// private 메서드는 가능하지만,
// API 요청 컨텍스트 밖에서 호출하면 에러
return Sonamu.getContext();
}
}
// ✅ 올바름: API 메서드 내에서 호출
class UserModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getCurrentUser(): Promise<User> {
const context = Sonamu.getContext(); // ← 정상
// ...
}
private async helperMethod(): Promise<void> {
// API 메서드에서 호출된 경우 정상
const context = Sonamu.getContext(); // ← 정상
// ...
}
}
