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

Entity ๊ธฐ๋ณธ

Entity ID๋Š” CamelCase๋กœ ๊ฐ•์ œ๋˜๋ฉฐ, ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฌ๋Ÿฌ ์ด๋ฆ„์ด ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.์ž…๋ ฅ: Product์ž๋™ ์ƒ์„ฑ๋˜๋Š” ์ด๋ฆ„๋“ค:
  1. Entity ID (id)
    • Product (์ž…๋ ฅ๊ฐ’ ๊ทธ๋Œ€๋กœ)
    • ๊ฒ€์ฆ: /^[A-Z][a-zA-Z0-9]*$/ (CamelCase ํ•„์ˆ˜)
  2. ํ…Œ์ด๋ธ”๋ช… (table)
    • products (์ž๋™ ๋ณต์ˆ˜ํ˜• ๋ณ€ํ™˜)
    • ์ปค์Šคํ…€ ๊ฐ€๋Šฅ (Sonamu UI์—์„œ ์ง์ ‘ ์ž…๋ ฅ)
  3. ํŒŒ์ผ/๋””๋ ‰ํ„ฐ๋ฆฌ๋ช… (names.fs)
    • product (์†Œ๋ฌธ์ž + ๋Œ€์‹œ)
    • ์‚ฌ์šฉ: ๋””๋ ‰ํ„ฐ๋ฆฌ๋ช…, ํŒŒ์ผ๋ช…
  4. ๋ชจ๋“ˆ๋ช… (names.module)
    • Product (์ž…๋ ฅ๊ฐ’ ๊ทธ๋Œ€๋กœ)
    • ์‚ฌ์šฉ: ํด๋ž˜์Šค๋ช…, ํƒ€์ž…๋ช…
์ƒ์„ฑ๋˜๋Š” ํŒŒ์ผ๋“ค:
api/src/application/product/product.entity.json
api/src/application/product/product.types.ts
api/src/application/product/product.model.ts (Scaffolding ์‹œ)
์ƒ์„ฑ๋˜๋Š” ํƒ€์ž…/ํด๋ž˜์Šค๋ช…:
ProductBaseSchema
ProductBaseListParams
ProductSubsetMapping
ProductModel (Model ํด๋ž˜์Šค)
ProductService (ํ”„๋ก ํŠธ์—”๋“œ)
๋ณ€ํ™˜ ๊ทœ์น™:
  • ID โ†’ table: inflection.underscore(inflection.pluralize(id))
    • Product โ†’ products
    • OrderItem โ†’ order_items
  • ID โ†’ fs: inflection.dasherize(inflection.underscore(id)).toLowerCase()
    • Product โ†’ product
    • OrderItem โ†’ order-item
์ฃผ์˜:
  • Entity ID๋Š” ๋ฐ˜๋“œ์‹œ ๋Œ€๋ฌธ์ž๋กœ ์‹œ์ž‘ (CamelCase)
  • ํ…Œ์ด๋ธ”๋ช…์€ ์ž๋™ ์ƒ์„ฑ๋˜์ง€๋งŒ ์ปค์Šคํ…€ ๊ฐ€๋Šฅ
Entity ์ˆ˜์ • ์‹œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ์ž๋™ ์ƒ์„ฑ๋˜์ง€ ์•Š๊ณ , Sonamu UI์˜ DB Migration ํƒญ์—์„œ ์ˆ˜๋™์œผ๋กœ Generateํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ ๋Œ€์ƒ (Prepared Migration Codes์— ํ‘œ์‹œ):Props ๋ณ€๊ฒฝ:
  • ํ”„๋กญ ์ถ”๊ฐ€/์‚ญ์ œ
  • ํ”„๋กญ ํƒ€์ž… ๋ณ€๊ฒฝ (string โ†’ numeric ๋“ฑ)
  • StringProp ๊ธธ์ด ๋ณ€๊ฒฝ (length)
  • nullable ๋ณ€๊ฒฝ
  • dbDefault ๋ณ€๊ฒฝ
  • NumberProp์ด numberType: "numeric"์ธ ๊ฒฝ์šฐ precision, scale ๋ณ€๊ฒฝ
๊ด€๊ณ„(relation) ๋ณ€๊ฒฝ:
  • BelongsToOne ์ถ”๊ฐ€/์‚ญ์ œ (FK ์ปฌ๋Ÿผ ์ƒ์„ฑ/์‚ญ์ œ)
  • OneToOne ์ถ”๊ฐ€/์‚ญ์ œ (hasJoinColumn: true์ธ ๊ฒฝ์šฐ)
  • ManyToMany ์ถ”๊ฐ€/์‚ญ์ œ (์กฐ์ธ ํ…Œ์ด๋ธ” ์ƒ์„ฑ/์‚ญ์ œ)
  • onUpdate, onDelete ๋ณ€๊ฒฝ
์ธ๋ฑ์Šค(indexes) ๋ณ€๊ฒฝ:
  • ์ธ๋ฑ์Šค ์ถ”๊ฐ€/์‚ญ์ œ
  • unique, index, fulltext ํƒ€์ž… ๋ณ€๊ฒฝ
  • ์ธ๋ฑ์Šค ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ
ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ:
  • ํ…Œ์ด๋ธ”๋ช… ๋ณ€๊ฒฝ
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š” ๋ณ€๊ฒฝ:
  • Subset ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ
  • Enum labels ๋ณ€๊ฒฝ
  • Virtual ํ”„๋กญ ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ
  • HasMany ๊ด€๊ณ„ ์ถ”๊ฐ€/์ˆ˜์ • (FK๊ฐ€ ์ƒ๋Œ€ํŽธ์— ์žˆ์Œ)
  • Title, Description ๋ณ€๊ฒฝ
์ฃผ์˜:
  • Entity ์ˆ˜์ • ํ›„ ์ž๋™์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ
  • DB Migration ํƒญ โ†’ Prepared Migration Codes ํ™•์ธ
  • Generate ๋ฒ„ํŠผ ํด๋ฆญํ•˜์—ฌ ์ˆ˜๋™ ์ƒ์„ฑ ํ•„์š”
1. Entity JSON ํŒŒ์ผ ์ƒ์„ฑ
  • {entity}.entity.json
  • ์œ„์น˜: api/src/application/{entity}/{entity}.entity.json
2. ํƒ€์ž… ํŒŒ์ผ ์ž๋™ ์ƒ์„ฑ
  • {entity}.types.ts
  • ์œ„์น˜: api/src/application/{entity}/{entity}.types.ts
3. ์ƒ์„ฑ ํŒŒ์ผ ๊ฐฑ์‹ 
  • sonamu.generated.sso.ts
  • sonamu.generated.ts
  • ์œ„์น˜: api/src/application/
4. ํ”„๋ก ํŠธ์—”๋“œ ํŒŒ์ผ ๋ณต์‚ฌ
  • user.types.ts โ†’ web/src/services/user/user.types.ts
  • sonamu.generated.ts โ†’ web/src/services/sonamu.generated.ts
5. sonamu.lock ๊ฐฑ์‹ 
  • ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ํŒŒ์ผ๋“ค์˜ ์ฒดํฌ์„ฌ ์ €์žฅ
์ฃผ์˜:
  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์€ ์ž๋™ ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ
  • DB Migration ํƒญ์—์„œ ๋ณ„๋„๋กœ Generate ํด๋ฆญ ํ•„์š”
  • model, model_test, view๋Š” Scaffolding ํƒญ์—์„œ ๋ณ„๋„ ์ƒ์„ฑ

Entity ํ”„๋กœํผํ‹ฐ

enums ๊ฐ์ฒด์— enum ID๋ฅผ ํ‚ค๋กœ, ๊ฐ’-๋ผ๋ฒจ ์Œ์„ ๊ฐ์ฒด๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค:
{
  "enums": {
    "UserRole": { 
      "normal": "๋…ธ๋ฉ€", 
      "admin": "๊ด€๋ฆฌ์ž" 
    },
    "UserOrderBy": {
      "id-desc": "ID์ตœ์‹ ์ˆœ"
    }
  }
}
  • ํ‚ค (normal, admin): DB์— ์ €์žฅ๋˜๋Š” ์‹ค์ œ ๊ฐ’
  • ๊ฐ’ (๋…ธ๋ฉ€, ๊ด€๋ฆฌ์ž): UI์— ํ‘œ์‹œ๋˜๋Š” ๋ผ๋ฒจ
  • enum ID (UserRole): TypeScript ํƒ€์ž…๋ช…์œผ๋กœ ์‚ฌ์šฉ๋จ
Best Practice:
  • DB ๊ฐ’์€ ์˜๋ฌธ์œผ๋กœ (๊ตญ์ œํ™”์™€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์šฉ์ด์„ฑ)
  • UI ๋ผ๋ฒจ์€ ์‚ฌ์šฉ์ž ์–ธ์–ด๋กœ ์ž‘์„ฑ
DB์— ์ปฌ๋Ÿผ์€ ์—†์ง€๋งŒ ์กฐํšŒ ๊ฒฐ๊ณผ์— ํฌํ•จ๋˜๋Š” ๊ณ„์‚ฐ ํ•„๋“œ์ž…๋‹ˆ๋‹ค.์ƒ์„ฑ ๋ฐฉ๋ฒ• (Sonamu UI):
  1. Entity ์ƒ์„ธ โ†’ Add a prop ํด๋ฆญ
  2. Type: virtual ์„ ํƒ
  3. Name: ํ•„๋“œ๋ช… ์ž…๋ ฅ (์˜ˆ: company_stats)
  4. Nullable: true ์ฒดํฌ
  5. CustomType ID: ํƒ€์ž… ์„ ํƒ (์˜ˆ: CompanyStatsType)
ํƒ€์ž… ์ •์˜ ({entity}.types.ts):
export const CompanyStatsType = z.object({
  totalDepartments: z.number(),
  totalEmployees: z.number(),
  avgSalary: z.number(),
  departments: z.array(z.object({
    id: z.number(),
    name: z.string(),
    employeeCount: z.number(),
  })),
});

export type CompanyStatsType = z.infer<typeof CompanyStatsType>;
๊ตฌํ˜„ (Enhancer ๋ฐฉ์‹):
// company.model.ts
const enhancers = this.createEnhancers({
  WithStats: async (row) => {
    const departments = await DepartmentModel.findMany('P', {
      company_id: row.id,
    });
    
    return {
      ...row,
      company_stats: {
        totalDepartments: departments.rows.length,
        totalEmployees: departments.rows.reduce((sum, d) => sum + d.employee_count, 0),
        avgSalary: calculateAvgSalary(departments.rows),
        departments: departments.rows.map(d => ({
          id: d.id,
          name: d.name,
          employeeCount: d.employee_count,
        })),
      },
    };
  },
});
์ฃผ์š” ์‚ฌ์šฉ ์‚ฌ๋ก€:
  • ๊ด€๊ณ„ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ (์ง์› ์ˆ˜, ๋Œ“๊ธ€ ์ˆ˜)
  • ๋ณต์žกํ•œ ํ†ต๊ณ„ ๊ฐ์ฒด (๋ถ€์„œ๋ณ„ ํ†ต๊ณ„, ํšŒ์‚ฌ ๋Œ€์‹œ๋ณด๋“œ)
  • ์‚ฌ์šฉ์ž๋ณ„ ์ƒํƒœ (์ข‹์•„์š” ์—ฌ๋ถ€, ๊ถŒํ•œ ์ •๋ณด)
  • ์ค‘์ฒฉ๋œ JSON ๊ตฌ์กฐ (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ, ์„ค์ • ๊ฐ์ฒด)
์ฃผ์˜:
  • SaveParams์—์„œ .omit({ virtual_field: true }) ํ•„์ˆ˜
  • Enhancer์—์„œ ๊ณ„์‚ฐ ๋กœ์ง ๊ตฌํ˜„
  • ๋ณต์žกํ•œ ๊ณ„์‚ฐ์€ ์„ฑ๋Šฅ ๊ณ ๋ ค ํ•„์š”

Entity ๊ด€๊ณ„

relation ํƒ€์ž…์˜ ํ•„๋“œ๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค:OneToOne: 1:1 ๊ด€๊ณ„
  • hasJoinColumn์œผ๋กœ FK ์†Œ์œ  ์ง€์ •
BelongsToOne: N:1 ๊ด€๊ณ„ (Many-to-One)
  • FK ํ‚ค๋ฅผ ๊ฐ€์ง„ Entity์—์„œ ์ •์˜
HasMany: 1:N ๊ด€๊ณ„ (One-to-Many)
  • virtual ๊ด€๊ณ„ (FK๊ฐ€ ์ƒ๋Œ€ํŽธ์— ์žˆ์Œ)
ManyToMany: N:M ๊ด€๊ณ„
  • joinTable ํ•„์š”
๊ฐ ๊ด€๊ณ„๋Š” FK ํ‚ค๊ฐ€ ์žˆ๋Š” Entity์—์„œ onUpdate, onDelete ์˜ต์…˜์œผ๋กœ ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
N:1 ๊ด€๊ณ„(Many-to-One)๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.์„ค์ • ๋ฐฉ๋ฒ• (Sonamu UI):
  1. Entity ์ƒ์„ธ โ†’ Add a prop ํด๋ฆญ
  2. Type: relation ์„ ํƒ
  3. Relation Type: BelongsToOne ์„ ํƒ
  4. Name: ๊ด€๊ณ„ ํ•„๋“œ๋ช… (์˜ˆ: department)
  5. With: ์—ฐ๊ฒฐํ•  Entity ID (์˜ˆ: Department)
  6. Nullable: ์„ ํƒ์  ๊ด€๊ณ„๋ฉด true
  7. ON UPDATE/ON DELETE: CASCADE, SET NULL, RESTRICT, NO ACTION ๋“ฑ ์„ ํƒ
์ƒ์„ฑ ๊ฒฐ๊ณผ:
  • ์™ธ๋ž˜ํ‚ค ์ปฌ๋Ÿผ ์ž๋™ ์ƒ์„ฑ (์˜ˆ: department_id)
  • ํƒ€์ž…์— department?: Department ํฌํ•จ
{
  "type": "relation",
  "name": "department",
  "with": "Department",
  "nullable": true,
  "relationType": "BelongsToOne",
  "onUpdate": "CASCADE",
  "onDelete": "SET NULL"
}

Model ํด๋ž˜์Šค

Entity ์—†์ด ๋…๋ฆฝ์ ์ธ API๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์€ ๋‘ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.๋ฐฉ๋ฒ• 1: ๊ธฐ์กด Model์— @api ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ถ”๊ฐ€
// api/src/application/user/user.model.ts
class UserModelClass extends BaseModelClass {
  // ๊ธฐ์กด CRUD ๋ฉ”์„œ๋“œ๋“ค...

  @api({ httpMethod: "GET" })
  async getMyIP(): Promise<{ ip: string }> {
    const context = Sonamu.getContext();
    return { ip: context.ip };
  }

  @api({ httpMethod: "POST" })
  async sendEmail(params: { to: string; subject: string }): Promise<boolean> {
    // ์™ธ๋ถ€ API ํ˜ธ์ถœ (DB ์กฐํšŒ ์—†์Œ)
    return true;
  }
}
๋ฐฉ๋ฒ• 2: Frame ์‚ฌ์šฉFrame์€ Entity ์—†์ด ๋…๋ฆฝ์ ์ธ API๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
// api/src/application/util/util.frame.ts
import { BaseFrameClass, api } from "sonamu";

class UtilFrameClass extends BaseFrameClass {
  @api({ httpMethod: "GET" })
  async healthCheck(): Promise<{ status: string; timestamp: Date }> {
    return { status: "ok", timestamp: new Date() };
  }
}

export const UtilFrame = new UtilFrameClass();
Frame ํŠน์ง•:
  • BaseFrameClass ์ƒ์†
  • getDB(), getUpsertBuilder() ๋ฉ”์„œ๋“œ ์ œ๊ณต
  • Entity, Subset, Model ๋ฉ”์„œ๋“œ ์—†์Œ
  • ํŒŒ์ผ๋ช…: {name}.frame.ts
์„ ํƒ ๊ธฐ์ค€:
  • ๊ธฐ์กด Entity์™€ ๊ด€๋ จ: ํ•ด๋‹น Model์— ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
  • ์™„์ „ํžˆ ๋…๋ฆฝ์ : Frame ํด๋ž˜์Šค ์ƒ์„ฑ
Model ํด๋ž˜์Šค๊ฐ€ ์ปค์ง€๋ฉด Frame ํด๋ž˜์Šค๋กœ ๋…๋ฆฝ ๋กœ์ง ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.๋ฌธ์ œ: user.model.ts๊ฐ€ ๋น„๋Œ€ํ•ด์ง
class UserModelClass extends BaseModelClass {
  async findById() { ... }
  async save() { ... }
  // Entity์™€ ๋ฌด๊ด€ํ•œ ๋กœ์ง๋“ค
  async sendEmail() { ... }
  async uploadAvatar() { ... }
  async generateReport() { ... }
}
๊ฐœ์„ : user-util.frame.ts๋กœ ๋ถ„๋ฆฌ
// user-util.frame.ts
class UserUtilFrameClass extends BaseFrameClass {
  @api({ httpMethod: "POST" })
  async sendEmail(params: EmailParams): Promise<boolean> {
    // ์ด๋ฉ”์ผ ์ „์†ก ๋กœ์ง
    return true;
  }

  @api({ httpMethod: "POST" })
  async uploadAvatar(file: File): Promise<string> {
    // ํŒŒ์ผ ์—…๋กœ๋“œ ๋กœ์ง
    return "url";
  }
}
๋ถ„๋ฆฌ ๊ธฐ์ค€:
  • User Entity์™€ ์ง์ ‘ ์—ฐ๊ด€: user.model.ts์— ์œ ์ง€
  • ์œ ํ‹ธ๋ฆฌํ‹ฐ์„ฑ ๊ธฐ๋Šฅ: user-util.frame.ts๋กœ ๋ถ„๋ฆฌ
  • ์™„์ „ํžˆ ๋…๋ฆฝ์ : ๋ณ„๋„ Frame ํด๋ž˜์Šค ์ƒ์„ฑ
๋ชจ๋“  Entity๋ฅผ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์‹ฑ๊ธ€ํ†ค ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.์ฃผ์š” ๊ธฐ๋Šฅ:
  • Entity ์กฐํšŒ
  • ์ด๋ฆ„ ์ž๋™ ์ƒ์„ฑ (table, fs, module)
  • ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ
  • Subset ์กฐํšŒ
  • Subset ํ•„๋“œ ์ „๊ฐœ
์‚ฌ์šฉ์ฒ˜:
  • Model ํด๋ž˜์Šค
  • API ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
  • Syncer
  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ๊ธฐ
Sonamu UI:
  • Entity ํŽธ์ง‘ ์‹œ ์‹ค์‹œ๊ฐ„ ์˜ค๋ฅ˜ ํ‘œ์‹œ
EntityManager:
const errors = EntityManager.schemaValidate(entity);
Syncer:
  • pnpm dev ์‹คํ–‰ ์‹œ ์ž๋™ ๊ฒ€์ฆ ๋ฐ ์—๋Ÿฌ ์ถœ๋ ฅ
๊ฒ€์ฆ ํ•ญ๋ชฉ:
  • ํ•„์ˆ˜ ํ•„๋“œ ์กด์žฌ
  • ํƒ€์ž… ์œ ํšจ์„ฑ
  • Subset ์ฐธ์กฐ
  • Enum ์œ ํšจ์„ฑ

๊ด€๋ จ ๋ฌธ์„œ