๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Entity๋Š” Sonamu ํ”„๋กœ์ ํŠธ์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ๋กœ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๊ณผ TypeScript ํƒ€์ž…์„ ํ•จ๊ป˜ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

Entity๋ž€?

Entity๋Š” ๋‹ค์Œ์„ ํฌํ•จํ•˜๋Š” ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ •์˜์ž…๋‹ˆ๋‹ค:
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ - ํ…Œ์ด๋ธ” ๊ตฌ์กฐ, ์ปฌ๋Ÿผ, ์ธ๋ฑ์Šค
  • TypeScript ํƒ€์ž… - ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ํƒ€์ž… ์ •์˜
  • ๊ด€๊ณ„(Relations) - ๋‹ค๋ฅธ Entity์™€์˜ ์—ฐ๊ฒฐ
  • Subset - API ์‘๋‹ต์„ ์œ„ํ•œ ํ•„๋“œ ์กฐํ•ฉ
  • Enum - ์—ด๊ฑฐํ˜• ํƒ€์ž…๊ณผ ๋ ˆ์ด๋ธ”

Entity ์ •์˜ ๋ฐฉ๋ฒ•

Entity๋Š” Sonamu UI(http://localhost:1028/sonamu-ui)์—์„œ ์‹œ๊ฐ์ ์œผ๋กœ ์ •์˜ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. Sonamu UI๋Š” ์ž๋™ ์™„์„ฑ, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

entity.json ํŒŒ์ผ ๊ตฌ์กฐ

Entity๋Š” entity.json ํŒŒ์ผ๋กœ ์ €์žฅ๋˜๋ฉฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค:
{
  "id": "User",
  "table": "users", 
  "title": "์‚ฌ์šฉ์ž",
  "props": [...],
  "indexes": [...],
  "subsets": {...},
  "enums": {...}
}
ํŒŒ์ผ ์œ„์น˜: api/src/application/{entity}/{entity}.entity.json

ํ•„์ˆ˜ ํ•„๋“œ

id

Entity์˜ ๊ณ ์œ  ์‹๋ณ„์ž์ž…๋‹ˆ๋‹ค. PascalCase๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
{
  "id": "User"
}
  • Entity ํด๋ž˜์Šค๋ช…, ํƒ€์ž…๋ช…์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค
  • ์˜ˆ: UserModel, User, UserBaseSchema

table

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ์ด๋ฆ„์ž…๋‹ˆ๋‹ค. snake_case๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
{
  "table": "users"
}
ํ…Œ์ด๋ธ”๋ช…์„ ์ƒ๋žตํ•˜๋ฉด Entity ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค:
  • User โ†’ users
  • BlogPost โ†’ blog_posts

title

Entity์˜ ํ•œ๊ธ€ ๋˜๋Š” ํ‘œ์‹œ์šฉ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.
{
  "title": "์‚ฌ์šฉ์ž"
}

props

Entity์˜ ์†์„ฑ(์ปฌ๋Ÿผ) ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค. ๊ฐ ์†์„ฑ์€ ํƒ€์ž…๊ณผ ์˜ต์…˜์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
{
  "props": [
    { "name": "id", "type": "integer", "desc": "ID" },
    { "name": "email", "type": "string", "length": 255, "desc": "์ด๋ฉ”์ผ" },
    { "name": "created_at", "type": "date", "dbDefault": "CURRENT_TIMESTAMP" }
  ]
}
๊ธฐ๋ณธ ์†์„ฑ ์˜ต์…˜:
์˜ต์…˜ํƒ€์ž…์„ค๋ช…๊ธฐ๋ณธ๊ฐ’
namestring์†์„ฑ๋ช… (snake_case)ํ•„์ˆ˜
typestring๋ฐ์ดํ„ฐ ํƒ€์ž…ํ•„์ˆ˜
descstring์„ค๋ช…-
nullablebooleanNULL ํ—ˆ์šฉ ์—ฌ๋ถ€false
dbDefaultstringDB ๊ธฐ๋ณธ๊ฐ’-
๋” ์•Œ์•„๋ณด๊ธฐ
  • Field Types - ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ํƒ€์ž…
  • Relations - Entity ๊ฐ„ ๊ด€๊ณ„ ์ •์˜

์„ ํƒ ํ•„๋“œ

parentId

๋‹ค๋ฅธ Entity๋ฅผ ์ƒ์†ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
{
  "id": "Admin",
  "parentId": "User",
  "title": "๊ด€๋ฆฌ์ž"
}
parentId๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ€๋ชจ Entity์˜ ๋ชจ๋“  props๋ฅผ ์ƒ์†๋ฐ›์Šต๋‹ˆ๋‹ค. ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜์„ธ์š”.

indexes

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
{
  "indexes": [
    {
      "type": "unique",
      "name": "users_email_unique",
      "columns": [{ "name": "email" }]
    },
    {
      "type": "index", 
      "name": "users_created_at_idx",
      "columns": [{ "name": "created_at" }]
    }
  ]
}
์ธ๋ฑ์Šค ํƒ€์ž…:
  • index - ์ผ๋ฐ˜ ์ธ๋ฑ์Šค (๊ฒ€์ƒ‰ ์„ฑ๋Šฅ ํ–ฅ์ƒ)
  • unique - ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค (์ค‘๋ณต ๋ฐฉ์ง€)
  • hnsw - Vector ๊ฒ€์ƒ‰์šฉ HNSW ์ธ๋ฑ์Šค
  • ivfflat - Vector ๊ฒ€์ƒ‰์šฉ IVFFlat ์ธ๋ฑ์Šค

subsets

API ์‘๋‹ต์—์„œ ์‚ฌ์šฉํ•  ํ•„๋“œ ์กฐํ•ฉ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
{
  "subsets": {
    "A": ["id", "email", "username", "created_at"],
    "P": ["id", "email", "username", "role"],
    "SS": ["id", "email"]
  }
}
Subset ํ™œ์šฉ:
async findAll(): Promise<UserSubsetA[]> {
  return this.puri()
    .select<UserSubsetA>("A")
    .many();
}
Relation ํ•„๋“œ ํฌํ•จ:
{
  "subsets": {
    "P": [
      "id",
      "username",
      "employee.id",
      "employee.department.name"
    ]
  }
}
Subset์˜ . ํ‘œ๊ธฐ๋ฒ•์œผ๋กœ ์—ฐ๊ด€ Entity์˜ ํ•„๋“œ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Sonamu๊ฐ€ ์ž๋™์œผ๋กœ JOIN์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

enums

์—ด๊ฑฐํ˜• ํƒ€์ž…๊ณผ ๋ ˆ์ด๋ธ”์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
{
  "enums": {
    "UserRole": {
      "normal": "์ผ๋ฐ˜",
      "admin": "๊ด€๋ฆฌ์ž"
    },
    "UserOrderBy": {
      "id-desc": "ID ์ตœ์‹ ์ˆœ",
      "created_at-desc": "๋“ฑ๋ก์ผ ์ตœ์‹ ์ˆœ"
    },
    "UserSearchField": {
      "email": "์ด๋ฉ”์ผ",
      "username": "์ด๋ฆ„"
    }
  }
}
Enum ํ™œ์šฉ:
// ์ž๋™ ์ƒ์„ฑ๋œ Zod ์Šคํ‚ค๋งˆ์™€ TypeScript ํƒ€์ž…
import { UserRole } from "./user.types";

// "normal" | "admin"
type Role = z.infer<typeof UserRole>;

// API์—์„œ ์‚ฌ์šฉ
async updateRole(userId: number, role: Role) {
  // ...
}

์‹ค์ œ ์˜ˆ์ œ

๊ธฐ๋ณธ Entity ์˜ˆ์ œ

user.entity.json
{
  "id": "User",
  "table": "users",
  "title": "์‚ฌ์šฉ์ž",
  "props": [
    { "name": "id", "type": "integer", "desc": "ID" },
    { "name": "created_at", "type": "date", "desc": "๋“ฑ๋ก์ผ์‹œ", "dbDefault": "CURRENT_TIMESTAMP" },
    { "name": "email", "type": "string", "length": 255, "desc": "์ด๋ฉ”์ผ" },
    { "name": "username", "type": "string", "length": 255, "desc": "์ด๋ฆ„" },
    { "name": "password", "type": "string", "length": 255, "desc": "๋น„๋ฐ€๋ฒˆํ˜ธ" },
    { "name": "birth_date", "type": "date", "nullable": true, "desc": "์ƒ์ผ" },
    { "name": "role", "type": "enum", "id": "UserRole", "desc": "์—ญํ• " },
    { "name": "is_verified", "type": "boolean", "desc": "์ธ์ฆ ์—ฌ๋ถ€", "dbDefault": "false" },
    { "name": "deleted_at", "type": "date", "nullable": true, "desc": "์‚ญ์ œ์ผ์‹œ" }
  ],
  "indexes": [
    { "type": "unique", "name": "users_email_unique", "columns": [{ "name": "email" }] }
  ],
  "subsets": {
    "A": ["id", "email", "username", "role", "created_at"],
    "P": ["id", "email", "username", "role"]
  },
  "enums": {
    "UserRole": { "normal": "์ผ๋ฐ˜", "admin": "๊ด€๋ฆฌ์ž" },
    "UserOrderBy": { "id-desc": "ID ์ตœ์‹ ์ˆœ" }
  }
}

Relation ํฌํ•จ ์˜ˆ์ œ

employee.entity.json
{
  "id": "Employee",
  "table": "employees",
  "title": "์ง์›",
  "props": [
    { "name": "id", "type": "integer", "desc": "ID" },
    { "name": "created_at", "type": "date", "desc": "๋“ฑ๋ก์ผ์‹œ", "dbDefault": "CURRENT_TIMESTAMP" },
    {
      "type": "relation",
      "name": "user",
      "with": "User",
      "desc": "์‚ฌ์šฉ์ž",
      "relationType": "OneToOne",
      "hasJoinColumn": true,
      "onDelete": "CASCADE"
    },
    {
      "type": "relation",
      "name": "department",
      "with": "Department",
      "nullable": true,
      "desc": "๋ถ€์„œ",
      "relationType": "BelongsToOne",
      "onDelete": "SET NULL"
    },
    { "name": "employee_number", "type": "string", "length": 32, "desc": "์‚ฌ๋ฒˆ" },
    { "name": "salary", "type": "numeric", "precision": 10, "scale": 2, "nullable": true, "desc": "๊ธ‰์—ฌ" },
    { "name": "hire_date", "type": "date", "nullable": true, "desc": "์ž…์‚ฌ์ผ" }
  ],
  "indexes": [
    {
      "type": "unique",
      "name": "employees_user_id_unique",
      "columns": [{ "name": "user_id" }]
    }
  ],
  "subsets": {
    "A": [
      "id",
      "created_at",
      "user.username",
      "department.name",
      "employee_number",
      "salary",
      "hire_date"
    ]
  },
  "enums": {
    "EmployeeOrderBy": { "id-desc": "ID ์ตœ์‹ ์ˆœ" }
  }
}

Sonamu UI์—์„œ Entity ์ •์˜ํ•˜๊ธฐ

  1. Sonamu UI ์ ‘์†
    # API ์„œ๋ฒ„ ์‹คํ–‰ ํ›„
    http://localhost:1028/sonamu-ui
    
  2. Entity ์ƒ์„ฑ
    • โ€œEntitiesโ€ ํƒญ ํด๋ฆญ
    • โ€œCreate Entityโ€ ๋ฒ„ํŠผ ํด๋ฆญ
    • Entity ID, ํ…Œ์ด๋ธ”๋ช…, Title ์ž…๋ ฅ
  3. ์†์„ฑ(Props) ์ถ”๊ฐ€
    • โ€œAdd Propertyโ€ ๋ฒ„ํŠผ์œผ๋กœ ์ƒˆ ์†์„ฑ ์ถ”๊ฐ€
    • ํƒ€์ž… ์„ ํƒ ๋ฐ ์˜ต์…˜ ์„ค์ •
    • ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์ˆœ์„œ ๋ณ€๊ฒฝ
  4. Subset ์ •์˜
    • โ€œSubsetsโ€ ํƒญ์—์„œ subset ํ‚ค ์ถ”๊ฐ€
    • ์ฒดํฌ๋ฐ•์Šค๋กœ ํฌํ•จํ•  ํ•„๋“œ ์„ ํƒ
    • Relation ํ•„๋“œ๋Š” ํŠธ๋ฆฌ ํ˜•ํƒœ๋กœ ํŽผ์ณ์„œ ์„ ํƒ
  5. ์ €์žฅ ๋ฐ ์ƒ์„ฑ
    • โ€œSaveโ€ ๋ฒ„ํŠผ ํด๋ฆญ
    • Entity ํŒŒ์ผ ์ž๋™ ์ƒ์„ฑ
    • Migration ์ž๋™ ์ƒ์„ฑ

๐Ÿ“ธ ํ•„์š”: Sonamu UI์—์„œ Entity ์ƒ์„ฑํ•˜๋Š” ์ „์ฒด ๊ณผ์ • (์Šคํฌ๋ฆฐ์ƒท ๋˜๋Š” GIF)

๋” ์•Œ์•„๋ณด๊ธฐ

Entity ์ •์˜ ํ›„ ์ž๋™ ์ƒ์„ฑ๋˜๋Š” ๊ฒƒ๋“ค

Entity๋ฅผ ์ •์˜ํ•˜๊ณ  ์ €์žฅํ•˜๋ฉด Sonamu๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ค์Œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:

TypeScript ํƒ€์ž…

{entity}.types.ts ํŒŒ์ผ์— ํƒ€์ž…๊ณผ Zod ์Šคํ‚ค๋งˆ ์ƒ์„ฑ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

ํ…Œ์ด๋ธ” ์ƒ์„ฑ SQL์ด ํฌํ•จ๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ƒ์„ฑ

Base ์Šคํ‚ค๋งˆ

sonamu.generated.ts์— Base ์Šคํ‚ค๋งˆ์™€ Enum ์ถ”๊ฐ€

Model Scaffold

{entity}.model.ts ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ (์„ ํƒ ์‹œ)

์ฃผ์˜์‚ฌํ•ญ

Entity ์ •์˜ ์ˆ˜์ • ์‹œ ์ฃผ์˜
  • ์ด๋ฏธ ๋ฐฐํฌ๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ํƒ€์ž…์„ ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” ์‹ ์ค‘ํ•˜๊ฒŒ ์ง„ํ–‰ํ•˜์„ธ์š”
  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ํ†ตํ•ด ๋‹จ๊ณ„์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค
  • Relation์„ ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ์„ ๊ณ ๋ คํ•˜์„ธ์š”
Entity ์„ค๊ณ„ ํŒ
  • ID๋Š” ํ•ญ์ƒ integer ํƒ€์ž…์œผ๋กœ ์‹œ์ž‘ํ•˜์„ธ์š”
  • created_at, updated_at ํ•„๋“œ๋ฅผ ํฌํ•จํ•˜์„ธ์š”
  • Unique ์ œ์•ฝ์ด ํ•„์š”ํ•œ ํ•„๋“œ๋Š” ์ธ๋ฑ์Šค๋กœ ๋ช…์‹œํ•˜์„ธ์š”
  • ์ž์ฃผ ๊ฒ€์ƒ‰๋˜๋Š” ํ•„๋“œ์—๋Š” ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”

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

Entity ์ •์˜๋ฅผ ๋งˆ์ณค๋‹ค๋ฉด, ๋‹ค์Œ ์ฃผ์ œ๋ฅผ ํ•™์Šตํ•˜์„ธ์š”: