snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
---
title: 객체 확장하기
description: 표준 Twenty 객체(Person, Company 등)에 필드를 추가합니다. 또는 `defineField`를 사용해 다른 앱의 객체에 필드를 추가합니다.
icon: wand-magic-sparkles
---
`defineField()`를 사용해 소유하지 않은 객체(예: Person이나 Company 같은 표준 Twenty 객체 또는 다른 설치된 앱에서 제공하는 객체)에 필드를 추가합니다. [`defineObject`](/l/ko/developers/extend/apps/data/objects) 안에 선언된 인라인 필드와 달리, 독립형 필드는 어느 객체를 확장할지 지정하기 위해 `objectUniversalIdentifier`가 필요합니다.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
## 핵심 요점
* `objectUniversalIdentifier`는 대상 객체를 식별합니다. 표준 Twenty 객체의 경우 `twenty-sdk`에서 상수를 가져옵니다:
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* `defineObject()`에서 필드를 **인라인으로 정의할 때는**, `objectUniversalIdentifier`가 **필요하지 않습니다** — 상위 객체에서 상속됩니다.
* `defineField()`는 `defineObject()`로 생성하지 않은 객체에 필드를 추가하는 유일한 방법입니다.
* 파일 위치는 자유롭게 정할 수 있습니다. 규칙으로는 `src/fields/\<name>.field.ts`를 사용하지만, SDK는 `src/` 어디에서든 필드를 감지합니다.
* `writability`는 필드별로도 설정할 수 있습니다. `MetadataWritability.APPLICATION`은 앱 자체의 로직 함수에서만 쓰기가 가능하도록 제한하고, `MetadataWritability.SYSTEM`은 플랫폼에서 관리하는 데이터에 예약되어 있습니다. 필드의 쓰기 가능 여부는 해당 객체의 수준보다 더 엄격하게만 설정할 수 있으며, 읽기 동작에는 영향을 주지 않습니다. 전체 의미는 [Objects](/l/ko/developers/extend/apps/data/objects)를 참조하세요.
* 표준 페이지 레이아웃(예: Task 또는 Company 상세 페이지)에 탭을 추가하려면, `twenty-sdk/define`의 `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS`와 함께 [`definePageLayoutTab`](/l/ko/developers/extend/apps/layout/page-layouts#definepagelayouttab)을(를) 사용하십시오.
## 기존 객체에 관계 추가하기
관계 필드(예: 사용자 정의 객체를 표준 `Person`에 연결)를 추가하려면, `FieldType.RELATION`과 함께 `defineField()`를 사용하세요. 패턴은 인라인 관계와 동일하지만, `objectUniversalIdentifier`를 명시적으로 설정합니다. 양방향 패턴은 [Relations](/l/ko/developers/extend/apps/data/relations)를 참조하세요.

View File

@@ -0,0 +1,143 @@
---
title: 개체
description: defineObject를 사용하여 고유한 필드를 가진 사용자 정의 테이블인 새 레코드 유형을 선언합니다.
icon: table
---
사용자 정의 **개체**는 워크스페이스에 앱이 추가하는 새로운 레코드 유형입니다. 엽서(Post Card), 송장(Invoice), 구독(Subscription) 등 도메인에 특화된 어떤 것이라도 될 수 있습니다. 각 개체는 스키마(필드, 관계, 기본값)와 동기화 및 배포 간에도 유지되는 안정적인 범용 식별자를 선언합니다.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
## 핵심 요점
* `universalIdentifier`는 배포 전반에서 고유하고 안정적이어야 합니다.
* 각 필드는 `name`, `type`, `label` 및 고유하고 안정적인 `universalIdentifier`가 필요합니다.
* `fields` 배열은 선택 사항입니다. 사용자 정의 필드 없이도 개체를 정의할 수 있습니다.
* `openRecordIn`은 이 오브젝트의 레코드를 클릭했을 때 어디에서 열릴지 설정합니다: `ObjectOpenRecordIn.USER_CHOICE`(기본값이며, 각 워크스페이스 구성원이 Settings → Experience에서 지정한 개인 설정을 따릅니다), `ObjectOpenRecordIn.SIDE_PANEL`, 또는 `ObjectOpenRecordIn.RECORD_PAGE`. 워크플로와 대시보드처럼 전체 페이지에서 사용해야 하는 레코드의 경우 `RECORD_PAGE`에 고정하고, 캘린더 이벤트처럼 빠른 패널로 볼 때만 의미가 있는 레코드의 경우 `SIDE_PANEL`에 고정하세요.
* `writability`는 역할 권한이 적용되기 전에, 해당 객체의 레코드를 누가 전반적으로 쓸 수 있는지를 제어합니다. `MetadataWritability.OPEN`(기본값 — 워크스페이스 역할이 결정함), `MetadataWritability.APPLICATION`(앱 자체의 로직 함수만 레코드를 생성, 업데이트 또는 삭제할 수 있음; 레코드가 동작을 부여하는 구성 항목과 유사한 객체에 사용하여, 광범위한 레코드 액세스를 가진 워크스페이스 구성원이 API를 통해 이를 편집하지 못하도록 함), 또는 `MetadataWritability.SYSTEM`(플랫폼이 관리하는 데이터용으로 예약됨) 중 하나입니다. 읽기 작업에는 영향을 주지 않습니다. 이는 서버 측에서 강제되며, UI 제공 요소만 숨기는 `isUIEditable`과는 다릅니다. 이는 필드 단위로도 존재하며, 이 경우 객체 수준보다 더 엄격하게만 설정할 수 있습니다.
* 여기에서 정의된 인라인 필드는 `objectUniversalIdentifier`가 **필요하지 않습니다** — 상위 개체에서 상속됩니다. 소유하지 않은 개체에 필드를 추가하려면 [`defineField()`](/l/ko/developers/extend/apps/data/extending-objects)를 사용하세요.
* `yarn twenty dev:add object`를 사용하여 새 개체를 스캐폴딩할 수 있으며, 이름, 필드, 관계 설정 과정을 안내합니다. [Architecture → Scaffolding entities](/l/ko/developers/extend/apps/getting-started/scaffolding)를 참조하세요.
<Note>
**기본 필드는 자동으로 추가됩니다.** 사용자 정의 개체를 정의하면 Twenty가 `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, `deletedAt`와 같은 표준 필드를 자동으로 생성합니다. 이 필드들은 `fields` 배열에 선언할 필요가 없습니다 — 사용자 정의 필드만 선언하면 됩니다. 동일한 이름으로 필드를 선언하여 기본 필드를 재정의할 수 있지만, 이는 거의 바람직하지 않습니다.
</Note>
## 필드 유형
`twenty-sdk/define`에서 내보낸 `FieldType` 값의 전체 집합:
| 카테고리 | 유형 |
| -------- | ------------------------------------------------------------------------------------------------------------------- |
| 텍스트 | `TEXT`, `RICH_TEXT`, `ARRAY` (문자열 배열), `RAW_JSON` |
| 숫자형 | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (임의 정밀도), `RATING`, `POSITION` |
| 날짜 | `DATE`, `DATE_TIME` |
| 선택형 | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| 복합 | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| 식별자 및 관계 | `UUID`, `RELATION`, `MORPH_RELATION` (자세한 내용은 [Relations](/l/ko/developers/extend/apps/data/relations)을 참조) |
| 시스템 | `TS_VECTOR` (서버에서 관리되는 전체 텍스트 검색 벡터) |
복합 타입은 여러 하위 필드를 저장합니다(예: `FULL_NAME` = 이름 + 성; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` 및 `MULTI_SELECT`는 위 예시와 같이 `options` 배열이 필요합니다.
## 기본값
리터럴 문자열 기본값은 문자열 **내부에서** 작은따옴표로 감싸야 합니다. 즉, `defaultValue: "'Draft'"`처럼 작성해야 하며, `defaultValue: "Draft"`처럼 작성하면 안 됩니다. 그래서 위의 `status` 필드는 `` `'${PostCardStatus.DRAFT}'` ``를 사용합니다.
따옴표로 감싸지 않은 문자열은 레코드가 생성될 때 평가되는 계산형 기본값으로 예약되어 있습니다.
* `'uuid'` — UUID를 생성합니다 (`UUID` 필드용).
* `'now'` — 현재 타임스탬프입니다 (`DATE_TIME` 필드용).
동일한 규칙이 복합 기본값의 문자열 하위 필드(예: `ACTOR` 필드의 `{ source: "'MANUAL'" }`)와 `SELECT`/`MULTI_SELECT` 값에도 적용됩니다. 따옴표로 감싸지 않은 리터럴 문자열 기본값은 앱을 빌드할 때 경고를 발생시킵니다.
## Null 허용 여부
`isNullable`는 필드가 `NULL`을 허용하는지 여부를 제어합니다. 기본값은 `true`이며, 선택적 필드의 경우 생략하면 됩니다. 데이터베이스 수준에서 필드를 필수로 만들려면 `isNullable: false`로 설정하세요.
`isNullable`에 대한 변경 사항은 기존 필드를 업데이트하는 동기화를 포함해 모든 동기화 시 적용되므로, 매니페스트를 수정하고 다시 동기화하여 필드의 Null 허용 여부를 전환할 수 있습니다.
<Note>
**기존 필드를 널 불가(non-nullable)로 변경하려면 기본값이 필요합니다.** 필드를 `isNullable: false`로 변경할 때는 널이 아닌 `defaultValue`도 함께 제공해야 합니다. 기본값은 `NOT NULL` 제약 조건이 적용되기 전에 기존의 `NULL` 행들을 모두 채웁니다. 기본값이 없으면 동기화가 실패하며 `Default value cannot be null for non-nullable fields` 오류가 발생합니다. 릴레이션 필드와 `TS_VECTOR` 필드는 항상 널 허용이므로 `isNullable` 설정이 이들 필드에는 영향을 주지 않습니다.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## 다음 단계
* **이 개체를 다른 개체와 연결** — 양방향 관계 패턴은 [Relations](/l/ko/developers/extend/apps/data/relations)를 참조하세요.
* **다른 앱의 개체에 필드 추가** — `defineField()`에 대해서는 [Extending Objects](/l/ko/developers/extend/apps/data/extending-objects)를 참조하세요.
* **이 오브젝트를 UI에 표시합니다** — 사이드바 항목을 추가하려면 [Navigation Menu Items](/l/ko/developers/extend/apps/layout/navigation-menu-items)를, 사용자 지정 목록 구성을 추가하려면 [Views](/l/ko/developers/extend/apps/layout/views)를 참조하세요.

View File

@@ -0,0 +1,101 @@
---
title: 개요
description: 워크스페이스에 앱이 추가하는 데이터(개체, 필드, 관계)의 구성을 정의하세요.
icon: database
---
Twenty 앱의 **데이터 레이어**는 워크스페이스에 앱이 *추가하는* 데이터입니다. 앱이 선언하는 새로운 레코드 타입, 기존 개체에 추가하는 열, 그리고 이러한 레코드들이 서로 연결되는 방식이 여기에 포함됩니다.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## 이 섹션에서 다루는 내용
<CardGroup cols={2}>
<Card title="개체" icon="table" href="/l/ko/developers/extend/apps/data/objects">
`defineObject` — 자체 필드를 가진 새로운 레코드 타입을 선언합니다.
</Card>
<Card title="객체 확장하기" icon="wand-magic-sparkles" href="/l/ko/developers/extend/apps/data/extending-objects">
`defineField` — 표준 개체 또는 다른 앱의 개체에 필드를 추가합니다.
</Card>
<Card title="관계" icon="diagram-project" href="/l/ko/developers/extend/apps/data/relations">
개체 간의 양방향 `MANY_TO_ONE` / `ONE_TO_MANY` 연결입니다.
</Card>
<Card title="타임라인 활동 유형" icon="clock-rotate-left" href="/l/ko/developers/extend/apps/data/timeline-activity-types">
레코드 타임라인의 자동 감사 이벤트 및 명시적 앱 이벤트를 정의합니다.
</Card>
</CardGroup>
## 엔터티 한눈에 보기
| 엔터티 | 목적 | 정의 방식 |
| -------------- | --------------------------------------------------------------------------- | ------------------------------------------ |
| **객체** | 고유한 필드를 가진 새로운 사용자 정의 레코드 타입(예: PostCard, Invoice) | `defineObject()` |
| **필드** | 객체의 열 독립형 필드를 사용하면 직접 생성하지 않은 객체도 확장할 수 있습니다(예: Company에 `loyaltyTier` 추가). | `defineField()` |
| **관계** | 두 객체 사이의 양방향 연결 — 양쪽 모두 필드로 선언됩니다. | `FieldType.RELATION`을 사용하는 `defineField()` |
| **색인** | 객체 중 하나에 대해 반복적으로 실행되는 쿼리를 빠르게 하기 위한 데이터베이스 색인 | `defineIndex()` |
| **타임라인 활동 유형** | 레코드 타임라인에 표시되는 자동 감사 이벤트 또는 명시적 도메인 이벤트 | `defineTimelineActivityType()` |
SDK는 빌드 시 AST 분석을 통해 이를 감지하므로 파일 구성은 자유롭게 정할 수 있습니다. 규칙은 `src/objects/`, `src/fields/`, `src/indexes/`, `src/timeline-activity-types/`입니다. 안정적인 `universalIdentifier` UUID가 배포 간 모든 것을 하나로 연결합니다.
## 색인(선택 사항)
앱은 반복적인 쿼리를 빠르게 유지하기 위해 객체와 함께 색인을 포함하여 배포할 수 있습니다. 가장 일반적인 사례는 자주 읽는 상태 열 또는 외래 키 열입니다.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### 고유 색인
`defineIndex`는 단일 열 및 다중 열 고유성 모두에 대해 `isUnique: true`를 허용합니다. 이것이 권장 기본 요소이며, `defineField({ isUnique: true })`는 사용이 중단(deprecated)되었고 향후 릴리스에서 제거될 예정입니다.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### 기타 제약 조건
* 부분 `WHERE` 절은 관리자 제어 하에 유지되며, 앱에서 이를 선언할 수 없습니다.
* 각 객체에는 사용자 정의 색인이 최대 10개까지 허용됩니다(프레임워크 자체 색인은 포함되지 않음).
`fields` 배열의 순서는 Postgres가 사용하는 순서대로 지정하세요. 전화번호부처럼 가장 왼쪽 열이 먼저 오도록 합니다. 색인은 공짜가 아닙니다. 테이블에 대한 모든 쓰기 작업은 색인을 갱신합니다. 해당 색인을 필요로 하는 쿼리가 있을 때에만 색인을 추가하세요.
<Note>
**Application Config** 또는 **Roles & Permissions**를 찾고 계신가요? 이러한 항목은 앱이 추가하는 데이터가 아니라 앱 자체를 설명하며, [Config](/l/ko/developers/extend/apps/config/overview) 아래에 있습니다. **Connections**(Linear, GitHub, Slack OAuth)를 찾고 계신가요? 이들은 로직 함수 *내부에서* 호출되기 위한 것이며, [Logic](/l/ko/developers/extend/apps/logic/connections) 아래에 있습니다.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: 관계
description: 양방향 MANY_TO_ONE / ONE_TO_MANY 관계로 객체들을 서로 연결합니다.
icon: diagram-project
---
관계는 두 객체를 서로 연결합니다. Twenty에서는 관계가 항상 **양방향**입니다 — 모든 관계에는 두 개의 측면이 있으며, 각 측면은 서로를 참조하는 필드로 선언됩니다.
| 관계 유형 | 설명 | 외래 키가 있나요? |
| ------------- | ------------------------------- | -------------------- |
| `MANY_TO_ONE` | 이 객체의 여러 레코드가 대상의 한 레코드를 가리킵니다 | 예 (`joinColumnName`) |
| `ONE_TO_MANY` | 이 객체의 하나의 레코드가 대상의 여러 레코드를 가집니다 | 아니오 (역방향) |
## 관계 작동 방식
모든 관계에는 서로를 참조하는 **두 개의 필드**가 필요합니다:
1. **MANY_TO_ONE** 측 — 외래 키를 보유한 객체에 존재합니다.
2. **ONE_TO_MANY** 측 — 컬렉션을 소유한 객체에 존재합니다.
두 필드 모두 `FieldType.RELATION`을 사용하며 `relationTargetFieldMetadataUniversalIdentifier`를 통해 서로를 교차 참조합니다.
## 예시: Post Card에는 여러 수신자가 있습니다
`PostCard`는 여러 `PostCardRecipient` 레코드에 전송될 수 있습니다. 각 수신자는 정확히 하나의 Post Card에 속합니다.
**1단계: PostCard에서 ONE_TO_MANY 측을 정의합니다** ("one" 측):
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**2단계: PostCardRecipient에서 MANY_TO_ONE 측을 정의합니다** ("many" 측 — 외래 키를 보유함):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**순환 임포트:** 두 관계 필드는 서로의 `universalIdentifier`를 참조합니다. 순환 임포트 문제를 피하려면, 각 파일에서 필드 ID를 명명된 상수로 export하고 다른 파일에서 import하세요. 빌드 시스템은 컴파일 시점에 이를 해결합니다.
</Note>
## 중간 관계들
중간 객체는 하나의 소스 레코드를 하나 이상의 대상 레코드에 연결합니다. 예를 들어, `PostCardRecipient`는 `postCard` 관계와 `recipient` 관계를 모두 포함할 수 있습니다. 소스 객체의 `ONE_TO_MANY` 필드에서 `junctionTargetFieldUniversalIdentifier`를 대상 객체를 가리키는 중간 객체의 관계 필드로 설정합니다:
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID`는 중간 객체의 `MANY_TO_ONE` 또는 morph 관계를 식별해야 합니다. [타임라인 활동 팬아웃](/l/ko/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events)과 같은 기능이 중간 객체를 순회할 때 어떤 관련 레코드가 대상인지 Twenty에 알려 줍니다.
## 표준 객체와의 관계 설정
내장 Twenty 객체(Person, Company 등)와 관계를 만들려면 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`를 사용하세요:
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
## 관계 필드 속성
| 속성 | 필수 | 설명 |
| ---------------------------------------------------------- | ------------------- | --------------------------------------------------------------------- |
| `type` | 예 | `FieldType.RELATION`이어야 함 |
| `relationTargetObjectMetadataUniversalIdentifier` | 예 | 대상 객체의 `universalIdentifier` |
| `relationTargetFieldMetadataUniversalIdentifier` | 예 | 대상 객체의 일치하는 필드의 `universalIdentifier` |
| `universalSettings.relationType` | 예 | `RelationType.MANY_TO_ONE` 또는 `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE에만 해당 | 참조된 레코드가 삭제될 때의 동작: `CASCADE`, `SET_NULL`, `RESTRICT`, 또는 `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE에만 해당 | 외래 키에 대한 데이터베이스 컬럼 이름(예: `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | 중간 `ONE_TO_MANY` 전용 | 팬아웃 대상을 가리키는 중간 객체의 관계 필드 |
## 인라인 관계 필드
[`defineObject`](/l/ko/developers/extend/apps/data/objects) 안에서 관계를 직접 선언할 수도 있습니다. 인라인인 경우에는 `objectUniversalIdentifier`를 생략하세요 — 상위 객체에서 상속됩니다:
```ts
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// … other fields
],
});
```

View File

@@ -0,0 +1,203 @@
---
title: 시스템 메타데이터 대상 지정
description: Twenty가 모든 객체에 자동으로 프로비저닝하는 메타데이터의 결정론적 범용 식별자를 해석하여, 앱이 하드코딩 없이 이를 참조할 수 있도록 합니다.
icon: gears
---
Twenty의 모든 객체에는 필드 집합과 열이 포함된 기본 목록 보기처럼, 직접 선언하지 않아도 되는 **시스템 메타데이터**가 함께 제공됩니다. 객체가 프로비저닝될 때 서버가 이러한 모든 요소를 생성하며, Twenty가 확장됨에 따라 해당 집합도 함께 증가합니다.
직접 이를 선언하지 않기 때문에, 가져올 수 있는 `universalIdentifier` 상수가 없습니다. 대신 서버는 각 식별자를 결정론적으로 **도출**하며, `twenty-sdk`는 동일한 도출 방식을 제공하여 매니페스트가 서버가 사용하는 정확한 값을 해석할 수 있게 합니다.
## 시스템 필드
모든 객체에 존재하는 스칼라 필드로, [`defineField()`](/l/ko/developers/extend/apps/data/extending-objects)로 선언하지 않는 필드들입니다:
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
그렇다면 [view](/l/ko/developers/extend/apps/layout/views)에서 `createdAt`을 컬럼으로 어떻게 참조할 수 있을까요?
### 문제
Twenty 2.19부터 시스템 필드의 유니버설 식별자는 애플리케이션 유니버설 식별자, 객체 유니버설 식별자, 필드 이름이라는 세 가지 입력값으로부터 서버에서 결정론적으로 도출됩니다. id를 임의로 만들고 하드코딩해도 동작하지 않습니다. 서버에서 어떤 것과도 일치하지 않으며, 동기화에서 이러한 끊어진 참조를 거부합니다:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### 해결책
<Note>
`getFieldUniversalIdentifier`는 `twenty-sdk` 2.21부터 사용할 수 있습니다.
</Note>
`getFieldUniversalIdentifier`를 사용해 서버가 사용하는 것과 정확히 동일한 값을 해석하세요. 이 함수는 세 가지 입력값을 받아 필드의 유니버설 식별자를 반환합니다:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier`는 [`defineApplication()`](/l/ko/developers/extend/apps/config/application)에 전달하는, 앱의 식별자입니다.
* `objectUniversalIdentifier`는 필드가 속한 객체의 식별자입니다.
* `name`은 시스템 필드 이름이며, 위에 나열된 값들 중 하나입니다.
### 예시: 뷰의 createdAt 컬럼
일반적인 사용 사례는 커스텀 객체 중 하나의 뷰에 `createdAt` 컬럼을 추가하는 것입니다. 필드 id를 해석한 다음, 다른 `fieldMetadataUniversalIdentifier`와 동일한 방식으로 참조하세요:
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
이렇게 해석된 동일한 id는 `fieldMetadataUniversalIdentifier`가 필요한 모든 곳에서 사용할 수 있습니다. 예를 들어 view 필드, 필터, 정렬, 그룹, 페이지 레이아웃 위젯 등입니다.
<Note>
id를 해석해서 사용하고, 하드코딩하지 마세요. 서버는 애플리케이션 id, 객체 id, 필드 이름으로부터 값을 도출하기 때문에,
`getFieldUniversalIdentifier`를 호출하면 이 입력값들이 변경되더라도 참조를 올바르게 유지할 수 있고, 도출 방식이 바뀌더라도 드리프트를 방지할 수 있습니다.
</Note>
### 시스템 관계 필드
<Note>
`getSystemRelationFieldUniversalIdentifier`는 `twenty-sdk`
2.23부터 사용 가능하며, Twenty 서버 버전 2.23 이상이 필요합니다.
</Note>
위에 언급된 스칼라 시스템 필드 외에도 서버는 모든 객체에 `timelineActivities`, `attachments`, `noteTargets`, `taskTargets`라는 네 개의 **시스템 관계 필드**를 제공하며, 각각은 해당하는 표준 관계 객체를 가리킵니다.
이 필드는 `getFieldUniversalIdentifier`로는 해결되지 않습니다. 이들의 식별자는 필드를 호스팅하는 객체와 필드가 가리키는 객체에서 **이름과 무관하게** 도출됩니다. 이 방식이면 객체의 이름을 변경해도 해당 객체의 관계 필드 식별자는 변경되지 않습니다.
이들을 해결하려면 `getSystemRelationFieldUniversalIdentifier`를 사용하세요:
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier`는 필드를 **호스팅하는** 객체입니다.
* `relationTargetObjectUniversalIdentifier`는 필드가 **가리키는** 객체입니다.
방향은 인수의 순서에 의해 인코딩됩니다. 반대쪽(예: `attachment.targetRocket`, 서버가 표준 관계 객체에 생성하는 morph 필드)을 해결하려면 두 인수의 순서를 바꾸면 됩니다:
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
스칼라 시스템 필드와 마찬가지로, 해결된 ID는 `fieldMetadataUniversalIdentifier`가 필요한 곳이면 어디서든 사용할 수 있습니다.
## 시스템 뷰
<Note>
`getSystemViewUniversalIdentifier`와 `getSystemViewFieldUniversalIdentifier`는 `twenty-sdk` 2.26부터 사용 가능하며, Twenty 서버 버전 2.26 이상이 필요합니다.
</Note>
서버는 또한 모든 객체에 대해 **시스템 뷰**를 프로비저닝합니다. 즉, 기본 목록 보기(`All {objectLabelPlural}`; 키는 `SYSTEM_VIEW_KEYS.INDEX`에서 파생된 `ViewKey.INDEX`)로, 표시 가능한 각 필드마다 하나의 열이 있습니다. 시스템 관계 필드와 마찬가지로, 이들의 식별자는 **이름에 의존하지 않고** 도출되므로, 객체나 필드의 이름을 변경해도 식별자는 절대 바뀌지 않습니다.
뷰를 해석하려면 `getSystemViewUniversalIdentifier`를 사용하세요:
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier`는 **객체**를 소유하는 애플리케이션으로, 뷰의 네임스페이스 기준이 됩니다.
* `objectUniversalIdentifier`는 뷰가 목록으로 표시하는 객체입니다.
* `viewKey`는 시스템 뷰를 구분하는 값입니다. 기본 목록 보기를 위한 `SYSTEM_VIEW_KEYS.INDEX`, 레코드 페이지 필드 위젯 뷰를 위한 `SYSTEM_VIEW_KEYS.FIELDS_WIDGET`이 있습니다. 이는 파생을 위한 키이며, 이 중 `INDEX`만 뷰 행에 함께 저장됩니다.
해석된 ID는 [`NavigationMenuItemType.VIEW`](/l/ko/developers/extend/apps/layout/navigation-menu-items) 사이드바 항목처럼, `viewUniversalIdentifier`가 필요한 모든 곳에서 사용할 수 있습니다. 객체의 기본 목록만 단순히 열고 싶다면, 파생 과정이 필요 없는 `targetObjectUniversalIdentifier`와 함께 `NavigationMenuItemType.OBJECT`를 사용하는 것이 좋습니다.
`getSystemViewFieldUniversalIdentifier`는 뷰와 해당 뷰에 표시되는 필드 정보를 기반으로 시스템 뷰의 개별 **열**을 해석합니다:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
첫 번째 인자에 주목하세요. 열은 뷰를 소유한 애플리케이션이 아니라, **표시되는 필드를 소유한** 애플리케이션을 기준으로 네임스페이스가 구분됩니다. 앱이 표준 객체에 추가한 필드는, Twenty가 소유한 뷰 위에 있지만 해당 열은 앱이 소유한 애플리케이션 네임스페이스 하에서 도출됩니다.
<Warning>
시스템 뷰와 그 열은 **서버 소유**입니다. 이들을 참조하기 위해서만 식별자를 해석해야 하며, 직접 선언하는 용도로는 사용해서는 안 됩니다. [`defineView()`](/l/ko/developers/extend/apps/layout/views)의 `key`는 더 이상 사용되지 않으며 무시되므로, 매니페스트 뷰가 `INDEX` 키를 차지할 수 없습니다. 또한 서버는 앱이 추가하는 모든 필드에 대해 이미 열을 프로비저닝하므로, 시스템 뷰에서 동일한 필드에 대해 별도의 `defineViewField()`를 선언하면 서버가 생성한 열과 충돌합니다.
</Warning>
## 표준 Twenty 객체
**표준** Twenty 객체(Person, Company, Opportunity 등)의 경우 어떤 것도 도출할 필요가 없습니다. 필드와 뷰 모두에 대해, 미리 계산된 상수 식별자를 바로 import하여 사용할 수 있습니다.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
[`defineObject()`](/l/ko/developers/extend/apps/data/objects)로 **앱에서 직접 정의한** 객체처럼, 그러한 상수가 존재하지 않는 경우에는 위에 소개한 헬퍼들을 사용하세요.
<Note>
`name`은 시스템 필드가 아니라 **기본(default)** 필드입니다. 이 필드는 자체 하드코딩된 유니버설 식별자를 유지하며,
`getFieldUniversalIdentifier`를 통해 해석되지 않습니다. 여러분이 정의한 객체에서는 `defineObject()`에서 부여한 식별자를 사용해 `name` 필드를 참조하세요.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: 타임라인 활동 유형
description: 레코드 타임라인에 표시되는 자동 감사 이벤트와 명시적 앱 이벤트를 정의합니다.
icon: clock-rotate-left
---
타임라인 활동 유형은 레코드의 타임라인에 표시되는 이벤트를 위한 안정적인 어휘를 정의합니다. 표준 객체와 앱 객체는 동일한 계약을 사용합니다. 유형에는 레이블과 아이콘이 있으며, 선택적으로 발생 시점을 선언하고 앱의 [프런트 컴포넌트](/l/ko/developers/extend/apps/layout/front-components) 중 하나를 통해 렌더링할 수 있습니다.
<Note>
타임라인 활동 유형은 베타 상태이며 Twenty 2.34에서 곧 제공됩니다. 앱 개발자의 사용 사례를 파악하는 동안
API는 변경될 수 있습니다.
</Note>
스캐폴더로 하나를 생성합니다:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
또는 직접 정의합니다:
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## 자동 및 명시적 이벤트
Twenty가 이 유형을 자동으로 생성해야 하는 경우 `emit`을 추가합니다. `emit.on`은 `created`, `updated`, `deleted`, `restored`, `linked`, `unlinked`를 지원하며, `emit.objectUniversalIdentifier`는 소스 객체를 식별합니다. 워크스페이스에서는 하나의 유효 유형만 동일한 emit 키, 즉 `on`, 객체 및 선택적 through 관계를 처리할 수 있습니다.
`emit.through`가 없으면 이벤트는 소스 레코드 자체의 타임라인에 기록됩니다. 연결된 레코드로 확산하려면 `emit.through.relationFieldUniversalIdentifier`를 소스 객체의 직접 다대일 관계 또는 [일대다 정션 관계](/l/ko/developers/extend/apps/data/relations#junction-relations)로 설정하세요. 모프 관계는 모프 그룹의 모든 멤버로 확산되므로 단일 선언으로 여러 객체 유형을 대상으로 지정할 수 있습니다.
직접 관계의 경우 소스 레코드를 생성하거나 복원하면 `linked`가 생성되고, 삭제하면 `unlinked`가 생성되며, 관계의 대상을 변경하면 이전 대상에 `unlinked`와 새 대상에 `linked`가 생성됩니다. 다른 소스 업데이트는 링크 이벤트를 생성하지 않습니다. 이는 대상이 직접 모프 관계인 첨부 파일에서 사용하는 계약입니다.
정션 관계의 경우 `universalSettings.junctionTargetFieldUniversalIdentifier`는 정션 객체에서 대상으로의 관계를 식별합니다. 정션 행을 생성하거나 삭제하면 `linked` 또는 `unlinked`가 생성됩니다. 정션 행의 어느 한쪽 대상이 변경되어도 링크 이벤트가 생성되지만, 관련 없는 정션 필드의 업데이트는 생성되지 않습니다.
`linked` 및 `unlinked` 이벤트는 구성된 직접 또는 정션 관계의 변경이 트리거이므로 `emit.through`가 필요합니다.
예를 들어, 이는 노트, 작업, 메시지, 캘린더 이벤트에서 사용되는 것과 동일한 일반 계약입니다:
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
기본적으로 모든 소스 업데이트에 대해 `updated` through 이벤트가 발생합니다. 선택한 소스 필드의 변경 사항만 대상 타임라인에 표시되어야 하는 경우 `emit.through.triggerFieldUniversalIdentifiers`를 설정합니다.
로직 함수가 직접 생성하는 명시적 도메인 이벤트에는 `emit`을 생략합니다. 이렇게 하면 동일한 작업에 대해 자동 감사 행과 명시적 행이 모두 생성되는 것을 방지할 수 있습니다.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
[`createTimelineActivity()`](/l/ko/developers/extend/apps/logic/logic-functions#create-a-timeline-activity)로 명시적 이벤트를 생성합니다. 앱 코드는 안정적인 범용 식별자를 사용하며, Twenty는 설치별 메타데이터 ID를 확인합니다.
## 사용자 지정 렌더링
프런트 컴포넌트가 없으면 Twenty는 유형 레이블, 아이콘 및 연결된 객체 메타데이터를 기반으로 네이티브 일반 행을 렌더링합니다. 이 방식은 객체별 렌더러 없이 표준 및 사용자 지정 객체에 사용할 수 있습니다.
사용자 지정 세부 정보를 위해 `frontComponentUniversalIdentifier`를 동일한 앱이 소유한 프런트 컴포넌트로 설정합니다:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
네이티브 행은 접힌 상태의 표시로 유지됩니다. Twenty는 사용자가 해당 행을 확장한 후에만 프런트 컴포넌트를 마운트하므로, 표시되는 모든 이벤트에 대해 샌드박스와 워커가 생성되는 것을 방지합니다. 컴포넌트 내부에서 `twenty-sdk/front-component`의 `useTimelineActivityId()`를 호출하여 행 ID를 읽고 표시에 필요한 데이터를 가져옵니다. 컴포넌트가 타임라인 행 외부에서 렌더링되면 `null`을 반환합니다.
## 다른 애플리케이션의 표시 재정의
앱은 자체 객체에 대한 자동 타임라인 계약을 소유합니다. 다른 앱이 소유한 객체의 이벤트를 사용자 지정하려면 `replacesTimelineActivityTypeUniversalIdentifier`로 기존 유형을 명시적으로 선언합니다:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
재정의는 명시적 전용 유형이 아닌 기존 자동 emit 슬롯을 대체하므로 `emit`을 포함해야 합니다. 참조된 유형은 대상 객체의 애플리케이션에 속해야 하며 동일한 작업과 경로를 설명해야 합니다. 재정의 앱을 제거하면 기본 유형이 복원되고, 기본 유형을 제거하면 재정의가 비활성화됩니다.
## 워크스페이스 재정의 및 음소거
워크스페이스 관리자는 애플리케이션을 포크하지 않고도 유형의 표시를 변경하거나 자동 및 명시적 이벤트를 음소거할 수 있습니다. 설치별 유형 ID와 함께 메타데이터 API의 `updateTimelineActivityType` 뮤테이션을 사용합니다:
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label`과 `icon`은 워크스페이스 재정의로 저장되므로 이후 애플리케이션 업데이트가 관리자의 선택을 덮어쓰지 않습니다. 자동 발생을 중지하고 해당 유형의 새 명시적 이벤트를 거부하려면 `isActive`를 `false`로 설정합니다. 기존 행은 계속 표시됩니다.
활성 상태를 포함한 애플리케이션 기본값을 다음으로 복원합니다:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## 구성 필드
| 필드 | 필수 | 설명 |
| ------------------------------------------------- | ------- | -------------------------------------- |
| `universalIdentifier` | 예 | 설치 및 업그레이드 전반에서 유형에 대한 안정적인 UUID |
| `name` | 예 | 앱 내 프로그래밍 방식 이름이며, 앱 내에서 고유해야 합니다 |
| `label` | 예 | 네이티브 렌더러에서 사용하는 사용자 대상 작업 텍스트 |
| `icon` | 아니요 | 이벤트 옆에 표시되는 Twenty 아이콘 이름 |
| `emit` | 아니요 | 자동 발생 선언; 명시적 전용 유형에서는 생략 |
| `emit.on` | emit 포함 | 이 유형이 기록되도록 하는 감사 작업 |
| `emit.objectUniversalIdentifier` | emit 포함 | 레코드가 이 유형을 발생시키는 객체 |
| `emit.through.relationFieldUniversalIdentifier` | 아니요 | 연결된 타임라인으로 확산하는 데 사용되는 직접 또는 정션 관계 |
| `emit.through.triggerFieldUniversalIdentifiers` | 아니요 | `updated` through 이벤트를 트리거할 수 있는 소스 필드 |
| `frontComponentUniversalIdentifier` | 아니요 | 행이 확장될 때 마운트되는 앱 소유 프런트 컴포넌트. |
| `replacesTimelineActivityTypeUniversalIdentifier` | 아니요 | 기존 emit 유형이 대체됨, `emit` 필요 |
Twenty는 이벤트가 생성될 때 의미론적 식별 정보와 지속적인 표시 대체 정보를 스냅샷으로 저장합니다. 기존 행은 원래의 작업 및 객체 의미를 유지합니다. 유형이 설치되어 있는 동안에는 현재 번역된 레이블, 아이콘 및 프런트 컴포넌트가 실시간으로 렌더링되며, 제거 후에는 스냅샷 대체 정보가 행의 가독성을 유지합니다. 확인은 범용 식별자를 사용하므로 앱을 다시 설치한 후에도 표시가 유지됩니다.