๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

@api ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ

@api ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ Model์ด๋‚˜ Frame ํด๋ž˜์Šค์˜ ๋ฉ”์„œ๋“œ์— ๋ถ™์—ฌ REST API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.๊ธฐ๋ณธ ์‚ฌ์šฉ:
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async findById(id: number): Promise<User> {
    return this.findById(id);
  }

  @api({ httpMethod: "POST" })
  async createUser(params: UserSaveParams): Promise<User> {
    return this.save(params);
  }

  @api({ httpMethod: "PUT" })
  async updateUser(id: number, params: UserSaveParams): Promise<User> {
    return this.save({ id, ...params });
  }

  @api({ httpMethod: "DELETE" })
  async deleteUser(id: number): Promise<void> {
    await this.del(id);
  }
}
์ž๋™ ์ƒ์„ฑ๋˜๋Š” ์—”๋“œํฌ์ธํŠธ:
GET    /user/findById?id=1
POST   /user/createUser
PUT    /user/updateUser
DELETE /user/deleteUser?id=1
httpMethod (ํ•„์ˆ˜):
@api({ httpMethod: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" })
guards (์ธ์ฆ/๊ถŒํ•œ):
@api({ 
  httpMethod: "POST",
  guards: ["user", "admin"]  // user ๋กœ๊ทธ์ธ ํ•„์š”, admin ๊ถŒํ•œ ํ•„์š”
})
async adminOnly() { }
noAuth (์ธ์ฆ ๋ถˆํ•„์š”):
@api({ 
  httpMethod: "GET",
  noAuth: true  // ๋กœ๊ทธ์ธ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅ
})
async publicApi() { }
path (์ปค์Šคํ…€ ๊ฒฝ๋กœ):
@api({ 
  httpMethod: "GET",
  path: "/custom/path"  // /user/method ๋Œ€์‹  /custom/path ์‚ฌ์šฉ
})
async customPath() { }
์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ (๋‹จ์ผ ๊ฐ’):
@api({ httpMethod: "GET" })
async getUser(id: number, includeProfile?: boolean): Promise<User> {
  // GET /user/getUser?id=1&includeProfile=true
}
์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ (๊ฐ์ฒด):
@api({ httpMethod: "GET" })
async listUsers(listParams: UserListParams): Promise<ListResult<User>> {
  // GET /user/listUsers?num=20&page=1&orderBy=id-desc
  return this.list("A", listParams);
}
๋‹จ์ผ ๊ฐ์ฒด:
@api({ httpMethod: "POST" })
async createUser(params: UserSaveParams): Promise<User> {
  // POST /user/createUser
  // Body: { name: "John", email: "[email protected]" }
  return this.save(params);
}
๋‹ค์ค‘ ํŒŒ๋ผ๋ฏธํ„ฐ:
@api({ httpMethod: "POST" })
async updateProfile(
  userId: number, 
  profile: ProfileSaveParams
): Promise<Profile> {
  // POST /user/updateProfile
  // Body: { userId: 1, profile: { bio: "..." } }
  return ProfileModel.save({ user_id: userId, ...profile });
}
๋‹จ์ผ ํŒŒ์ผ:
import { api, upload, Sonamu } from "sonamu";

@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async uploadAvatar(): Promise<{ url: string; filename: string }> {
  // POST /user/uploadAvatar
  // Content-Type: multipart/form-data
  
  const { file } = Sonamu.getUploadContext();
  
  const key = `avatars/${Date.now()}_${file.filename}`;
  const url = await file.saveToDisk(key);
  
  return { url, filename: file.filename };
}
๋‹ค์ค‘ ํŒŒ์ผ:
import { api, upload, Sonamu } from "sonamu";

@api({ httpMethod: "POST" })
@upload({ mode: "multiple" })
async uploadFiles(): Promise<{ files: Array<{ url: string; filename: string }> }> {
  // POST /user/uploadFiles
  // Content-Type: multipart/form-data
  
  const { files } = Sonamu.getUploadContext();
  
  const results = [];
  for (const file of files) {
    const key = `files/${Date.now()}_${file.filename}`;
    const url = await file.saveToDisk(key);
    results.push({ url, filename: file.filename });
  }
  
  return { files: results };
}
BadRequestException (400):
import { BadRequestException } from "sonamu";

@api({ httpMethod: "POST" })
async createUser(params: UserSaveParams): Promise<User> {
  if (!params.email) {
    throw new BadRequestException("์ด๋ฉ”์ผ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค");
  }
  return this.save(params);
}
UnauthorizedException (401):
import { UnauthorizedException } from "sonamu";

@api({ httpMethod: "GET" })
async getMyProfile(): Promise<User> {
  const userId = Sonamu.getContext().user?.id;
  if (!userId) {
    throw new UnauthorizedException("๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
  }
  return this.findById(userId);
}
๊ถŒํ•œ ์˜ค๋ฅ˜ (403):
import { UnauthorizedException } from "sonamu";

@api({ httpMethod: "DELETE" })
async deleteUser(id: number): Promise<void> {
  const currentUser = Sonamu.getContext().user;
  if (currentUser?.role !== "admin") {
    throw new UnauthorizedException("๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
  }
  await this.del(id);
}
NotFoundException (404):
import { NotFoundException } from "sonamu";

@api({ httpMethod: "GET" })
async getUserById(id: number): Promise<User> {
  const user = await this.findById(id);
  if (!user) {
    throw new NotFoundException("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค");
  }
  return user;
}

Context์™€ ์„ธ์…˜

import { Sonamu } from "sonamu";

@api({ httpMethod: "GET" })
async getMyProfile(): Promise<User> {
  const context = Sonamu.getContext();
  const userId = context.user?.id;
  
  if (!userId) {
    throw new UnauthorizedException("๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
  }
  
  return this.findById(userId);
}
const context = Sonamu.getContext();

// ์‚ฌ์šฉ์ž ์ •๋ณด
context.user           // ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ๊ฐ์ฒด
context.userId         // ์‚ฌ์šฉ์ž ID

// ์š”์ฒญ ์ •๋ณด
context.ip             // ํด๋ผ์ด์–ธํŠธ IP
context.userAgent      // User-Agent ํ—ค๋”

// Fastify ๊ฐ์ฒด
context.request        // Fastify Request
context.reply          // Fastify Reply

// ์„ธ์…˜
context.session        // ์„ธ์…˜ ๊ฐ์ฒด
์„ธ์…˜ ์„ค์ •:
// sonamu.config.ts
export default {
  server: {
    session: {
      secret: process.env.SESSION_SECRET || "my-secret",
      cookie: {
        maxAge: 24 * 60 * 60 * 1000,  // 24์‹œ๊ฐ„
        secure: process.env.NODE_ENV === "production"
      }
    }
  }
} satisfies SonamuConfig;
์„ธ์…˜ ์ €์žฅ:
@api({ httpMethod: "POST", noAuth: true })
async login(email: string, password: string): Promise<User> {
  const user = await this.findOne("A", { email });
  
  if (!user || !await bcrypt.compare(password, user.password)) {
    throw new UnauthorizedException("์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค");
  }
  
  const context = Sonamu.getContext();
  context.session.userId = user.id;
  context.session.role = user.role;
  
  return user;
}
์„ธ์…˜ ์กฐํšŒ:
@api({ httpMethod: "GET" })
async getMyInfo(): Promise<User> {
  const context = Sonamu.getContext();
  const userId = context.session.userId;
  
  if (!userId) {
    throw new UnauthorizedException("๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
  }
  
  return this.findById(userId);
}
์„ธ์…˜ ์‚ญ์ œ:
@api({ httpMethod: "POST" })
async logout(): Promise<{ success: boolean }> {
  const context = Sonamu.getContext();
  context.session.destroy();
  return { success: true };
}

์ธ์ฆ๊ณผ ๊ถŒํ•œ

Guard ์„ค์ •:
// sonamu.config.ts
export default {
  server: {
    auth: {
      guards: {
        user: async (request) => {
          const userId = request.session.userId;
          if (!userId) return null;
          return UserModel.findById(userId);
        },
        admin: async (request) => {
          const user = request.session.user;
          if (!user || user.role !== "admin") return null;
          return user;
        }
      }
    }
  }
} satisfies SonamuConfig;
Guard ์‚ฌ์šฉ:
// user ๋กœ๊ทธ์ธ ํ•„์š”
@api({ httpMethod: "GET", guards: ["user"] })
async getMyProfile(): Promise<User> {
  const userId = Sonamu.getContext().user?.id;
  return this.findById(userId!);
}

// admin ๊ถŒํ•œ ํ•„์š”
@api({ httpMethod: "DELETE", guards: ["admin"] })
async deleteUser(id: number): Promise<void> {
  await this.del(id);
}

// user ๋˜๋Š” admin
@api({ httpMethod: "GET", guards: ["user", "admin"] })
async viewStats(): Promise<Stats> {
  // ...
}

์‘๋‹ต ํ˜•์‹

๊ธฐ๋ณธ ์‘๋‹ต:
@api({ httpMethod: "GET" })
async getUser(id: number): Promise<User> {
  return this.findById(id);
}

// ์‘๋‹ต:
// {
//   "id": 1,
//   "name": "John",
//   "email": "[email protected]"
// }
์ปค์Šคํ…€ ์‘๋‹ต:
@api({ httpMethod: "GET" })
async getUserWithStats(id: number): Promise<{
  user: User;
  stats: { postCount: number; commentCount: number };
}> {
  const user = await this.findById(id);
  const postCount = await PostModel.count({ user_id: id });
  const commentCount = await CommentModel.count({ user_id: id });
  
  return {
    user,
    stats: { postCount, commentCount }
  };
}
ํ—ค๋” ์„ค์ •:
@api({ httpMethod: "GET" })
async downloadFile(id: number): Promise<Buffer> {
  const file = await FileModel.findById(id);
  
  const context = Sonamu.getContext();
  context.reply.header("Content-Type", "application/pdf");
  context.reply.header("Content-Disposition", `attachment; filename="${file.name}"`);
  
  return file.buffer;
}
ListResult ์‚ฌ์šฉ:
@api({ httpMethod: "GET" })
async listUsers(listParams: UserListParams): Promise<ListResult<UserA>> {
  // ListResult = { rows: T[], total: number }
  return this.list("A", listParams);
}

// ์‘๋‹ต:
// {
//   "rows": [
//     { "id": 1, "name": "John" },
//     { "id": 2, "name": "Jane" }
//   ],
//   "total": 100
// }

ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™

์„œ๋น„์Šค ํŒŒ์ผ ์ž๋™ ์ƒ์„ฑ:Model ํŒŒ์ผ์ด ๋ณ€๊ฒฝ๋˜๋ฉด {entity}.service.ts๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
// web/src/services/user/user.service.ts (์ž๋™ ์ƒ์„ฑ)
export const UserService = {
  async findById(id: number): Promise<User> {
    const response = await axios.get("/user/findById", { params: { id } });
    return response.data;
  },
  
  async createUser(params: UserSaveParams): Promise<User> {
    const response = await axios.post("/user/createUser", params);
    return response.data;
  }
};
React์—์„œ ์‚ฌ์šฉ:
import { UserService } from "@/services/user/user.service";

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    UserService.findById(userId).then(setUser);
  }, [userId]);
  
  return <div>{user?.name}</div>;
}
TanStack Query hook ์‚ฌ์šฉ:
import { useUserFindById } from "@/services/user/user.service";

function UserProfile({ userId }: { userId: number }) {
  const { data: user, error, isLoading } = useUserFindById({ id: userId });
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return <div>{user?.name}</div>;
}

๊ด€๋ จ ๋ฌธ์„œ