๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Scaffolding ํƒญ์—์„œ๋Š” Entity๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Model๊ณผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. CLI์˜ pnpm scaffold ๋ช…๋ น์–ด๋ฅผ ์‹œ๊ฐ์  ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Scaffolding ํƒญ ๊ตฌ์กฐ

Scaffolding ํ™”๋ฉด

Scaffolding ํƒญ์€ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ์˜์—ญ์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค:
  • ์™ผ์ชฝ Sidebar: Entity ๋ชฉ๋ก๊ณผ ์„ ํƒ ์ฒดํฌ๋ฐ•์Šค
  • ์˜ค๋ฅธ์ชฝ Content: ์ฝ”๋“œ ์ƒ์„ฑ ์˜ต์…˜ (Model Class, Model Test, View)

์ƒ์„ฑ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ

์ฝ”๋“œ ํƒ€์ž…์„ค๋ช…์ƒํƒœCLI ๋ช…๋ น์–ด
Model ClassEntity๋ฅผ ์œ„ํ•œ Model ํด๋ž˜์Šคโœ… ์‚ฌ์šฉ ๊ฐ€๋Šฅpnpm scaffold model
Model TestModel ํ…Œ์ŠคํŠธ ํŒŒ์ผโœ… ์‚ฌ์šฉ ๊ฐ€๋Šฅpnpm scaffold model_test
View List๋ชฉ๋ก View ์ปดํฌ๋„ŒํŠธ๐Ÿšง ๊ฐœ๋ฐœ ์ค‘-
View Formํผ View ์ปดํฌ๋„ŒํŠธ๐Ÿšง ๊ฐœ๋ฐœ ์ค‘-
View ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ๊ธฐ๋Šฅ์€ ํ˜„์žฌ ๊ฐœ๋ฐœ ์ค‘์ž…๋‹ˆ๋‹ค. Sonamu UI์˜ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด React ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Model ํด๋ž˜์Šค ์ƒ์„ฑ

1. Entity ์„ ํƒ

์™ผ์ชฝ Entity ๋ชฉ๋ก์—์„œ Model์„ ์ƒ์„ฑํ•  Entity๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ผ ์„ ํƒ:
โ˜‘ User
โ–ก Post
โ–ก Comment
๋‹ค์ค‘ ์„ ํƒ:
โ˜‘ User
โ˜‘ Post
โ˜‘ Comment

2. ์ƒ์„ฑ ์˜ต์…˜ ์„ ํƒ

โ˜‘ Model Class ์ฒดํฌ๋ฐ•์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

3. Generate ์‹คํ–‰

[Generate] ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋‹ค์Œ ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค:
๐Ÿ“src/models/
๐Ÿ“„TSUser.model.ts - User Model ํด๋ž˜์Šค

์ƒ์„ฑ๋œ ์ฝ”๋“œ

src/models/User.model.ts
import { BaseModelClass } from "sonamu";
import type { InferSelectModel } from "sonamu";
import { UserEntity } from "../entities/User.entity";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";

export type User = InferSelectModel<typeof UserEntity>;

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }
  
  // TODO: Model ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
}

export const UserModel = new UserModelClass();
์ž๋™ ์ƒ์„ฑ๋˜๋Š” ๊ธฐ๋Šฅ:
  • โœ… TypeScript ํƒ€์ž… ์ •์˜
  • โœ… Entity ์—ฐ๊ฒฐ
  • โœ… BaseModel ์ƒ์†
  • โœ… ๊ธฐ๋ณธ CRUD ๋ฉ”์„œ๋“œ

Model ํ…Œ์ŠคํŠธ ์ƒ์„ฑ

1. Entity ์„ ํƒ

Model๊ณผ ๋™์ผํ•˜๊ฒŒ Entity๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

2. ์ƒ์„ฑ ์˜ต์…˜ ์„ ํƒ

โ˜‘ Model Test ์ฒดํฌ๋ฐ•์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
Model๊ณผ Test ๋™์‹œ ์ƒ์„ฑ: Model Class์™€ Model Test๋ฅผ ๋™์‹œ์— ์„ ํƒํ•˜์—ฌ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. Generate ์‹คํ–‰

[Generate] ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋‹ค์Œ ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค:
๐Ÿ“src/models/
๐Ÿ“„TSUser.model.test.ts - User Model ํ…Œ์ŠคํŠธ

์ƒ์„ฑ๋œ ์ฝ”๋“œ

src/models/User.model.test.ts
import { beforeAll, describe, test } from "bun:test";
import { expect } from "@jest/globals";
import { FixtureManager } from "sonamu/test";
import { UserModel } from "./User.model";

describe("User Model", () => {
  beforeAll(async () => {
    await FixtureManager.sync();
  });

  test("findById", async () => {
    const user = await UserModel.findById(1);
    expect(user).toBeDefined();
    expect(user?.id).toBe(1);
  });

  test("findMany", async () => {
    const users = await UserModel.findMany({
      num: 10,
      page: 1,
    });
    expect(users.rows.length).toBeGreaterThan(0);
  });

  // TODO: ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
});
์ž๋™ ์ƒ์„ฑ๋˜๋Š” ํ…Œ์ŠคํŠธ:
  • โœ… findById: ID๋กœ ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ์กฐํšŒ
  • โœ… findMany: ๋ชฉ๋ก ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜)

์ผ๊ด„ ์ƒ์„ฑ

๋ชจ๋“  Entity์— ๋Œ€ํ•ด ์ƒ์„ฑ

  1. [Select All] ๋ฒ„ํŠผ ํด๋ฆญ
  2. ์ƒ์„ฑ ์˜ต์…˜ ์„ ํƒ
  3. [Generate] ํด๋ฆญ
๊ฒฐ๊ณผ:
โœ“ User.model.ts created
โœ“ User.model.test.ts created
โœ“ Post.model.ts created
โœ“ Post.model.test.ts created
โœ“ Comment.model.ts created
โœ“ Comment.model.test.ts created

6 files generated successfully!

ํŠน์ • Entity๋งŒ ์„ ํƒ

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ์„ค์ • ์‹œ ๋ชจ๋“  Entity์— ๋Œ€ํ•ด:
โ˜‘ User
โ˜‘ Post  
โ˜‘ Comment
โ˜‘ Category
โ˜‘ Tag

[Generate] โ†’ 10๊ฐœ ํŒŒ์ผ ์ƒ์„ฑ
์ƒˆ Entity ์ถ”๊ฐ€ ์‹œ ํ•ด๋‹น Entity๋งŒ:
โ–ก User (์ด๋ฏธ ์ƒ์„ฑ๋จ)
โ–ก Post (์ด๋ฏธ ์ƒ์„ฑ๋จ)
โ˜‘ Product (์‹ ๊ทœ)

[Generate] โ†’ 2๊ฐœ ํŒŒ์ผ ์ƒ์„ฑ

์ƒ์„ฑ ๊ฒฐ๊ณผ ํ™•์ธ

์„ฑ๊ณต ๋ฉ”์‹œ์ง€

โœ… Code Generation Complete

Generated files:
  โœ“ src/models/User.model.ts
  โœ“ src/models/User.model.test.ts
  โœ“ src/models/Post.model.ts
  โœ“ src/models/Post.model.test.ts

Total: 4 files

ํŒŒ์ผ ์ถฉ๋Œ

์ด๋ฏธ ์กด์žฌํ•˜๋Š” ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ ค๊ณ  ํ•˜๋ฉด ํ™•์ธ ๋ชจ๋‹ฌ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค:
โš ๏ธ Files Already Exist

The following files will be overwritten:
  โ€ข src/models/User.model.ts
  โ€ข src/models/User.model.test.ts

[Cancel] [Backup & Overwrite] [Overwrite]
์„ ํƒ ์˜ต์…˜:
  • Cancel: ์ƒ์„ฑ ์ทจ์†Œ
  • Backup & Overwrite: ๊ธฐ์กด ํŒŒ์ผ์„ .bak๋กœ ๋ฐฑ์—… ํ›„ ๋ฎ์–ด์“ฐ๊ธฐ
  • Overwrite: ๋ฐ”๋กœ ๋ฎ์–ด์“ฐ๊ธฐ (์ฃผ์˜!)
์ฝ”๋“œ ์†์‹ค ์ฃผ์˜: ๊ธฐ์กด ํŒŒ์ผ์„ ๋ฎ์–ด์“ฐ๋ฉด ์ž‘์„ฑํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์‚ญ์ œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•œ Model์€ ๋ฐฑ์—…ํ•˜๊ฑฐ๋‚˜ ๋ฎ์–ด์“ฐ๊ธฐ๋ฅผ ํ”ผํ•˜์„ธ์š”.

View ์ƒ์„ฑ (๊ฐœ๋ฐœ ์ค‘)

View ์ปดํฌ๋„ŒํŠธ ์ž๋™ ์ƒ์„ฑ ๊ธฐ๋Šฅ์€ ํ˜„์žฌ ๊ฐœ๋ฐœ ์ค‘์ž…๋‹ˆ๋‹ค.

๋Œ€์•ˆ: Sonamu UI์˜ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ ์‚ฌ์šฉ

ํ˜„์žฌ๋Š” ๋‹ค์Œ ๋ฐฉ๋ฒ•์œผ๋กœ View๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
  1. Entity ๊ธฐ๋ฐ˜ ์ž๋™ ์ƒ์„ฑ: Entity๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๊ธฐ๋ณธ View ํ…œํ”Œ๋ฆฟ์ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค
  2. ์ˆ˜๋™ ์ž‘์„ฑ: React ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค
  3. ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: @sonamu-kit/react-components ํ™œ์šฉ
View List์™€ View Form ์ƒ์„ฑ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜๋ฉด ์ด ๋ฌธ์„œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค.

์ƒ์„ฑ ํ›„ ์›Œํฌํ”Œ๋กœ์šฐ

1. Model ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

์ƒ์„ฑ๋œ Model์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:
src/models/User.model.ts
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }
  
  // ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ
  async findByEmail(email: string): Promise<User | null> {
    return this.getPuri("r")
      .where("email", email)
      .first();
  }
  
  // ํ™œ์„ฑ ์‚ฌ์šฉ์ž๋งŒ ์กฐํšŒ
  async findActiveUsers() {
    return this.getPuri("r")
      .where("is_active", true)
      .where("deleted_at", null);
  }
}

2. ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

์ƒ์„ฑ๋œ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์— ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค:
src/models/User.model.test.ts
describe("User Model - Extended", () => {
  test("findByEmail", async () => {
    const user = await UserModel.findByEmail("[email protected]");
    expect(user).toBeDefined();
    expect(user?.email).toBe("[email protected]");
  });

  test("findActiveUsers", async () => {
    const users = await UserModel.findActiveUsers();
    users.forEach(user => {
      expect(user.is_active).toBe(true);
    });
  });
});

3. API ์ถ”๊ฐ€

Model์— @api ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async findActiveUsers() {
    const { qb } = this.getSubsetQueries("A");
    qb.where("is_active", true);
    
    return this.executeSubsetQuery({
      subset: "A",
      qb,
      params: { num: 20, page: 1 },
    });
  }
}

์‹ค์ „ ํŒ

1. ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ์„ค์ •

1. ๋ชจ๋“  Entity ์ •์˜
   โ†“
2. Scaffolding ํƒญ์—์„œ ์ผ๊ด„ ์ƒ์„ฑ
   - [Select All]
   - Model Class โ˜‘
   - Model Test โ˜‘
   - [Generate]
   โ†“
3. ๊ฐ Model์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ถ”๊ฐ€

2. ์ƒˆ Entity ์ถ”๊ฐ€ ์‹œ

1. Entity ํƒญ์—์„œ Entity ์ƒ์„ฑ
   โ†“
2. Migration ํƒญ์—์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰
   โ†“
3. Scaffolding ํƒญ์—์„œ ํ•ด๋‹น Entity๋งŒ ์ƒ์„ฑ
   โ†“
4. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฐ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

3. ์žฌ์ƒ์„ฑ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

Entity ๊ตฌ์กฐ๊ฐ€ ํฌ๊ฒŒ ๋ณ€๊ฒฝ๋˜์–ด Model์„ ์žฌ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ:
# ๊ธฐ์กด ํŒŒ์ผ ๋ฐฑ์—…
cp src/models/User.model.ts src/models/User.model.ts.bak

# UI์—์„œ ์žฌ์ƒ์„ฑ
[Backup & Overwrite]

# ๋ฐฑ์—… ํŒŒ์ผ์—์„œ ์ปค์Šคํ…€ ๋กœ์ง ๋ณต์›

๋‹ค์Œ ๋‹จ๊ณ„