Model์ Sonamu์ ํต์ฌ ๋ ์ด์ด๋ก, ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ฐ์ดํฐ ์ก์ธ์ค๋ฅผ ๋ด๋นํฉ๋๋ค. Entity ์ ์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ, API ์๋ํฌ์ธํธ, ํ์
์์ ํ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
Model์ ์ญํ
๋ฐ์ดํฐ ์ก์ธ์ค
๋ฐ์ดํฐ๋ฒ ์ด์ค CRUD ์์
Puri ์ฟผ๋ฆฌ ๋น๋๋ฅผ ํตํ ํ์
์์ ํ ์ฟผ๋ฆฌ
๋น์ฆ๋์ค ๋ก์ง
๋๋ฉ์ธ ๊ท์น ๊ตฌํ๋ฐ์ดํฐ ๊ฒ์ฆ, ๋ณํ, ๊ณ์ฐ
API ์๋ํฌ์ธํธ
HTTP API ์๋ ์์ฑ@api ๋ฐ์ฝ๋ ์ดํฐ๋ก REST API ์ ๊ณต
ํ์
์์ ์ฑ
์ปดํ์ผ ํ์ ํ์
์ฒดํฌEntity ๊ธฐ๋ฐ ์๋ ํ์
์์ฑ
Sonamu ์ํคํ
์ฒ์์์ ์์น
๋ ์ด์ด ๊ตฌ์กฐ:
- Client - ํ๋ก ํธ์๋ (React, Vue ๋ฑ)
- API Layer - HTTP ์๋ํฌ์ธํธ (@api ๋ฐ์ฝ๋ ์ดํฐ)
- Model Layer - ๋น์ฆ๋์ค ๋ก์ง โญ
- Database - PostgreSQL
Model์ API Layer์ Database ์ฌ์ด์ ๋น์ฆ๋์ค ๋ก์ง ๋ ์ด์ด์
๋๋ค. ๋ชจ๋ ๋ฐ์ดํฐ ์ ๊ทผ๊ณผ ๋น์ฆ๋์ค ๊ท์น์ Model์์ ์ฒ๋ฆฌ๋ฉ๋๋ค.
Model ํด๋์ค ๊ตฌ์กฐ
๊ธฐ๋ณธ ๊ตฌ์กฐ
import { BaseModelClass, api } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";
class UserModelClass extends BaseModelClass<
UserSubsetKey, // "A" | "P" | "SS"
UserSubsetMapping, // ๊ฐ subset์ ํ์
typeof userSubsetQueries, // subset ์ฟผ๋ฆฌ ํจ์๋ค
typeof userLoaderQueries // loader ์ฟผ๋ฆฌ ํจ์๋ค
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
@api({ httpMethod: "GET" })
async findById(id: number): Promise<UserSubsetMapping["A"]> {
// ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ
}
}
export const UserModel = new UserModelClass();
์ ๋ค๋ฆญ ํ์
ํ๋ผ๋ฏธํฐ
| ํ๋ผ๋ฏธํฐ | ์ค๋ช
| ์์ |
|---|
TSubsetKey | Subset ํค์ Union ํ์
| "A" | "P" | "SS" |
TSubsetMapping | Subset๋ณ ๊ฒฐ๊ณผ ํ์
๋งคํ | { A: UserA, P: UserP } |
TSubsetQueries | Subset ์ฟผ๋ฆฌ ํจ์ ํ์
| ์๋ ์์ฑ |
TLoaderQueries | Loader ์ฟผ๋ฆฌ ํ์
| ์๋ ์์ฑ |
์ด ์ ๋ค๋ฆญ ํ์
๋ค์ sonamu.generated.ts์ sonamu.generated.sso.ts์์ ์๋์ผ๋ก ์์ฑ๋๋ฏ๋ก ์ง์ ์์ฑํ ํ์๊ฐ ์์ต๋๋ค.
Model vs Entity vs Types
Sonamu์์ ์์ฃผ ํผ๋๋๋ ๊ฐ๋
๋ค์ ์ ๋ฆฌํด๋ด
์๋ค:
| ๊ตฌ๋ถ | Entity | Types | Model |
|---|
| ํ์ผ | {entity}.entity.json | {entity}.types.ts | {entity}.model.ts |
| ์ญํ | ๋ฐ์ดํฐ ๊ตฌ์กฐ ์ ์ | ํ์
์ ์ | ๋น์ฆ๋์ค ๋ก์ง |
| ์์ฑ | ์๋ (Sonamu UI) | ์๋ ์์ฑ | ์๋ (Scaffold) |
| ๋ด์ฉ | ํ
์ด๋ธ, ์ปฌ๋ผ, ๊ด๊ณ | TypeScript ํ์
, Zod | ๋ฉ์๋, API, ์ฟผ๋ฆฌ |
| ๋ณ๊ฒฝ ๋น๋ | ๋ฎ์ | ์๋ ์ฌ์์ฑ | ๋์ |
์์:
{
"id": "User",
"table": "users",
"props": [
{ "name": "id", "type": "integer" },
{ "name": "email", "type": "string" }
]
}
Model์ด ์ ๊ณตํ๋ ๊ธฐ๋ฅ
1. ํ์
์์ ํ ๋ฐ์ดํฐ ์ก์ธ์ค
// โ
ํ์
์์ : subset ๊ธฐ๋ฐ ํ์
์ถ๋ก
const user = await UserModel.findById("A", 1);
// user์ ํ์
: UserSubsetMapping["A"]
// โ
์ปดํ์ผ ์๋ฌ: ์๋ชป๋ subset
const user = await UserModel.findById("INVALID", 1);
// ^^^^^^^^^
// Error: Argument of type '"INVALID"' is not assignable
2. Subset ๊ธฐ๋ฐ ์ฟผ๋ฆฌ
async findMany<T extends UserSubsetKey>(
subset: T,
params: UserListParams
): Promise<ListResult<UserSubsetMapping[T]>> {
const { qb } = this.getSubsetQueries(subset);
// subset์ ๋ง๋ ํ๋๋ง ์๋ SELECT
qb.where("id", params.id);
return this.executeSubsetQuery({ subset, qb, params });
}
Subset์ API ์๋ต์ ํ์ํ ํ๋๋ง ์ ํํ์ฌ ๋คํธ์ํฌ ํธ๋ํฝ์ ์ค์ด๊ณ ์ฑ๋ฅ์ ํฅ์์ํต๋๋ค.
3. ์๋ API ์์ฑ
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
async findById(id: number): Promise<User> {
// ๋ฉ์๋ ๊ตฌํ
}
์๋ ์์ฑ๋๋ ๊ฒ๋ค:
- HTTP ์๋ํฌ์ธํธ:
GET /user/findById?id=1
- TypeScript ํด๋ผ์ด์ธํธ ์ฝ๋
- TanStack Query hooks
- API ๋ฌธ์
4. ํธ๋์ญ์
๊ด๋ฆฌ
@transactional()
async updateUserAndProfile(
userId: number,
userData: UserData,
profileData: ProfileData
): Promise<void> {
// ๋ชจ๋ ์์
์ด ํ๋์ ํธ๋์ญ์
์ผ๋ก ์คํ๋จ
await this.save([userData]);
await ProfileModel.save([profileData]);
// ์๋ ์ปค๋ฐ ๋๋ ๋กค๋ฐฑ
}
5. ๋ฐ์ดํฐ ๊ฒ์ฆ๊ณผ ๋ณํ
async save(params: UserSaveParams[]): Promise<number[]> {
// ๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ
for (const param of params) {
if (param.age && param.age < 18) {
throw new BadRequestException("18์ธ ๋ฏธ๋ง์ ๊ฐ์
ํ ์ ์์ต๋๋ค");
}
}
// ๋ฐ์ดํฐ ๋ณํ
const hashedParams = params.map(p => ({
...p,
password: bcrypt.hashSync(p.password, 10)
}));
// ์ ์ฅ
return this.getPuri("w").ubUpsert("users", hashedParams);
}
Model ์์ฑ ํจํด
CRUD ๋ฉ์๋
์ผ๋ฐ์ ์ธ Model์ ๋ค์ CRUD ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค:
class UserModelClass extends BaseModelClass {
// Create
@api({ httpMethod: "POST" })
async save(params: UserSaveParams[]): Promise<number[]> {
// ...
}
// Read
@api({ httpMethod: "GET" })
async findById(id: number): Promise<User> {
// ...
}
@api({ httpMethod: "GET" })
async findMany(params: UserListParams): Promise<ListResult<User>> {
// ...
}
// Update
@api({ httpMethod: "PUT" })
async update(id: number, params: UserUpdateParams): Promise<User> {
// ...
}
// Delete
@api({ httpMethod: "DELETE" })
async del(ids: number[]): Promise<number> {
// ...
}
}
๋๋ฉ์ธ๋ณ ๋ฉ์๋
๋น์ฆ๋์ค ๋๋ฉ์ธ์ ํนํ๋ ๋ฉ์๋๋ ์ถ๊ฐํ ์ ์์ต๋๋ค:
class UserModelClass extends BaseModelClass {
@api({ httpMethod: "POST" })
async login(params: LoginParams): Promise<{ user: User }> {
// ๋ก๊ทธ์ธ ๋ก์ง
}
@api({ httpMethod: "POST" })
async logout(): Promise<{ message: string }> {
// ๋ก๊ทธ์์ ๋ก์ง
}
@api({ httpMethod: "GET" })
async me(): Promise<User | null> {
// ํ์ฌ ์ฌ์ฉ์ ์ ๋ณด
}
@api({ httpMethod: "POST" })
async changePassword(params: ChangePasswordParams): Promise<void> {
// ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ
}
}
BaseModel ๋ฉ์๋
BaseModelClass๋ ๋ค์ ์ ํธ๋ฆฌํฐ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค:
| ๋ฉ์๋ | ์ค๋ช
| ๋ฐํ ํ์
|
|---|
getDB(preset) | ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ๋ | Knex |
getPuri(preset) | Puri ์ฟผ๋ฆฌ ๋น๋ ํ๋ | PuriWrapper |
getSubsetQueries(subset) | Subset ์ฟผ๋ฆฌ ๋น๋ ํ๋ | { qb, onSubset } |
executeSubsetQuery(params) | Subset ์ฟผ๋ฆฌ ์คํ | ListResult |
createEnhancers(enhancers) | Enhancer ์์ฑ ํฌํผ | EnhancerMap |
Model์ ์ฅ์
Entity ์ ์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก TypeScript ํ์
์ด ์๋ ์์ฑ๋์ด, ์ปดํ์ผ ํ์์ ์ค๋ฅ๋ฅผ ์ก์ ์ ์์ต๋๋ค.// โ
ํ์
์ฒดํฌ
const user: UserSubsetMapping["A"] = await UserModel.findById("A", 1);
// โ ์ปดํ์ผ ์๋ฌ
user.nonExistentField;
๊ณตํต ๋ก์ง์ Model ๋ฉ์๋๋ก ์์ฑํ์ฌ ์ฌ๋ฌ API์์ ์ฌ์ฌ์ฉํ ์ ์์ต๋๋ค.// Model ๋ฉ์๋
async findActiveUsers(): Promise<User[]> {
return this.puri().where("is_active", true).many();
}
// ์ฌ๋ฌ API์์ ์ฌ์ฌ์ฉ
@api()
async api1() {
return this.findActiveUsers();
}
@api()
async api2() {
const users = await this.findActiveUsers();
// ์ถ๊ฐ ๋ก์ง...
}
Model์ ๋
๋ฆฝ์ ์ผ๋ก ํ
์คํธํ ์ ์์ต๋๋ค.describe("UserModel", () => {
it("should find user by id", async () => {
const user = await UserModel.findById("A", 1);
expect(user.id).toBe(1);
});
});
๋น์ฆ๋์ค ๋ก์ง(Model)๊ณผ HTTP ์ฒ๋ฆฌ(Controller/API)๋ฅผ ๋ช
ํํ ๋ถ๋ฆฌํฉ๋๋ค.
- Model: ๋ฐ์ดํฐ ์ฒ๋ฆฌ, ๋น์ฆ๋์ค ๊ท์น
- API: HTTP ์์ฒญ/์๋ต, ์ธ์ฆ, ๊ถํ
๋ค์ ๋จ๊ณ
Model์ ๊ธฐ๋ณธ ๊ฐ๋
์ ์ดํดํ๋ค๋ฉด, ๋ค์ ์ฃผ์ ๋ฅผ ํ์ตํ์ธ์: