메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
SonamuλŠ” Entity μ •μ˜λ‘œλΆ€ν„° λ‹€μ–‘ν•œ νƒ€μž…μ„ μžλ™ μƒμ„±ν•©λ‹ˆλ‹€. Base νƒ€μž…λΏλ§Œ μ•„λ‹ˆλΌ API νŒŒλΌλ―Έν„°, Subset νƒ€μž…, Enum 등이 μžλ™μœΌλ‘œ μƒμ„±λ˜μ–΄ μ™„λ²½ν•œ νƒ€μž… μ•ˆμ „μ„±μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

생성 νƒ€μž… κ°œμš”

Base νƒ€μž…

Entity의 κΈ°λ³Έ νƒ€μž… User, Post, Comment

Subset νƒ€μž…

λΆ€λΆ„ 쑰회 νƒ€μž… UserA, UserP, UserSS

Params νƒ€μž…

API νŒŒλΌλ―Έν„° νƒ€μž… ListParams, SaveParams

Enum νƒ€μž…

Enumκ³Ό 헬퍼 ν•¨μˆ˜ UserRole, userRoleLabel

Base νƒ€μž…

Entity의 λͺ¨λ“  ν•„λ“œλ₯Ό ν¬ν•¨ν•˜λŠ” κΈ°λ³Έ νƒ€μž…μž…λ‹ˆλ‹€.
{
  "id": "User",
  "props": [
    { "name": "id", "type": "integer" },
    { "name": "email", "type": "string" },
    { "name": "username", "type": "string" },
    { "name": "created_at", "type": "date" }
  ]
}
νŠΉμ§•:
  • Relation의 HasMany, ManyToManyλŠ” μ œμ™Έ
  • BelongsToOne, OneToOne(hasJoinColumn)은 {name}_id ν˜•νƒœλ‘œ 포함
  • Virtual ν•„λ“œλŠ” νƒ€μž…λ§Œ μ •μ˜, 값은 Enhancerμ—μ„œ 계산

Subset νƒ€μž…

Entity의 λΆ€λΆ„ ν•„λ“œλ§Œ ν¬ν•¨ν•˜λŠ” νƒ€μž…μž…λ‹ˆλ‹€.
{
  "subsets": {
    "A": ["id", "email", "username", "created_at"],
    "P": ["id", "email", "employee.id", "employee.department.name"],
    "SS": ["id", "email"]
  }
}
μ‚¬μš© μ˜ˆμ‹œ:
import type { UserSubsetKey, UserSubsetMapping } from "./sonamu.generated";

// μ œλ„€λ¦­μœΌλ‘œ Subset νƒ€μž… 선택
async function findMany<T extends UserSubsetKey>(
  subset: T
): Promise<UserSubsetMapping[T][]> {
  // ...
}

const users = await findMany("A"); // UserA[] νƒ€μž…
const profiles = await findMany("P"); // UserP[] νƒ€μž…

ListParams νƒ€μž…

λͺ©λ‘ 쑰회 API의 νŒŒλΌλ―Έν„° νƒ€μž…μž…λ‹ˆλ‹€.
{
  "props": [
    { "name": "email", "type": "string", "toFilter": true },
    { "name": "username", "type": "string", "toFilter": true }
  ]
}
μ‚¬μš© μ˜ˆμ‹œ:
// API 호좜
const result = await findManyUsers({
  num: 20,
  page: 1,
  search: "email",
  keyword: "john",
  orderBy: "created_at-desc",
  email: ["[email protected]", "[email protected]"], // 배열도 κ°€λŠ₯
});
zArrayable: 단일 κ°’κ³Ό 배열을 λͺ¨λ‘ ν—ˆμš©ν•˜λŠ” ν—¬νΌμž…λ‹ˆλ‹€.
// λ‘˜ λ‹€ κ°€λŠ₯
{
  email: "[email protected]";
}
{
  email: ["[email protected]", "[email protected]"];
}

SaveParams νƒ€μž…

μ €μž₯ API의 νŒŒλΌλ―Έν„° νƒ€μž…μž…λ‹ˆλ‹€.
// SaveParams = Base νƒ€μž…μ˜ idλ₯Ό optional둜
export const UserSaveParams = User.partial({ id: true });
export type UserSaveParams = z.infer<typeof UserSaveParams>;

// 동일:
// type UserSaveParams = {
// id?: number;
// email: string;
// username: string;
// created_at: Date;
// }

νŠΉμ§•:
  • id만 optional, λ‚˜λ¨Έμ§€ ν•„λ“œλŠ” ν•„μˆ˜
  • idκ°€ 있으면 UPDATE, μ—†μœΌλ©΄ INSERT
  • λ°°μ—΄λ‘œ μ—¬λŸ¬ λ ˆμ½”λ“œ λ™μ‹œ μ €μž₯ κ°€λŠ₯

Enum νƒ€μž…

Entity의 Enum이 μžλ™μœΌλ‘œ μƒμ„±λ©λ‹ˆλ‹€.
{
  "enums": {
    "UserRole": {
      "admin": "κ΄€λ¦¬μž",
      "moderator": "운영자",
      "normal": "일반 μ‚¬μš©μž"
    }
  }
}
μ‚¬μš© μ˜ˆμ‹œ:
import { UserRole, userRoleLabel } from "./sonamu.generated";

// νƒ€μž… μ•ˆμ „ν•œ Enum μ‚¬μš©
const role: UserRole = "admin";

// 라벨 ν‘œμ‹œ
console.log(userRoleLabel(role)); // "κ΄€λ¦¬μž"

// UIμ—μ„œ 선택 μ˜΅μ…˜ 생성
const options = UserRole.options.map((value) => ({
  value,
  label: userRoleLabel(value),
}));
// [
//   { value: "admin", label: "κ΄€λ¦¬μž" },
//   { value: "moderator", label: "운영자" },
//   { value: "normal", label: "일반 μ‚¬μš©μž" }
// ]

μ»€μŠ€ν…€ Params νƒ€μž…

Modelμ—μ„œ μΆ”κ°€ APIλ₯Ό λ§Œλ“€ λ•Œ μ»€μŠ€ν…€ νŒŒλΌλ―Έν„° νƒ€μž…μ„ μ •μ˜ν•©λ‹ˆλ‹€.
// μ»€μŠ€ν…€ νŒŒλΌλ―Έν„° μΆ”κ°€
export const UserLoginParams = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  rememberMe: z.boolean().optional(),
});
export type UserLoginParams = z.infer<typeof UserLoginParams>;

export const UserRegisterParams = User.pick({
  email: true,
  username: true,
}).extend({
  password: z.string().min(8),
  passwordConfirm: z.string().min(8),
}).refine(
  (data) => data.password === data.passwordConfirm,
  { message: "λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€", path: ["passwordConfirm"] }
);
export type UserRegisterParams = z.infer<typeof UserRegisterParams>;
μžλ™ 생성:
// services.generated.ts
export async function loginUser(
  params: UserLoginParams
): Promise<{ user: User; token: string }> {
  const { data } = await axios.post("/user/login", { params });
  return data;
}

export function useLoginUser() {
  return useMutation({
    mutationFn: (params: UserLoginParams) => loginUser(params),
  });
}

Action Params νƒ€μž…

νŠΉμ • μ•‘μ…˜μ„ μœ„ν•œ νŒŒλΌλ―Έν„° νƒ€μž…μž…λ‹ˆλ‹€.
// ID λ°°μ—΄ νŒŒλΌλ―Έν„°
export const UserBulkDeleteParams = z.object({
  ids: z.number().int().array().min(1),
});
export type UserBulkDeleteParams = z.infer<typeof UserBulkDeleteParams>;

// μƒνƒœ λ³€κ²½ νŒŒλΌλ―Έν„°
export const UserStatusUpdateParams = z.object({
ids: z.number().int().array().min(1),
status: z.enum(["active", "suspended", "deleted"]),
reason: z.string().optional(),
});
export type UserStatusUpdateParams = z.infer<typeof UserStatusUpdateParams>;

ListResult νƒ€μž…

λͺ©λ‘ 쑰회 결과의 νƒ€μž…μž…λ‹ˆλ‹€.
sonamu.shared.ts
export type ListResult<
  LP extends { num?: number; page?: number; queryMode?: string },
  T
> = LP["queryMode"] extends "list"
  ? { rows: T[] }
  : LP["queryMode"] extends "count"
  ? { total: number }
  : { rows: T[]; total: number };
μ‚¬μš© μ˜ˆμ‹œ:
// queryMode에 따라 νƒ€μž…μ΄ μžλ™μœΌλ‘œ 결정됨

// queryMode: "both" (κΈ°λ³Έκ°’)
const result = await findManyUsers({ num: 10, page: 1 });
// ListResult<UserListParams, User> = { rows: User[]; total: number }

// queryMode: "list"
const result = await findManyUsers({ num: 10, page: 1, queryMode: "list" });
// ListResult<UserListParams, User> = { rows: User[] }

// queryMode: "count"
const result = await findManyUsers({ queryMode: "count" });
// ListResult<UserListParams, User> = { total: number }
쑰건뢀 νƒ€μž…: ListResultλŠ” TypeScript의 쑰건뢀 νƒ€μž…μ„ ν™œμš©ν•˜μ—¬ queryMode에 따라 λ‹€λ₯Έ νƒ€μž…μ„ λ°˜ν™˜ν•©λ‹ˆλ‹€.

νƒ€μž… 내보내기

μƒμ„±λœ νƒ€μž…μ€ μžλ™μœΌλ‘œ exportλ©λ‹ˆλ‹€.
sonamu.generated.ts
// Entity별 νƒ€μž… μ „λΆ€ re-export
export * from "./user/user.model";
export * from "./post/post.model";
export * from "./comment/comment.model";

// Model μΈμŠ€ν„΄μŠ€λ„ export
export { UserModel } from "./user/user.model";
export { PostModel } from "./post/post.model";
μ‚¬μš©:
// ν•œ κ³³μ—μ„œ import
import {
  User,
  UserListParams,
  UserSaveParams,
  UserRole,
} from "./sonamu.generated";

// λ˜λŠ” κ°œλ³„ import
import type { User } from "./user/user.types";
import { UserModel } from "./user/user.model";

νƒ€μž… μœ ν‹Έλ¦¬ν‹°

Sonamuκ°€ μ œκ³΅ν•˜λŠ” μœ μš©ν•œ νƒ€μž… μœ ν‹Έλ¦¬ν‹°μž…λ‹ˆλ‹€.

zArrayable

단일 κ°’κ³Ό 배열을 λͺ¨λ‘ ν—ˆμš©ν•˜λŠ” νƒ€μž…μž…λ‹ˆλ‹€.
import { zArrayable } from "sonamu";

const UserListParams = z.object({
  id: zArrayable(z.number().int()).optional(),
  email: zArrayable(z.string()).optional(),
});

// λ‘˜ λ‹€ κ°€λŠ₯
{
  id: 1;
}
{
  id: [1, 2, 3];
}

DistributiveOmit

일반 Omit보닀 Union νƒ€μž…μ—μ„œ μ•ˆμ „ν•©λ‹ˆλ‹€.
import type { DistributiveOmit } from "sonamu";

type UserOrAdmin =
  | { type: "user"; userId: number }
  | { type: "admin"; adminId: number };

// DistributiveOmit은 각 νƒ€μž…μ— κ°œλ³„ 적용
type WithoutId = DistributiveOmit<UserOrAdmin, "userId" | "adminId">;
// = { type: "user" } | { type: "admin" }

νƒ€μž… 생성 μ»€μŠ€ν„°λ§ˆμ΄μ§•

μžλ™ μƒμ„±λœ νƒ€μž…μ„ ν™•μž₯ν•˜κ±°λ‚˜ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Params ν™•μž₯

user.types.ts
// κΈ°λ³Έ ListParams ν™•μž₯
export const ExtendedUserListParams = UserListParams.extend({
  includeDeleted: z.boolean().optional(),
  dateRange: z
    .object({
      from: z.date(),
      to: z.date(),
    })
    .optional(),
});
export type ExtendedUserListParams = z.infer<typeof ExtendedUserListParams>;

Subset νƒ€μž… ν™•μž₯

user.types.ts
// Subset νƒ€μž…μ— 계산 ν•„λ“œ μΆ”κ°€
export type UserAWithStats = UserA & {
  post_count: number;
  follower_count: number;
};

쑰건뢀 νƒ€μž… 생성

user.types.ts
// 역할에 따라 λ‹€λ₯Έ νƒ€μž…
export type UserByRole<T extends UserRole> = T extends "admin"
  ? UserA & { permissions: string[] }
  : T extends "normal"
  ? UserSS
  : User;

μ‹€μ „ μ˜ˆμ‹œ

μ‹€μ œ ν”„λ‘œμ νŠΈμ—μ„œ 생성 νƒ€μž…μ„ ν™œμš©ν•˜λŠ” μ˜ˆμ‹œμž…λ‹ˆλ‹€.
import type {
  UserSubsetKey,
  UserSubsetMapping,
  UserListParams,
  UserSaveParams,
  UserLoginParams,
} from "./user.types";

class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries

> {
> @api({ httpMethod: "GET" })
> async findMany<T extends UserSubsetKey>(

    subset: T,
    params?: UserListParams

): Promise<ListResult<UserListParams, UserSubsetMapping[T]>> {
// νƒ€μž… μ•ˆμ „ν•œ κ΅¬ν˜„
}

@api({ httpMethod: "POST" })
async save(params: UserSaveParams[]): Promise<number[]> {
// νƒ€μž… μ•ˆμ „ν•œ κ΅¬ν˜„
}

@api({ httpMethod: "POST" })
async login(params: UserLoginParams): Promise<{ user: User; token: string }> {
// νƒ€μž… μ•ˆμ „ν•œ κ΅¬ν˜„
}
}

λ‹€μŒ 단계